@pranaysahith/decap-cms-core 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/@pranaysahith/decap-cms-core.js +52 -0
- package/dist/@pranaysahith/decap-cms-core.js.LICENSE.txt +141 -0
- package/dist/@pranaysahith/decap-cms-core.js.map +1 -0
- package/dist/decap-cms-core.js +47 -0
- package/dist/decap-cms-core.js.LICENSE.txt +116 -0
- package/dist/decap-cms-core.js.map +1 -0
- package/dist/esm/actions/auth.js +97 -0
- package/dist/esm/actions/collections.js +15 -0
- package/dist/esm/actions/config.js +493 -0
- package/dist/esm/actions/deploys.js +79 -0
- package/dist/esm/actions/editorialWorkflow.js +480 -0
- package/dist/esm/actions/entries.js +865 -0
- package/dist/esm/actions/media.js +147 -0
- package/dist/esm/actions/mediaLibrary.js +552 -0
- package/dist/esm/actions/notifications.js +21 -0
- package/dist/esm/actions/search.js +149 -0
- package/dist/esm/actions/status.js +74 -0
- package/dist/esm/actions/waitUntil.js +32 -0
- package/dist/esm/backend.js +1082 -0
- package/dist/esm/bootstrap.js +101 -0
- package/dist/esm/components/App/App.js +289 -0
- package/dist/esm/components/App/Header.js +172 -0
- package/dist/esm/components/App/NotFoundPage.js +19 -0
- package/dist/esm/components/Collection/Collection.js +198 -0
- package/dist/esm/components/Collection/CollectionControls.js +46 -0
- package/dist/esm/components/Collection/CollectionSearch.js +222 -0
- package/dist/esm/components/Collection/CollectionTop.js +68 -0
- package/dist/esm/components/Collection/ControlButton.js +17 -0
- package/dist/esm/components/Collection/Entries/Entries.js +73 -0
- package/dist/esm/components/Collection/Entries/EntriesCollection.js +241 -0
- package/dist/esm/components/Collection/Entries/EntriesSearch.js +113 -0
- package/dist/esm/components/Collection/Entries/EntryCard.js +177 -0
- package/dist/esm/components/Collection/Entries/EntryListing.js +143 -0
- package/dist/esm/components/Collection/FilterControl.js +33 -0
- package/dist/esm/components/Collection/FolderRenameControl.js +403 -0
- package/dist/esm/components/Collection/GroupControl.js +33 -0
- package/dist/esm/components/Collection/NestedCollection.js +308 -0
- package/dist/esm/components/Collection/Sidebar.js +91 -0
- package/dist/esm/components/Collection/SortControl.js +59 -0
- package/dist/esm/components/Collection/ViewStyleControl.js +38 -0
- package/dist/esm/components/Editor/Editor.js +466 -0
- package/dist/esm/components/Editor/EditorControlPane/EditorControl.js +395 -0
- package/dist/esm/components/Editor/EditorControlPane/EditorControlPane.js +254 -0
- package/dist/esm/components/Editor/EditorControlPane/Widget.js +374 -0
- package/dist/esm/components/Editor/EditorInterface.js +386 -0
- package/dist/esm/components/Editor/EditorPreviewPane/EditorPreview.js +47 -0
- package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewContent.js +66 -0
- package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +288 -0
- package/dist/esm/components/Editor/EditorPreviewPane/PreviewHOC.js +27 -0
- package/dist/esm/components/Editor/EditorToolbar.js +536 -0
- package/dist/esm/components/Editor/EntryPathEditor.js +272 -0
- package/dist/esm/components/Editor/withWorkflow.js +56 -0
- package/dist/esm/components/EditorWidgets/Unknown/UnknownControl.js +18 -0
- package/dist/esm/components/EditorWidgets/Unknown/UnknownPreview.js +20 -0
- package/dist/esm/components/EditorWidgets/index.js +4 -0
- package/dist/esm/components/MediaLibrary/EmptyMessage.js +22 -0
- package/dist/esm/components/MediaLibrary/MediaLibrary.js +446 -0
- package/dist/esm/components/MediaLibrary/MediaLibraryButtons.js +93 -0
- package/dist/esm/components/MediaLibrary/MediaLibraryCard.js +99 -0
- package/dist/esm/components/MediaLibrary/MediaLibraryCardGrid.js +198 -0
- package/dist/esm/components/MediaLibrary/MediaLibraryHeader.js +32 -0
- package/dist/esm/components/MediaLibrary/MediaLibraryModal.js +156 -0
- package/dist/esm/components/MediaLibrary/MediaLibrarySearch.js +51 -0
- package/dist/esm/components/MediaLibrary/MediaLibraryTop.js +123 -0
- package/dist/esm/components/UI/DragDrop.js +67 -0
- package/dist/esm/components/UI/ErrorBoundary.js +173 -0
- package/dist/esm/components/UI/FileUploadButton.js +27 -0
- package/dist/esm/components/UI/Modal.js +104 -0
- package/dist/esm/components/UI/Notifications.js +62 -0
- package/dist/esm/components/UI/SettingsDropdown.js +107 -0
- package/dist/esm/components/UI/index.js +6 -0
- package/dist/esm/components/Workflow/Workflow.js +133 -0
- package/dist/esm/components/Workflow/WorkflowCard.js +128 -0
- package/dist/esm/components/Workflow/WorkflowList.js +204 -0
- package/dist/esm/constants/collectionTypes.js +2 -0
- package/dist/esm/constants/collectionViews.js +2 -0
- package/dist/esm/constants/commitProps.js +2 -0
- package/dist/esm/constants/configSchema.js +644 -0
- package/dist/esm/constants/fieldInference.js +57 -0
- package/dist/esm/constants/publishModes.js +18 -0
- package/dist/esm/constants/validationErrorTypes.js +6 -0
- package/dist/esm/formats/formats.js +83 -0
- package/dist/esm/formats/frontmatter.js +146 -0
- package/dist/esm/formats/helpers.js +12 -0
- package/dist/esm/formats/json.js +8 -0
- package/dist/esm/formats/toml.js +32 -0
- package/dist/esm/formats/yaml.js +51 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/integrations/index.js +28 -0
- package/dist/esm/integrations/providers/algolia/implementation.js +174 -0
- package/dist/esm/integrations/providers/assetStore/implementation.js +165 -0
- package/dist/esm/lib/consoleError.js +3 -0
- package/dist/esm/lib/formatters.js +191 -0
- package/dist/esm/lib/i18n.js +367 -0
- package/dist/esm/lib/phrases.js +6 -0
- package/dist/esm/lib/polyfill.js +8 -0
- package/dist/esm/lib/registry.js +329 -0
- package/dist/esm/lib/serializeEntryValues.js +67 -0
- package/dist/esm/lib/stega.js +142 -0
- package/dist/esm/lib/textHelper.js +9 -0
- package/dist/esm/lib/urlHelper.js +111 -0
- package/dist/esm/mediaLibrary.js +37 -0
- package/dist/esm/reducers/auth.js +27 -0
- package/dist/esm/reducers/collections.js +428 -0
- package/dist/esm/reducers/combinedReducer.js +8 -0
- package/dist/esm/reducers/config.js +29 -0
- package/dist/esm/reducers/cursors.js +31 -0
- package/dist/esm/reducers/deploys.js +45 -0
- package/dist/esm/reducers/editorialWorkflow.js +83 -0
- package/dist/esm/reducers/entries.js +568 -0
- package/dist/esm/reducers/entryDraft.js +212 -0
- package/dist/esm/reducers/globalUI.js +25 -0
- package/dist/esm/reducers/index.js +66 -0
- package/dist/esm/reducers/integrations.js +53 -0
- package/dist/esm/reducers/mediaLibrary.js +252 -0
- package/dist/esm/reducers/medias.js +68 -0
- package/dist/esm/reducers/notifications.js +23 -0
- package/dist/esm/reducers/search.js +92 -0
- package/dist/esm/reducers/status.js +30 -0
- package/dist/esm/redux/index.js +7 -0
- package/dist/esm/redux/middleware/waitUntilAction.js +48 -0
- package/dist/esm/routing/history.js +12 -0
- package/dist/esm/types/diacritics.d.js +0 -0
- package/dist/esm/types/global.d.js +1 -0
- package/dist/esm/types/immutable.js +7 -0
- package/dist/esm/types/redux.js +14 -0
- package/dist/esm/types/tomlify-j0.4.d.js +0 -0
- package/dist/esm/valueObjects/AssetProxy.js +44 -0
- package/dist/esm/valueObjects/EditorComponent.js +34 -0
- package/dist/esm/valueObjects/Entry.js +20 -0
- package/index.d.ts +618 -0
- package/package.json +106 -0
- package/src/__tests__/backend.spec.js +1161 -0
- package/src/actions/__tests__/config.spec.js +1009 -0
- package/src/actions/__tests__/editorialWorkflow.spec.js +216 -0
- package/src/actions/__tests__/entries.spec.js +596 -0
- package/src/actions/__tests__/media.spec.ts +171 -0
- package/src/actions/__tests__/mediaLibrary.spec.js +327 -0
- package/src/actions/__tests__/search.spec.js +209 -0
- package/src/actions/auth.ts +127 -0
- package/src/actions/collections.ts +18 -0
- package/src/actions/config.ts +565 -0
- package/src/actions/deploys.ts +104 -0
- package/src/actions/editorialWorkflow.ts +567 -0
- package/src/actions/entries.ts +1055 -0
- package/src/actions/media.ts +139 -0
- package/src/actions/mediaLibrary.ts +574 -0
- package/src/actions/notifications.ts +36 -0
- package/src/actions/search.ts +221 -0
- package/src/actions/status.ts +99 -0
- package/src/actions/waitUntil.ts +49 -0
- package/src/backend.ts +1400 -0
- package/src/bootstrap.js +104 -0
- package/src/components/App/App.js +286 -0
- package/src/components/App/Header.js +266 -0
- package/src/components/App/NotFoundPage.js +23 -0
- package/src/components/Collection/Collection.js +210 -0
- package/src/components/Collection/CollectionControls.js +58 -0
- package/src/components/Collection/CollectionSearch.js +243 -0
- package/src/components/Collection/CollectionTop.js +81 -0
- package/src/components/Collection/ControlButton.js +27 -0
- package/src/components/Collection/Entries/Entries.js +82 -0
- package/src/components/Collection/Entries/EntriesCollection.js +277 -0
- package/src/components/Collection/Entries/EntriesSearch.js +102 -0
- package/src/components/Collection/Entries/EntryCard.js +246 -0
- package/src/components/Collection/Entries/EntryListing.js +151 -0
- package/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js +163 -0
- package/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +46 -0
- package/src/components/Collection/FilterControl.js +39 -0
- package/src/components/Collection/GroupControl.js +39 -0
- package/src/components/Collection/NestedCollection.js +330 -0
- package/src/components/Collection/Sidebar.js +136 -0
- package/src/components/Collection/SortControl.js +68 -0
- package/src/components/Collection/ViewStyleControl.js +50 -0
- package/src/components/Collection/__tests__/Collection.spec.js +75 -0
- package/src/components/Collection/__tests__/NestedCollection.spec.js +445 -0
- package/src/components/Collection/__tests__/Sidebar.spec.js +87 -0
- package/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap +144 -0
- package/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +550 -0
- package/src/components/Collection/__tests__/__snapshots__/Sidebar.spec.js.snap +312 -0
- package/src/components/Editor/Editor.js +497 -0
- package/src/components/Editor/EditorControlPane/EditorControl.js +452 -0
- package/src/components/Editor/EditorControlPane/EditorControlPane.js +269 -0
- package/src/components/Editor/EditorControlPane/Widget.js +384 -0
- package/src/components/Editor/EditorInterface.js +444 -0
- package/src/components/Editor/EditorPreviewPane/EditorPreview.js +40 -0
- package/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js +74 -0
- package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +333 -0
- package/src/components/Editor/EditorPreviewPane/PreviewHOC.js +33 -0
- package/src/components/Editor/EditorToolbar.js +691 -0
- package/src/components/Editor/__tests__/Editor.spec.js +221 -0
- package/src/components/Editor/__tests__/EditorToolbar.spec.js +120 -0
- package/src/components/Editor/__tests__/__snapshots__/Editor.spec.js.snap +45 -0
- package/src/components/Editor/__tests__/__snapshots__/EditorToolbar.spec.js.snap +4233 -0
- package/src/components/Editor/withWorkflow.js +61 -0
- package/src/components/EditorWidgets/Unknown/UnknownControl.js +17 -0
- package/src/components/EditorWidgets/Unknown/UnknownPreview.js +19 -0
- package/src/components/EditorWidgets/index.js +5 -0
- package/src/components/MediaLibrary/EmptyMessage.js +28 -0
- package/src/components/MediaLibrary/MediaLibrary.js +411 -0
- package/src/components/MediaLibrary/MediaLibraryButtons.js +136 -0
- package/src/components/MediaLibrary/MediaLibraryCard.js +128 -0
- package/src/components/MediaLibrary/MediaLibraryCardGrid.js +199 -0
- package/src/components/MediaLibrary/MediaLibraryHeader.js +48 -0
- package/src/components/MediaLibrary/MediaLibraryModal.js +200 -0
- package/src/components/MediaLibrary/MediaLibrarySearch.js +61 -0
- package/src/components/MediaLibrary/MediaLibraryTop.js +143 -0
- package/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js +45 -0
- package/src/components/MediaLibrary/__tests__/MediaLibraryCard.spec.js +49 -0
- package/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap +264 -0
- package/src/components/UI/DragDrop.js +66 -0
- package/src/components/UI/ErrorBoundary.js +214 -0
- package/src/components/UI/FileUploadButton.js +24 -0
- package/src/components/UI/Modal.js +112 -0
- package/src/components/UI/Notifications.tsx +83 -0
- package/src/components/UI/SettingsDropdown.js +103 -0
- package/src/components/UI/__tests__/ErrorBoundary.spec.js +57 -0
- package/src/components/UI/index.js +6 -0
- package/src/components/Workflow/Workflow.js +169 -0
- package/src/components/Workflow/WorkflowCard.js +177 -0
- package/src/components/Workflow/WorkflowList.js +272 -0
- package/src/constants/__tests__/configSchema.spec.js +611 -0
- package/src/constants/collectionTypes.ts +2 -0
- package/src/constants/collectionViews.js +2 -0
- package/src/constants/commitProps.ts +2 -0
- package/src/constants/configSchema.js +441 -0
- package/src/constants/fieldInference.tsx +78 -0
- package/src/constants/publishModes.ts +22 -0
- package/src/constants/validationErrorTypes.js +6 -0
- package/src/formats/__tests__/formats.spec.js +87 -0
- package/src/formats/__tests__/frontmatter.spec.js +429 -0
- package/src/formats/__tests__/toml.spec.js +9 -0
- package/src/formats/__tests__/yaml.spec.js +162 -0
- package/src/formats/formats.ts +97 -0
- package/src/formats/frontmatter.ts +150 -0
- package/src/formats/helpers.ts +14 -0
- package/src/formats/json.ts +9 -0
- package/src/formats/toml.ts +33 -0
- package/src/formats/yaml.ts +58 -0
- package/src/index.js +8 -0
- package/src/integrations/index.js +35 -0
- package/src/integrations/providers/algolia/implementation.js +176 -0
- package/src/integrations/providers/assetStore/implementation.js +148 -0
- package/src/lib/__tests__/formatters.spec.js +751 -0
- package/src/lib/__tests__/i18n.spec.js +792 -0
- package/src/lib/__tests__/phrases.spec.js +119 -0
- package/src/lib/__tests__/registry.spec.js +261 -0
- package/src/lib/__tests__/serializeEntryValues.spec.js +22 -0
- package/src/lib/__tests__/urlHelper.spec.js +138 -0
- package/src/lib/consoleError.js +7 -0
- package/src/lib/formatters.ts +286 -0
- package/src/lib/i18n.ts +454 -0
- package/src/lib/phrases.js +8 -0
- package/src/lib/polyfill.js +9 -0
- package/src/lib/registry.js +312 -0
- package/src/lib/serializeEntryValues.js +75 -0
- package/src/lib/stega.ts +145 -0
- package/src/lib/textHelper.js +11 -0
- package/src/lib/urlHelper.ts +128 -0
- package/src/mediaLibrary.ts +51 -0
- package/src/reducers/__tests__/auth.spec.ts +38 -0
- package/src/reducers/__tests__/collections.spec.js +610 -0
- package/src/reducers/__tests__/config.spec.js +38 -0
- package/src/reducers/__tests__/entries.spec.js +694 -0
- package/src/reducers/__tests__/entryDraft.spec.js +297 -0
- package/src/reducers/__tests__/globalUI.js +43 -0
- package/src/reducers/__tests__/integrations.spec.ts +76 -0
- package/src/reducers/__tests__/mediaLibrary.spec.js +154 -0
- package/src/reducers/__tests__/medias.spec.ts +49 -0
- package/src/reducers/auth.ts +46 -0
- package/src/reducers/collections.ts +535 -0
- package/src/reducers/combinedReducer.ts +11 -0
- package/src/reducers/config.ts +38 -0
- package/src/reducers/cursors.js +36 -0
- package/src/reducers/deploys.ts +52 -0
- package/src/reducers/editorialWorkflow.ts +163 -0
- package/src/reducers/entries.ts +819 -0
- package/src/reducers/entryDraft.js +260 -0
- package/src/reducers/globalUI.ts +45 -0
- package/src/reducers/index.ts +82 -0
- package/src/reducers/integrations.ts +59 -0
- package/src/reducers/mediaLibrary.ts +296 -0
- package/src/reducers/medias.ts +66 -0
- package/src/reducers/notifications.ts +52 -0
- package/src/reducers/search.ts +111 -0
- package/src/reducers/status.ts +40 -0
- package/src/redux/index.ts +18 -0
- package/src/redux/middleware/waitUntilAction.ts +64 -0
- package/src/routing/__tests__/history.spec.ts +49 -0
- package/src/routing/history.ts +17 -0
- package/src/types/diacritics.d.ts +1 -0
- package/src/types/global.d.ts +8 -0
- package/src/types/immutable.ts +49 -0
- package/src/types/redux.ts +827 -0
- package/src/types/tomlify-j0.4.d.ts +13 -0
- package/src/valueObjects/AssetProxy.ts +48 -0
- package/src/valueObjects/EditorComponent.js +38 -0
- package/src/valueObjects/Entry.ts +63 -0
package/src/backend.ts
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
import attempt from 'lodash/attempt';
|
|
2
|
+
import flatten from 'lodash/flatten';
|
|
3
|
+
import isError from 'lodash/isError';
|
|
4
|
+
import uniq from 'lodash/uniq';
|
|
5
|
+
import trim from 'lodash/trim';
|
|
6
|
+
import sortBy from 'lodash/sortBy';
|
|
7
|
+
import get from 'lodash/get';
|
|
8
|
+
import set from 'lodash/set';
|
|
9
|
+
import { List, fromJS, Set } from 'immutable';
|
|
10
|
+
import * as fuzzy from 'fuzzy';
|
|
11
|
+
import {
|
|
12
|
+
localForage,
|
|
13
|
+
Cursor,
|
|
14
|
+
CURSOR_COMPATIBILITY_SYMBOL,
|
|
15
|
+
getPathDepth,
|
|
16
|
+
blobToFileObj,
|
|
17
|
+
asyncLock,
|
|
18
|
+
EDITORIAL_WORKFLOW_ERROR,
|
|
19
|
+
} from 'decap-cms-lib-util';
|
|
20
|
+
import { basename, join, extname, dirname } from 'path';
|
|
21
|
+
import { stringTemplate } from 'decap-cms-lib-widgets';
|
|
22
|
+
|
|
23
|
+
import { resolveFormat } from './formats/formats';
|
|
24
|
+
import { selectUseWorkflow } from './reducers/config';
|
|
25
|
+
import { selectMediaFilePath, selectEntry } from './reducers/entries';
|
|
26
|
+
import { selectIntegration } from './reducers/integrations';
|
|
27
|
+
import {
|
|
28
|
+
selectEntrySlug,
|
|
29
|
+
selectEntryPath,
|
|
30
|
+
selectFileEntryLabel,
|
|
31
|
+
selectAllowNewEntries,
|
|
32
|
+
selectAllowDeletion,
|
|
33
|
+
selectFolderEntryExtension,
|
|
34
|
+
selectInferredField,
|
|
35
|
+
selectMediaFolders,
|
|
36
|
+
selectFieldsComments,
|
|
37
|
+
selectHasMetaPath,
|
|
38
|
+
} from './reducers/collections';
|
|
39
|
+
import { createEntry } from './valueObjects/Entry';
|
|
40
|
+
import { sanitizeChar } from './lib/urlHelper';
|
|
41
|
+
import { getBackend, invokeEvent } from './lib/registry';
|
|
42
|
+
import { commitMessageFormatter, slugFormatter, previewUrlFormatter } from './lib/formatters';
|
|
43
|
+
import { status } from './constants/publishModes';
|
|
44
|
+
import { FOLDER, FILES } from './constants/collectionTypes';
|
|
45
|
+
import { selectCustomPath } from './reducers/entryDraft';
|
|
46
|
+
import {
|
|
47
|
+
getI18nFilesDepth,
|
|
48
|
+
getI18nFiles,
|
|
49
|
+
hasI18n,
|
|
50
|
+
getFilePaths,
|
|
51
|
+
getI18nEntry,
|
|
52
|
+
groupEntries,
|
|
53
|
+
getI18nDataFiles,
|
|
54
|
+
getI18nBackup,
|
|
55
|
+
formatI18nBackup,
|
|
56
|
+
getI18nInfo,
|
|
57
|
+
I18N_STRUCTURE,
|
|
58
|
+
} from './lib/i18n';
|
|
59
|
+
|
|
60
|
+
import type { I18nInfo } from './lib/i18n';
|
|
61
|
+
import type AssetProxy from './valueObjects/AssetProxy';
|
|
62
|
+
import type {
|
|
63
|
+
CmsConfig,
|
|
64
|
+
EntryMap,
|
|
65
|
+
FilterRule,
|
|
66
|
+
EntryDraft,
|
|
67
|
+
Collection,
|
|
68
|
+
Collections,
|
|
69
|
+
CollectionFile,
|
|
70
|
+
State,
|
|
71
|
+
EntryField,
|
|
72
|
+
} from './types/redux';
|
|
73
|
+
import type { EntryValue } from './valueObjects/Entry';
|
|
74
|
+
import type {
|
|
75
|
+
Implementation as BackendImplementation,
|
|
76
|
+
DisplayURL,
|
|
77
|
+
ImplementationEntry,
|
|
78
|
+
Credentials,
|
|
79
|
+
User,
|
|
80
|
+
AsyncLock,
|
|
81
|
+
UnpublishedEntry,
|
|
82
|
+
DataFile,
|
|
83
|
+
UnpublishedEntryDiff,
|
|
84
|
+
} from 'decap-cms-lib-util';
|
|
85
|
+
import type { Map } from 'immutable';
|
|
86
|
+
|
|
87
|
+
const { extractTemplateVars, dateParsers, expandPath } = stringTemplate;
|
|
88
|
+
|
|
89
|
+
function updateAssetProxies(
|
|
90
|
+
assetProxies: AssetProxy[],
|
|
91
|
+
config: CmsConfig,
|
|
92
|
+
collection: Collection,
|
|
93
|
+
entryDraft: EntryDraft,
|
|
94
|
+
path: string,
|
|
95
|
+
) {
|
|
96
|
+
assetProxies.map(asset => {
|
|
97
|
+
// update media files path based on entry path
|
|
98
|
+
const oldPath = asset.path;
|
|
99
|
+
const newPath = selectMediaFilePath(
|
|
100
|
+
config,
|
|
101
|
+
collection,
|
|
102
|
+
entryDraft.get('entry').set('path', path),
|
|
103
|
+
oldPath,
|
|
104
|
+
asset.field,
|
|
105
|
+
);
|
|
106
|
+
asset.path = newPath;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class LocalStorageAuthStore {
|
|
111
|
+
storageKey = 'decap-cms-user';
|
|
112
|
+
|
|
113
|
+
retrieve() {
|
|
114
|
+
const data = window.localStorage.getItem(this.storageKey);
|
|
115
|
+
return data && JSON.parse(data);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
store(userData: unknown) {
|
|
119
|
+
window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logout() {
|
|
123
|
+
window.localStorage.removeItem(this.storageKey);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getEntryBackupKey(collectionName?: string, slug?: string) {
|
|
128
|
+
const baseKey = 'backup';
|
|
129
|
+
if (!collectionName) {
|
|
130
|
+
return baseKey;
|
|
131
|
+
}
|
|
132
|
+
const suffix = slug ? `.${slug}` : '';
|
|
133
|
+
return `${baseKey}.${collectionName}${suffix}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getEntryField(field: string, entry: EntryValue) {
|
|
137
|
+
const value = get(entry.data, field);
|
|
138
|
+
if (value) {
|
|
139
|
+
return String(value);
|
|
140
|
+
} else {
|
|
141
|
+
const firstFieldPart = field.split('.')[0];
|
|
142
|
+
if (entry[firstFieldPart as keyof EntryValue]) {
|
|
143
|
+
// allows searching using entry.slug/entry.path etc.
|
|
144
|
+
return entry[firstFieldPart as keyof EntryValue];
|
|
145
|
+
} else {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function extractSearchFields(searchFields: string[]) {
|
|
152
|
+
return (entry: EntryValue) =>
|
|
153
|
+
searchFields.reduce((acc, field) => {
|
|
154
|
+
const value = getEntryField(field, entry);
|
|
155
|
+
if (value) {
|
|
156
|
+
return `${acc} ${value}`;
|
|
157
|
+
} else {
|
|
158
|
+
return acc;
|
|
159
|
+
}
|
|
160
|
+
}, '');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function expandSearchEntries(entries: EntryValue[], searchFields: string[]) {
|
|
164
|
+
// expand the entries for the purpose of the search
|
|
165
|
+
const expandedEntries = entries.reduce((acc, e) => {
|
|
166
|
+
const expandedFields = searchFields.reduce((acc, f) => {
|
|
167
|
+
const fields = expandPath({ data: e.data, path: f });
|
|
168
|
+
acc.push(...fields);
|
|
169
|
+
return acc;
|
|
170
|
+
}, [] as string[]);
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < expandedFields.length; i++) {
|
|
173
|
+
acc.push({ ...e, field: expandedFields[i] });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return acc;
|
|
177
|
+
}, [] as (EntryValue & { field: string })[]);
|
|
178
|
+
|
|
179
|
+
return expandedEntries;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function mergeExpandedEntries(entries: (EntryValue & { field: string })[]) {
|
|
183
|
+
// merge the search results by slug and only keep data that matched the search
|
|
184
|
+
const fields = entries.map(f => f.field);
|
|
185
|
+
const arrayPaths: Record<string, Set<string>> = {};
|
|
186
|
+
|
|
187
|
+
const merged = entries.reduce((acc, e) => {
|
|
188
|
+
if (!acc[e.slug]) {
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
190
|
+
const { field, ...rest } = e;
|
|
191
|
+
acc[e.slug] = rest;
|
|
192
|
+
arrayPaths[e.slug] = Set();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nestedFields = e.field.split('.');
|
|
196
|
+
let value = acc[e.slug].data;
|
|
197
|
+
for (let i = 0; i < nestedFields.length; i++) {
|
|
198
|
+
value = value[nestedFields[i]];
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
const path = nestedFields.slice(0, i + 1).join('.');
|
|
201
|
+
arrayPaths[e.slug] = arrayPaths[e.slug].add(path);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return acc;
|
|
206
|
+
}, {} as Record<string, EntryValue>);
|
|
207
|
+
|
|
208
|
+
// this keeps the search score sorting order designated by the order in entries
|
|
209
|
+
// and filters non matching items
|
|
210
|
+
Object.keys(merged).forEach(slug => {
|
|
211
|
+
const data = merged[slug].data;
|
|
212
|
+
for (const path of arrayPaths[slug].toArray()) {
|
|
213
|
+
const array = get(data, path) as unknown[];
|
|
214
|
+
const filtered = array.filter((_, index) => {
|
|
215
|
+
return fields.some(f => `${f}.`.startsWith(`${path}.${index}.`));
|
|
216
|
+
});
|
|
217
|
+
filtered.sort((a, b) => {
|
|
218
|
+
const indexOfA = array.indexOf(a);
|
|
219
|
+
const indexOfB = array.indexOf(b);
|
|
220
|
+
const pathOfA = `${path}.${indexOfA}.`;
|
|
221
|
+
const pathOfB = `${path}.${indexOfB}.`;
|
|
222
|
+
|
|
223
|
+
const matchingFieldIndexA = fields.findIndex(f => `${f}.`.startsWith(pathOfA));
|
|
224
|
+
const matchingFieldIndexB = fields.findIndex(f => `${f}.`.startsWith(pathOfB));
|
|
225
|
+
|
|
226
|
+
return matchingFieldIndexA - matchingFieldIndexB;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
set(data, path, filtered);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return Object.values(merged);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sortByScore(a: fuzzy.FilterResult<EntryValue>, b: fuzzy.FilterResult<EntryValue>) {
|
|
237
|
+
if (a.score > b.score) return -1;
|
|
238
|
+
if (a.score < b.score) return 1;
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function slugFromCustomPath(collection: Collection, customPath: string) {
|
|
243
|
+
const folderPath = collection.get('folder', '') as string;
|
|
244
|
+
const entryPath = customPath.toLowerCase().replace(folderPath.toLowerCase(), '');
|
|
245
|
+
const slug = join(dirname(trim(entryPath, '/')), basename(entryPath, extname(customPath)));
|
|
246
|
+
return slug;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface AuthStore {
|
|
250
|
+
retrieve: () => User;
|
|
251
|
+
store: (user: User) => void;
|
|
252
|
+
logout: () => void;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface BackendOptions {
|
|
256
|
+
backendName: string;
|
|
257
|
+
config: CmsConfig;
|
|
258
|
+
authStore?: AuthStore;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface MediaFile {
|
|
262
|
+
name: string;
|
|
263
|
+
id: string;
|
|
264
|
+
size?: number;
|
|
265
|
+
displayURL?: DisplayURL;
|
|
266
|
+
path: string;
|
|
267
|
+
draft?: boolean;
|
|
268
|
+
url?: string;
|
|
269
|
+
file?: File;
|
|
270
|
+
field?: EntryField;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface BackupEntry {
|
|
274
|
+
raw: string;
|
|
275
|
+
path: string;
|
|
276
|
+
mediaFiles: MediaFile[];
|
|
277
|
+
i18n?: Record<string, { raw: string }>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface PersistArgs {
|
|
281
|
+
config: CmsConfig;
|
|
282
|
+
collection: Collection;
|
|
283
|
+
entryDraft: EntryDraft;
|
|
284
|
+
assetProxies: AssetProxy[];
|
|
285
|
+
usedSlugs: List<string>;
|
|
286
|
+
unpublished?: boolean;
|
|
287
|
+
status?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface ImplementationInitOptions {
|
|
291
|
+
useWorkflow: boolean;
|
|
292
|
+
updateUserCredentials: (credentials: Credentials) => void;
|
|
293
|
+
initialWorkflowStatus: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type Implementation = BackendImplementation & {
|
|
297
|
+
init: (config: CmsConfig, options: ImplementationInitOptions) => Implementation;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
function prepareMetaPath(path: string, collection: Collection) {
|
|
301
|
+
if (!selectHasMetaPath(collection)) {
|
|
302
|
+
return path;
|
|
303
|
+
}
|
|
304
|
+
const dir = dirname(path);
|
|
305
|
+
return dir.slice(collection.get('folder')!.length + 1) || '/';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function collectionDepth(collection: Collection) {
|
|
309
|
+
let depth;
|
|
310
|
+
depth =
|
|
311
|
+
collection.get('nested')?.get('depth') || getPathDepth(collection.get('path', '') as string);
|
|
312
|
+
|
|
313
|
+
if (hasI18n(collection)) {
|
|
314
|
+
depth = getI18nFilesDepth(collection, depth);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return depth;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function i18nRulestring(ruleString: string, { defaultLocale, structure }: I18nInfo): string {
|
|
321
|
+
if (structure === I18N_STRUCTURE.MULTIPLE_FOLDERS) {
|
|
322
|
+
return `${defaultLocale}\\/${ruleString}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (structure === I18N_STRUCTURE.MULTIPLE_FILES) {
|
|
326
|
+
return `${ruleString}\\.${defaultLocale}\\..*`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return ruleString;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function collectionRegex(collection: Collection): RegExp | undefined {
|
|
333
|
+
let ruleString = '';
|
|
334
|
+
|
|
335
|
+
if (collection.get('path')) {
|
|
336
|
+
ruleString = `${collection.get('folder')}/${collection.get('path')}`.replace(
|
|
337
|
+
/{{.*}}/gm,
|
|
338
|
+
'(.*)',
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (hasI18n(collection)) {
|
|
343
|
+
ruleString = i18nRulestring(ruleString, getI18nInfo(collection) as I18nInfo);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return ruleString ? new RegExp(ruleString) : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export class Backend {
|
|
350
|
+
implementation: Implementation;
|
|
351
|
+
backendName: string;
|
|
352
|
+
config: CmsConfig;
|
|
353
|
+
authStore?: AuthStore;
|
|
354
|
+
user?: User | null;
|
|
355
|
+
backupSync: AsyncLock;
|
|
356
|
+
|
|
357
|
+
constructor(implementation: Implementation, { backendName, authStore, config }: BackendOptions) {
|
|
358
|
+
// We can't reliably run this on exit, so we do cleanup on load.
|
|
359
|
+
this.deleteAnonymousBackup();
|
|
360
|
+
this.config = config;
|
|
361
|
+
this.implementation = implementation.init(this.config, {
|
|
362
|
+
useWorkflow: selectUseWorkflow(this.config),
|
|
363
|
+
updateUserCredentials: this.updateUserCredentials,
|
|
364
|
+
initialWorkflowStatus: status.first(),
|
|
365
|
+
});
|
|
366
|
+
this.backendName = backendName;
|
|
367
|
+
this.authStore = authStore;
|
|
368
|
+
if (this.implementation === null) {
|
|
369
|
+
throw new Error('Cannot instantiate a Backend with no implementation');
|
|
370
|
+
}
|
|
371
|
+
this.backupSync = asyncLock();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async status() {
|
|
375
|
+
const attempts = 3;
|
|
376
|
+
let status: {
|
|
377
|
+
auth: { status: boolean };
|
|
378
|
+
api: { status: boolean; statusPage: string };
|
|
379
|
+
} = {
|
|
380
|
+
auth: { status: true },
|
|
381
|
+
api: { status: true, statusPage: '' },
|
|
382
|
+
};
|
|
383
|
+
for (let i = 1; i <= attempts; i++) {
|
|
384
|
+
status = await this.implementation.status();
|
|
385
|
+
// return on first success
|
|
386
|
+
if (Object.values(status).every(s => s.status === true)) {
|
|
387
|
+
return status;
|
|
388
|
+
} else {
|
|
389
|
+
await new Promise(resolve => setTimeout(resolve, i * 1000));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return status;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
currentUser() {
|
|
396
|
+
if (this.user) {
|
|
397
|
+
return this.user;
|
|
398
|
+
}
|
|
399
|
+
const stored = this.authStore!.retrieve();
|
|
400
|
+
if (stored && stored.backendName === this.backendName) {
|
|
401
|
+
return Promise.resolve(this.implementation.restoreUser(stored)).then(user => {
|
|
402
|
+
this.user = { ...user, backendName: this.backendName };
|
|
403
|
+
// return confirmed/rehydrated user object instead of stored
|
|
404
|
+
this.authStore!.store(this.user as User);
|
|
405
|
+
return this.user;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return Promise.resolve(null);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
isGitBackend() {
|
|
412
|
+
return this.implementation.isGitBackend?.() || false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
updateUserCredentials = (updatedCredentials: Credentials) => {
|
|
416
|
+
const storedUser = this.authStore!.retrieve();
|
|
417
|
+
if (storedUser && storedUser.backendName === this.backendName) {
|
|
418
|
+
this.user = { ...storedUser, ...updatedCredentials };
|
|
419
|
+
this.authStore!.store(this.user as User);
|
|
420
|
+
return this.user;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
authComponent() {
|
|
425
|
+
return this.implementation.authComponent();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
authenticate(credentials: Credentials) {
|
|
429
|
+
return this.implementation.authenticate(credentials).then(user => {
|
|
430
|
+
this.user = { ...user, backendName: this.backendName };
|
|
431
|
+
if (this.authStore) {
|
|
432
|
+
this.authStore.store(this.user as User);
|
|
433
|
+
}
|
|
434
|
+
return this.user;
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async logout() {
|
|
439
|
+
try {
|
|
440
|
+
await this.implementation.logout();
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.warn('Error during logout', e.message);
|
|
443
|
+
} finally {
|
|
444
|
+
this.user = null;
|
|
445
|
+
if (this.authStore) {
|
|
446
|
+
this.authStore.logout();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
getToken = () => this.implementation.getToken();
|
|
452
|
+
|
|
453
|
+
async entryExist(collection: Collection, path: string, slug: string, useWorkflow: boolean) {
|
|
454
|
+
const unpublishedEntry =
|
|
455
|
+
useWorkflow &&
|
|
456
|
+
(await this.implementation
|
|
457
|
+
.unpublishedEntry({ collection: collection.get('name'), slug })
|
|
458
|
+
.catch(error => {
|
|
459
|
+
if (error.name === EDITORIAL_WORKFLOW_ERROR && error.notUnderEditorialWorkflow) {
|
|
460
|
+
return Promise.resolve(false);
|
|
461
|
+
}
|
|
462
|
+
return Promise.reject(error);
|
|
463
|
+
}));
|
|
464
|
+
|
|
465
|
+
if (unpublishedEntry) return unpublishedEntry;
|
|
466
|
+
|
|
467
|
+
const publishedEntry = await this.implementation
|
|
468
|
+
.getEntry(path)
|
|
469
|
+
.then(({ data }) => data)
|
|
470
|
+
.catch(() => {
|
|
471
|
+
return Promise.resolve(false);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
return publishedEntry;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async generateUniqueSlug(
|
|
478
|
+
collection: Collection,
|
|
479
|
+
entryData: Map<string, unknown>,
|
|
480
|
+
config: CmsConfig,
|
|
481
|
+
usedSlugs: List<string>,
|
|
482
|
+
customPath: string | undefined,
|
|
483
|
+
) {
|
|
484
|
+
const slugConfig = config.slug;
|
|
485
|
+
let slug: string;
|
|
486
|
+
if (customPath) {
|
|
487
|
+
slug = slugFromCustomPath(collection, customPath);
|
|
488
|
+
} else {
|
|
489
|
+
slug = slugFormatter(collection, entryData, slugConfig);
|
|
490
|
+
}
|
|
491
|
+
let i = 1;
|
|
492
|
+
let uniqueSlug = slug;
|
|
493
|
+
|
|
494
|
+
// Check for duplicate slug in loaded entities store first before repo
|
|
495
|
+
while (
|
|
496
|
+
usedSlugs.includes(uniqueSlug) ||
|
|
497
|
+
(await this.entryExist(
|
|
498
|
+
collection,
|
|
499
|
+
selectEntryPath(collection, uniqueSlug) as string,
|
|
500
|
+
uniqueSlug,
|
|
501
|
+
selectUseWorkflow(config),
|
|
502
|
+
))
|
|
503
|
+
) {
|
|
504
|
+
uniqueSlug = `${slug}${sanitizeChar(' ', slugConfig)}${i++}`;
|
|
505
|
+
}
|
|
506
|
+
return uniqueSlug;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
processEntries(loadedEntries: ImplementationEntry[], collection: Collection) {
|
|
510
|
+
const entries = loadedEntries.map(loadedEntry =>
|
|
511
|
+
createEntry(
|
|
512
|
+
collection.get('name'),
|
|
513
|
+
selectEntrySlug(collection, loadedEntry.file.path),
|
|
514
|
+
loadedEntry.file.path,
|
|
515
|
+
{
|
|
516
|
+
raw: loadedEntry.data || '',
|
|
517
|
+
label: loadedEntry.file.label,
|
|
518
|
+
author: loadedEntry.file.author,
|
|
519
|
+
updatedOn: loadedEntry.file.updatedOn,
|
|
520
|
+
meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
|
|
521
|
+
},
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
const formattedEntries = entries.map(this.entryWithFormat(collection));
|
|
525
|
+
// If this collection has a "filter" property, filter entries accordingly
|
|
526
|
+
const collectionFilter = collection.get('filter');
|
|
527
|
+
const filteredEntries = collectionFilter
|
|
528
|
+
? this.filterEntries({ entries: formattedEntries }, collectionFilter)
|
|
529
|
+
: formattedEntries;
|
|
530
|
+
|
|
531
|
+
if (hasI18n(collection)) {
|
|
532
|
+
const extension = selectFolderEntryExtension(collection);
|
|
533
|
+
const groupedEntries = groupEntries(collection, extension, filteredEntries);
|
|
534
|
+
return groupedEntries;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return filteredEntries;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async listEntries(collection: Collection) {
|
|
541
|
+
const extension = selectFolderEntryExtension(collection);
|
|
542
|
+
let listMethod: () => Promise<ImplementationEntry[]>;
|
|
543
|
+
const collectionType = collection.get('type');
|
|
544
|
+
if (collectionType === FOLDER) {
|
|
545
|
+
listMethod = () => {
|
|
546
|
+
const depth = collectionDepth(collection);
|
|
547
|
+
return this.implementation.entriesByFolder(
|
|
548
|
+
collection.get('folder') as string,
|
|
549
|
+
extension,
|
|
550
|
+
depth,
|
|
551
|
+
);
|
|
552
|
+
};
|
|
553
|
+
} else if (collectionType === FILES) {
|
|
554
|
+
const files = collection
|
|
555
|
+
.get('files')!
|
|
556
|
+
.map(collectionFile => ({
|
|
557
|
+
path: collectionFile!.get('file'),
|
|
558
|
+
label: collectionFile!.get('label'),
|
|
559
|
+
}))
|
|
560
|
+
.toArray();
|
|
561
|
+
listMethod = () => this.implementation.entriesByFiles(files);
|
|
562
|
+
} else {
|
|
563
|
+
throw new Error(`Unknown collection type: ${collectionType}`);
|
|
564
|
+
}
|
|
565
|
+
const loadedEntries = await listMethod();
|
|
566
|
+
/*
|
|
567
|
+
Wrap cursors so we can tell which collection the cursor is
|
|
568
|
+
from. This is done to prevent traverseCursor from requiring a
|
|
569
|
+
`collection` argument.
|
|
570
|
+
*/
|
|
571
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
572
|
+
// @ts-ignore
|
|
573
|
+
const cursor = Cursor.create(loadedEntries[CURSOR_COMPATIBILITY_SYMBOL]).wrapData({
|
|
574
|
+
cursorType: 'collectionEntries',
|
|
575
|
+
collection,
|
|
576
|
+
});
|
|
577
|
+
return {
|
|
578
|
+
entries: this.processEntries(loadedEntries, collection),
|
|
579
|
+
pagination: cursor.meta?.get('page'),
|
|
580
|
+
cursor,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// The same as listEntries, except that if a cursor with the "next"
|
|
585
|
+
// action available is returned, it calls "next" on the cursor and
|
|
586
|
+
// repeats the process. Once there is no available "next" action, it
|
|
587
|
+
// returns all the collected entries. Used to retrieve all entries
|
|
588
|
+
// for local searches and queries.
|
|
589
|
+
async listAllEntries(collection: Collection) {
|
|
590
|
+
if (collection.get('folder') && this.implementation.allEntriesByFolder) {
|
|
591
|
+
const depth = collectionDepth(collection);
|
|
592
|
+
const extension = selectFolderEntryExtension(collection);
|
|
593
|
+
return this.implementation
|
|
594
|
+
.allEntriesByFolder(
|
|
595
|
+
collection.get('folder') as string,
|
|
596
|
+
extension,
|
|
597
|
+
depth,
|
|
598
|
+
collectionRegex(collection),
|
|
599
|
+
)
|
|
600
|
+
.then(entries => this.processEntries(entries, collection));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const response = await this.listEntries(collection);
|
|
604
|
+
const { entries } = response;
|
|
605
|
+
let { cursor } = response;
|
|
606
|
+
while (cursor && cursor.actions!.includes('next')) {
|
|
607
|
+
const { entries: newEntries, cursor: newCursor } = await this.traverseCursor(cursor, 'next');
|
|
608
|
+
entries.push(...newEntries);
|
|
609
|
+
cursor = newCursor;
|
|
610
|
+
}
|
|
611
|
+
return entries;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async search(collections: Collection[], searchTerm: string) {
|
|
615
|
+
// Perform a local search by requesting all entries. For each
|
|
616
|
+
// collection, load it, search, and call onCollectionResults with
|
|
617
|
+
// its results.
|
|
618
|
+
const errors: Error[] = [];
|
|
619
|
+
const collectionEntriesRequests = collections
|
|
620
|
+
.map(async collection => {
|
|
621
|
+
const summary = collection.get('summary', '') as string;
|
|
622
|
+
const summaryFields = extractTemplateVars(summary);
|
|
623
|
+
|
|
624
|
+
// TODO: pass search fields in as an argument
|
|
625
|
+
let searchFields: (string | null | undefined)[] = [];
|
|
626
|
+
|
|
627
|
+
if (collection.get('type') === FILES) {
|
|
628
|
+
collection.get('files')?.forEach(f => {
|
|
629
|
+
const topLevelFields = f!
|
|
630
|
+
.get('fields')
|
|
631
|
+
.map(f => f!.get('name'))
|
|
632
|
+
.toArray();
|
|
633
|
+
searchFields = [...searchFields, ...topLevelFields];
|
|
634
|
+
});
|
|
635
|
+
} else {
|
|
636
|
+
searchFields = [
|
|
637
|
+
selectInferredField(collection, 'title'),
|
|
638
|
+
selectInferredField(collection, 'shortTitle'),
|
|
639
|
+
selectInferredField(collection, 'author'),
|
|
640
|
+
...summaryFields.map(elem => {
|
|
641
|
+
if (dateParsers[elem]) {
|
|
642
|
+
return selectInferredField(collection, 'date');
|
|
643
|
+
}
|
|
644
|
+
return elem;
|
|
645
|
+
}),
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
const filteredSearchFields = searchFields.filter(Boolean) as string[];
|
|
649
|
+
const collectionEntries = await this.listAllEntries(collection);
|
|
650
|
+
return fuzzy.filter(searchTerm, collectionEntries, {
|
|
651
|
+
extract: extractSearchFields(uniq(filteredSearchFields)),
|
|
652
|
+
});
|
|
653
|
+
})
|
|
654
|
+
.map(p =>
|
|
655
|
+
p.catch(err => {
|
|
656
|
+
errors.push(err);
|
|
657
|
+
return [] as fuzzy.FilterResult<EntryValue>[];
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const entries = await Promise.all(collectionEntriesRequests).then(arrays => flatten(arrays));
|
|
662
|
+
|
|
663
|
+
if (errors.length > 0) {
|
|
664
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
665
|
+
// @ts-ignore
|
|
666
|
+
throw new Error({ message: 'Errors occurred while searching entries locally!', errors });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const hits = entries
|
|
670
|
+
.filter(({ score }: fuzzy.FilterResult<EntryValue>) => score > 5)
|
|
671
|
+
.sort(sortByScore)
|
|
672
|
+
.map((f: fuzzy.FilterResult<EntryValue>) => f.original);
|
|
673
|
+
return { entries: hits };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async query(
|
|
677
|
+
collection: Collection,
|
|
678
|
+
searchFields: string[],
|
|
679
|
+
searchTerm: string,
|
|
680
|
+
file?: string,
|
|
681
|
+
limit?: number,
|
|
682
|
+
) {
|
|
683
|
+
let entries = await this.listAllEntries(collection);
|
|
684
|
+
if (file) {
|
|
685
|
+
entries = entries.filter(e => e.slug === file);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const expandedEntries = expandSearchEntries(entries, searchFields);
|
|
689
|
+
|
|
690
|
+
let hits = fuzzy
|
|
691
|
+
.filter(searchTerm, expandedEntries, {
|
|
692
|
+
extract: entry => {
|
|
693
|
+
return getEntryField(entry.field, entry);
|
|
694
|
+
},
|
|
695
|
+
})
|
|
696
|
+
.sort(sortByScore)
|
|
697
|
+
.map(f => f.original);
|
|
698
|
+
|
|
699
|
+
if (limit !== undefined && limit > 0) {
|
|
700
|
+
hits = hits.slice(0, limit);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const merged = mergeExpandedEntries(hits);
|
|
704
|
+
return { query: searchTerm, hits: merged };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
traverseCursor(cursor: Cursor, action: string) {
|
|
708
|
+
const [data, unwrappedCursor] = cursor.unwrapData();
|
|
709
|
+
// TODO: stop assuming all cursors are for collections
|
|
710
|
+
const collection = data.get('collection') as Collection;
|
|
711
|
+
return this.implementation!.traverseCursor!(unwrappedCursor, action).then(
|
|
712
|
+
async ({ entries, cursor: newCursor }) => ({
|
|
713
|
+
entries: this.processEntries(entries, collection),
|
|
714
|
+
cursor: Cursor.create(newCursor).wrapData({
|
|
715
|
+
cursorType: 'collectionEntries',
|
|
716
|
+
collection,
|
|
717
|
+
}),
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async getLocalDraftBackup(collection: Collection, slug: string) {
|
|
723
|
+
const key = getEntryBackupKey(collection.get('name'), slug);
|
|
724
|
+
const backup = await localForage.getItem<BackupEntry>(key);
|
|
725
|
+
if (!backup || !backup.raw.trim()) {
|
|
726
|
+
return {};
|
|
727
|
+
}
|
|
728
|
+
const { raw, path } = backup;
|
|
729
|
+
let { mediaFiles = [] } = backup;
|
|
730
|
+
|
|
731
|
+
mediaFiles = mediaFiles.map(file => {
|
|
732
|
+
// de-serialize the file object
|
|
733
|
+
if (file.file) {
|
|
734
|
+
return { ...file, url: URL.createObjectURL(file.file) };
|
|
735
|
+
}
|
|
736
|
+
return file;
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const label = selectFileEntryLabel(collection, slug);
|
|
740
|
+
|
|
741
|
+
const formatRawData = (raw: string) => {
|
|
742
|
+
return this.entryWithFormat(collection)(
|
|
743
|
+
createEntry(collection.get('name'), slug, path, {
|
|
744
|
+
raw,
|
|
745
|
+
label,
|
|
746
|
+
mediaFiles,
|
|
747
|
+
meta: { path: prepareMetaPath(path, collection) },
|
|
748
|
+
}),
|
|
749
|
+
);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const entry: EntryValue = formatRawData(raw);
|
|
753
|
+
if (hasI18n(collection) && backup.i18n) {
|
|
754
|
+
const i18n = formatI18nBackup(backup.i18n, formatRawData);
|
|
755
|
+
entry.i18n = i18n;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return { entry };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async persistLocalDraftBackup(entry: EntryMap, collection: Collection) {
|
|
762
|
+
try {
|
|
763
|
+
await this.backupSync.acquire();
|
|
764
|
+
const key = getEntryBackupKey(collection.get('name'), entry.get('slug'));
|
|
765
|
+
const raw = this.entryToRaw(collection, entry);
|
|
766
|
+
|
|
767
|
+
if (!raw.trim()) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const mediaFiles = await Promise.all<MediaFile>(
|
|
772
|
+
entry
|
|
773
|
+
.get('mediaFiles')
|
|
774
|
+
.toJS()
|
|
775
|
+
.map(async (file: MediaFile) => {
|
|
776
|
+
// make sure to serialize the file
|
|
777
|
+
if (file.url?.startsWith('blob:')) {
|
|
778
|
+
const blob = await fetch(file.url as string).then(res => res.blob());
|
|
779
|
+
return { ...file, file: blobToFileObj(file.name, blob) };
|
|
780
|
+
}
|
|
781
|
+
return file;
|
|
782
|
+
}),
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
let i18n;
|
|
786
|
+
if (hasI18n(collection)) {
|
|
787
|
+
i18n = getI18nBackup(collection, entry, entry => this.entryToRaw(collection, entry));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
await localForage.setItem<BackupEntry>(key, {
|
|
791
|
+
raw,
|
|
792
|
+
path: entry.get('path'),
|
|
793
|
+
mediaFiles,
|
|
794
|
+
...(i18n && { i18n }),
|
|
795
|
+
});
|
|
796
|
+
const result = await localForage.setItem(getEntryBackupKey(), raw);
|
|
797
|
+
return result;
|
|
798
|
+
} catch (e) {
|
|
799
|
+
console.warn('persistLocalDraftBackup', e);
|
|
800
|
+
} finally {
|
|
801
|
+
this.backupSync.release();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async deleteLocalDraftBackup(collection: Collection, slug: string) {
|
|
806
|
+
try {
|
|
807
|
+
await this.backupSync.acquire();
|
|
808
|
+
await localForage.removeItem(getEntryBackupKey(collection.get('name'), slug));
|
|
809
|
+
// delete new entry backup if not deleted
|
|
810
|
+
slug && (await localForage.removeItem(getEntryBackupKey(collection.get('name'))));
|
|
811
|
+
const result = await this.deleteAnonymousBackup();
|
|
812
|
+
return result;
|
|
813
|
+
} catch (e) {
|
|
814
|
+
console.warn('deleteLocalDraftBackup', e);
|
|
815
|
+
} finally {
|
|
816
|
+
this.backupSync.release();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Unnamed backup for use in the global error boundary, should always be
|
|
821
|
+
// deleted on cms load.
|
|
822
|
+
deleteAnonymousBackup() {
|
|
823
|
+
return localForage.removeItem(getEntryBackupKey());
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async getEntry(state: State, collection: Collection, slug: string) {
|
|
827
|
+
const path = selectEntryPath(collection, slug) as string;
|
|
828
|
+
const label = selectFileEntryLabel(collection, slug);
|
|
829
|
+
const extension = selectFolderEntryExtension(collection);
|
|
830
|
+
|
|
831
|
+
const getEntryValue = async (path: string) => {
|
|
832
|
+
const loadedEntry = await this.implementation.getEntry(path);
|
|
833
|
+
let entry = createEntry(collection.get('name'), slug, loadedEntry.file.path, {
|
|
834
|
+
raw: loadedEntry.data,
|
|
835
|
+
label,
|
|
836
|
+
mediaFiles: [],
|
|
837
|
+
meta: { path: prepareMetaPath(loadedEntry.file.path, collection) },
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
entry = this.entryWithFormat(collection)(entry);
|
|
841
|
+
entry = await this.processEntry(state, collection, entry);
|
|
842
|
+
|
|
843
|
+
return entry;
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
let entryValue: EntryValue;
|
|
847
|
+
if (hasI18n(collection)) {
|
|
848
|
+
entryValue = await getI18nEntry(collection, extension, path, slug, getEntryValue);
|
|
849
|
+
} else {
|
|
850
|
+
entryValue = await getEntryValue(path);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return entryValue;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
getMedia() {
|
|
857
|
+
return this.implementation.getMedia();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
getMediaFile(path: string) {
|
|
861
|
+
return this.implementation.getMediaFile(path);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
getMediaDisplayURL(displayURL: DisplayURL) {
|
|
865
|
+
if (this.implementation.getMediaDisplayURL) {
|
|
866
|
+
return this.implementation.getMediaDisplayURL(displayURL);
|
|
867
|
+
}
|
|
868
|
+
const err = new Error(
|
|
869
|
+
'getMediaDisplayURL is not implemented by the current backend, but the backend returned a displayURL which was not a string!',
|
|
870
|
+
) as Error & { displayURL: DisplayURL };
|
|
871
|
+
err.displayURL = displayURL;
|
|
872
|
+
return Promise.reject(err);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
entryWithFormat(collection: Collection) {
|
|
876
|
+
return (entry: EntryValue): EntryValue => {
|
|
877
|
+
const format = resolveFormat(collection, entry);
|
|
878
|
+
if (entry && entry.raw !== undefined) {
|
|
879
|
+
const data = (format && attempt(format.fromFile.bind(format, entry.raw))) || {};
|
|
880
|
+
if (isError(data)) console.error(data);
|
|
881
|
+
return Object.assign(entry, { data: isError(data) ? {} : data });
|
|
882
|
+
}
|
|
883
|
+
return format.fromFile(entry);
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async processUnpublishedEntry(
|
|
888
|
+
collection: Collection,
|
|
889
|
+
entryData: UnpublishedEntry,
|
|
890
|
+
withMediaFiles: boolean,
|
|
891
|
+
) {
|
|
892
|
+
const { slug } = entryData;
|
|
893
|
+
let extension: string;
|
|
894
|
+
if (collection.get('type') === FILES) {
|
|
895
|
+
const file = collection.get('files')!.find(f => f?.get('name') === slug);
|
|
896
|
+
extension = extname(file.get('file'));
|
|
897
|
+
} else {
|
|
898
|
+
extension = selectFolderEntryExtension(collection);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const mediaFiles: MediaFile[] = [];
|
|
902
|
+
if (withMediaFiles) {
|
|
903
|
+
const nonDataFiles = entryData.diffs.filter(d => !d.path.endsWith(extension));
|
|
904
|
+
const files = await Promise.all(
|
|
905
|
+
nonDataFiles.map(f =>
|
|
906
|
+
this.implementation!.unpublishedEntryMediaFile(
|
|
907
|
+
collection.get('name'),
|
|
908
|
+
slug,
|
|
909
|
+
f.path,
|
|
910
|
+
f.id,
|
|
911
|
+
),
|
|
912
|
+
),
|
|
913
|
+
);
|
|
914
|
+
mediaFiles.push(...files.map(f => ({ ...f, draft: true })));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const dataFiles = sortBy(
|
|
918
|
+
entryData.diffs.filter(d => d.path.endsWith(extension)),
|
|
919
|
+
f => f.path.length,
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const formatData = (data: string, path: string, newFile: boolean) => {
|
|
923
|
+
const entry = createEntry(collection.get('name'), slug, path, {
|
|
924
|
+
raw: data,
|
|
925
|
+
isModification: !newFile,
|
|
926
|
+
label: collection && selectFileEntryLabel(collection, slug),
|
|
927
|
+
mediaFiles,
|
|
928
|
+
updatedOn: entryData.updatedAt,
|
|
929
|
+
author: entryData.pullRequestAuthor,
|
|
930
|
+
status: entryData.status,
|
|
931
|
+
meta: { path: prepareMetaPath(path, collection) },
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
const entryWithFormat = this.entryWithFormat(collection)(entry);
|
|
935
|
+
return entryWithFormat;
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const readAndFormatDataFile = async (dataFile: UnpublishedEntryDiff) => {
|
|
939
|
+
const data = await this.implementation.unpublishedEntryDataFile(
|
|
940
|
+
collection.get('name'),
|
|
941
|
+
entryData.slug,
|
|
942
|
+
dataFile.path,
|
|
943
|
+
dataFile.id,
|
|
944
|
+
);
|
|
945
|
+
const entryWithFormat = formatData(data, dataFile.path, dataFile.newFile);
|
|
946
|
+
return entryWithFormat;
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// if the unpublished entry has no diffs, return the original
|
|
950
|
+
if (dataFiles.length <= 0) {
|
|
951
|
+
const loadedEntry = await this.implementation.getEntry(
|
|
952
|
+
selectEntryPath(collection, slug) as string,
|
|
953
|
+
);
|
|
954
|
+
return formatData(loadedEntry.data, loadedEntry.file.path, false);
|
|
955
|
+
} else if (hasI18n(collection)) {
|
|
956
|
+
// we need to read all locales files and not just the changes
|
|
957
|
+
const path = selectEntryPath(collection, slug) as string;
|
|
958
|
+
const i18nFiles = getI18nDataFiles(collection, extension, path, slug, dataFiles);
|
|
959
|
+
let entries = await Promise.all(
|
|
960
|
+
i18nFiles.map(dataFile => readAndFormatDataFile(dataFile).catch(() => null)),
|
|
961
|
+
);
|
|
962
|
+
entries = entries.filter(Boolean);
|
|
963
|
+
const grouped = await groupEntries(collection, extension, entries as EntryValue[]);
|
|
964
|
+
return grouped[0];
|
|
965
|
+
} else {
|
|
966
|
+
const entryWithFormat = await readAndFormatDataFile(dataFiles[0]);
|
|
967
|
+
return entryWithFormat;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async unpublishedEntries(collections: Collections) {
|
|
972
|
+
const ids = await this.implementation.unpublishedEntries!();
|
|
973
|
+
const entries = (
|
|
974
|
+
await Promise.all(
|
|
975
|
+
ids.map(async id => {
|
|
976
|
+
const entryData = await this.implementation.unpublishedEntry({ id });
|
|
977
|
+
const collectionName = entryData.collection;
|
|
978
|
+
const collection = collections.find(c => c.get('name') === collectionName);
|
|
979
|
+
if (!collection) {
|
|
980
|
+
console.warn(`Missing collection '${collectionName}' for unpublished entry '${id}'`);
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
const entry = await this.processUnpublishedEntry(collection, entryData, false);
|
|
984
|
+
return entry;
|
|
985
|
+
}),
|
|
986
|
+
)
|
|
987
|
+
).filter(Boolean) as EntryValue[];
|
|
988
|
+
|
|
989
|
+
return { pagination: 0, entries };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async processEntry(state: State, collection: Collection, entry: EntryValue) {
|
|
993
|
+
const integration = selectIntegration(state.integrations, null, 'assetStore');
|
|
994
|
+
const mediaFolders = selectMediaFolders(state.config, collection, fromJS(entry));
|
|
995
|
+
if (mediaFolders.length > 0 && !integration) {
|
|
996
|
+
const files = await Promise.all(
|
|
997
|
+
mediaFolders.map(folder => this.implementation.getMedia(folder)),
|
|
998
|
+
);
|
|
999
|
+
entry.mediaFiles = entry.mediaFiles.concat(...files);
|
|
1000
|
+
} else {
|
|
1001
|
+
entry.mediaFiles = entry.mediaFiles.concat(state.mediaLibrary.get('files') || []);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return entry;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async unpublishedEntry(state: State, collection: Collection, slug: string) {
|
|
1008
|
+
const entryData = await this.implementation!.unpublishedEntry!({
|
|
1009
|
+
collection: collection.get('name') as string,
|
|
1010
|
+
slug,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
let entry = await this.processUnpublishedEntry(collection, entryData, true);
|
|
1014
|
+
entry = await this.processEntry(state, collection, entry);
|
|
1015
|
+
return entry;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Creates a URL using `site_url` from the config and `preview_path` from the
|
|
1020
|
+
* entry's collection. Does not currently make a request through the backend,
|
|
1021
|
+
* but likely will in the future.
|
|
1022
|
+
*/
|
|
1023
|
+
getDeploy(collection: Collection, slug: string, entry: EntryMap) {
|
|
1024
|
+
/**
|
|
1025
|
+
* If `site_url` is undefined or `show_preview_links` in the config is set to false, do nothing.
|
|
1026
|
+
*/
|
|
1027
|
+
|
|
1028
|
+
const baseUrl = this.config.site_url;
|
|
1029
|
+
|
|
1030
|
+
if (!baseUrl || this.config.show_preview_links === false) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return {
|
|
1035
|
+
url: previewUrlFormatter(baseUrl, collection, slug, entry, this.config.slug),
|
|
1036
|
+
status: 'SUCCESS',
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Requests a base URL from the backend for previewing a specific entry.
|
|
1042
|
+
* Supports polling via `maxAttempts` and `interval` options, as there is
|
|
1043
|
+
* often a delay before a preview URL is available.
|
|
1044
|
+
*/
|
|
1045
|
+
async getDeployPreview(
|
|
1046
|
+
collection: Collection,
|
|
1047
|
+
slug: string,
|
|
1048
|
+
entry: EntryMap,
|
|
1049
|
+
{ maxAttempts = 1, interval = 5000 } = {},
|
|
1050
|
+
) {
|
|
1051
|
+
/**
|
|
1052
|
+
* If the registered backend does not provide a `getDeployPreview` method, or
|
|
1053
|
+
* `show_preview_links` in the config is set to false, do nothing.
|
|
1054
|
+
*/
|
|
1055
|
+
if (!this.implementation.getDeployPreview || this.config.show_preview_links === false) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Poll for the deploy preview URL (defaults to 1 attempt, so no polling by
|
|
1061
|
+
* default).
|
|
1062
|
+
*/
|
|
1063
|
+
let deployPreview,
|
|
1064
|
+
count = 0;
|
|
1065
|
+
while (!deployPreview && count < maxAttempts) {
|
|
1066
|
+
count++;
|
|
1067
|
+
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
|
|
1068
|
+
if (!deployPreview) {
|
|
1069
|
+
await new Promise(resolve => setTimeout(() => resolve(undefined), interval));
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* If there's no deploy preview, do nothing.
|
|
1075
|
+
*/
|
|
1076
|
+
if (!deployPreview) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return {
|
|
1081
|
+
/**
|
|
1082
|
+
* Create a URL using the collection `preview_path`, if provided.
|
|
1083
|
+
*/
|
|
1084
|
+
url: previewUrlFormatter(deployPreview.url, collection, slug, entry, this.config.slug),
|
|
1085
|
+
/**
|
|
1086
|
+
* Always capitalize the status for consistency.
|
|
1087
|
+
*/
|
|
1088
|
+
status: deployPreview.status ? deployPreview.status.toUpperCase() : '',
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async persistEntry({
|
|
1093
|
+
config,
|
|
1094
|
+
collection,
|
|
1095
|
+
entryDraft: draft,
|
|
1096
|
+
assetProxies,
|
|
1097
|
+
usedSlugs,
|
|
1098
|
+
unpublished = false,
|
|
1099
|
+
status,
|
|
1100
|
+
}: PersistArgs) {
|
|
1101
|
+
const updatedEntity = await this.invokePreSaveEvent(draft.get('entry'));
|
|
1102
|
+
|
|
1103
|
+
let entryDraft;
|
|
1104
|
+
if (updatedEntity.get('data') === undefined) {
|
|
1105
|
+
entryDraft = (updatedEntity && draft.setIn(['entry', 'data'], updatedEntity)) || draft;
|
|
1106
|
+
} else {
|
|
1107
|
+
entryDraft = (updatedEntity && draft.setIn(['entry'], updatedEntity)) || draft;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
|
1111
|
+
|
|
1112
|
+
const useWorkflow = selectUseWorkflow(config);
|
|
1113
|
+
|
|
1114
|
+
const customPath = selectCustomPath(collection, entryDraft);
|
|
1115
|
+
|
|
1116
|
+
let dataFile: DataFile;
|
|
1117
|
+
if (newEntry) {
|
|
1118
|
+
if (!selectAllowNewEntries(collection)) {
|
|
1119
|
+
throw new Error('Not allowed to create new entries in this collection');
|
|
1120
|
+
}
|
|
1121
|
+
const slug = await this.generateUniqueSlug(
|
|
1122
|
+
collection,
|
|
1123
|
+
entryDraft.getIn(['entry', 'data']),
|
|
1124
|
+
config,
|
|
1125
|
+
usedSlugs,
|
|
1126
|
+
customPath,
|
|
1127
|
+
);
|
|
1128
|
+
const path = customPath || (selectEntryPath(collection, slug) as string);
|
|
1129
|
+
dataFile = {
|
|
1130
|
+
path,
|
|
1131
|
+
slug,
|
|
1132
|
+
raw: this.entryToRaw(collection, entryDraft.get('entry')),
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
updateAssetProxies(assetProxies, config, collection, entryDraft, path);
|
|
1136
|
+
} else {
|
|
1137
|
+
const slug = entryDraft.getIn(['entry', 'slug']);
|
|
1138
|
+
const path = entryDraft.getIn(['entry', 'path']);
|
|
1139
|
+
dataFile = {
|
|
1140
|
+
path,
|
|
1141
|
+
// for workflow entries we refresh the slug on publish
|
|
1142
|
+
slug: customPath && !useWorkflow ? slugFromCustomPath(collection, customPath) : slug,
|
|
1143
|
+
raw: this.entryToRaw(collection, entryDraft.get('entry')),
|
|
1144
|
+
newPath: customPath === path ? undefined : customPath,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const { slug, path, newPath } = dataFile;
|
|
1149
|
+
|
|
1150
|
+
let dataFiles = [dataFile];
|
|
1151
|
+
if (hasI18n(collection)) {
|
|
1152
|
+
const extension = selectFolderEntryExtension(collection);
|
|
1153
|
+
dataFiles = getI18nFiles(
|
|
1154
|
+
collection,
|
|
1155
|
+
extension,
|
|
1156
|
+
entryDraft.get('entry'),
|
|
1157
|
+
(draftData: EntryMap) => this.entryToRaw(collection, draftData),
|
|
1158
|
+
path,
|
|
1159
|
+
slug,
|
|
1160
|
+
newPath,
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const user = (await this.currentUser()) as User;
|
|
1165
|
+
const commitMessage = commitMessageFormatter(
|
|
1166
|
+
newEntry ? 'create' : 'update',
|
|
1167
|
+
config,
|
|
1168
|
+
{
|
|
1169
|
+
collection,
|
|
1170
|
+
slug,
|
|
1171
|
+
path,
|
|
1172
|
+
authorLogin: user.login,
|
|
1173
|
+
authorName: user.name,
|
|
1174
|
+
},
|
|
1175
|
+
user.useOpenAuthoring,
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
const collectionName = collection.get('name');
|
|
1179
|
+
const hasSubfolders = collection.get('nested')?.get('subfolders') !== false;
|
|
1180
|
+
|
|
1181
|
+
const updatedOptions = { unpublished, status };
|
|
1182
|
+
const opts = {
|
|
1183
|
+
newEntry,
|
|
1184
|
+
commitMessage,
|
|
1185
|
+
collectionName,
|
|
1186
|
+
useWorkflow,
|
|
1187
|
+
hasSubfolders,
|
|
1188
|
+
...updatedOptions,
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
if (!useWorkflow) {
|
|
1192
|
+
await this.invokePrePublishEvent(entryDraft.get('entry'));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
await this.implementation.persistEntry(
|
|
1196
|
+
{
|
|
1197
|
+
dataFiles,
|
|
1198
|
+
assets: assetProxies,
|
|
1199
|
+
},
|
|
1200
|
+
opts,
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
await this.invokePostSaveEvent(entryDraft.get('entry'));
|
|
1204
|
+
|
|
1205
|
+
if (!useWorkflow) {
|
|
1206
|
+
await this.invokePostPublishEvent(entryDraft.get('entry'));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return slug;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
async invokeEventWithEntry(event: string, entry: EntryMap) {
|
|
1213
|
+
const { login, name } = (await this.currentUser()) as User;
|
|
1214
|
+
return await invokeEvent({ name: event, data: { entry, author: { login, name } } });
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async invokePrePublishEvent(entry: EntryMap) {
|
|
1218
|
+
await this.invokeEventWithEntry('prePublish', entry);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async invokePostPublishEvent(entry: EntryMap) {
|
|
1222
|
+
await this.invokeEventWithEntry('postPublish', entry);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async invokePreUnpublishEvent(entry: EntryMap) {
|
|
1226
|
+
await this.invokeEventWithEntry('preUnpublish', entry);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
async invokePostUnpublishEvent(entry: EntryMap) {
|
|
1230
|
+
await this.invokeEventWithEntry('postUnpublish', entry);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
async invokePreSaveEvent(entry: EntryMap) {
|
|
1234
|
+
return await this.invokeEventWithEntry('preSave', entry);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async invokePostSaveEvent(entry: EntryMap) {
|
|
1238
|
+
await this.invokeEventWithEntry('postSave', entry);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async persistMedia(config: CmsConfig, file: AssetProxy) {
|
|
1242
|
+
const user = (await this.currentUser()) as User;
|
|
1243
|
+
const options = {
|
|
1244
|
+
commitMessage: commitMessageFormatter(
|
|
1245
|
+
'uploadMedia',
|
|
1246
|
+
config,
|
|
1247
|
+
{
|
|
1248
|
+
path: file.path,
|
|
1249
|
+
authorLogin: user.login,
|
|
1250
|
+
authorName: user.name,
|
|
1251
|
+
},
|
|
1252
|
+
user.useOpenAuthoring,
|
|
1253
|
+
),
|
|
1254
|
+
};
|
|
1255
|
+
return this.implementation.persistMedia(file, options);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async deleteEntry(state: State, collection: Collection, slug: string) {
|
|
1259
|
+
const config = state.config;
|
|
1260
|
+
const path = selectEntryPath(collection, slug) as string;
|
|
1261
|
+
const extension = selectFolderEntryExtension(collection) as string;
|
|
1262
|
+
|
|
1263
|
+
if (!selectAllowDeletion(collection)) {
|
|
1264
|
+
throw new Error('Not allowed to delete entries in this collection');
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const user = (await this.currentUser()) as User;
|
|
1268
|
+
const commitMessage = commitMessageFormatter(
|
|
1269
|
+
'delete',
|
|
1270
|
+
config,
|
|
1271
|
+
{
|
|
1272
|
+
collection,
|
|
1273
|
+
slug,
|
|
1274
|
+
path,
|
|
1275
|
+
authorLogin: user.login,
|
|
1276
|
+
authorName: user.name,
|
|
1277
|
+
},
|
|
1278
|
+
user.useOpenAuthoring,
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
const entry = selectEntry(state.entries, collection.get('name'), slug);
|
|
1282
|
+
await this.invokePreUnpublishEvent(entry);
|
|
1283
|
+
let paths = [path];
|
|
1284
|
+
if (hasI18n(collection)) {
|
|
1285
|
+
paths = getFilePaths(collection, extension, path, slug);
|
|
1286
|
+
}
|
|
1287
|
+
await this.implementation.deleteFiles(paths, commitMessage);
|
|
1288
|
+
|
|
1289
|
+
await this.invokePostUnpublishEvent(entry);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async deleteMedia(config: CmsConfig, path: string) {
|
|
1293
|
+
const user = (await this.currentUser()) as User;
|
|
1294
|
+
const commitMessage = commitMessageFormatter(
|
|
1295
|
+
'deleteMedia',
|
|
1296
|
+
config,
|
|
1297
|
+
{
|
|
1298
|
+
path,
|
|
1299
|
+
authorLogin: user.login,
|
|
1300
|
+
authorName: user.name,
|
|
1301
|
+
},
|
|
1302
|
+
user.useOpenAuthoring,
|
|
1303
|
+
);
|
|
1304
|
+
return this.implementation.deleteFiles([path], commitMessage);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
persistUnpublishedEntry(args: PersistArgs) {
|
|
1308
|
+
return this.persistEntry({ ...args, unpublished: true });
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
|
1312
|
+
return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async publishUnpublishedEntry(entry: EntryMap) {
|
|
1316
|
+
const collection = entry.get('collection');
|
|
1317
|
+
const slug = entry.get('slug');
|
|
1318
|
+
|
|
1319
|
+
await this.invokePrePublishEvent(entry);
|
|
1320
|
+
await this.implementation.publishUnpublishedEntry!(collection, slug);
|
|
1321
|
+
await this.invokePostPublishEvent(entry);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
deleteUnpublishedEntry(collection: string, slug: string) {
|
|
1325
|
+
return this.implementation.deleteUnpublishedEntry!(collection, slug);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
entryToRaw(collection: Collection, entry: EntryMap): string {
|
|
1329
|
+
const format = resolveFormat(collection, entry.toJS());
|
|
1330
|
+
const fieldsOrder = this.fieldsOrder(collection, entry);
|
|
1331
|
+
const fieldsComments = selectFieldsComments(collection, entry);
|
|
1332
|
+
let content = format.toFile(entry.get('data').toJS(), fieldsOrder, fieldsComments);
|
|
1333
|
+
if (content.slice(-1) != '\n') {
|
|
1334
|
+
// add the EOL if it does not exist.
|
|
1335
|
+
content += '\n';
|
|
1336
|
+
}
|
|
1337
|
+
return content;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
fieldsOrder(collection: Collection, entry: EntryMap) {
|
|
1341
|
+
const fields = collection.get('fields');
|
|
1342
|
+
if (fields) {
|
|
1343
|
+
return collection
|
|
1344
|
+
.get('fields')
|
|
1345
|
+
.map(f => f!.get('name'))
|
|
1346
|
+
.toArray();
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const files = collection.get('files');
|
|
1350
|
+
const file = (files || List<CollectionFile>())
|
|
1351
|
+
.filter(f => f!.get('name') === entry.get('slug'))
|
|
1352
|
+
.get(0);
|
|
1353
|
+
|
|
1354
|
+
if (file == null) {
|
|
1355
|
+
throw new Error(`No file found for ${entry.get('slug')} in ${collection.get('name')}`);
|
|
1356
|
+
}
|
|
1357
|
+
return file
|
|
1358
|
+
.get('fields')
|
|
1359
|
+
.map(f => f!.get('name'))
|
|
1360
|
+
.toArray();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
filterEntries(collection: { entries: EntryValue[] }, filterRule: FilterRule) {
|
|
1364
|
+
return collection.entries.filter(entry => {
|
|
1365
|
+
const fieldValue = entry.data[filterRule.get('field')];
|
|
1366
|
+
if (Array.isArray(fieldValue)) {
|
|
1367
|
+
return fieldValue.includes(filterRule.get('value'));
|
|
1368
|
+
}
|
|
1369
|
+
return fieldValue === filterRule.get('value');
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
export function resolveBackend(config: CmsConfig) {
|
|
1375
|
+
if (!config.backend.name) {
|
|
1376
|
+
throw new Error('No backend defined in configuration');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const { name } = config.backend;
|
|
1380
|
+
const authStore = new LocalStorageAuthStore();
|
|
1381
|
+
|
|
1382
|
+
const backend = getBackend(name);
|
|
1383
|
+
if (!backend) {
|
|
1384
|
+
throw new Error(`Backend not found: ${name}`);
|
|
1385
|
+
} else {
|
|
1386
|
+
return new Backend(backend, { backendName: name, authStore, config });
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
export const currentBackend = (function () {
|
|
1391
|
+
let backend: Backend;
|
|
1392
|
+
|
|
1393
|
+
return (config: CmsConfig) => {
|
|
1394
|
+
if (backend) {
|
|
1395
|
+
return backend;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return (backend = resolveBackend(config));
|
|
1399
|
+
};
|
|
1400
|
+
})();
|