Feature/remove editorial workflow (#8)
This commit is contained in:
parent
5e6164efc4
commit
10b442428a
@ -2,7 +2,6 @@ backend:
|
||||
name: test-repo
|
||||
site_url: 'https://example.com'
|
||||
media_folder: assets/uploads
|
||||
publish_mode: editorial_workflow
|
||||
collections:
|
||||
- name: posts
|
||||
label: Posts
|
||||
|
68
index.d.ts
vendored
68
index.d.ts
vendored
@ -49,8 +49,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
|
||||
export type CmsAuthScope = 'repo' | 'public_repo';
|
||||
|
||||
export type CmsPublishMode = 'simple' | 'editorial_workflow';
|
||||
|
||||
export type CmsSlugEncoding = 'unicode' | 'ascii';
|
||||
|
||||
export interface CmsI18nConfig {
|
||||
@ -277,8 +275,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
fields: CmsField[];
|
||||
label_singular?: string;
|
||||
description?: string;
|
||||
preview_path?: string;
|
||||
preview_path_date_field?: string;
|
||||
i18n?: boolean | CmsI18nConfig;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
@ -322,8 +318,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
identifier_field?: string;
|
||||
summary?: string;
|
||||
slug?: string;
|
||||
preview_path?: string;
|
||||
preview_path_date_field?: string;
|
||||
create?: boolean;
|
||||
delete?: boolean;
|
||||
hide?: boolean;
|
||||
@ -359,8 +353,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
export interface CmsBackend {
|
||||
name: CmsBackendType;
|
||||
auth_scope?: CmsAuthScope;
|
||||
open_authoring?: boolean;
|
||||
always_fork?: boolean;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
@ -369,8 +361,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
auth_endpoint?: string;
|
||||
app_id?: string;
|
||||
auth_type?: 'implicit' | 'pkce';
|
||||
cms_label_prefix?: string;
|
||||
squash_merges?: boolean;
|
||||
proxy_url?: string;
|
||||
commit_messages?: {
|
||||
create?: string;
|
||||
@ -378,7 +368,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
delete?: string;
|
||||
uploadMedia?: string;
|
||||
deleteMedia?: string;
|
||||
openAuthoring?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -400,12 +389,10 @@ declare module '@simplecms/simple-cms-core' {
|
||||
site_url?: string;
|
||||
display_url?: string;
|
||||
logo_url?: string;
|
||||
show_preview_links?: boolean;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
media_folder_relative?: boolean;
|
||||
media_library?: CmsMediaLibrary;
|
||||
publish_mode?: CmsPublishMode;
|
||||
load_config_file?: boolean;
|
||||
integrations?: {
|
||||
hooks: string[];
|
||||
@ -515,7 +502,7 @@ declare module '@simplecms/simple-cms-core' {
|
||||
}
|
||||
|
||||
export interface CmsEventListener {
|
||||
name: 'prePublish' | 'postPublish' | 'preUnpublish' | 'postUnpublish' | 'preSave' | 'postSave';
|
||||
name: 'prePublish' | 'postPublish' | 'preSave' | 'postSave';
|
||||
handler: ({
|
||||
entry,
|
||||
author,
|
||||
@ -652,8 +639,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
newEntry?: boolean;
|
||||
commitMessage: string;
|
||||
collectionName?: string;
|
||||
useWorkflow?: boolean;
|
||||
unpublished?: boolean;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
@ -665,7 +650,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
backendName?: string;
|
||||
login?: string;
|
||||
name: string;
|
||||
useOpenAuthoring?: boolean;
|
||||
};
|
||||
|
||||
export interface ImplementationEntry {
|
||||
@ -689,26 +673,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryMediaFile {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryDiff {
|
||||
id: string;
|
||||
path: string;
|
||||
newFile: boolean;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntry {
|
||||
pullRequestAuthor?: string;
|
||||
slug: string;
|
||||
collection: string;
|
||||
status: string;
|
||||
diffs: UnpublishedEntryDiff[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type CursorStoreObject = {
|
||||
actions: Set<string>;
|
||||
data: Map<string, unknown>;
|
||||
@ -779,36 +743,6 @@ declare module '@simplecms/simple-cms-core' {
|
||||
persistMedia: (file: AssetProxy, opts: PersistOptions) => Promise<ImplementationMediaFile>;
|
||||
deleteFiles: (paths: string[], commitMessage: string) => Promise<void>;
|
||||
|
||||
unpublishedEntries: () => Promise<string[]>;
|
||||
unpublishedEntry: (args: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) => Promise<UnpublishedEntry>;
|
||||
unpublishedEntryDataFile: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
path: string,
|
||||
id: string,
|
||||
) => Promise<string>;
|
||||
unpublishedEntryMediaFile: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
path: string,
|
||||
id: string,
|
||||
) => Promise<ImplementationMediaFile>;
|
||||
updateUnpublishedEntryStatus: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
newStatus: string,
|
||||
) => Promise<void>;
|
||||
publishUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
|
||||
deleteUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
|
||||
getDeployPreview: (
|
||||
collectionName: string,
|
||||
slug: string,
|
||||
) => Promise<{ url: string; status: string } | null>;
|
||||
|
||||
allEntriesByFolder?: (
|
||||
folder: string,
|
||||
extension: string,
|
||||
|
@ -175,6 +175,7 @@
|
||||
"@types/js-base64": "3.3.1",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/lodash": "4.14.185",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/react": "17.0.50",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
|
@ -5,13 +5,11 @@ import type { Credentials, User } from '../lib/util';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { State } from '../types/redux';
|
||||
import type { t } from 'react-polyglot';
|
||||
|
||||
export const AUTH_REQUEST = 'AUTH_REQUEST';
|
||||
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
|
||||
export const AUTH_FAILURE = 'AUTH_FAILURE';
|
||||
export const AUTH_REQUEST_DONE = 'AUTH_REQUEST_DONE';
|
||||
export const USE_OPEN_AUTHORING = 'USE_OPEN_AUTHORING';
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export function authenticating() {
|
||||
@ -41,12 +39,6 @@ export function doneAuthenticating() {
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function useOpenAuthoring() {
|
||||
return {
|
||||
type: USE_OPEN_AUTHORING,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return {
|
||||
type: LOGOUT,
|
||||
@ -62,9 +54,6 @@ export function authenticateUser() {
|
||||
return Promise.resolve(backend.currentUser())
|
||||
.then(user => {
|
||||
if (user) {
|
||||
if (user.useOpenAuthoring) {
|
||||
dispatch(useOpenAuthoring());
|
||||
}
|
||||
dispatch(authenticate(user));
|
||||
} else {
|
||||
dispatch(doneAuthenticating());
|
||||
@ -86,9 +75,6 @@ export function loginUser(credentials: Credentials) {
|
||||
return backend
|
||||
.authenticate(credentials)
|
||||
.then(user => {
|
||||
if (user.useOpenAuthoring) {
|
||||
dispatch(useOpenAuthoring());
|
||||
}
|
||||
dispatch(authenticate(user));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
|
@ -4,7 +4,6 @@ import deepmerge from 'deepmerge';
|
||||
import { produce } from 'immer';
|
||||
import { trimStart, trim, isEmpty } from 'lodash';
|
||||
|
||||
import { SIMPLE as SIMPLE_PUBLISH_MODE } from '../constants/publishModes';
|
||||
import { validateConfig } from '../constants/configSchema';
|
||||
import { selectDefaultSortableFields } from '../reducers/collections';
|
||||
import { getIntegrations, selectIntegration } from '../reducers/integrations';
|
||||
@ -15,14 +14,13 @@ import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { State } from '../types/redux';
|
||||
import {
|
||||
import type {
|
||||
CmsConfig,
|
||||
CmsField,
|
||||
CmsFieldBase,
|
||||
CmsFieldObject,
|
||||
CmsFieldList,
|
||||
CmsI18nConfig,
|
||||
CmsPublishMode,
|
||||
CmsLocalBackend,
|
||||
CmsCollection,
|
||||
} from '../interface';
|
||||
@ -197,7 +195,6 @@ export function normalizeConfig(config: CmsConfig) {
|
||||
|
||||
export function applyDefaults(originalConfig: CmsConfig) {
|
||||
return produce(originalConfig, config => {
|
||||
config.publish_mode = config.publish_mode || SIMPLE_PUBLISH_MODE;
|
||||
config.slug = config.slug || {};
|
||||
config.collections = config.collections || [];
|
||||
|
||||
@ -434,14 +431,13 @@ export async function detectProxyServer(localBackend?: boolean | CmsLocalBackend
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'info' }),
|
||||
});
|
||||
const { repo, publish_modes, type } = (await res.json()) as {
|
||||
const { repo, type } = (await res.json()) as {
|
||||
repo?: string;
|
||||
publish_modes?: CmsPublishMode[];
|
||||
type?: string;
|
||||
};
|
||||
if (typeof repo === 'string' && Array.isArray(publish_modes) && typeof type === 'string') {
|
||||
if (typeof repo === 'string' && typeof type === 'string') {
|
||||
console.info(`Detected Simple CMS Proxy Server at '${proxyUrl}' with repo: '${repo}'`);
|
||||
return { proxyUrl, publish_modes, type };
|
||||
return { proxyUrl, type };
|
||||
} else {
|
||||
console.info(`Simple CMS Proxy Server not detected at '${proxyUrl}'`);
|
||||
return {};
|
||||
@ -452,27 +448,13 @@ export async function detectProxyServer(localBackend?: boolean | CmsLocalBackend
|
||||
}
|
||||
}
|
||||
|
||||
function getPublishMode(config: CmsConfig, publishModes?: CmsPublishMode[], backendType?: string) {
|
||||
if (config.publish_mode && publishModes && !publishModes.includes(config.publish_mode)) {
|
||||
const newPublishMode = publishModes[0];
|
||||
console.info(
|
||||
`'${config.publish_mode}' is not supported by '${backendType}' backend, switching to '${newPublishMode}'`,
|
||||
);
|
||||
return newPublishMode;
|
||||
}
|
||||
|
||||
return config.publish_mode;
|
||||
}
|
||||
|
||||
export async function handleLocalBackend(originalConfig: CmsConfig) {
|
||||
if (!originalConfig.local_backend) {
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
const {
|
||||
proxyUrl,
|
||||
publish_modes: publishModes,
|
||||
type: backendType,
|
||||
proxyUrl
|
||||
} = await detectProxyServer(originalConfig.local_backend);
|
||||
|
||||
if (!proxyUrl) {
|
||||
@ -482,10 +464,6 @@ export async function handleLocalBackend(originalConfig: CmsConfig) {
|
||||
return produce(originalConfig, config => {
|
||||
config.backend.name = 'proxy';
|
||||
config.backend.proxy_url = proxyUrl;
|
||||
|
||||
if (config.publish_mode) {
|
||||
config.publish_mode = getPublishMode(config, publishModes, backendType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,101 +0,0 @@
|
||||
import { currentBackend } from '../backend';
|
||||
import { selectDeployPreview } from '../reducers';
|
||||
import { addSnackbar } from '../store/slices/snackbars';
|
||||
|
||||
import type { t } from 'react-polyglot';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { Collection, Entry, State } from '../types/redux';
|
||||
|
||||
export const DEPLOY_PREVIEW_REQUEST = 'DEPLOY_PREVIEW_REQUEST';
|
||||
export const DEPLOY_PREVIEW_SUCCESS = 'DEPLOY_PREVIEW_SUCCESS';
|
||||
export const DEPLOY_PREVIEW_FAILURE = 'DEPLOY_PREVIEW_FAILURE';
|
||||
|
||||
function deployPreviewLoading(collection: string, slug: string) {
|
||||
return {
|
||||
type: DEPLOY_PREVIEW_REQUEST,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
function deployPreviewLoaded(
|
||||
collection: string,
|
||||
slug: string,
|
||||
deploy: { url: string | undefined; status: string },
|
||||
) {
|
||||
const { url, status } = deploy;
|
||||
return {
|
||||
type: DEPLOY_PREVIEW_SUCCESS,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
url,
|
||||
status,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
function deployPreviewError(collection: string, slug: string) {
|
||||
return {
|
||||
type: DEPLOY_PREVIEW_FAILURE,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a deploy preview object from the registered backend.
|
||||
*/
|
||||
export function loadDeployPreview(
|
||||
collection: Collection,
|
||||
slug: string,
|
||||
entry: Entry,
|
||||
published: boolean,
|
||||
opts?: { maxAttempts?: number; interval?: number },
|
||||
) {
|
||||
return async (dispatch: ThunkDispatch<State, undefined, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const collectionName = collection.get('name');
|
||||
|
||||
// Exit if currently fetching
|
||||
const deployState = selectDeployPreview(state, collectionName, slug);
|
||||
if (deployState && deployState.isFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(deployPreviewLoading(collectionName, slug));
|
||||
|
||||
try {
|
||||
/**
|
||||
* `getDeploy` is for published entries, while `getDeployPreview` is for
|
||||
* unpublished entries.
|
||||
*/
|
||||
const deploy = published
|
||||
? backend.getDeploy(collection, slug, entry)
|
||||
: await backend.getDeployPreview(collection, slug, entry, opts);
|
||||
if (deploy) {
|
||||
return dispatch(deployPreviewLoaded(collectionName, slug, deploy));
|
||||
}
|
||||
return dispatch(deployPreviewError(collectionName, slug));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToLoadDeployPreview', details: error.message },
|
||||
}),
|
||||
);
|
||||
dispatch(deployPreviewError(collectionName, slug));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type DeploysAction = ReturnType<
|
||||
typeof deployPreviewLoading | typeof deployPreviewLoaded | typeof deployPreviewError
|
||||
>;
|
@ -1,536 +0,0 @@
|
||||
import { List, Map } from 'immutable';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { currentBackend, slugFromCustomPath } from '../backend';
|
||||
import { EDITORIAL_WORKFLOW, status } from '../constants/publishModes';
|
||||
import ValidationErrorTypes from '../constants/validationErrorTypes';
|
||||
import { EDITORIAL_WORKFLOW_ERROR } from '../lib/util';
|
||||
import {
|
||||
selectEntry,
|
||||
selectPublishedSlugs,
|
||||
selectUnpublishedEntry,
|
||||
selectUnpublishedSlugs,
|
||||
} from '../reducers';
|
||||
import { selectEditingDraft } from '../reducers/entries';
|
||||
import { navigateToEntry } from '../routing/history';
|
||||
import { addSnackbar } from '../store/slices/snackbars';
|
||||
import { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import {
|
||||
createDraftFromEntry,
|
||||
entryDeleted,
|
||||
getMediaAssets,
|
||||
getSerializedEntry,
|
||||
loadEntries,
|
||||
loadEntry,
|
||||
} from './entries';
|
||||
import { addAssets } from './media';
|
||||
import { loadMedia } from './mediaLibrary';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { ThunkDispatch } from 'redux-thunk';
|
||||
import type { Status } from '../constants/publishModes';
|
||||
import type {
|
||||
Collection,
|
||||
Collections,
|
||||
EntryDraft,
|
||||
EntryMap,
|
||||
MediaFile,
|
||||
State,
|
||||
} from '../types/redux';
|
||||
import type { EntryValue } from '../valueObjects/Entry';
|
||||
|
||||
/*
|
||||
* Constant Declarations
|
||||
*/
|
||||
export const UNPUBLISHED_ENTRY_REQUEST = 'UNPUBLISHED_ENTRY_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_SUCCESS = 'UNPUBLISHED_ENTRY_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_REDIRECT = 'UNPUBLISHED_ENTRY_REDIRECT';
|
||||
|
||||
export const UNPUBLISHED_ENTRIES_REQUEST = 'UNPUBLISHED_ENTRIES_REQUEST';
|
||||
export const UNPUBLISHED_ENTRIES_SUCCESS = 'UNPUBLISHED_ENTRIES_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRIES_FAILURE = 'UNPUBLISHED_ENTRIES_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_REQUEST = 'UNPUBLISHED_ENTRY_PERSIST_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_SUCCESS = 'UNPUBLISHED_ENTRY_PERSIST_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_FAILURE = 'UNPUBLISHED_ENTRY_PERSIST_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_FAILURE = 'UNPUBLISHED_ENTRY_PUBLISH_FAILURE';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_DELETE_REQUEST = 'UNPUBLISHED_ENTRY_DELETE_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_DELETE_SUCCESS = 'UNPUBLISHED_ENTRY_DELETE_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_DELETE_FAILURE = 'UNPUBLISHED_ENTRY_DELETE_FAILURE';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
*/
|
||||
|
||||
function unpublishedEntryLoading(collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_REQUEST,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryLoaded(
|
||||
collection: Collection,
|
||||
entry: EntryValue & { mediaFiles: MediaFile[] },
|
||||
) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_SUCCESS,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
entry,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryRedirected(collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_REDIRECT,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesLoading() {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesLoaded(entries: EntryValue[], pagination: number) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
entries,
|
||||
pages: pagination,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesFailed(error: Error) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_FAILURE,
|
||||
error: 'Failed to load entries',
|
||||
payload: error,
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersisting(collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersisted(collection: Collection, entry: EntryMap) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
payload: {
|
||||
collection: collection.get('name'),
|
||||
entry,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersistedFail(error: Error, collection: Collection, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
collection: collection.get('name'),
|
||||
slug,
|
||||
},
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangeRequest(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangePersisted(
|
||||
collection: string,
|
||||
slug: string,
|
||||
newStatus: Status,
|
||||
) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
payload: {
|
||||
collection,
|
||||
slug,
|
||||
newStatus,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangeError(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishRequest(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublished(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishError(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryDeleteRequest(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_DELETE_REQUEST,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryDeleted(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_DELETE_SUCCESS,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryDeleteError(collection: string, slug: string) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_DELETE_FAILURE,
|
||||
payload: { collection, slug },
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported Thunk Action Creators
|
||||
*/
|
||||
|
||||
export function loadUnpublishedEntry(collection: Collection, slug: string) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
|
||||
//run possible unpublishedEntries migration
|
||||
if (!entriesLoaded) {
|
||||
try {
|
||||
const { entries, pagination } = await backend.unpublishedEntries(state.collections);
|
||||
dispatch(unpublishedEntriesLoaded(entries, pagination));
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
dispatch(unpublishedEntryLoading(collection, slug));
|
||||
|
||||
try {
|
||||
const entry = (await backend.unpublishedEntry(state, collection, slug)) as EntryValue;
|
||||
const assetProxies = await Promise.all(
|
||||
entry.mediaFiles
|
||||
.filter(file => file.draft)
|
||||
.map(({ url, file, path }) =>
|
||||
createAssetProxy({
|
||||
path,
|
||||
url,
|
||||
file,
|
||||
}),
|
||||
),
|
||||
);
|
||||
dispatch(addAssets(assetProxies));
|
||||
dispatch(unpublishedEntryLoaded(collection, entry));
|
||||
dispatch(createDraftFromEntry(entry));
|
||||
} catch (error: any) {
|
||||
if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
|
||||
dispatch(unpublishedEntryRedirected(collection, slug));
|
||||
dispatch(loadEntry(collection, slug));
|
||||
} else {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToLoadEntries', details: error },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadUnpublishedEntries(collections: Collections) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
|
||||
|
||||
if (state.config.publish_mode !== EDITORIAL_WORKFLOW || entriesLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(unpublishedEntriesLoading());
|
||||
backend
|
||||
.unpublishedEntries(collections)
|
||||
.then(response => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)))
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToLoadEntries', details: error },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntriesFailed(error));
|
||||
Promise.reject(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function persistUnpublishedEntry(collection: Collection, existingUnpublishedEntry: boolean) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const entryDraft = state.entryDraft;
|
||||
const fieldsErrors = entryDraft.get('fieldsErrors');
|
||||
const unpublishedSlugs = selectUnpublishedSlugs(state, collection.get('name'));
|
||||
const publishedSlugs = selectPublishedSlugs(state, collection.get('name'));
|
||||
const usedSlugs = publishedSlugs.concat(unpublishedSlugs) as List<string>;
|
||||
const entriesLoaded = get(state.editorialWorkflow.toJS(), 'pages.ids', false);
|
||||
|
||||
//load unpublishedEntries
|
||||
!entriesLoaded && dispatch(loadUnpublishedEntries(state.collections));
|
||||
|
||||
// Early return if draft contains validation errors
|
||||
if (!fieldsErrors.isEmpty()) {
|
||||
const hasPresenceErrors = fieldsErrors.some(errors =>
|
||||
errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE),
|
||||
);
|
||||
|
||||
if (hasPresenceErrors) {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.missingRequiredField' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const backend = currentBackend(state.config);
|
||||
const entry = entryDraft.get('entry');
|
||||
const assetProxies = getMediaAssets({
|
||||
entry,
|
||||
});
|
||||
|
||||
const serializedEntry = getSerializedEntry(collection, entry);
|
||||
const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
|
||||
|
||||
dispatch(unpublishedEntryPersisting(collection, entry.get('slug')));
|
||||
const persistAction = existingUnpublishedEntry
|
||||
? backend.persistUnpublishedEntry
|
||||
: backend.persistEntry;
|
||||
|
||||
try {
|
||||
const newSlug = await persistAction.call(backend, {
|
||||
config: state.config,
|
||||
collection,
|
||||
entryDraft: serializedEntryDraft,
|
||||
assetProxies,
|
||||
usedSlugs,
|
||||
});
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'success',
|
||||
message: { key: 'ui.toast.entrySaved' },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPersisted(collection, serializedEntry));
|
||||
|
||||
if (entry.get('slug') !== newSlug) {
|
||||
dispatch(loadUnpublishedEntry(collection, newSlug));
|
||||
navigateToEntry(collection.get('name'), newSlug);
|
||||
}
|
||||
} catch (error: any) {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToPersist', details: error },
|
||||
}),
|
||||
);
|
||||
return Promise.reject(
|
||||
dispatch(unpublishedEntryPersistedFail(error, collection, entry.get('slug'))),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUnpublishedEntryStatus(
|
||||
collection: string,
|
||||
slug: string,
|
||||
oldStatus: Status,
|
||||
newStatus: Status,
|
||||
) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
if (oldStatus === newStatus) return;
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryStatusChangeRequest(collection, slug));
|
||||
backend
|
||||
.updateUnpublishedEntryStatus(collection, slug, newStatus)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'success',
|
||||
message: { key: 'ui.toast.entryUpdated' },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryStatusChangePersisted(collection, slug, newStatus));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToUpdateStatus', details: error },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryStatusChangeError(collection, slug));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryDeleteRequest(collection, slug));
|
||||
return backend
|
||||
.deleteUnpublishedEntry(collection, slug)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'success',
|
||||
message: { key: 'ui.toast.onDeleteUnpublishedChanges' },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryDeleted(collection, slug));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onDeleteUnpublishedChanges', details: error },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryDeleteError(collection, slug));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function publishUnpublishedEntry(collectionName: string, slug: string) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const collections = state.collections;
|
||||
const backend = currentBackend(state.config);
|
||||
const entry = selectUnpublishedEntry(state, collectionName, slug);
|
||||
dispatch(unpublishedEntryPublishRequest(collectionName, slug));
|
||||
try {
|
||||
await backend.publishUnpublishedEntry(entry);
|
||||
// re-load media after entry was published
|
||||
dispatch(loadMedia());
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'success',
|
||||
message: { key: 'ui.toast.entryPublished' },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPublished(collectionName, slug));
|
||||
const collection = collections.get(collectionName);
|
||||
if (collection.has('nested')) {
|
||||
dispatch(loadEntries(collection));
|
||||
const newSlug = slugFromCustomPath(collection, entry.get('path'));
|
||||
loadEntry(collection, newSlug);
|
||||
if (slug !== newSlug && selectEditingDraft(state.entryDraft)) {
|
||||
navigateToEntry(collection.get('name'), newSlug);
|
||||
}
|
||||
} else {
|
||||
return dispatch(loadEntry(collection, slug));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToPublishEntry', details: error },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPublishError(collectionName, slug));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function unpublishPublishedEntry(collection: Collection, slug: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const entry = selectEntry(state, collection.get('name'), slug);
|
||||
const entryDraft = Map().set('entry', entry) as unknown as EntryDraft;
|
||||
dispatch(unpublishedEntryPersisting(collection, slug));
|
||||
return backend
|
||||
.deleteEntry(state, collection, slug)
|
||||
.then(() =>
|
||||
backend.persistEntry({
|
||||
config: state.config,
|
||||
collection,
|
||||
entryDraft,
|
||||
assetProxies: [],
|
||||
usedSlugs: List(),
|
||||
status: status.get('PENDING_PUBLISH'),
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(unpublishedEntryPersisted(collection, entry));
|
||||
dispatch(entryDeleted(collection, slug));
|
||||
dispatch(loadUnpublishedEntry(collection, slug));
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'success',
|
||||
message: { key: 'ui.toast.entryUnpublished' },
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
dispatch(
|
||||
addSnackbar({
|
||||
type: 'error',
|
||||
message: { key: 'ui.toast.onFailToUnpublishEntry', details: error },
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPersistedFail(error, collection, entry.get('slug')));
|
||||
});
|
||||
};
|
||||
}
|
@ -17,17 +17,17 @@ export async function waitUntilWithTimeout<T>(
|
||||
dispatch: ThunkDispatch<State, {}, AnyAction>,
|
||||
waitActionArgs: (resolve: (value?: T) => void) => WaitActionArgs,
|
||||
timeout = 30000,
|
||||
): Promise<T | null> {
|
||||
): Promise<T | null | undefined | void> {
|
||||
let waitDone = false;
|
||||
|
||||
const waitPromise = new Promise<T>(resolve => {
|
||||
const waitPromise = new Promise<T | undefined>(resolve => {
|
||||
dispatch(waitUntil(waitActionArgs(resolve)));
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise<T | null>(resolve => {
|
||||
const timeoutPromise = new Promise<T | null | void>(resolve => {
|
||||
setTimeout(() => {
|
||||
if (waitDone) {
|
||||
resolve();
|
||||
resolve(null);
|
||||
} else {
|
||||
console.warn('Wait Action timed out');
|
||||
resolve(null);
|
||||
|
362
src/backend.ts
362
src/backend.ts
@ -1,22 +1,20 @@
|
||||
import * as fuzzy from 'fuzzy';
|
||||
import { fromJS, List, Set } from 'immutable';
|
||||
import { attempt, flatten, get, isError, set, sortBy, trim, uniq } from 'lodash';
|
||||
import { attempt, flatten, get, isError, set, trim, uniq } from 'lodash';
|
||||
import { basename, dirname, extname, join } from 'path';
|
||||
|
||||
import { FILES, FOLDER } from './constants/collectionTypes';
|
||||
import { status } from './constants/publishModes';
|
||||
import { resolveFormat } from './formats/formats';
|
||||
import { commitMessageFormatter, previewUrlFormatter, slugFormatter } from './lib/formatters';
|
||||
import { commitMessageFormatter, slugFormatter } from './lib/formatters';
|
||||
import {
|
||||
formatI18nBackup,
|
||||
getFilePaths,
|
||||
getI18nBackup,
|
||||
getI18nDataFiles,
|
||||
getI18nEntry,
|
||||
getI18nFiles,
|
||||
getI18nFilesDepth,
|
||||
groupEntries,
|
||||
hasI18n,
|
||||
hasI18n
|
||||
} from './lib/i18n';
|
||||
import { getBackend, invokeEvent } from './lib/registry';
|
||||
import { sanitizeChar } from './lib/urlHelper';
|
||||
@ -25,9 +23,8 @@ import {
|
||||
blobToFileObj,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
EDITORIAL_WORKFLOW_ERROR,
|
||||
getPathDepth,
|
||||
localForage,
|
||||
localForage
|
||||
} from './lib/util';
|
||||
import { stringTemplate } from './lib/widgets';
|
||||
import {
|
||||
@ -40,36 +37,31 @@ import {
|
||||
selectFolderEntryExtension,
|
||||
selectHasMetaPath,
|
||||
selectInferedField,
|
||||
selectMediaFolders,
|
||||
selectMediaFolders
|
||||
} from './reducers/collections';
|
||||
import { selectUseWorkflow } from './reducers/config';
|
||||
import { selectEntry, selectMediaFilePath } from './reducers/entries';
|
||||
import { selectMediaFilePath } from './reducers/entries';
|
||||
import { selectCustomPath } from './reducers/entryDraft';
|
||||
import { selectIntegration } from './reducers/integrations';
|
||||
import { createEntry } from './valueObjects/Entry';
|
||||
|
||||
import type { Map } from 'immutable';
|
||||
import type { CmsConfig } from './interface';
|
||||
import type { CmsConfig, ImplementationEntry } from './interface';
|
||||
import type {
|
||||
AsyncLock,
|
||||
Credentials,
|
||||
DataFile,
|
||||
DisplayURL,
|
||||
Implementation as BackendImplementation,
|
||||
ImplementationEntry,
|
||||
UnpublishedEntry,
|
||||
UnpublishedEntryDiff,
|
||||
User,
|
||||
User
|
||||
} from './lib/util';
|
||||
import type {
|
||||
Collection,
|
||||
CollectionFile,
|
||||
Collections,
|
||||
EntryDraft,
|
||||
EntryField,
|
||||
EntryMap,
|
||||
FilterRule,
|
||||
State,
|
||||
State
|
||||
} from './types/redux';
|
||||
import type AssetProxy from './valueObjects/AssetProxy';
|
||||
import type { EntryValue } from './valueObjects/Entry';
|
||||
@ -273,14 +265,11 @@ interface PersistArgs {
|
||||
entryDraft: EntryDraft;
|
||||
assetProxies: AssetProxy[];
|
||||
usedSlugs: List<string>;
|
||||
unpublished?: boolean;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface ImplementationInitOptions {
|
||||
useWorkflow: boolean;
|
||||
updateUserCredentials: (credentials: Credentials) => void;
|
||||
initialWorkflowStatus: string;
|
||||
}
|
||||
|
||||
type Implementation = BackendImplementation & {
|
||||
@ -320,9 +309,7 @@ export class Backend {
|
||||
this.deleteAnonymousBackup();
|
||||
this.config = config;
|
||||
this.implementation = implementation.init(this.config, {
|
||||
useWorkflow: selectUseWorkflow(this.config),
|
||||
updateUserCredentials: this.updateUserCredentials,
|
||||
initialWorkflowStatus: status.first(),
|
||||
});
|
||||
this.backendName = backendName;
|
||||
this.authStore = authStore;
|
||||
@ -411,20 +398,7 @@ export class Backend {
|
||||
|
||||
getToken = () => this.implementation.getToken();
|
||||
|
||||
async entryExist(collection: Collection, path: string, slug: string, useWorkflow: boolean) {
|
||||
const unpublishedEntry =
|
||||
useWorkflow &&
|
||||
(await this.implementation
|
||||
.unpublishedEntry({ collection: collection.get('name'), slug })
|
||||
.catch(error => {
|
||||
if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}));
|
||||
|
||||
if (unpublishedEntry) return unpublishedEntry;
|
||||
|
||||
async entryExist(path: string) {
|
||||
const publishedEntry = await this.implementation
|
||||
.getEntry(path)
|
||||
.then(({ data }) => data)
|
||||
@ -455,12 +429,7 @@ export class Backend {
|
||||
// Check for duplicate slug in loaded entities store first before repo
|
||||
while (
|
||||
usedSlugs.includes(uniqueSlug) ||
|
||||
(await this.entryExist(
|
||||
collection,
|
||||
selectEntryPath(collection, uniqueSlug) as string,
|
||||
uniqueSlug,
|
||||
selectUseWorkflow(config),
|
||||
))
|
||||
(await this.entryExist(selectEntryPath(collection, uniqueSlug) as string))
|
||||
) {
|
||||
uniqueSlug = `${slug}${sanitizeChar(' ', slugConfig)}${i++}`;
|
||||
}
|
||||
@ -840,111 +809,6 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
async processUnpublishedEntry(
|
||||
collection: Collection,
|
||||
entryData: UnpublishedEntry,
|
||||
withMediaFiles: boolean,
|
||||
) {
|
||||
const { slug } = entryData;
|
||||
let extension: string;
|
||||
if (collection.get('type') === FILES) {
|
||||
const file = collection.get('files')!.find(f => f?.get('name') === slug);
|
||||
extension = extname(file.get('file'));
|
||||
} else {
|
||||
extension = selectFolderEntryExtension(collection);
|
||||
}
|
||||
|
||||
const mediaFiles: MediaFile[] = [];
|
||||
if (withMediaFiles) {
|
||||
const nonDataFiles = entryData.diffs.filter(d => !d.path.endsWith(extension));
|
||||
const files = await Promise.all(
|
||||
nonDataFiles.map(f =>
|
||||
this.implementation!.unpublishedEntryMediaFile(
|
||||
collection.get('name'),
|
||||
slug,
|
||||
f.path,
|
||||
f.id,
|
||||
),
|
||||
),
|
||||
);
|
||||
mediaFiles.push(...files.map(f => ({ ...f, draft: true })));
|
||||
}
|
||||
|
||||
const dataFiles = sortBy(
|
||||
entryData.diffs.filter(d => d.path.endsWith(extension)),
|
||||
f => f.path.length,
|
||||
);
|
||||
|
||||
const formatData = (data: string, path: string, newFile: boolean) => {
|
||||
const entry = createEntry(collection.get('name'), slug, path, {
|
||||
raw: data,
|
||||
isModification: !newFile,
|
||||
label: collection && selectFileEntryLabel(collection, slug),
|
||||
mediaFiles,
|
||||
updatedOn: entryData.updatedAt,
|
||||
author: entryData.pullRequestAuthor,
|
||||
status: entryData.status,
|
||||
meta: { path: prepareMetaPath(path, collection) },
|
||||
});
|
||||
|
||||
const entryWithFormat = this.entryWithFormat(collection)(entry);
|
||||
return entryWithFormat;
|
||||
};
|
||||
|
||||
const readAndFormatDataFile = async (dataFile: UnpublishedEntryDiff) => {
|
||||
const data = await this.implementation.unpublishedEntryDataFile(
|
||||
collection.get('name'),
|
||||
entryData.slug,
|
||||
dataFile.path,
|
||||
dataFile.id,
|
||||
);
|
||||
const entryWithFormat = formatData(data, dataFile.path, dataFile.newFile);
|
||||
return entryWithFormat;
|
||||
};
|
||||
|
||||
// if the unpublished entry has no diffs, return the original
|
||||
if (dataFiles.length <= 0) {
|
||||
const loadedEntry = await this.implementation.getEntry(
|
||||
selectEntryPath(collection, slug) as string,
|
||||
);
|
||||
return formatData(loadedEntry.data, loadedEntry.file.path, false);
|
||||
} else if (hasI18n(collection)) {
|
||||
// we need to read all locales files and not just the changes
|
||||
const path = selectEntryPath(collection, slug) as string;
|
||||
const i18nFiles = getI18nDataFiles(collection, extension, path, slug, dataFiles);
|
||||
let entries = await Promise.all(
|
||||
i18nFiles.map(dataFile => readAndFormatDataFile(dataFile).catch(() => null)),
|
||||
);
|
||||
entries = entries.filter(Boolean);
|
||||
const grouped = await groupEntries(collection, extension, entries as EntryValue[]);
|
||||
return grouped[0];
|
||||
} else {
|
||||
const entryWithFormat = await readAndFormatDataFile(dataFiles[0]);
|
||||
return entryWithFormat;
|
||||
}
|
||||
}
|
||||
|
||||
async unpublishedEntries(collections: Collections) {
|
||||
const ids = await this.implementation.unpublishedEntries!();
|
||||
const entries = (
|
||||
await Promise.all(
|
||||
ids.map(async id => {
|
||||
const entryData = await this.implementation.unpublishedEntry({ id });
|
||||
const collectionName = entryData.collection;
|
||||
const collection = collections.find(c => c.get('name') === collectionName);
|
||||
if (!collection) {
|
||||
console.warn(`Missing collection '${collectionName}' for unpublished entry '${id}'`);
|
||||
return null;
|
||||
}
|
||||
const entry = await this.processUnpublishedEntry(collection, entryData, false);
|
||||
return entry;
|
||||
}),
|
||||
)
|
||||
).filter(Boolean) as EntryValue[];
|
||||
|
||||
return { pagination: 0, entries };
|
||||
}
|
||||
|
||||
async processEntry(state: State, collection: Collection, entry: EntryValue) {
|
||||
const integration = selectIntegration(state.integrations, null, 'assetStore');
|
||||
const mediaFolders = selectMediaFolders(state.config, collection, fromJS(entry));
|
||||
@ -960,98 +824,12 @@ export class Backend {
|
||||
return entry;
|
||||
}
|
||||
|
||||
async unpublishedEntry(state: State, collection: Collection, slug: string) {
|
||||
const entryData = await this.implementation!.unpublishedEntry!({
|
||||
collection: collection.get('name') as string,
|
||||
slug,
|
||||
});
|
||||
|
||||
let entry = await this.processUnpublishedEntry(collection, entryData, true);
|
||||
entry = await this.processEntry(state, collection, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URL using `site_url` from the config and `preview_path` from the
|
||||
* entry's collection. Does not currently make a request through the backend,
|
||||
* but likely will in the future.
|
||||
*/
|
||||
getDeploy(collection: Collection, slug: string, entry: EntryMap) {
|
||||
/**
|
||||
* If `site_url` is undefined or `show_preview_links` in the config is set to false, do nothing.
|
||||
*/
|
||||
|
||||
const baseUrl = this.config.site_url;
|
||||
|
||||
if (!baseUrl || this.config.show_preview_links === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
url: previewUrlFormatter(baseUrl, collection, slug, entry, this.config.slug),
|
||||
status: 'SUCCESS',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a base URL from the backend for previewing a specific entry.
|
||||
* Supports polling via `maxAttempts` and `interval` options, as there is
|
||||
* often a delay before a preview URL is available.
|
||||
*/
|
||||
async getDeployPreview(
|
||||
collection: Collection,
|
||||
slug: string,
|
||||
entry: EntryMap,
|
||||
{ maxAttempts = 1, interval = 5000 } = {},
|
||||
) {
|
||||
/**
|
||||
* If the registered backend does not provide a `getDeployPreview` method, or
|
||||
* `show_preview_links` in the config is set to false, do nothing.
|
||||
*/
|
||||
if (!this.implementation.getDeployPreview || this.config.show_preview_links === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for the deploy preview URL (defaults to 1 attempt, so no polling by
|
||||
* default).
|
||||
*/
|
||||
let deployPreview,
|
||||
count = 0;
|
||||
while (!deployPreview && count < maxAttempts) {
|
||||
count++;
|
||||
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
|
||||
if (!deployPreview) {
|
||||
await new Promise(resolve => setTimeout(() => resolve(undefined), interval));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's no deploy preview, do nothing.
|
||||
*/
|
||||
if (!deployPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Create a URL using the collection `preview_path`, if provided.
|
||||
*/
|
||||
url: previewUrlFormatter(deployPreview.url, collection, slug, entry, this.config.slug),
|
||||
/**
|
||||
* Always capitalize the status for consistency.
|
||||
*/
|
||||
status: deployPreview.status ? deployPreview.status.toUpperCase() : '',
|
||||
};
|
||||
}
|
||||
|
||||
async persistEntry({
|
||||
config,
|
||||
collection,
|
||||
entryDraft: draft,
|
||||
assetProxies,
|
||||
usedSlugs,
|
||||
unpublished = false,
|
||||
status,
|
||||
}: PersistArgs) {
|
||||
const modifiedData = await this.invokePreSaveEvent(draft.get('entry'));
|
||||
@ -1059,8 +837,6 @@ export class Backend {
|
||||
|
||||
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
||||
|
||||
const useWorkflow = selectUseWorkflow(config);
|
||||
|
||||
const customPath = selectCustomPath(collection, entryDraft);
|
||||
|
||||
let dataFile: DataFile;
|
||||
@ -1087,8 +863,7 @@ export class Backend {
|
||||
const slug = entryDraft.getIn(['entry', 'slug']);
|
||||
dataFile = {
|
||||
path: entryDraft.getIn(['entry', 'path']),
|
||||
// for workflow entries we refresh the slug on publish
|
||||
slug: customPath && !useWorkflow ? slugFromCustomPath(collection, customPath) : slug,
|
||||
slug: customPath ? slugFromCustomPath(collection, customPath) : slug,
|
||||
raw: this.entryToRaw(collection, entryDraft.get('entry')),
|
||||
newPath: customPath,
|
||||
};
|
||||
@ -1111,33 +886,25 @@ export class Backend {
|
||||
}
|
||||
|
||||
const user = (await this.currentUser()) as User;
|
||||
const commitMessage = commitMessageFormatter(
|
||||
newEntry ? 'create' : 'update',
|
||||
config,
|
||||
{
|
||||
collection,
|
||||
slug,
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
const commitMessage = commitMessageFormatter(newEntry ? 'create' : 'update', config, {
|
||||
collection,
|
||||
slug,
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
});
|
||||
|
||||
const collectionName = collection.get('name');
|
||||
|
||||
const updatedOptions = { unpublished, status };
|
||||
const updatedOptions = { status };
|
||||
const opts = {
|
||||
newEntry,
|
||||
commitMessage,
|
||||
collectionName,
|
||||
useWorkflow,
|
||||
...updatedOptions,
|
||||
};
|
||||
|
||||
if (!useWorkflow) {
|
||||
await this.invokePrePublishEvent(entryDraft.get('entry'));
|
||||
}
|
||||
await this.invokePrePublishEvent(entryDraft.get('entry'));
|
||||
|
||||
await this.implementation.persistEntry(
|
||||
{
|
||||
@ -1148,10 +915,7 @@ export class Backend {
|
||||
);
|
||||
|
||||
await this.invokePostSaveEvent(entryDraft.get('entry'));
|
||||
|
||||
if (!useWorkflow) {
|
||||
await this.invokePostPublishEvent(entryDraft.get('entry'));
|
||||
}
|
||||
await this.invokePostPublishEvent(entryDraft.get('entry'));
|
||||
|
||||
return slug;
|
||||
}
|
||||
@ -1169,14 +933,6 @@ export class Backend {
|
||||
await this.invokeEventWithEntry('postPublish', entry);
|
||||
}
|
||||
|
||||
async invokePreUnpublishEvent(entry: EntryMap) {
|
||||
await this.invokeEventWithEntry('preUnpublish', entry);
|
||||
}
|
||||
|
||||
async invokePostUnpublishEvent(entry: EntryMap) {
|
||||
await this.invokeEventWithEntry('postUnpublish', entry);
|
||||
}
|
||||
|
||||
async invokePreSaveEvent(entry: EntryMap) {
|
||||
return await this.invokeEventWithEntry('preSave', entry);
|
||||
}
|
||||
@ -1188,16 +944,11 @@ export class Backend {
|
||||
async persistMedia(config: CmsConfig, file: AssetProxy) {
|
||||
const user = (await this.currentUser()) as User;
|
||||
const options = {
|
||||
commitMessage: commitMessageFormatter(
|
||||
'uploadMedia',
|
||||
config,
|
||||
{
|
||||
path: file.path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
),
|
||||
commitMessage: commitMessageFormatter('uploadMedia', config, {
|
||||
path: file.path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
}),
|
||||
};
|
||||
return this.implementation.persistMedia(file, options);
|
||||
}
|
||||
@ -1212,66 +963,31 @@ export class Backend {
|
||||
}
|
||||
|
||||
const user = (await this.currentUser()) as User;
|
||||
const commitMessage = commitMessageFormatter(
|
||||
'delete',
|
||||
config,
|
||||
{
|
||||
collection,
|
||||
slug,
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
const commitMessage = commitMessageFormatter('delete', config, {
|
||||
collection,
|
||||
slug,
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
});
|
||||
|
||||
const entry = selectEntry(state.entries, collection.get('name'), slug);
|
||||
await this.invokePreUnpublishEvent(entry);
|
||||
let paths = [path];
|
||||
if (hasI18n(collection)) {
|
||||
paths = getFilePaths(collection, extension, path, slug);
|
||||
}
|
||||
await this.implementation.deleteFiles(paths, commitMessage);
|
||||
|
||||
await this.invokePostUnpublishEvent(entry);
|
||||
}
|
||||
|
||||
async deleteMedia(config: CmsConfig, path: string) {
|
||||
const user = (await this.currentUser()) as User;
|
||||
const commitMessage = commitMessageFormatter(
|
||||
'deleteMedia',
|
||||
config,
|
||||
{
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
const commitMessage = commitMessageFormatter('deleteMedia', config, {
|
||||
path,
|
||||
authorLogin: user.login,
|
||||
authorName: user.name,
|
||||
});
|
||||
return this.implementation.deleteFiles([path], commitMessage);
|
||||
}
|
||||
|
||||
persistUnpublishedEntry(args: PersistArgs) {
|
||||
return this.persistEntry({ ...args, unpublished: true });
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(entry: EntryMap) {
|
||||
const collection = entry.get('collection');
|
||||
const slug = entry.get('slug');
|
||||
|
||||
await this.invokePrePublishEvent(entry);
|
||||
await this.implementation.publishUnpublishedEntry!(collection, slug);
|
||||
await this.invokePostPublishEvent(entry);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.implementation.deleteUnpublishedEntry!(collection, slug);
|
||||
}
|
||||
|
||||
entryToRaw(collection: Collection, entry: EntryMap): string {
|
||||
const format = resolveFormat(collection, entry.toJS());
|
||||
const fieldsOrder = this.fieldsOrder(collection, entry);
|
||||
|
@ -1,29 +1,14 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import { partial, result, trim, trimStart } from 'lodash';
|
||||
import { dirname, basename } from 'path';
|
||||
import { basename, dirname } from 'path';
|
||||
|
||||
import {
|
||||
localForage,
|
||||
APIError,
|
||||
unsentRequest,
|
||||
requestWithBackoff,
|
||||
responseParser,
|
||||
readFile,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
generateContentKey,
|
||||
parseContentKey,
|
||||
labelToStatus,
|
||||
isCMSLabel,
|
||||
EditorialWorkflowError,
|
||||
statusToLabel,
|
||||
PreviewState,
|
||||
readFileMetadata,
|
||||
branchFromContentKey,
|
||||
APIError, localForage, readFile, readFileMetadata, requestWithBackoff,
|
||||
responseParser, unsentRequest
|
||||
} from '../../lib/util';
|
||||
|
||||
import type { ApiRequest, AssetProxy, PersistOptions, DataFile } from '../../lib/util';
|
||||
import type { Map } from 'immutable';
|
||||
import type { ApiRequest, AssetProxy, DataFile, PersistOptions } from '../../lib/util';
|
||||
|
||||
export const API_NAME = 'Azure DevOps';
|
||||
|
||||
@ -43,32 +28,6 @@ type AzureGitItem = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/get%20pull%20request?view=azure-devops-rest-6.1#gitpullrequest
|
||||
type AzureWebApiTagDefinition = {
|
||||
active: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type AzurePullRequest = {
|
||||
title: string;
|
||||
artifactId: string;
|
||||
closedDate: string;
|
||||
creationDate: string;
|
||||
isDraft: string;
|
||||
status: AzurePullRequestStatus;
|
||||
lastMergeSourceCommit: AzureGitChangeItem;
|
||||
mergeStatus: AzureAsyncPullRequestStatus;
|
||||
pullRequestId: number;
|
||||
labels: AzureWebApiTagDefinition[];
|
||||
sourceRefName: string;
|
||||
createdBy?: {
|
||||
displayName?: string;
|
||||
uniqueName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AzurePullRequestCommit = { commitId: string };
|
||||
|
||||
enum AzureCommitStatusState {
|
||||
@ -104,20 +63,6 @@ enum AzureItemContentType {
|
||||
BASE64 = 'base64encoded',
|
||||
}
|
||||
|
||||
enum AzurePullRequestStatus {
|
||||
ACTIVE = 'active',
|
||||
COMPLETED = 'completed',
|
||||
ABANDONED = 'abandoned',
|
||||
}
|
||||
|
||||
enum AzureAsyncPullRequestStatus {
|
||||
CONFLICTS = 'conflicts',
|
||||
FAILURE = 'failure',
|
||||
QUEUED = 'queued',
|
||||
REJECTED = 'rejectedByPolicy',
|
||||
SUCCEEDED = 'succeeded',
|
||||
}
|
||||
|
||||
enum AzureObjectType {
|
||||
BLOB = 'blob',
|
||||
TREE = 'tree',
|
||||
@ -212,9 +157,6 @@ interface AzureApiConfig {
|
||||
apiRoot: string;
|
||||
repo: { org: string; project: string; repoName: string };
|
||||
branch: string;
|
||||
squashMerges: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
@ -222,10 +164,7 @@ export default class API {
|
||||
apiVersion: string;
|
||||
token: string;
|
||||
branch: string;
|
||||
mergeStrategy: string;
|
||||
endpointUrl: string;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
|
||||
constructor(config: AzureApiConfig, token: string) {
|
||||
const { repo } = config;
|
||||
@ -233,10 +172,7 @@ export default class API {
|
||||
this.endpointUrl = `${apiRoot}/${repo.org}/${repo.project}/_apis/git/repositories/${repo.repoName}`;
|
||||
this.token = token;
|
||||
this.branch = config.branch;
|
||||
this.mergeStrategy = config.squashMerges ? 'squash' : 'noFastForward';
|
||||
this.initialWorkflowStatus = config.initialWorkflowStatus;
|
||||
this.apiVersion = config.apiVersion;
|
||||
this.cmsLabelPrefix = config.cmsLabelPrefix;
|
||||
}
|
||||
|
||||
withHeaders = (req: ApiRequest) => {
|
||||
@ -400,22 +336,6 @@ export default class API {
|
||||
return refs.find(b => b.name == this.branchToRef(branch))!;
|
||||
}
|
||||
|
||||
async deleteRef(ref: AzureRef): Promise<void> {
|
||||
const deleteBranchPayload = [
|
||||
{
|
||||
name: ref.name,
|
||||
oldObjectId: ref.objectId,
|
||||
newObjectId: '0000000000000000000000000000000000000000',
|
||||
},
|
||||
];
|
||||
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.endpointUrl}/refs`,
|
||||
body: JSON.stringify(deleteBranchPayload),
|
||||
});
|
||||
}
|
||||
|
||||
async uploadAndCommit(
|
||||
items: AzureCommitItem[],
|
||||
comment: string,
|
||||
@ -445,62 +365,6 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveUnpublishedEntryData(contentKey: string) {
|
||||
const { collection, slug } = parseContentKey(contentKey);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const diffs = await this.getDifferences(pullRequest.sourceRefName);
|
||||
const diffsWithIds = await Promise.all(
|
||||
diffs.map(async d => {
|
||||
const path = trimStart(d.item.path, '/');
|
||||
const newFile = d.changeType === AzureCommitChangeType.ADD;
|
||||
const id = d.item.objectId;
|
||||
return { id, path, newFile };
|
||||
}),
|
||||
);
|
||||
const label = pullRequest.labels.find(l => isCMSLabel(l.name, this.cmsLabelPrefix));
|
||||
const labelName = label && label.name ? label.name : this.cmsLabelPrefix;
|
||||
const status = labelToStatus(labelName, this.cmsLabelPrefix);
|
||||
// Uses creationDate, as we do not have direct access to the updated date
|
||||
const updatedAt = pullRequest.closedDate ? pullRequest.closedDate : pullRequest.creationDate;
|
||||
const pullRequestAuthor =
|
||||
pullRequest.createdBy?.displayName || pullRequest.createdBy?.uniqueName;
|
||||
return {
|
||||
collection,
|
||||
slug,
|
||||
status,
|
||||
diffs: diffsWithIds,
|
||||
updatedAt,
|
||||
pullRequestAuthor,
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestStatues(pullRequest: AzurePullRequest) {
|
||||
const { value: commits } = await this.requestJSON<AzureArray<AzurePullRequestCommit>>({
|
||||
url: `${this.endpointUrl}/pullrequests/${pullRequest.pullRequestId}/commits`,
|
||||
params: {
|
||||
$top: 1,
|
||||
},
|
||||
});
|
||||
const { value: statuses } = await this.requestJSON<AzureArray<AzureCommitStatus>>({
|
||||
url: `${this.endpointUrl}/commits/${commits[0].commitId}/statuses`,
|
||||
params: { latestOnly: true },
|
||||
});
|
||||
return statuses;
|
||||
}
|
||||
|
||||
async getStatuses(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const statuses = await this.getPullRequestStatues(pullRequest);
|
||||
return statuses.map(({ context, state, targetUrl }) => ({
|
||||
context: context.name,
|
||||
state: state === AzureCommitStatusState.SUCCEEDED ? PreviewState.Success : PreviewState.Other,
|
||||
target_url: targetUrl,
|
||||
}));
|
||||
}
|
||||
|
||||
async getCommitItems(files: { path: string; newPath?: string }[], branch: string) {
|
||||
const items = await Promise.all(
|
||||
files.map(async file => {
|
||||
@ -545,14 +409,9 @@ export default class API {
|
||||
|
||||
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
|
||||
const files = [...dataFiles, ...mediaFiles];
|
||||
if (options.useWorkflow) {
|
||||
const slug = dataFiles[0].slug;
|
||||
return this.editorialWorkflowGit(files, slug, options);
|
||||
} else {
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
|
||||
return this.uploadAndCommit(items, options.commitMessage, this.branch, true);
|
||||
}
|
||||
return this.uploadAndCommit(items, options.commitMessage, this.branch, true);
|
||||
}
|
||||
|
||||
async deleteFiles(paths: string[], comment: string) {
|
||||
@ -578,29 +437,6 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
async getPullRequests(sourceBranch?: string) {
|
||||
const { value: pullRequests } = await this.requestJSON<AzureArray<AzurePullRequest>>({
|
||||
url: `${this.endpointUrl}/pullrequests`,
|
||||
params: {
|
||||
'searchCriteria.status': 'active',
|
||||
'searchCriteria.targetRefName': this.branchToRef(this.branch),
|
||||
'searchCriteria.includeLinks': false,
|
||||
...(sourceBranch ? { 'searchCriteria.sourceRefName': this.branchToRef(sourceBranch) } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const filtered = pullRequests.filter(pr => {
|
||||
return pr.labels.some(label => isCMSLabel(label.name, this.cmsLabelPrefix));
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async listUnpublishedBranches(): Promise<string[]> {
|
||||
const pullRequests = await this.getPullRequests();
|
||||
const branches = pullRequests.map(pr => this.refToBranch(pr.sourceRefName));
|
||||
return branches;
|
||||
}
|
||||
|
||||
async isFileExists(path: string, branch: string) {
|
||||
try {
|
||||
await this.requestText({
|
||||
@ -616,179 +452,4 @@ export default class API {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createPullRequest(branch: string, commitMessage: string, status: string) {
|
||||
const pr = {
|
||||
sourceRefName: this.branchToRef(branch),
|
||||
targetRefName: this.branchToRef(this.branch),
|
||||
title: commitMessage,
|
||||
description: DEFAULT_PR_BODY,
|
||||
labels: [
|
||||
{
|
||||
name: statusToLabel(status, this.cmsLabelPrefix),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.endpointUrl}/pullrequests`,
|
||||
params: {
|
||||
supportsIterations: false,
|
||||
},
|
||||
body: JSON.stringify(pr),
|
||||
});
|
||||
}
|
||||
|
||||
async getBranchPullRequest(branch: string) {
|
||||
const pullRequests = await this.getPullRequests(branch);
|
||||
|
||||
if (pullRequests.length <= 0) {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
|
||||
return pullRequests[0];
|
||||
}
|
||||
|
||||
async getDifferences(to: string) {
|
||||
const result = await this.requestJSON<AzureGitCommitDiffs>({
|
||||
url: `${this.endpointUrl}/diffs/commits`,
|
||||
params: {
|
||||
baseVersion: this.branch,
|
||||
targetVersion: this.refToBranch(to),
|
||||
},
|
||||
});
|
||||
|
||||
return result.changes.filter(
|
||||
d =>
|
||||
d.item.gitObjectType === AzureObjectType.BLOB &&
|
||||
Object.values(AzureCommitChangeType).includes(d.changeType),
|
||||
);
|
||||
}
|
||||
|
||||
async editorialWorkflowGit(
|
||||
files: (DataFile | AssetProxy)[],
|
||||
slug: string,
|
||||
options: PersistOptions,
|
||||
) {
|
||||
const contentKey = generateContentKey(options.collectionName as string, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const unpublished = options.unpublished || false;
|
||||
|
||||
if (!unpublished) {
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
|
||||
await this.uploadAndCommit(items, options.commitMessage, branch, true);
|
||||
await this.createPullRequest(
|
||||
branch,
|
||||
options.commitMessage,
|
||||
options.status || this.initialWorkflowStatus,
|
||||
);
|
||||
} else {
|
||||
const items = await this.getCommitItems(files, branch);
|
||||
await this.uploadAndCommit(items, options.commitMessage, branch, false);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
|
||||
const nonCmsLabels = pullRequest.labels
|
||||
.filter(label => !isCMSLabel(label.name, this.cmsLabelPrefix))
|
||||
.map(label => label.name);
|
||||
|
||||
const labels = [...nonCmsLabels, statusToLabel(newStatus, this.cmsLabelPrefix)];
|
||||
await this.updatePullRequestLabels(pullRequest, labels);
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
await this.abandonPullRequest(pullRequest);
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
await this.completePullRequest(pullRequest);
|
||||
}
|
||||
|
||||
async updatePullRequestLabels(pullRequest: AzurePullRequest, labels: string[]) {
|
||||
const cmsLabels = pullRequest.labels.filter(l => isCMSLabel(l.name, this.cmsLabelPrefix));
|
||||
await Promise.all(
|
||||
cmsLabels.map(l => {
|
||||
return this.requestText({
|
||||
method: 'DELETE',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(
|
||||
pullRequest.pullRequestId,
|
||||
)}/labels/${encodeURIComponent(l.id)}`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
labels.map(l => {
|
||||
return this.requestText({
|
||||
method: 'POST',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(
|
||||
pullRequest.pullRequestId,
|
||||
)}/labels`,
|
||||
body: JSON.stringify({ name: l }),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async completePullRequest(pullRequest: AzurePullRequest) {
|
||||
const pullRequestCompletion = {
|
||||
status: AzurePullRequestStatus.COMPLETED,
|
||||
lastMergeSourceCommit: pullRequest.lastMergeSourceCommit,
|
||||
completionOptions: {
|
||||
deleteSourceBranch: true,
|
||||
mergeCommitMessage: MERGE_COMMIT_MESSAGE,
|
||||
mergeStrategy: this.mergeStrategy,
|
||||
},
|
||||
};
|
||||
|
||||
let response = await this.requestJSON<AzurePullRequest>({
|
||||
method: 'PATCH',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
|
||||
body: JSON.stringify(pullRequestCompletion),
|
||||
});
|
||||
|
||||
// We need to wait for Azure to complete the pull request to actually complete
|
||||
// Sometimes this is instant, but frequently it is 1-3 seconds
|
||||
const DELAY_MILLISECONDS = 500;
|
||||
const MAX_ATTEMPTS = 10;
|
||||
let attempt = 1;
|
||||
while (response.mergeStatus === AzureAsyncPullRequestStatus.QUEUED && attempt <= MAX_ATTEMPTS) {
|
||||
await delay(DELAY_MILLISECONDS);
|
||||
response = await this.requestJSON({
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
|
||||
});
|
||||
attempt = attempt + 1;
|
||||
}
|
||||
}
|
||||
|
||||
async abandonPullRequest(pullRequest: AzurePullRequest) {
|
||||
const pullRequestAbandon = {
|
||||
status: AzurePullRequestStatus.ABANDONED,
|
||||
};
|
||||
|
||||
await this.requestJSON({
|
||||
method: 'PATCH',
|
||||
url: `${this.endpointUrl}/pullrequests/${encodeURIComponent(pullRequest.pullRequestId)}`,
|
||||
body: JSON.stringify(pullRequestAbandon),
|
||||
});
|
||||
|
||||
await this.deleteRef({
|
||||
name: pullRequest.sourceRefName,
|
||||
objectId: pullRequest.lastMergeSourceCommit.commitId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,18 @@
|
||||
import { trimStart, trim } from 'lodash';
|
||||
import { trim, trimStart } from 'lodash';
|
||||
import semaphore from 'semaphore';
|
||||
|
||||
import {
|
||||
basename,
|
||||
getMediaDisplayURL,
|
||||
generateContentKey,
|
||||
getMediaAsBlob,
|
||||
getPreviewStatus,
|
||||
asyncLock,
|
||||
runWithLock,
|
||||
unpublishedEntries,
|
||||
entriesByFiles,
|
||||
filterByExtension,
|
||||
branchFromContentKey,
|
||||
entriesByFolder,
|
||||
contentKeyFromBranch,
|
||||
getBlobSHA,
|
||||
asyncLock, basename, entriesByFiles, entriesByFolder, filterByExtension, getBlobSHA, getMediaAsBlob, getMediaDisplayURL
|
||||
} from '../../lib/util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type {
|
||||
Credentials,
|
||||
Implementation,
|
||||
AssetProxy, AsyncLock, Config, Credentials, DisplayURL,
|
||||
Entry, Implementation,
|
||||
ImplementationFile,
|
||||
ImplementationMediaFile,
|
||||
DisplayURL,
|
||||
Entry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
Config,
|
||||
AsyncLock,
|
||||
User,
|
||||
UnpublishedEntryMediaFile,
|
||||
ImplementationMediaFile, PersistOptions, User
|
||||
} from '../../lib/util';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
@ -61,9 +40,7 @@ function parseAzureRepo(config: Config) {
|
||||
export default class Azure implements Implementation {
|
||||
lock: AsyncLock;
|
||||
api?: API;
|
||||
options: {
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
options: {};
|
||||
repo: {
|
||||
org: string;
|
||||
project: string;
|
||||
@ -73,16 +50,12 @@ export default class Azure implements Implementation {
|
||||
apiRoot: string;
|
||||
apiVersion: string;
|
||||
token: string | null;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
|
||||
_mediaDisplayURLSem?: Semaphore;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
@ -91,10 +64,7 @@ export default class Azure implements Implementation {
|
||||
this.apiRoot = config.backend.api_root || 'https://dev.azure.com';
|
||||
this.apiVersion = config.backend.api_version || '6.1-preview';
|
||||
this.token = '';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.mediaFolder = trim(config.media_folder, '/');
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
@ -130,9 +100,6 @@ export default class Azure implements Implementation {
|
||||
apiVersion: this.apiVersion,
|
||||
repo: this.repo,
|
||||
branch: this.branch,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
},
|
||||
this.token,
|
||||
);
|
||||
@ -262,122 +229,4 @@ export default class Azure implements Implementation {
|
||||
async deleteFiles(paths: string[], commitMessage: string) {
|
||||
await this.api!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
|
||||
async loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
const blob = await getMediaAsBlob(file.path, null, readFile);
|
||||
const name = basename(file.path);
|
||||
const fileObj = new File([blob], name);
|
||||
return {
|
||||
id: file.path,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
}
|
||||
|
||||
async loadEntryMediaFiles(branch: string, files: UnpublishedEntryMediaFile[]) {
|
||||
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
|
||||
|
||||
return mediaFiles;
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(contentKey);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error('Missing unpublished entry id or collection and slug');
|
||||
}
|
||||
}
|
||||
|
||||
getBranch(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
return branch;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
||||
return data;
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
try {
|
||||
const statuses = await this.api!.getStatuses(collection, slug);
|
||||
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
||||
|
||||
if (deployStatus) {
|
||||
const { target_url: url, state } = deployStatus;
|
||||
return { url, status: state };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,14 @@
|
||||
import { flow, get } from 'lodash';
|
||||
import { dirname } from 'path';
|
||||
import { oneLine } from 'common-tags';
|
||||
import { parse } from 'what-the-diff';
|
||||
|
||||
import {
|
||||
localForage,
|
||||
unsentRequest,
|
||||
responseParser,
|
||||
then,
|
||||
basename,
|
||||
Cursor,
|
||||
APIError,
|
||||
readFile,
|
||||
CMS_BRANCH_PREFIX,
|
||||
generateContentKey,
|
||||
labelToStatus,
|
||||
isCMSLabel,
|
||||
EditorialWorkflowError,
|
||||
statusToLabel,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
PreviewState,
|
||||
parseContentKey,
|
||||
branchFromContentKey,
|
||||
requestWithBackoff,
|
||||
readFileMetadata,
|
||||
throwOnConflictingBranches,
|
||||
APIError, basename,
|
||||
Cursor, localForage, readFile, readFileMetadata, requestWithBackoff, responseParser,
|
||||
then, throwOnConflictingBranches, unsentRequest
|
||||
} from '../../lib/util';
|
||||
|
||||
import type {
|
||||
ApiRequest,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
FetchError,
|
||||
DataFile,
|
||||
} from '../../lib/util';
|
||||
import type { ApiRequest, AssetProxy, DataFile, FetchError, PersistOptions } from '../../lib/util';
|
||||
|
||||
interface Config {
|
||||
apiRoot?: string;
|
||||
@ -43,9 +17,6 @@ interface Config {
|
||||
repo?: string;
|
||||
requestFunction?: (req: ApiRequest) => Promise<Response>;
|
||||
hasWriteAccess?: () => Promise<boolean>;
|
||||
squashMerges: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
}
|
||||
|
||||
interface CommitAuthor {
|
||||
@ -53,96 +24,6 @@ interface CommitAuthor {
|
||||
email: string;
|
||||
}
|
||||
|
||||
enum BitBucketPullRequestState {
|
||||
MERGED = 'MERGED',
|
||||
SUPERSEDED = 'SUPERSEDED',
|
||||
OPEN = 'OPEN',
|
||||
DECLINED = 'DECLINED',
|
||||
}
|
||||
|
||||
type BitBucketPullRequest = {
|
||||
description: string;
|
||||
id: number;
|
||||
title: string;
|
||||
state: BitBucketPullRequestState;
|
||||
updated_on: string;
|
||||
summary: {
|
||||
raw: string;
|
||||
};
|
||||
source: {
|
||||
commit: {
|
||||
hash: string;
|
||||
};
|
||||
branch: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
destination: {
|
||||
commit: {
|
||||
hash: string;
|
||||
};
|
||||
branch: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
author: BitBucketUser;
|
||||
};
|
||||
|
||||
type BitBucketPullRequests = {
|
||||
size: number;
|
||||
page: number;
|
||||
pagelen: number;
|
||||
next: string;
|
||||
preview: string;
|
||||
values: BitBucketPullRequest[];
|
||||
};
|
||||
|
||||
type BitBucketPullComment = {
|
||||
content: {
|
||||
raw: string;
|
||||
};
|
||||
};
|
||||
|
||||
type BitBucketPullComments = {
|
||||
size: number;
|
||||
page: number;
|
||||
pagelen: number;
|
||||
next: string;
|
||||
preview: string;
|
||||
values: BitBucketPullComment[];
|
||||
};
|
||||
|
||||
enum BitBucketPullRequestStatusState {
|
||||
Successful = 'SUCCESSFUL',
|
||||
Failed = 'FAILED',
|
||||
InProgress = 'INPROGRESS',
|
||||
Stopped = 'STOPPED',
|
||||
}
|
||||
|
||||
type BitBucketPullRequestStatus = {
|
||||
uuid: string;
|
||||
name: string;
|
||||
key: string;
|
||||
refname: string;
|
||||
url: string;
|
||||
description: string;
|
||||
state: BitBucketPullRequestStatusState;
|
||||
};
|
||||
|
||||
type BitBucketPullRequestStatues = {
|
||||
size: number;
|
||||
page: number;
|
||||
pagelen: number;
|
||||
next: string;
|
||||
preview: string;
|
||||
values: BitBucketPullRequestStatus[];
|
||||
};
|
||||
|
||||
type DeleteEntry = {
|
||||
path: string;
|
||||
delete: true;
|
||||
};
|
||||
|
||||
type BitBucketFile = {
|
||||
id: string;
|
||||
type: string;
|
||||
@ -189,8 +70,6 @@ type BitBucketCommit = {
|
||||
|
||||
export const API_NAME = 'Bitbucket';
|
||||
|
||||
const APPLICATION_JSON = 'application/json; charset=utf-8';
|
||||
|
||||
function replace404WithEmptyResponse(err: FetchError) {
|
||||
if (err && err.status === 404) {
|
||||
console.info('This 404 was expected and handled appropriately.');
|
||||
@ -207,9 +86,6 @@ export default class API {
|
||||
requestFunction: (req: ApiRequest) => Promise<Response>;
|
||||
repoURL: string;
|
||||
commitAuthor?: CommitAuthor;
|
||||
mergeStrategy: string;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
|
||||
constructor(config: Config) {
|
||||
this.apiRoot = config.apiRoot || 'https://api.bitbucket.org/2.0';
|
||||
@ -219,9 +95,6 @@ export default class API {
|
||||
// Allow overriding this.hasWriteAccess
|
||||
this.hasWriteAccess = config.hasWriteAccess || this.hasWriteAccess;
|
||||
this.repoURL = this.repo ? `/repositories/${this.repo}` : '';
|
||||
this.mergeStrategy = config.squashMerges ? 'squash' : 'merge_commit';
|
||||
this.initialWorkflowStatus = config.initialWorkflowStatus;
|
||||
this.cmsLabelPrefix = config.cmsLabelPrefix;
|
||||
}
|
||||
|
||||
buildRequest = (req: ApiRequest) => {
|
||||
@ -508,60 +381,7 @@ export default class API {
|
||||
|
||||
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
|
||||
const files = [...dataFiles, ...mediaFiles];
|
||||
if (options.useWorkflow) {
|
||||
const slug = dataFiles[0].slug;
|
||||
return this.editorialWorkflowGit(files, slug, options);
|
||||
} else {
|
||||
return this.uploadFiles(files, { commitMessage: options.commitMessage, branch: this.branch });
|
||||
}
|
||||
}
|
||||
|
||||
async addPullRequestComment(pullRequest: BitBucketPullRequest, comment: string) {
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.repoURL}/pullrequests/${pullRequest.id}/comments`,
|
||||
headers: { 'Content-Type': APPLICATION_JSON },
|
||||
body: JSON.stringify({
|
||||
content: {
|
||||
raw: comment,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async getPullRequestLabel(id: number) {
|
||||
const comments: BitBucketPullComments = await this.requestJSON({
|
||||
url: `${this.repoURL}/pullrequests/${id}/comments`,
|
||||
params: {
|
||||
pagelen: 100,
|
||||
},
|
||||
});
|
||||
return comments.values.map(c => c.content.raw)[comments.values.length - 1];
|
||||
}
|
||||
|
||||
async createPullRequest(branch: string, commitMessage: string, status: string) {
|
||||
const pullRequest: BitBucketPullRequest = await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.repoURL}/pullrequests`,
|
||||
headers: { 'Content-Type': APPLICATION_JSON },
|
||||
body: JSON.stringify({
|
||||
title: commitMessage,
|
||||
source: {
|
||||
branch: {
|
||||
name: branch,
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
branch: {
|
||||
name: this.branch,
|
||||
},
|
||||
},
|
||||
description: DEFAULT_PR_BODY,
|
||||
close_source_branch: true,
|
||||
}),
|
||||
});
|
||||
// use comments for status labels
|
||||
await this.addPullRequestComment(pullRequest, statusToLabel(status, this.cmsLabelPrefix));
|
||||
return this.uploadFiles(files, { commitMessage: options.commitMessage, branch: this.branch });
|
||||
}
|
||||
|
||||
async getDifferences(source: string, destination: string = this.branch) {
|
||||
@ -591,43 +411,6 @@ export default class API {
|
||||
return diffs;
|
||||
}
|
||||
|
||||
async editorialWorkflowGit(
|
||||
files: (DataFile | AssetProxy)[],
|
||||
slug: string,
|
||||
options: PersistOptions,
|
||||
) {
|
||||
const contentKey = generateContentKey(options.collectionName as string, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const unpublished = options.unpublished || false;
|
||||
if (!unpublished) {
|
||||
const defaultBranchSha = await this.branchCommitSha(this.branch);
|
||||
await this.uploadFiles(files, {
|
||||
commitMessage: options.commitMessage,
|
||||
branch,
|
||||
parentSha: defaultBranchSha,
|
||||
});
|
||||
await this.createPullRequest(
|
||||
branch,
|
||||
options.commitMessage,
|
||||
options.status || this.initialWorkflowStatus,
|
||||
);
|
||||
} else {
|
||||
// mark files for deletion
|
||||
const diffs = await this.getDifferences(branch);
|
||||
const toDelete: DeleteEntry[] = [];
|
||||
for (const diff of diffs.filter(d => d.binary && d.status !== 'deleted')) {
|
||||
if (!files.some(file => file.path === diff.path)) {
|
||||
toDelete.push({ path: diff.path, delete: true });
|
||||
}
|
||||
}
|
||||
|
||||
await this.uploadFiles([...files, ...toDelete], {
|
||||
commitMessage: options.commitMessage,
|
||||
branch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deleteFiles = (paths: string[], message: string) => {
|
||||
const body = new FormData();
|
||||
paths.forEach(path => {
|
||||
@ -645,159 +428,4 @@ export default class API {
|
||||
`${this.repoURL}/src`,
|
||||
);
|
||||
};
|
||||
|
||||
async getPullRequests(sourceBranch?: string) {
|
||||
const sourceQuery = sourceBranch
|
||||
? `source.branch.name = "${sourceBranch}"`
|
||||
: `source.branch.name ~ "${CMS_BRANCH_PREFIX}/"`;
|
||||
|
||||
const pullRequests: BitBucketPullRequests = await this.requestJSON({
|
||||
url: `${this.repoURL}/pullrequests`,
|
||||
params: {
|
||||
pagelen: 50,
|
||||
q: oneLine`
|
||||
source.repository.full_name = "${this.repo}"
|
||||
AND state = "${BitBucketPullRequestState.OPEN}"
|
||||
AND destination.branch.name = "${this.branch}"
|
||||
AND comment_count > 0
|
||||
AND ${sourceQuery}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const labels = await Promise.all(
|
||||
pullRequests.values.map(pr => this.getPullRequestLabel(pr.id)),
|
||||
);
|
||||
|
||||
return pullRequests.values.filter((_, index) => isCMSLabel(labels[index], this.cmsLabelPrefix));
|
||||
}
|
||||
|
||||
async getBranchPullRequest(branch: string) {
|
||||
const pullRequests = await this.getPullRequests(branch);
|
||||
if (pullRequests.length <= 0) {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
|
||||
return pullRequests[0];
|
||||
}
|
||||
|
||||
async listUnpublishedBranches() {
|
||||
console.info(
|
||||
'%c Checking for Unpublished entries',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
);
|
||||
|
||||
const pullRequests = await this.getPullRequests();
|
||||
const branches = pullRequests.map(mr => mr.source.branch.name);
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
async retrieveUnpublishedEntryData(contentKey: string) {
|
||||
const { collection, slug } = parseContentKey(contentKey);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const diffs = await this.getDifferences(branch);
|
||||
const label = await this.getPullRequestLabel(pullRequest.id);
|
||||
const status = labelToStatus(label, this.cmsLabelPrefix);
|
||||
const updatedAt = pullRequest.updated_on;
|
||||
const pullRequestAuthor = pullRequest.author.display_name;
|
||||
return {
|
||||
collection,
|
||||
slug,
|
||||
status,
|
||||
// TODO: get real id
|
||||
diffs: diffs
|
||||
.filter(d => d.status !== 'deleted')
|
||||
.map(d => ({ path: d.path, newFile: d.newFile, id: '' })),
|
||||
updatedAt,
|
||||
pullRequestAuthor,
|
||||
};
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
|
||||
await this.addPullRequestComment(pullRequest, statusToLabel(newStatus, this.cmsLabelPrefix));
|
||||
}
|
||||
|
||||
async mergePullRequest(pullRequest: BitBucketPullRequest) {
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.repoURL}/pullrequests/${pullRequest.id}/merge`,
|
||||
headers: { 'Content-Type': APPLICATION_JSON },
|
||||
body: JSON.stringify({
|
||||
message: MERGE_COMMIT_MESSAGE,
|
||||
close_source_branch: true,
|
||||
merge_strategy: this.mergeStrategy,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
|
||||
await this.mergePullRequest(pullRequest);
|
||||
}
|
||||
|
||||
async declinePullRequest(pullRequest: BitBucketPullRequest) {
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.repoURL}/pullrequests/${pullRequest.id}/decline`,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBranch(branch: string) {
|
||||
await this.request({
|
||||
method: 'DELETE',
|
||||
url: `${this.repoURL}/refs/branches/${branch}`,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
|
||||
await this.declinePullRequest(pullRequest);
|
||||
await this.deleteBranch(branch);
|
||||
}
|
||||
|
||||
async getPullRequestStatuses(pullRequest: BitBucketPullRequest) {
|
||||
const statuses: BitBucketPullRequestStatues = await this.requestJSON({
|
||||
url: `${this.repoURL}/pullrequests/${pullRequest.id}/statuses`,
|
||||
params: {
|
||||
pagelen: 100,
|
||||
},
|
||||
});
|
||||
|
||||
return statuses.values;
|
||||
}
|
||||
|
||||
async getStatuses(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const statuses = await this.getPullRequestStatuses(pullRequest);
|
||||
|
||||
return statuses.map(({ key, state, url }) => ({
|
||||
context: key,
|
||||
state:
|
||||
state === BitBucketPullRequestStatusState.Successful
|
||||
? PreviewState.Success
|
||||
: PreviewState.Other,
|
||||
target_url: url,
|
||||
}));
|
||||
}
|
||||
|
||||
async getUnpublishedEntrySha(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
return pullRequest.destination.commit.hash;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { AuthenticationPage, Icon } from '../../ui';
|
||||
import { SimpleAuthenticator, ImplicitAuthenticator } from '../../lib/auth';
|
||||
import { NetlifyAuthenticator, ImplicitAuthenticator } from '../../lib/auth';
|
||||
|
||||
const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
|
@ -1,53 +1,48 @@
|
||||
import semaphore from 'semaphore';
|
||||
import { trimStart } from 'lodash';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { trimStart } from 'lodash';
|
||||
import semaphore from 'semaphore';
|
||||
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
filterByExtension,
|
||||
unsentRequest,
|
||||
basename,
|
||||
getBlobSHA,
|
||||
entriesByFolder,
|
||||
entriesByFiles,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
unpublishedEntries,
|
||||
runWithLock,
|
||||
asyncLock,
|
||||
getPreviewStatus,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
getPointerFileForMediaFileObj,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
blobToFileObj,
|
||||
contentKeyFromBranch,
|
||||
generateContentKey,
|
||||
localForage,
|
||||
allEntriesByFolder,
|
||||
AccessTokenError,
|
||||
branchFromContentKey,
|
||||
} from '../../lib/util';
|
||||
import { NetlifyAuthenticator } from '../../lib/auth';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import {
|
||||
AccessTokenError,
|
||||
allEntriesByFolder,
|
||||
asyncLock,
|
||||
basename,
|
||||
blobToFileObj,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
filterByExtension,
|
||||
getBlobSHA,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
getMediaAsBlob,
|
||||
getMediaDisplayURL,
|
||||
getPointerFileForMediaFileObj,
|
||||
localForage,
|
||||
runWithLock,
|
||||
unsentRequest,
|
||||
} from '../../lib/util';
|
||||
import API, { API_NAME } from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import { GitLfsClient } from './git-lfs-client';
|
||||
|
||||
import type {
|
||||
Entry,
|
||||
ApiRequest,
|
||||
Cursor,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
DisplayURL,
|
||||
Implementation,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
AsyncLock,
|
||||
FetchError,
|
||||
} from '../../lib/util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type {
|
||||
ApiRequest,
|
||||
AssetProxy,
|
||||
AsyncLock,
|
||||
Config,
|
||||
Credentials,
|
||||
Cursor,
|
||||
DisplayURL,
|
||||
Entry,
|
||||
FetchError,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
PersistOptions,
|
||||
User,
|
||||
} from '../../lib/util';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
@ -69,7 +64,6 @@ export default class BitbucketBackend implements Implementation {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
updateUserCredentials: (args: { token: string; refresh_token: string }) => Promise<null>;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
repo: string;
|
||||
branch: string;
|
||||
@ -82,9 +76,6 @@ export default class BitbucketBackend implements Implementation {
|
||||
refreshedTokenPromise?: Promise<string>;
|
||||
authenticator?: NetlifyAuthenticator;
|
||||
_mediaDisplayURLSem?: Semaphore;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
previewContext: string;
|
||||
largeMediaURL: string;
|
||||
_largeMediaClientPromise?: Promise<GitLfsClient>;
|
||||
authType: string;
|
||||
@ -94,7 +85,6 @@ export default class BitbucketBackend implements Implementation {
|
||||
proxied: false,
|
||||
API: null,
|
||||
updateUserCredentials: async () => null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
@ -118,9 +108,6 @@ export default class BitbucketBackend implements Implementation {
|
||||
config.backend.large_media_url || `https://bitbucket.org/${config.backend.repo}/info/lfs`;
|
||||
this.token = '';
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.lock = asyncLock();
|
||||
this.authType = config.backend.auth_type || '';
|
||||
}
|
||||
@ -172,9 +159,6 @@ export default class BitbucketBackend implements Implementation {
|
||||
requestFunction: this.apiRequestFunction,
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
});
|
||||
}
|
||||
|
||||
@ -196,9 +180,6 @@ export default class BitbucketBackend implements Implementation {
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
apiRoot: this.apiRoot,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
});
|
||||
|
||||
const isCollab = await this.api.hasWriteAccess().catch(error => {
|
||||
@ -535,96 +516,4 @@ export default class BitbucketBackend implements Implementation {
|
||||
file: fileObj,
|
||||
};
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const entryId = generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error('Missing unpublished entry id or collection and slug');
|
||||
}
|
||||
}
|
||||
|
||||
getBranch(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
return branch;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(path, id, { branch });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
try {
|
||||
const statuses = await this.api!.getStatuses(collection, slug);
|
||||
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
||||
|
||||
if (deployStatus) {
|
||||
const { target_url: url, state } = deployStatus;
|
||||
return { url, status: state };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { APIError } from '../../lib/util';
|
||||
import { API as GithubAPI } from '../github';
|
||||
|
||||
import type { Config as GitHubConfig, Diff } from '../github/API';
|
||||
import type { FetchError } from '../../lib/util';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import type { Config as GitHubConfig } from '../github/API';
|
||||
|
||||
type Config = GitHubConfig & {
|
||||
apiRoot: string;
|
||||
@ -119,12 +118,4 @@ export default class API extends GithubAPI {
|
||||
nextUrlProcessor() {
|
||||
return (url: string) => url.replace(/^(?:[a-z]+:\/\/.+?\/.+?\/.+?\/)/, `${this.apiRoot}/`);
|
||||
}
|
||||
|
||||
async diffFromFile(file: Octokit.ReposCompareCommitsResponseFilesItem): Promise<Diff> {
|
||||
const diff = await super.diffFromFile(file);
|
||||
return {
|
||||
...diff,
|
||||
binary: diff.binary || (await this.isLargeMedia(file.filename)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,26 @@
|
||||
import GoTrue from 'gotrue-js';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { get, pick, intersection } from 'lodash';
|
||||
import ini from 'ini';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { get, intersection, pick } from 'lodash';
|
||||
|
||||
import {
|
||||
APIError,
|
||||
unsentRequest,
|
||||
basename,
|
||||
entriesByFiles,
|
||||
parsePointerFile,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
getPointerFileForMediaFileObj,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
AccessTokenError,
|
||||
PreviewState,
|
||||
AccessTokenError, APIError, basename,
|
||||
entriesByFiles, getLargeMediaFilteredMediaFiles, getLargeMediaPatternsFromGitAttributesFile,
|
||||
getPointerFileForMediaFileObj, parsePointerFile, unsentRequest
|
||||
} from '../../lib/util';
|
||||
import { API as BitBucketAPI, BitbucketBackend } from '../bitbucket';
|
||||
import { GitHubBackend } from '../github';
|
||||
import { GitLabBackend } from '../gitlab';
|
||||
import { BitbucketBackend, API as BitBucketAPI } from '../bitbucket';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import GitHubAPI from './GitHubAPI';
|
||||
import GitLabAPI from './GitLabAPI';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import { getClient } from './netlify-lfs-client';
|
||||
|
||||
import type { Client } from './netlify-lfs-client';
|
||||
import type {
|
||||
ApiRequest,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
Entry,
|
||||
Cursor,
|
||||
Implementation,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
DisplayURLObject,
|
||||
AssetProxy, Config, Credentials, Cursor, DisplayURL, DisplayURLObject, Entry, Implementation, ImplementationFile, PersistOptions, User
|
||||
} from '../../lib/util';
|
||||
import type { Client } from './netlify-lfs-client';
|
||||
|
||||
const STATUS_PAGE = 'https://www.netlifystatus.com';
|
||||
const GIT_GATEWAY_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
|
||||
@ -126,18 +109,10 @@ interface NetlifyUser extends Credentials {
|
||||
user_metadata: { full_name: string; avatar_url: string };
|
||||
}
|
||||
|
||||
async function apiGet(path: string) {
|
||||
const apiRoot = 'https://api.netlify.com/api/v1/sites';
|
||||
const response = await fetch(`${apiRoot}/${path}`).then(res => res.json());
|
||||
return response;
|
||||
}
|
||||
|
||||
export default class GitGateway implements Implementation {
|
||||
config: Config;
|
||||
api?: GitHubAPI | GitLabAPI | BitBucketAPI;
|
||||
branch: string;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
mediaFolder: string;
|
||||
transformImages: boolean;
|
||||
gatewayUrl: string;
|
||||
@ -153,19 +128,15 @@ export default class GitGateway implements Implementation {
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: GitHubAPI | GitLabAPI | BitBucketAPI | null;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = {
|
||||
proxied: true,
|
||||
API: null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
this.config = config;
|
||||
this.branch = config.backend.branch?.trim() || 'main';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.mediaFolder = config.media_folder;
|
||||
const { use_large_media_transforms_in_media_library: transformImages = true } = config.backend;
|
||||
this.transformImages = transformImages;
|
||||
@ -339,9 +310,6 @@ export default class GitGateway implements Implementation {
|
||||
tokenPromise: this.tokenPromise!,
|
||||
commitAuthor: pick(userData, ['name', 'email']),
|
||||
isLargeMedia: (filename: string) => this.isLargeMediaFile(filename),
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
};
|
||||
|
||||
if (this.backendType === 'github') {
|
||||
@ -402,35 +370,11 @@ export default class GitGateway implements Implementation {
|
||||
return this.backend!.getEntry(path);
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
return this.backend!.unpublishedEntryDataFile(collection, slug, path, id);
|
||||
}
|
||||
|
||||
async isLargeMediaFile(path: string) {
|
||||
const client = await this.getLargeMediaClient();
|
||||
return client.enabled && client.matchPath(path);
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const isLargeMedia = await this.isLargeMediaFile(path);
|
||||
if (isLargeMedia) {
|
||||
const branch = this.backend!.getBranch(collection, slug);
|
||||
const { url, blob } = await this.getLargeMediaDisplayURL({ path, id }, branch);
|
||||
const name = basename(path);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
path,
|
||||
url,
|
||||
displayURL: url,
|
||||
file: new File([blob], name),
|
||||
size: blob.size,
|
||||
};
|
||||
} else {
|
||||
return this.backend!.unpublishedEntryMediaFile(collection, slug, path, id);
|
||||
}
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
return this.backend!.getMedia(mediaFolder);
|
||||
}
|
||||
@ -578,49 +522,6 @@ export default class GitGateway implements Implementation {
|
||||
deleteFiles(paths: string[], commitMessage: string) {
|
||||
return this.backend!.deleteFiles(paths, commitMessage);
|
||||
}
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
let preview = await this.backend!.getDeployPreview(collection, slug);
|
||||
if (!preview) {
|
||||
try {
|
||||
// if the commit doesn't have a status, try to use Netlify API directly
|
||||
// this is useful when builds are queue up in Netlify and don't have a commit status yet
|
||||
// and only works with public logs at the moment
|
||||
// TODO: get Netlify API Token and use it to access private logs
|
||||
const siteId = new URL(localStorage.getItem('netlifySiteURL') || '').hostname;
|
||||
const site = await apiGet(siteId);
|
||||
const deploys: { state: string; commit_ref: string; deploy_url: string }[] = await apiGet(
|
||||
`${site.id}/deploys?per_page=100`,
|
||||
);
|
||||
if (deploys.length > 0) {
|
||||
const ref = await this.api!.getUnpublishedEntrySha(collection, slug);
|
||||
const deploy = deploys.find(d => d.commit_ref === ref);
|
||||
if (deploy) {
|
||||
preview = {
|
||||
status: deploy.state === 'ready' ? PreviewState.Success : PreviewState.Other,
|
||||
url: deploy.deploy_url,
|
||||
};
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
unpublishedEntries() {
|
||||
return this.backend!.unpublishedEntries();
|
||||
}
|
||||
unpublishedEntry({ id, collection, slug }: { id?: string; collection?: string; slug?: string }) {
|
||||
return this.backend!.unpublishedEntry({ id, collection, slug });
|
||||
}
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
return this.backend!.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
||||
}
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.backend!.deleteUnpublishedEntry(collection, slug);
|
||||
}
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.backend!.publishUnpublishedEntry(collection, slug);
|
||||
}
|
||||
traverseCursor(cursor: Cursor, action: string) {
|
||||
return this.backend!.traverseCursor!(cursor, action);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -9,19 +9,6 @@ const LoginButtonIcon = styled(Icon)`
|
||||
margin-right: 18px;
|
||||
`;
|
||||
|
||||
const ForkApprovalContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
flex-grow: 0.2;
|
||||
`;
|
||||
const ForkButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export default class GitHubAuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
@ -36,35 +23,6 @@ export default class GitHubAuthenticationPage extends React.Component {
|
||||
|
||||
state = {};
|
||||
|
||||
getPermissionToFork = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.setState({
|
||||
requestingFork: true,
|
||||
approveFork: () => {
|
||||
this.setState({ requestingFork: false });
|
||||
resolve();
|
||||
},
|
||||
refuseFork: () => {
|
||||
this.setState({ requestingFork: false });
|
||||
reject();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
loginWithOpenAuthoring(data) {
|
||||
const { backend } = this.props;
|
||||
|
||||
this.setState({ findingFork: true });
|
||||
return backend
|
||||
.authenticateWithFork({ userData: data, getPermissionToFork: this.getPermissionToFork })
|
||||
.catch(err => {
|
||||
this.setState({ findingFork: false });
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
handleLogin = e => {
|
||||
e.preventDefault();
|
||||
const cfg = {
|
||||
@ -77,25 +35,22 @@ export default class GitHubAuthenticationPage extends React.Component {
|
||||
};
|
||||
const auth = new NetlifyAuthenticator(cfg);
|
||||
|
||||
const { open_authoring: openAuthoring = false, auth_scope: authScope = '' } =
|
||||
const { auth_scope: authScope = '' } =
|
||||
this.props.config.backend;
|
||||
|
||||
const scope = authScope || (openAuthoring ? 'public_repo' : 'repo');
|
||||
const scope = authScope || 'repo';
|
||||
auth.authenticate({ provider: 'github', scope }, (err, data) => {
|
||||
if (err) {
|
||||
this.setState({ loginError: err.toString() });
|
||||
return;
|
||||
}
|
||||
if (openAuthoring) {
|
||||
return this.loginWithOpenAuthoring(data).then(() => this.props.onLogin(data));
|
||||
}
|
||||
this.props.onLogin(data);
|
||||
});
|
||||
};
|
||||
|
||||
renderLoginButton = () => {
|
||||
const { inProgress, t } = this.props;
|
||||
return inProgress || this.state.findingFork ? (
|
||||
return inProgress ? (
|
||||
t('auth.loggingIn')
|
||||
) : (
|
||||
<React.Fragment>
|
||||
@ -106,28 +61,6 @@ export default class GitHubAuthenticationPage extends React.Component {
|
||||
};
|
||||
|
||||
getAuthenticationPageRenderArgs() {
|
||||
const { requestingFork } = this.state;
|
||||
|
||||
if (requestingFork) {
|
||||
const { approveFork, refuseFork } = this.state;
|
||||
return {
|
||||
renderPageContent: ({ LoginButton, TextButton, showAbortButton }) => (
|
||||
<ForkApprovalContainer>
|
||||
<p>
|
||||
Open Authoring is enabled: we need to use a fork on your github account. (If a fork
|
||||
already exists, we'll use that.)
|
||||
</p>
|
||||
<ForkButtonsContainer>
|
||||
<LoginButton onClick={approveFork}>Fork the repo</LoginButton>
|
||||
{showAbortButton && (
|
||||
<TextButton onClick={refuseFork}>Don't fork the repo</TextButton>
|
||||
)}
|
||||
</ForkButtonsContainer>
|
||||
</ForkApprovalContainer>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
renderButtonContent: this.renderLoginButton,
|
||||
};
|
||||
@ -135,12 +68,12 @@ export default class GitHubAuthenticationPage extends React.Component {
|
||||
|
||||
render() {
|
||||
const { inProgress, config, t } = this.props;
|
||||
const { loginError, requestingFork, findingFork } = this.state;
|
||||
const { loginError } = this.state;
|
||||
|
||||
return (
|
||||
<AuthenticationPage
|
||||
onLogin={this.handleLogin}
|
||||
loginDisabled={inProgress || findingFork || requestingFork}
|
||||
loginDisabled={inProgress}
|
||||
loginErrorMessage={loginError}
|
||||
logoUrl={config.logo_url}
|
||||
siteUrl={config.site_url}
|
||||
|
@ -1,32 +1,22 @@
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import {
|
||||
InMemoryCache,
|
||||
defaultDataIdFromObject,
|
||||
IntrospectionFragmentMatcher,
|
||||
InMemoryCache, IntrospectionFragmentMatcher
|
||||
} from 'apollo-cache-inmemory';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { setContext } from 'apollo-link-context';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { trim, trimStart } from 'lodash';
|
||||
|
||||
import {
|
||||
APIError,
|
||||
readFile,
|
||||
localForage,
|
||||
DEFAULT_PR_BODY,
|
||||
branchFromContentKey,
|
||||
CMS_BRANCH_PREFIX,
|
||||
throwOnConflictingBranches,
|
||||
APIError, localForage, readFile, throwOnConflictingBranches
|
||||
} from '../../lib/util';
|
||||
import API, { API_NAME } from './API';
|
||||
import introspectionQueryResultData from './fragmentTypes';
|
||||
import API, { API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
|
||||
import * as queries from './queries';
|
||||
import * as mutations from './mutations';
|
||||
import * as queries from './queries';
|
||||
|
||||
import type { Config, BlobArgs } from './API';
|
||||
import type { NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||
import type { QueryOptions, MutationOptions, OperationVariables } from 'apollo-client';
|
||||
import type { GraphQLError } from 'graphql';
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import type { MutationOptions, OperationVariables, QueryOptions } from 'apollo-client';
|
||||
import type { BlobArgs, Config } from './API';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
const CACHE_FIRST = 'cache-first';
|
||||
@ -55,44 +45,6 @@ interface TreeFile {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type GraphQLPullRequest = {
|
||||
id: string;
|
||||
baseRefName: string;
|
||||
baseRefOid: string;
|
||||
body: string;
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
number: number;
|
||||
state: string;
|
||||
title: string;
|
||||
mergedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
labels: { nodes: { name: string }[] };
|
||||
repository: {
|
||||
id: string;
|
||||
isFork: boolean;
|
||||
};
|
||||
user: GraphQLPullsListResponseItemUser;
|
||||
};
|
||||
|
||||
type GraphQLPullsListResponseItemUser = {
|
||||
avatar_url: string;
|
||||
login: string;
|
||||
url: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function transformPullRequest(pr: GraphQLPullRequest) {
|
||||
return {
|
||||
...pr,
|
||||
labels: pr.labels.nodes,
|
||||
head: { ref: pr.headRefName, sha: pr.headRefOid, repo: { fork: pr.repository.isFork } },
|
||||
base: { ref: pr.baseRefName, sha: pr.baseRefOid },
|
||||
};
|
||||
}
|
||||
|
||||
type Error = GraphQLError & { type: string };
|
||||
|
||||
export default class GraphQLAPI extends API {
|
||||
client: ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
@ -160,29 +112,6 @@ export default class GraphQLAPI extends API {
|
||||
if (branchName) {
|
||||
await throwOnConflictingBranches(branchName, name => this.getBranch(name), API_NAME);
|
||||
}
|
||||
} else if (
|
||||
Array.isArray(errors) &&
|
||||
errors.some(e =>
|
||||
new RegExp(
|
||||
`A ref named "refs/heads/${CMS_BRANCH_PREFIX}/.+?" already exists in the repository.`,
|
||||
).test(e.message),
|
||||
)
|
||||
) {
|
||||
const refName = options?.variables?.createRefInput?.name || '';
|
||||
const sha = options?.variables?.createRefInput?.oid || '';
|
||||
const branchName = trimStart(refName, 'refs/heads/');
|
||||
if (branchName && branchName.startsWith(`${CMS_BRANCH_PREFIX}/`) && sha) {
|
||||
try {
|
||||
// this can happen if the branch wasn't deleted when the PR was merged
|
||||
// we backup the existing branch just in case an re-run the mutation
|
||||
await this.backupBranch(branchName);
|
||||
await this.deleteBranch(branchName);
|
||||
const result = await this.client.mutate(options);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new APIError(error.message, 500, 'GitHub');
|
||||
}
|
||||
@ -280,81 +209,6 @@ export default class GraphQLAPI extends API {
|
||||
}
|
||||
}
|
||||
|
||||
async getPullRequestAuthor(pullRequest: Octokit.PullsListResponseItem) {
|
||||
const user = pullRequest.user as unknown as GraphQLPullsListResponseItemUser;
|
||||
return user?.name || user?.login;
|
||||
}
|
||||
|
||||
async getPullRequests(
|
||||
head: string | undefined,
|
||||
state: PullRequestState,
|
||||
predicate: (pr: Octokit.PullsListResponseItem) => boolean,
|
||||
) {
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
let states;
|
||||
if (state === PullRequestState.Open) {
|
||||
states = ['OPEN'];
|
||||
} else if (state === PullRequestState.Closed) {
|
||||
states = ['CLOSED', 'MERGED'];
|
||||
} else {
|
||||
states = ['OPEN', 'CLOSED', 'MERGED'];
|
||||
}
|
||||
const { data } = await this.query({
|
||||
query: queries.pullRequests,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
...(head ? { head } : {}),
|
||||
states,
|
||||
},
|
||||
});
|
||||
const {
|
||||
pullRequests,
|
||||
}: {
|
||||
pullRequests: {
|
||||
nodes: GraphQLPullRequest[];
|
||||
};
|
||||
} = data.repository;
|
||||
|
||||
const mapped = pullRequests.nodes.map(transformPullRequest);
|
||||
|
||||
return (mapped as unknown as Octokit.PullsListResponseItem[]).filter(
|
||||
pr => pr.head.ref.startsWith(`${CMS_BRANCH_PREFIX}/`) && predicate(pr),
|
||||
);
|
||||
}
|
||||
|
||||
async getOpenAuthoringBranches() {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
const { data } = await this.query({
|
||||
query: queries.openAuthoringBranches,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
refPrefix: `refs/heads/cms/${this.repo}/`,
|
||||
},
|
||||
});
|
||||
|
||||
return data.repository.refs.nodes.map(({ name, prefix }: { name: string; prefix: string }) => ({
|
||||
ref: `${prefix}${name}`,
|
||||
}));
|
||||
}
|
||||
|
||||
async getStatuses(collectionName: string, slug: string) {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const pullRequest = await this.getBranchPullRequest(branch);
|
||||
const sha = pullRequest.head.sha;
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
|
||||
if (data.repository.object) {
|
||||
const { status } = data.repository.object;
|
||||
const { contexts } = status || { contexts: [] };
|
||||
return contexts;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getAllFiles(entries: TreeEntry[], path: string) {
|
||||
const allFiles: TreeFile[] = entries.reduce((acc, item) => {
|
||||
if (item.type === 'tree') {
|
||||
@ -427,273 +281,21 @@ export default class GraphQLAPI extends API {
|
||||
return data.repository.branch;
|
||||
}
|
||||
|
||||
async patchRef(type: string, name: string, sha: string, opts: { force?: boolean } = {}) {
|
||||
async patchRef(type: string, name: string, sha: string) {
|
||||
if (type !== 'heads') {
|
||||
return super.patchRef(type, name, sha, opts);
|
||||
return super.patchRef(type, name, sha);
|
||||
}
|
||||
|
||||
const force = opts.force || false;
|
||||
|
||||
const branch = await this.getBranch(name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.updateBranch,
|
||||
variables: {
|
||||
input: { oid: sha, refId: branch.id, force },
|
||||
input: { oid: sha, refId: branch.id },
|
||||
},
|
||||
});
|
||||
return data!.updateRef.branch;
|
||||
}
|
||||
|
||||
async deleteBranch(branchName: string) {
|
||||
const branch = await this.getBranch(branchName);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.deleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (store: any) => store.data.delete(defaultDataIdFromObject(branch)),
|
||||
});
|
||||
|
||||
return data!.deleteRef;
|
||||
}
|
||||
|
||||
getPullRequestQuery(number: number) {
|
||||
const { originRepoOwner: owner, originRepoName: name } = this;
|
||||
|
||||
return {
|
||||
query: queries.pullRequest,
|
||||
variables: { owner, name, number },
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequest(number: number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestQuery(number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
// https://developer.github.com/v4/enum/pullrequeststate/
|
||||
// GraphQL state: [CLOSED, MERGED, OPEN]
|
||||
// REST API state: [closed, open]
|
||||
const state =
|
||||
data.repository.pullRequest.state === 'OPEN'
|
||||
? PullRequestState.Open
|
||||
: PullRequestState.Closed;
|
||||
return {
|
||||
...data.repository.pullRequest,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
getPullRequestAndBranchQuery(branch: string, number: number) {
|
||||
const { repoOwner: owner, repoName: name } = this;
|
||||
const { originRepoOwner, originRepoName } = this;
|
||||
return {
|
||||
query: queries.pullRequestAndBranch,
|
||||
variables: {
|
||||
owner,
|
||||
name,
|
||||
originRepoOwner,
|
||||
originRepoName,
|
||||
number,
|
||||
qualifiedName: this.getBranchQualifiedName(branch),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getPullRequestAndBranch(branch: string, number: number) {
|
||||
const { data } = await this.query({
|
||||
...this.getPullRequestAndBranchQuery(branch, number),
|
||||
fetchPolicy: CACHE_FIRST,
|
||||
});
|
||||
|
||||
const { repository, origin } = data;
|
||||
return { branch: repository.branch, pullRequest: origin.pullRequest };
|
||||
}
|
||||
|
||||
async openPR(number: number) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.reopenPullRequest,
|
||||
variables: {
|
||||
reopenPullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.reopenPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data!.reopenPullRequest;
|
||||
}
|
||||
|
||||
async closePR(number: number) {
|
||||
const pullRequest = await this.getPullRequest(number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequest,
|
||||
variables: {
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.closePullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return data!.closePullRequest;
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
try {
|
||||
const contentKey = this.generateContentKey(collectionName, slug);
|
||||
const branchName = branchFromContentKey(contentKey);
|
||||
const pr = await this.getBranchPullRequest(branchName);
|
||||
if (pr.number !== MOCK_PULL_REQUEST) {
|
||||
const { branch, pullRequest } = await this.getPullRequestAndBranch(branchName, pr.number);
|
||||
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.closePullRequestAndDeleteBranch,
|
||||
variables: {
|
||||
deleteRefInput: { refId: branch.id },
|
||||
closePullRequestInput: { pullRequestId: pullRequest.id },
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (store: any) => {
|
||||
store.data.delete(defaultDataIdFromObject(branch));
|
||||
store.data.delete(defaultDataIdFromObject(pullRequest));
|
||||
},
|
||||
});
|
||||
|
||||
return data!.closePullRequest;
|
||||
} else {
|
||||
return await this.deleteBranch(branchName);
|
||||
}
|
||||
} catch (e: any) {
|
||||
const { graphQLErrors } = e;
|
||||
if (graphQLErrors && graphQLErrors.length > 0) {
|
||||
const branchNotFound = graphQLErrors.some((e: Error) => e.type === 'NOT_FOUND');
|
||||
if (branchNotFound) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createPR(title: string, head: string) {
|
||||
const [repository, headReference] = await Promise.all([
|
||||
this.getRepository(this.originRepoOwner, this.originRepoName),
|
||||
this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head,
|
||||
]);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createPullRequest,
|
||||
variables: {
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: headReference,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { pullRequest } = mutationResult!.createPullRequest;
|
||||
const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestQuery(pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data!.createPullRequest;
|
||||
return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
|
||||
}
|
||||
|
||||
async createBranch(branchName: string, sha: string) {
|
||||
const owner = this.repoOwner;
|
||||
const name = this.repoName;
|
||||
const repository = await this.getRepository(owner, name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranch,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult!.createRef;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName, owner, name),
|
||||
data: branchData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { branch } = data!.createRef;
|
||||
return { ...branch, ref: `${branch.prefix}${branch.name}` };
|
||||
}
|
||||
|
||||
async createBranchAndPullRequest(branchName: string, sha: string, title: string) {
|
||||
const owner = this.originRepoOwner;
|
||||
const name = this.originRepoName;
|
||||
const repository = await this.getRepository(owner, name);
|
||||
const { data } = await this.mutate({
|
||||
mutation: mutations.createBranchAndPullRequest,
|
||||
variables: {
|
||||
createRefInput: {
|
||||
name: this.getBranchQualifiedName(branchName),
|
||||
oid: sha,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
createPullRequestInput: {
|
||||
baseRefName: this.branch,
|
||||
body: DEFAULT_PR_BODY,
|
||||
title,
|
||||
headRefName: branchName,
|
||||
repositoryId: repository.id,
|
||||
},
|
||||
},
|
||||
update: (store, { data: mutationResult }) => {
|
||||
const { branch } = mutationResult!.createRef;
|
||||
const { pullRequest } = mutationResult!.createPullRequest;
|
||||
const branchData = { repository: { ...branch.repository, branch } };
|
||||
const pullRequestData = {
|
||||
repository: { ...pullRequest.repository, branch },
|
||||
origin: { ...pullRequest.repository, pullRequest },
|
||||
};
|
||||
|
||||
store.writeQuery({
|
||||
...this.getBranchQuery(branchName, owner, name),
|
||||
data: branchData,
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
...this.getPullRequestAndBranchQuery(branchName, pullRequest.number),
|
||||
data: pullRequestData,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { pullRequest } = data!.createPullRequest;
|
||||
return transformPullRequest(pullRequest) as unknown as Octokit.PullsCreateResponse;
|
||||
}
|
||||
|
||||
async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) {
|
||||
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
|
||||
const { data } = await this.query({
|
||||
|
@ -1,46 +1,41 @@
|
||||
import { stripIndent } from 'common-tags';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import * as React from 'react';
|
||||
import semaphore from 'semaphore';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
Cursor,
|
||||
asyncLock,
|
||||
basename,
|
||||
getBlobSHA,
|
||||
entriesByFolder,
|
||||
entriesByFiles,
|
||||
unpublishedEntries,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
filterByExtension,
|
||||
getPreviewStatus,
|
||||
runWithLock,
|
||||
blobToFileObj,
|
||||
contentKeyFromBranch,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
filterByExtension,
|
||||
getBlobSHA,
|
||||
getMediaAsBlob,
|
||||
getMediaDisplayURL,
|
||||
runWithLock,
|
||||
unsentRequest,
|
||||
branchFromContentKey,
|
||||
} from '../../lib/util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import GraphQLAPI from './GraphQLAPI';
|
||||
|
||||
import type { Octokit } from '@octokit/rest';
|
||||
import type {
|
||||
AsyncLock,
|
||||
Implementation,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
UnpublishedEntryMediaFile,
|
||||
Entry,
|
||||
} from '../../lib/util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type {
|
||||
AssetProxy,
|
||||
AsyncLock,
|
||||
Config,
|
||||
Credentials,
|
||||
DisplayURL,
|
||||
Entry,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
PersistOptions,
|
||||
User,
|
||||
} from '../../lib/util';
|
||||
|
||||
type GitHubUser = Octokit.UsersGetAuthenticatedResponse;
|
||||
|
||||
@ -65,21 +60,13 @@ export default class GitHub implements Implementation {
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
useWorkflow?: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
originRepo: string;
|
||||
repo?: string;
|
||||
openAuthoringEnabled: boolean;
|
||||
useOpenAuthoring?: boolean;
|
||||
alwaysForkEnabled: boolean;
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
token: string | null;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
useGraphql: boolean;
|
||||
_currentUserPromise?: Promise<GitHubUser>;
|
||||
_userIsOriginMaintainerPromises?: {
|
||||
@ -91,7 +78,6 @@ export default class GitHub implements Implementation {
|
||||
this.options = {
|
||||
proxied: false,
|
||||
API: null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
@ -103,27 +89,12 @@ export default class GitHub implements Implementation {
|
||||
}
|
||||
|
||||
this.api = this.options.API || null;
|
||||
|
||||
this.openAuthoringEnabled = config.backend.open_authoring || false;
|
||||
if (this.openAuthoringEnabled) {
|
||||
if (!this.options.useWorkflow) {
|
||||
throw new Error(
|
||||
'backend.open_authoring is true but publish_mode is not set to editorial_workflow.',
|
||||
);
|
||||
}
|
||||
this.originRepo = config.backend.repo || '';
|
||||
} else {
|
||||
this.repo = this.originRepo = config.backend.repo || '';
|
||||
}
|
||||
this.alwaysForkEnabled = config.backend.always_fork || false;
|
||||
this.repo = this.originRepo = config.backend.repo || '';
|
||||
this.branch = config.backend.branch?.trim() || 'main';
|
||||
this.apiRoot = config.backend.api_root || 'https://api.github.com';
|
||||
this.token = '';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.useGraphql = config.backend.use_graphql || false;
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.lock = asyncLock();
|
||||
}
|
||||
|
||||
@ -173,35 +144,7 @@ export default class GitHub implements Implementation {
|
||||
}
|
||||
|
||||
restoreUser(user: User) {
|
||||
return this.openAuthoringEnabled
|
||||
? this.authenticateWithFork({ userData: user, getPermissionToFork: () => true }).then(() =>
|
||||
this.authenticate(user),
|
||||
)
|
||||
: this.authenticate(user);
|
||||
}
|
||||
|
||||
async pollUntilForkExists({ repo, token }: { repo: string; token: string }) {
|
||||
const pollDelay = 250; // milliseconds
|
||||
let repoExists = false;
|
||||
while (!repoExists) {
|
||||
repoExists = await fetch(`${this.apiRoot}/repos/${repo}`, {
|
||||
headers: { Authorization: `token ${token}` },
|
||||
})
|
||||
.then(() => true)
|
||||
.catch(err => {
|
||||
if (err && err.status === 404) {
|
||||
console.info('This 404 was expected and handled appropriately.');
|
||||
return false;
|
||||
} else {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
});
|
||||
// wait between polls
|
||||
if (!repoExists) {
|
||||
await new Promise(resolve => setTimeout(resolve, pollDelay));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return this.authenticate(user);
|
||||
}
|
||||
|
||||
async currentUser({ token }: { token: string }) {
|
||||
@ -239,65 +182,6 @@ export default class GitHub implements Implementation {
|
||||
return this._userIsOriginMaintainerPromises[username];
|
||||
}
|
||||
|
||||
async forkExists({ token }: { token: string }) {
|
||||
try {
|
||||
const currentUser = await this.currentUser({ token });
|
||||
const repoName = this.originRepo.split('/')[1];
|
||||
const repo = await fetch(`${this.apiRoot}/repos/${currentUser.login}/${repoName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
// https://developer.github.com/v3/repos/#get
|
||||
// The parent and source objects are present when the repository is a fork.
|
||||
// parent is the repository this repository was forked from, source is the ultimate source for the network.
|
||||
const forkExists =
|
||||
repo.fork === true &&
|
||||
repo.parent &&
|
||||
repo.parent.full_name.toLowerCase() === this.originRepo.toLowerCase();
|
||||
return forkExists;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateWithFork({
|
||||
userData,
|
||||
getPermissionToFork,
|
||||
}: {
|
||||
userData: User;
|
||||
getPermissionToFork: () => Promise<boolean> | boolean;
|
||||
}) {
|
||||
if (!this.openAuthoringEnabled) {
|
||||
throw new Error('Cannot authenticate with fork; Open Authoring is turned off.');
|
||||
}
|
||||
const token = userData.token as string;
|
||||
|
||||
// Origin maintainers should be able to use the CMS normally. If alwaysFork
|
||||
// is enabled we always fork (and avoid the origin maintainer check)
|
||||
if (!this.alwaysForkEnabled && (await this.userIsOriginMaintainer({ token }))) {
|
||||
this.repo = this.originRepo;
|
||||
this.useOpenAuthoring = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!(await this.forkExists({ token }))) {
|
||||
await getPermissionToFork();
|
||||
}
|
||||
|
||||
const fork = await fetch(`${this.apiRoot}/repos/${this.originRepo}/forks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
this.useOpenAuthoring = true;
|
||||
this.repo = fork.full_name;
|
||||
return this.pollUntilForkExists({ repo: fork.full_name, token });
|
||||
}
|
||||
|
||||
async authenticate(state: Credentials) {
|
||||
this.token = state.token as string;
|
||||
const apiCtor = this.useGraphql ? GraphQLAPI : API;
|
||||
@ -307,10 +191,6 @@ export default class GitHub implements Implementation {
|
||||
repo: this.repo,
|
||||
originRepo: this.originRepo,
|
||||
apiRoot: this.apiRoot,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
useOpenAuthoring: this.useOpenAuthoring,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
});
|
||||
const user = await this.api!.user();
|
||||
const isCollab = await this.api!.hasWriteAccess().catch(error => {
|
||||
@ -333,7 +213,7 @@ export default class GitHub implements Implementation {
|
||||
}
|
||||
|
||||
// Authorized user
|
||||
return { ...user, token: state.token as string, useOpenAuthoring: this.useOpenAuthoring };
|
||||
return { ...user, token: state.token as string };
|
||||
}
|
||||
|
||||
logout() {
|
||||
@ -425,7 +305,7 @@ export default class GitHub implements Implementation {
|
||||
}
|
||||
|
||||
entriesByFiles(files: ImplementationFile[]) {
|
||||
const repoURL = this.useOpenAuthoring ? this.api!.originRepoURL : this.api!.repoURL;
|
||||
const repoURL = this.api!.repoURL;
|
||||
|
||||
const readFile = (path: string, id: string | null | undefined) =>
|
||||
this.api!.readFile(path, id, { repoURL }).catch(() => '') as Promise<string>;
|
||||
@ -558,116 +438,4 @@ export default class GitHub implements Implementation {
|
||||
cursor: result.cursor,
|
||||
};
|
||||
}
|
||||
|
||||
async loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
const blob = await getMediaAsBlob(file.path, file.id, readFile);
|
||||
const name = basename(file.path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.id,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const entryId = this.api!.generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error('Missing unpublished entry id or collection and slug');
|
||||
}
|
||||
}
|
||||
|
||||
getBranch(collection: string, slug: string) {
|
||||
const contentKey = this.api!.generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
return branch;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
try {
|
||||
const statuses = await this.api!.getStatuses(collection, slug);
|
||||
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
||||
|
||||
if (deployStatus) {
|
||||
const { target_url: url, state } = deployStatus;
|
||||
return { url, status: state };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,98 +13,3 @@ export const updateBranch = gql`
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// deleteRef only works for branches at the moment
|
||||
const deleteRefMutationPart = `
|
||||
deleteRef(input: $deleteRefInput) {
|
||||
clientMutationId
|
||||
}
|
||||
`;
|
||||
export const deleteBranch = gql`
|
||||
mutation deleteRef($deleteRefInput: DeleteRefInput!) {
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
`;
|
||||
|
||||
const closePullRequestMutationPart = `
|
||||
closePullRequest(input: $closePullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const closePullRequest = gql`
|
||||
mutation closePullRequestAndDeleteBranch($closePullRequestInput: ClosePullRequestInput!) {
|
||||
${closePullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const closePullRequestAndDeleteBranch = gql`
|
||||
mutation closePullRequestAndDeleteBranch(
|
||||
$closePullRequestInput: ClosePullRequestInput!
|
||||
$deleteRefInput: DeleteRefInput!
|
||||
) {
|
||||
${closePullRequestMutationPart}
|
||||
${deleteRefMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
const createPullRequestMutationPart = `
|
||||
createPullRequest(input: $createPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const createPullRequest = gql`
|
||||
mutation createPullRequest($createPullRequestInput: CreatePullRequestInput!) {
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const createBranch = gql`
|
||||
mutation createBranch($createRefInput: CreateRefInput!) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
// createRef only works for branches at the moment
|
||||
export const createBranchAndPullRequest = gql`
|
||||
mutation createBranchAndPullRequest(
|
||||
$createRefInput: CreateRefInput!
|
||||
$createPullRequestInput: CreatePullRequestInput!
|
||||
) {
|
||||
createRef(input: $createRefInput) {
|
||||
branch: ref {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
${createPullRequestMutationPart}
|
||||
}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const reopenPullRequest = gql`
|
||||
mutation reopenPullRequest($reopenPullRequestInput: ReopenPullRequestInput!) {
|
||||
reopenPullRequest(input: $reopenPullRequestInput) {
|
||||
clientMutationId
|
||||
pullRequest {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
@ -129,21 +129,6 @@ export const branch = gql`
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const openAuthoringBranches = gql`
|
||||
query openAuthoringBranches($owner: String!, $name: String!, $refPrefix: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
refs(refPrefix: $refPrefix, last: 100) {
|
||||
nodes {
|
||||
...BranchParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
`;
|
||||
|
||||
export const repository = gql`
|
||||
query repository($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
@ -153,52 +138,6 @@ export const repository = gql`
|
||||
${fragments.repository}
|
||||
`;
|
||||
|
||||
const pullRequestQueryPart = `
|
||||
pullRequest(number: $number) {
|
||||
...PullRequestParts
|
||||
}
|
||||
`;
|
||||
|
||||
export const pullRequest = gql`
|
||||
query pullRequest($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequests = gql`
|
||||
query pullRequests($owner: String!, $name: String!, $head: String, $states: [PullRequestState!]) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
pullRequests(last: 100, headRefName: $head, states: $states) {
|
||||
nodes {
|
||||
...PullRequestParts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const pullRequestAndBranch = gql`
|
||||
query pullRequestAndBranch($owner: String!, $name: String!, $originRepoOwner: String!, $originRepoName: String!, $qualifiedName: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
...RepositoryParts
|
||||
${branchQueryPart}
|
||||
}
|
||||
origin: repository(owner: $originRepoOwner, name: $originRepoName) {
|
||||
...RepositoryParts
|
||||
${pullRequestQueryPart}
|
||||
}
|
||||
}
|
||||
${fragments.repository}
|
||||
${fragments.branch}
|
||||
${fragments.pullRequest}
|
||||
`;
|
||||
|
||||
export const fileSha = gql`
|
||||
query fileSha($owner: String!, $name: String!, $expression: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
|
@ -1,48 +1,33 @@
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { setContext } from 'apollo-link-context';
|
||||
import { Base64 } from 'js-base64';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { Map } from 'immutable';
|
||||
import { Base64 } from 'js-base64';
|
||||
import { flow, partial, result, trimStart } from 'lodash';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
import * as queries from './queries';
|
||||
import {
|
||||
localForage,
|
||||
parseLinkHeader,
|
||||
unsentRequest,
|
||||
then,
|
||||
APIError,
|
||||
Cursor,
|
||||
readFile,
|
||||
CMS_BRANCH_PREFIX,
|
||||
generateContentKey,
|
||||
isCMSLabel,
|
||||
EditorialWorkflowError,
|
||||
labelToStatus,
|
||||
statusToLabel,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
responseParser,
|
||||
PreviewState,
|
||||
parseContentKey,
|
||||
branchFromContentKey,
|
||||
requestWithBackoff,
|
||||
APIError, Cursor, localForage, parseLinkHeader, readFile,
|
||||
readFileMetadata,
|
||||
requestWithBackoff,
|
||||
responseParser, then,
|
||||
throwOnConflictingBranches,
|
||||
unsentRequest
|
||||
} from '../../lib/util';
|
||||
import * as queries from './queries';
|
||||
|
||||
const NO_CACHE = 'no-cache';
|
||||
|
||||
import type { ApolloQueryResult } from 'apollo-client';
|
||||
import type { NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||
import type { ApolloQueryResult } from 'apollo-client';
|
||||
import type {
|
||||
ApiRequest,
|
||||
DataFile,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
DataFile,
|
||||
FetchError,
|
||||
ImplementationFile,
|
||||
PersistOptions
|
||||
} from '../../lib/util';
|
||||
|
||||
export const API_NAME = 'GitLab';
|
||||
@ -53,9 +38,6 @@ export interface Config {
|
||||
token?: string;
|
||||
branch?: string;
|
||||
repo?: string;
|
||||
squashMerges: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
useGraphQL?: boolean;
|
||||
}
|
||||
|
||||
@ -103,55 +85,6 @@ type GitLabCommitDiff = {
|
||||
deleted_file: boolean;
|
||||
};
|
||||
|
||||
enum GitLabCommitStatuses {
|
||||
Pending = 'pending',
|
||||
Running = 'running',
|
||||
Success = 'success',
|
||||
Failed = 'failed',
|
||||
Canceled = 'canceled',
|
||||
}
|
||||
|
||||
type GitLabCommitStatus = {
|
||||
status: GitLabCommitStatuses;
|
||||
name: string;
|
||||
author: {
|
||||
username: string;
|
||||
name: string;
|
||||
};
|
||||
description: null;
|
||||
sha: string;
|
||||
ref: string;
|
||||
target_url: string;
|
||||
};
|
||||
|
||||
type GitLabMergeRebase = {
|
||||
rebase_in_progress: boolean;
|
||||
merge_error: string;
|
||||
};
|
||||
|
||||
type GitLabMergeRequest = {
|
||||
id: number;
|
||||
iid: number;
|
||||
title: string;
|
||||
description: string;
|
||||
state: string;
|
||||
merged_by: {
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
merged_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
target_branch: string;
|
||||
source_branch: string;
|
||||
author: {
|
||||
name: string;
|
||||
username: string;
|
||||
};
|
||||
labels: string[];
|
||||
sha: string;
|
||||
};
|
||||
|
||||
type GitLabRepo = {
|
||||
shared_with_groups: { group_access_level: number }[] | null;
|
||||
permissions: {
|
||||
@ -209,13 +142,9 @@ export default class API {
|
||||
graphQLAPIRoot: string;
|
||||
token: string | boolean;
|
||||
branch: string;
|
||||
useOpenAuthoring?: boolean;
|
||||
repo: string;
|
||||
repoURL: string;
|
||||
commitAuthor?: CommitAuthor;
|
||||
squashMerges: boolean;
|
||||
initialWorkflowStatus: string;
|
||||
cmsLabelPrefix: string;
|
||||
|
||||
graphQLClient?: ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
@ -226,9 +155,6 @@ export default class API {
|
||||
this.branch = config.branch || 'main';
|
||||
this.repo = config.repo || '';
|
||||
this.repoURL = `/projects/${encodeURIComponent(this.repo)}`;
|
||||
this.squashMerges = config.squashMerges;
|
||||
this.initialWorkflowStatus = config.initialWorkflowStatus;
|
||||
this.cmsLabelPrefix = config.cmsLabelPrefix;
|
||||
if (config.useGraphQL === true) {
|
||||
this.graphQLClient = this.getApolloClient();
|
||||
}
|
||||
@ -664,15 +590,10 @@ export default class API {
|
||||
|
||||
async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) {
|
||||
const files = [...dataFiles, ...mediaFiles];
|
||||
if (options.useWorkflow) {
|
||||
const slug = dataFiles[0].slug;
|
||||
return this.editorialWorkflowGit(files, slug, options);
|
||||
} else {
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
return this.uploadAndCommit(items, {
|
||||
commitMessage: options.commitMessage,
|
||||
});
|
||||
}
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
return this.uploadAndCommit(items, {
|
||||
commitMessage: options.commitMessage,
|
||||
});
|
||||
}
|
||||
|
||||
deleteFiles = (paths: string[], commitMessage: string) => {
|
||||
@ -690,37 +611,6 @@ export default class API {
|
||||
});
|
||||
};
|
||||
|
||||
async getMergeRequests(sourceBranch?: string) {
|
||||
const mergeRequests: GitLabMergeRequest[] = await this.requestJSON({
|
||||
url: `${this.repoURL}/merge_requests`,
|
||||
params: {
|
||||
state: 'opened',
|
||||
labels: 'Any',
|
||||
per_page: 100,
|
||||
target_branch: this.branch,
|
||||
...(sourceBranch ? { source_branch: sourceBranch } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return mergeRequests.filter(
|
||||
mr =>
|
||||
mr.source_branch.startsWith(CMS_BRANCH_PREFIX) &&
|
||||
mr.labels.some(l => isCMSLabel(l, this.cmsLabelPrefix)),
|
||||
);
|
||||
}
|
||||
|
||||
async listUnpublishedBranches() {
|
||||
console.info(
|
||||
'%c Checking for Unpublished entries',
|
||||
'line-height: 30px;text-align: center;font-weight: bold',
|
||||
);
|
||||
|
||||
const mergeRequests = await this.getMergeRequests();
|
||||
const branches = mergeRequests.map(mr => mr.source_branch);
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
async getFileId(path: string, branch: string) {
|
||||
const request = await this.request({
|
||||
method: 'HEAD',
|
||||
@ -749,15 +639,6 @@ export default class API {
|
||||
return fileExists;
|
||||
}
|
||||
|
||||
async getBranchMergeRequest(branch: string) {
|
||||
const mergeRequests = await this.getMergeRequests(branch);
|
||||
if (mergeRequests.length <= 0) {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
|
||||
return mergeRequests[0];
|
||||
}
|
||||
|
||||
async getDifferences(to: string, from = this.branch) {
|
||||
if (to === from) {
|
||||
return [];
|
||||
@ -794,169 +675,6 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveUnpublishedEntryData(contentKey: string) {
|
||||
const { collection, slug } = parseContentKey(contentKey);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
const diffs = await this.getDifferences(mergeRequest.sha);
|
||||
const diffsWithIds = await Promise.all(
|
||||
diffs.map(async d => {
|
||||
const { path, newFile } = d;
|
||||
const id = await this.getFileId(path, branch);
|
||||
return { id, path, newFile };
|
||||
}),
|
||||
);
|
||||
const label = mergeRequest.labels.find(l => isCMSLabel(l, this.cmsLabelPrefix)) as string;
|
||||
const status = labelToStatus(label, this.cmsLabelPrefix);
|
||||
const updatedAt = mergeRequest.updated_at;
|
||||
const pullRequestAuthor = mergeRequest.author.name;
|
||||
return {
|
||||
collection,
|
||||
slug,
|
||||
status,
|
||||
diffs: diffsWithIds,
|
||||
updatedAt,
|
||||
pullRequestAuthor,
|
||||
};
|
||||
}
|
||||
|
||||
async rebaseMergeRequest(mergeRequest: GitLabMergeRequest) {
|
||||
let rebase: GitLabMergeRebase = await this.requestJSON({
|
||||
method: 'PUT',
|
||||
url: `${this.repoURL}/merge_requests/${mergeRequest.iid}/rebase?skip_ci=true`,
|
||||
});
|
||||
|
||||
let i = 1;
|
||||
while (rebase.rebase_in_progress) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
rebase = await this.requestJSON({
|
||||
url: `${this.repoURL}/merge_requests/${mergeRequest.iid}`,
|
||||
params: {
|
||||
include_rebase_in_progress: true,
|
||||
},
|
||||
});
|
||||
if (!rebase.rebase_in_progress || i > 30) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (rebase.rebase_in_progress) {
|
||||
throw new APIError('Timed out rebasing merge request', null, API_NAME);
|
||||
} else if (rebase.merge_error) {
|
||||
throw new APIError(`Rebase error: ${rebase.merge_error}`, null, API_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
async createMergeRequest(branch: string, commitMessage: string, status: string) {
|
||||
await this.requestJSON({
|
||||
method: 'POST',
|
||||
url: `${this.repoURL}/merge_requests`,
|
||||
params: {
|
||||
source_branch: branch,
|
||||
target_branch: this.branch,
|
||||
title: commitMessage,
|
||||
description: DEFAULT_PR_BODY,
|
||||
labels: statusToLabel(status, this.cmsLabelPrefix),
|
||||
remove_source_branch: true,
|
||||
squash: this.squashMerges,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async editorialWorkflowGit(
|
||||
files: (DataFile | AssetProxy)[],
|
||||
slug: string,
|
||||
options: PersistOptions,
|
||||
) {
|
||||
const contentKey = generateContentKey(options.collectionName as string, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const unpublished = options.unpublished || false;
|
||||
if (!unpublished) {
|
||||
const items = await this.getCommitItems(files, this.branch);
|
||||
await this.uploadAndCommit(items, {
|
||||
commitMessage: options.commitMessage,
|
||||
branch,
|
||||
newBranch: true,
|
||||
});
|
||||
await this.createMergeRequest(
|
||||
branch,
|
||||
options.commitMessage,
|
||||
options.status || this.initialWorkflowStatus,
|
||||
);
|
||||
} else {
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
await this.rebaseMergeRequest(mergeRequest);
|
||||
const [items, diffs] = await Promise.all([
|
||||
this.getCommitItems(files, branch),
|
||||
this.getDifferences(branch),
|
||||
]);
|
||||
// mark files for deletion
|
||||
for (const diff of diffs.filter(d => d.binary)) {
|
||||
if (!items.some(item => item.path === diff.path)) {
|
||||
items.push({ action: CommitAction.DELETE, path: diff.newPath });
|
||||
}
|
||||
}
|
||||
|
||||
await this.uploadAndCommit(items, {
|
||||
commitMessage: options.commitMessage,
|
||||
branch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateMergeRequestLabels(mergeRequest: GitLabMergeRequest, labels: string[]) {
|
||||
await this.requestJSON({
|
||||
method: 'PUT',
|
||||
url: `${this.repoURL}/merge_requests/${mergeRequest.iid}`,
|
||||
params: {
|
||||
labels: labels.join(','),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
|
||||
const labels = [
|
||||
...mergeRequest.labels.filter(label => !isCMSLabel(label, this.cmsLabelPrefix)),
|
||||
statusToLabel(newStatus, this.cmsLabelPrefix),
|
||||
];
|
||||
await this.updateMergeRequestLabels(mergeRequest, labels);
|
||||
}
|
||||
|
||||
async mergeMergeRequest(mergeRequest: GitLabMergeRequest) {
|
||||
await this.requestJSON({
|
||||
method: 'PUT',
|
||||
url: `${this.repoURL}/merge_requests/${mergeRequest.iid}/merge`,
|
||||
params: {
|
||||
merge_commit_message: MERGE_COMMIT_MESSAGE,
|
||||
squash_commit_message: MERGE_COMMIT_MESSAGE,
|
||||
squash: this.squashMerges,
|
||||
should_remove_source_branch: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
await this.mergeMergeRequest(mergeRequest);
|
||||
}
|
||||
|
||||
async closeMergeRequest(mergeRequest: GitLabMergeRequest) {
|
||||
await this.requestJSON({
|
||||
method: 'PUT',
|
||||
url: `${this.repoURL}/merge_requests/${mergeRequest.iid}`,
|
||||
params: {
|
||||
state_event: 'close',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getDefaultBranch() {
|
||||
const branch: GitLabBranch = await this.getBranch(this.branch);
|
||||
return branch;
|
||||
@ -971,48 +689,4 @@ export default class API {
|
||||
});
|
||||
return refs.some(r => r.name === branch);
|
||||
}
|
||||
|
||||
async deleteBranch(branch: string) {
|
||||
await this.request({
|
||||
method: 'DELETE',
|
||||
url: `${this.repoURL}/repository/branches/${encodeURIComponent(branch)}`,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
await this.closeMergeRequest(mergeRequest);
|
||||
await this.deleteBranch(branch);
|
||||
}
|
||||
|
||||
async getMergeRequestStatues(mergeRequest: GitLabMergeRequest, branch: string) {
|
||||
const statuses: GitLabCommitStatus[] = await this.requestJSON({
|
||||
url: `${this.repoURL}/repository/commits/${mergeRequest.sha}/statuses`,
|
||||
params: {
|
||||
ref: branch,
|
||||
},
|
||||
});
|
||||
return statuses;
|
||||
}
|
||||
|
||||
async getStatuses(collectionName: string, slug: string) {
|
||||
const contentKey = generateContentKey(collectionName, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
const statuses: GitLabCommitStatus[] = await this.getMergeRequestStatues(mergeRequest, branch);
|
||||
return statuses.map(({ name, status, target_url }) => ({
|
||||
context: name,
|
||||
state: status === GitLabCommitStatuses.Success ? PreviewState.Success : PreviewState.Other,
|
||||
target_url,
|
||||
}));
|
||||
}
|
||||
|
||||
async getUnpublishedEntrySha(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
const mergeRequest = await this.getBranchMergeRequest(branch);
|
||||
return mergeRequest.sha;
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,40 @@
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { trim } from 'lodash';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import semaphore from 'semaphore';
|
||||
import { trim } from 'lodash';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import {
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
basename,
|
||||
entriesByFolder,
|
||||
entriesByFiles,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
unpublishedEntries,
|
||||
getPreviewStatus,
|
||||
asyncLock,
|
||||
runWithLock,
|
||||
getBlobSHA,
|
||||
blobToFileObj,
|
||||
contentKeyFromBranch,
|
||||
generateContentKey,
|
||||
localForage,
|
||||
allEntriesByFolder,
|
||||
asyncLock,
|
||||
basename,
|
||||
blobToFileObj,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
filterByExtension,
|
||||
branchFromContentKey,
|
||||
getBlobSHA,
|
||||
getMediaAsBlob,
|
||||
getMediaDisplayURL,
|
||||
localForage,
|
||||
runWithLock,
|
||||
} from '../../lib/util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type {
|
||||
Entry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
Cursor,
|
||||
Implementation,
|
||||
DisplayURL,
|
||||
User,
|
||||
Credentials,
|
||||
Config,
|
||||
ImplementationFile,
|
||||
UnpublishedEntryMediaFile,
|
||||
AsyncLock,
|
||||
} from '../../lib/util';
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type {
|
||||
AssetProxy,
|
||||
AsyncLock,
|
||||
Config,
|
||||
Credentials,
|
||||
Cursor,
|
||||
DisplayURL,
|
||||
Entry,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
PersistOptions,
|
||||
User,
|
||||
} from '../../lib/util';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
@ -50,16 +44,12 @@ export default class GitLab implements Implementation {
|
||||
options: {
|
||||
proxied: boolean;
|
||||
API: API | null;
|
||||
initialWorkflowStatus: string;
|
||||
};
|
||||
repo: string;
|
||||
branch: string;
|
||||
apiRoot: string;
|
||||
token: string | null;
|
||||
squashMerges: boolean;
|
||||
cmsLabelPrefix: string;
|
||||
mediaFolder: string;
|
||||
previewContext: string;
|
||||
useGraphQL: boolean;
|
||||
graphQLAPIRoot: string;
|
||||
|
||||
@ -69,7 +59,6 @@ export default class GitLab implements Implementation {
|
||||
this.options = {
|
||||
proxied: false,
|
||||
API: null,
|
||||
initialWorkflowStatus: '',
|
||||
...options,
|
||||
};
|
||||
|
||||
@ -86,10 +75,7 @@ export default class GitLab implements Implementation {
|
||||
this.branch = config.backend.branch || 'main';
|
||||
this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4';
|
||||
this.token = '';
|
||||
this.squashMerges = config.backend.squash_merges || false;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.previewContext = config.backend.preview_context || '';
|
||||
this.useGraphQL = config.backend.use_graphql || false;
|
||||
this.graphQLAPIRoot = config.backend.graphql_api_root || 'https://gitlab.com/api/graphql';
|
||||
this.lock = asyncLock();
|
||||
@ -127,9 +113,6 @@ export default class GitLab implements Implementation {
|
||||
branch: this.branch,
|
||||
repo: this.repo,
|
||||
apiRoot: this.apiRoot,
|
||||
squashMerges: this.squashMerges,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
||||
useGraphQL: this.useGraphQL,
|
||||
graphQLAPIRoot: this.graphQLAPIRoot,
|
||||
});
|
||||
@ -334,123 +317,4 @@ export default class GitLab implements Implementation {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
loadMediaFile(branch: string, file: UnpublishedEntryMediaFile) {
|
||||
const readFile = (
|
||||
path: string,
|
||||
id: string | null | undefined,
|
||||
{ parseText }: { parseText: boolean },
|
||||
) => this.api!.readFile(path, id, { branch, parseText });
|
||||
|
||||
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
||||
const name = basename(file.path);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.path,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
path: file.path,
|
||||
name,
|
||||
size: fileObj.size,
|
||||
file: fileObj,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async loadEntryMediaFiles(branch: string, files: UnpublishedEntryMediaFile[]) {
|
||||
const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
|
||||
|
||||
return mediaFiles;
|
||||
}
|
||||
|
||||
async unpublishedEntries() {
|
||||
const listEntriesKeys = () =>
|
||||
this.api!.listUnpublishedBranches().then(branches =>
|
||||
branches.map(branch => contentKeyFromBranch(branch)),
|
||||
);
|
||||
|
||||
const ids = await unpublishedEntries(listEntriesKeys);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
if (id) {
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(id);
|
||||
return data;
|
||||
} else if (collection && slug) {
|
||||
const entryId = generateContentKey(collection, slug);
|
||||
const data = await this.api!.retrieveUnpublishedEntryData(entryId);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error('Missing unpublished entry id or collection and slug');
|
||||
}
|
||||
}
|
||||
|
||||
getBranch(collection: string, slug: string) {
|
||||
const contentKey = generateContentKey(collection, slug);
|
||||
const branch = branchFromContentKey(contentKey);
|
||||
return branch;
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const data = (await this.api!.readFile(path, id, { branch })) as string;
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const branch = this.getBranch(collection, slug);
|
||||
const mediaFile = await this.loadMediaFile(branch, { path, id });
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
async updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
// updateUnpublishedEntryStatus is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.updateUnpublishedEntryStatus(collection, slug, newStatus),
|
||||
'Failed to acquire update entry status lock',
|
||||
);
|
||||
}
|
||||
|
||||
async deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
// deleteUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.deleteUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire delete entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async publishUnpublishedEntry(collection: string, slug: string) {
|
||||
// publishUnpublishedEntry is a transactional operation
|
||||
return runWithLock(
|
||||
this.lock,
|
||||
() => this.api!.publishUnpublishedEntry(collection, slug),
|
||||
'Failed to acquire publish entry lock',
|
||||
);
|
||||
}
|
||||
|
||||
async getDeployPreview(collection: string, slug: string) {
|
||||
try {
|
||||
const statuses = await this.api!.getStatuses(collection, slug);
|
||||
const deployStatus = getPreviewStatus(statuses, this.previewContext);
|
||||
|
||||
if (deployStatus) {
|
||||
const { target_url: url, state } = deployStatus;
|
||||
return { url, status: state };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,12 @@
|
||||
import {
|
||||
EditorialWorkflowError,
|
||||
APIError,
|
||||
unsentRequest,
|
||||
blobToFileObj,
|
||||
APIError, blobToFileObj, unsentRequest
|
||||
} from '../../lib/util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type {
|
||||
Entry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
User,
|
||||
Config,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
UnpublishedEntry,
|
||||
AssetProxy, Config, Entry, Implementation,
|
||||
ImplementationFile, PersistOptions,
|
||||
User
|
||||
} from '../../lib/util';
|
||||
|
||||
async function serializeAsset(assetProxy: AssetProxy) {
|
||||
@ -50,9 +42,8 @@ function deserializeMediaFile({ id, content, encoding, path, name }: MediaFile)
|
||||
export default class ProxyBackend implements Implementation {
|
||||
proxyUrl: string;
|
||||
mediaFolder: string;
|
||||
options: { initialWorkflowStatus?: string };
|
||||
options: {};
|
||||
branch: string;
|
||||
cmsLabelPrefix?: string;
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
if (!config.backend.proxy_url) {
|
||||
@ -63,7 +54,6 @@ export default class ProxyBackend implements Implementation {
|
||||
this.proxyUrl = config.backend.proxy_url;
|
||||
this.mediaFolder = config.media_folder;
|
||||
this.options = options;
|
||||
this.cmsLabelPrefix = config.backend.cms_label_prefix;
|
||||
}
|
||||
|
||||
isGitBackend() {
|
||||
@ -131,60 +121,6 @@ export default class ProxyBackend implements Implementation {
|
||||
});
|
||||
}
|
||||
|
||||
unpublishedEntries() {
|
||||
return this.request({
|
||||
action: 'unpublishedEntries',
|
||||
params: { branch: this.branch },
|
||||
});
|
||||
}
|
||||
|
||||
async unpublishedEntry({
|
||||
id,
|
||||
collection,
|
||||
slug,
|
||||
}: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) {
|
||||
try {
|
||||
const entry: UnpublishedEntry = await this.request({
|
||||
action: 'unpublishedEntry',
|
||||
params: { branch: this.branch, id, collection, slug, cmsLabelPrefix: this.cmsLabelPrefix },
|
||||
});
|
||||
|
||||
return entry;
|
||||
} catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
throw new EditorialWorkflowError('content is not under editorial workflow', true);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string, id: string) {
|
||||
const { data } = await this.request({
|
||||
action: 'unpublishedEntryDataFile',
|
||||
params: { branch: this.branch, collection, slug, path, id },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string, id: string) {
|
||||
const file = await this.request({
|
||||
action: 'unpublishedEntryMediaFile',
|
||||
params: { branch: this.branch, collection, slug, path, id },
|
||||
});
|
||||
return deserializeMediaFile(file);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.request({
|
||||
action: 'deleteUnpublishedEntry',
|
||||
params: { branch: this.branch, collection, slug },
|
||||
});
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions) {
|
||||
const assets = await Promise.all(entry.assets.map(serializeAsset));
|
||||
return this.request({
|
||||
@ -193,32 +129,11 @@ export default class ProxyBackend implements Implementation {
|
||||
branch: this.branch,
|
||||
dataFiles: entry.dataFiles,
|
||||
assets,
|
||||
options: { ...options, status: options.status || this.options.initialWorkflowStatus },
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
options: { ...options },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
return this.request({
|
||||
action: 'updateUnpublishedEntryStatus',
|
||||
params: {
|
||||
branch: this.branch,
|
||||
collection,
|
||||
slug,
|
||||
newStatus,
|
||||
cmsLabelPrefix: this.cmsLabelPrefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.request({
|
||||
action: 'publishUnpublishedEntry',
|
||||
params: { branch: this.branch, collection, slug },
|
||||
});
|
||||
}
|
||||
|
||||
async getMedia(mediaFolder = this.mediaFolder) {
|
||||
const files: MediaFile[] = await this.request({
|
||||
action: 'getMedia',
|
||||
@ -252,11 +167,4 @@ export default class ProxyBackend implements Implementation {
|
||||
params: { branch: this.branch, paths, options: { commitMessage } },
|
||||
});
|
||||
}
|
||||
|
||||
getDeployPreview(collection: string, slug: string) {
|
||||
return this.request({
|
||||
action: 'getDeployPreview',
|
||||
params: { branch: this.branch, collection, slug },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +1,30 @@
|
||||
import { attempt, isError, take, unset, isEmpty } from 'lodash';
|
||||
import { attempt, isError, take, unset } from 'lodash';
|
||||
import { extname } from 'path';
|
||||
import uuid from 'uuid/v4';
|
||||
import { extname, dirname } from 'path';
|
||||
|
||||
import {
|
||||
EditorialWorkflowError,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
basename,
|
||||
} from '../../lib/util';
|
||||
import { basename, Cursor, CURSOR_COMPATIBILITY_SYMBOL } from '../../lib/util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type { ImplementationEntry } from '../../interface';
|
||||
import type {
|
||||
Implementation,
|
||||
Entry,
|
||||
ImplementationEntry,
|
||||
AssetProxy,
|
||||
PersistOptions,
|
||||
User,
|
||||
Config,
|
||||
Entry,
|
||||
Implementation,
|
||||
ImplementationFile,
|
||||
DataFile,
|
||||
User,
|
||||
} from '../../lib/util';
|
||||
|
||||
type RepoFile = { path: string; content: string | AssetProxy };
|
||||
type RepoTree = { [key: string]: RepoFile | RepoTree };
|
||||
|
||||
type Diff = {
|
||||
id: string;
|
||||
originalPath?: string;
|
||||
path: string;
|
||||
newFile: boolean;
|
||||
status: string;
|
||||
content: string | AssetProxy;
|
||||
};
|
||||
|
||||
type UnpublishedRepoEntry = {
|
||||
slug: string;
|
||||
collection: string;
|
||||
status: string;
|
||||
diffs: Diff[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
repoFiles: RepoTree;
|
||||
repoFilesUnpublished: { [key: string]: UnpublishedRepoEntry };
|
||||
}
|
||||
}
|
||||
|
||||
window.repoFiles = window.repoFiles || {};
|
||||
window.repoFilesUnpublished = window.repoFilesUnpublished || [];
|
||||
|
||||
function getFile(path: string, tree: RepoTree) {
|
||||
const segments = path.split('/');
|
||||
@ -126,7 +100,7 @@ export function getFolderFiles(
|
||||
|
||||
export default class TestBackend implements Implementation {
|
||||
mediaFolder: string;
|
||||
options: { initialWorkflowStatus?: string };
|
||||
options: {};
|
||||
|
||||
constructor(config: Config, options = {}) {
|
||||
this.options = options;
|
||||
@ -225,103 +199,7 @@ export default class TestBackend implements Implementation {
|
||||
});
|
||||
}
|
||||
|
||||
unpublishedEntries() {
|
||||
return Promise.resolve(Object.keys(window.repoFilesUnpublished));
|
||||
}
|
||||
|
||||
unpublishedEntry({ id, collection, slug }: { id?: string; collection?: string; slug?: string }) {
|
||||
if (id) {
|
||||
const parts = id.split('/');
|
||||
collection = parts[0];
|
||||
slug = parts[1];
|
||||
}
|
||||
const entry = window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
if (!entry) {
|
||||
return Promise.reject(
|
||||
new EditorialWorkflowError('content is not under editorial workflow', true),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(entry);
|
||||
}
|
||||
|
||||
async unpublishedEntryDataFile(collection: string, slug: string, path: string) {
|
||||
const entry = window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
const file = entry.diffs.find(d => d.path === path);
|
||||
return file?.content as string;
|
||||
}
|
||||
|
||||
async unpublishedEntryMediaFile(collection: string, slug: string, path: string) {
|
||||
const entry = window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
const file = entry.diffs.find(d => d.path === path);
|
||||
return this.normalizeAsset(file?.content as AssetProxy);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
delete window.repoFilesUnpublished[`${collection}/${slug}`];
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async addOrUpdateUnpublishedEntry(
|
||||
key: string,
|
||||
dataFiles: DataFile[],
|
||||
assetProxies: AssetProxy[],
|
||||
slug: string,
|
||||
collection: string,
|
||||
status: string,
|
||||
) {
|
||||
const diffs: Diff[] = [];
|
||||
dataFiles.forEach(dataFile => {
|
||||
const { path, newPath, raw } = dataFile;
|
||||
const currentDataFile = window.repoFilesUnpublished[key]?.diffs.find(d => d.path === path);
|
||||
const originalPath = currentDataFile ? currentDataFile.originalPath : path;
|
||||
diffs.push({
|
||||
originalPath,
|
||||
id: newPath || path,
|
||||
path: newPath || path,
|
||||
newFile: isEmpty(getFile(originalPath as string, window.repoFiles)),
|
||||
status: 'added',
|
||||
content: raw,
|
||||
});
|
||||
});
|
||||
assetProxies.forEach(a => {
|
||||
const asset = this.normalizeAsset(a);
|
||||
diffs.push({
|
||||
id: asset.id,
|
||||
path: asset.path,
|
||||
newFile: true,
|
||||
status: 'added',
|
||||
content: asset,
|
||||
});
|
||||
});
|
||||
window.repoFilesUnpublished[key] = {
|
||||
slug,
|
||||
collection,
|
||||
status,
|
||||
diffs,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry, options: PersistOptions) {
|
||||
if (options.useWorkflow) {
|
||||
const slug = entry.dataFiles[0].slug;
|
||||
const key = `${options.collectionName}/${slug}`;
|
||||
const currentEntry = window.repoFilesUnpublished[key];
|
||||
const status =
|
||||
currentEntry?.status || options.status || (this.options.initialWorkflowStatus as string);
|
||||
|
||||
this.addOrUpdateUnpublishedEntry(
|
||||
key,
|
||||
entry.dataFiles,
|
||||
entry.assets,
|
||||
slug,
|
||||
options.collectionName as string,
|
||||
status,
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async persistEntry(entry: Entry) {
|
||||
entry.dataFiles.forEach(dataFile => {
|
||||
const { path, raw } = dataFile;
|
||||
writeFile(path, raw, window.repoFiles);
|
||||
@ -332,37 +210,6 @@ export default class TestBackend implements Implementation {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
window.repoFilesUnpublished[`${collection}/${slug}`].status = newStatus;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
const key = `${collection}/${slug}`;
|
||||
const unpubEntry = window.repoFilesUnpublished[key];
|
||||
|
||||
delete window.repoFilesUnpublished[key];
|
||||
|
||||
const tree = window.repoFiles;
|
||||
unpubEntry.diffs.forEach(d => {
|
||||
if (d.originalPath && !d.newFile) {
|
||||
const originalPath = d.originalPath;
|
||||
const sourceDir = dirname(originalPath);
|
||||
const destDir = dirname(d.path);
|
||||
const toMove = getFolderFiles(tree, originalPath.split('/')[0], '', 100).filter(f =>
|
||||
f.path.startsWith(sourceDir),
|
||||
);
|
||||
toMove.forEach(f => {
|
||||
deleteFile(f.path, tree);
|
||||
writeFile(f.path.replace(sourceDir, destDir), f.content, tree);
|
||||
});
|
||||
}
|
||||
writeFile(d.path, d.content, tree);
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getMedia(mediaFolder = this.mediaFolder) {
|
||||
const files = getFolderFiles(window.repoFiles, mediaFolder.split('/')[0], '', 100).filter(f =>
|
||||
f.path.startsWith(mediaFolder),
|
||||
@ -423,8 +270,4 @@ export default class TestBackend implements Implementation {
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getDeployPreview() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import { loginUser, logoutUser } from '../../actions/auth';
|
||||
import { createNewEntry } from '../../actions/collections';
|
||||
import { openMediaLibrary } from '../../actions/mediaLibrary';
|
||||
import { currentBackend } from '../../backend';
|
||||
import { EDITORIAL_WORKFLOW, SIMPLE } from '../../constants/publishModes';
|
||||
import { history } from '../../routing/history';
|
||||
import { colors, Loader } from '../../ui';
|
||||
import Collection from '../Collection/Collection';
|
||||
@ -23,7 +22,6 @@ import Page from '../page/Page';
|
||||
import Snackbars from '../snackbar/Snackbars';
|
||||
import { Alert } from '../UI/Alert';
|
||||
import { Confirm } from '../UI/Confirm';
|
||||
import Workflow from '../Workflow/Workflow';
|
||||
import Header from './Header';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
|
||||
@ -142,7 +140,6 @@ class App extends React.Component {
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
user: PropTypes.object,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
publishMode: PropTypes.oneOf([SIMPLE, EDITORIAL_WORKFLOW]),
|
||||
siteId: PropTypes.string,
|
||||
useMediaLibrary: PropTypes.bool,
|
||||
openMediaLibrary: PropTypes.func.isRequired,
|
||||
@ -210,7 +207,6 @@ class App extends React.Component {
|
||||
collections,
|
||||
logoutUser,
|
||||
isFetching,
|
||||
publishMode,
|
||||
useMediaLibrary,
|
||||
openMediaLibrary,
|
||||
t,
|
||||
@ -235,7 +231,6 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
const defaultPath = getDefaultPath(collections);
|
||||
const hasWorkflow = publishMode === EDITORIAL_WORKFLOW;
|
||||
|
||||
return (
|
||||
<ScrollSync enabled={scrollSyncEnabled}>
|
||||
@ -248,7 +243,6 @@ class App extends React.Component {
|
||||
onCreateEntryClick={createNewEntry}
|
||||
onLogoutClick={logoutUser}
|
||||
openMediaLibrary={openMediaLibrary}
|
||||
hasWorkflow={hasWorkflow}
|
||||
displayUrl={config.display_url}
|
||||
isTestRepo={config.backend.name === 'test-repo'}
|
||||
showMediaButton={showMediaButton}
|
||||
@ -270,7 +264,6 @@ class App extends React.Component {
|
||||
from="/error=access_denied&error_description=Signups+not+allowed+for+this+instance"
|
||||
to={defaultPath}
|
||||
/>
|
||||
{hasWorkflow ? <Route path="/workflow" component={Workflow} /> : null}
|
||||
<RouteInCollectionDefault
|
||||
exact
|
||||
collections={collections}
|
||||
@ -327,7 +320,6 @@ function mapStateToProps(state) {
|
||||
const { auth, config, collections, globalUI, mediaLibrary, scroll } = state;
|
||||
const user = auth.user;
|
||||
const isFetching = globalUI.isFetching;
|
||||
const publishMode = config.publish_mode;
|
||||
const useMediaLibrary = !mediaLibrary.get('externalLibrary');
|
||||
const showMediaButton = mediaLibrary.get('showMediaButton');
|
||||
const scrollSyncEnabled = scroll.isScrolling;
|
||||
@ -337,7 +329,6 @@ function mapStateToProps(state) {
|
||||
collections,
|
||||
user,
|
||||
isFetching,
|
||||
publishMode,
|
||||
showMediaButton,
|
||||
useMediaLibrary,
|
||||
scrollSyncEnabled,
|
||||
|
@ -121,7 +121,6 @@ class Header extends React.Component {
|
||||
onCreateEntryClick: PropTypes.func.isRequired,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
openMediaLibrary: PropTypes.func.isRequired,
|
||||
hasWorkflow: PropTypes.bool.isRequired,
|
||||
displayUrl: PropTypes.string,
|
||||
isTestRepo: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
@ -153,7 +152,6 @@ class Header extends React.Component {
|
||||
collections,
|
||||
onLogoutClick,
|
||||
openMediaLibrary,
|
||||
hasWorkflow,
|
||||
displayUrl,
|
||||
isTestRepo,
|
||||
t,
|
||||
@ -179,14 +177,6 @@ class Header extends React.Component {
|
||||
{t('app.header.content')}
|
||||
</AppHeaderNavLink>
|
||||
</li>
|
||||
{hasWorkflow && (
|
||||
<li>
|
||||
<AppHeaderNavLink to="/workflow" activeClassName="header-link-active">
|
||||
<Icon type="workflow" />
|
||||
{t('app.header.workflow')}
|
||||
</AppHeaderNavLink>
|
||||
</li>
|
||||
)}
|
||||
{showMediaButton && (
|
||||
<li>
|
||||
<AppHeaderButton onClick={openMediaLibrary}>
|
||||
|
@ -6,13 +6,6 @@ import { translate } from 'react-polyglot';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { logoutUser } from '../../actions/auth';
|
||||
import { loadDeployPreview } from '../../actions/deploys';
|
||||
import {
|
||||
deleteUnpublishedEntry,
|
||||
publishUnpublishedEntry,
|
||||
unpublishPublishedEntry,
|
||||
updateUnpublishedEntryStatus,
|
||||
} from '../../actions/editorialWorkflow';
|
||||
import {
|
||||
changeDraftField,
|
||||
changeDraftFieldValidation,
|
||||
@ -29,15 +22,12 @@ import {
|
||||
retrieveLocalBackup,
|
||||
} from '../../actions/entries';
|
||||
import { loadScroll, toggleScroll } from '../../actions/scroll';
|
||||
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
||||
import { selectDeployPreview, selectEntry, selectUnpublishedEntry } from '../../reducers';
|
||||
import { selectEntry } from '../../reducers';
|
||||
import { selectFields } from '../../reducers/collections';
|
||||
import { history, navigateToCollection, navigateToNewEntry } from '../../routing/history';
|
||||
import { Loader } from '../../ui';
|
||||
import alert from '../UI/Alert';
|
||||
import confirm from '../UI/Confirm';
|
||||
import EditorInterface from './EditorInterface';
|
||||
import withWorkflow from './withWorkflow';
|
||||
|
||||
export class Editor extends React.Component {
|
||||
static propTypes = {
|
||||
@ -57,18 +47,10 @@ export class Editor extends React.Component {
|
||||
slug: PropTypes.string,
|
||||
newEntry: PropTypes.bool.isRequired,
|
||||
displayUrl: PropTypes.string,
|
||||
hasWorkflow: PropTypes.bool,
|
||||
useOpenAuthoring: PropTypes.bool,
|
||||
unpublishedEntry: PropTypes.bool,
|
||||
isModification: PropTypes.bool,
|
||||
collectionEntriesLoaded: PropTypes.bool,
|
||||
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
|
||||
publishUnpublishedEntry: PropTypes.func.isRequired,
|
||||
deleteUnpublishedEntry: PropTypes.func.isRequired,
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
loadEntries: PropTypes.func.isRequired,
|
||||
deployPreview: PropTypes.object,
|
||||
loadDeployPreview: PropTypes.func.isRequired,
|
||||
currentStatus: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
location: PropTypes.shape({
|
||||
@ -170,7 +152,7 @@ export class Editor extends React.Component {
|
||||
}
|
||||
|
||||
async checkLocalBackup(prevProps) {
|
||||
const { t, hasChanged, localBackup, loadLocalBackup, entryDraft, collection } = this.props;
|
||||
const { hasChanged, localBackup, loadLocalBackup, entryDraft, collection } = this.props;
|
||||
|
||||
if (!prevProps.localBackup && localBackup) {
|
||||
const confirmLoadBackup = await confirm({
|
||||
@ -214,24 +196,10 @@ export class Editor extends React.Component {
|
||||
}, 2000);
|
||||
|
||||
handleChangeDraftField = (field, value, metadata, i18n) => {
|
||||
const entries = [this.props.unPublishedEntry, this.props.publishedEntry].filter(Boolean);
|
||||
const entries = [this.props.publishedEntry].filter(Boolean);
|
||||
this.props.changeDraftField({ field, value, metadata, entries, i18n });
|
||||
};
|
||||
|
||||
handleChangeStatus = newStatusName => {
|
||||
const { entryDraft, updateUnpublishedEntryStatus, collection, slug, currentStatus } =
|
||||
this.props;
|
||||
if (entryDraft.get('hasChanged')) {
|
||||
alert({
|
||||
title: 'editor.editor.onUpdatingWithUnsavedChangesTitle',
|
||||
body: 'editor.editor.onUpdatingWithUnsavedChangesBody',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newStatus = status.get(newStatusName);
|
||||
updateUnpublishedEntryStatus(collection.get('name'), slug, currentStatus, newStatus);
|
||||
};
|
||||
|
||||
deleteBackup() {
|
||||
const { deleteLocalBackup, collection, slug, newEntry } = this.props;
|
||||
this.createBackup.cancel();
|
||||
@ -243,10 +211,6 @@ export class Editor extends React.Component {
|
||||
const {
|
||||
persistEntry,
|
||||
collection,
|
||||
currentStatus,
|
||||
hasWorkflow,
|
||||
loadEntry,
|
||||
slug,
|
||||
createDraftDuplicateFromEntry,
|
||||
entryDraft,
|
||||
} = this.props;
|
||||
@ -258,70 +222,9 @@ export class Editor extends React.Component {
|
||||
if (createNew) {
|
||||
navigateToNewEntry(collection.get('name'));
|
||||
duplicate && createDraftDuplicateFromEntry(entryDraft.get('entry'));
|
||||
} else if (slug && hasWorkflow && !currentStatus) {
|
||||
loadEntry(collection, slug);
|
||||
}
|
||||
};
|
||||
|
||||
handlePublishEntry = async (opts = {}) => {
|
||||
const { createNew = false, duplicate = false } = opts;
|
||||
const {
|
||||
publishUnpublishedEntry,
|
||||
createDraftDuplicateFromEntry,
|
||||
entryDraft,
|
||||
collection,
|
||||
slug,
|
||||
currentStatus,
|
||||
} = this.props;
|
||||
if (currentStatus !== status.last()) {
|
||||
alert({
|
||||
title: 'editor.editor.onPublishingNotReadyTitle',
|
||||
body: 'editor.editor.onPublishingNotReadyBody',
|
||||
});
|
||||
return;
|
||||
} else if (entryDraft.get('hasChanged')) {
|
||||
alert({
|
||||
title: 'editor.editor.onPublishingWithUnsavedChangesTitle',
|
||||
body: 'editor.editor.onPublishingWithUnsavedChangesBody',
|
||||
});
|
||||
return;
|
||||
} else if (
|
||||
!(await confirm({
|
||||
title: 'editor.editor.onPublishingTitle',
|
||||
body: 'editor.editor.onPublishingBody',
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await publishUnpublishedEntry(collection.get('name'), slug);
|
||||
|
||||
this.deleteBackup();
|
||||
|
||||
if (createNew) {
|
||||
navigateToNewEntry(collection.get('name'));
|
||||
}
|
||||
|
||||
duplicate && createDraftDuplicateFromEntry(entryDraft.get('entry'));
|
||||
};
|
||||
|
||||
handleUnpublishEntry = async () => {
|
||||
const { unpublishPublishedEntry, collection, slug } = this.props;
|
||||
if (
|
||||
!(await confirm({
|
||||
title: 'editor.editor.onUnpublishingTitle',
|
||||
body: 'editor.editor.onUnpublishingBody',
|
||||
color: 'error',
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await unpublishPublishedEntry(collection, slug);
|
||||
|
||||
return navigateToCollection(collection.get('name'));
|
||||
};
|
||||
|
||||
handleDuplicateEntry = () => {
|
||||
const { createDraftDuplicateFromEntry, collection, entryDraft } = this.props;
|
||||
|
||||
@ -362,38 +265,6 @@ export class Editor extends React.Component {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
handleDeleteUnpublishedChanges = async () => {
|
||||
const { entryDraft, collection, slug, deleteUnpublishedEntry, loadEntry, isModification } =
|
||||
this.props;
|
||||
if (
|
||||
entryDraft.get('hasChanged') &&
|
||||
!(await confirm({
|
||||
title: 'editor.editor.onDeleteUnpublishedChangesWithUnsavedChangesTitle',
|
||||
body: 'editor.editor.onDeleteUnpublishedChangesWithUnsavedChangesBody',
|
||||
color: 'error',
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
!(await confirm({
|
||||
title: 'editor.editor.onDeleteUnpublishedChangesTitle',
|
||||
body: 'editor.editor.onDeleteUnpublishedChangesBody',
|
||||
color: 'error',
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await deleteUnpublishedEntry(collection.get('name'), slug);
|
||||
|
||||
this.deleteBackup();
|
||||
|
||||
if (isModification) {
|
||||
loadEntry(collection, slug);
|
||||
} else {
|
||||
navigateToCollection(collection.get('name'));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
entry,
|
||||
@ -404,17 +275,11 @@ export class Editor extends React.Component {
|
||||
user,
|
||||
hasChanged,
|
||||
displayUrl,
|
||||
hasWorkflow,
|
||||
useOpenAuthoring,
|
||||
unpublishedEntry,
|
||||
newEntry,
|
||||
isModification,
|
||||
currentStatus,
|
||||
logoutUser,
|
||||
deployPreview,
|
||||
loadDeployPreview,
|
||||
draftKey,
|
||||
slug,
|
||||
t,
|
||||
editorBackLink,
|
||||
toggleScroll,
|
||||
@ -422,8 +287,6 @@ export class Editor extends React.Component {
|
||||
loadScroll,
|
||||
} = this.props;
|
||||
|
||||
const isPublished = !newEntry && !unpublishedEntry;
|
||||
|
||||
if (entry && entry.get('error')) {
|
||||
return (
|
||||
<div>
|
||||
@ -450,24 +313,17 @@ export class Editor extends React.Component {
|
||||
onValidate={changeDraftFieldValidation}
|
||||
onPersist={this.handlePersistEntry}
|
||||
onDelete={this.handleDeleteEntry}
|
||||
onDeleteUnpublishedChanges={this.handleDeleteUnpublishedChanges}
|
||||
onChangeStatus={this.handleChangeStatus}
|
||||
onPublish={this.handlePublishEntry}
|
||||
unPublish={this.handleUnpublishEntry}
|
||||
onDuplicate={this.handleDuplicateEntry}
|
||||
showDelete={this.props.showDelete}
|
||||
user={user}
|
||||
hasChanged={hasChanged}
|
||||
displayUrl={displayUrl}
|
||||
hasWorkflow={hasWorkflow}
|
||||
useOpenAuthoring={useOpenAuthoring}
|
||||
hasUnpublishedChanges={unpublishedEntry}
|
||||
isNewEntry={newEntry}
|
||||
isModification={isModification}
|
||||
currentStatus={currentStatus}
|
||||
onLogoutClick={logoutUser}
|
||||
deployPreview={deployPreview}
|
||||
loadDeployPreview={opts => loadDeployPreview(collection, slug, entry, isPublished, opts)}
|
||||
editorBackLink={editorBackLink}
|
||||
toggleScroll={toggleScroll}
|
||||
scrollSyncEnabled={scrollSyncEnabled}
|
||||
@ -479,7 +335,7 @@ export class Editor extends React.Component {
|
||||
}
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const { collections, entryDraft, auth, config, entries, globalUI, scroll } = state;
|
||||
const { collections, entryDraft, auth, config, entries, scroll } = state;
|
||||
const slug = ownProps.match.params[0];
|
||||
const collection = collections.get(ownProps.match.params.name);
|
||||
const collectionName = collection.get('name');
|
||||
@ -489,21 +345,12 @@ function mapStateToProps(state, ownProps) {
|
||||
const user = auth.user;
|
||||
const hasChanged = entryDraft.get('hasChanged');
|
||||
const displayUrl = config.display_url;
|
||||
const hasWorkflow = config.publish_mode === EDITORIAL_WORKFLOW;
|
||||
const useOpenAuthoring = globalUI.useOpenAuthoring;
|
||||
const isModification = entryDraft.getIn(['entry', 'isModification']);
|
||||
const collectionEntriesLoaded = !!entries.getIn(['pages', collectionName]);
|
||||
const unPublishedEntry = selectUnpublishedEntry(state, collectionName, slug);
|
||||
const publishedEntry = selectEntry(state, collectionName, slug);
|
||||
const currentStatus = unPublishedEntry && unPublishedEntry.get('status');
|
||||
const deployPreview = selectDeployPreview(state, collectionName, slug);
|
||||
const localBackup = entryDraft.get('localBackup');
|
||||
const draftKey = entryDraft.get('key');
|
||||
let editorBackLink = `/collections/${collectionName}`;
|
||||
if (new URLSearchParams(ownProps.location.search).get('ref') === 'workflow') {
|
||||
editorBackLink = `/workflow`;
|
||||
}
|
||||
|
||||
if (collection.has('files') && collection.get('files').size === 1) {
|
||||
editorBackLink = '/';
|
||||
}
|
||||
@ -528,16 +375,11 @@ function mapStateToProps(state, ownProps) {
|
||||
user,
|
||||
hasChanged,
|
||||
displayUrl,
|
||||
hasWorkflow,
|
||||
useOpenAuthoring,
|
||||
isModification,
|
||||
collectionEntriesLoaded,
|
||||
currentStatus,
|
||||
deployPreview,
|
||||
localBackup,
|
||||
draftKey,
|
||||
publishedEntry,
|
||||
unPublishedEntry,
|
||||
editorBackLink,
|
||||
scrollSyncEnabled,
|
||||
};
|
||||
@ -548,7 +390,6 @@ const mapDispatchToProps = {
|
||||
changeDraftFieldValidation,
|
||||
loadEntry,
|
||||
loadEntries,
|
||||
loadDeployPreview,
|
||||
loadLocalBackup,
|
||||
retrieveLocalBackup,
|
||||
persistLocalBackup,
|
||||
@ -558,13 +399,9 @@ const mapDispatchToProps = {
|
||||
discardDraft,
|
||||
persistEntry,
|
||||
deleteEntry,
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry,
|
||||
unpublishPublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
logoutUser,
|
||||
toggleScroll,
|
||||
loadScroll,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withWorkflow(translate()(Editor)));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(translate()(Editor));
|
||||
|
@ -108,7 +108,7 @@ export default class ControlPane extends React.Component {
|
||||
};
|
||||
|
||||
copyFromOtherLocale =
|
||||
({ targetLocale, t }) =>
|
||||
({ targetLocale }) =>
|
||||
async sourceLocale => {
|
||||
if (
|
||||
!(await confirm({
|
||||
|
@ -206,24 +206,17 @@ class EditorInterface extends Component {
|
||||
onChange,
|
||||
showDelete,
|
||||
onDelete,
|
||||
onDeleteUnpublishedChanges,
|
||||
onChangeStatus,
|
||||
onPublish,
|
||||
unPublish,
|
||||
onDuplicate,
|
||||
onValidate,
|
||||
user,
|
||||
hasChanged,
|
||||
displayUrl,
|
||||
hasWorkflow,
|
||||
useOpenAuthoring,
|
||||
hasUnpublishedChanges,
|
||||
isNewEntry,
|
||||
isModification,
|
||||
currentStatus,
|
||||
onLogoutClick,
|
||||
loadDeployPreview,
|
||||
deployPreview,
|
||||
draftKey,
|
||||
editorBackLink,
|
||||
scrollSyncEnabled,
|
||||
@ -311,11 +304,9 @@ class EditorInterface extends Component {
|
||||
onPersistAndNew={() => this.handleOnPersist({ createNew: true })}
|
||||
onPersistAndDuplicate={() => this.handleOnPersist({ createNew: true, duplicate: true })}
|
||||
onDelete={onDelete}
|
||||
onDeleteUnpublishedChanges={onDeleteUnpublishedChanges}
|
||||
onChangeStatus={onChangeStatus}
|
||||
showDelete={showDelete}
|
||||
onPublish={onPublish}
|
||||
unPublish={unPublish}
|
||||
onDuplicate={onDuplicate}
|
||||
onPublishAndNew={() => this.handleOnPublish({ createNew: true })}
|
||||
onPublishAndDuplicate={() => this.handleOnPublish({ createNew: true, duplicate: true })}
|
||||
@ -323,15 +314,10 @@ class EditorInterface extends Component {
|
||||
hasChanged={hasChanged}
|
||||
displayUrl={displayUrl}
|
||||
collection={collection}
|
||||
hasWorkflow={hasWorkflow}
|
||||
useOpenAuthoring={useOpenAuthoring}
|
||||
hasUnpublishedChanges={hasUnpublishedChanges}
|
||||
isNewEntry={isNewEntry}
|
||||
isModification={isModification}
|
||||
currentStatus={currentStatus}
|
||||
onLogoutClick={onLogoutClick}
|
||||
loadDeployPreview={loadDeployPreview}
|
||||
deployPreview={deployPreview}
|
||||
editorBackLink={editorBackLink}
|
||||
/>
|
||||
<Editor key={draftKey}>
|
||||
@ -389,23 +375,16 @@ EditorInterface.propTypes = {
|
||||
onPersist: PropTypes.func.isRequired,
|
||||
showDelete: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
unPublish: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onChangeStatus: PropTypes.func.isRequired,
|
||||
user: PropTypes.object,
|
||||
hasChanged: PropTypes.bool,
|
||||
displayUrl: PropTypes.string,
|
||||
hasWorkflow: PropTypes.bool,
|
||||
useOpenAuthoring: PropTypes.bool,
|
||||
hasUnpublishedChanges: PropTypes.bool,
|
||||
isNewEntry: PropTypes.bool,
|
||||
isModification: PropTypes.bool,
|
||||
currentStatus: PropTypes.string,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
deployPreview: PropTypes.object,
|
||||
loadDeployPreview: PropTypes.func.isRequired,
|
||||
draftKey: PropTypes.string.isRequired,
|
||||
toggleScroll: PropTypes.func.isRequired,
|
||||
scrollSyncEnabled: PropTypes.bool.isRequired,
|
||||
|
@ -1,11 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||
/* eslint-disable func-style */
|
||||
import styled from '@emotion/styled';
|
||||
import { CmsWidgetPreviewProps } from '../../../interface';
|
||||
import React, { ComponentType, ReactNode, useMemo } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
|
||||
import type { CmsWidgetPreviewProps } from '../../../interface';
|
||||
|
||||
interface PreviewContentProps {
|
||||
previewComponent?:
|
||||
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
|
||||
|
@ -17,7 +17,6 @@ import {
|
||||
buttons,
|
||||
zIndex,
|
||||
} from '../../ui';
|
||||
import { status } from '../../constants/publishModes';
|
||||
import { SettingsDropdown } from '../UI';
|
||||
|
||||
const styles = {
|
||||
@ -110,10 +109,6 @@ const ToolbarSubSectionFirst = styled.div`
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ToolbarSubSectionLast = styled(ToolbarSubSectionFirst)`
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const ToolbarSectionBackLink = styled(Link)`
|
||||
${styles.toolbarSection};
|
||||
border-right-width: 1px;
|
||||
@ -180,13 +175,6 @@ const DeleteButton = styled(ToolbarButton)`
|
||||
${buttons.lightRed};
|
||||
`;
|
||||
|
||||
const SaveButton = styled(ToolbarButton)`
|
||||
${buttons.lightBlue};
|
||||
&[disabled] {
|
||||
${buttons.disabled};
|
||||
}
|
||||
`;
|
||||
|
||||
const PublishedToolbarButton = styled(DropdownButton)`
|
||||
${styles.publishedButton}
|
||||
`;
|
||||
@ -199,47 +187,6 @@ const PublishButton = styled(DropdownButton)`
|
||||
background-color: ${colorsRaw.teal};
|
||||
`;
|
||||
|
||||
const StatusButton = styled(DropdownButton)`
|
||||
background-color: ${colorsRaw.tealLight};
|
||||
color: ${colorsRaw.teal};
|
||||
`;
|
||||
|
||||
const PreviewButtonContainer = styled.div`
|
||||
margin-right: 12px;
|
||||
color: ${colorsRaw.blue};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a,
|
||||
${Icon} {
|
||||
color: ${colorsRaw.blue};
|
||||
}
|
||||
|
||||
${Icon} {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const RefreshPreviewButton = styled.button`
|
||||
background: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
color: ${colorsRaw.blue};
|
||||
|
||||
span {
|
||||
margin-right: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PreviewLink = RefreshPreviewButton.withComponent('a');
|
||||
|
||||
const StatusDropdownItem = styled(DropdownItem)`
|
||||
${Icon} {
|
||||
color: ${colors.infoText};
|
||||
}
|
||||
`;
|
||||
|
||||
export class EditorToolbar extends React.Component {
|
||||
static propTypes = {
|
||||
isPersisting: PropTypes.bool,
|
||||
@ -251,10 +198,8 @@ export class EditorToolbar extends React.Component {
|
||||
onPersistAndDuplicate: PropTypes.func.isRequired,
|
||||
showDelete: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
||||
onChangeStatus: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
unPublish: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onPublishAndNew: PropTypes.func.isRequired,
|
||||
onPublishAndDuplicate: PropTypes.func.isRequired,
|
||||
@ -262,33 +207,14 @@ export class EditorToolbar extends React.Component {
|
||||
hasChanged: PropTypes.bool,
|
||||
displayUrl: PropTypes.string,
|
||||
collection: ImmutablePropTypes.map.isRequired,
|
||||
hasWorkflow: PropTypes.bool,
|
||||
useOpenAuthoring: PropTypes.bool,
|
||||
hasUnpublishedChanges: PropTypes.bool,
|
||||
isNewEntry: PropTypes.bool,
|
||||
isModification: PropTypes.bool,
|
||||
currentStatus: PropTypes.string,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
deployPreview: PropTypes.object,
|
||||
loadDeployPreview: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
editorBackLink: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { isNewEntry, loadDeployPreview } = this.props;
|
||||
if (!isNewEntry) {
|
||||
loadDeployPreview({ maxAttempts: 3 });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { isNewEntry, isPersisting, loadDeployPreview } = this.props;
|
||||
if (!isNewEntry && prevProps.isPersisting && !isPersisting) {
|
||||
loadDeployPreview({ maxAttempts: 3 });
|
||||
}
|
||||
}
|
||||
|
||||
renderSimpleControls = () => {
|
||||
const { collection, hasChanged, isNewEntry, showDelete, onDelete, t } = this.props;
|
||||
const canCreate = collection.get('create');
|
||||
@ -309,37 +235,6 @@ export class EditorToolbar extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
renderDeployPreviewControls = label => {
|
||||
const { deployPreview = {}, loadDeployPreview, t } = this.props;
|
||||
const { url, status, isFetching } = deployPreview;
|
||||
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deployPreviewReady = status === 'SUCCESS' && !isFetching;
|
||||
return (
|
||||
<PreviewButtonContainer>
|
||||
{deployPreviewReady ? (
|
||||
<PreviewLink
|
||||
key="preview-ready-button"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={url}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<Icon type="new-tab" size="xsmall" />
|
||||
</PreviewLink>
|
||||
) : (
|
||||
<RefreshPreviewButton key="preview-pending-button" onClick={loadDeployPreview}>
|
||||
<span>{t('editor.editorToolbar.deployPreviewPendingButtonLabel')}</span>
|
||||
<Icon type="refresh" size="xsmall" />
|
||||
</RefreshPreviewButton>
|
||||
)}
|
||||
</PreviewButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
renderStatusInfoTooltip = () => {
|
||||
const { t, currentStatus } = this.props;
|
||||
|
||||
@ -363,131 +258,6 @@ export class EditorToolbar extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
renderWorkflowStatusControls = () => {
|
||||
const { isUpdatingStatus, onChangeStatus, currentStatus, t, useOpenAuthoring } = this.props;
|
||||
|
||||
const statusToTranslation = {
|
||||
[status.get('DRAFT')]: t('editor.editorToolbar.draft'),
|
||||
[status.get('PENDING_REVIEW')]: t('editor.editorToolbar.inReview'),
|
||||
[status.get('PENDING_PUBLISH')]: t('editor.editorToolbar.ready'),
|
||||
};
|
||||
|
||||
const buttonText = isUpdatingStatus
|
||||
? t('editor.editorToolbar.updating')
|
||||
: t('editor.editorToolbar.status', { status: statusToTranslation[currentStatus] });
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarDropdown
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="120px"
|
||||
renderButton={() => <StatusButton>{buttonText}</StatusButton>}
|
||||
>
|
||||
<StatusDropdownItem
|
||||
label={t('editor.editorToolbar.draft')}
|
||||
onClick={() => onChangeStatus('DRAFT')}
|
||||
icon={currentStatus === status.get('DRAFT') ? 'check' : null}
|
||||
/>
|
||||
<StatusDropdownItem
|
||||
label={t('editor.editorToolbar.inReview')}
|
||||
onClick={() => onChangeStatus('PENDING_REVIEW')}
|
||||
icon={currentStatus === status.get('PENDING_REVIEW') ? 'check' : null}
|
||||
/>
|
||||
{useOpenAuthoring ? (
|
||||
''
|
||||
) : (
|
||||
<StatusDropdownItem
|
||||
key="workflow-status-pending-publish"
|
||||
label={t('editor.editorToolbar.ready')}
|
||||
onClick={() => onChangeStatus('PENDING_PUBLISH')}
|
||||
icon={currentStatus === status.get('PENDING_PUBLISH') ? 'check' : null}
|
||||
/>
|
||||
)}
|
||||
</ToolbarDropdown>
|
||||
{useOpenAuthoring && this.renderStatusInfoTooltip()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderNewEntryWorkflowPublishControls = ({ canCreate, canPublish }) => {
|
||||
const { isPublishing, onPublish, onPublishAndNew, onPublishAndDuplicate, t } = this.props;
|
||||
|
||||
return canPublish ? (
|
||||
<ToolbarDropdown
|
||||
key="workflow-new-publish-controls"
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
renderButton={() => (
|
||||
<PublishButton>
|
||||
{isPublishing
|
||||
? t('editor.editorToolbar.publishing')
|
||||
: t('editor.editorToolbar.publish')}
|
||||
</PublishButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.publishNow')}
|
||||
icon="arrow"
|
||||
iconDirection="right"
|
||||
onClick={onPublish}
|
||||
/>
|
||||
{canCreate ? (
|
||||
<>
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.publishAndCreateNew')}
|
||||
icon="add"
|
||||
onClick={onPublishAndNew}
|
||||
/>
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.publishAndDuplicate')}
|
||||
icon="add"
|
||||
onClick={onPublishAndDuplicate}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</ToolbarDropdown>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
renderExistingEntryWorkflowPublishControls = ({ canCreate, canPublish, canDelete }) => {
|
||||
const { unPublish, onDuplicate, isPersisting, t } = this.props;
|
||||
|
||||
return canPublish || canCreate ? (
|
||||
<ToolbarDropdown
|
||||
key="workflow-existing-publish-controls"
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
renderButton={() => (
|
||||
<PublishedToolbarButton>
|
||||
{isPersisting
|
||||
? t('editor.editorToolbar.unpublishing')
|
||||
: t('editor.editorToolbar.published')}
|
||||
</PublishedToolbarButton>
|
||||
)}
|
||||
>
|
||||
{canDelete && canPublish && (
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.unpublish')}
|
||||
icon="arrow"
|
||||
iconDirection="right"
|
||||
onClick={unPublish}
|
||||
/>
|
||||
)}
|
||||
{canCreate && (
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.duplicate')}
|
||||
icon="add"
|
||||
onClick={onDuplicate}
|
||||
/>
|
||||
)}
|
||||
</ToolbarDropdown>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
renderExistingEntrySimplePublishControls = ({ canCreate }) => {
|
||||
const { onDuplicate, t } = this.props;
|
||||
return canCreate ? (
|
||||
@ -553,93 +323,12 @@ export class EditorToolbar extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
renderSimpleDeployPreviewControls = () => {
|
||||
const { hasChanged, isNewEntry, t } = this.props;
|
||||
|
||||
if (!isNewEntry && !hasChanged) {
|
||||
return this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'));
|
||||
}
|
||||
};
|
||||
|
||||
renderWorkflowControls = () => {
|
||||
const {
|
||||
onPersist,
|
||||
onDelete,
|
||||
onDeleteUnpublishedChanges,
|
||||
showDelete,
|
||||
hasChanged,
|
||||
hasUnpublishedChanges,
|
||||
useOpenAuthoring,
|
||||
isPersisting,
|
||||
isDeleting,
|
||||
isNewEntry,
|
||||
isModification,
|
||||
currentStatus,
|
||||
collection,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
const canCreate = collection.get('create');
|
||||
const canPublish = collection.get('publish') && !useOpenAuthoring;
|
||||
const canDelete = collection.get('delete', true);
|
||||
|
||||
const deleteLabel =
|
||||
(hasUnpublishedChanges &&
|
||||
isModification &&
|
||||
t('editor.editorToolbar.deleteUnpublishedChanges')) ||
|
||||
(hasUnpublishedChanges &&
|
||||
(isNewEntry || !isModification) &&
|
||||
t('editor.editorToolbar.deleteUnpublishedEntry')) ||
|
||||
(!hasUnpublishedChanges && !isModification && t('editor.editorToolbar.deletePublishedEntry'));
|
||||
|
||||
return [
|
||||
<SaveButton
|
||||
disabled={!hasChanged}
|
||||
key="save-button"
|
||||
onClick={() => hasChanged && onPersist()}
|
||||
>
|
||||
{isPersisting ? t('editor.editorToolbar.saving') : t('editor.editorToolbar.save')}
|
||||
</SaveButton>,
|
||||
currentStatus
|
||||
? [
|
||||
this.renderWorkflowStatusControls(),
|
||||
this.renderNewEntryWorkflowPublishControls({ canCreate, canPublish }),
|
||||
]
|
||||
: !isNewEntry &&
|
||||
this.renderExistingEntryWorkflowPublishControls({ canCreate, canPublish, canDelete }),
|
||||
(!showDelete || useOpenAuthoring) && !hasUnpublishedChanges && !isModification ? null : (
|
||||
<DeleteButton
|
||||
key="delete-button"
|
||||
onClick={hasUnpublishedChanges ? onDeleteUnpublishedChanges : onDelete}
|
||||
>
|
||||
{isDeleting ? t('editor.editorToolbar.deleting') : deleteLabel}
|
||||
</DeleteButton>
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
renderWorkflowDeployPreviewControls = () => {
|
||||
const { currentStatus, isNewEntry, t } = this.props;
|
||||
|
||||
if (currentStatus) {
|
||||
return this.renderDeployPreviewControls(t('editor.editorToolbar.deployPreviewButtonLabel'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish control for published workflow entry.
|
||||
*/
|
||||
if (!isNewEntry) {
|
||||
return this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
hasChanged,
|
||||
displayUrl,
|
||||
collection,
|
||||
hasWorkflow,
|
||||
onLogoutClick,
|
||||
t,
|
||||
editorBackLink,
|
||||
@ -664,13 +353,8 @@ export class EditorToolbar extends React.Component {
|
||||
</ToolbarSectionBackLink>
|
||||
<ToolbarSectionMain>
|
||||
<ToolbarSubSectionFirst>
|
||||
{hasWorkflow ? this.renderWorkflowControls() : this.renderSimpleControls()}
|
||||
{this.renderSimpleControls()}
|
||||
</ToolbarSubSectionFirst>
|
||||
<ToolbarSubSectionLast>
|
||||
{hasWorkflow
|
||||
? this.renderWorkflowDeployPreviewControls()
|
||||
: this.renderSimpleDeployPreviewControls()}
|
||||
</ToolbarSubSectionLast>
|
||||
</ToolbarSectionMain>
|
||||
<ToolbarSectionMeta>
|
||||
<SettingsDropdown
|
||||
|
@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { loadUnpublishedEntry, persistUnpublishedEntry } from '../../actions/editorialWorkflow';
|
||||
import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
|
||||
import { selectUnpublishedEntry } from '../../reducers';
|
||||
import { selectAllowDeletion } from '../../reducers/collections';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const { collections } = state;
|
||||
const isEditorialWorkflow = state.config.publish_mode === EDITORIAL_WORKFLOW;
|
||||
const collection = collections.get(ownProps.match.params.name);
|
||||
const returnObj = {
|
||||
isEditorialWorkflow,
|
||||
showDelete: !ownProps.newEntry && selectAllowDeletion(collection),
|
||||
};
|
||||
if (isEditorialWorkflow) {
|
||||
const slug = ownProps.match.params[0];
|
||||
const unpublishedEntry = selectUnpublishedEntry(state, collection.get('name'), slug);
|
||||
if (unpublishedEntry) {
|
||||
returnObj.unpublishedEntry = true;
|
||||
returnObj.entry = unpublishedEntry;
|
||||
}
|
||||
}
|
||||
return returnObj;
|
||||
}
|
||||
|
||||
function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
const { isEditorialWorkflow, unpublishedEntry } = stateProps;
|
||||
const { dispatch } = dispatchProps;
|
||||
const returnObj = {};
|
||||
|
||||
if (isEditorialWorkflow) {
|
||||
// Overwrite loadEntry to loadUnpublishedEntry
|
||||
returnObj.loadEntry = (collection, slug) => dispatch(loadUnpublishedEntry(collection, slug));
|
||||
|
||||
// Overwrite persistEntry to persistUnpublishedEntry
|
||||
returnObj.persistEntry = collection =>
|
||||
dispatch(persistUnpublishedEntry(collection, unpublishedEntry));
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
...returnObj,
|
||||
};
|
||||
}
|
||||
|
||||
export default function withWorkflow(Editor) {
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
mergeProps,
|
||||
)(
|
||||
class WorkflowEditor extends React.Component {
|
||||
render() {
|
||||
return <Editor {...this.props} />;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
@ -217,7 +217,7 @@ class MediaLibrary extends React.Component {
|
||||
*/
|
||||
handleDelete = async () => {
|
||||
const { selectedFile } = this.state;
|
||||
const { files, deleteMedia, privateUpload, t } = this.props;
|
||||
const { files, deleteMedia, privateUpload } = this.props;
|
||||
if (
|
||||
!(await confirm({
|
||||
title: 'mediaLibrary.mediaLibrary.onDeleteTitle',
|
||||
|
@ -5,11 +5,13 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { translate, TranslateProps } from 'react-polyglot';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
||||
import AlertEvent from '../../lib/util/events/AlertEvent';
|
||||
import { useWindowEvent } from '../../lib/util/window.util';
|
||||
|
||||
import type { TranslateProps } from 'react-polyglot';
|
||||
|
||||
interface AlertProps {
|
||||
title: string | { key: string; options?: any };
|
||||
body: string | { key: string; options?: any };
|
||||
|
@ -5,11 +5,13 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { translate, TranslateProps } from 'react-polyglot';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
||||
import ConfirmEvent from '../../lib/util/events/ConfirmEvent';
|
||||
import { useWindowEvent } from '../../lib/util/window.util';
|
||||
|
||||
import type { TranslateProps } from 'react-polyglot';
|
||||
|
||||
interface ConfirmProps {
|
||||
title: string | { key: string; options?: any };
|
||||
body: string | { key: string; options?: any };
|
||||
|
@ -1,166 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from '@emotion/styled';
|
||||
import { OrderedMap } from 'immutable';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
StyledDropdownButton,
|
||||
Loader,
|
||||
lengths,
|
||||
components,
|
||||
shadows,
|
||||
} from '../../ui';
|
||||
import { createNewEntry } from '../../actions/collections';
|
||||
import {
|
||||
loadUnpublishedEntries,
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
} from '../../actions/editorialWorkflow';
|
||||
import { selectUnpublishedEntriesByStatus } from '../../reducers';
|
||||
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
||||
import WorkflowList from './WorkflowList';
|
||||
|
||||
const WorkflowContainer = styled.div`
|
||||
padding: ${lengths.pageMargin} 0;
|
||||
height: calc(100vh - 56px);
|
||||
`;
|
||||
|
||||
const WorkflowTop = styled.div`
|
||||
${components.cardTop};
|
||||
`;
|
||||
|
||||
const WorkflowTopRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
span[role='button'] {
|
||||
${shadows.dropDeep};
|
||||
}
|
||||
`;
|
||||
|
||||
const WorkflowTopHeading = styled.h1`
|
||||
${components.cardTopHeading};
|
||||
`;
|
||||
|
||||
const WorkflowTopDescription = styled.p`
|
||||
${components.cardTopDescription};
|
||||
`;
|
||||
|
||||
class Workflow extends Component {
|
||||
static propTypes = {
|
||||
collections: ImmutablePropTypes.map.isRequired,
|
||||
isEditorialWorkflow: PropTypes.bool.isRequired,
|
||||
isOpenAuthoring: PropTypes.bool,
|
||||
isFetching: PropTypes.bool,
|
||||
unpublishedEntries: ImmutablePropTypes.map,
|
||||
loadUnpublishedEntries: PropTypes.func.isRequired,
|
||||
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
|
||||
publishUnpublishedEntry: PropTypes.func.isRequired,
|
||||
deleteUnpublishedEntry: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { loadUnpublishedEntries, isEditorialWorkflow, collections } = this.props;
|
||||
if (isEditorialWorkflow) {
|
||||
loadUnpublishedEntries(collections);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isEditorialWorkflow,
|
||||
isOpenAuthoring,
|
||||
isFetching,
|
||||
unpublishedEntries,
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
collections,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
if (!isEditorialWorkflow) return null;
|
||||
if (isFetching) return <Loader active>{t('workflow.workflow.loading')}</Loader>;
|
||||
const reviewCount = unpublishedEntries.get('pending_review').size;
|
||||
const readyCount = unpublishedEntries.get('pending_publish').size;
|
||||
|
||||
return (
|
||||
<WorkflowContainer>
|
||||
<WorkflowTop>
|
||||
<WorkflowTopRow>
|
||||
<WorkflowTopHeading>{t('workflow.workflow.workflowHeading')}</WorkflowTopHeading>
|
||||
<Dropdown
|
||||
dropdownWidth="160px"
|
||||
dropdownPosition="left"
|
||||
dropdownTopOverlap="40px"
|
||||
renderButton={() => (
|
||||
<StyledDropdownButton>{t('workflow.workflow.newPost')}</StyledDropdownButton>
|
||||
)}
|
||||
>
|
||||
{collections
|
||||
.filter(collection => collection.get('create'))
|
||||
.toList()
|
||||
.map(collection => (
|
||||
<DropdownItem
|
||||
key={collection.get('name')}
|
||||
label={collection.get('label')}
|
||||
onClick={() => createNewEntry(collection.get('name'))}
|
||||
/>
|
||||
))}
|
||||
</Dropdown>
|
||||
</WorkflowTopRow>
|
||||
<WorkflowTopDescription>
|
||||
{t('workflow.workflow.description', {
|
||||
smart_count: reviewCount,
|
||||
readyCount,
|
||||
})}
|
||||
</WorkflowTopDescription>
|
||||
</WorkflowTop>
|
||||
<WorkflowList
|
||||
entries={unpublishedEntries}
|
||||
handleChangeStatus={updateUnpublishedEntryStatus}
|
||||
handlePublish={publishUnpublishedEntry}
|
||||
handleDelete={deleteUnpublishedEntry}
|
||||
isOpenAuthoring={isOpenAuthoring}
|
||||
collections={collections}
|
||||
/>
|
||||
</WorkflowContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { collections, config, globalUI } = state;
|
||||
const isEditorialWorkflow = config.publish_mode === EDITORIAL_WORKFLOW;
|
||||
const isOpenAuthoring = globalUI.useOpenAuthoring;
|
||||
const returnObj = { collections, isEditorialWorkflow, isOpenAuthoring };
|
||||
|
||||
if (isEditorialWorkflow) {
|
||||
returnObj.isFetching = state.editorialWorkflow.getIn(['pages', 'isFetching'], false);
|
||||
|
||||
/*
|
||||
* Generates an ordered Map of the available status as keys.
|
||||
* Each key containing a Sequence of available unpubhlished entries
|
||||
* Eg.: OrderedMap{'draft':Seq(), 'pending_review':Seq(), 'pending_publish':Seq()}
|
||||
*/
|
||||
returnObj.unpublishedEntries = status.reduce((acc, currStatus) => {
|
||||
const entries = selectUnpublishedEntriesByStatus(state, currStatus);
|
||||
return acc.set(currStatus, entries);
|
||||
}, OrderedMap());
|
||||
}
|
||||
return returnObj;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
loadUnpublishedEntries,
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
})(translate()(Workflow));
|
@ -1,178 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { components, colors, colorsRaw, transitions, buttons } from '../../ui';
|
||||
|
||||
const styles = {
|
||||
text: css`
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
margin-top: 4px;
|
||||
`,
|
||||
button: css`
|
||||
${buttons.button};
|
||||
width: auto;
|
||||
flex: 1 0 0;
|
||||
font-size: 13px;
|
||||
padding: 6px 0;
|
||||
`,
|
||||
};
|
||||
|
||||
const WorkflowLink = styled(Link)`
|
||||
display: block;
|
||||
padding: 0 18px 18px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const CardCollection = styled.div`
|
||||
font-size: 14px;
|
||||
color: ${colors.textLead};
|
||||
text-transform: uppercase;
|
||||
margin-top: 12px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
margin: 28px 0 0;
|
||||
color: ${colors.textLead};
|
||||
`;
|
||||
|
||||
const CardDateContainer = styled.div`
|
||||
${styles.text};
|
||||
`;
|
||||
|
||||
const CardBody = styled.p`
|
||||
${styles.text};
|
||||
color: ${colors.text};
|
||||
margin: 24px 0 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
`;
|
||||
|
||||
const CardButtonContainer = styled.div`
|
||||
background-color: ${colors.foreground};
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 12px 18px;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity ${transitions.main};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
${styles.button};
|
||||
background-color: ${colorsRaw.redLight};
|
||||
color: ${colorsRaw.red};
|
||||
margin-right: 6px;
|
||||
`;
|
||||
|
||||
const PublishButton = styled.button`
|
||||
${styles.button};
|
||||
background-color: ${colorsRaw.teal};
|
||||
color: ${colors.textLight};
|
||||
margin-left: 6px;
|
||||
|
||||
&[disabled] {
|
||||
${buttons.disabled};
|
||||
}
|
||||
`;
|
||||
|
||||
const WorkflowCardContainer = styled.div`
|
||||
${components.card};
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover ${CardButtonContainer} {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
function lastChangePhraseKey(date, author) {
|
||||
if (date && author) {
|
||||
return 'lastChange';
|
||||
} else if (date) {
|
||||
return 'lastChangeNoAuthor';
|
||||
} else if (author) {
|
||||
return 'lastChangeNoDate';
|
||||
}
|
||||
}
|
||||
|
||||
const CardDate = translate()(({ t, date, author }) => {
|
||||
const key = lastChangePhraseKey(date, author);
|
||||
if (key) {
|
||||
return (
|
||||
<CardDateContainer>{t(`workflow.workflowCard.${key}`, { date, author })}</CardDateContainer>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function WorkflowCard({
|
||||
collectionLabel,
|
||||
title,
|
||||
authorLastChange,
|
||||
body,
|
||||
isModification,
|
||||
editLink,
|
||||
timestamp,
|
||||
onDelete,
|
||||
allowPublish,
|
||||
canPublish,
|
||||
onPublish,
|
||||
postAuthor,
|
||||
t,
|
||||
}) {
|
||||
return (
|
||||
<WorkflowCardContainer>
|
||||
<WorkflowLink to={editLink}>
|
||||
<CardCollection>{collectionLabel}</CardCollection>
|
||||
{postAuthor}
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{(timestamp || authorLastChange) && <CardDate date={timestamp} author={authorLastChange} />}
|
||||
<CardBody>{body}</CardBody>
|
||||
</WorkflowLink>
|
||||
<CardButtonContainer>
|
||||
<DeleteButton onClick={onDelete}>
|
||||
{isModification
|
||||
? t('workflow.workflowCard.deleteChanges')
|
||||
: t('workflow.workflowCard.deleteNewEntry')}
|
||||
</DeleteButton>
|
||||
{allowPublish && (
|
||||
<PublishButton disabled={!canPublish} onClick={onPublish}>
|
||||
{isModification
|
||||
? t('workflow.workflowCard.publishChanges')
|
||||
: t('workflow.workflowCard.publishNewEntry')}
|
||||
</PublishButton>
|
||||
)}
|
||||
</CardButtonContainer>
|
||||
</WorkflowCardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowCard.propTypes = {
|
||||
collectionLabel: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
authorLastChange: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
isModification: PropTypes.bool,
|
||||
editLink: PropTypes.string.isRequired,
|
||||
timestamp: PropTypes.string.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
allowPublish: PropTypes.bool.isRequired,
|
||||
canPublish: PropTypes.bool.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
postAuthor: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default translate()(WorkflowCard);
|
@ -1,284 +0,0 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
||||
import { status } from '../../constants/publishModes';
|
||||
import { selectEntryCollectionTitle } from '../../reducers/collections';
|
||||
import { colors, lengths } from '../../ui';
|
||||
import { DragSource, DropTarget, HTML5DragDrop } from '../UI';
|
||||
import alert from '../UI/Alert';
|
||||
import confirm from '../UI/Confirm';
|
||||
import WorkflowCard from './WorkflowCard';
|
||||
|
||||
const WorkflowListContainer = styled.div`
|
||||
min-height: 60%;
|
||||
display: grid;
|
||||
grid-template-columns: 33.3% 33.3% 33.3%;
|
||||
`;
|
||||
|
||||
const WorkflowListContainerOpenAuthoring = styled.div`
|
||||
min-height: 60%;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50% 0%;
|
||||
`;
|
||||
|
||||
const styles = {
|
||||
columnPosition: idx =>
|
||||
(idx === 0 &&
|
||||
css`
|
||||
margin-left: 0;
|
||||
`) ||
|
||||
(idx === 2 &&
|
||||
css`
|
||||
margin-right: 0;
|
||||
`) ||
|
||||
css`
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 80%;
|
||||
top: 76px;
|
||||
background-color: ${colors.textFieldBorder};
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: -23px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: -23px;
|
||||
}
|
||||
`,
|
||||
column: css`
|
||||
margin: 0 20px;
|
||||
transition: background-color 0.5s ease;
|
||||
border: 2px dashed transparent;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
`,
|
||||
columnHovered: css`
|
||||
border-color: ${colors.active};
|
||||
`,
|
||||
hiddenColumn: css`
|
||||
display: none;
|
||||
`,
|
||||
hiddenRightBorder: css`
|
||||
&:not(:first-child):not(:last-child) {
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const ColumnHeader = styled.h2`
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
padding: 4px 14px;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
margin-bottom: 28px;
|
||||
|
||||
${props =>
|
||||
props.name === 'draft' &&
|
||||
css`
|
||||
background-color: ${colors.statusDraftBackground};
|
||||
color: ${colors.statusDraftText};
|
||||
`}
|
||||
|
||||
${props =>
|
||||
props.name === 'pending_review' &&
|
||||
css`
|
||||
background-color: ${colors.statusReviewBackground};
|
||||
color: ${colors.statusReviewText};
|
||||
`}
|
||||
|
||||
${props =>
|
||||
props.name === 'pending_publish' &&
|
||||
css`
|
||||
background-color: ${colors.statusReadyBackground};
|
||||
color: ${colors.statusReadyText};
|
||||
`}
|
||||
`;
|
||||
|
||||
const ColumnCount = styled.p`
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${colors.text};
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
// This is a namespace so that we can only drop these elements on a DropTarget with the same
|
||||
const DNDNamespace = 'cms-workflow';
|
||||
|
||||
function getColumnHeaderText(columnName, t) {
|
||||
switch (columnName) {
|
||||
case 'draft':
|
||||
return t('workflow.workflowList.draftHeader');
|
||||
case 'pending_review':
|
||||
return t('workflow.workflowList.inReviewHeader');
|
||||
case 'pending_publish':
|
||||
return t('workflow.workflowList.readyHeader');
|
||||
}
|
||||
}
|
||||
|
||||
class WorkflowList extends React.Component {
|
||||
static propTypes = {
|
||||
entries: ImmutablePropTypes.orderedMap,
|
||||
handleChangeStatus: PropTypes.func.isRequired,
|
||||
handlePublish: PropTypes.func.isRequired,
|
||||
handleDelete: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isOpenAuthoring: PropTypes.bool,
|
||||
collections: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
handleChangeStatus = (newStatus, dragProps) => {
|
||||
const slug = dragProps.slug;
|
||||
const collection = dragProps.collection;
|
||||
const oldStatus = dragProps.ownStatus;
|
||||
this.props.handleChangeStatus(collection, slug, oldStatus, newStatus);
|
||||
};
|
||||
|
||||
requestDelete = async (collection, slug, ownStatus) => {
|
||||
if (
|
||||
await confirm({
|
||||
title: 'workflow.workflowList.onDeleteEntryTitle',
|
||||
body: 'workflow.workflowList.onDeleteEntryBody',
|
||||
})
|
||||
) {
|
||||
this.props.handleDelete(collection, slug, ownStatus);
|
||||
}
|
||||
};
|
||||
|
||||
requestPublish = async (collection, slug, ownStatus) => {
|
||||
if (ownStatus !== status.last()) {
|
||||
alert({
|
||||
title: 'workflow.workflowList.onPublishingNotReadyEntryTitle',
|
||||
body: 'workflow.workflowList.onPublishingNotReadyEntryBody',
|
||||
});
|
||||
return;
|
||||
} else if (
|
||||
!(await confirm({
|
||||
title: 'workflow.workflowList.onPublishEntryTitle',
|
||||
body: 'workflow.workflowList.onPublishEntryBody',
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.props.handlePublish(collection, slug);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderColumns = (entries, column) => {
|
||||
const { isOpenAuthoring, collections, t } = this.props;
|
||||
if (!entries) return null;
|
||||
|
||||
if (!column) {
|
||||
return entries.entrySeq().map(([currColumn, currEntries], idx) => (
|
||||
<DropTarget
|
||||
namespace={DNDNamespace}
|
||||
key={currColumn}
|
||||
onDrop={this.handleChangeStatus.bind(this, currColumn)}
|
||||
>
|
||||
{(connect, { isHovered }) =>
|
||||
connect(
|
||||
<div style={{ height: '100%' }}>
|
||||
<div
|
||||
css={[
|
||||
styles.column,
|
||||
styles.columnPosition(idx),
|
||||
isHovered && styles.columnHovered,
|
||||
isOpenAuthoring && currColumn === 'pending_publish' && styles.hiddenColumn,
|
||||
isOpenAuthoring && currColumn === 'pending_review' && styles.hiddenRightBorder,
|
||||
]}
|
||||
>
|
||||
<ColumnHeader name={currColumn}>
|
||||
{getColumnHeaderText(currColumn, this.props.t)}
|
||||
</ColumnHeader>
|
||||
<ColumnCount>
|
||||
{this.props.t('workflow.workflowList.currentEntries', {
|
||||
smart_count: currEntries.size,
|
||||
})}
|
||||
</ColumnCount>
|
||||
{this.renderColumns(currEntries, currColumn)}
|
||||
</div>
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
</DropTarget>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{entries.map(entry => {
|
||||
const timestamp = moment(entry.get('updatedOn')).format(
|
||||
t('workflow.workflow.dateFormat'),
|
||||
);
|
||||
const slug = entry.get('slug');
|
||||
const collectionName = entry.get('collection');
|
||||
const editLink = `collections/${collectionName}/entries/${slug}?ref=workflow`;
|
||||
const ownStatus = entry.get('status');
|
||||
const collection = collections.find(
|
||||
collection => collection.get('name') === collectionName,
|
||||
);
|
||||
const collectionLabel = collection?.get('label');
|
||||
const isModification = entry.get('isModification');
|
||||
|
||||
const allowPublish = collection?.get('publish');
|
||||
const canPublish = ownStatus === status.last() && !entry.get('isPersisting', false);
|
||||
const postAuthor = entry.get('author');
|
||||
|
||||
return (
|
||||
<DragSource
|
||||
namespace={DNDNamespace}
|
||||
key={`${collectionName}-${slug}`}
|
||||
slug={slug}
|
||||
collection={collectionName}
|
||||
ownStatus={ownStatus}
|
||||
>
|
||||
{connect =>
|
||||
connect(
|
||||
<div>
|
||||
<WorkflowCard
|
||||
collectionLabel={collectionLabel || collectionName}
|
||||
title={selectEntryCollectionTitle(collection, entry)}
|
||||
authorLastChange={entry.getIn(['metaData', 'user'])}
|
||||
body={entry.getIn(['data', 'body'])}
|
||||
isModification={isModification}
|
||||
editLink={editLink}
|
||||
timestamp={timestamp}
|
||||
onDelete={this.requestDelete.bind(this, collectionName, slug, ownStatus)}
|
||||
allowPublish={allowPublish}
|
||||
canPublish={canPublish}
|
||||
onPublish={this.requestPublish.bind(this, collectionName, slug, ownStatus)}
|
||||
postAuthor={postAuthor}
|
||||
/>
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
</DragSource>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const columns = this.renderColumns(this.props.entries);
|
||||
const ListContainer = this.props.isOpenAuthoring
|
||||
? WorkflowListContainerOpenAuthoring
|
||||
: WorkflowListContainer;
|
||||
return <ListContainer>{columns}</ListContainer>;
|
||||
}
|
||||
}
|
||||
|
||||
export default HTML5DragDrop(translate()(WorkflowList));
|
@ -4,10 +4,11 @@ import { translate } from 'react-polyglot';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getAdditionalLink } from '../../lib/registry';
|
||||
import { Collections, State } from '../../types/redux';
|
||||
import { lengths } from '../../ui';
|
||||
import Sidebar from '../Collection/Sidebar';
|
||||
|
||||
import type { Collections, State } from '../../types/redux';
|
||||
|
||||
const StylePage = styled('div')`
|
||||
margin: ${lengths.pageMargin};
|
||||
`;
|
||||
|
@ -135,8 +135,6 @@ function getConfigSchema() {
|
||||
examples: ['repo', 'public_repo'],
|
||||
enum: ['repo', 'public_repo'],
|
||||
},
|
||||
cms_label_prefix: { type: 'string', minLength: 1 },
|
||||
open_authoring: { type: 'boolean', examples: [true] },
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
@ -161,7 +159,6 @@ function getConfigSchema() {
|
||||
site_url: { type: 'string', examples: ['https://example.com'] },
|
||||
display_url: { type: 'string', examples: ['https://example.com'] },
|
||||
logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
|
||||
show_preview_links: { type: 'boolean' },
|
||||
media_folder: { type: 'string', examples: ['assets/uploads'] },
|
||||
public_folder: { type: 'string', examples: ['/uploads'] },
|
||||
media_folder_relative: { type: 'boolean' },
|
||||
@ -173,11 +170,6 @@ function getConfigSchema() {
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
publish_mode: {
|
||||
type: 'string',
|
||||
enum: ['simple', 'editorial_workflow'],
|
||||
examples: ['editorial_workflow'],
|
||||
},
|
||||
slug: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { Map, OrderedMap } from 'immutable';
|
||||
|
||||
// Create/edit workflow modes
|
||||
export const SIMPLE = 'simple';
|
||||
export const EDITORIAL_WORKFLOW = 'editorial_workflow';
|
||||
|
||||
export const Statues = {
|
||||
DRAFT: 'draft',
|
||||
PENDING_REVIEW: 'pending_review',
|
||||
PENDING_PUBLISH: 'pending_publish',
|
||||
};
|
||||
|
||||
// Available status
|
||||
export const status = OrderedMap(Statues);
|
||||
|
||||
export const statusDescriptions = Map({
|
||||
[status.get('DRAFT')]: 'Draft',
|
||||
[status.get('PENDING_REVIEW')]: 'Waiting for Review',
|
||||
[status.get('PENDING_PUBLISH')]: 'Waiting to go live',
|
||||
});
|
||||
|
||||
export type Status = keyof typeof Statues;
|
@ -92,8 +92,6 @@ export type PersistOptions = {
|
||||
newEntry?: boolean;
|
||||
commitMessage: string;
|
||||
collectionName?: string;
|
||||
useWorkflow?: boolean;
|
||||
unpublished?: boolean;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
@ -105,7 +103,6 @@ export type User = Credentials & {
|
||||
backendName?: string;
|
||||
login?: string;
|
||||
name: string;
|
||||
useOpenAuthoring?: boolean;
|
||||
};
|
||||
|
||||
export interface ImplementationEntry {
|
||||
@ -129,26 +126,6 @@ export interface ImplementationMediaFile {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryMediaFile {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryDiff {
|
||||
id: string;
|
||||
path: string;
|
||||
newFile: boolean;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntry {
|
||||
pullRequestAuthor?: string;
|
||||
slug: string;
|
||||
collection: string;
|
||||
status: string;
|
||||
diffs: UnpublishedEntryDiff[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type CursorStoreObject = {
|
||||
actions: Set<string>;
|
||||
data: Map<string, unknown>;
|
||||
@ -198,36 +175,6 @@ export interface Implementation {
|
||||
persistMedia: (file: AssetProxy, opts: PersistOptions) => Promise<ImplementationMediaFile>;
|
||||
deleteFiles: (paths: string[], commitMessage: string) => Promise<void>;
|
||||
|
||||
unpublishedEntries: () => Promise<string[]>;
|
||||
unpublishedEntry: (args: {
|
||||
id?: string;
|
||||
collection?: string;
|
||||
slug?: string;
|
||||
}) => Promise<UnpublishedEntry>;
|
||||
unpublishedEntryDataFile: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
path: string,
|
||||
id: string,
|
||||
) => Promise<string>;
|
||||
unpublishedEntryMediaFile: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
path: string,
|
||||
id: string,
|
||||
) => Promise<ImplementationMediaFile>;
|
||||
updateUnpublishedEntryStatus: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
newStatus: string,
|
||||
) => Promise<void>;
|
||||
publishUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
|
||||
deleteUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
|
||||
getDeployPreview: (
|
||||
collectionName: string,
|
||||
slug: string,
|
||||
) => Promise<{ url: string; status: string } | null>;
|
||||
|
||||
allEntriesByFolder?: (
|
||||
folder: string,
|
||||
extension: string,
|
||||
@ -312,8 +259,6 @@ export type CmsCollectionFormatType =
|
||||
|
||||
export type CmsAuthScope = 'repo' | 'public_repo';
|
||||
|
||||
export type CmsPublishMode = 'simple' | 'editorial_workflow';
|
||||
|
||||
export type CmsSlugEncoding = 'unicode' | 'ascii';
|
||||
|
||||
export interface CmsI18nConfig {
|
||||
@ -629,8 +574,6 @@ export interface CmsCollection {
|
||||
export interface CmsBackend {
|
||||
name: CmsBackendType;
|
||||
auth_scope?: CmsAuthScope;
|
||||
open_authoring?: boolean;
|
||||
always_fork?: boolean;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
@ -639,8 +582,6 @@ export interface CmsBackend {
|
||||
auth_endpoint?: string;
|
||||
app_id?: string;
|
||||
auth_type?: 'implicit' | 'pkce';
|
||||
cms_label_prefix?: string;
|
||||
squash_merges?: boolean;
|
||||
proxy_url?: string;
|
||||
commit_messages?: {
|
||||
create?: string;
|
||||
@ -648,7 +589,6 @@ export interface CmsBackend {
|
||||
delete?: string;
|
||||
uploadMedia?: string;
|
||||
deleteMedia?: string;
|
||||
openAuthoring?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -670,12 +610,10 @@ export interface CmsConfig {
|
||||
site_url?: string;
|
||||
display_url?: string;
|
||||
logo_url?: string;
|
||||
show_preview_links?: boolean;
|
||||
media_folder?: string;
|
||||
public_folder?: string;
|
||||
media_folder_relative?: boolean;
|
||||
media_library?: CmsMediaLibrary;
|
||||
publish_mode?: CmsPublishMode;
|
||||
load_config_file?: boolean;
|
||||
integrations?: {
|
||||
hooks: string[];
|
||||
@ -727,7 +665,7 @@ export interface EditorComponentManualOptions {
|
||||
export type EditorComponentOptions = EditorComponentManualOptions | EditorComponentWidgetOptions;
|
||||
|
||||
export interface CmsEventListener {
|
||||
name: 'prePublish' | 'postPublish' | 'preUnpublish' | 'postUnpublish' | 'preSave' | 'postSave';
|
||||
name: 'prePublish' | 'postPublish' | 'preSave' | 'postSave';
|
||||
handler: ({
|
||||
entry,
|
||||
author,
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
import { sanitizeSlug } from './urlHelper';
|
||||
|
||||
import type { Map } from 'immutable';
|
||||
import { CmsConfig } from '../interface';
|
||||
import type { CmsConfig } from '../interface';
|
||||
import type { CmsSlug, Collection, EntryMap } from '../types/redux';
|
||||
|
||||
const {
|
||||
@ -30,7 +30,6 @@ const commitMessageTemplates = {
|
||||
delete: 'Delete {{collection}} “{{slug}}”',
|
||||
uploadMedia: 'Upload “{{path}}”',
|
||||
deleteMedia: 'Delete “{{path}}”',
|
||||
openAuthoring: '{{message}}',
|
||||
} as const;
|
||||
|
||||
const variableRegex = /\{\{([^}]+)\}\}/g;
|
||||
@ -47,11 +46,10 @@ export function commitMessageFormatter(
|
||||
type: keyof typeof commitMessageTemplates,
|
||||
config: CmsConfig,
|
||||
{ slug, path, collection, authorLogin, authorName }: Options,
|
||||
isOpenAuthoring?: boolean,
|
||||
) {
|
||||
const templates = { ...commitMessageTemplates, ...(config.backend.commit_messages || {}) };
|
||||
|
||||
const commitMessage = templates[type].replace(variableRegex, (_, variable) => {
|
||||
return templates[type].replace(variableRegex, (_, variable) => {
|
||||
switch (variable) {
|
||||
case 'slug':
|
||||
return slug || '';
|
||||
@ -68,26 +66,6 @@ export function commitMessageFormatter(
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
if (!isOpenAuthoring) {
|
||||
return commitMessage;
|
||||
}
|
||||
|
||||
const message = templates.openAuthoring.replace(variableRegex, (_, variable) => {
|
||||
switch (variable) {
|
||||
case 'message':
|
||||
return commitMessage;
|
||||
case 'author-login':
|
||||
return authorLogin || '';
|
||||
case 'author-name':
|
||||
return authorName || '';
|
||||
default:
|
||||
console.warn(`Ignoring unknown variable “${variable}” in open authoring message template.`);
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export function prepareSlug(slug: string) {
|
||||
|
@ -7,8 +7,6 @@ import EditorComponent from '../valueObjects/EditorComponent';
|
||||
const allowedEvents = [
|
||||
'prePublish',
|
||||
'postPublish',
|
||||
'preUnpublish',
|
||||
'postUnpublish',
|
||||
'preSave',
|
||||
'postSave',
|
||||
];
|
||||
|
@ -142,45 +142,6 @@ export async function readFileMetadata(
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keywords for inferring a status that will provide a deploy preview URL.
|
||||
*/
|
||||
const PREVIEW_CONTEXT_KEYWORDS = ['deploy'];
|
||||
|
||||
/**
|
||||
* Check a given status context string to determine if it provides a link to a
|
||||
* deploy preview. Checks for an exact match against `previewContext` if given,
|
||||
* otherwise checks for inclusion of a value from `PREVIEW_CONTEXT_KEYWORDS`.
|
||||
*/
|
||||
export function isPreviewContext(context: string, previewContext: string) {
|
||||
if (previewContext) {
|
||||
return context === previewContext;
|
||||
}
|
||||
return PREVIEW_CONTEXT_KEYWORDS.some(keyword => context.includes(keyword));
|
||||
}
|
||||
|
||||
export enum PreviewState {
|
||||
Other = 'other',
|
||||
Success = 'success',
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a deploy preview URL from an array of statuses. By default, a
|
||||
* matching status is inferred via `isPreviewContext`.
|
||||
*/
|
||||
export function getPreviewStatus(
|
||||
statuses: {
|
||||
context: string;
|
||||
target_url: string;
|
||||
state: PreviewState;
|
||||
}[],
|
||||
previewContext: string,
|
||||
) {
|
||||
return statuses.find(({ context }) => {
|
||||
return isPreviewContext(context, previewContext);
|
||||
});
|
||||
}
|
||||
|
||||
function getConflictingBranches(branchName: string) {
|
||||
// for cms/posts/post-1, conflicting branches are cms/posts, cms
|
||||
const parts = branchName.split('/');
|
||||
|
@ -1,25 +1,3 @@
|
||||
export const CMS_BRANCH_PREFIX = 'cms';
|
||||
export const DEFAULT_PR_BODY = 'Automatically generated by Simple CMS';
|
||||
export const MERGE_COMMIT_MESSAGE = 'Automatically generated. Merged on Simple CMS.';
|
||||
|
||||
const DEFAULT_SIMPLE_CMS_LABEL_PREFIX = 'simple-cms/';
|
||||
|
||||
function getLabelPrefix(labelPrefix: string) {
|
||||
return labelPrefix || DEFAULT_SIMPLE_CMS_LABEL_PREFIX;
|
||||
}
|
||||
|
||||
export function isCMSLabel(label: string, labelPrefix: string) {
|
||||
return label.startsWith(getLabelPrefix(labelPrefix));
|
||||
}
|
||||
|
||||
export function labelToStatus(label: string, labelPrefix: string) {
|
||||
return label.slice(getLabelPrefix(labelPrefix).length);
|
||||
}
|
||||
|
||||
export function statusToLabel(status: string, labelPrefix: string) {
|
||||
return `${getLabelPrefix(labelPrefix)}${status}`;
|
||||
}
|
||||
|
||||
export function generateContentKey(collectionName: string, slug: string) {
|
||||
return `${collectionName}/${slug}`;
|
||||
}
|
||||
@ -28,11 +6,3 @@ export function parseContentKey(contentKey: string) {
|
||||
const index = contentKey.indexOf('/');
|
||||
return { collection: contentKey.slice(0, index), slug: contentKey.slice(index + 1) };
|
||||
}
|
||||
|
||||
export function contentKeyFromBranch(branch: string) {
|
||||
return branch.slice(`${CMS_BRANCH_PREFIX}/`.length);
|
||||
}
|
||||
|
||||
export function branchFromContentKey(contentKey: string) {
|
||||
return `${CMS_BRANCH_PREFIX}/${contentKey}`;
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
export const EDITORIAL_WORKFLOW_ERROR = 'EDITORIAL_WORKFLOW_ERROR';
|
||||
|
||||
export default class EditorialWorkflowError extends Error {
|
||||
message: string;
|
||||
notUnderEditorialWorkflow: boolean;
|
||||
constructor(message: string, notUnderEditorialWorkflow: boolean) {
|
||||
super(message);
|
||||
this.message = message;
|
||||
this.notUnderEditorialWorkflow = notUnderEditorialWorkflow;
|
||||
this.name = EDITORIAL_WORKFLOW_ERROR;
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ export function asyncLock(): AsyncLock {
|
||||
try {
|
||||
// suppress too many calls to leave error
|
||||
lock.leave();
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
// calling 'leave' too many times might not be good behavior
|
||||
// but there is no reason to completely fail on it
|
||||
if (e.message !== 'leave called too many times.') {
|
||||
|
@ -16,7 +16,7 @@ function catchFormatErrors(format: string, formatter: Formatter) {
|
||||
return (res: Response) => {
|
||||
try {
|
||||
return formatter(res);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new Error(
|
||||
`Response cannot be parsed into the expected format (${format}): ${err.message}`,
|
||||
);
|
||||
@ -50,7 +50,7 @@ export async function parseResponse(
|
||||
throw new Error(`${format} is not a supported response format.`);
|
||||
}
|
||||
body = await formatter(res);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new APIError(err.message, res.status, apiName);
|
||||
}
|
||||
if (expectingOk && !res.ok) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AlertDialogProps } from '../../../components/UI/Alert';
|
||||
import type { AlertDialogProps } from '../../../components/UI/Alert';
|
||||
|
||||
export default class AlertEvent extends CustomEvent<AlertDialogProps> {
|
||||
constructor(detail: AlertDialogProps) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ConfirmDialogProps } from '../../../components/UI/Confirm';
|
||||
import type { ConfirmDialogProps } from '../../../components/UI/Confirm';
|
||||
|
||||
export default class ConfirmEvent extends CustomEvent<ConfirmDialogProps> {
|
||||
constructor(detail: ConfirmDialogProps) {
|
||||
|
@ -4,7 +4,7 @@ import semaphore from 'semaphore';
|
||||
import { basename } from './path';
|
||||
|
||||
import type { Semaphore } from 'semaphore';
|
||||
import type { Implementation as I } from '../../interface';
|
||||
import type { Implementation as I, ImplementationEntry } from '../../interface';
|
||||
import type { FileMetadata } from './API';
|
||||
import type { AsyncLock } from './asyncLock';
|
||||
|
||||
@ -23,31 +23,6 @@ export interface ImplementationMediaFile {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryMediaFile {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ImplementationEntry {
|
||||
data: string;
|
||||
file: { path: string; label?: string; id?: string | null; author?: string; updatedOn?: string };
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryDiff {
|
||||
id: string;
|
||||
path: string;
|
||||
newFile: boolean;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntry {
|
||||
pullRequestAuthor?: string;
|
||||
slug: string;
|
||||
collection: string;
|
||||
status: string;
|
||||
diffs: UnpublishedEntryDiff[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Map {
|
||||
get: <T>(key: string, defaultValue?: T) => T;
|
||||
getIn: <T>(key: string[], defaultValue?: T) => T;
|
||||
@ -77,8 +52,6 @@ export type PersistOptions = {
|
||||
newEntry?: boolean;
|
||||
commitMessage: string;
|
||||
collectionName?: string;
|
||||
useWorkflow?: boolean;
|
||||
unpublished?: boolean;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
@ -90,17 +63,13 @@ export type User = Credentials & {
|
||||
backendName?: string;
|
||||
login?: string;
|
||||
name: string;
|
||||
useOpenAuthoring?: boolean;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
backend: {
|
||||
repo?: string | null;
|
||||
open_authoring?: boolean;
|
||||
always_fork?: boolean;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
squash_merges?: boolean;
|
||||
use_graphql?: boolean;
|
||||
graphql_api_root?: string;
|
||||
preview_context?: string;
|
||||
@ -111,7 +80,6 @@ export type Config = {
|
||||
proxy_url?: string;
|
||||
auth_type?: string;
|
||||
app_id?: string;
|
||||
cms_label_prefix?: string;
|
||||
api_version?: string;
|
||||
};
|
||||
media_folder: string;
|
||||
@ -191,18 +159,6 @@ export async function entriesByFiles(
|
||||
return fetchFiles(files, readFile, readFileMetadata, apiName);
|
||||
}
|
||||
|
||||
export async function unpublishedEntries(listEntriesKeys: () => Promise<string[]>) {
|
||||
try {
|
||||
const keys = await listEntriesKeys();
|
||||
return keys;
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Not Found') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function blobToFileObj(name: string, blob: Blob) {
|
||||
const options = name.match(/.svg$/) ? { type: 'image/svg+xml' } : {};
|
||||
return new File([blob], name, options);
|
||||
|
@ -1,89 +1,64 @@
|
||||
import APIError from './APIError';
|
||||
import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from './Cursor';
|
||||
import EditorialWorkflowError, { EDITORIAL_WORKFLOW_ERROR } from './EditorialWorkflowError';
|
||||
import AccessTokenError from './AccessTokenError';
|
||||
import localForage from './localForage';
|
||||
import { isAbsolutePath, basename, fileExtensionWithSeparator, fileExtension } from './path';
|
||||
import { onlySuccessfulPromises, flowAsync, then } from './promise';
|
||||
import unsentRequest from './unsentRequest';
|
||||
import { readFile, readFileMetadata, requestWithBackoff, throwOnConflictingBranches } from './API';
|
||||
import APIError from './APIError';
|
||||
import {
|
||||
generateContentKey,
|
||||
parseContentKey
|
||||
} from './APIUtils';
|
||||
import { asyncLock } from './asyncLock';
|
||||
import {
|
||||
filterByExtension,
|
||||
getAllResponses,
|
||||
getPathDepth,
|
||||
parseLinkHeader,
|
||||
parseResponse,
|
||||
responseParser,
|
||||
getPathDepth,
|
||||
responseParser
|
||||
} from './backendUtil';
|
||||
import loadScript from './loadScript';
|
||||
import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from './Cursor';
|
||||
import getBlobSHA from './getBlobSHA';
|
||||
import { asyncLock } from './asyncLock';
|
||||
import {
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
unpublishedEntries,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
runWithLock,
|
||||
blobToFileObj,
|
||||
allEntriesByFolder,
|
||||
} from './implementation';
|
||||
import {
|
||||
readFile,
|
||||
readFileMetadata,
|
||||
isPreviewContext,
|
||||
getPreviewStatus,
|
||||
PreviewState,
|
||||
requestWithBackoff,
|
||||
throwOnConflictingBranches,
|
||||
} from './API';
|
||||
import {
|
||||
CMS_BRANCH_PREFIX,
|
||||
generateContentKey,
|
||||
isCMSLabel,
|
||||
labelToStatus,
|
||||
statusToLabel,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
parseContentKey,
|
||||
branchFromContentKey,
|
||||
contentKeyFromBranch,
|
||||
} from './APIUtils';
|
||||
import {
|
||||
createPointerFile,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
parsePointerFile,
|
||||
getPointerFileForMediaFileObj,
|
||||
parsePointerFile
|
||||
} from './git-lfs';
|
||||
import {
|
||||
allEntriesByFolder,
|
||||
blobToFileObj,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
getMediaAsBlob,
|
||||
getMediaDisplayURL,
|
||||
runWithLock
|
||||
} from './implementation';
|
||||
import loadScript from './loadScript';
|
||||
import localForage from './localForage';
|
||||
import { basename, fileExtension, fileExtensionWithSeparator, isAbsolutePath } from './path';
|
||||
import { flowAsync, onlySuccessfulPromises, then } from './promise';
|
||||
import transientOptions from './transientOptions';
|
||||
import unsentRequest from './unsentRequest';
|
||||
|
||||
import type { ApiRequest as AR, FetchError as FE } from './API';
|
||||
import type { AsyncLock as AL } from './asyncLock';
|
||||
import type { PointerFile as PF } from './git-lfs';
|
||||
import type { FetchError as FE, ApiRequest as AR } from './API';
|
||||
import type {
|
||||
Implementation as I,
|
||||
ImplementationEntry as IE,
|
||||
UnpublishedEntryDiff as UED,
|
||||
UnpublishedEntry as UE,
|
||||
ImplementationMediaFile as IMF,
|
||||
ImplementationFile as IF,
|
||||
DisplayURLObject as DUO,
|
||||
DisplayURL as DU,
|
||||
Credentials as Cred,
|
||||
User as U,
|
||||
Entry as E,
|
||||
PersistOptions as PO,
|
||||
AssetProxy as AP,
|
||||
Config as C,
|
||||
UnpublishedEntryMediaFile as UEMF,
|
||||
Credentials as Cred,
|
||||
DataFile as DF,
|
||||
DisplayURL as DU,
|
||||
DisplayURLObject as DUO,
|
||||
Entry as E,
|
||||
Implementation as I,
|
||||
ImplementationFile as IF,
|
||||
ImplementationMediaFile as IMF,
|
||||
PersistOptions as PO,
|
||||
User as U
|
||||
} from './implementation';
|
||||
import type { AsyncLock as AL } from './asyncLock';
|
||||
|
||||
export type AsyncLock = AL;
|
||||
export type Implementation = I;
|
||||
export type ImplementationEntry = IE;
|
||||
export type UnpublishedEntryDiff = UED;
|
||||
export type UnpublishedEntry = UE;
|
||||
export type ImplementationMediaFile = IMF;
|
||||
export type ImplementationFile = IF;
|
||||
export type DisplayURL = DU;
|
||||
@ -91,7 +66,6 @@ export type DisplayURLObject = DUO;
|
||||
export type Credentials = Cred;
|
||||
export type User = U;
|
||||
export type Entry = E;
|
||||
export type UnpublishedEntryMediaFile = UEMF;
|
||||
export type PersistOptions = PO;
|
||||
export type AssetProxy = AP;
|
||||
export type ApiRequest = AR;
|
||||
@ -104,8 +78,6 @@ export const SimpleCmsLibUtil = {
|
||||
APIError,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
EditorialWorkflowError,
|
||||
EDITORIAL_WORKFLOW_ERROR,
|
||||
localForage,
|
||||
basename,
|
||||
fileExtensionWithSeparator,
|
||||
@ -123,43 +95,29 @@ export const SimpleCmsLibUtil = {
|
||||
getPathDepth,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
unpublishedEntries,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
readFile,
|
||||
readFileMetadata,
|
||||
CMS_BRANCH_PREFIX,
|
||||
generateContentKey,
|
||||
isCMSLabel,
|
||||
labelToStatus,
|
||||
statusToLabel,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
isPreviewContext,
|
||||
getPreviewStatus,
|
||||
runWithLock,
|
||||
PreviewState,
|
||||
parseContentKey,
|
||||
createPointerFile,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
parsePointerFile,
|
||||
getPointerFileForMediaFileObj,
|
||||
branchFromContentKey,
|
||||
contentKeyFromBranch,
|
||||
blobToFileObj,
|
||||
requestWithBackoff,
|
||||
allEntriesByFolder,
|
||||
AccessTokenError,
|
||||
throwOnConflictingBranches,
|
||||
transientOptions
|
||||
transientOptions,
|
||||
};
|
||||
export {
|
||||
APIError,
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
EditorialWorkflowError,
|
||||
EDITORIAL_WORKFLOW_ERROR,
|
||||
localForage,
|
||||
basename,
|
||||
fileExtensionWithSeparator,
|
||||
@ -180,30 +138,18 @@ export {
|
||||
getPathDepth,
|
||||
entriesByFiles,
|
||||
entriesByFolder,
|
||||
unpublishedEntries,
|
||||
getMediaDisplayURL,
|
||||
getMediaAsBlob,
|
||||
readFile,
|
||||
readFileMetadata,
|
||||
CMS_BRANCH_PREFIX,
|
||||
generateContentKey,
|
||||
isCMSLabel,
|
||||
labelToStatus,
|
||||
statusToLabel,
|
||||
DEFAULT_PR_BODY,
|
||||
MERGE_COMMIT_MESSAGE,
|
||||
isPreviewContext,
|
||||
getPreviewStatus,
|
||||
runWithLock,
|
||||
PreviewState,
|
||||
parseContentKey,
|
||||
createPointerFile,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
getLargeMediaPatternsFromGitAttributesFile,
|
||||
parsePointerFile,
|
||||
getPointerFileForMediaFileObj,
|
||||
branchFromContentKey,
|
||||
contentKeyFromBranch,
|
||||
blobToFileObj,
|
||||
requestWithBackoff,
|
||||
allEntriesByFolder,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import AlertEvent from './events/AlertEvent';
|
||||
import ConfirmEvent from './events/ConfirmEvent';
|
||||
|
||||
import type AlertEvent from './events/AlertEvent';
|
||||
import type ConfirmEvent from './events/ConfirmEvent';
|
||||
|
||||
interface EventMap {
|
||||
alert: AlertEvent;
|
||||
@ -9,10 +10,16 @@ interface EventMap {
|
||||
|
||||
export function useWindowEvent<K extends keyof WindowEventMap>(
|
||||
eventName: K,
|
||||
callback: (event: WindowEventMap[K]) => void
|
||||
callback: (event: WindowEventMap[K]) => void,
|
||||
): void;
|
||||
export function useWindowEvent<K extends keyof EventMap>(eventName: K, callback: (event: EventMap[K]) => void): void;
|
||||
export function useWindowEvent(eventName: string, callback: EventListenerOrEventListenerObject): void {
|
||||
export function useWindowEvent<K extends keyof EventMap>(
|
||||
eventName: K,
|
||||
callback: (event: EventMap[K]) => void,
|
||||
): void;
|
||||
export function useWindowEvent(
|
||||
eventName: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
window.addEventListener(eventName, callback);
|
||||
|
||||
|
@ -101,14 +101,9 @@ const bg = {
|
||||
onPublishingNotReady: 'Моля, актуализирайте състоянието на „Готово“, преди да публикувате',
|
||||
onPublishingWithUnsavedChanges: 'Имате незапазени промени, моля, запазете преди публикуване.',
|
||||
onPublishing: 'Наистина ли искате да публикувате този запис?',
|
||||
onUnpublishing: 'Наистина ли искате да прекратите публикуването на този запис?',
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Наистина ли искате да изтриете този публикуван запис, както и незаписаните промени от текущата сесия?',
|
||||
onDeletePublishedEntry: 'Наистина ли искате да изтриете този публикуван запис?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Това ще изтрие всички непубликувани промени в този запис, както и незаписаните ви промени от текущата сесия. Все още ли искате да изтриете?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Всички непубликувани промени в този запис ще бъдат изтрити. Все още ли искате да изтриете?',
|
||||
loadingEntry: 'Зареждане на запис...',
|
||||
confirmLoadBackup:
|
||||
'За този запис беше възстановен локален архив, бихте ли искали да го използвате?',
|
||||
@ -122,14 +117,9 @@ const bg = {
|
||||
publishing: 'Публикуване...',
|
||||
publish: 'Публикувай',
|
||||
published: 'Публикуван',
|
||||
unpublish: 'Непубликувай',
|
||||
duplicate: 'Дублирай',
|
||||
unpublishing: 'Непубликуване...',
|
||||
publishAndCreateNew: 'Публикувай и създай нов',
|
||||
publishAndDuplicate: 'Публикувай и дублирай',
|
||||
deleteUnpublishedChanges: 'Изтриване на непубликувани промени',
|
||||
deleteUnpublishedEntry: 'Изтрий непубликувани записи',
|
||||
deletePublishedEntry: 'Изтрий публикувани записи',
|
||||
deleteEntry: 'Изтрий запис',
|
||||
saving: 'Запазване...',
|
||||
save: 'Запази',
|
||||
@ -258,11 +248,8 @@ const bg = {
|
||||
'Извинете, пропуснахте задължително поле. Моля, попълнете преди запазване.',
|
||||
entrySaved: 'Записът е запазен',
|
||||
entryPublished: 'Записът е публикуван',
|
||||
entryUnpublished: 'Записът е непубликуван',
|
||||
onFailToPublishEntry: 'Неуспешно публикуване на запис: %{details}',
|
||||
onFailToUnpublishEntry: 'Неуспешно премахване на публикацията на записа: %{details}',
|
||||
entryUpdated: 'Статусът на записа е актуализиран',
|
||||
onDeleteUnpublishedChanges: 'Непубликуваните промени са изтрити',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Излезли сте. Моля, запазете всички данни и влезте отново',
|
||||
onBackendDown: 'Има прекъсване в работата на бекенда. Виж детайлите %{details}',
|
||||
|
@ -101,14 +101,9 @@ const ca = {
|
||||
onPublishingWithUnsavedChanges:
|
||||
"Tens canvis no guardats, si us plau, guarda'ls abans de publicar-los.",
|
||||
onPublishing: 'Estàs segur que vols publicar aquesta entrada?',
|
||||
onUnpublishing: 'Estàs segur que vols esborrar aquesta entrada?',
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Està segur que vol eliminar aquesta entrada publicada, així com els canvis no guardats de la sessió actual?',
|
||||
onDeletePublishedEntry: 'Està segur que vol eliminar aquesta entrada publicada?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
"Això eliminarà tots els canvis no publicats d'aquesta entrada així com els canvis no guardats de la sessió actual. Encara vol procedir?",
|
||||
onDeleteUnpublishedChanges:
|
||||
'Tots els canvis no publicats en aquesta entrada seràn esborrats. Encara els vol eliminar?',
|
||||
loadingEntry: 'Carregant entrada...',
|
||||
confirmLoadBackup:
|
||||
"S'ha recuperat una copia de seguretat local per aquesta entrada. La vol utilitzar?",
|
||||
@ -121,14 +116,9 @@ const ca = {
|
||||
publishing: 'Publicant...',
|
||||
publish: 'Publicar',
|
||||
published: 'Publicat',
|
||||
unpublish: 'Despublicar',
|
||||
duplicate: 'Duplicar',
|
||||
unpublishing: 'Despublicant...',
|
||||
publishAndCreateNew: 'Publicar i crear de nou',
|
||||
publishAndDuplicate: 'Publica i duplica',
|
||||
deleteUnpublishedChanges: 'Eliminar canvis no publicats',
|
||||
deleteUnpublishedEntry: 'Eliminar entrada no publicada',
|
||||
deletePublishedEntry: 'Eliminar entrada publicada',
|
||||
deleteEntry: 'Eliminar entrada',
|
||||
saving: 'Guardant...',
|
||||
save: 'Guardar',
|
||||
@ -255,11 +245,8 @@ const ca = {
|
||||
"Ups, no ha omplert un camp obligatori. Si us plau, ompli'l abans de guardar.",
|
||||
entrySaved: 'Entrada guardada',
|
||||
entryPublished: 'Entrada publicada',
|
||||
entryUnpublished: 'Entrada despublicada',
|
||||
onFailToPublishEntry: "No s'ha pogut publicar: %{details}",
|
||||
onFailToUnpublishEntry: "No s'ha pogut despublicar l'entrada: %{details}",
|
||||
entryUpdated: "Estat de l'entrada actualitzat",
|
||||
onDeleteUnpublishedChanges: 'Canvis no publicats eliminats',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'La teva sessió ha estat tancada. Si us plau, torna a iniciar-la',
|
||||
onBackendDown: 'El servidor està patint problemes. Consulta %{details} per a més informació',
|
||||
|
@ -101,14 +101,9 @@ const cs = {
|
||||
onPublishingNotReady: 'Změňte stav na „Připraveno“ před publikováním.',
|
||||
onPublishingWithUnsavedChanges: 'Máte neuložené změny, prosím uložte je před publikováním.',
|
||||
onPublishing: 'Chcete opravdu publikovat tento záznam?',
|
||||
onUnpublishing: 'Chcete opravdu zrušit publikování tohoto záznamu?',
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Chcete opravdu vymazat tento publikovaný záznam a všechny neuložené změny z této relace?',
|
||||
onDeletePublishedEntry: 'Chcete opravdu smazat tento publikovaný záznam?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Tato akce vymaže všechny nepublikované změny v tomto záznamu a také všechny neuložené změny z této relace. Chcete záznam skutečně vymazat?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Všechny nepublivkoané změny v tomto záznamu budou vymazány. Chcete ho skuteně vymazat?',
|
||||
loadingEntry: 'Načítání záznamu…',
|
||||
confirmLoadBackup: 'Lokální kopie tohoto záznamu byla nalezena, chcete ji použít?',
|
||||
},
|
||||
@ -121,14 +116,9 @@ const cs = {
|
||||
publishing: 'Publikování…',
|
||||
publish: 'Publikovat',
|
||||
published: 'Publikovaný',
|
||||
unpublish: 'Zrušit publikování',
|
||||
duplicate: 'Duplikovat',
|
||||
unpublishing: 'Rušení publikování…',
|
||||
publishAndCreateNew: 'Publikovat a vytvořit nový',
|
||||
publishAndDuplicate: 'Publikovat a duplikovat',
|
||||
deleteUnpublishedChanges: 'Vymazat nepublikované změny',
|
||||
deleteUnpublishedEntry: 'Vymazat nepublikovaný záznam',
|
||||
deletePublishedEntry: 'Vymazat publikovaný záznam',
|
||||
deleteEntry: 'Vymazat záznam',
|
||||
saving: 'Ukládání…',
|
||||
save: 'Uložit',
|
||||
@ -255,11 +245,8 @@ const cs = {
|
||||
missingRequiredField: 'Vynechali jste povinné pole. Prosím vyplňte ho.',
|
||||
entrySaved: 'Záznam uložen',
|
||||
entryPublished: 'Záznam publikován',
|
||||
entryUnpublished: 'Publikování záznamu zrušeno',
|
||||
onFailToPublishEntry: 'Chyba při publikování záznamu: %{details}',
|
||||
onFailToUnpublishEntry: 'Chyba při rušení publikování záznamu: %{details}',
|
||||
entryUpdated: 'Stav záznamu byl změněn',
|
||||
onDeleteUnpublishedChanges: 'Nepublikované změny byly smazány',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Byli jste odhlášeni, prosím zálohujte všechna data a znova se přihlašte',
|
||||
onBackendDown: 'Backend zaznamenal výpadek. Podívejte se do %{details} pro více informací.',
|
||||
|
@ -101,15 +101,10 @@ const da = {
|
||||
onPublishingNotReady: 'Skift status til "Klar" inden publicering.',
|
||||
onPublishingWithUnsavedChanges: 'Du har ændringer der ikke er gemt, gem inden publicing.',
|
||||
onPublishing: 'Er du sikker på at du vil publicere dette dokument?',
|
||||
onUnpublishing: 'Er du sikker på at du vil afpublicere dette dokument?',
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Er du sikker på at du vil slette dette tidliere publiceret dokument, samt dine nuværende ugemte ændringer fra denne session?',
|
||||
onDeletePublishedEntry:
|
||||
'Er du sikker på at du vil slette dette tidliere publiceret dokument?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Alle ikke publicerede ændringer til dette dokument vil blive slettet ligesom dine nuværende ugemte ændringer fra denne session. Er du sikker på at du vil slette?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Alle ikke publicerede ændringer til dette dokument vil blive slettet. Er du sikker på at du vil slette?',
|
||||
loadingEntry: 'Indlæser dokument...',
|
||||
confirmLoadBackup:
|
||||
'En lokal sikkerhedskopi blev gendannet for dette dokument, vil du anvende denne?',
|
||||
@ -118,14 +113,9 @@ const da = {
|
||||
publishing: 'Publicerer...',
|
||||
publish: 'Publicer',
|
||||
published: 'Publiceret',
|
||||
unpublish: 'Afpublicer',
|
||||
duplicate: 'Kopier',
|
||||
unpublishing: 'Afpublicerer...',
|
||||
publishAndCreateNew: 'Publicer og opret ny',
|
||||
publishAndDuplicate: 'Publicer og kopier',
|
||||
deleteUnpublishedChanges: 'Slet upublicerede ændringer',
|
||||
deleteUnpublishedEntry: 'Slet upubliceret dokument',
|
||||
deletePublishedEntry: 'Slet publiceret dokument',
|
||||
deleteEntry: 'Slet dokument',
|
||||
saving: 'Gemmer...',
|
||||
save: 'Gem',
|
||||
@ -242,11 +232,8 @@ const da = {
|
||||
'Ups, du mangler et påkrævet felt. Udfyld de påkrævede felter før dokumentet gemmes.',
|
||||
entrySaved: 'Dokumentet er gemt',
|
||||
entryPublished: 'Dokumentet er publiceret ',
|
||||
entryUnpublished: 'Dokumentet er afpubliceret',
|
||||
onFailToPublishEntry: 'Kunne ikke publicere på grund af en fejl: %{details}',
|
||||
onFailToUnpublishEntry: 'Kunne ikke afpublicere på grund af en fejl: %{details}',
|
||||
entryUpdated: 'Dokumentstatus er opdateret',
|
||||
onDeleteUnpublishedChanges: 'Upublicerede ændringer blev slettet',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Du er blevet logget ind, gem venligst evt. ændringer og log på igen',
|
||||
onBackendDown:
|
||||
|
@ -106,14 +106,9 @@ const de = {
|
||||
onPublishingWithUnsavedChanges:
|
||||
'Es sind noch ungespeicherte Änderungen vorhanden. Bitte speicheren Sie vor dem Veröffentlichen.',
|
||||
onPublishing: 'Soll dieser Beitrag wirklich veröffentlicht werden?',
|
||||
onUnpublishing: 'Soll die Veröffentlichung dieses Beitrags wirklich zurückgezogen werden?',
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Möchten Sie diesen veröffentlichten Beitrag, sowie Ihre nicht gespeicherten Änderungen löschen?',
|
||||
onDeletePublishedEntry: 'Soll dieser veröffentlichte Beitrag wirklich gelöscht werden?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Möchten Sie diesen unveröffentlichten Beitrag, sowie Ihre nicht gespeicherten Änderungen löschen?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Alle unveröffentlichten Änderungen werden gelöscht. Möchten Sie wirklich löschen?',
|
||||
loadingEntry: 'Beitrag laden...',
|
||||
confirmLoadBackup:
|
||||
'Für diesen Beitrag ist ein lokales Backup vorhanden. Möchten Sie dieses benutzen?',
|
||||
@ -127,14 +122,9 @@ const de = {
|
||||
publishing: 'Veröffentlichen...',
|
||||
publish: 'Veröffentlichen',
|
||||
published: 'Veröffentlicht',
|
||||
unpublish: 'Veröffentlichung zurückziehen',
|
||||
duplicate: 'Duplizieren',
|
||||
unpublishing: 'Veröffentlichung wird zurückgezogen...',
|
||||
publishAndCreateNew: 'Veröffentlichen und neuen Beitrag erstellen',
|
||||
publishAndDuplicate: 'Veröffentlichen und Beitrag duplizieren',
|
||||
deleteUnpublishedChanges: 'Unveröffentlichte Änderungen verwerfen',
|
||||
deleteUnpublishedEntry: 'Lösche unveröffentlichten Beitrag',
|
||||
deletePublishedEntry: 'Lösche veröffentlichten Beitrag',
|
||||
deleteEntry: 'Lösche Beitrag',
|
||||
saving: 'Speichern...',
|
||||
save: 'Speichern',
|
||||
@ -269,12 +259,8 @@ const de = {
|
||||
missingRequiredField: 'Oops, einige zwingend erforderliche Felder sind nicht ausgefüllt.',
|
||||
entrySaved: 'Beitrag gespeichert',
|
||||
entryPublished: 'Beitrag veröffentlicht',
|
||||
entryUnpublished: 'Beitrag nicht mehr öffentlich',
|
||||
onFailToPublishEntry: 'Veröffentlichen fehlgeschlagen: %{details}',
|
||||
onFailToUnpublishEntry:
|
||||
'Veröffentlichung des Beitrags konnte nicht rückgängig gemacht werden: %{details}',
|
||||
entryUpdated: 'Beitragsstatus aktualisiert',
|
||||
onDeleteUnpublishedChanges: 'Unveröffentlichte Änderungen verworfen',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut:
|
||||
'Sie wurden ausgeloggt. Bitte sichern Sie Ihre Daten und melden Sie sich erneut an.',
|
||||
|
@ -16,15 +16,9 @@ const en = {
|
||||
'Unable to access identity settings. When using git-gateway backend make sure to enable Identity service and Git Gateway.',
|
||||
},
|
||||
},
|
||||
api: {
|
||||
labelsMigrationTitle: 'Labels Migration',
|
||||
labelsMigrationBody:
|
||||
'Simple CMS is adding labels to %{pullRequests} of your Editorial Workflow entries. The "Workflow" tab will be unavailable during this migration. You may use other areas of the CMS during this time. Note that closing the CMS will pause the migration.',
|
||||
},
|
||||
app: {
|
||||
header: {
|
||||
content: 'Contents',
|
||||
workflow: 'Workflow',
|
||||
media: 'Media',
|
||||
quickAdd: 'Quick add',
|
||||
},
|
||||
@ -118,19 +112,11 @@ const en = {
|
||||
'You have unsaved changes, please save before publishing.',
|
||||
onPublishingTitle: 'Publish this entry?',
|
||||
onPublishingBody: 'Are you sure you want to publish this entry?',
|
||||
onUnpublishingTitle: 'Unpublish this entry?',
|
||||
onUnpublishingBody: 'Are you sure you want to unpublish this entry?',
|
||||
onDeleteWithUnsavedChangesTitle: 'Delete this published entry?',
|
||||
onDeleteWithUnsavedChangesBody:
|
||||
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
|
||||
onDeletePublishedEntryTitle: 'Delete this published entry?',
|
||||
onDeletePublishedEntryBody: 'Are you sure you want to delete this published entry?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChangesTitle: 'Delete this entry?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChangesBody:
|
||||
'This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?',
|
||||
onDeleteUnpublishedChangesTitle: 'Delete this entry?',
|
||||
onDeleteUnpublishedChangesBody:
|
||||
'All unpublished changes to this entry will be deleted. Do you still want to delete?',
|
||||
loadingEntry: 'Loading entry...',
|
||||
confirmLoadBackupTitle: 'Use local backup?',
|
||||
confirmLoadBackupBody:
|
||||
@ -145,14 +131,9 @@ const en = {
|
||||
publishing: 'Publishing...',
|
||||
publish: 'Publish',
|
||||
published: 'Published',
|
||||
unpublish: 'Unpublish',
|
||||
duplicate: 'Duplicate',
|
||||
unpublishing: 'Unpublishing...',
|
||||
publishAndCreateNew: 'Publish and create new',
|
||||
publishAndDuplicate: 'Publish and duplicate',
|
||||
deleteUnpublishedChanges: 'Delete unpublished changes',
|
||||
deleteUnpublishedEntry: 'Delete unpublished entry',
|
||||
deletePublishedEntry: 'Delete published entry',
|
||||
deleteEntry: 'Delete entry',
|
||||
saving: 'Saving...',
|
||||
save: 'Save',
|
||||
@ -170,9 +151,6 @@ const en = {
|
||||
inReview: 'In review',
|
||||
ready: 'Ready',
|
||||
publishNow: 'Publish now',
|
||||
deployPreviewPendingButtonLabel: 'Check for Preview',
|
||||
deployPreviewButtonLabel: 'View Preview',
|
||||
deployButtonLabel: 'View Live',
|
||||
},
|
||||
editorWidgets: {
|
||||
markdown: {
|
||||
@ -297,7 +275,6 @@ const en = {
|
||||
},
|
||||
toast: {
|
||||
onFailToLoadEntries: 'Failed to load entry: %{details}',
|
||||
onFailToLoadDeployPreview: 'Failed to load preview: %{details}',
|
||||
onFailToPersist: 'Failed to persist entry: %{details}',
|
||||
onFailToPersistMedia: 'Failed to persist media: %{details}',
|
||||
onFailToDelete: 'Failed to delete entry: %{details}',
|
||||
@ -306,49 +283,14 @@ const en = {
|
||||
missingRequiredField: "Oops, you've missed a required field. Please complete before saving.",
|
||||
entrySaved: 'Entry saved',
|
||||
entryPublished: 'Entry published',
|
||||
entryUnpublished: 'Entry unpublished',
|
||||
onFailToPublishEntry: 'Failed to publish: %{details}',
|
||||
onFailToUnpublishEntry: 'Failed to unpublish entry: %{details}',
|
||||
entryUpdated: 'Entry status updated',
|
||||
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'You have been logged out, please back up any data and login again',
|
||||
onBackendDown:
|
||||
'The backend service is experiencing an outage. See %{details} for more information',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
workflow: {
|
||||
loading: 'Loading Editorial Workflow Entries',
|
||||
workflowHeading: 'Editorial Workflow',
|
||||
newPost: 'New Post',
|
||||
description:
|
||||
'%{smart_count} entry waiting for review, %{readyCount} ready to go live. |||| %{smart_count} entries waiting for review, %{readyCount} ready to go live. ',
|
||||
dateFormat: 'MMMM D',
|
||||
},
|
||||
workflowCard: {
|
||||
lastChange: '%{date} by %{author}',
|
||||
lastChangeNoAuthor: '%{date}',
|
||||
lastChangeNoDate: 'by %{author}',
|
||||
deleteChanges: 'Delete changes',
|
||||
deleteNewEntry: 'Delete new entry',
|
||||
publishChanges: 'Publish changes',
|
||||
publishNewEntry: 'Publish new entry',
|
||||
},
|
||||
workflowList: {
|
||||
onDeleteEntryTitle: 'Delete this entry?',
|
||||
onDeleteEntryBody: 'Are you sure you want to delete this entry?',
|
||||
onPublishingNotReadyEntryTitle: 'Not ready to be published',
|
||||
onPublishingNotReadyEntryBody:
|
||||
'Only items with a "Ready" status can be published. Please drag the card to the "Ready" column to enable publishing.',
|
||||
onPublishEntryTitle: 'Publish this entry?',
|
||||
onPublishEntryBody: 'Are you sure you want to publish this entry?',
|
||||
draftHeader: 'Drafts',
|
||||
inReviewHeader: 'In Review',
|
||||
readyHeader: 'Ready',
|
||||
currentEntries: '%{smart_count} entry |||| %{smart_count} entries',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
@ -90,10 +90,6 @@ const es = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'¿Está seguro de que desea eliminar esta entrada publicada, así como los cambios no guardados de la sesión actual?',
|
||||
onDeletePublishedEntry: '¿Estás seguro de que quieres borrar esta entrada publicada?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Esto eliminará todos los cambios no publicados de esta entrada, así como los cambios no guardados de la sesión actual. ¿Todavía quieres borrar?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Todos los cambios no publicados en esta entrada serán eliminados. ¿Todavía quieres borrar?',
|
||||
loadingEntry: 'Cargando entrada...',
|
||||
confirmLoadBackup:
|
||||
'Se recuperó una copia de seguridad local para esta entrada, ¿le gustaría utilizarla?',
|
||||
@ -107,9 +103,6 @@ const es = {
|
||||
unpublishing: 'Retirando...',
|
||||
publishAndCreateNew: 'Publicar y crear nuevo',
|
||||
publishAndDuplicate: 'Publicar y duplicar',
|
||||
deleteUnpublishedChanges: 'Eliminar cambios no publicados',
|
||||
deleteUnpublishedEntry: 'Eliminar entrada no publicada',
|
||||
deletePublishedEntry: 'Eliminar entrada publicada',
|
||||
deleteEntry: 'Eliminar entrada',
|
||||
saving: 'Guardando...',
|
||||
save: 'Guardar',
|
||||
@ -217,11 +210,8 @@ const es = {
|
||||
'Oops, no ha rellenado un campo obligatorio. Por favor, rellénelo antes de guardar.',
|
||||
entrySaved: 'Entrada guardada',
|
||||
entryPublished: 'Entrada publicada',
|
||||
entryUnpublished: 'Entrada retirada',
|
||||
onFailToPublishEntry: 'No se ha podido publicar: %{details}',
|
||||
onFailToUnpublishEntry: 'No se ha podido retirar la entrada: %{details}',
|
||||
entryUpdated: 'Estado de entrada actualizado',
|
||||
onDeleteUnpublishedChanges: 'Cambios no publicados eliminados',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -106,10 +106,6 @@ const fr = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Voulez-vous vraiment supprimer cette entrée publiée ainsi que vos modifications non enregistrées de cette session ?',
|
||||
onDeletePublishedEntry: 'Voulez-vous vraiment supprimer cette entrée publiée ?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Ceci supprimera toutes les modifications non publiées de cette entrée ainsi que vos modifications non enregistrées de cette session. Voulez-vous toujours supprimer ?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Toutes les modifications non publiées de cette entrée seront supprimées. Voulez-vous toujours supprimer ?',
|
||||
loadingEntry: "Chargement de l'entrée...",
|
||||
confirmLoadBackup:
|
||||
"Une sauvegarde locale a été trouvée pour cette entrée. Voulez-vous l'utiliser ?",
|
||||
@ -128,9 +124,6 @@ const fr = {
|
||||
unpublishing: 'Dépublication...',
|
||||
publishAndCreateNew: 'Publier et créer une nouvelle entrée',
|
||||
publishAndDuplicate: 'Publier et dupliquer',
|
||||
deleteUnpublishedChanges: 'Supprimer les modications non publiées',
|
||||
deleteUnpublishedEntry: "Supprimer l'entrée non publiée",
|
||||
deletePublishedEntry: "Supprimer l'entrée publiée",
|
||||
deleteEntry: "Supprimer l'entrée",
|
||||
saving: 'Enregistrement...',
|
||||
save: 'Enregistrer',
|
||||
@ -263,11 +256,8 @@ const fr = {
|
||||
'Oops, il manque un champ requis. Veuillez le renseigner avant de soumettre.',
|
||||
entrySaved: 'Entrée enregistrée',
|
||||
entryPublished: 'Entrée publiée',
|
||||
entryUnpublished: 'Entrée dépubliée',
|
||||
onFailToPublishEntry: 'Échec de la publication : %{details}',
|
||||
onFailToUnpublishEntry: "Impossible de dépublier l'entrée : %{details}",
|
||||
entryUpdated: "Statut de l'entrée mis à jour",
|
||||
onDeleteUnpublishedChanges: 'Modifications non publiées supprimées',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Vous avez été déconnecté, merci de sauvegarder les données et vous reconnecter',
|
||||
onBackendDown:
|
||||
|
@ -76,10 +76,6 @@ const gr = {
|
||||
'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη δημοσιευμένη καταχώρηση, καθώς και τις αλλαγές που δεν αποθηκεύσατε από την τρέχουσα περίοδο λειτουργίας;',
|
||||
onDeletePublishedEntry:
|
||||
'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη δημοσιευμένη καταχώρηση;',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Αυτό θα διαγράψει όλες τις μη δημοσιευμένες αλλαγές σε αυτήν την καταχώρηση, καθώς και τις αλλαγές που δεν έχετε αποθηκεύσει από την τρέχουσα περίοδο λειτουργίας. Θέλετε ακόμα να διαγράψετε;',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Όλες οι μη δημοσιευμένες αλλαγές σε αυτήν την καταχώρηση θα διαγραφούν. Θέλετε ακόμα να διαγράψετε;',
|
||||
loadingEntry: 'Φόρτωση εισόδου...',
|
||||
confirmLoadBackup:
|
||||
'Ανακτήθηκε ένα τοπικό αντίγραφο ασφαλείας για αυτήν την καταχώρηση, θέλετε να το χρησιμοποιήσετε;',
|
||||
@ -93,9 +89,6 @@ const gr = {
|
||||
unpublishing: 'Κατάργηση δημοσίευσης...',
|
||||
publishAndCreateNew: 'Δημοσίευση και δημιουργία νέων',
|
||||
publishAndDuplicate: 'Δημοσίευση και αντίγραφο',
|
||||
deleteUnpublishedChanges: 'Διαγραφή μη δημοσιευμένων αλλαγών',
|
||||
deleteUnpublishedEntry: 'Διαγραφή μη δημοσιευμένης καταχώρησης',
|
||||
deletePublishedEntry: 'Διαγραφή δημοσιευμένης καταχώρησης',
|
||||
deleteEntry: 'Διαγραφή καταχώρησης',
|
||||
saving: 'Εξοικονόμηση...',
|
||||
save: 'Αποθήκευση',
|
||||
@ -190,11 +183,8 @@ const gr = {
|
||||
'Ουπς, ξεχάσατε ένα απαιτούμενο πεδίο. Συμπληρώστε το πριν από την αποθήκευση.',
|
||||
entrySaved: 'Η καταχώρηση Αποθηκεύτηκε',
|
||||
entryPublished: 'Η καταχώρηση δημοσιεύτηκε',
|
||||
entryUnpublished: 'Μη δημοσιευμένη καταχώρηση',
|
||||
onFailToPublishEntry: 'Η δημοσίευση απέτυχε: %{details}',
|
||||
onFailToUnpublishEntry: 'Απέτυχε η κατάργηση δημοσίευσης καταχώρησης: %{details}',
|
||||
entryUpdated: 'Η κατάσταση εισόδου ενημερώθηκε',
|
||||
onDeleteUnpublishedChanges: 'Οι μη δημοσιευμένες αλλαγές διαγράφηκαν',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -104,10 +104,6 @@ const he = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'האם ברצונך למחוק את האייטם הזה לפני פרסומו, וכן את השינויים שבוצעו כעת וטרם נשמרו?',
|
||||
onDeletePublishedEntry: 'האם ברצונך למחוק את האייטם הזה לאחר פרסומו?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'פעולה זו תמחק את כל השינויים שבוצעו באייטם זה ולא פורסמו, וכן את השינויים שבוצעו כעת וטרם נשמרו. האם ברצונך למחוק?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'כל השינויים שבוצעו באייטם זה ולא פורסמו יימחקו. האם ברצונך למחוק אותו?',
|
||||
loadingEntry: 'טעינת אייטם...',
|
||||
confirmLoadBackup: 'קיים עותק מקומי שמור של האייטם. האם ברצונך לטעון אותו?',
|
||||
},
|
||||
@ -125,9 +121,6 @@ const he = {
|
||||
unpublishing: 'ביטול הפרסום...',
|
||||
publishAndCreateNew: 'פרסום ויצירת אייטם חדש',
|
||||
publishAndDuplicate: 'פרסום ושכפול',
|
||||
deleteUnpublishedChanges: 'מחיקת השינויים שלא פורסמו',
|
||||
deleteUnpublishedEntry: 'מחיקת אייטם שטרם פורסם',
|
||||
deletePublishedEntry: 'מחיקת אייטם שפורסם',
|
||||
deleteEntry: 'מחיקת האייטם',
|
||||
saving: 'שמירה...',
|
||||
save: 'שמירה',
|
||||
@ -262,11 +255,9 @@ const he = {
|
||||
missingRequiredField: 'אופס, שכחת למלא שדה חובה. נא להשלים את המידע החסר לפני השמירה',
|
||||
entrySaved: 'האייטם נשמר',
|
||||
entryPublished: 'האייטם פורסם',
|
||||
entryUnpublished: 'האייטם הועבר לטיוטות',
|
||||
onFailToPublishEntry: 'פרסום האייטם %{details} נכשל',
|
||||
onFailToUnpublishEntry: 'ביטול פרסום האייטם %{details} נכשל',
|
||||
entryUpdated: 'מצב האייטם עודכן',
|
||||
onDeleteUnpublishedChanges: 'השינויים שלא פורסמו נמחקו',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'נותקת מהמערכת. יש לגבות מידע לא שמור ולהתחבר שוב',
|
||||
onBackendDown: 'ה-backend המוגדר אינו זמין. ראו %{details} למידע נוסף',
|
||||
|
@ -106,10 +106,6 @@ const hr = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Jeste li sigurni da želite obrisati objavljeni unos, te nespremljene promjene u trenutnoj sesiji?',
|
||||
onDeletePublishedEntry: 'Jeste li sigurni da želite obrisati ovaj objavljeni unos?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Obrisat ćete sve neobjavljene promjene na ovom unosu, te sve nespremljene promjene u trenutnoj sesiji. Želite li i dalje obrisati?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Sve nespremljene promjene na ovom unosu će biti obrisane. Želite li i dalje obrisati?',
|
||||
loadingEntry: 'Učitavanje unosa...',
|
||||
confirmLoadBackup: 'Lokalna kopija je dohvaćena za ovaj unos, želite li ju koristiti?',
|
||||
},
|
||||
@ -122,9 +118,6 @@ const hr = {
|
||||
unpublishing: 'Brisanje iz objava...',
|
||||
publishAndCreateNew: 'Objavi i kreiraj novo',
|
||||
publishAndDuplicate: 'Objavi i dupliciraj',
|
||||
deleteUnpublishedChanges: 'Obriši neobjavljene promjene',
|
||||
deleteUnpublishedEntry: 'Obriši neobjavljene unose',
|
||||
deletePublishedEntry: 'Obriši objavljeni unos',
|
||||
deleteEntry: 'Obriši unos',
|
||||
saving: 'Spremanje...',
|
||||
save: 'Spremi',
|
||||
@ -241,11 +234,9 @@ const hr = {
|
||||
missingRequiredField: 'Uups, preskočili ste obvezno polje. Molimo popunite prije spremanja.',
|
||||
entrySaved: 'Unos spremljen',
|
||||
entryPublished: 'Unos objavljen',
|
||||
entryUnpublished: 'Unos obrisan',
|
||||
onFailToPublishEntry: 'Neuspjelo objavljivanje unosa: %{details}',
|
||||
onFailToUnpublishEntry: 'Neuspjelo brisanje unosa: %{details}',
|
||||
entryUpdated: 'Status unosa ažuriran',
|
||||
onDeleteUnpublishedChanges: 'Otkrivene neobjavljene objave',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Odjavljeni ste, molimo spremite sve podatke i prijavite se ponovno',
|
||||
onBackendDown: 'Backend servis ima prekid rada. Pogledaj %{details} za više informacija',
|
||||
|
@ -60,10 +60,6 @@ const hu = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Töröljük ezt a publikált bejegyzést, a többi mentetlen modositással együtt?',
|
||||
onDeletePublishedEntry: 'Töröljük ezt a publikált bejegyzést?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Ezzel törli a bejegyzés összes nem közzétett módosítását, valamint az aktuális munkamenetből nem mentett módosításokat. Még mindig törli?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'A bejegyzés összes, nem közzétett módosítása törlődik. Még mindig törli?',
|
||||
loadingEntry: 'Bejegyzés betöltése...',
|
||||
confirmLoadBackup:
|
||||
'Helyi biztonsági másolat került helyre ehhez a bejegyzéshez, szeretné használni?',
|
||||
@ -77,9 +73,6 @@ const hu = {
|
||||
unpublishing: 'Publikálás visszavonása...',
|
||||
publishAndCreateNew: 'Publikálás és új létrehozása',
|
||||
publishAndDuplicate: 'Publikálás és duplikál',
|
||||
deleteUnpublishedChanges: 'Nempublikált változtatások törlése',
|
||||
deleteUnpublishedEntry: 'Nempublikált bejegyzés törlése',
|
||||
deletePublishedEntry: 'Publikált bejegyzés törlése',
|
||||
deleteEntry: 'Bejegyzés törlése',
|
||||
saving: 'Mentés...',
|
||||
save: 'Mentés',
|
||||
@ -175,11 +168,8 @@ const hu = {
|
||||
missingRequiredField: 'Hoppá, kihagytál egy kötelező mezőt. Mentés előtt töltsd ki.',
|
||||
entrySaved: 'Bejegyzés elmentve',
|
||||
entryPublished: 'Bejegyzés publikálva',
|
||||
entryUnpublished: 'Bejegyzés publikálása visszavonva',
|
||||
onFailToPublishEntry: 'Bejegyzés publikálása sikertelen: %{details}',
|
||||
onFailToUnpublishEntry: 'Bejegyzés publikálásának visszavonása sikertelen: %{details}',
|
||||
entryUpdated: 'Bejegyzés állapota frissült',
|
||||
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -74,10 +74,6 @@ const it = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Sei sicuro di voler cancellare questa voce pubblicata e tutte le modifiche non salvate della tua sessione corrente?',
|
||||
onDeletePublishedEntry: 'Sei sicuro di voler cancellare questa voce pubblicata?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Questo cancellerà tutte le modifiche non pubblicate di questa voce, come anche tutte le modifiche non salvate della sessione corrente. Vuoi ancora cancellarle?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Tutte le modifiche non pubblicate a questa voce saranno cancellate. Vuoi ancora cancellarle?',
|
||||
loadingEntry: 'Caricando la voce...',
|
||||
confirmLoadBackup: 'Un backup locale è stato recuperato per questa voce, vuoi utilizzarlo?',
|
||||
},
|
||||
@ -90,9 +86,6 @@ const it = {
|
||||
unpublishing: 'Rimuovendo dalla pubblicazione...',
|
||||
publishAndCreateNew: 'Pubblica e creane uno nuovo',
|
||||
publishAndDuplicate: 'Pubblica e duplica',
|
||||
deleteUnpublishedChanges: 'Cancella le modifiche non pubblicate',
|
||||
deleteUnpublishedEntry: 'Cancella le voci non pubblicate',
|
||||
deletePublishedEntry: 'Cancella la voce pubblicata',
|
||||
deleteEntry: 'Cancella voce',
|
||||
saving: 'Salvando...',
|
||||
save: 'Salva',
|
||||
@ -187,11 +180,8 @@ const it = {
|
||||
'Oops, ti sei perso un campo obbligatorio. Per favore completalo prima di salvare.',
|
||||
entrySaved: 'Voce salvata',
|
||||
entryPublished: 'Voce pubblicata',
|
||||
entryUnpublished: 'Voce rimossa dalla pubblicazione',
|
||||
onFailToPublishEntry: 'Pubblicazione fallita: %{details}',
|
||||
onFailToUnpublishEntry: 'Rimozione della pubblicazione fallita: %{details}',
|
||||
entryUpdated: 'Status della voce aggiornato',
|
||||
onDeleteUnpublishedChanges: 'Modifiche non pubblicate cancellate',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -105,10 +105,6 @@ const ja = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'保存されていない変更も削除されますが、この公開エントリを削除しますか?',
|
||||
onDeletePublishedEntry: 'この公開エントリを削除しますか?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'保存されていない変更も削除されますが、このエントリの未公開の変更を削除しますか?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'公開されていない変更も削除されますが、このエントリを削除しますか?',
|
||||
loadingEntry: 'エントリの読込中...',
|
||||
confirmLoadBackup: 'ローカルのバックアップが復旧できました。利用しますか?',
|
||||
},
|
||||
@ -126,9 +122,6 @@ const ja = {
|
||||
unpublishing: '未公開にしています...',
|
||||
publishAndCreateNew: '公開して新規作成',
|
||||
publishAndDuplicate: '公開して複製する',
|
||||
deleteUnpublishedChanges: '未公開の変更を削除',
|
||||
deleteUnpublishedEntry: '未公開エントリを削除',
|
||||
deletePublishedEntry: '公開エントリを削除',
|
||||
deleteEntry: 'エントリを削除',
|
||||
saving: '保存中...',
|
||||
save: '保存',
|
||||
@ -259,11 +252,8 @@ const ja = {
|
||||
missingRequiredField: 'すべての必須項目を入力してください。',
|
||||
entrySaved: '保存しました。',
|
||||
entryPublished: '公開しました。',
|
||||
entryUnpublished: '未公開にしました。',
|
||||
onFailToPublishEntry: 'エントリの公開に失敗しました。%{details}',
|
||||
onFailToUnpublishEntry: 'エントリを未公開にするのに失敗しました。%{details}',
|
||||
entryUpdated: 'エントリのステータスを更新しました。',
|
||||
onDeleteUnpublishedChanges: '未公開の変更を削除しました。',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'ログアウトされています。データをバックアップし、再度ログインしてください。',
|
||||
onBackendDown: 'バックエンドのシステムが停止しています。%{details}',
|
||||
|
@ -97,10 +97,6 @@ const ko = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'현재 세션에서의 저장되지 않은 변경사항과 이 게시된 항목을 삭제하시겠습니까?',
|
||||
onDeletePublishedEntry: '이 게시된 항목을 삭제하시겠습니까?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'이 항목의 게시되지 않은 모든 변경사항과 현재 세션의 저장되지 않은 변경사항이 삭제됩니다. 정말로 삭제하시겠습니까?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'이 항목에 대해 게시되지 않은 변경사항이 삭제됩니다. 정말로 삭제하시겠습니까?',
|
||||
loadingEntry: '항목 불러오는 중...',
|
||||
confirmLoadBackup:
|
||||
'이 항목에 대한 로컬 백업이 복구되었습니다, 복구된 것으로 사용하시겠습니까?',
|
||||
@ -114,9 +110,6 @@ const ko = {
|
||||
unpublishing: '게시 철회 중...',
|
||||
publishAndCreateNew: '게시하고 새로 만들기',
|
||||
publishAndDuplicate: '게시하고 복제',
|
||||
deleteUnpublishedChanges: '게시 안된 변경사항 삭제',
|
||||
deleteUnpublishedEntry: '게시 안된 항목 삭제',
|
||||
deletePublishedEntry: '게시된 항목 삭제',
|
||||
deleteEntry: '항목 삭제',
|
||||
saving: '저장 중...',
|
||||
save: '저장',
|
||||
@ -223,11 +216,8 @@ const ko = {
|
||||
missingRequiredField: '이런! 필수 필드를 놓치셨습니다. 저장하기 전에 먼저 채우세요.',
|
||||
entrySaved: '항목 저장됨',
|
||||
entryPublished: '항목 게시됨',
|
||||
entryUnpublished: '항목 게시 철회됨',
|
||||
onFailToPublishEntry: '게시 실패: %{details}',
|
||||
onFailToUnpublishEntry: '항목 게시 철회 실해: %{details}',
|
||||
entryUpdated: '항목 상태 업데이트됨',
|
||||
onDeleteUnpublishedChanges: '게시되지 않은 변경사항 삭제됨',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: '로그아웃 하셨습니다, 데티어를 백업하시고 다시 로그인 하세요.',
|
||||
onBackendDown:
|
||||
|
@ -107,10 +107,6 @@ const lt = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Tikrai norite panaikinti publikuotą įrašą ir Jūsų pakeiitmus iš dabartinės sesijos?',
|
||||
onDeletePublishedEntry: 'Tikrai norite ištrinti šį publikuotą įrašą?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Tai ištrins visus nepublikuotus pakeitimus įraše, taip pat neišsaugotus pakeitimus per dabartinę sesiją. Vis tiek norite trinti?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Visi Jūsų pakeitimai įraše bus panaikinti. Ar tikrai norite trinti jį?',
|
||||
loadingEntry: 'Kraunamas įrašas...',
|
||||
confirmLoadBackup:
|
||||
'Radome Jūsų įrenginyje išsaugota juodraštį šiam įrašui, ar norite jį atgaivinti ir naudoti?',
|
||||
@ -124,9 +120,6 @@ const lt = {
|
||||
unpublishing: 'Nebeskelbiama...',
|
||||
publishAndCreateNew: 'Publikuoti šitą, po to kurti kažką naujo',
|
||||
publishAndDuplicate: 'Publikuoti šitą, po to kurti šito dublį',
|
||||
deleteUnpublishedChanges: 'Ištrinti publikuotus pakeitimus',
|
||||
deleteUnpublishedEntry: 'Ištrinti nepaskelbtą įrašą',
|
||||
deletePublishedEntry: 'Ištrinti paskelbtą įrašą',
|
||||
deleteEntry: 'Panaikinti įrašą',
|
||||
saving: 'Išsaugojama...',
|
||||
save: 'Išsaugoti',
|
||||
@ -244,11 +237,8 @@ const lt = {
|
||||
'Pasitikrinkite — kažkurio (ar kelių) laukelių neužpildėte. Tai padarius galėsite išsaugoti įrašą.',
|
||||
entrySaved: 'Įrašas išsaugotos',
|
||||
entryPublished: 'Įrašas publikuotas',
|
||||
entryUnpublished: 'Įrašas nepublikuotas',
|
||||
onFailToPublishEntry: 'Nepavyko publikuoti: %{details}',
|
||||
onFailToUnpublishEntry: 'Nepavyko panaikinti publikavimą: %{details}',
|
||||
entryUpdated: 'Įrašo statusas pakeistas',
|
||||
onDeleteUnpublishedChanges: 'Nepublikuoti pakeitimai ištrinti',
|
||||
onFailToAuth: 'Nepavyko prisijungti: %{details}',
|
||||
onLoggedOut:
|
||||
'Mes jus atjungėme. Jeigu yra poreikis, sukurkite duomenų atsarginę kopiją. Galite tiesiog iš naujo prisijungti.',
|
||||
|
@ -88,10 +88,6 @@ const nb_no = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Er du sikker på at du vil slette et publisert innlegg med tilhørende ulagrede endringer?',
|
||||
onDeletePublishedEntry: 'Er du sikker på at du vil slette dette publiserte innlegget?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Handlingen sletter endringer som ikke er publisert eller lagret enda. Er du sikker på du vil fortsette?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Alle endringer som ikke er publisert i dette innlegget vil gå tapt. Vil du fortsette?',
|
||||
loadingEntry: 'Laster innlegg...',
|
||||
confirmLoadBackup: 'Vil du gjenopprette tidligere endringer som ikke har blitt lagret?',
|
||||
},
|
||||
@ -104,9 +100,6 @@ const nb_no = {
|
||||
unpublishing: 'Avpubliserer...',
|
||||
publishAndCreateNew: 'Publiser og lag nytt',
|
||||
publishAndDuplicate: 'Publiser og dupliser',
|
||||
deleteUnpublishedChanges: 'Slett upubliserte endringer',
|
||||
deleteUnpublishedEntry: 'Slett upublisert innlegg',
|
||||
deletePublishedEntry: 'Slett publisert innlegg',
|
||||
deleteEntry: 'Slett innlegg',
|
||||
saving: 'Lagrer...',
|
||||
save: 'Lagre',
|
||||
@ -213,11 +206,8 @@ const nb_no = {
|
||||
'Oisann, ser ut som du glemte et påkrevd felt. Du må fylle det ut før du kan fortsette.',
|
||||
entrySaved: 'Innlegg lagret',
|
||||
entryPublished: 'Innlegg publisert',
|
||||
entryUnpublished: 'Innlegg avpublisert',
|
||||
onFailToPublishEntry: 'Kunne ikke publisere: %{details}',
|
||||
onFailToUnpublishEntry: 'Kunne ikke avpublisere: %{details}',
|
||||
entryUpdated: 'Innleggsstatus oppdatert',
|
||||
onDeleteUnpublishedChanges: 'Avpubliserte endringer slettet',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -104,10 +104,6 @@ const nl = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Weet u zeker dat u dit gepubliceerde item en uw niet-opgeslagen wijzigingen uit de huidige sessie wilt verwijderen?',
|
||||
onDeletePublishedEntry: 'Weet u zeker dat u dit gepubliceerde item wilt verwijderen?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Alle niet-gepubliceerde wijzigingen in dit item worden verwijderd, evenals uw niet-opgeslagen wijzigingen uit de huidige sessie. Wilt u nog steeds verwijderen?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Alle niet-gepubliceerde wijzigingen in dit item worden verwijderd. Wilt u nog steeds verwijderen?',
|
||||
loadingEntry: 'Item laden...',
|
||||
confirmLoadBackup: 'Voor dit item is een lokale back-up hersteld, wilt u deze gebruiken?',
|
||||
},
|
||||
@ -125,9 +121,6 @@ const nl = {
|
||||
unpublishing: 'Publicatie ongedaan maken...',
|
||||
publishAndCreateNew: 'Publiceer en maak nieuw item aan',
|
||||
publishAndDuplicate: 'Publiceer en dupliceer item',
|
||||
deleteUnpublishedChanges: 'Verwijder niet-gepubliceerde wijzigingen',
|
||||
deleteUnpublishedEntry: 'Niet-gepubliceerd item verwijderen',
|
||||
deletePublishedEntry: 'Gepubliceerd item verwijderen',
|
||||
deleteEntry: 'Item verwijderen',
|
||||
saving: 'Opslaan...',
|
||||
save: 'Opslaan',
|
||||
@ -258,11 +251,8 @@ const nl = {
|
||||
missingRequiredField: 'Oeps, sommige verplichte velden zijn niet ingevuld.',
|
||||
entrySaved: 'Item opgeslagen',
|
||||
entryPublished: 'Item gepubliceerd',
|
||||
entryUnpublished: 'Publicatie teruggetrokken',
|
||||
onFailToPublishEntry: 'Kan item niet publiceren: %{details}',
|
||||
onFailToUnpublishEntry: 'Kan item niet terugtrekken: %{details}',
|
||||
entryUpdated: 'Status van item geüpdatet',
|
||||
onDeleteUnpublishedChanges: 'Niet-gepubliceerde wijzigingen verwijderd',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Je bent uitgelogd, back-up alstublieft uw data log daarna in',
|
||||
onBackendDown:
|
||||
|
@ -88,10 +88,6 @@ const nn_no = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Er du sikkert på at du vil slette eit publisert innlegg med tilhøyrande ulagra endringar?',
|
||||
onDeletePublishedEntry: 'Er du sikker på at du vil slette dette publiserte innlegget?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Handlinga slettar endringar som ikkje er publisert eller lagra. Vil du halde fram?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Alle endringar som ikkje er publisert vil gå tapt. Vil du halde fram?',
|
||||
loadingEntry: 'Lastar innlegg...',
|
||||
confirmLoadBackup:
|
||||
'Ynskjer du å gjennopprette tidlegare endringar som ikkje har verta lagra?',
|
||||
@ -105,9 +101,6 @@ const nn_no = {
|
||||
unpublishing: 'Avpubliserer...',
|
||||
publishAndCreateNew: 'Publiser og lag nytt',
|
||||
publishAndDuplicate: 'Publiser og dupliser',
|
||||
deleteUnpublishedChanges: 'Slett upubliserte endringar',
|
||||
deleteUnpublishedEntry: 'Slett upublisert innlegg',
|
||||
deletePublishedEntry: 'Slett publisert innlegg',
|
||||
deleteEntry: 'Slettar innlegg',
|
||||
saving: 'Lagrar...',
|
||||
save: 'Lagre',
|
||||
@ -214,11 +207,8 @@ const nn_no = {
|
||||
'Oisann, gløymte du noko? Alle påkrevde felt må fyllast ut før du kan halde fram',
|
||||
entrySaved: 'Innlegg lagra',
|
||||
entryPublished: 'Innlegg publisert',
|
||||
entryUnpublished: 'Innlegg avpublisert',
|
||||
onFailToPublishEntry: 'Kunne ikkje publisere: %{details}',
|
||||
onFailToUnpublishEntry: 'Kunne ikkje avpublisere: %{details}',
|
||||
entryUpdated: 'Innleggsstatus oppdatert',
|
||||
onDeleteUnpublishedChanges: 'Avpubliserte endringar sletta',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -105,10 +105,6 @@ const pl = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Czy na pewno chcesz usunąć tę opublikowaną pozycję, a także niezapisane zmiany z bieżącej sesji?',
|
||||
onDeletePublishedEntry: 'Czy na pewno chcesz usunąć tę opublikowaną pozycję?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Spowoduje to usunięcie wszystkich nieopublikowanych zmian tej pozycji, a także niezapisanych zmian z bieżącej sesji. Czy nadal chcesz usunąć?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Wszystkie nieopublikowane zmiany tej pozycji zostaną usunięte. Czy nadal chcesz usunąć?',
|
||||
loadingEntry: 'Ładowanie pozycji...',
|
||||
confirmLoadBackup: 'Odzyskano lokalną kopię zapasową tej pozycji, czy chcesz jej użyć?',
|
||||
},
|
||||
@ -126,9 +122,6 @@ const pl = {
|
||||
unpublishing: 'Cofanie publikacji...',
|
||||
publishAndCreateNew: 'Opublikuj i dodaj nowy',
|
||||
publishAndDuplicate: 'Opublikuj i zduplikuj',
|
||||
deleteUnpublishedChanges: 'Usuń nieopublikowane zmiany',
|
||||
deleteUnpublishedEntry: 'Usuń nieopublikowaną pozycję',
|
||||
deletePublishedEntry: 'Usuń opublikowaną pozycję',
|
||||
deleteEntry: 'Usuń pozycję',
|
||||
saving: 'Zapisywanie...',
|
||||
save: 'Zapisz',
|
||||
@ -263,11 +256,8 @@ const pl = {
|
||||
missingRequiredField: 'Ups, przegapiłeś wymagane pole. Proszę uzupełnij przed zapisaniem.',
|
||||
entrySaved: 'Pozycja zapisana',
|
||||
entryPublished: 'Pozycja opublikowana',
|
||||
entryUnpublished: 'Cofnięto publikację pozycji',
|
||||
onFailToPublishEntry: 'Nie udało się opublikować: %{details}',
|
||||
onFailToUnpublishEntry: 'Nie udało się cofnąć publikacji pozycji: %{details}',
|
||||
entryUpdated: 'Zaktualizowano status pozycji',
|
||||
onDeleteUnpublishedChanges: 'Nieopublikowane zmiany zostały usunięte',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Zostałeś wylogowany, utwórz kopię zapasową danych i zaloguj się ponownie.',
|
||||
onBackendDown: 'Usługa backendu uległa awarii. Zobacz więcej informacji: %{details}',
|
||||
|
@ -106,10 +106,6 @@ const pt = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Tem certeza de que deseja excluir esta entrada publicada, bem como as alterações não salvas da sessão atual?',
|
||||
onDeletePublishedEntry: 'Tem certeza de que deseja excluir esta entrada publicada?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Isso excluirá todas as alterações não publicadas nesta entrada, bem como as alterações não salvas da sessão atual. Você ainda deseja excluir?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Todas as alterações não publicadas nesta entrada serão excluídas. Você ainda deseja excluir?',
|
||||
loadingEntry: 'Carregando entrada...',
|
||||
confirmLoadBackup: 'Um backup local foi recuperado para esta entrada. Deseja usá-lo?',
|
||||
},
|
||||
@ -127,9 +123,6 @@ const pt = {
|
||||
unpublishing: 'Despublicando...',
|
||||
publishAndCreateNew: 'Publicar e criar novo(a)',
|
||||
publishAndDuplicate: 'Publicar e duplicar',
|
||||
deleteUnpublishedChanges: 'Excluir alterações não publicadas',
|
||||
deleteUnpublishedEntry: 'Excluir entrada não publicada',
|
||||
deletePublishedEntry: 'Excluir entrada publicada',
|
||||
deleteEntry: 'Excluir entrada',
|
||||
saving: 'Salvando...',
|
||||
save: 'Salvar',
|
||||
@ -266,11 +259,8 @@ const pt = {
|
||||
'Ops, você perdeu um campo obrigatório. Por favor, preencha antes de salvar.',
|
||||
entrySaved: 'Entrada salva',
|
||||
entryPublished: 'Entrada publicada',
|
||||
entryUnpublished: 'Entrada despublicada',
|
||||
onFailToPublishEntry: 'Falha ao publicar: %{details}',
|
||||
onFailToUnpublishEntry: 'Falha ao cancelar a publicação da entrada: %{details}',
|
||||
entryUpdated: 'Status da entrada atualizado',
|
||||
onDeleteUnpublishedChanges: 'Alterações não publicadas excluídas',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Você foi desconectado. Por favor, salve as alterações e entre novamente',
|
||||
onBackendDown: 'O serviço de back-end está fora do ar. Veja %{details} para mais informações',
|
||||
|
@ -106,10 +106,6 @@ const ro = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Ești sigur/ă că dorești să ștergi această publicare, dar și modificările nesalvate din sesiunea curentă?',
|
||||
onDeletePublishedEntry: 'Ești sigur/ă că dorești să ștergi această publicare?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Se vor șterge toate modificările nepublicate din aceast articol și modificările nesalvate din sesiunea curentă. Continui cu ștergerea?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Toate modificările nepublicate din acest articol vor fi șterse. Continui cu ștergerea?',
|
||||
loadingEntry: 'Se încarcă...',
|
||||
confirmLoadBackup:
|
||||
'Un backup local a fost recuperat pentru această intrare, dorești să îl folosești?',
|
||||
@ -128,9 +124,6 @@ const ro = {
|
||||
unpublishing: 'Se anulează publicarea...',
|
||||
publishAndCreateNew: 'Publicare apoi crează altul',
|
||||
publishAndDuplicate: 'Publicare apoi duplifică',
|
||||
deleteUnpublishedChanges: 'Șterge modificări nepublicate',
|
||||
deleteUnpublishedEntry: 'Șterge intrarea nepublicată',
|
||||
deletePublishedEntry: 'Șterge intrarea publicată',
|
||||
deleteEntry: 'Șterge intrare',
|
||||
saving: 'Se salvează...',
|
||||
save: 'Salvează',
|
||||
@ -257,11 +250,8 @@ const ro = {
|
||||
missingRequiredField: 'Oops, ai ratat un câmp obligatoriu. Completează-l pentru a salva.',
|
||||
entrySaved: 'Intrare salvată',
|
||||
entryPublished: 'Intrare publicată',
|
||||
entryUnpublished: 'Publicare anulată',
|
||||
onFailToPublishEntry: 'A eșuat publicarea: %{details}',
|
||||
onFailToUnpublishEntry: 'A eșuat anularea publicării: %{details}',
|
||||
entryUpdated: 'S-a actualizat status-ul intrării',
|
||||
onDeleteUnpublishedChanges: 'Modificări nepublicate șterse',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Ai fost delogat, te rugăm salvează orice date și autentifică-te din nou.',
|
||||
onBackendDown: 'Există probleme la server. Vezi %{details} pentru mai multe informații.',
|
||||
|
@ -106,10 +106,6 @@ const ru = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Вы уверены, что хотите удалить эту опубликованную запись, а также несохраненные изменения из текущего сеанса?',
|
||||
onDeletePublishedEntry: 'Вы уверены, что хотите удалить эту опубликованную запись?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Это удалит все неопубликованные изменения в этой записи, а также ваши несохраненные изменения из текущего сеанса. Вы все еще хотите удалить?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Все неопубликованные изменения в этой записи будут удалены. Вы все еще хотите удалить?',
|
||||
loadingEntry: 'Загрузка записи…',
|
||||
confirmLoadBackup:
|
||||
'Для этой записи была восстановлена локальная резервная копия, хотите ли вы ее использовать?',
|
||||
@ -123,9 +119,6 @@ const ru = {
|
||||
unpublishing: 'Отмена публикации…',
|
||||
publishAndCreateNew: 'Опубликовать и создать новую',
|
||||
publishAndDuplicate: 'Опубликовать и дублировать',
|
||||
deleteUnpublishedChanges: 'Удалить неопубликованные изменения',
|
||||
deleteUnpublishedEntry: 'Удалить неопубликованную запись',
|
||||
deletePublishedEntry: 'Удалить опубликованную запись',
|
||||
deleteEntry: 'Удалить запись',
|
||||
saving: 'Сохранение…',
|
||||
save: 'Сохранить',
|
||||
@ -254,11 +247,8 @@ const ru = {
|
||||
'К сожалению, вы пропустили обязательное поле. Пожалуйста, заполните перед сохранением.',
|
||||
entrySaved: 'Запись сохранена',
|
||||
entryPublished: 'Запись опубликована',
|
||||
entryUnpublished: 'Публикация записи отменена',
|
||||
onFailToPublishEntry: 'Не удалось опубликовать запись: %{details}',
|
||||
onFailToUnpublishEntry: 'Не удалось отменить публикацию записи: %{details}',
|
||||
entryUpdated: 'Статус записи обновлен',
|
||||
onDeleteUnpublishedChanges: 'Неопубликованные изменения удалены',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Вы вышли. Пожалуйста, сохраните все данные и войдите снова',
|
||||
onBackendDown: 'Происходят перебои в работе бекенда. См. %{details}',
|
||||
|
@ -106,10 +106,6 @@ const sv = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Är du säker på att du vill radera det här publicerade inlägget, inklusive dina osparade ändringar från nuvarande session?',
|
||||
onDeletePublishedEntry: 'Är du säker på att du vill radera det här publicerade inlägget?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Du är på väg att radera alla opublicerade ändringar för det här inlägget, inklusive dina osparade ändringar från nuvarande session. Vill du fortfarande radera?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Alla opublicerade ändringar kommer raderas. Vill du fortfarande radera?',
|
||||
loadingEntry: 'Hämtar inlägg...',
|
||||
confirmLoadBackup: 'En lokal kopia hittades för det här inlägget, vill du använda den?',
|
||||
},
|
||||
@ -127,9 +123,6 @@ const sv = {
|
||||
unpublishing: 'Avpublicerar...',
|
||||
publishAndCreateNew: 'Publicera och skapa ny',
|
||||
publishAndDuplicate: 'Publicera och duplicera',
|
||||
deleteUnpublishedChanges: 'Radera opublicerade ändringar',
|
||||
deleteUnpublishedEntry: 'Radera opublicerat inlägg',
|
||||
deletePublishedEntry: 'Radera publicerat inlägg',
|
||||
deleteEntry: 'Radera inlägg',
|
||||
saving: 'Sparar...',
|
||||
save: 'Spara',
|
||||
@ -258,11 +251,8 @@ const sv = {
|
||||
'Oops, du har missat ett obligatoriskt fält. Vänligen fyll i det innan du sparar.',
|
||||
entrySaved: 'Inlägg sparat',
|
||||
entryPublished: 'Inlägg publicerat',
|
||||
entryUnpublished: 'Inlägg avpublicerat',
|
||||
onFailToPublishEntry: 'Kunde inte publicera: %{details}',
|
||||
onFailToUnpublishEntry: 'Kunde inte avpublicera inlägg: %{details}',
|
||||
entryUpdated: 'Inläggsstatus uppdaterad',
|
||||
onDeleteUnpublishedChanges: 'Opublicerade ändringar raderade',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut:
|
||||
'Du har blivit utloggad, vänligen spara en kopia av eventuella ändringar och logga in på nytt',
|
||||
|
@ -100,9 +100,6 @@ const th = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'คุณแน่ใจหรือว่าจะต้องการลบการเผยแพร่เนื้อหานี้ รวมถึงการเปลี่ยนแปลงที่ยังไม่ได้บันทึก?',
|
||||
onDeletePublishedEntry: 'คุณแน่ใจหรือว่าจะต้องการลบการเผยแพร่เนื้อหานี้?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'คุณแน่ใจหรือว่าจะต้องการลบเนื้อหาที่ยังไม่ได้เผยแพร่ทั้งหมดนี้ รวมถึงการเปลี่ยนแปลงที่ยังไม่ได้บันทึก?',
|
||||
onDeleteUnpublishedChanges: 'คุณแน่ใจหรือว่าจะต้องการลบเนื้อหาที่ยังไม่ได้เผยแพร่ทั้งหมดนี้?',
|
||||
loadingEntry: 'กำลังโหลดเนื้อหา...',
|
||||
confirmLoadBackup: 'ข้อมูลสำรองได้ถูกกู้คืนสำหรับเนื้อหานี้ คุณต้องการใช้มันไหม?',
|
||||
},
|
||||
@ -115,9 +112,6 @@ const th = {
|
||||
unpublishing: 'ไม่ทำการเผยแพร่...',
|
||||
publishAndCreateNew: 'เผยแพร่ และ สร้างใหม่',
|
||||
publishAndDuplicate: 'เผยแพร่ และ ทำซ้ำ',
|
||||
deleteUnpublishedChanges: 'ลบการเปลี่ยแปลงเนื้อหาที่ยังไม่ได้เผยแพร่',
|
||||
deleteUnpublishedEntry: 'ลบเนื้อหาที่ยังไม่ได้เผยแพร่',
|
||||
deletePublishedEntry: 'ลบเนื้อหาที่เผยแพร่',
|
||||
deleteEntry: 'ลบเนื้อหา',
|
||||
saving: 'กำลังบันทึก...',
|
||||
save: 'บันทึก',
|
||||
@ -223,11 +217,9 @@ const th = {
|
||||
missingRequiredField: 'คุณไม่ได้ใส่ข้อมูลในช่องที่ต้องการ กรุณาใส่ข้อมูลก่อนบันทึก',
|
||||
entrySaved: 'เนื้อหาถูกบันทึก',
|
||||
entryPublished: 'เนื้อหาถูกเผยแพร่',
|
||||
entryUnpublished: 'เนื้อหาไม่ได้ถูกเผยแพร่',
|
||||
onFailToPublishEntry: 'ล้มเหลวในการเผยแพร่เนื้อหา: %{details}',
|
||||
onFailToUnpublishEntry: 'ล้มเหลวในการไม่เผยแพร่เนื้อหา: %{details}',
|
||||
entryUpdated: 'สถานะเนื้อหาถูกอัปเดต',
|
||||
onDeleteUnpublishedChanges: 'การเปลี่ยนแปลงเนื้อหาไม่ถูกเผยแพร่ได้ถูกลบ',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'คุณได้ออกจากระบบ โปรดสำรองข้อมูลแล้วเข้าสู่ระบบอีกครั้ง',
|
||||
onBackendDown: 'บริการแบ็กเอนด์เกิดการขัดข้อง ดู %{details} สำหรับข้อมูลเพิ่มเติม',
|
||||
|
@ -110,10 +110,6 @@ const tr = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Bu oturumda kaydedilmiş değişikliklerin yanı sıra geçerli oturumdaki kaydedilmemiş değişikliklerinizi silmek istediğinize emin misiniz?',
|
||||
onDeletePublishedEntry: 'Bu yayınlanmış girdiyi silmek istediğinize emin misiniz?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Bu girdide yayınlanmamış tüm değişiklikleri ve geçerli oturumdaki kaydedilmemiş değişikliklerinizi siler. Hala silmek istiyor musun?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Bu girdide yayınlanmamış tüm değişiklikler silinecek. Hala silmek istiyor musun?',
|
||||
loadingEntry: 'Girdiler yükleniyor...',
|
||||
confirmLoadBackup: 'Bu girdi için yerel bir yedekleme kurtarıldı, kullanmak ister misiniz?',
|
||||
},
|
||||
@ -131,9 +127,6 @@ const tr = {
|
||||
unpublishing: 'Yayından kaldırılıyor...',
|
||||
publishAndCreateNew: 'Yayınla ve yeni oluştur',
|
||||
publishAndDuplicate: 'Yayınla ve kopya oluştur',
|
||||
deleteUnpublishedChanges: 'Yayımlanmamış değişiklikleri sil',
|
||||
deleteUnpublishedEntry: 'Yayımlanmamış girdiyi sil',
|
||||
deletePublishedEntry: 'Yayınlanan girdiyi sil',
|
||||
deleteEntry: 'Girdiyi sil',
|
||||
saving: 'Kaydediliyor...',
|
||||
save: 'Kaydet',
|
||||
@ -269,11 +262,8 @@ const tr = {
|
||||
missingRequiredField: 'Gerekli bir alan eksik. Lütfen kaydetmeden önce tamamlayın.',
|
||||
entrySaved: 'Girdi kaydedildi',
|
||||
entryPublished: 'Girdi yayınlandı',
|
||||
entryUnpublished: 'Girdi yayınlanmamış',
|
||||
onFailToPublishEntry: 'Yayınlanamadı: %{details}',
|
||||
onFailToUnpublishEntry: 'Girdi yayından kaldırılamadı: %{details}',
|
||||
entryUpdated: 'Girdi durumu güncellendi',
|
||||
onDeleteUnpublishedChanges: 'Yayımlanmamış değişiklikler silindi',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Çıkış yaptınız, lütfen tüm verileri yedekleyin ve tekrar giriş yapın',
|
||||
onBackendDown:
|
||||
|
@ -59,10 +59,6 @@ const uk = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Ви дійсно бажаєте видалити опублікований запис, як і всі незбережені зміни під час поточної сесії?',
|
||||
onDeletePublishedEntry: 'Ви дійсно бажаєте видалити опублікований запис?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Видаляться всі неопубліковані зміни до цього запису, а також всі незбережені зміни під час поточної сесії. Бажаєте продовжити?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Всі незбережені зміни до цього запису буде видалено. Бажаєте продовжити?',
|
||||
loadingEntry: 'Завантаження...',
|
||||
confirmLoadBackup: 'Відновлено резервну копію, бажаєте її використати?',
|
||||
},
|
||||
@ -71,9 +67,6 @@ const uk = {
|
||||
publish: 'Опублікувати',
|
||||
published: 'Опубліковано',
|
||||
publishAndCreateNew: 'Опублікувати і створити нову',
|
||||
deleteUnpublishedChanges: 'Видалити неопубліковані зміни',
|
||||
deleteUnpublishedEntry: 'Видалити неопубліковану сторінку',
|
||||
deletePublishedEntry: 'Видалити опубліковану сторінку',
|
||||
deleteEntry: 'Видалити',
|
||||
saving: 'Збереження...',
|
||||
save: 'Зберегти',
|
||||
@ -168,7 +161,6 @@ const uk = {
|
||||
entryPublished: 'Опубліковано',
|
||||
onFailToPublishEntry: 'Помилка публікації: %{details}',
|
||||
entryUpdated: 'Статус оновлено',
|
||||
onDeleteUnpublishedChanges: 'Видалено неопубліковані зміни',
|
||||
onFailToAuth: '%{details}',
|
||||
},
|
||||
},
|
||||
|
@ -96,10 +96,6 @@ const vi = {
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Bạn có chắc rằng bạn muốn xoá mục đã được công bố này, cũng như là những thay đổi chưa lưu của bạn trong phiên làm việc này?',
|
||||
onDeletePublishedEntry: 'Bạn có chắc rằng bạn muốn xoá mục đã được công bố này?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'Điều này sẽ xoá tất cả những thay đổi chưa được lưu trong mục này, cũng như là những thay đổi chưa được lưu của bạn trong phiên làm việc này. Bạn vẫn muốn xoá chứ?',
|
||||
onDeleteUnpublishedChanges:
|
||||
'Tất cả những thay đổi chưa được lưu trong mục này sẽ bị xoá. Bạn vẫn muốn xoá chứ?',
|
||||
loadingEntry: 'Đang tải...',
|
||||
confirmLoadBackup:
|
||||
'Một bản sao lưu trên máy đã được phục hồi cho mục này, bạn có muốn tải lên không?',
|
||||
@ -113,9 +109,6 @@ const vi = {
|
||||
unpublishing: 'Đang ngừng công bố...',
|
||||
publishAndCreateNew: 'Công bố và tạo mới',
|
||||
publishAndDuplicate: 'Công bố và sao chép',
|
||||
deleteUnpublishedChanges: 'Xoá thay đổi chưa công bố này',
|
||||
deleteUnpublishedEntry: 'Xoá mục chưa được công bố này',
|
||||
deletePublishedEntry: 'Xoá mục đã được công bố này',
|
||||
deleteEntry: 'Xoá mục này',
|
||||
saving: 'Đang lưu...',
|
||||
save: 'Lưu',
|
||||
@ -221,11 +214,9 @@ const vi = {
|
||||
missingRequiredField: 'Bạn còn thiếu vài thông tin bắt buộc. Hãy hoàn thành trước khi lưu.',
|
||||
entrySaved: 'Mục đã được lưu',
|
||||
entryPublished: 'Mục đã được công bố',
|
||||
entryUnpublished: 'Mục đã ngừng công bố',
|
||||
onFailToPublishEntry: 'Không thể công bố: %{details}',
|
||||
onFailToUnpublishEntry: 'Không thể ngừng công bố mục: %{details}',
|
||||
entryUpdated: 'Trạng thái của mục đã được cập nhật',
|
||||
onDeleteUnpublishedChanges: 'Những thay đổi chưa được công bố đã được xoá',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: 'Bạn đã đăng xuất, hãy sao lưu dữ liệu và đăng nhập lại',
|
||||
onBackendDown: 'Dịch vụ backend đang gặp trục trặc. Hãy xem {details} để biết thêm thông tin',
|
||||
|
@ -103,9 +103,6 @@ const zh_Hans = {
|
||||
onUnpublishing: '你确定要撤销发布此内容吗?',
|
||||
onDeleteWithUnsavedChanges: '你确定要删除这个已经发布的内容,以及当前尚未保存的修改吗?',
|
||||
onDeletePublishedEntry: '你确定要删除这个已经发布的内容吗?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'此内容所有未被发布的修改,以及当前尚未保存的修改都将被删除,你确定吗?',
|
||||
onDeleteUnpublishedChanges: '此内容所有未被发布的修改都将被删除,你确定吗?',
|
||||
loadingEntry: '正在加载内容...',
|
||||
confirmLoadBackup: '发现了一个对应此内容的本地备份,你要加载它吗?',
|
||||
},
|
||||
@ -123,9 +120,6 @@ const zh_Hans = {
|
||||
unpublishing: '正在撤销发布...',
|
||||
publishAndCreateNew: '发布,然后新建内容',
|
||||
publishAndDuplicate: '发布,然后复制内容',
|
||||
deleteUnpublishedChanges: '删除未发布的修改',
|
||||
deleteUnpublishedEntry: '删除未发布的内容',
|
||||
deletePublishedEntry: '删除已发布的内容',
|
||||
deleteEntry: '删除内容',
|
||||
saving: '正在保存...',
|
||||
save: '保存',
|
||||
@ -252,11 +246,9 @@ const zh_Hans = {
|
||||
missingRequiredField: '你漏掉了一个必填项,请在保存之前将它填写好',
|
||||
entrySaved: '内容已保存',
|
||||
entryPublished: '内容已发布',
|
||||
entryUnpublished: '内容已撤销发布',
|
||||
onFailToPublishEntry: '发布失败: %{details}',
|
||||
onFailToUnpublishEntry: '撤销发布失败: %{details}',
|
||||
entryUpdated: '内容状态已更新',
|
||||
onDeleteUnpublishedChanges: '未发布的修改已删除',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: '你已注销,请先保存好数据然后再次登录',
|
||||
onBackendDown: 'Backend 服务已中断,欲知详情请查看:%{details}',
|
||||
|
@ -97,9 +97,6 @@ const zh_Hant = {
|
||||
onUnpublishing: '你確定要取消發表此內容嗎?',
|
||||
onDeleteWithUnsavedChanges: '你確定要刪除這篇已發布的內容以及你尚未儲存的變更?',
|
||||
onDeletePublishedEntry: '你確定要刪除這篇已發布的內容?',
|
||||
onDeleteUnpublishedChangesWithUnsavedChanges:
|
||||
'這將會刪除此內容所有未發布的變更,以及未儲存的變更。你確定還是要刪除?',
|
||||
onDeleteUnpublishedChanges: '此內容所有未發布的變更都將會被刪除。你確定還是要刪除?',
|
||||
loadingEntry: '載入內容中...',
|
||||
confirmLoadBackup: '此內容的本地備份已經還原,你想要使用嗎?',
|
||||
},
|
||||
@ -112,9 +109,6 @@ const zh_Hant = {
|
||||
unpublishing: '取消發布中...',
|
||||
publishAndCreateNew: '發布並建立內容',
|
||||
publishAndDuplicate: '發布並複製內容',
|
||||
deleteUnpublishedChanges: '刪除未發布的變更',
|
||||
deleteUnpublishedEntry: '刪除未發布的內容',
|
||||
deletePublishedEntry: '刪除已發布的內容',
|
||||
deleteEntry: '刪除內容',
|
||||
saving: '儲存中...',
|
||||
save: '儲存',
|
||||
@ -230,11 +224,8 @@ const zh_Hant = {
|
||||
missingRequiredField: '糟了!你漏填了一個必須填入的欄位,在儲存前請先填完所有內容',
|
||||
entrySaved: '已儲存內容',
|
||||
entryPublished: '已發布內容',
|
||||
entryUnpublished: '已取消發布內容',
|
||||
onFailToPublishEntry: '無法發布: %{details}',
|
||||
onFailToUnpublishEntry: '無法取消發布: %{details}',
|
||||
entryUpdated: '內容狀態已更新',
|
||||
onDeleteUnpublishedChanges: '已刪除未發布的變更',
|
||||
onFailToAuth: '%{details}',
|
||||
onLoggedOut: '你已經登出,請備份任何資料然後重新登入',
|
||||
onBackendDown: '後端服務發生中斷。看 %{details} 取得更多資訊',
|
||||
|
@ -9,7 +9,7 @@ import { store } from './store';
|
||||
import { configFailed } from './actions/config';
|
||||
import { createMediaLibrary, insertMedia } from './actions/mediaLibrary';
|
||||
|
||||
import type { MediaLibraryInstance } from './types/redux';
|
||||
import type { MediaLibraryInstance, State } from './types/redux';
|
||||
|
||||
type MediaLibraryOptions = {};
|
||||
|
||||
@ -40,7 +40,7 @@ const initializeMediaLibrary = once(async function initializeMediaLibrary(name,
|
||||
});
|
||||
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
const state = store.getState() as unknown as State;
|
||||
if (state) {
|
||||
const mediaLibraryName = state.config.media_library?.name;
|
||||
if (mediaLibraryName && !state.mediaLibrary.get('externalLibrary')) {
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { CONFIG_FAILURE, CONFIG_REQUEST, CONFIG_SUCCESS } from '../actions/config';
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import { CmsConfig } from '../interface';
|
||||
import type { CmsConfig } from '../interface';
|
||||
|
||||
interface ConfigState extends Partial<CmsConfig> {
|
||||
export interface ConfigState extends Partial<CmsConfig> {
|
||||
isFetching: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@ -36,8 +35,4 @@ export function selectLocale(state: CmsConfig) {
|
||||
return state.locale || 'en';
|
||||
}
|
||||
|
||||
export function selectUseWorkflow(state: CmsConfig) {
|
||||
return state.publish_mode === EDITORIAL_WORKFLOW;
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
DEPLOY_PREVIEW_REQUEST,
|
||||
DEPLOY_PREVIEW_SUCCESS,
|
||||
DEPLOY_PREVIEW_FAILURE,
|
||||
} from '../actions/deploys';
|
||||
|
||||
import type { DeploysAction } from '../actions/deploys';
|
||||
|
||||
export type Deploys = {
|
||||
[key: string]: {
|
||||
isFetching: boolean;
|
||||
url?: string;
|
||||
status?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const defaultState: Deploys = {};
|
||||
|
||||
const deploys = produce((state: Deploys, action: DeploysAction) => {
|
||||
switch (action.type) {
|
||||
case DEPLOY_PREVIEW_REQUEST: {
|
||||
const { collection, slug } = action.payload;
|
||||
const key = `${collection}.${slug}`;
|
||||
state[key] = state[key] || {};
|
||||
state[key].isFetching = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case DEPLOY_PREVIEW_SUCCESS: {
|
||||
const { collection, slug, url, status } = action.payload;
|
||||
const key = `${collection}.${slug}`;
|
||||
state[key].isFetching = false;
|
||||
state[key].url = url;
|
||||
state[key].status = status;
|
||||
break;
|
||||
}
|
||||
|
||||
case DEPLOY_PREVIEW_FAILURE: {
|
||||
const { collection, slug } = action.payload;
|
||||
state[`${collection}.${slug}`].isFetching = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectDeployPreview(state: Deploys, collection: string, slug: string) {
|
||||
return state[`${collection}.${slug}`];
|
||||
}
|
||||
|
||||
export default deploys;
|
@ -1,163 +0,0 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { startsWith } from 'lodash';
|
||||
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_REQUEST,
|
||||
UNPUBLISHED_ENTRY_REDIRECT,
|
||||
UNPUBLISHED_ENTRY_SUCCESS,
|
||||
UNPUBLISHED_ENTRIES_REQUEST,
|
||||
UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
UNPUBLISHED_ENTRY_DELETE_SUCCESS,
|
||||
} from '../actions/editorialWorkflow';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
import type { EditorialWorkflowAction, EditorialWorkflow, Entities } from '../types/redux';
|
||||
|
||||
function unpublishedEntries(state = Map(), action: EditorialWorkflowAction) {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const publishMode = action.payload && action.payload.publish_mode;
|
||||
if (publishMode === EDITORIAL_WORKFLOW) {
|
||||
// Editorial workflow state is explicitly initiated after the config.
|
||||
return Map({ entities: Map(), pages: Map() });
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case UNPUBLISHED_ENTRY_REQUEST:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isFetching'],
|
||||
true,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_REDIRECT:
|
||||
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
|
||||
|
||||
case UNPUBLISHED_ENTRY_SUCCESS:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.entry.slug}`],
|
||||
fromJS(action.payload!.entry),
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRIES_REQUEST:
|
||||
return state.setIn(['pages', 'isFetching'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRIES_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
action.payload!.entries.forEach(entry =>
|
||||
map.setIn(
|
||||
['entities', `${entry.collection}.${entry.slug}`],
|
||||
fromJS(entry).set('isFetching', false),
|
||||
),
|
||||
);
|
||||
map.set(
|
||||
'pages',
|
||||
Map({
|
||||
...action.payload!.pages,
|
||||
ids: List(action.payload!.entries.map(entry => entry.slug)),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_PERSIST_REQUEST: {
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPersisting'],
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
case UNPUBLISHED_ENTRY_PERSIST_SUCCESS:
|
||||
// Update Optimistically
|
||||
return state.withMutations(map => {
|
||||
map.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.entry.get('slug')}`],
|
||||
fromJS(action.payload!.entry),
|
||||
);
|
||||
map.deleteIn([
|
||||
'entities',
|
||||
`${action.payload!.collection}.${action.payload!.entry.get('slug')}`,
|
||||
'isPersisting',
|
||||
]);
|
||||
map.updateIn(['pages', 'ids'], List(), list =>
|
||||
list.push(action.payload!.entry.get('slug')),
|
||||
);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_PERSIST_FAILURE:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPersisting'],
|
||||
false,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST:
|
||||
// Update Optimistically
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
|
||||
true,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'status'],
|
||||
action.payload!.newStatus,
|
||||
);
|
||||
map.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
|
||||
false,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPublishing'],
|
||||
true,
|
||||
);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
|
||||
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
|
||||
|
||||
case UNPUBLISHED_ENTRY_DELETE_SUCCESS:
|
||||
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_FAILURE:
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntry(state: EditorialWorkflow, collection: string, slug: string) {
|
||||
return state && state.getIn(['entities', `${collection}.${slug}`]);
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntriesByStatus(state: EditorialWorkflow, status: string) {
|
||||
if (!state) return null;
|
||||
const entities = state.get('entities') as Entities;
|
||||
return entities.filter(entry => entry.get('status') === status).valueSeq();
|
||||
}
|
||||
|
||||
export function selectUnpublishedSlugs(state: EditorialWorkflow, collection: string) {
|
||||
if (!state.get('entities')) return null;
|
||||
const entities = state.get('entities') as Entities;
|
||||
return entities
|
||||
.filter((_v, k) => startsWith(k as string, `${collection}.`))
|
||||
.map(entry => entry.get('slug'))
|
||||
.valueSeq();
|
||||
}
|
||||
|
||||
export default unpublishedEntries;
|
@ -799,8 +799,7 @@ export function selectMediaFilePublicPath(
|
||||
|
||||
export function selectEditingDraft(state: EntryDraft) {
|
||||
const entry = state.get('entry');
|
||||
const workflowDraft = entry && !entry.isEmpty();
|
||||
return workflowDraft;
|
||||
return entry && !entry.isEmpty();
|
||||
}
|
||||
|
||||
export default entries;
|
||||
|
@ -20,17 +20,6 @@ import {
|
||||
ADD_DRAFT_ENTRY_MEDIA_FILE,
|
||||
REMOVE_DRAFT_ENTRY_MEDIA_FILE,
|
||||
} from '../actions/entries';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PERSIST_FAILURE,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
} from '../actions/editorialWorkflow';
|
||||
import { selectFolderEntryExtension, selectHasMetaPath } from './collections';
|
||||
import { getDataPath, duplicateI18nFields } from '../lib/i18n';
|
||||
|
||||
@ -135,32 +124,15 @@ function entryDraftReducer(state = Map(), action) {
|
||||
return state.set('fieldsErrors', Map());
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_REQUEST:
|
||||
case UNPUBLISHED_ENTRY_PERSIST_REQUEST: {
|
||||
case ENTRY_PERSIST_REQUEST: {
|
||||
return state.setIn(['entry', 'isPersisting'], true);
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_FAILURE:
|
||||
case UNPUBLISHED_ENTRY_PERSIST_FAILURE: {
|
||||
case ENTRY_PERSIST_FAILURE:{
|
||||
return state.deleteIn(['entry', 'isPersisting']);
|
||||
}
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST:
|
||||
return state.setIn(['entry', 'isUpdatingStatus'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE:
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
|
||||
return state.deleteIn(['entry', 'isUpdatingStatus']);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
|
||||
return state.setIn(['entry', 'isPublishing'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_FAILURE:
|
||||
return state.deleteIn(['entry', 'isPublishing']);
|
||||
|
||||
case ENTRY_PERSIST_SUCCESS:
|
||||
case UNPUBLISHED_ENTRY_PERSIST_SUCCESS:
|
||||
return state.withMutations(state => {
|
||||
state.deleteIn(['entry', 'isPersisting']);
|
||||
state.set('hasChanged', false);
|
||||
|
@ -1,28 +1,13 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { USE_OPEN_AUTHORING } from '../actions/auth';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
export type GlobalUI = {
|
||||
isFetching: boolean;
|
||||
useOpenAuthoring: boolean;
|
||||
};
|
||||
|
||||
const LOADING_IGNORE_LIST = [
|
||||
'DEPLOY_PREVIEW',
|
||||
'STATUS_REQUEST',
|
||||
'STATUS_SUCCESS',
|
||||
'STATUS_FAILURE',
|
||||
];
|
||||
|
||||
function ignoreWhenLoading(action: AnyAction) {
|
||||
return LOADING_IGNORE_LIST.some(type => action.type.includes(type));
|
||||
}
|
||||
|
||||
const defaultState: GlobalUI = {
|
||||
isFetching: false,
|
||||
useOpenAuthoring: false,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -30,15 +15,10 @@ const defaultState: GlobalUI = {
|
||||
*/
|
||||
const globalUI = produce((state: GlobalUI, action: AnyAction) => {
|
||||
// Generic, global loading indicator
|
||||
if (!ignoreWhenLoading(action) && action.type.includes('REQUEST')) {
|
||||
if (!action.type.includes('REQUEST')) {
|
||||
state.isFetching = true;
|
||||
} else if (
|
||||
!ignoreWhenLoading(action) &&
|
||||
(action.type.includes('SUCCESS') || action.type.includes('FAILURE'))
|
||||
) {
|
||||
} else if (action.type.includes('SUCCESS') || action.type.includes('FAILURE')) {
|
||||
state.isFetching = false;
|
||||
} else if (action.type === USE_OPEN_AUTHORING) {
|
||||
state.useOpenAuthoring = true;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
|
@ -5,18 +5,15 @@ import config from './config';
|
||||
import integrations, * as fromIntegrations from './integrations';
|
||||
import entries, * as fromEntries from './entries';
|
||||
import cursors from './cursors';
|
||||
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||
import entryDraft from './entryDraft';
|
||||
import collections from './collections';
|
||||
import search from './search';
|
||||
import medias from './medias';
|
||||
import mediaLibrary from './mediaLibrary';
|
||||
import deploys, * as fromDeploys from './deploys';
|
||||
import globalUI from './globalUI';
|
||||
import status from './status';
|
||||
import scroll from './scroll';
|
||||
|
||||
import type { Status } from '../constants/publishModes';
|
||||
import type { State, Collection } from '../types/redux';
|
||||
|
||||
const reducers = {
|
||||
@ -27,11 +24,9 @@ const reducers = {
|
||||
integrations,
|
||||
entries,
|
||||
cursors,
|
||||
editorialWorkflow,
|
||||
entryDraft,
|
||||
medias,
|
||||
mediaLibrary,
|
||||
deploys,
|
||||
globalUI,
|
||||
status,
|
||||
scroll,
|
||||
@ -61,22 +56,6 @@ export function selectSearchedEntries(state: State, availableCollections: string
|
||||
.map(entryId => fromEntries.selectEntry(state.entries, entryId!.collection, entryId!.slug));
|
||||
}
|
||||
|
||||
export function selectDeployPreview(state: State, collection: string, slug: string) {
|
||||
return fromDeploys.selectDeployPreview(state.deploys, collection, slug);
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntry(state: State, collection: string, slug: string) {
|
||||
return fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, collection, slug);
|
||||
}
|
||||
|
||||
export function selectUnpublishedEntriesByStatus(state: State, status: Status) {
|
||||
return fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status);
|
||||
}
|
||||
|
||||
export function selectUnpublishedSlugs(state: State, collection: string) {
|
||||
return fromEditorialWorkflow.selectUnpublishedSlugs(state.editorialWorkflow, collection);
|
||||
}
|
||||
|
||||
export function selectIntegration(state: State, collection: string | null, hook: string) {
|
||||
return fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { fromJS } from 'immutable';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import { CmsConfig } from '../interface';
|
||||
import type { CmsConfig } from '../interface';
|
||||
import type { Integrations } from '../types/redux';
|
||||
|
||||
interface Acc {
|
||||
|
@ -83,7 +83,7 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
|
||||
replaceIndex,
|
||||
});
|
||||
}
|
||||
return state.withMutations(map => {
|
||||
return state.withMutations((map: Map<string, any>) => {
|
||||
map.set('isVisible', true);
|
||||
map.set('forImage', forImage);
|
||||
map.set('controlID', controlID);
|
||||
@ -151,7 +151,7 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
|
||||
}
|
||||
|
||||
const filesWithKeys = files.map(file => ({ ...file, key: uuid() }));
|
||||
return state.withMutations(map => {
|
||||
return state.withMutations((map: Map<string, any>) => {
|
||||
map.set('isLoading', false);
|
||||
map.set('isPaginating', false);
|
||||
map.set('page', page);
|
||||
|
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
import type { CmsConfig } from './interface';
|
||||
|
||||
|
@ -4,7 +4,6 @@ import type { MediaFile as BackendMediaFile } from '../backend';
|
||||
import type { formatExtensions } from '../formats/formats';
|
||||
import type { CmsConfig, CmsSortableFields, SortDirection, ViewFilter, ViewGroup } from '../interface';
|
||||
import type { Auth } from '../reducers/auth';
|
||||
import type { Deploys } from '../reducers/deploys';
|
||||
import type { GlobalUI } from '../reducers/globalUI';
|
||||
import type { Medias } from '../reducers/medias';
|
||||
import type { ScrollState } from '../reducers/scroll';
|
||||
@ -57,8 +56,6 @@ export type CmsCollectionFormatType =
|
||||
|
||||
export type CmsAuthScope = 'repo' | 'public_repo';
|
||||
|
||||
export type CmsPublishMode = 'simple' | 'editorial_workflow';
|
||||
|
||||
export type CmsSlugEncoding = 'unicode' | 'ascii';
|
||||
|
||||
export interface CmsI18nConfig {
|
||||
@ -295,15 +292,12 @@ export interface CmsCollectionFile {
|
||||
export interface CmsBackend {
|
||||
name: CmsBackendType;
|
||||
auth_scope?: CmsAuthScope;
|
||||
open_authoring?: boolean;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
site_domain?: string;
|
||||
base_url?: string;
|
||||
auth_endpoint?: string;
|
||||
cms_label_prefix?: string;
|
||||
squash_merges?: boolean;
|
||||
proxy_url?: string;
|
||||
commit_messages?: {
|
||||
create?: string;
|
||||
@ -311,7 +305,6 @@ export interface CmsBackend {
|
||||
delete?: string;
|
||||
uploadMedia?: string;
|
||||
deleteMedia?: string;
|
||||
openAuthoring?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -342,10 +335,8 @@ export type SlugConfig = StaticallyTypedRecord<{
|
||||
type BackendObject = {
|
||||
name: string;
|
||||
repo?: string | null;
|
||||
open_authoring?: boolean;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
squash_merges?: boolean;
|
||||
use_graphql?: boolean;
|
||||
preview_context?: string;
|
||||
identity_url?: string;
|
||||
@ -361,7 +352,6 @@ export type Config = StaticallyTypedRecord<{
|
||||
backend: Backend;
|
||||
media_folder: string;
|
||||
public_folder: string;
|
||||
publish_mode?: string;
|
||||
media_library: StaticallyTypedRecord<{ name: string }> & { name: string };
|
||||
locale?: string;
|
||||
slug: SlugConfig;
|
||||
@ -369,7 +359,6 @@ export type Config = StaticallyTypedRecord<{
|
||||
base_url?: string;
|
||||
site_id?: string;
|
||||
site_url?: string;
|
||||
show_preview_links?: boolean;
|
||||
isFetching?: boolean;
|
||||
integrations: List<Integration>;
|
||||
collections: List<StaticallyTypedRecord<{ name: string }>>;
|
||||
@ -415,11 +404,6 @@ export type Entries = StaticallyTypedRecord<{
|
||||
viewStyle: string;
|
||||
}>;
|
||||
|
||||
export type EditorialWorkflow = StaticallyTypedRecord<{
|
||||
pages: Pages & PagesObject;
|
||||
entities: Entities & EntitiesObject;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type EntryObject = {
|
||||
path: string;
|
||||
@ -590,9 +574,7 @@ export interface State {
|
||||
config: CmsConfig;
|
||||
cursors: Cursors;
|
||||
collections: Collections;
|
||||
deploys: Deploys;
|
||||
globalUI: GlobalUI;
|
||||
editorialWorkflow: EditorialWorkflow;
|
||||
entries: Entries;
|
||||
entryDraft: EntryDraft;
|
||||
integrations: Integrations;
|
||||
@ -690,23 +672,3 @@ export interface EntriesAction extends Action<string> {
|
||||
collection: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EditorialWorkflowAction extends Action<string> {
|
||||
payload?: CmsConfig & {
|
||||
collection: string;
|
||||
entry: { slug: string };
|
||||
} & {
|
||||
collection: string;
|
||||
slug: string;
|
||||
} & {
|
||||
pages: [];
|
||||
entries: { collection: string; slug: string }[];
|
||||
} & {
|
||||
collection: string;
|
||||
entry: StaticallyTypedRecord<{ slug: string }>;
|
||||
} & {
|
||||
collection: string;
|
||||
slug: string;
|
||||
newStatus: string;
|
||||
};
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ import iconScroll from './scroll.svg';
|
||||
import iconSearch from './search.svg';
|
||||
import iconSettings from './settings.svg';
|
||||
import iconUser from './user.svg';
|
||||
import iconWorkflow from './workflow.svg';
|
||||
import iconWrite from './write.svg';
|
||||
|
||||
const iconix = iconAdd;
|
||||
@ -94,7 +93,6 @@ const images = {
|
||||
search: iconSearch,
|
||||
settings: iconSettings,
|
||||
user: iconUser,
|
||||
workflow: iconWorkflow,
|
||||
write: iconWrite,
|
||||
};
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><path id="icon-workflow@2x-a" d="M10,4 L13,4 C13.5522847,4 14,4.44771525 14,5 L14,14 L14,14 C14,14.5522847 13.5522847,15 13,15 L10,15 L10,15 C9.44771525,15 9,14.5522847 9,14 L9,5 L9,5 C9,4.44771525 9.44771525,4 10,4 Z M17,4 L20,4 C20.5522847,4 21,4.44771525 21,5 L21,15 C21,15.5522847 20.5522847,16 20,16 L17,16 C16.4477153,16 16,15.5522847 16,15 L16,5 L16,5 C16,4.44771525 16.4477153,4 17,4 Z M3,4 L6,4 C6.55228475,4 7,4.44771525 7,5 L7,19 C7,19.5522847 6.55228475,20 6,20 L3,20 L3,20 C2.44771525,20 2,19.5522847 2,19 L2,5 L2,5 C2,4.44771525 2.44771525,4 3,4 L3,4 Z"/></svg>
|
Before Width: | Height: | Size: 701 B |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user