From 10b442428a94ef003040a83e08691ff4038a17eb Mon Sep 17 00:00:00 2001 From: Daniel Lautzenheiser Date: Sat, 1 Oct 2022 13:45:01 -0400 Subject: [PATCH] Feature/remove editorial workflow (#8) --- dev-test/config.yml | 1 - index.d.ts | 68 +- package.json | 1 + src/actions/auth.ts | 14 - src/actions/config.ts | 32 +- src/actions/deploys.ts | 101 --- src/actions/editorialWorkflow.ts | 536 ----------- src/actions/waitUntil.ts | 8 +- src/backend.ts | 362 +------- src/backends/azure/API.ts | 351 +------- src/backends/azure/implementation.ts | 165 +--- src/backends/bitbucket/API.ts | 382 +------- src/backends/bitbucket/AuthenticationPage.js | 2 +- src/backends/bitbucket/implementation.ts | 187 +--- src/backends/git-gateway/GitHubAPI.ts | 11 +- src/backends/git-gateway/implementation.ts | 117 +-- src/backends/github/API.ts | 846 +----------------- src/backends/github/AuthenticationPage.js | 77 +- src/backends/github/GraphQLAPI.ts | 420 +-------- src/backends/github/implementation.tsx | 288 +----- src/backends/github/mutations.ts | 95 -- src/backends/github/queries.ts | 61 -- src/backends/gitlab/API.ts | 360 +------- src/backends/gitlab/implementation.ts | 190 +--- src/backends/proxy/implementation.ts | 104 +-- src/backends/test/implementation.ts | 175 +--- src/components/App/App.js | 9 - src/components/App/Header.js | 10 - src/components/Editor/Editor.js | 173 +--- .../EditorControlPane/EditorControlPane.js | 2 +- src/components/Editor/EditorInterface.js | 21 - .../EditorPreviewContent.tsx | 3 +- src/components/Editor/EditorToolbar.js | 318 +------ src/components/Editor/withWorkflow.js | 61 -- src/components/MediaLibrary/MediaLibrary.js | 2 +- src/components/UI/Alert.tsx | 4 +- src/components/UI/Confirm.tsx | 4 +- src/components/Workflow/Workflow.js | 166 ---- src/components/Workflow/WorkflowCard.js | 178 ---- src/components/Workflow/WorkflowList.js | 284 ------ src/components/page/Page.tsx | 3 +- src/constants/configSchema.js | 8 - src/constants/publishModes.ts | 22 - src/interface.ts | 64 +- src/lib/formatters.ts | 26 +- src/lib/registry.js | 2 - src/lib/util/API.ts | 39 - src/lib/util/APIUtils.ts | 30 - src/lib/util/EditorialWorkflowError.ts | 12 - src/lib/util/asyncLock.ts | 2 +- src/lib/util/backendUtil.ts | 4 +- src/lib/util/events/AlertEvent.ts | 2 +- src/lib/util/events/ConfirmEvent.ts | 2 +- src/lib/util/implementation.ts | 46 +- src/lib/util/index.ts | 128 +-- src/lib/util/window.util.ts | 17 +- src/locales/bg/index.js | 13 - src/locales/ca/index.js | 13 - src/locales/cs/index.js | 13 - src/locales/da/index.js | 13 - src/locales/de/index.js | 14 - src/locales/en/index.js | 58 -- src/locales/es/index.js | 10 - src/locales/fr/index.js | 10 - src/locales/gr/index.js | 10 - src/locales/he/index.js | 9 - src/locales/hr/index.js | 9 - src/locales/hu/index.js | 10 - src/locales/it/index.js | 10 - src/locales/ja/index.js | 10 - src/locales/ko/index.js | 10 - src/locales/lt/index.js | 10 - src/locales/nb_no/index.js | 10 - src/locales/nl/index.js | 10 - src/locales/nn_no/index.js | 10 - src/locales/pl/index.js | 10 - src/locales/pt/index.js | 10 - src/locales/ro/index.js | 10 - src/locales/ru/index.js | 10 - src/locales/sv/index.js | 10 - src/locales/th/index.js | 8 - src/locales/tr/index.js | 10 - src/locales/uk/index.js | 8 - src/locales/vi/index.js | 9 - src/locales/zh_Hans/index.js | 8 - src/locales/zh_Hant/index.js | 9 - src/mediaLibrary.ts | 4 +- src/reducers/config.ts | 9 +- src/reducers/deploys.ts | 52 -- src/reducers/editorialWorkflow.ts | 163 ---- src/reducers/entries.ts | 3 +- src/reducers/entryDraft.js | 32 +- src/reducers/globalUI.ts | 24 +- src/reducers/index.ts | 21 - src/reducers/integrations.ts | 2 +- src/reducers/mediaLibrary.ts | 4 +- src/types/global.d.ts | 2 + src/types/redux.ts | 38 - src/ui/Icon/images/_index.js | 2 - src/ui/Icon/images/workflow.svg | 1 - src/valueObjects/AssetProxy.ts | 2 +- things-to-remove.txt | 0 website/content/docs/add-to-your-site.md | 11 - website/content/docs/architecture.md | 13 - website/content/docs/backends-overview.md | 1 - website/content/docs/beta-features.md | 36 +- website/content/docs/bitbucket-backend.md | 2 +- website/content/docs/configuration-options.md | 76 +- website/content/docs/deploy-preview-links.md | 149 --- website/content/docs/github-backend.md | 16 - website/content/docs/open-authoring.md | 79 -- .../content/docs/site-generator-overview.md | 2 +- website/content/docs/writing-style-guide.md | 2 +- website/static/_redirects | 1 - website/static/admin/config.yml | 4 - yarn.lock | 5 + 116 files changed, 344 insertions(+), 7362 deletions(-) delete mode 100644 src/actions/deploys.ts delete mode 100644 src/actions/editorialWorkflow.ts delete mode 100644 src/components/Editor/withWorkflow.js delete mode 100644 src/components/Workflow/Workflow.js delete mode 100644 src/components/Workflow/WorkflowCard.js delete mode 100644 src/components/Workflow/WorkflowList.js delete mode 100644 src/constants/publishModes.ts delete mode 100644 src/lib/util/EditorialWorkflowError.ts delete mode 100644 src/reducers/deploys.ts delete mode 100644 src/reducers/editorialWorkflow.ts delete mode 100644 src/ui/Icon/images/workflow.svg create mode 100644 things-to-remove.txt delete mode 100644 website/content/docs/deploy-preview-links.md delete mode 100644 website/content/docs/open-authoring.md diff --git a/dev-test/config.yml b/dev-test/config.yml index 913396a4..350ccccf 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -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 diff --git a/index.d.ts b/index.d.ts index 9c4705ce..d43d953f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; data: Map; @@ -779,36 +743,6 @@ declare module '@simplecms/simple-cms-core' { persistMedia: (file: AssetProxy, opts: PersistOptions) => Promise; deleteFiles: (paths: string[], commitMessage: string) => Promise; - unpublishedEntries: () => Promise; - unpublishedEntry: (args: { - id?: string; - collection?: string; - slug?: string; - }) => Promise; - unpublishedEntryDataFile: ( - collection: string, - slug: string, - path: string, - id: string, - ) => Promise; - unpublishedEntryMediaFile: ( - collection: string, - slug: string, - path: string, - id: string, - ) => Promise; - updateUnpublishedEntryStatus: ( - collection: string, - slug: string, - newStatus: string, - ) => Promise; - publishUnpublishedEntry: (collection: string, slug: string) => Promise; - deleteUnpublishedEntry: (collection: string, slug: string) => Promise; - getDeployPreview: ( - collectionName: string, - slug: string, - ) => Promise<{ url: string; status: string } | null>; - allEntriesByFolder?: ( folder: string, extension: string, diff --git a/package.json b/package.json index 7b3cd42f..4798175a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/auth.ts b/src/actions/auth.ts index dc711452..e749c0a6 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -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) => { diff --git a/src/actions/config.ts b/src/actions/config.ts index 8897b762..79ca5839 100644 --- a/src/actions/config.ts +++ b/src/actions/config.ts @@ -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); - } }); } diff --git a/src/actions/deploys.ts b/src/actions/deploys.ts deleted file mode 100644 index c32459ac..00000000 --- a/src/actions/deploys.ts +++ /dev/null @@ -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, 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 ->; diff --git a/src/actions/editorialWorkflow.ts b/src/actions/editorialWorkflow.ts deleted file mode 100644 index ca6d6696..00000000 --- a/src/actions/editorialWorkflow.ts +++ /dev/null @@ -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, 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, 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, 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; - 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, 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, 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, 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, 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'))); - }); - }; -} diff --git a/src/actions/waitUntil.ts b/src/actions/waitUntil.ts index 524dae83..bd842f7a 100644 --- a/src/actions/waitUntil.ts +++ b/src/actions/waitUntil.ts @@ -17,17 +17,17 @@ export async function waitUntilWithTimeout( dispatch: ThunkDispatch, waitActionArgs: (resolve: (value?: T) => void) => WaitActionArgs, timeout = 30000, -): Promise { +): Promise { let waitDone = false; - const waitPromise = new Promise(resolve => { + const waitPromise = new Promise(resolve => { dispatch(waitUntil(waitActionArgs(resolve))); }); - const timeoutPromise = new Promise(resolve => { + const timeoutPromise = new Promise(resolve => { setTimeout(() => { if (waitDone) { - resolve(); + resolve(null); } else { console.warn('Wait Action timed out'); resolve(null); diff --git a/src/backend.ts b/src/backend.ts index 61e14a75..c3c44f69 100644 --- a/src/backend.ts +++ b/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; - 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); diff --git a/src/backends/azure/API.ts b/src/backends/azure/API.ts index 0b5998f3..68d9838e 100644 --- a/src/backends/azure/API.ts +++ b/src/backends/azure/API.ts @@ -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 { - 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>({ - url: `${this.endpointUrl}/pullrequests/${pullRequest.pullRequestId}/commits`, - params: { - $top: 1, - }, - }); - const { value: statuses } = await this.requestJSON>({ - 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>({ - 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 { - 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({ - 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({ - 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, - }); - } } diff --git a/src/backends/azure/implementation.ts b/src/backends/azure/implementation.ts index bd297605..94533576 100644 --- a/src/backends/azure/implementation.ts +++ b/src/backends/azure/implementation.ts @@ -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; - } - } } diff --git a/src/backends/bitbucket/API.ts b/src/backends/bitbucket/API.ts index f4b0fb3e..fca1ad00 100644 --- a/src/backends/bitbucket/API.ts +++ b/src/backends/bitbucket/API.ts @@ -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; hasWriteAccess?: () => Promise; - 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; 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; - } } diff --git a/src/backends/bitbucket/AuthenticationPage.js b/src/backends/bitbucket/AuthenticationPage.js index 561f2216..f5a05660 100644 --- a/src/backends/bitbucket/AuthenticationPage.js +++ b/src/backends/bitbucket/AuthenticationPage.js @@ -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; diff --git a/src/backends/bitbucket/implementation.ts b/src/backends/bitbucket/implementation.ts index 46f80d0b..e4c0ce33 100644 --- a/src/backends/bitbucket/implementation.ts +++ b/src/backends/bitbucket/implementation.ts @@ -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; - initialWorkflowStatus: string; }; repo: string; branch: string; @@ -82,9 +76,6 @@ export default class BitbucketBackend implements Implementation { refreshedTokenPromise?: Promise; authenticator?: NetlifyAuthenticator; _mediaDisplayURLSem?: Semaphore; - squashMerges: boolean; - cmsLabelPrefix: string; - previewContext: string; largeMediaURL: string; _largeMediaClientPromise?: Promise; 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; - } - } } diff --git a/src/backends/git-gateway/GitHubAPI.ts b/src/backends/git-gateway/GitHubAPI.ts index 86b97f51..90c8e906 100644 --- a/src/backends/git-gateway/GitHubAPI.ts +++ b/src/backends/git-gateway/GitHubAPI.ts @@ -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 { - const diff = await super.diffFromFile(file); - return { - ...diff, - binary: diff.binary || (await this.isLargeMedia(file.filename)), - }; - } } diff --git a/src/backends/git-gateway/implementation.ts b/src/backends/git-gateway/implementation.ts index e39724d1..d65c6911 100644 --- a/src/backends/git-gateway/implementation.ts +++ b/src/backends/git-gateway/implementation.ts @@ -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); } diff --git a/src/backends/github/API.ts b/src/backends/github/API.ts index 661398ad..28cf6288 100644 --- a/src/backends/github/API.ts +++ b/src/backends/github/API.ts @@ -1,57 +1,38 @@ import { Base64 } from 'js-base64'; -import semaphore from 'semaphore'; -import { initial, last, partial, result, trimStart, trim } from 'lodash'; +import { initial, last, partial, result, trim, trimStart } from 'lodash'; import { dirname } from 'path'; +import semaphore from 'semaphore'; import { - getAllResponses, APIError, - EditorialWorkflowError, - localForage, basename, - readFileMetadata, - CMS_BRANCH_PREFIX, generateContentKey, - DEFAULT_PR_BODY, - MERGE_COMMIT_MESSAGE, - PreviewState, + getAllResponses, + localForage, parseContentKey, - branchFromContentKey, - isCMSLabel, - labelToStatus, - statusToLabel, - contentKeyFromBranch, + readFileMetadata, requestWithBackoff, unsentRequest, - throwOnConflictingBranches, } from '../../lib/util'; -import alert from '../../components/UI/Alert'; -import type { AssetProxy, DataFile, PersistOptions, FetchError, ApiRequest } from '../../lib/util'; -import type { Semaphore } from 'semaphore'; import type { Octokit } from '@octokit/rest'; +import type { Semaphore } from 'semaphore'; +import type { ApiRequest, AssetProxy, DataFile, FetchError, PersistOptions } from '../../lib/util'; type GitHubUser = Octokit.UsersGetAuthenticatedResponse; type GitCreateTreeParamsTree = Octokit.GitCreateTreeParamsTree; type GitHubCompareCommit = Octokit.ReposCompareCommitsResponseCommitsItem; type GitHubAuthor = Octokit.GitCreateCommitResponseAuthor; type GitHubCommitter = Octokit.GitCreateCommitResponseCommitter; -type GitHubPull = Octokit.PullsListResponseItem; export const API_NAME = 'GitHub'; -export const MOCK_PULL_REQUEST = -1; - export interface Config { apiRoot?: string; token?: string; branch?: string; - useOpenAuthoring?: boolean; repo?: string; originRepo?: string; - squashMerges: boolean; - initialWorkflowStatus: string; - cmsLabelPrefix: string; } interface TreeFile { @@ -73,23 +54,6 @@ type GitHubCompareFile = Octokit.ReposCompareCommitsResponseFilesItem & { type GitHubCompareFiles = GitHubCompareFile[]; -enum GitHubCommitStatusState { - Error = 'error', - Failure = 'failure', - Pending = 'pending', - Success = 'success', -} - -export enum PullRequestState { - Open = 'open', - Closed = 'closed', - All = 'all', -} - -type GitHubCommitStatus = Octokit.ReposListStatusesForRefResponseItem & { - state: GitHubCommitStatusState; -}; - interface MetaDataObjects { entry: { path: string; sha: string }; files: MediaFile[]; @@ -100,10 +64,6 @@ export interface Metadata { objects: MetaDataObjects; branch: string; status: string; - pr?: { - number: number; - head: string | { sha: string }; - }; collection: string; commitMessage: string; version?: string; @@ -128,34 +88,6 @@ type MediaFile = { path: string; }; -function withCmsLabel(pr: GitHubPull, cmsLabelPrefix: string) { - return pr.labels.some(l => isCMSLabel(l.name, cmsLabelPrefix)); -} - -function withoutCmsLabel(pr: GitHubPull, cmsLabelPrefix: string) { - return pr.labels.every(l => !isCMSLabel(l.name, cmsLabelPrefix)); -} - -function getTreeFiles(files: GitHubCompareFiles) { - const treeFiles = files.reduce((arr, file) => { - if (file.status === 'removed') { - // delete the file - arr.push({ sha: null, path: file.filename }); - } else if (file.status === 'renamed') { - // delete the previous file - arr.push({ sha: null, path: file.previous_filename as string }); - // add the renamed file - arr.push({ sha: file.sha, path: file.filename }); - } else { - // add the file - arr.push({ sha: file.sha, path: file.filename }); - } - return arr; - }, [] as { sha: string | null; path: string }[]); - - return treeFiles; -} - export type Diff = { path: string; newFile: boolean; @@ -163,13 +95,10 @@ export type Diff = { binary: boolean; }; -let migrationNotified = false; - export default class API { apiRoot: string; token: string; branch: string; - useOpenAuthoring?: boolean; repo: string; originRepo: string; repoOwner: string; @@ -178,9 +107,6 @@ export default class API { originRepoName: string; repoURL: string; originRepoURL: string; - mergeMethod: string; - initialWorkflowStatus: string; - cmsLabelPrefix: string; _userPromise?: Promise; _metadataSemaphore?: Semaphore; @@ -191,11 +117,9 @@ export default class API { this.apiRoot = config.apiRoot || 'https://api.github.com'; this.token = config.token || ''; this.branch = config.branch || 'main'; - this.useOpenAuthoring = config.useOpenAuthoring; this.repo = config.repo || ''; this.originRepo = config.originRepo || this.repo; this.repoURL = `/repos/${this.repo}`; - // when not in 'useOpenAuthoring' mode originRepoURL === repoURL this.originRepoURL = `/repos/${this.originRepo}`; const [repoParts, originRepoParts] = [this.repo.split('/'), this.originRepo.split('/')]; @@ -204,10 +128,6 @@ export default class API { this.originRepoOwner = originRepoParts[0]; this.originRepoName = originRepoParts[1]; - - this.mergeMethod = config.squashMerges ? 'squash' : 'merge'; - this.cmsLabelPrefix = config.cmsLabelPrefix; - this.initialWorkflowStatus = config.initialWorkflowStatus; } static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Simple CMS'; @@ -343,20 +263,11 @@ export default class API { } generateContentKey(collectionName: string, slug: string) { - const contentKey = generateContentKey(collectionName, slug); - if (!this.useOpenAuthoring) { - return contentKey; - } - - return `${this.repo}/${contentKey}`; + return generateContentKey(collectionName, slug); } parseContentKey(contentKey: string) { - if (!this.useOpenAuthoring) { - return parseContentKey(contentKey); - } - - return parseContentKey(contentKey.slice(this.repo.length + 1)); + return parseContentKey(contentKey); } checkMetadataRef() { @@ -458,150 +369,16 @@ export default class API { throw err; } - if (!this.useOpenAuthoring) { - const result = await this.request( - `${this.repoURL}/contents/${key}.json`, - metadataRequestOptions, - ) - .then((response: string) => JSON.parse(response)) - .catch(errorHandler); - - return result; - } - - const [user, repo] = key.split('/'); - const result = this.request( - `/repos/${user}/${repo}/contents/${key}.json`, + const result = await this.request( + `${this.repoURL}/contents/${key}.json`, metadataRequestOptions, ) .then((response: string) => JSON.parse(response)) .catch(errorHandler); + return result; } - async getPullRequests( - head: string | undefined, - state: PullRequestState, - predicate: (pr: GitHubPull) => boolean, - ) { - const pullRequests: Octokit.PullsListResponse = await this.requestAllPages( - `${this.originRepoURL}/pulls`, - { - params: { - ...(head ? { head: await this.getHeadReference(head) } : {}), - base: this.branch, - state, - per_page: 100, - }, - }, - ); - - return pullRequests.filter( - pr => pr.head.ref.startsWith(`${CMS_BRANCH_PREFIX}/`) && predicate(pr), - ); - } - - async getOpenAuthoringPullRequest(branch: string, pullRequests: GitHubPull[]) { - // we can't use labels when using open authoring - // since the contributor doesn't have access to set labels - // a branch without a pr (or a closed pr) means a 'draft' entry - // a branch with an opened pr means a 'pending_review' entry - const data = await this.getBranch(branch).catch(() => { - throw new EditorialWorkflowError('content is not under editorial workflow', true); - }); - // since we get all (open and closed) pull requests by branch name, make sure to filter by head sha - const pullRequest = pullRequests.filter(pr => pr.head.sha === data.commit.sha)[0]; - // if no pull request is found for the branch we return a mocked one - if (!pullRequest) { - try { - return { - head: { sha: data.commit.sha }, - number: MOCK_PULL_REQUEST, - labels: [{ name: statusToLabel(this.initialWorkflowStatus, this.cmsLabelPrefix) }], - state: PullRequestState.Open, - } as GitHubPull; - } catch (e) { - throw new EditorialWorkflowError('content is not under editorial workflow', true); - } - } else { - pullRequest.labels = pullRequest.labels.filter(l => !isCMSLabel(l.name, this.cmsLabelPrefix)); - const cmsLabel = - pullRequest.state === PullRequestState.Closed - ? { name: statusToLabel(this.initialWorkflowStatus, this.cmsLabelPrefix) } - : { name: statusToLabel('pending_review', this.cmsLabelPrefix) }; - - pullRequest.labels.push(cmsLabel as Octokit.PullsGetResponseLabelsItem); - return pullRequest; - } - } - - async getBranchPullRequest(branch: string) { - if (this.useOpenAuthoring) { - const pullRequests = await this.getPullRequests(branch, PullRequestState.All, () => true); - return this.getOpenAuthoringPullRequest(branch, pullRequests); - } else { - const pullRequests = await this.getPullRequests(branch, PullRequestState.Open, pr => - withCmsLabel(pr, this.cmsLabelPrefix), - ); - if (pullRequests.length <= 0) { - throw new EditorialWorkflowError('content is not under editorial workflow', true); - } - return pullRequests[0]; - } - } - - async getPullRequestCommits(number: number) { - if (number === MOCK_PULL_REQUEST) { - return []; - } - try { - const commits: Octokit.PullsListCommitsResponseItem[] = await this.request( - `${this.originRepoURL}/pulls/${number}/commits`, - ); - return commits; - } catch (e) { - console.info(e); - return []; - } - } - - async getPullRequestAuthor(pullRequest: Octokit.PullsListResponseItem) { - if (!pullRequest.user?.login) { - return; - } - - try { - const user: GitHubUser = await this.request(`/users/${pullRequest.user.login}`); - return user.name || user.login; - } catch { - return; - } - } - - async retrieveUnpublishedEntryData(contentKey: string) { - const { collection, slug } = this.parseContentKey(contentKey); - const branch = branchFromContentKey(contentKey); - const pullRequest = await this.getBranchPullRequest(branch); - const [{ files }, pullRequestAuthor] = await Promise.all([ - this.getDifferences(this.branch, pullRequest.head.sha), - this.getPullRequestAuthor(pullRequest), - ]); - const diffs = await Promise.all(files.map(file => this.diffFromFile(file))); - const label = pullRequest.labels.find(l => isCMSLabel(l.name, this.cmsLabelPrefix)) as { - name: string; - }; - const status = labelToStatus(label.name, this.cmsLabelPrefix); - const updatedAt = pullRequest.updated_at; - return { - collection, - slug, - status, - diffs: diffs.map(d => ({ path: d.path, newFile: d.newFile, id: d.sha })), - updatedAt, - pullRequestAuthor, - }; - } - async readFile( path: string, sha?: string | null, @@ -701,208 +478,17 @@ export default class API { } } - filterOpenAuthoringBranches = async (branch: string) => { - try { - const pullRequest = await this.getBranchPullRequest(branch); - const { state: currentState, merged_at: mergedAt } = pullRequest; - if ( - pullRequest.number !== MOCK_PULL_REQUEST && - currentState === PullRequestState.Closed && - mergedAt - ) { - // pr was merged, delete branch - await this.deleteBranch(branch); - return { branch, filter: false }; - } else { - return { branch, filter: true }; - } - } catch (e) { - return { branch, filter: false }; - } - }; - - async migrateToVersion1(pullRequest: GitHubPull, metadata: Metadata) { - // hard code key/branch generation logic to ignore future changes - const oldContentKey = pullRequest.head.ref.slice(`cms/`.length); - const newContentKey = `${metadata.collection}/${oldContentKey}`; - const newBranchName = `cms/${newContentKey}`; - - // retrieve or create new branch and pull request in new format - const branch = await this.getBranch(newBranchName).catch(() => undefined); - if (!branch) { - await this.createBranch(newBranchName, pullRequest.head.sha as string); - } - - const pr = - (await this.getPullRequests(newBranchName, PullRequestState.All, () => true))[0] || - (await this.createPR(pullRequest.title, newBranchName)); - - // store new metadata - const newMetadata = { - ...metadata, - pr: { - number: pr.number, - head: pr.head.sha, - }, - branch: newBranchName, - version: '1', - }; - await this.storeMetadata(newContentKey, newMetadata); - - // remove old data - await this.closePR(pullRequest.number); - await this.deleteBranch(pullRequest.head.ref); - await this.deleteMetadata(oldContentKey); - - return { metadata: newMetadata, pullRequest: pr }; - } - - async migrateToPullRequestLabels(pullRequest: GitHubPull, metadata: Metadata) { - await this.setPullRequestStatus(pullRequest, metadata.status); - - const contentKey = pullRequest.head.ref.slice(`cms/`.length); - await this.deleteMetadata(contentKey); - } - - async migratePullRequest(pullRequest: GitHubPull, countMessage: string) { - const { number } = pullRequest; - console.info(`Migrating Pull Request '${number}' (${countMessage})`); - const contentKey = contentKeyFromBranch(pullRequest.head.ref); - let metadata = await this.retrieveMetadataOld(contentKey).catch(() => undefined); - - if (!metadata) { - console.info(`Skipped migrating Pull Request '${number}' (${countMessage})`); - return; - } - - let newNumber = number; - if (!metadata.version) { - console.info(`Migrating Pull Request '${number}' to version 1`); - // migrate branch from cms/slug to cms/collection/slug - try { - ({ metadata, pullRequest } = await this.migrateToVersion1(pullRequest, metadata)); - } catch (e) { - console.info(`Failed to migrate Pull Request '${number}' to version 1. See error below.`); - console.error(e); - return; - } - newNumber = pullRequest.number; - console.info( - `Done migrating Pull Request '${number}' to version 1. New pull request '${newNumber}' created.`, - ); - } - - if (metadata.version === '1') { - console.info(`Migrating Pull Request '${newNumber}' to labels`); - // migrate branch from using orphan ref to store metadata to pull requests label - await this.migrateToPullRequestLabels(pullRequest, metadata); - console.info(`Done migrating Pull Request '${newNumber}' to labels`); - } - - console.info( - `Done migrating Pull Request '${ - number === newNumber ? newNumber : `${number} => ${newNumber}` - }'`, - ); - } - - async getOpenAuthoringBranches() { - const cmsBranches = await this.requestAllPages( - `${this.repoURL}/git/refs/heads/cms/${this.repo}`, - ).catch(() => [] as Octokit.GitListMatchingRefsResponseItem[]); - return cmsBranches; - } - - async listUnpublishedBranches() { - console.info( - '%c Checking for Unpublished entries', - 'line-height: 30px;text-align: center;font-weight: bold', - ); - - let branches: string[]; - if (this.useOpenAuthoring) { - // open authoring branches can exist without a pr - const cmsBranches: Octokit.GitListMatchingRefsResponse = - await this.getOpenAuthoringBranches(); - branches = cmsBranches.map(b => b.ref.slice('refs/heads/'.length)); - // filter irrelevant branches - const branchesWithFilter = await Promise.all( - branches.map(b => this.filterOpenAuthoringBranches(b)), - ); - branches = branchesWithFilter.filter(b => b.filter).map(b => b.branch); - } else { - // backwards compatibility code, get relevant pull requests and migrate them - const pullRequests = await this.getPullRequests( - undefined, - PullRequestState.Open, - pr => !pr.head.repo.fork && withoutCmsLabel(pr, this.cmsLabelPrefix), - ); - let prCount = 0; - for (const pr of pullRequests) { - if (!migrationNotified) { - migrationNotified = true; - alert({ - title: 'api.labelsMigrationTitle', - body: { - key: 'api.labelsMigrationBody', - options: { pullRequests: pullRequests.length }, - }, - }); - } - prCount = prCount + 1; - await this.migratePullRequest(pr, `${prCount} of ${pullRequests.length}`); - } - const cmsPullRequests = await this.getPullRequests(undefined, PullRequestState.Open, pr => - withCmsLabel(pr, this.cmsLabelPrefix), - ); - branches = cmsPullRequests.map(pr => pr.head.ref); - } - - return branches; - } - - /** - * Retrieve statuses for a given SHA. Unrelated to the editorial workflow - * concept of entry "status". Useful for things like deploy preview links. - */ - 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 resp: { statuses: GitHubCommitStatus[] } = await this.request( - `${this.originRepoURL}/commits/${sha}/status`, - ); - return resp.statuses.map(s => ({ - context: s.context, - target_url: s.target_url, - state: - s.state === GitHubCommitStatusState.Success ? PreviewState.Success : PreviewState.Other, - })); - } - async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) { const files = mediaFiles.concat(dataFiles); const uploadPromises = files.map(file => this.uploadBlob(file)); await Promise.all(uploadPromises); - if (!options.useWorkflow) { - return this.getDefaultBranch() - .then(branchData => - this.updateTree(branchData.commit.sha, files as { sha: string; path: string }[]), - ) - .then(changeTree => this.commit(options.commitMessage, changeTree)) - .then(response => this.patchBranch(this.branch, response.sha)); - } else { - const mediaFilesList = (mediaFiles as { sha: string; path: string }[]).map( - ({ sha, path }) => ({ - path: trimStart(path, '/'), - sha, - }), - ); - const slug = dataFiles[0].slug; - return this.editorialWorkflowGit(files as TreeFile[], slug, mediaFilesList, options); - } + return this.getDefaultBranch() + .then(branchData => + this.updateTree(branchData.commit.sha, files as { sha: string; path: string }[]), + ) + .then(changeTree => this.commit(options.commitMessage, changeTree)) + .then(response => this.patchBranch(this.branch, response.sha)); } async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) { @@ -928,10 +514,6 @@ export default class API { } async deleteFiles(paths: string[], message: string) { - if (this.useOpenAuthoring) { - return Promise.reject('Cannot delete published entries as an Open Authoring user!'); - } - const branchData = await this.getDefaultBranch(); const files = paths.map(path => ({ path, sha: null })); const changeTree = await this.updateTree(branchData.commit.sha, files); @@ -939,229 +521,6 @@ export default class API { await this.patchBranch(this.branch, commit.sha); } - async createBranchAndPullRequest(branchName: string, sha: string, commitMessage: string) { - await this.createBranch(branchName, sha); - return this.createPR(commitMessage, branchName); - } - - async updatePullRequestLabels(number: number, labels: string[]) { - await this.request(`${this.repoURL}/issues/${number}/labels`, { - method: 'PUT', - body: JSON.stringify({ labels }), - }); - } - - // async since it is overridden in a child class - async diffFromFile(diff: Octokit.ReposCompareCommitsResponseFilesItem): Promise { - return { - path: diff.filename, - newFile: diff.status === 'added', - sha: diff.sha, - // media files diffs don't have a patch attribute, except svg files - // renamed files don't have a patch attribute too - binary: (diff.status !== 'renamed' && !diff.patch) || diff.filename.endsWith('.svg'), - }; - } - - async editorialWorkflowGit( - files: TreeFile[], - slug: string, - mediaFilesList: MediaFile[], - options: PersistOptions, - ) { - const contentKey = this.generateContentKey(options.collectionName as string, slug); - const branch = branchFromContentKey(contentKey); - const unpublished = options.unpublished || false; - if (!unpublished) { - const branchData = await this.getDefaultBranch(); - const changeTree = await this.updateTree(branchData.commit.sha, files); - const commitResponse = await this.commit(options.commitMessage, changeTree); - - if (this.useOpenAuthoring) { - await this.createBranch(branch, commitResponse.sha); - } else { - const pr = await this.createBranchAndPullRequest( - branch, - commitResponse.sha, - options.commitMessage, - ); - await this.setPullRequestStatus(pr, options.status || this.initialWorkflowStatus); - } - } else { - // Entry is already on editorial review workflow - commit to existing branch - const { files: diffFiles } = await this.getDifferences( - this.branch, - await this.getHeadReference(branch), - ); - - const diffs = await Promise.all(diffFiles.map(file => this.diffFromFile(file))); - // mark media files to remove - const mediaFilesToRemove: { path: string; sha: string | null }[] = []; - for (const diff of diffs.filter(d => d.binary)) { - if (!mediaFilesList.some(file => file.path === diff.path)) { - mediaFilesToRemove.push({ path: diff.path, sha: null }); - } - } - - // rebase the branch before applying new changes - const rebasedHead = await this.rebaseBranch(branch); - const treeFiles = mediaFilesToRemove.concat(files); - const changeTree = await this.updateTree(rebasedHead.sha, treeFiles, branch); - const commit = await this.commit(options.commitMessage, changeTree); - - return this.patchBranch(branch, commit.sha, { force: true }); - } - } - - async getDifferences(from: string, to: string) { - // retry this as sometimes GitHub returns an initial 404 on cross repo compare - const attempts = this.useOpenAuthoring ? 10 : 1; - for (let i = 1; i <= attempts; i++) { - try { - const result: Octokit.ReposCompareCommitsResponse = await this.request( - `${this.originRepoURL}/compare/${from}...${to}`, - ); - return result; - } catch (e) { - if (i === attempts) { - console.warn(`Reached maximum number of attempts '${attempts}' for getDifferences`); - throw e; - } - await new Promise(resolve => setTimeout(resolve, i * 500)); - } - } - throw new APIError('Not Found', 404, API_NAME); - } - - async rebaseSingleCommit(baseCommit: GitHubCompareCommit, commit: GitHubCompareCommit) { - // first get the diff between the commits - const result = await this.getDifferences(commit.parents[0].sha, commit.sha); - const files = getTreeFiles(result.files as GitHubCompareFiles); - - // only update the tree if changes were detected - if (files.length > 0) { - // create a tree with baseCommit as the base with the diff applied - const tree = await this.updateTree(baseCommit.sha, files); - const { message, author, committer } = commit.commit; - - // create a new commit from the updated tree - const newCommit = await this.createCommit( - message, - tree.sha, - [baseCommit.sha], - author, - committer, - ); - return newCommit as unknown as GitHubCompareCommit; - } else { - return commit; - } - } - - /** - * Rebase an array of commits one-by-one, starting from a given base SHA - */ - async rebaseCommits(baseCommit: GitHubCompareCommit, commits: GitHubCompareCommits) { - /** - * If the parent of the first commit already matches the target base, - * return commits as is. - */ - if (commits.length === 0 || commits[0].parents[0].sha === baseCommit.sha) { - const head = last(commits) as GitHubCompareCommit; - return head; - } else { - /** - * Re-create each commit over the new base, applying each to the previous, - * changing only the parent SHA and tree for each, but retaining all other - * info, such as the author/committer data. - */ - const newHeadPromise = commits.reduce((lastCommitPromise, commit) => { - return lastCommitPromise.then(newParent => { - const parent = newParent; - const commitToRebase = commit; - return this.rebaseSingleCommit(parent, commitToRebase); - }); - }, Promise.resolve(baseCommit)); - return newHeadPromise; - } - } - - async rebaseBranch(branch: string) { - try { - // Get the diff between the default branch the published branch - const { base_commit: baseCommit, commits } = await this.getDifferences( - this.branch, - await this.getHeadReference(branch), - ); - // Rebase the branch based on the diff - const rebasedHead = await this.rebaseCommits(baseCommit, commits); - return rebasedHead; - } catch (error) { - console.error(error); - throw error; - } - } - - async setPullRequestStatus(pullRequest: GitHubPull, newStatus: string) { - const labels = [ - ...pullRequest.labels - .filter(label => !isCMSLabel(label.name, this.cmsLabelPrefix)) - .map(l => l.name), - statusToLabel(newStatus, this.cmsLabelPrefix), - ]; - await this.updatePullRequestLabels(pullRequest.number, labels); - } - - async updateUnpublishedEntryStatus(collectionName: string, slug: string, newStatus: string) { - const contentKey = this.generateContentKey(collectionName, slug); - const branch = branchFromContentKey(contentKey); - const pullRequest = await this.getBranchPullRequest(branch); - - if (!this.useOpenAuthoring) { - await this.setPullRequestStatus(pullRequest, newStatus); - } else { - if (status === 'pending_publish') { - throw new Error('Open Authoring entries may not be set to the status "pending_publish".'); - } - - if (pullRequest.number !== MOCK_PULL_REQUEST) { - const { state } = pullRequest; - if (state === PullRequestState.Open && newStatus === 'draft') { - await this.closePR(pullRequest.number); - } - if (state === PullRequestState.Closed && newStatus === 'pending_review') { - await this.openPR(pullRequest.number); - } - } else if (newStatus === 'pending_review') { - const branch = branchFromContentKey(contentKey); - // get the first commit message as the pr title - const diff = await this.getDifferences(this.branch, await this.getHeadReference(branch)); - const title = diff.commits[0]?.commit?.message || API.DEFAULT_COMMIT_MESSAGE; - await this.createPR(title, branch); - } - } - } - - async deleteUnpublishedEntry(collectionName: string, slug: string) { - const contentKey = this.generateContentKey(collectionName, slug); - const branch = branchFromContentKey(contentKey); - - const pullRequest = await this.getBranchPullRequest(branch); - if (pullRequest.number !== MOCK_PULL_REQUEST) { - await this.closePR(pullRequest.number); - } - await this.deleteBranch(branch); - } - - async publishUnpublishedEntry(collectionName: string, slug: string) { - const contentKey = this.generateContentKey(collectionName, slug); - const branch = branchFromContentKey(contentKey); - - const pullRequest = await this.getBranchPullRequest(branch); - await this.mergePR(pullRequest); - await this.deleteBranch(branch); - } - async createRef(type: string, name: string, sha: string) { const result: Octokit.GitCreateRefResponse = await this.request(`${this.repoURL}/git/refs`, { method: 'POST', @@ -1170,13 +529,12 @@ export default class API { return result; } - async patchRef(type: string, name: string, sha: string, opts: { force?: boolean } = {}) { - const force = opts.force || false; + async patchRef(type: string, name: string, sha: string) { const result: Octokit.GitUpdateRefResponse = await this.request( `${this.repoURL}/git/refs/${type}/${encodeURIComponent(name)}`, { method: 'PATCH', - body: JSON.stringify({ sha, force }), + body: JSON.stringify({ sha }), }, ); return result; @@ -1188,13 +546,6 @@ export default class API { }); } - async getBranch(branch: string) { - const result: Octokit.ReposGetBranchResponse = await this.request( - `${this.repoURL}/branches/${encodeURIComponent(branch)}`, - ); - return result; - } - async getDefaultBranch() { const result: Octokit.ReposGetBranchResponse = await this.request( `${this.originRepoURL}/branches/${encodeURIComponent(this.branch)}`, @@ -1202,160 +553,14 @@ export default class API { return result; } - async backupBranch(branchName: string) { - try { - const existingBranch = await this.getBranch(branchName); - await this.createBranch( - existingBranch.name.replace( - new RegExp(`${CMS_BRANCH_PREFIX}/`), - `${CMS_BRANCH_PREFIX}_${Date.now()}/`, - ), - existingBranch.commit.sha, - ); - } catch (e) { - console.warn(e); - } - } - - async createBranch(branchName: string, sha: string) { - try { - const result = await this.createRef('heads', branchName, sha); - return result; - } catch (e: any) { - const message = String(e.message || ''); - if (message === 'Reference update failed') { - await throwOnConflictingBranches(branchName, name => this.getBranch(name), API_NAME); - } else if ( - message === 'Reference already exists' && - branchName.startsWith(`${CMS_BRANCH_PREFIX}/`) - ) { - try { - // this can happen if the branch wasn't deleted when the PR was merged - // we backup the existing branch just in case and patch it with the new sha - await this.backupBranch(branchName); - const result = await this.patchBranch(branchName, sha, { force: true }); - return result; - } catch (e) { - console.error(e); - } - } - throw e; - } - } - - assertCmsBranch(branchName: string) { - return branchName.startsWith(`${CMS_BRANCH_PREFIX}/`); - } - - patchBranch(branchName: string, sha: string, opts: { force?: boolean } = {}) { - const force = opts.force || false; - if (force && !this.assertCmsBranch(branchName)) { - throw Error(`Only CMS branches can be force updated, cannot force update ${branchName}`); - } - return this.patchRef('heads', branchName, sha, { force }); - } - - deleteBranch(branchName: string) { - return this.deleteRef('heads', branchName).catch((err: Error) => { - // If the branch doesn't exist, then it has already been deleted - - // deletion should be idempotent, so we can consider this a - // success. - if (err.message === 'Reference does not exist') { - return Promise.resolve(); - } - console.error(err); - return Promise.reject(err); - }); + patchBranch(branchName: string, sha: string) { + return this.patchRef('heads', branchName, sha); } async getHeadReference(head: string) { return `${this.repoOwner}:${head}`; } - async createPR(title: string, head: string) { - const result: Octokit.PullsCreateResponse = await this.request(`${this.originRepoURL}/pulls`, { - method: 'POST', - body: JSON.stringify({ - title, - body: DEFAULT_PR_BODY, - head: await this.getHeadReference(head), - base: this.branch, - }), - }); - - return result; - } - - async openPR(number: number) { - console.info('%c Re-opening PR', 'line-height: 30px;text-align: center;font-weight: bold'); - const result: Octokit.PullsUpdateBranchResponse = await this.request( - `${this.originRepoURL}/pulls/${number}`, - { - method: 'PATCH', - body: JSON.stringify({ - state: PullRequestState.Open, - }), - }, - ); - return result; - } - - async closePR(number: number) { - console.info('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold'); - const result: Octokit.PullsUpdateBranchResponse = await this.request( - `${this.originRepoURL}/pulls/${number}`, - { - method: 'PATCH', - body: JSON.stringify({ - state: PullRequestState.Closed, - }), - }, - ); - return result; - } - - async mergePR(pullrequest: GitHubPull) { - console.info('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold'); - try { - const result: Octokit.PullsMergeResponse = await this.request( - `${this.originRepoURL}/pulls/${pullrequest.number}/merge`, - { - method: 'PUT', - body: JSON.stringify({ - commit_message: MERGE_COMMIT_MESSAGE, - sha: pullrequest.head.sha, - merge_method: this.mergeMethod, - }), - }, - ); - return result; - } catch (error) { - if (error instanceof APIError && error.status === 405) { - return this.forceMergePR(pullrequest); - } else { - throw error; - } - } - } - - async forceMergePR(pullRequest: GitHubPull) { - const result = await this.getDifferences(pullRequest.base.sha, pullRequest.head.sha); - const files = getTreeFiles(result.files as GitHubCompareFiles); - - let commitMessage = 'Automatically generated. Merged on Simple CMS\n\nForce merge of:'; - files.forEach(file => { - commitMessage += `\n* "${file.path}"`; - }); - console.info( - '%c Automatic merge not possible - Forcing merge.', - 'line-height: 30px;text-align: center;font-weight: bold', - ); - return this.getDefaultBranch() - .then(branchData => this.updateTree(branchData.commit.sha, files)) - .then(changeTree => this.commit(commitMessage, changeTree)) - .then(response => this.patchBranch(this.branch, response.sha)); - } - toBase64(str: string) { return Promise.resolve(Base64.encode(str)); } @@ -1455,11 +660,4 @@ export default class API { ); return result; } - - async getUnpublishedEntrySha(collection: string, slug: string) { - const contentKey = this.generateContentKey(collection, slug); - const branch = branchFromContentKey(contentKey); - const pullRequest = await this.getBranchPullRequest(branch); - return pullRequest.head.sha; - } } diff --git a/src/backends/github/AuthenticationPage.js b/src/backends/github/AuthenticationPage.js index 9f8319a8..e7c1c616 100644 --- a/src/backends/github/AuthenticationPage.js +++ b/src/backends/github/AuthenticationPage.js @@ -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') ) : ( @@ -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 }) => ( - -

- Open Authoring is enabled: we need to use a fork on your github account. (If a fork - already exists, we'll use that.) -

- - Fork the repo - {showAbortButton && ( - Don't fork the repo - )} - -
- ), - }; - } - 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 ( ; @@ -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({ diff --git a/src/backends/github/implementation.tsx b/src/backends/github/implementation.tsx index e51a0a3c..e27a32f0 100644 --- a/src/backends/github/implementation.tsx +++ b/src/backends/github/implementation.tsx @@ -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; _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; - }) { - 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; @@ -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', - ); - } } diff --git a/src/backends/github/mutations.ts b/src/backends/github/mutations.ts index 6c0627c7..7c869ad5 100644 --- a/src/backends/github/mutations.ts +++ b/src/backends/github/mutations.ts @@ -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} -`; diff --git a/src/backends/github/queries.ts b/src/backends/github/queries.ts index 9e54ecd4..f9715280 100644 --- a/src/backends/github/queries.ts +++ b/src/backends/github/queries.ts @@ -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) { diff --git a/src/backends/gitlab/API.ts b/src/backends/gitlab/API.ts index 4f35aba2..128c648f 100644 --- a/src/backends/gitlab/API.ts +++ b/src/backends/gitlab/API.ts @@ -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; @@ -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; - } } diff --git a/src/backends/gitlab/implementation.ts b/src/backends/gitlab/implementation.ts index 997bc7d6..ed24eda8 100644 --- a/src/backends/gitlab/implementation.ts +++ b/src/backends/gitlab/implementation.ts @@ -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; - } - } } diff --git a/src/backends/proxy/implementation.ts b/src/backends/proxy/implementation.ts index 48fb8e3b..76d1ea8a 100644 --- a/src/backends/proxy/implementation.ts +++ b/src/backends/proxy/implementation.ts @@ -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 }, - }); - } } diff --git a/src/backends/test/implementation.ts b/src/backends/test/implementation.ts index 9dc00491..42512688 100644 --- a/src/backends/test/implementation.ts +++ b/src/backends/test/implementation.ts @@ -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; - } } diff --git a/src/components/App/App.js b/src/components/App/App.js index 72e63c36..4141bbd9 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -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 ( @@ -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 ? : null} - {hasWorkflow && ( -
  • - - - {t('app.header.workflow')} - -
  • - )} {showMediaButton && (
  • diff --git a/src/components/Editor/Editor.js b/src/components/Editor/Editor.js index 76d8b63b..0914d3d1 100644 --- a/src/components/Editor/Editor.js +++ b/src/components/Editor/Editor.js @@ -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 (
    @@ -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)); diff --git a/src/components/Editor/EditorControlPane/EditorControlPane.js b/src/components/Editor/EditorControlPane/EditorControlPane.js index 4b3992a4..a71cefbd 100644 --- a/src/components/Editor/EditorControlPane/EditorControlPane.js +++ b/src/components/Editor/EditorControlPane/EditorControlPane.js @@ -108,7 +108,7 @@ export default class ControlPane extends React.Component { }; copyFromOtherLocale = - ({ targetLocale, t }) => + ({ targetLocale }) => async sourceLocale => { if ( !(await confirm({ diff --git a/src/components/Editor/EditorInterface.js b/src/components/Editor/EditorInterface.js index 480fc1d8..58b2bb1e 100644 --- a/src/components/Editor/EditorInterface.js +++ b/src/components/Editor/EditorInterface.js @@ -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} /> @@ -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, diff --git a/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx b/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx index d7a58f78..8fe533d2 100644 --- a/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx +++ b/src/components/Editor/EditorPreviewPane/EditorPreviewContent.tsx @@ -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> diff --git a/src/components/Editor/EditorToolbar.js b/src/components/Editor/EditorToolbar.js index c1c62f6f..e0d2079d 100644 --- a/src/components/Editor/EditorToolbar.js +++ b/src/components/Editor/EditorToolbar.js @@ -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 ( - - {deployPreviewReady ? ( - - {label} - - - ) : ( - - {t('editor.editorToolbar.deployPreviewPendingButtonLabel')} - - - )} - - ); - }; - 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 ( - <> - {buttonText}} - > - onChangeStatus('DRAFT')} - icon={currentStatus === status.get('DRAFT') ? 'check' : null} - /> - onChangeStatus('PENDING_REVIEW')} - icon={currentStatus === status.get('PENDING_REVIEW') ? 'check' : null} - /> - {useOpenAuthoring ? ( - '' - ) : ( - onChangeStatus('PENDING_PUBLISH')} - icon={currentStatus === status.get('PENDING_PUBLISH') ? 'check' : null} - /> - )} - - {useOpenAuthoring && this.renderStatusInfoTooltip()} - - ); - }; - - renderNewEntryWorkflowPublishControls = ({ canCreate, canPublish }) => { - const { isPublishing, onPublish, onPublishAndNew, onPublishAndDuplicate, t } = this.props; - - return canPublish ? ( - ( - - {isPublishing - ? t('editor.editorToolbar.publishing') - : t('editor.editorToolbar.publish')} - - )} - > - - {canCreate ? ( - <> - - - - ) : null} - - ) : ( - '' - ); - }; - - renderExistingEntryWorkflowPublishControls = ({ canCreate, canPublish, canDelete }) => { - const { unPublish, onDuplicate, isPersisting, t } = this.props; - - return canPublish || canCreate ? ( - ( - - {isPersisting - ? t('editor.editorToolbar.unpublishing') - : t('editor.editorToolbar.published')} - - )} - > - {canDelete && canPublish && ( - - )} - {canCreate && ( - - )} - - ) : ( - '' - ); - }; - 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 [ - hasChanged && onPersist()} - > - {isPersisting ? t('editor.editorToolbar.saving') : t('editor.editorToolbar.save')} - , - currentStatus - ? [ - this.renderWorkflowStatusControls(), - this.renderNewEntryWorkflowPublishControls({ canCreate, canPublish }), - ] - : !isNewEntry && - this.renderExistingEntryWorkflowPublishControls({ canCreate, canPublish, canDelete }), - (!showDelete || useOpenAuthoring) && !hasUnpublishedChanges && !isModification ? null : ( - - {isDeleting ? t('editor.editorToolbar.deleting') : deleteLabel} - - ), - ]; - }; - - 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 { - {hasWorkflow ? this.renderWorkflowControls() : this.renderSimpleControls()} + {this.renderSimpleControls()} - - {hasWorkflow - ? this.renderWorkflowDeployPreviewControls() - : this.renderSimpleDeployPreviewControls()} - 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 ; - } - }, - ); -} diff --git a/src/components/MediaLibrary/MediaLibrary.js b/src/components/MediaLibrary/MediaLibrary.js index 8fda51d7..dbbace16 100644 --- a/src/components/MediaLibrary/MediaLibrary.js +++ b/src/components/MediaLibrary/MediaLibrary.js @@ -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', diff --git a/src/components/UI/Alert.tsx b/src/components/UI/Alert.tsx index d7b19cd6..d054c107 100644 --- a/src/components/UI/Alert.tsx +++ b/src/components/UI/Alert.tsx @@ -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 }; diff --git a/src/components/UI/Confirm.tsx b/src/components/UI/Confirm.tsx index 706bb802..84185a97 100644 --- a/src/components/UI/Confirm.tsx +++ b/src/components/UI/Confirm.tsx @@ -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 }; diff --git a/src/components/Workflow/Workflow.js b/src/components/Workflow/Workflow.js deleted file mode 100644 index 796b2a81..00000000 --- a/src/components/Workflow/Workflow.js +++ /dev/null @@ -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 {t('workflow.workflow.loading')}; - const reviewCount = unpublishedEntries.get('pending_review').size; - const readyCount = unpublishedEntries.get('pending_publish').size; - - return ( - - - - {t('workflow.workflow.workflowHeading')} - ( - {t('workflow.workflow.newPost')} - )} - > - {collections - .filter(collection => collection.get('create')) - .toList() - .map(collection => ( - createNewEntry(collection.get('name'))} - /> - ))} - - - - {t('workflow.workflow.description', { - smart_count: reviewCount, - readyCount, - })} - - - - - ); - } -} - -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)); diff --git a/src/components/Workflow/WorkflowCard.js b/src/components/Workflow/WorkflowCard.js deleted file mode 100644 index ebfa5a4a..00000000 --- a/src/components/Workflow/WorkflowCard.js +++ /dev/null @@ -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 ( - {t(`workflow.workflowCard.${key}`, { date, author })} - ); - } -}); - -function WorkflowCard({ - collectionLabel, - title, - authorLastChange, - body, - isModification, - editLink, - timestamp, - onDelete, - allowPublish, - canPublish, - onPublish, - postAuthor, - t, -}) { - return ( - - - {collectionLabel} - {postAuthor} - {title} - {(timestamp || authorLastChange) && } - {body} - - - - {isModification - ? t('workflow.workflowCard.deleteChanges') - : t('workflow.workflowCard.deleteNewEntry')} - - {allowPublish && ( - - {isModification - ? t('workflow.workflowCard.publishChanges') - : t('workflow.workflowCard.publishNewEntry')} - - )} - - - ); -} - -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); diff --git a/src/components/Workflow/WorkflowList.js b/src/components/Workflow/WorkflowList.js deleted file mode 100644 index 63243afa..00000000 --- a/src/components/Workflow/WorkflowList.js +++ /dev/null @@ -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) => ( - - {(connect, { isHovered }) => - connect( -
    -
    - - {getColumnHeaderText(currColumn, this.props.t)} - - - {this.props.t('workflow.workflowList.currentEntries', { - smart_count: currEntries.size, - })} - - {this.renderColumns(currEntries, currColumn)} -
    -
    , - ) - } -
    - )); - } - return ( -
    - {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 ( - - {connect => - connect( -
    - -
    , - ) - } -
    - ); - })} -
    - ); - }; - - render() { - const columns = this.renderColumns(this.props.entries); - const ListContainer = this.props.isOpenAuthoring - ? WorkflowListContainerOpenAuthoring - : WorkflowListContainer; - return {columns}; - } -} - -export default HTML5DragDrop(translate()(WorkflowList)); diff --git a/src/components/page/Page.tsx b/src/components/page/Page.tsx index 6b76ea87..4535e848 100644 --- a/src/components/page/Page.tsx +++ b/src/components/page/Page.tsx @@ -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}; `; diff --git a/src/constants/configSchema.js b/src/constants/configSchema.js index efb8465f..e4714a6c 100644 --- a/src/constants/configSchema.js +++ b/src/constants/configSchema.js @@ -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: { diff --git a/src/constants/publishModes.ts b/src/constants/publishModes.ts deleted file mode 100644 index 56fba78f..00000000 --- a/src/constants/publishModes.ts +++ /dev/null @@ -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; diff --git a/src/interface.ts b/src/interface.ts index 84d85587..1912d2b1 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -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; data: Map; @@ -198,36 +175,6 @@ export interface Implementation { persistMedia: (file: AssetProxy, opts: PersistOptions) => Promise; deleteFiles: (paths: string[], commitMessage: string) => Promise; - unpublishedEntries: () => Promise; - unpublishedEntry: (args: { - id?: string; - collection?: string; - slug?: string; - }) => Promise; - unpublishedEntryDataFile: ( - collection: string, - slug: string, - path: string, - id: string, - ) => Promise; - unpublishedEntryMediaFile: ( - collection: string, - slug: string, - path: string, - id: string, - ) => Promise; - updateUnpublishedEntryStatus: ( - collection: string, - slug: string, - newStatus: string, - ) => Promise; - publishUnpublishedEntry: (collection: string, slug: string) => Promise; - deleteUnpublishedEntry: (collection: string, slug: string) => Promise; - 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, diff --git a/src/lib/formatters.ts b/src/lib/formatters.ts index 46af0063..88071133 100644 --- a/src/lib/formatters.ts +++ b/src/lib/formatters.ts @@ -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) { diff --git a/src/lib/registry.js b/src/lib/registry.js index eb36c109..207d166b 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -7,8 +7,6 @@ import EditorComponent from '../valueObjects/EditorComponent'; const allowedEvents = [ 'prePublish', 'postPublish', - 'preUnpublish', - 'postUnpublish', 'preSave', 'postSave', ]; diff --git a/src/lib/util/API.ts b/src/lib/util/API.ts index a7481936..09560965 100644 --- a/src/lib/util/API.ts +++ b/src/lib/util/API.ts @@ -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('/'); diff --git a/src/lib/util/APIUtils.ts b/src/lib/util/APIUtils.ts index dacd36e7..04ac16b9 100644 --- a/src/lib/util/APIUtils.ts +++ b/src/lib/util/APIUtils.ts @@ -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}`; -} diff --git a/src/lib/util/EditorialWorkflowError.ts b/src/lib/util/EditorialWorkflowError.ts deleted file mode 100644 index 9a620529..00000000 --- a/src/lib/util/EditorialWorkflowError.ts +++ /dev/null @@ -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; - } -} diff --git a/src/lib/util/asyncLock.ts b/src/lib/util/asyncLock.ts index bb0d577d..2a45f7d5 100644 --- a/src/lib/util/asyncLock.ts +++ b/src/lib/util/asyncLock.ts @@ -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.') { diff --git a/src/lib/util/backendUtil.ts b/src/lib/util/backendUtil.ts index 7e550320..d26de566 100644 --- a/src/lib/util/backendUtil.ts +++ b/src/lib/util/backendUtil.ts @@ -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) { diff --git a/src/lib/util/events/AlertEvent.ts b/src/lib/util/events/AlertEvent.ts index bd2144a1..af9b0d3c 100644 --- a/src/lib/util/events/AlertEvent.ts +++ b/src/lib/util/events/AlertEvent.ts @@ -1,4 +1,4 @@ -import { AlertDialogProps } from '../../../components/UI/Alert'; +import type { AlertDialogProps } from '../../../components/UI/Alert'; export default class AlertEvent extends CustomEvent { constructor(detail: AlertDialogProps) { diff --git a/src/lib/util/events/ConfirmEvent.ts b/src/lib/util/events/ConfirmEvent.ts index 969a69eb..6c23740c 100644 --- a/src/lib/util/events/ConfirmEvent.ts +++ b/src/lib/util/events/ConfirmEvent.ts @@ -1,4 +1,4 @@ -import { ConfirmDialogProps } from '../../../components/UI/Confirm'; +import type { ConfirmDialogProps } from '../../../components/UI/Confirm'; export default class ConfirmEvent extends CustomEvent { constructor(detail: ConfirmDialogProps) { diff --git a/src/lib/util/implementation.ts b/src/lib/util/implementation.ts index f598d2ed..c9afbd9a 100644 --- a/src/lib/util/implementation.ts +++ b/src/lib/util/implementation.ts @@ -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: (key: string, defaultValue?: T) => T; getIn: (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) { - 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); diff --git a/src/lib/util/index.ts b/src/lib/util/index.ts index 7150221e..3585b3e4 100644 --- a/src/lib/util/index.ts +++ b/src/lib/util/index.ts @@ -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, diff --git a/src/lib/util/window.util.ts b/src/lib/util/window.util.ts index 5a7f760e..d15a8130 100644 --- a/src/lib/util/window.util.ts +++ b/src/lib/util/window.util.ts @@ -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( eventName: K, - callback: (event: WindowEventMap[K]) => void + callback: (event: WindowEventMap[K]) => void, ): void; -export function useWindowEvent(eventName: K, callback: (event: EventMap[K]) => void): void; -export function useWindowEvent(eventName: string, callback: EventListenerOrEventListenerObject): void { +export function useWindowEvent( + eventName: K, + callback: (event: EventMap[K]) => void, +): void; +export function useWindowEvent( + eventName: string, + callback: EventListenerOrEventListenerObject, +): void { useEffect(() => { window.addEventListener(eventName, callback); diff --git a/src/locales/bg/index.js b/src/locales/bg/index.js index 5af3b0e8..aac86b8f 100644 --- a/src/locales/bg/index.js +++ b/src/locales/bg/index.js @@ -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}', diff --git a/src/locales/ca/index.js b/src/locales/ca/index.js index 7e509daf..03f5bd83 100644 --- a/src/locales/ca/index.js +++ b/src/locales/ca/index.js @@ -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ó', diff --git a/src/locales/cs/index.js b/src/locales/cs/index.js index 2e635fc4..1d883f41 100644 --- a/src/locales/cs/index.js +++ b/src/locales/cs/index.js @@ -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í.', diff --git a/src/locales/da/index.js b/src/locales/da/index.js index 30e87e60..2c92d230 100644 --- a/src/locales/da/index.js +++ b/src/locales/da/index.js @@ -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: diff --git a/src/locales/de/index.js b/src/locales/de/index.js index 9dbd79e5..d625e8dc 100644 --- a/src/locales/de/index.js +++ b/src/locales/de/index.js @@ -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.', diff --git a/src/locales/en/index.js b/src/locales/en/index.js index 68449f2b..283afdb4 100644 --- a/src/locales/en/index.js +++ b/src/locales/en/index.js @@ -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; diff --git a/src/locales/es/index.js b/src/locales/es/index.js index d8d1558e..0630b7a4 100644 --- a/src/locales/es/index.js +++ b/src/locales/es/index.js @@ -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}', }, }, diff --git a/src/locales/fr/index.js b/src/locales/fr/index.js index 206710fa..8f227cb4 100644 --- a/src/locales/fr/index.js +++ b/src/locales/fr/index.js @@ -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: diff --git a/src/locales/gr/index.js b/src/locales/gr/index.js index 9b52888c..f1e9dfa6 100644 --- a/src/locales/gr/index.js +++ b/src/locales/gr/index.js @@ -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}', }, }, diff --git a/src/locales/he/index.js b/src/locales/he/index.js index 03c20edb..76f58294 100644 --- a/src/locales/he/index.js +++ b/src/locales/he/index.js @@ -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} למידע נוסף', diff --git a/src/locales/hr/index.js b/src/locales/hr/index.js index 1cb9ab9b..8fee4e0a 100644 --- a/src/locales/hr/index.js +++ b/src/locales/hr/index.js @@ -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', diff --git a/src/locales/hu/index.js b/src/locales/hu/index.js index e49edd96..f0ca6814 100644 --- a/src/locales/hu/index.js +++ b/src/locales/hu/index.js @@ -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}', }, }, diff --git a/src/locales/it/index.js b/src/locales/it/index.js index ef72c4bf..b2fbe99e 100644 --- a/src/locales/it/index.js +++ b/src/locales/it/index.js @@ -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}', }, }, diff --git a/src/locales/ja/index.js b/src/locales/ja/index.js index bc020081..31f9c72f 100644 --- a/src/locales/ja/index.js +++ b/src/locales/ja/index.js @@ -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}', diff --git a/src/locales/ko/index.js b/src/locales/ko/index.js index 96a139b2..a25d6f25 100644 --- a/src/locales/ko/index.js +++ b/src/locales/ko/index.js @@ -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: diff --git a/src/locales/lt/index.js b/src/locales/lt/index.js index d22811ec..aacd8749 100644 --- a/src/locales/lt/index.js +++ b/src/locales/lt/index.js @@ -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.', diff --git a/src/locales/nb_no/index.js b/src/locales/nb_no/index.js index 72ce9faa..d7385c02 100644 --- a/src/locales/nb_no/index.js +++ b/src/locales/nb_no/index.js @@ -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}', }, }, diff --git a/src/locales/nl/index.js b/src/locales/nl/index.js index ba4b96b5..da653880 100644 --- a/src/locales/nl/index.js +++ b/src/locales/nl/index.js @@ -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: diff --git a/src/locales/nn_no/index.js b/src/locales/nn_no/index.js index 6be99524..26f5d248 100644 --- a/src/locales/nn_no/index.js +++ b/src/locales/nn_no/index.js @@ -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}', }, }, diff --git a/src/locales/pl/index.js b/src/locales/pl/index.js index 6d6eba0e..dc71854b 100644 --- a/src/locales/pl/index.js +++ b/src/locales/pl/index.js @@ -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}', diff --git a/src/locales/pt/index.js b/src/locales/pt/index.js index 96ee9205..5cbfc4fe 100644 --- a/src/locales/pt/index.js +++ b/src/locales/pt/index.js @@ -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', diff --git a/src/locales/ro/index.js b/src/locales/ro/index.js index 266101b5..a20411b9 100644 --- a/src/locales/ro/index.js +++ b/src/locales/ro/index.js @@ -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.', diff --git a/src/locales/ru/index.js b/src/locales/ru/index.js index b39d0249..2c01b3a9 100644 --- a/src/locales/ru/index.js +++ b/src/locales/ru/index.js @@ -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}', diff --git a/src/locales/sv/index.js b/src/locales/sv/index.js index ef63f30e..f67378b3 100644 --- a/src/locales/sv/index.js +++ b/src/locales/sv/index.js @@ -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', diff --git a/src/locales/th/index.js b/src/locales/th/index.js index d71b828d..43d81438 100644 --- a/src/locales/th/index.js +++ b/src/locales/th/index.js @@ -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} สำหรับข้อมูลเพิ่มเติม', diff --git a/src/locales/tr/index.js b/src/locales/tr/index.js index 506e3562..0df960fd 100644 --- a/src/locales/tr/index.js +++ b/src/locales/tr/index.js @@ -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: diff --git a/src/locales/uk/index.js b/src/locales/uk/index.js index 3a2cd58b..d4b9c611 100644 --- a/src/locales/uk/index.js +++ b/src/locales/uk/index.js @@ -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}', }, }, diff --git a/src/locales/vi/index.js b/src/locales/vi/index.js index 2925ab1f..393035f4 100644 --- a/src/locales/vi/index.js +++ b/src/locales/vi/index.js @@ -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', diff --git a/src/locales/zh_Hans/index.js b/src/locales/zh_Hans/index.js index c54a8b65..51f95966 100644 --- a/src/locales/zh_Hans/index.js +++ b/src/locales/zh_Hans/index.js @@ -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}', diff --git a/src/locales/zh_Hant/index.js b/src/locales/zh_Hant/index.js index 531faefa..85a12eb2 100644 --- a/src/locales/zh_Hant/index.js +++ b/src/locales/zh_Hant/index.js @@ -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} 取得更多資訊', diff --git a/src/mediaLibrary.ts b/src/mediaLibrary.ts index 8a419e39..1e9750d0 100644 --- a/src/mediaLibrary.ts +++ b/src/mediaLibrary.ts @@ -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')) { diff --git a/src/reducers/config.ts b/src/reducers/config.ts index b1b85390..44926c8a 100644 --- a/src/reducers/config.ts +++ b/src/reducers/config.ts @@ -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 { +export interface ConfigState extends Partial { 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; diff --git a/src/reducers/deploys.ts b/src/reducers/deploys.ts deleted file mode 100644 index d8113e83..00000000 --- a/src/reducers/deploys.ts +++ /dev/null @@ -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; diff --git a/src/reducers/editorialWorkflow.ts b/src/reducers/editorialWorkflow.ts deleted file mode 100644 index c15b47b3..00000000 --- a/src/reducers/editorialWorkflow.ts +++ /dev/null @@ -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; diff --git a/src/reducers/entries.ts b/src/reducers/entries.ts index 193ba25e..8bcced83 100644 --- a/src/reducers/entries.ts +++ b/src/reducers/entries.ts @@ -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; diff --git a/src/reducers/entryDraft.js b/src/reducers/entryDraft.js index dab7973b..44c629ca 100644 --- a/src/reducers/entryDraft.js +++ b/src/reducers/entryDraft.js @@ -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); diff --git a/src/reducers/globalUI.ts b/src/reducers/globalUI.ts index 4d82b779..cdeaf028 100644 --- a/src/reducers/globalUI.ts +++ b/src/reducers/globalUI.ts @@ -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); diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 3cf201e9..b98e0af2 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -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); } diff --git a/src/reducers/integrations.ts b/src/reducers/integrations.ts index e0a835f9..b20b99b9 100644 --- a/src/reducers/integrations.ts +++ b/src/reducers/integrations.ts @@ -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 { diff --git a/src/reducers/mediaLibrary.ts b/src/reducers/mediaLibrary.ts index 0a592dab..813c99a4 100644 --- a/src/reducers/mediaLibrary.ts +++ b/src/reducers/mediaLibrary.ts @@ -83,7 +83,7 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) { replaceIndex, }); } - return state.withMutations(map => { + return state.withMutations((map: Map) => { 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) => { map.set('isLoading', false); map.set('isPaginating', false); map.set('page', page); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index ad2e4c4c..92d34243 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,3 +1,5 @@ +export {}; + declare global { import type { CmsConfig } from './interface'; diff --git a/src/types/redux.ts b/src/types/redux.ts index b3f7815a..5920dfd6 100644 --- a/src/types/redux.ts +++ b/src/types/redux.ts @@ -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; collections: List>; @@ -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 { collection: string; }; } - -export interface EditorialWorkflowAction extends Action { - 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; - }; -} diff --git a/src/ui/Icon/images/_index.js b/src/ui/Icon/images/_index.js index d6d88f56..b6843716 100644 --- a/src/ui/Icon/images/_index.js +++ b/src/ui/Icon/images/_index.js @@ -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, }; diff --git a/src/ui/Icon/images/workflow.svg b/src/ui/Icon/images/workflow.svg deleted file mode 100644 index de6512bf..00000000 --- a/src/ui/Icon/images/workflow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/valueObjects/AssetProxy.ts b/src/valueObjects/AssetProxy.ts index 28f51c07..86f73799 100644 --- a/src/valueObjects/AssetProxy.ts +++ b/src/valueObjects/AssetProxy.ts @@ -14,7 +14,7 @@ export default class AssetProxy { field?: EntryField; constructor({ url, file, path, field }: AssetProxyArgs) { - this.url = url ? url : window.URL.createObjectURL(file); + this.url = url ? url : window.URL.createObjectURL(file as Blob); this.fileObj = file; this.path = path; this.field = field; diff --git a/things-to-remove.txt b/things-to-remove.txt new file mode 100644 index 00000000..e69de29b diff --git a/website/content/docs/add-to-your-site.md b/website/content/docs/add-to-your-site.md index 5c5e5bae..129f27c7 100644 --- a/website/content/docs/add-to-your-site.md +++ b/website/content/docs/add-to-your-site.md @@ -95,17 +95,6 @@ backend: The configuration above specifies your backend protocol and your publication branch. Git Gateway is an open source API that acts as a proxy between authenticated users of your site and your site repo. (We'll get to the details of that in the [Authentication section](#authentication) below.) If you leave out the `branch` declaration, it defaults to `master`. -### Editorial Workflow - -**Note:** Editorial workflow works with GitHub repositories, and support for GitLab and Bitbucket is [in beta](/docs/beta-features/#gitlab-and-bitbucket-editorial-workflow-support). - -By default, saving a post in the CMS interface pushes a commit directly to the publication branch specified in `backend`. However, you also have the option to enable the [Editorial Workflow](../configuration-options/#publish-mode), which adds an interface for drafting, reviewing, and approving posts. To do this, add the following line to your Simple CMS `config.yml`: - -```yaml -# This line should *not* be indented -publish_mode: editorial_workflow -``` - ### Media and Public Folders Simple CMS allows users to upload images directly within the editor. For this to work, the CMS needs to know where to save them. If you already have an `images` folder in your project, you could use its path, possibly creating an `uploads` sub-folder, for example: diff --git a/website/content/docs/architecture.md b/website/content/docs/architecture.md index 99119f3e..20a585d2 100644 --- a/website/content/docs/architecture.md +++ b/website/content/docs/architecture.md @@ -63,16 +63,3 @@ The control component receives one (1) callback as a prop: `onChange`. Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its URI) for the user should always be done via `getAsset`, as it returns an AssetProxy that can return the correct value for both medias already persisted on the server and cached media not yet uploaded. The actual persistence of the content and medias inserted into the control component is delegated to the backend implementation. The backend will be called with the updated values and a list of assetProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs. - - -## Editorial Workflow implementation - -Instead of adding logic to `CollectionPage` and `EntryPage`, the Editorial Workflow is implemented as Higher Order Components, adding UI and dispatching additional actions. - -Furthermore, all editorial workflow state is managed in Redux - there's an `actions/editorialWorkflow.js` file and a `reducers/editorialWorkflow.js` file. - -### About metadata - -Simple CMS embraces the idea of Git-as-backend for storing metadata. The first time it runs with the `editorial_workflow` setup, it creates a new ref called `meta/_simple_cms`, pointing to an empty, orphan tree. - -Actual data are stored in individual `json` files committed to this tree. diff --git a/website/content/docs/backends-overview.md b/website/content/docs/backends-overview.md index 51c1d98b..91489e63 100644 --- a/website/content/docs/backends-overview.md +++ b/website/content/docs/backends-overview.md @@ -18,7 +18,6 @@ Individual backends should provide their own configuration documentation, but th | `site_domain` | `location.hostname` (or `cms.netlify.com` when on `localhost`) | Sets the `site_id` query param sent to the API endpoint. Non-Netlify auth setups will often need to set this for local development to work properly. | | `base_url` | `https://api.netlify.com` (GitHub, Bitbucket) or `https://gitlab.com` (GitLab) | OAuth client hostname (just the base domain, no path). **Required** when using an external OAuth server or self-hosted GitLab. | | `auth_endpoint` | `auth` (GitHub, Bitbucket) or `oauth/authorize` (GitLab) | Path to append to `base_url` for authentication requests. Optional. | -| `cms_label_prefix` | `simple-cms/` | Pull (or Merge) Requests label prefix when using editorial workflow. Optional. | ## Creating a New Backend diff --git a/website/content/docs/beta-features.md b/website/content/docs/beta-features.md index 912df561..44611370 100644 --- a/website/content/docs/beta-features.md +++ b/website/content/docs/beta-features.md @@ -28,7 +28,7 @@ local_backend: true 4. Start your local development server (e.g. run `gatsby develop`). 5. Open `http://localhost:/admin` to verify that your can administer your content locally. Replace `` with the port of your local development server. For example Gatsby's default port is `8000` -**Note:** `netlify-cms-proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development. Also note that `editorial_workflow` is not supported in this environment. +**Note:** `netlify-cms-proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development. ### Configure the Simple CMS proxy server port number @@ -51,16 +51,6 @@ local_backend: allowed_hosts: ['192.168.0.1'] ``` -## GitLab and BitBucket Editorial Workflow Support - -You can enable the Editorial Workflow with the following line in your Simple CMS `config.yml` file: - -```yaml -publish_mode: editorial_workflow -``` - -In order to track unpublished entries statuses the GitLab implementation uses merge requests labels and the BitBucket implementation uses pull requests comments. - ## i18n Support The CMS can provide a side by side interface for authoring content in multiple languages. @@ -224,13 +214,6 @@ backend: # optional, defaults to 'https://gitlab.com/api/graphql'. Can be used to configure a self hosted GitLab instance. graphql_api_root: https://my-self-hosted-gitlab.com/api/graphql ``` -## Open Authoring - -When using the [GitHub backend](/docs/github-backend), you can use Simple CMS to accept contributions from GitHub users without giving them access to your repository. When they make changes in the CMS, the CMS forks your repository for them behind the scenes, and all the changes are made to the fork. When the contributor is ready to submit their changes, they can set their draft as ready for review in the CMS. This triggers a pull request to your repository, which you can merge using the GitHub UI. - -At the same time, any contributors who *do* have write access to the repository can continue to use Simple CMS normally. - -More details and setup instructions can be found on [the Open Authoring docs page](/docs/open-authoring). ## Folder Collections Path @@ -473,19 +456,6 @@ import styles from '!css-loader!sass-loader!../main.scss'; CMS.registerPreviewStyle(styles.toString(), { raw: true }); ``` -## Squash merge GitHub pull requests - -When using the [Editorial Workflow](../configuration-options/#publish-mode) with the `github` or GitHub-connected `git-gateway` backends, Simple CMS creates a pull request for each unpublished entry. Every time the unpublished entry is changed and saved, a new commit is added to the pull request. When the entry is published, the pull request is merged, and all of those commits are added to your project commit history in a merge commit. - -The squash merge option causes all commits to be "squashed" into a single commit when the pull request is merged, and the resulting commit is rebased onto the target branch, avoiding the merge commit altogether. - -To enable this feature, you can set the following option in your Simple CMS `config.yml`: - -```yaml -backend: - squash_merges: true -``` - ## Commit Message Templates You can customize the templates used by Simple CMS to generate commit messages by setting the `commit_messages` option under `backend` in your Simple CMS `config.yml`. @@ -502,7 +472,6 @@ backend: delete: Delete {{collection}} “{{slug}}” uploadMedia: Upload “{{path}}” deleteMedia: Delete “{{path}}” - openAuthoring: '{{message}}' ``` Simple CMS generates the following commit types: @@ -514,7 +483,6 @@ Simple CMS generates the following commit types: | `delete` | An existing entry is deleted | `slug`, `path`, `collection`, `author-login`, `author-name` | | `uploadMedia` | A media file is uploaded | `path`, `author-login`, `author-name` | | `deleteMedia` | A media file is deleted | `path`, `author-login`, `author-name` | -| `openAuthoring` | A commit is made via a forked repository | `message`, `author-login`, `author-name` | Template tags produce the following output: @@ -575,7 +543,7 @@ CMS.registerEventListener({ }); ``` -Supported events are `prePublish`, `postPublish`, `preUnpublish`, `postUnpublish`, `preSave` and `postSave`. The `preSave` hook can be used to modify the entry data like so: +Supported events are `prePublish`, `postPublish`, `preSave` and `postSave`. The `preSave` hook can be used to modify the entry data like so: ```javascript CMS.registerEventListener({ diff --git a/website/content/docs/bitbucket-backend.md b/website/content/docs/bitbucket-backend.md index 10a5326b..e263ee4b 100644 --- a/website/content/docs/bitbucket-backend.md +++ b/website/content/docs/bitbucket-backend.md @@ -20,7 +20,7 @@ To enable it: With Bitbucket's Implicit Grant, users can authenticate with Bitbucket directly from the client. To do this: -1. Follow the [Atlassian docs](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html) to create an OAuth consumer. Make sure you allow `Account/Read` and `Repository/Write` permissions. To use the [Editorial Workflow](https://www.simplecms.github.io/simple-cms/docs/configuration-options/#publish-mode), allow `PullRequests/Write` permissions. For the **Callback URL**, enter the address where you access Simple CMS, for example, `https://www.mysite.com/admin/`. +1. Follow the [Atlassian docs](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html) to create an OAuth consumer. Make sure you allow `Account/Read` and `Repository/Write` permissions. For the **Callback URL**, enter the address where you access Simple CMS, for example, `https://www.mysite.com/admin/`. 2. Bitbucket gives you a **Key**. Copy this Key and enter it in your Simple CMS `config.yml` file, along with the following settings: ```yaml diff --git a/website/content/docs/configuration-options.md b/website/content/docs/configuration-options.md index 5e169938..30ddb93d 100644 --- a/website/content/docs/configuration-options.md +++ b/website/content/docs/configuration-options.md @@ -26,29 +26,6 @@ The `backend` option specifies how to access the content for your site, includin **Note**: no matter where you access Simple CMS — whether running locally, in a staging environment, or in your published site — it will always fetch and commit files in your hosted repository (for example, on GitHub), on the branch you configured in your Simple CMS config.yml file. This means that content fetched in the admin UI will match the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI will save directly to the hosted repository, even if you're running the UI locally or in staging. If you want to have your local CMS write to a local repository, try the `local_backend` setting, [currently in beta](/docs/beta-features/#working-with-a-local-git-repository). -## Publish Mode - -By default, all entries created or edited in the Simple CMS are committed directly into the main repository branch. - -The `publish_mode` option allows you to enable "Editorial Workflow" mode for more control over the content publishing phases. All unpublished entries will be arranged in a board according to their status, and they can be further reviewed and edited before going live. - -**Note:** Editorial workflow works with GitHub repositories, and support for GitLab and Bitbucket is [in beta](/docs/beta-features/#gitlab-and-bitbucket-editorial-workflow-support). - -You can enable the Editorial Workflow with the following line in your Simple CMS `config.yml` file: - -```yaml -# /admin/config.yml -publish_mode: editorial_workflow -``` - -From a technical perspective, the workflow translates editor UI actions into common Git commands: - -| Actions in Simple CMS UI | Perform these Git actions | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| Save draft | Commits to a new branch (named according to the pattern `cms/collectionName/entrySlug`), and opens a pull request | -| Edit draft | Pushes another commit to the draft branch/pull request | -| Approve and publish draft | Merges pull request and deletes branch | - ## Media and Public Folders Simple CMS users can upload files to your repository using the Media Gallery. The following settings specify where these files are saved, and where they can be accessed on your built site. @@ -148,16 +125,6 @@ When a translation for the selected locale is missing the English one will be us > All locales are registered by default (so you only need to update your `config.yml`). -## Show Preview Links - -[Deploy preview links](../deploy-preview-links) can be disabled by setting `show_preview_links` to `false`. - -**Example:** - -```yaml -show_preview_links: false -``` - ## Search The search functionally requires loading all collection(s) entries, which can exhaust rate limits on large repositories. @@ -210,7 +177,6 @@ The `collections` setting is the heart of your Simple CMS configuration, as it d * `files` or `folder` (requires one of these): specifies the collection type and location; details in [Collection Types](../collection-types) * `filter`: optional filter for `folder` collections; details in [Collection Types](../collection-types) * `create`: for `folder` collections only; `true` allows users to create new items in the collection; defaults to `false` -* `publish`: for `publish_mode: editorial_workflow` only; `false` hides UI publishing controls for a collection; defaults to `true` * `hide`: `true` hides a collection in the CMS UI; defaults to `false`. Useful when using the relation widget to hide referenced collections. * `delete`: `false` prevents users from deleting items in a collection; defaults to `true` * `extension`: see detailed description below @@ -297,46 +263,6 @@ slug: "{{year}}-{{month}}-{{day}}_{{title}}_{{some_other_field}}" slug: "{{year}}-{{month}}-{{day}}_{{fields.slug}}" ``` -### `preview_path` - -A string representing the path where content in this collection can be found on the live site. This allows deploy preview links to direct to lead to a specific piece of content rather than the site root of a deploy preview. - -**Available template tags:** - -Template tags are the same as those for [slug](#slug), with the following exceptions: - -* `{{slug}}` is the entire slug for the current entry (not just the url-safe identifier, as is the case with [`slug` configuration](#slug)) -* The date based template tags, such as `{{year}}` and `{{month}}`, are pulled from a date field in your entry, and may require additional configuration - see [`preview_path_date_field`](#preview_path_date_field) for details. If a date template tag is used and no date can be found, `preview_path` will be ignored. -* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`. -* `{{filename}}` The file name without the extension part. -* `{{extension}}` The file extension. - -**Examples:** - -```yaml -collections: - - name: posts - preview_path: "blog/{{year}}/{{month}}/{{slug}}" -``` - -```yaml -collections: - - name: posts - preview_path: "blog/{{year}}/{{month}}/{{filename}}.{{extension}}" -``` - -### `preview_path_date_field` - -The name of a date field for parsing date-based template tags from `preview_path`. If this field is not provided and `preview_path` contains date-based template tags (eg. `{{year}}`), Simple CMS will attempt to infer a usable date field by checking for common date field names, such as `date`. If you find that you need to specify a date field, you can use `preview_path_date_field` to tell Simple CMS which field to use for preview path template tags. - -**Example:** - -```yaml -collections: - - name: posts - preview_path_date_field: "updated_on" -``` - ### `fields` The `fields` option maps editor UI widgets to field-value pairs in the saved file. The order of the fields in your Simple CMS `config.yml` file determines their order in the editor UI and in the saved file. @@ -454,4 +380,4 @@ Defaults to an empty list. pattern: \d{4} - label: Drafts field: draft -``` \ No newline at end of file +``` diff --git a/website/content/docs/deploy-preview-links.md b/website/content/docs/deploy-preview-links.md deleted file mode 100644 index 33653c2d..00000000 --- a/website/content/docs/deploy-preview-links.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -group: Workflow -weight: 10 -title: Deploy Preview Links ---- -When using the editorial workflow, content editors can create and save content without publishing it -to a live site. Deploy preview links provide a way to view live content when it has not been -published, provided that you're using a continuous deployment platform to provide "deploy previews" -of your unmerged content. - -## Using deploy preview links - -Deploy preview links will work without configuration when all of the following requirements are met: - -* Simple CMS version is 2.4.0+ for GitHub support and 2.10.6+ for GitLab/Bitbucket support -* Using editorial workflow -* Have a continuous deployment platform that builds every commit and provides statuses to your repo - -Any site created using one of the Deploy to Netlify options on our [starters -page](../start-with-a-template) will automatically meet these criteria (barring any changes made to -your Netlify settings), but you may need to [update](../update-the-cms-version) your Simple CMS version to get the -functionality. - -**Note:** If you're using a custom backend (one that is not included with Simple CMS), please check the -documentation for that backend for more information about enabling deploy preview links. - -Deploy preview links are provided in the editor toolbar, near the publishing controls: - -![Deploy preview link for unpublished content](/img/preview-link-unpublished.png) - -### Waiting for builds - -Deploy your site preview may take ten seconds or ten minutes, depending on many factors. For maximum -flexibility, Simple CMS provides a "Check for Preview" refresh button when the deploy preview is -pending, which a content editor can use to manually check for a finished preview until it's ready: - -![Deploy preview link for unpublished content](/img/preview-link-check.png) - -## Configuring preview paths - -Deploy preview links point to the site root by default, but you'll probably want them to point to -the specific piece of content that the content editor is viewing. You can do this by providing a -`preview_path` string template for each collection, or for inidividual files in a files collection. - -Let's say we have a `blog` collection that stores content in our repo under `content/blog`. The path -to a post in your repo may look like `content/blog/2018-01-new-post.md`, but the path to that post -on your site would look more like: `/blog/2018-01-new-post/`. Here's how you would use -`preview_path` in your configuration for this scenario: - -```yaml -collections: - - name: blog - folder: content/blog - slug: {{year}}-{{month}}-{{slug}} - preview_path: blog/{{slug}} -``` - -Similarly, for an `about` page in a files collection under `content/pages` which maps to `/about-the-project` -on your site, you would configure `preview_path` like this: - -```yaml -collections: - - name: pages - files: - - name: about - file: content/pages/about.md - preview_path: about-the-project -``` - -With the above configuration, the deploy preview URL from your backend will be combined with your -preview path to create a URL to a specific blog post. - -**Note:** `{{slug}}` in `preview_path` is different than `{{slug}}` in `slug`. In the `slug` -template, `{{slug}}` is only the url-safe [identifier -field](../configuration-options/#identifier_field), while in the `preview_path` template, `{{slug}}` -is the entire slug for the entry. For example: - -```yaml -# for an entry created Jan 1, 2000 with identifier "My New Post!" -collections: - - name: posts - slug: {{year}}-{{month}}-{{slug}} # {{slug}} will compile to "my-new-post" - preview_path: blog/{{slug}} # {{slug}} will compile to "2000-01-my-new-post" -``` - -### Dates in preview paths - -Some static site generators allow URL's to be customized with date parameters - for example, Hugo -can be configured to use values like `year` and `month` in a URL. These values are generally derived -by the static site generator from a date field in the content file. `preview_path` accepts these -parameters as well, similar to the `slug` configuration, except `preview_path` populates date values -based on a date value from the entry, just like static site generators do. Simple CMS will attempt -to infer an obvious date field, but you can also specify which date field to use for `preview_path` -template tags by using -[`preview_path_date_field`](../configuration-options/#preview_path_date_field). - -Together with your other field values, dates can be used to configure most URL schemes available -through static site generators. - -**Example** - -```yaml -# This collection's date field will be inferred because it has a field named `"date"` -collections: - - name: posts - preview_path: blog/{{year}}/{{month}}/{{title}} - fields: - - { name: title, label: Title } - { name: date, label: Date, widget: date } - { name: body, label: Body, widget: markdown } -# This collection requires `path_preview_date_field` because the no obvious date field is available -collections: - - name: posts - preview_path: blog/{{year}}/{{month}}/{{title}} - preview_path_date_field: published_at - fields: - - { name: title, label: Title } - { name: published_at, label: Published At, widget: date } - { name: body, label: Body, widget: markdown } -``` - -## Preview links for published content - -You may also want preview links for published content as a convenience. You can do this by providing -a `site_url` in your configuration, which will be used in place of the deploy preview URL that a -backend would provide for an unpublished entry. Just as for deploy preview links to unpublished -content, links to published content will use any `preview_path` values that are defined in the -collection configurations. - -Preview links for published content will also work if you are not using the editorial workflow. - -![Deploy preview link for unpublished content](/img/preview-link-unpublished.png) - -## Disabling deploy preview links - -To disable deploy preview links, set `show_preview_links` to false in your CMS configuration. - -## How it works - -Deploy preview links are provided through your CMS backend, and Simple CMS is unopinionated about -where the links come from or how they're created. That said, the general approach for Git backends -like GitHub is powered by "commit statuses". Continuous deployment platforms like Netlify can deploy -a version of your site for every commit that is pushed to your remote Git repository, and then send -a commit status back to your repository host with the URL. - -The deploy preview URL provided by a backend will lead to the root of the deployed site. Simple CMS -will then use the `preview_path` template in an entry's collection configuration to build a path to -a specific piece of content. If a `preview_path` is not provided for an entry's collection, the URL -will be used as is. diff --git a/website/content/docs/github-backend.md b/website/content/docs/github-backend.md index 9883fbc7..d74bade1 100644 --- a/website/content/docs/github-backend.md +++ b/website/content/docs/github-backend.md @@ -20,22 +20,6 @@ backend: # branch: main ``` -## Specifying a status for deploy previews - -The GitHub backend supports [deploy preview links](../deploy-preview-links). Simple CMS checks the -`context` of a commit's [statuses](https://help.github.com/articles/about-status-checks/) and infers -one that seems to represent a deploy preview. If you need to customize this behavior, you can -specify which context to look for using `preview_context`: - -```yaml -backend: - name: github - repo: my/repo - preview_context: my-provider/deployment -``` - -The above configuration would look for the status who's `"context"` is `"my-provider/deployment"`. - ## Git Large File Storage (LFS) Please note that the GitHub backend **does not** support [git-lfs](https://git-lfs.github.com/), see [this issue](https://github.com/SimpleCMS/simple-cms/issues/1206) for more information. diff --git a/website/content/docs/open-authoring.md b/website/content/docs/open-authoring.md deleted file mode 100644 index bbeaef4a..00000000 --- a/website/content/docs/open-authoring.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -group: Workflow -weight: 20 -title: Open Authoring ---- -**This is a [beta feature](/docs/beta-features#open-authoring).** - -When using the [GitHub backend](/docs/github-backend), you can use Simple CMS to accept contributions from GitHub users without giving them access to your repository. When they make changes in the CMS, the CMS forks your repository for them behind the scenes, and all the changes are made to the fork. When the contributor is ready to submit their changes, they can set their draft as ready for review in the CMS. This triggers a pull request to your repository, which you can merge using the GitHub UI. - -At the same time, any contributors who *do* have write access to the repository can continue to use Simple CMS normally. - -## Requirements - -* You must use [the GitHub backend](/docs/github-backend). - - **Note that the [Git Gateway backend](/docs/git-gateway-backend/#git-gateway-with-netlify-identity) does *not* support Open Authoring, even when the underlying repo is on GitHub.** -* For private GitHub repos the user must have `read` access on the repo, and you must explicitly set the auth_scope to `repo`, for example: - -```yaml -backend: - name: github - repo: owner-name/private-repo-name # path to private repo - auth_scope: repo # this is needed to fork the private repo - open_authoring: true -``` - -## Enabling Open Authoring - -1. [Enable the editorial workflow](/docs/configuration-options/#publish-mode) by setting `publish_mode` to `editorial_workflow` in your `config.yml`. -2. Set `open_authoring` to `true` in the `backend` section of your `config.yml`, as follows: - - ```yaml - backend: - name: github - repo: owner-name/repo-name # Path to your GitHub repository - open_authoring: true - ``` - -## Usage - -When a user logs into Simple CMS who doesn't have write access to your repo, the CMS asks for permission to create a fork of your repo (or uses their existing fork, if they already have one). They are then presented with the normal CMS interface. The published content shown is from the original repo, so it stays up-to-date as changes are made. - -On the editorial workflow screen, the normal three columns are replaced by two columns instead — "Draft" and "Ready to Review". - -When they make changes to content in the CMS, the changes are made to a branch on their fork. In the editorial workflow screen, they see only their own pending changes. Once they're ready to submit their changes, they can move the card into the "Ready To Review" column to create a pull request. When the pull request is merged (by a repository maintainer via the GitHub UI), Simple CMS deletes the branch and removes the card from the user's editorial workflow screen. Open Authoring users cannot publish entries through the CMS. - -Users who *do* have write access to the original repository continue to use the CMS normally. Unpublished changes made by users via Open Authoring are not visible on the editorial workflow screen, and their unpublished changes must be merged through the GitHub UI. - -## Alternative for external contributors with Git Gateway - -[As noted above](#requirements), Open Authoring does not work with the Git Gateway backend. However, you can use Git Gateway on a site with Netlify Identity that has [open registration](https://www.netlify.com/docs/identity/#adding-identity-users). This lets users create accounts on your site and log into the CMS. There are a few differences, including the following: - -* Users don't need to know about GitHub or create a GitHub account. Instead, they use Netlify Identity accounts that are created on your site and managed by you. -* The CMS applies users' changes directly to your repo, not to a fork. (If you use the editorial workflow, you can use features like [GitHub's protected branches](https://help.github.com/en/articles/about-protected-branches) or [Netlify's locked deploys](https://www.netlify.com/docs/locked-deploys/) to prevent users from publishing directly to your site from the CMS.) -* There is no distinction between users with write access to the repo and users without — all editorial workflow entries are visible from within the CMS and can be published with the CMS. (Unpublished Open Authoring entries, on the other hand, are visible only to the author in the CMS UI or publicly as GitHub PRs.) - -## Linking to specific entries in the CMS - -Open authoring often includes some sort of "Edit this page" link on the live site. Simple CMS supports this via the **edit** path: - -```js -/#/edit/{collectionName}/{entryName} -``` - -For the entry named "general" in the "settings" file collection - -```html -https://www.example.com/path-to-cms/#/edit/settings/general -``` - -For blog post "test.md" in the "posts" folder collection - -```html -https://www.example.com/path-to-cms/#/edit/posts/test -``` - -* **`collectionName`**: the name of the collection as entered in the CMS config. -* **`entryName`** *(for [file collections](/docs/collection-types/#file-collections)*: the `name` of the entry from the CMS config. -* **`entryName`** *(for [folder collections](/docs/collection-types/#folder-collections)*: the filename, sans extension (the slug). \ No newline at end of file diff --git a/website/content/docs/site-generator-overview.md b/website/content/docs/site-generator-overview.md index a0fea023..260317f3 100644 --- a/website/content/docs/site-generator-overview.md +++ b/website/content/docs/site-generator-overview.md @@ -23,7 +23,7 @@ Once you've gotten the hang of it, you can use the file to build whatever collec ### Render the content provided by Simple CMS as web pages -Simple CMS manages your content, and provides editorial and admin features, but it doesn't deliver content. It only makes your content available through an API. +Simple CMS manages your content, and provides admin features, but it doesn't deliver content. It only makes your content available through an API. It is up to developers to determine how to build the raw content into something useful and delightful on the frontend. diff --git a/website/content/docs/writing-style-guide.md b/website/content/docs/writing-style-guide.md index 7c1dd7e2..36d5fbe1 100644 --- a/website/content/docs/writing-style-guide.md +++ b/website/content/docs/writing-style-guide.md @@ -111,7 +111,7 @@ _____ For field values of type string or integer, use normal style without quotation marks. -Do: Set the value of `publish_mode` to editorial_workflow. +Do: Set the value of `imagePullPolicy` to Always. Don't: Set the value of `imagePullPolicy` to "Always". _____ diff --git a/website/static/_redirects b/website/static/_redirects index ea897419..16e843a0 100644 --- a/website/static/_redirects +++ b/website/static/_redirects @@ -3,7 +3,6 @@ /docs/authentication-backends /docs/backends-overview 301 /docs/extending /docs/custom-widgets 301 /docs/validation /docs/custom-widgets/#advanced-field-validation 301 -/docs/editorial-workflow /docs/configuration/#publish-mode 301 /docs/test-drive /docs/start-with-a-template 301 /docs/quick-start /docs/add-to-your-site 301 /simple-cms/chat https://join.slack.com/t/simple-cms/shared_invite/zt-1gvgnf5yv-E4sR17YnEcOy6fLFH9m7bQ 301 diff --git a/website/static/admin/config.yml b/website/static/admin/config.yml index f52974bc..bb2be3d6 100644 --- a/website/static/admin/config.yml +++ b/website/static/admin/config.yml @@ -1,15 +1,11 @@ backend: name: github repo: SimpleCMS/simple-cms - squash_merges: true - open_authoring: true local_backend: true site_url: https://www.netlifycms.org -publish_mode: editorial_workflow - media_folder: website/static/img public_folder: /img diff --git a/yarn.lock b/yarn.lock index ef61b342..33d98914 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1761,6 +1761,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/minimatch@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + "@types/minimist@^1.2.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"