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