@evfrenkel/decap-cms-core 3.13.0-image-conversions.0

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 (306) hide show
  1. package/README.md +9 -0
  2. package/dist/20.decap-cms-core.js +2 -0
  3. package/dist/20.decap-cms-core.js.map +1 -0
  4. package/dist/3802306e7b58a11862fb.wasm +0 -0
  5. package/dist/@evfrenkel/20.decap-cms-core.js +2 -0
  6. package/dist/@evfrenkel/20.decap-cms-core.js.map +1 -0
  7. package/dist/@evfrenkel/decap-cms-core.js +44 -0
  8. package/dist/@evfrenkel/decap-cms-core.js.LICENSE.txt +126 -0
  9. package/dist/@evfrenkel/decap-cms-core.js.map +1 -0
  10. package/dist/decap-cms-core.js +47 -0
  11. package/dist/decap-cms-core.js.LICENSE.txt +126 -0
  12. package/dist/decap-cms-core.js.map +1 -0
  13. package/dist/esm/actions/auth.js +97 -0
  14. package/dist/esm/actions/collections.js +15 -0
  15. package/dist/esm/actions/config.js +503 -0
  16. package/dist/esm/actions/deploys.js +80 -0
  17. package/dist/esm/actions/editorialWorkflow.js +480 -0
  18. package/dist/esm/actions/entries.js +876 -0
  19. package/dist/esm/actions/media.js +147 -0
  20. package/dist/esm/actions/mediaLibrary.js +599 -0
  21. package/dist/esm/actions/notifications.js +21 -0
  22. package/dist/esm/actions/search.js +149 -0
  23. package/dist/esm/actions/status.js +74 -0
  24. package/dist/esm/actions/waitUntil.js +32 -0
  25. package/dist/esm/backend.js +1090 -0
  26. package/dist/esm/bootstrap.js +101 -0
  27. package/dist/esm/components/App/App.js +296 -0
  28. package/dist/esm/components/App/Header.js +172 -0
  29. package/dist/esm/components/App/NotFoundPage.js +19 -0
  30. package/dist/esm/components/Collection/Collection.js +198 -0
  31. package/dist/esm/components/Collection/CollectionControls.js +47 -0
  32. package/dist/esm/components/Collection/CollectionSearch.js +222 -0
  33. package/dist/esm/components/Collection/CollectionTop.js +68 -0
  34. package/dist/esm/components/Collection/ControlButton.js +17 -0
  35. package/dist/esm/components/Collection/Entries/Entries.js +73 -0
  36. package/dist/esm/components/Collection/Entries/EntriesCollection.js +241 -0
  37. package/dist/esm/components/Collection/Entries/EntriesSearch.js +113 -0
  38. package/dist/esm/components/Collection/Entries/EntryCard.js +192 -0
  39. package/dist/esm/components/Collection/Entries/EntryListing.js +143 -0
  40. package/dist/esm/components/Collection/FilterControl.js +33 -0
  41. package/dist/esm/components/Collection/GroupControl.js +33 -0
  42. package/dist/esm/components/Collection/NestedCollection.js +308 -0
  43. package/dist/esm/components/Collection/Sidebar.js +91 -0
  44. package/dist/esm/components/Collection/SortControl.js +59 -0
  45. package/dist/esm/components/Collection/ViewStyleControl.js +41 -0
  46. package/dist/esm/components/Editor/Editor.js +466 -0
  47. package/dist/esm/components/Editor/EditorControlPane/EditorControl.js +398 -0
  48. package/dist/esm/components/Editor/EditorControlPane/EditorControlPane.js +254 -0
  49. package/dist/esm/components/Editor/EditorControlPane/Widget.js +374 -0
  50. package/dist/esm/components/Editor/EditorInterface.js +386 -0
  51. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreview.js +47 -0
  52. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewContent.js +67 -0
  53. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +306 -0
  54. package/dist/esm/components/Editor/EditorPreviewPane/PreviewHOC.js +27 -0
  55. package/dist/esm/components/Editor/EditorToolbar.js +554 -0
  56. package/dist/esm/components/Editor/withWorkflow.js +56 -0
  57. package/dist/esm/components/EditorWidgets/Unknown/UnknownControl.js +18 -0
  58. package/dist/esm/components/EditorWidgets/Unknown/UnknownPreview.js +20 -0
  59. package/dist/esm/components/EditorWidgets/index.js +4 -0
  60. package/dist/esm/components/MediaLibrary/EmptyMessage.js +22 -0
  61. package/dist/esm/components/MediaLibrary/MediaLibrary.js +446 -0
  62. package/dist/esm/components/MediaLibrary/MediaLibraryButtons.js +92 -0
  63. package/dist/esm/components/MediaLibrary/MediaLibraryCard.js +99 -0
  64. package/dist/esm/components/MediaLibrary/MediaLibraryCardGrid.js +198 -0
  65. package/dist/esm/components/MediaLibrary/MediaLibraryHeader.js +34 -0
  66. package/dist/esm/components/MediaLibrary/MediaLibraryModal.js +156 -0
  67. package/dist/esm/components/MediaLibrary/MediaLibrarySearch.js +51 -0
  68. package/dist/esm/components/MediaLibrary/MediaLibraryTop.js +124 -0
  69. package/dist/esm/components/UI/DragDrop.js +67 -0
  70. package/dist/esm/components/UI/ErrorBoundary.js +170 -0
  71. package/dist/esm/components/UI/FileUploadButton.js +27 -0
  72. package/dist/esm/components/UI/Modal.js +104 -0
  73. package/dist/esm/components/UI/Notifications.js +62 -0
  74. package/dist/esm/components/UI/SettingsDropdown.js +107 -0
  75. package/dist/esm/components/UI/index.js +6 -0
  76. package/dist/esm/components/Workflow/Workflow.js +133 -0
  77. package/dist/esm/components/Workflow/WorkflowCard.js +128 -0
  78. package/dist/esm/components/Workflow/WorkflowList.js +204 -0
  79. package/dist/esm/constants/collectionTypes.js +2 -0
  80. package/dist/esm/constants/collectionViews.js +2 -0
  81. package/dist/esm/constants/commitProps.js +2 -0
  82. package/dist/esm/constants/configSchema.js +695 -0
  83. package/dist/esm/constants/fieldInference.js +57 -0
  84. package/dist/esm/constants/publishModes.js +18 -0
  85. package/dist/esm/constants/validationErrorTypes.js +6 -0
  86. package/dist/esm/formats/formats.js +83 -0
  87. package/dist/esm/formats/frontmatter.js +146 -0
  88. package/dist/esm/formats/helpers.js +12 -0
  89. package/dist/esm/formats/json.js +8 -0
  90. package/dist/esm/formats/toml.js +32 -0
  91. package/dist/esm/formats/yaml.js +60 -0
  92. package/dist/esm/index.js +7 -0
  93. package/dist/esm/integrations/index.js +28 -0
  94. package/dist/esm/integrations/providers/algolia/implementation.js +174 -0
  95. package/dist/esm/integrations/providers/assetStore/implementation.js +165 -0
  96. package/dist/esm/lib/consoleError.js +3 -0
  97. package/dist/esm/lib/formatters.js +201 -0
  98. package/dist/esm/lib/i18n.js +372 -0
  99. package/dist/esm/lib/imageTransformations.js +143 -0
  100. package/dist/esm/lib/phrases.js +6 -0
  101. package/dist/esm/lib/polyfill.js +8 -0
  102. package/dist/esm/lib/registry.js +332 -0
  103. package/dist/esm/lib/serializeEntryValues.js +67 -0
  104. package/dist/esm/lib/stega.js +142 -0
  105. package/dist/esm/lib/textHelper.js +9 -0
  106. package/dist/esm/lib/urlHelper.js +129 -0
  107. package/dist/esm/mediaLibrary.js +37 -0
  108. package/dist/esm/reducers/auth.js +27 -0
  109. package/dist/esm/reducers/collections.js +428 -0
  110. package/dist/esm/reducers/combinedReducer.js +8 -0
  111. package/dist/esm/reducers/config.js +29 -0
  112. package/dist/esm/reducers/cursors.js +31 -0
  113. package/dist/esm/reducers/deploys.js +50 -0
  114. package/dist/esm/reducers/editorialWorkflow.js +83 -0
  115. package/dist/esm/reducers/entries.js +568 -0
  116. package/dist/esm/reducers/entryDraft.js +211 -0
  117. package/dist/esm/reducers/globalUI.js +25 -0
  118. package/dist/esm/reducers/index.js +66 -0
  119. package/dist/esm/reducers/integrations.js +53 -0
  120. package/dist/esm/reducers/mediaLibrary.js +252 -0
  121. package/dist/esm/reducers/medias.js +68 -0
  122. package/dist/esm/reducers/notifications.js +23 -0
  123. package/dist/esm/reducers/search.js +92 -0
  124. package/dist/esm/reducers/status.js +30 -0
  125. package/dist/esm/redux/index.js +7 -0
  126. package/dist/esm/redux/middleware/waitUntilAction.js +48 -0
  127. package/dist/esm/routing/history.js +12 -0
  128. package/dist/esm/types/diacritics.d.js +0 -0
  129. package/dist/esm/types/global.d.js +1 -0
  130. package/dist/esm/types/immutable.js +7 -0
  131. package/dist/esm/types/redux.js +14 -0
  132. package/dist/esm/types/tomlify-j0.4.d.js +0 -0
  133. package/dist/esm/valueObjects/AssetProxy.js +44 -0
  134. package/dist/esm/valueObjects/EditorComponent.js +34 -0
  135. package/dist/esm/valueObjects/Entry.js +20 -0
  136. package/index.d.ts +662 -0
  137. package/package.json +100 -0
  138. package/src/__tests__/backend.spec.js +1223 -0
  139. package/src/actions/__tests__/config.spec.js +1023 -0
  140. package/src/actions/__tests__/editorialWorkflow.spec.js +216 -0
  141. package/src/actions/__tests__/entries.spec.js +610 -0
  142. package/src/actions/__tests__/media.spec.ts +171 -0
  143. package/src/actions/__tests__/mediaLibrary.spec.js +462 -0
  144. package/src/actions/__tests__/search.spec.js +209 -0
  145. package/src/actions/auth.ts +127 -0
  146. package/src/actions/collections.ts +18 -0
  147. package/src/actions/config.ts +574 -0
  148. package/src/actions/deploys.ts +105 -0
  149. package/src/actions/editorialWorkflow.ts +567 -0
  150. package/src/actions/entries.ts +1070 -0
  151. package/src/actions/media.ts +139 -0
  152. package/src/actions/mediaLibrary.ts +639 -0
  153. package/src/actions/notifications.ts +36 -0
  154. package/src/actions/search.ts +221 -0
  155. package/src/actions/status.ts +99 -0
  156. package/src/actions/waitUntil.ts +49 -0
  157. package/src/backend.ts +1411 -0
  158. package/src/bootstrap.js +104 -0
  159. package/src/components/App/App.js +295 -0
  160. package/src/components/App/Header.js +291 -0
  161. package/src/components/App/NotFoundPage.js +23 -0
  162. package/src/components/Collection/Collection.js +210 -0
  163. package/src/components/Collection/CollectionControls.js +58 -0
  164. package/src/components/Collection/CollectionSearch.js +243 -0
  165. package/src/components/Collection/CollectionTop.js +81 -0
  166. package/src/components/Collection/ControlButton.js +27 -0
  167. package/src/components/Collection/Entries/Entries.js +82 -0
  168. package/src/components/Collection/Entries/EntriesCollection.js +277 -0
  169. package/src/components/Collection/Entries/EntriesSearch.js +102 -0
  170. package/src/components/Collection/Entries/EntryCard.js +256 -0
  171. package/src/components/Collection/Entries/EntryListing.js +151 -0
  172. package/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js +163 -0
  173. package/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +46 -0
  174. package/src/components/Collection/FilterControl.js +39 -0
  175. package/src/components/Collection/GroupControl.js +39 -0
  176. package/src/components/Collection/NestedCollection.js +330 -0
  177. package/src/components/Collection/Sidebar.js +136 -0
  178. package/src/components/Collection/SortControl.js +68 -0
  179. package/src/components/Collection/ViewStyleControl.js +52 -0
  180. package/src/components/Collection/__tests__/Collection.spec.js +75 -0
  181. package/src/components/Collection/__tests__/NestedCollection.spec.js +445 -0
  182. package/src/components/Collection/__tests__/Sidebar.spec.js +87 -0
  183. package/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap +144 -0
  184. package/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +550 -0
  185. package/src/components/Collection/__tests__/__snapshots__/Sidebar.spec.js.snap +312 -0
  186. package/src/components/Editor/Editor.js +497 -0
  187. package/src/components/Editor/EditorControlPane/EditorControl.js +453 -0
  188. package/src/components/Editor/EditorControlPane/EditorControlPane.js +269 -0
  189. package/src/components/Editor/EditorControlPane/Widget.js +384 -0
  190. package/src/components/Editor/EditorInterface.js +444 -0
  191. package/src/components/Editor/EditorPreviewPane/EditorPreview.js +40 -0
  192. package/src/components/Editor/EditorPreviewPane/EditorPreviewContent.js +75 -0
  193. package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +337 -0
  194. package/src/components/Editor/EditorPreviewPane/PreviewHOC.js +33 -0
  195. package/src/components/Editor/EditorToolbar.js +728 -0
  196. package/src/components/Editor/__tests__/Editor.spec.js +221 -0
  197. package/src/components/Editor/__tests__/EditorToolbar.spec.js +166 -0
  198. package/src/components/Editor/__tests__/__snapshots__/Editor.spec.js.snap +45 -0
  199. package/src/components/Editor/__tests__/__snapshots__/EditorToolbar.spec.js.snap +4265 -0
  200. package/src/components/Editor/withWorkflow.js +61 -0
  201. package/src/components/EditorWidgets/Unknown/UnknownControl.js +17 -0
  202. package/src/components/EditorWidgets/Unknown/UnknownPreview.js +19 -0
  203. package/src/components/EditorWidgets/index.js +5 -0
  204. package/src/components/MediaLibrary/EmptyMessage.js +28 -0
  205. package/src/components/MediaLibrary/MediaLibrary.js +411 -0
  206. package/src/components/MediaLibrary/MediaLibraryButtons.js +135 -0
  207. package/src/components/MediaLibrary/MediaLibraryCard.js +128 -0
  208. package/src/components/MediaLibrary/MediaLibraryCardGrid.js +199 -0
  209. package/src/components/MediaLibrary/MediaLibraryHeader.js +48 -0
  210. package/src/components/MediaLibrary/MediaLibraryModal.js +200 -0
  211. package/src/components/MediaLibrary/MediaLibrarySearch.js +61 -0
  212. package/src/components/MediaLibrary/MediaLibraryTop.js +144 -0
  213. package/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js +45 -0
  214. package/src/components/MediaLibrary/__tests__/MediaLibraryCard.spec.js +49 -0
  215. package/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap +264 -0
  216. package/src/components/UI/DragDrop.js +66 -0
  217. package/src/components/UI/ErrorBoundary.js +213 -0
  218. package/src/components/UI/FileUploadButton.js +24 -0
  219. package/src/components/UI/Modal.js +112 -0
  220. package/src/components/UI/Notifications.tsx +83 -0
  221. package/src/components/UI/SettingsDropdown.js +130 -0
  222. package/src/components/UI/__tests__/ErrorBoundary.spec.js +57 -0
  223. package/src/components/UI/index.js +6 -0
  224. package/src/components/Workflow/Workflow.js +169 -0
  225. package/src/components/Workflow/WorkflowCard.js +177 -0
  226. package/src/components/Workflow/WorkflowList.js +272 -0
  227. package/src/constants/__tests__/configSchema.spec.js +644 -0
  228. package/src/constants/collectionTypes.ts +2 -0
  229. package/src/constants/collectionViews.js +2 -0
  230. package/src/constants/commitProps.ts +2 -0
  231. package/src/constants/configSchema.js +489 -0
  232. package/src/constants/fieldInference.tsx +78 -0
  233. package/src/constants/publishModes.ts +22 -0
  234. package/src/constants/validationErrorTypes.js +6 -0
  235. package/src/formats/__tests__/formats.spec.js +87 -0
  236. package/src/formats/__tests__/frontmatter.spec.js +450 -0
  237. package/src/formats/__tests__/toml.spec.js +9 -0
  238. package/src/formats/__tests__/yaml.spec.js +189 -0
  239. package/src/formats/formats.ts +97 -0
  240. package/src/formats/frontmatter.ts +150 -0
  241. package/src/formats/helpers.ts +14 -0
  242. package/src/formats/json.ts +9 -0
  243. package/src/formats/toml.ts +33 -0
  244. package/src/formats/yaml.ts +73 -0
  245. package/src/index.js +8 -0
  246. package/src/integrations/index.js +35 -0
  247. package/src/integrations/providers/algolia/implementation.js +176 -0
  248. package/src/integrations/providers/assetStore/implementation.js +148 -0
  249. package/src/lib/__tests__/formatters.spec.js +844 -0
  250. package/src/lib/__tests__/i18n.spec.js +792 -0
  251. package/src/lib/__tests__/imageTransformations.spec.ts +97 -0
  252. package/src/lib/__tests__/phrases.spec.js +119 -0
  253. package/src/lib/__tests__/registry.spec.js +261 -0
  254. package/src/lib/__tests__/serializeEntryValues.spec.js +22 -0
  255. package/src/lib/__tests__/urlHelper.spec.js +145 -0
  256. package/src/lib/consoleError.js +7 -0
  257. package/src/lib/formatters.ts +297 -0
  258. package/src/lib/i18n.ts +462 -0
  259. package/src/lib/imageTransformations.ts +212 -0
  260. package/src/lib/phrases.js +8 -0
  261. package/src/lib/polyfill.js +9 -0
  262. package/src/lib/registry.js +315 -0
  263. package/src/lib/serializeEntryValues.js +75 -0
  264. package/src/lib/stega.ts +145 -0
  265. package/src/lib/textHelper.js +11 -0
  266. package/src/lib/urlHelper.ts +152 -0
  267. package/src/mediaLibrary.ts +51 -0
  268. package/src/reducers/__tests__/auth.spec.ts +38 -0
  269. package/src/reducers/__tests__/collections.spec.js +610 -0
  270. package/src/reducers/__tests__/config.spec.js +38 -0
  271. package/src/reducers/__tests__/deploys.spec.ts +111 -0
  272. package/src/reducers/__tests__/entries.spec.js +694 -0
  273. package/src/reducers/__tests__/entryDraft.spec.js +315 -0
  274. package/src/reducers/__tests__/globalUI.js +43 -0
  275. package/src/reducers/__tests__/integrations.spec.ts +76 -0
  276. package/src/reducers/__tests__/mediaLibrary.spec.js +154 -0
  277. package/src/reducers/__tests__/medias.spec.ts +49 -0
  278. package/src/reducers/auth.ts +46 -0
  279. package/src/reducers/collections.ts +535 -0
  280. package/src/reducers/combinedReducer.ts +11 -0
  281. package/src/reducers/config.ts +38 -0
  282. package/src/reducers/cursors.js +36 -0
  283. package/src/reducers/deploys.ts +54 -0
  284. package/src/reducers/editorialWorkflow.ts +163 -0
  285. package/src/reducers/entries.ts +819 -0
  286. package/src/reducers/entryDraft.js +259 -0
  287. package/src/reducers/globalUI.ts +45 -0
  288. package/src/reducers/index.ts +82 -0
  289. package/src/reducers/integrations.ts +59 -0
  290. package/src/reducers/mediaLibrary.ts +296 -0
  291. package/src/reducers/medias.ts +66 -0
  292. package/src/reducers/notifications.ts +52 -0
  293. package/src/reducers/search.ts +111 -0
  294. package/src/reducers/status.ts +40 -0
  295. package/src/redux/index.ts +18 -0
  296. package/src/redux/middleware/waitUntilAction.ts +64 -0
  297. package/src/routing/__tests__/history.spec.ts +49 -0
  298. package/src/routing/history.ts +17 -0
  299. package/src/types/diacritics.d.ts +1 -0
  300. package/src/types/global.d.ts +8 -0
  301. package/src/types/immutable.ts +49 -0
  302. package/src/types/redux.ts +875 -0
  303. package/src/types/tomlify-j0.4.d.ts +13 -0
  304. package/src/valueObjects/AssetProxy.ts +48 -0
  305. package/src/valueObjects/EditorComponent.js +38 -0
  306. package/src/valueObjects/Entry.ts +63 -0
@@ -0,0 +1,1223 @@
1
+ import { Map, List, fromJS } from 'immutable';
2
+
3
+ import {
4
+ resolveBackend,
5
+ Backend,
6
+ extractSearchFields,
7
+ expandSearchEntries,
8
+ mergeExpandedEntries,
9
+ } from '../backend';
10
+ import { getBackend } from '../lib/registry';
11
+ import { FOLDER, FILES } from '../constants/collectionTypes';
12
+
13
+ jest.mock('../lib/registry');
14
+ jest.mock('decap-cms-lib-util');
15
+ jest.mock('../lib/urlHelper');
16
+
17
+ describe('Backend', () => {
18
+ describe('filterEntries', () => {
19
+ let backend;
20
+
21
+ beforeEach(() => {
22
+ getBackend.mockReturnValue({
23
+ init: jest.fn(),
24
+ });
25
+ backend = resolveBackend({
26
+ backend: {
27
+ name: 'git-gateway',
28
+ },
29
+ });
30
+ });
31
+
32
+ it('filters string values', () => {
33
+ const result = backend.filterEntries(
34
+ {
35
+ entries: [
36
+ {
37
+ data: {
38
+ testField: 'testValue',
39
+ },
40
+ },
41
+ {
42
+ data: {
43
+ testField: 'testValue2',
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ Map({ field: 'testField', value: 'testValue' }),
49
+ );
50
+
51
+ expect(result.length).toBe(1);
52
+ });
53
+
54
+ it('filters number values', () => {
55
+ const result = backend.filterEntries(
56
+ {
57
+ entries: [
58
+ {
59
+ data: {
60
+ testField: 42,
61
+ },
62
+ },
63
+ {
64
+ data: {
65
+ testField: 5,
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ Map({ field: 'testField', value: 42 }),
71
+ );
72
+
73
+ expect(result.length).toBe(1);
74
+ });
75
+
76
+ it('filters boolean values', () => {
77
+ const result = backend.filterEntries(
78
+ {
79
+ entries: [
80
+ {
81
+ data: {
82
+ testField: false,
83
+ },
84
+ },
85
+ {
86
+ data: {
87
+ testField: true,
88
+ },
89
+ },
90
+ ],
91
+ },
92
+ Map({ field: 'testField', value: false }),
93
+ );
94
+
95
+ expect(result.length).toBe(1);
96
+ });
97
+
98
+ it('filters list values', () => {
99
+ const result = backend.filterEntries(
100
+ {
101
+ entries: [
102
+ {
103
+ data: {
104
+ testField: ['valueOne', 'valueTwo', 'testValue'],
105
+ },
106
+ },
107
+ {
108
+ data: {
109
+ testField: ['valueThree'],
110
+ },
111
+ },
112
+ ],
113
+ },
114
+ Map({ field: 'testField', value: 'testValue' }),
115
+ );
116
+
117
+ expect(result.length).toBe(1);
118
+ });
119
+ });
120
+
121
+ describe('getLocalDraftBackup', () => {
122
+ const { localForage, asyncLock } = require('decap-cms-lib-util');
123
+
124
+ asyncLock.mockImplementation(() => ({ acquire: jest.fn(), release: jest.fn() }));
125
+
126
+ beforeEach(() => {
127
+ jest.clearAllMocks();
128
+ });
129
+
130
+ it('should return empty object on no item', async () => {
131
+ const implementation = {
132
+ init: jest.fn(() => implementation),
133
+ };
134
+
135
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
136
+
137
+ const collection = Map({
138
+ name: 'posts',
139
+ });
140
+ const slug = 'slug';
141
+
142
+ localForage.getItem.mockReturnValue();
143
+
144
+ const result = await backend.getLocalDraftBackup(collection, slug);
145
+
146
+ expect(result).toEqual({});
147
+ expect(localForage.getItem).toHaveBeenCalledTimes(1);
148
+ expect(localForage.getItem).toHaveBeenCalledWith('backup.posts.slug');
149
+ });
150
+
151
+ it('should return empty object on item with empty content', async () => {
152
+ const implementation = {
153
+ init: jest.fn(() => implementation),
154
+ };
155
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
156
+
157
+ const collection = Map({
158
+ name: 'posts',
159
+ });
160
+ const slug = 'slug';
161
+
162
+ localForage.getItem.mockReturnValue({ raw: '' });
163
+
164
+ const result = await backend.getLocalDraftBackup(collection, slug);
165
+
166
+ expect(result).toEqual({});
167
+ expect(localForage.getItem).toHaveBeenCalledTimes(1);
168
+ expect(localForage.getItem).toHaveBeenCalledWith('backup.posts.slug');
169
+ });
170
+
171
+ it('should return backup entry, empty media files and assets when only raw property was saved', async () => {
172
+ const implementation = {
173
+ init: jest.fn(() => implementation),
174
+ };
175
+
176
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
177
+
178
+ const collection = Map({
179
+ name: 'posts',
180
+ });
181
+ const slug = 'slug';
182
+
183
+ localForage.getItem.mockReturnValue({
184
+ raw: '---\ntitle: "Hello World"\n---\n',
185
+ });
186
+
187
+ const result = await backend.getLocalDraftBackup(collection, slug);
188
+
189
+ expect(result).toEqual({
190
+ entry: {
191
+ author: '',
192
+ mediaFiles: [],
193
+ collection: 'posts',
194
+ slug: 'slug',
195
+ path: '',
196
+ partial: false,
197
+ raw: '---\ntitle: "Hello World"\n---\n',
198
+ data: { title: 'Hello World' },
199
+ meta: {},
200
+ i18n: {},
201
+ label: null,
202
+ isModification: null,
203
+ status: '',
204
+ updatedOn: '',
205
+ },
206
+ });
207
+ expect(localForage.getItem).toHaveBeenCalledTimes(1);
208
+ expect(localForage.getItem).toHaveBeenCalledWith('backup.posts.slug');
209
+ });
210
+
211
+ it('should return backup entry, media files and assets when all were backed up', async () => {
212
+ const implementation = {
213
+ init: jest.fn(() => implementation),
214
+ };
215
+
216
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
217
+
218
+ const collection = Map({
219
+ name: 'posts',
220
+ });
221
+ const slug = 'slug';
222
+
223
+ localForage.getItem.mockReturnValue({
224
+ raw: '---\ntitle: "Hello World"\n---\n',
225
+ mediaFiles: [{ id: '1' }],
226
+ });
227
+
228
+ const result = await backend.getLocalDraftBackup(collection, slug);
229
+
230
+ expect(result).toEqual({
231
+ entry: {
232
+ author: '',
233
+ mediaFiles: [{ id: '1' }],
234
+ collection: 'posts',
235
+ slug: 'slug',
236
+ path: '',
237
+ partial: false,
238
+ raw: '---\ntitle: "Hello World"\n---\n',
239
+ data: { title: 'Hello World' },
240
+ meta: {},
241
+ i18n: {},
242
+ label: null,
243
+ isModification: null,
244
+ status: '',
245
+ updatedOn: '',
246
+ },
247
+ });
248
+ expect(localForage.getItem).toHaveBeenCalledTimes(1);
249
+ expect(localForage.getItem).toHaveBeenCalledWith('backup.posts.slug');
250
+ });
251
+ });
252
+
253
+ describe('persistLocalDraftBackup', () => {
254
+ const { localForage } = require('decap-cms-lib-util');
255
+
256
+ beforeEach(() => {
257
+ jest.clearAllMocks();
258
+ });
259
+
260
+ it('should not persist empty entry', async () => {
261
+ const implementation = {
262
+ init: jest.fn(() => implementation),
263
+ };
264
+
265
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
266
+
267
+ backend.entryToRaw = jest.fn().mockReturnValue('');
268
+
269
+ const collection = Map({
270
+ name: 'posts',
271
+ });
272
+
273
+ const slug = 'slug';
274
+
275
+ const entry = Map({
276
+ slug,
277
+ });
278
+
279
+ await backend.persistLocalDraftBackup(entry, collection);
280
+
281
+ expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
282
+ expect(backend.entryToRaw).toHaveBeenCalledWith(collection, entry);
283
+ expect(localForage.setItem).toHaveBeenCalledTimes(0);
284
+ });
285
+
286
+ it('should persist non empty entry', async () => {
287
+ const implementation = {
288
+ init: jest.fn(() => implementation),
289
+ };
290
+
291
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
292
+
293
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
294
+
295
+ const collection = Map({
296
+ name: 'posts',
297
+ });
298
+
299
+ const slug = 'slug';
300
+
301
+ const entry = Map({
302
+ slug,
303
+ path: 'content/posts/entry.md',
304
+ mediaFiles: List([{ id: '1' }]),
305
+ });
306
+
307
+ await backend.persistLocalDraftBackup(entry, collection);
308
+
309
+ expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
310
+ expect(backend.entryToRaw).toHaveBeenCalledWith(collection, entry);
311
+ expect(localForage.setItem).toHaveBeenCalledTimes(2);
312
+ expect(localForage.setItem).toHaveBeenCalledWith('backup.posts.slug', {
313
+ mediaFiles: [{ id: '1' }],
314
+ path: 'content/posts/entry.md',
315
+ raw: 'content',
316
+ });
317
+ expect(localForage.setItem).toHaveBeenCalledWith('backup', 'content');
318
+ });
319
+ });
320
+
321
+ describe('persistEntry', () => {
322
+ it('should update the draft with the new entry returned by preSave event', async () => {
323
+ const implementation = {
324
+ init: jest.fn(() => implementation),
325
+ persistEntry: jest.fn(() => implementation),
326
+ };
327
+
328
+ const config = {
329
+ backend: {
330
+ commit_messages: 'commit-messages',
331
+ },
332
+ };
333
+ const collection = Map({
334
+ name: 'posts',
335
+ });
336
+ const entry = Map({
337
+ data: 'old_data',
338
+ });
339
+ const newEntry = Map({
340
+ data: 'new_data',
341
+ });
342
+ const entryDraft = Map({
343
+ entry,
344
+ });
345
+ const user = { login: 'login', name: 'name' };
346
+ const backend = new Backend(implementation, { config, backendName: 'github' });
347
+
348
+ backend.currentUser = jest.fn().mockResolvedValue(user);
349
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
350
+ backend.invokePreSaveEvent = jest.fn().mockReturnValueOnce(newEntry);
351
+
352
+ await backend.persistEntry({ config, collection, entryDraft });
353
+
354
+ expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
355
+ expect(backend.entryToRaw).toHaveBeenCalledWith(collection, newEntry);
356
+ });
357
+
358
+ it('should update the draft with the new data returned by preSave event', async () => {
359
+ const implementation = {
360
+ init: jest.fn(() => implementation),
361
+ persistEntry: jest.fn(() => implementation),
362
+ };
363
+
364
+ const config = {
365
+ backend: {
366
+ commit_messages: 'commit-messages',
367
+ },
368
+ };
369
+ const collection = Map({
370
+ name: 'posts',
371
+ });
372
+ const entry = Map({
373
+ data: Map({}),
374
+ });
375
+ const newData = Map({});
376
+ const newEntry = Map({
377
+ data: newData,
378
+ });
379
+ const entryDraft = Map({
380
+ entry,
381
+ });
382
+ const user = { login: 'login', name: 'name' };
383
+ const backend = new Backend(implementation, { config, backendName: 'github' });
384
+
385
+ backend.currentUser = jest.fn().mockResolvedValue(user);
386
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
387
+ backend.invokePreSaveEvent = jest.fn().mockReturnValueOnce(newData);
388
+
389
+ await backend.persistEntry({ config, collection, entryDraft });
390
+
391
+ expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
392
+ expect(backend.entryToRaw).toHaveBeenCalledWith(collection, newEntry);
393
+ });
394
+
395
+ it('should preserve slug when preSave event handler modifies file collection entry', async () => {
396
+ const implementation = {
397
+ init: jest.fn(() => implementation),
398
+ persistEntry: jest.fn(() => implementation),
399
+ };
400
+
401
+ const config = {
402
+ backend: {
403
+ commit_messages: 'commit-messages',
404
+ },
405
+ };
406
+
407
+ // File collection with a single file
408
+ const collection = Map({
409
+ name: 'settings',
410
+ type: FILES,
411
+ files: List([
412
+ Map({
413
+ name: 'config',
414
+ file: 'data/config.json',
415
+ fields: List([Map({ name: 'title', widget: 'string' })]),
416
+ }),
417
+ ]),
418
+ });
419
+
420
+ const originalEntry = Map({
421
+ slug: 'config',
422
+ path: 'data/config.json',
423
+ data: Map({ title: 'original' }),
424
+ meta: Map({ path: 'data/config.json' }),
425
+ });
426
+
427
+ const entryDraft = Map({
428
+ entry: originalEntry,
429
+ });
430
+
431
+ const user = { login: 'login', name: 'name' };
432
+ const backend = new Backend(implementation, { config, backendName: 'github' });
433
+
434
+ backend.currentUser = jest.fn().mockResolvedValue(user);
435
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
436
+
437
+ // Mock invokePreSaveEvent to simulate a preSave handler that modifies data
438
+ // This is what happens when custom widgets or event handlers modify entry data
439
+ // The key is that it returns the FULL entry with slug, not just the data
440
+ backend.invokePreSaveEvent = jest.fn().mockImplementation(async entry => {
441
+ // Simulate a preSave handler modifying the data field
442
+ return entry.setIn(['data', 'title'], 'modified');
443
+ });
444
+
445
+ await backend.persistEntry({ config, collection, entryDraft });
446
+
447
+ // Verify entryToRaw was called with an entry that has the slug
448
+ expect(backend.entryToRaw).toHaveBeenCalledTimes(1);
449
+ const entryPassedToRaw = backend.entryToRaw.mock.calls[0][1];
450
+
451
+ // Critical assertion: slug must be preserved
452
+ expect(entryPassedToRaw.get('slug')).toBe('config');
453
+ expect(entryPassedToRaw.get('path')).toBe('data/config.json');
454
+ expect(entryPassedToRaw.getIn(['data', 'title'])).toBe('modified');
455
+ });
456
+ });
457
+
458
+ describe('persistMedia', () => {
459
+ it('should persist media', async () => {
460
+ const persistMediaResult = {};
461
+ const implementation = {
462
+ init: jest.fn(() => implementation),
463
+ persistMedia: jest.fn().mockResolvedValue(persistMediaResult),
464
+ };
465
+ const config = { backend: { name: 'github' } };
466
+
467
+ const backend = new Backend(implementation, { config, backendName: config.backend.name });
468
+ const user = { login: 'login', name: 'name' };
469
+ backend.currentUser = jest.fn().mockResolvedValue(user);
470
+
471
+ const file = { path: 'static/media/image.png' };
472
+
473
+ const result = await backend.persistMedia(config, file);
474
+ expect(result).toBe(persistMediaResult);
475
+ expect(implementation.persistMedia).toHaveBeenCalledTimes(1);
476
+ expect(implementation.persistMedia).toHaveBeenCalledWith(
477
+ { path: 'static/media/image.png' },
478
+ { commitMessage: 'Upload “static/media/image.png”' },
479
+ );
480
+ });
481
+ });
482
+
483
+ describe('unpublishedEntry', () => {
484
+ it('should return unpublished entry', async () => {
485
+ const unpublishedEntryResult = {
486
+ diffs: [{ path: 'src/posts/index.md', newFile: false }, { path: 'netlify.png' }],
487
+ };
488
+ const implementation = {
489
+ init: jest.fn(() => implementation),
490
+ unpublishedEntry: jest.fn().mockResolvedValue(unpublishedEntryResult),
491
+ unpublishedEntryDataFile: jest
492
+ .fn()
493
+ .mockResolvedValueOnce('---\ntitle: "Hello World"\n---\n'),
494
+ unpublishedEntryMediaFile: jest.fn().mockResolvedValueOnce({ id: '1' }),
495
+ };
496
+ const config = {
497
+ media_folder: 'static/images',
498
+ };
499
+
500
+ const backend = new Backend(implementation, { config, backendName: 'github' });
501
+
502
+ const collection = fromJS({
503
+ name: 'posts',
504
+ folder: 'src/posts',
505
+ fields: [],
506
+ });
507
+
508
+ const state = {
509
+ config,
510
+ integrations: Map({}),
511
+ mediaLibrary: Map({}),
512
+ };
513
+
514
+ const slug = 'slug';
515
+
516
+ const result = await backend.unpublishedEntry(state, collection, slug);
517
+ expect(result).toEqual({
518
+ author: '',
519
+ collection: 'posts',
520
+ slug: '',
521
+ path: 'src/posts/index.md',
522
+ partial: false,
523
+ raw: '---\ntitle: "Hello World"\n---\n',
524
+ data: { title: 'Hello World' },
525
+ meta: { path: 'src/posts/index.md' },
526
+ i18n: {},
527
+ label: null,
528
+ isModification: true,
529
+ mediaFiles: [{ id: '1', draft: true }],
530
+ status: '',
531
+ updatedOn: '',
532
+ });
533
+ });
534
+ });
535
+
536
+ describe('generateUniqueSlug', () => {
537
+ beforeEach(() => {
538
+ jest.resetAllMocks();
539
+ });
540
+
541
+ it("should return unique slug when entry doesn't exist", async () => {
542
+ const { sanitizeSlug } = require('../lib/urlHelper');
543
+ sanitizeSlug.mockReturnValue('some-post-title');
544
+
545
+ const implementation = {
546
+ init: jest.fn(() => implementation),
547
+ getEntry: jest.fn(() => Promise.resolve()),
548
+ };
549
+
550
+ const collection = fromJS({
551
+ name: 'posts',
552
+ fields: [
553
+ {
554
+ name: 'title',
555
+ },
556
+ ],
557
+ type: FOLDER,
558
+ folder: 'posts',
559
+ slug: '{{slug}}',
560
+ path: 'sub_dir/{{slug}}',
561
+ });
562
+
563
+ const entry = Map({
564
+ title: 'some post title',
565
+ });
566
+
567
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
568
+
569
+ await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe(
570
+ 'sub_dir/some-post-title',
571
+ );
572
+ });
573
+
574
+ it('should return unique slug when entry exists', async () => {
575
+ const { sanitizeSlug, sanitizeChar } = require('../lib/urlHelper');
576
+ sanitizeSlug.mockReturnValue('some-post-title');
577
+ sanitizeChar.mockReturnValue('-');
578
+
579
+ const implementation = {
580
+ init: jest.fn(() => implementation),
581
+ getEntry: jest.fn(),
582
+ };
583
+
584
+ implementation.getEntry.mockResolvedValueOnce({ data: 'data' });
585
+ implementation.getEntry.mockResolvedValueOnce();
586
+
587
+ const collection = fromJS({
588
+ name: 'posts',
589
+ fields: [
590
+ {
591
+ name: 'title',
592
+ },
593
+ ],
594
+ type: FOLDER,
595
+ folder: 'posts',
596
+ slug: '{{slug}}',
597
+ path: 'sub_dir/{{slug}}',
598
+ });
599
+
600
+ const entry = Map({
601
+ title: 'some post title',
602
+ });
603
+
604
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
605
+
606
+ await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe(
607
+ 'sub_dir/some-post-title-1',
608
+ );
609
+ });
610
+ });
611
+
612
+ describe('extractSearchFields', () => {
613
+ it('should extract slug', () => {
614
+ expect(extractSearchFields(['slug'])({ slug: 'entry-slug', data: {} })).toEqual(
615
+ ' entry-slug',
616
+ );
617
+ });
618
+
619
+ it('should extract path', () => {
620
+ expect(extractSearchFields(['path'])({ path: 'entry-path', data: {} })).toEqual(
621
+ ' entry-path',
622
+ );
623
+ });
624
+
625
+ it('should extract fields', () => {
626
+ expect(
627
+ extractSearchFields(['title', 'order'])({ data: { title: 'Entry Title', order: 5 } }),
628
+ ).toEqual(' Entry Title 5');
629
+ });
630
+
631
+ it('should extract nested fields', () => {
632
+ expect(
633
+ extractSearchFields(['nested.title'])({ data: { nested: { title: 'nested title' } } }),
634
+ ).toEqual(' nested title');
635
+ });
636
+ });
637
+
638
+ describe('search/query', () => {
639
+ const collections = [
640
+ fromJS({
641
+ name: 'posts',
642
+ folder: 'posts',
643
+ fields: [
644
+ { name: 'title', widget: 'string' },
645
+ { name: 'short_title', widget: 'string' },
646
+ { name: 'author', widget: 'string' },
647
+ { name: 'description', widget: 'string' },
648
+ { name: 'nested', widget: 'object', fields: { name: 'title', widget: 'string' } },
649
+ ],
650
+ }),
651
+ fromJS({
652
+ name: 'pages',
653
+ folder: 'pages',
654
+ fields: [
655
+ { name: 'title', widget: 'string' },
656
+ { name: 'short_title', widget: 'string' },
657
+ { name: 'author', widget: 'string' },
658
+ { name: 'description', widget: 'string' },
659
+ { name: 'nested', widget: 'object', fields: { name: 'title', widget: 'string' } },
660
+ ],
661
+ }),
662
+ ];
663
+
664
+ const posts = [
665
+ {
666
+ path: 'posts/find-me.md',
667
+ slug: 'find-me',
668
+ data: {
669
+ title: 'find me by title',
670
+ short_title: 'find me by short title',
671
+ author: 'find me by author',
672
+ description: 'find me by description',
673
+ nested: { title: 'find me by nested title' },
674
+ },
675
+ },
676
+ { path: 'posts/not-me.md', slug: 'not-me', data: { title: 'not me' } },
677
+ ];
678
+
679
+ const pages = [
680
+ {
681
+ path: 'pages/find-me.md',
682
+ slug: 'find-me',
683
+ data: {
684
+ title: 'find me by title',
685
+ short_title: 'find me by short title',
686
+ author: 'find me by author',
687
+ description: 'find me by description',
688
+ nested: { title: 'find me by nested title' },
689
+ },
690
+ },
691
+ { path: 'pages/not-me.md', slug: 'not-me', data: { title: 'not me' } },
692
+ ];
693
+
694
+ const files = [
695
+ {
696
+ path: 'files/file1.md',
697
+ slug: 'file1',
698
+ data: {
699
+ author: 'find me by author',
700
+ },
701
+ },
702
+ {
703
+ path: 'files/file2.md',
704
+ slug: 'file2',
705
+ data: {
706
+ other: 'find me by other',
707
+ },
708
+ },
709
+ ];
710
+
711
+ const implementation = {
712
+ init: jest.fn(() => implementation),
713
+ };
714
+
715
+ let backend;
716
+ beforeEach(() => {
717
+ backend = new Backend(implementation, { config: {}, backendName: 'github' });
718
+ backend.listAllEntries = jest.fn(collection => {
719
+ if (collection.get('name') === 'posts') {
720
+ return Promise.resolve(posts);
721
+ }
722
+ if (collection.get('name') === 'pages') {
723
+ return Promise.resolve(pages);
724
+ }
725
+ if (collection.get('name') === 'files') {
726
+ return Promise.resolve(files);
727
+ }
728
+ return Promise.resolve([]);
729
+ });
730
+ });
731
+
732
+ it('should search collections by title', async () => {
733
+ const results = await backend.search(collections, 'find me by title');
734
+
735
+ expect(results).toEqual({
736
+ entries: [posts[0], pages[0]],
737
+ });
738
+ });
739
+
740
+ it('should search collections by short title', async () => {
741
+ const results = await backend.search(collections, 'find me by short title');
742
+
743
+ expect(results).toEqual({
744
+ entries: [posts[0], pages[0]],
745
+ });
746
+ });
747
+
748
+ it('should search collections by author', async () => {
749
+ const results = await backend.search(collections, 'find me by author');
750
+
751
+ expect(results).toEqual({
752
+ entries: [posts[0], pages[0]],
753
+ });
754
+ });
755
+
756
+ it('should search collections by summary description', async () => {
757
+ const results = await backend.search(
758
+ collections.map(c => c.set('summary', '{{description}}')),
759
+ 'find me by description',
760
+ );
761
+
762
+ expect(results).toEqual({
763
+ entries: [posts[0], pages[0]],
764
+ });
765
+ });
766
+
767
+ it('should search in file collection using top level fields', async () => {
768
+ const collections = [
769
+ fromJS({
770
+ name: 'files',
771
+ files: [
772
+ {
773
+ name: 'file1',
774
+ fields: [{ name: 'author', widget: 'string' }],
775
+ },
776
+ {
777
+ name: 'file2',
778
+ fields: [{ name: 'other', widget: 'string' }],
779
+ },
780
+ ],
781
+ type: FILES,
782
+ }),
783
+ ];
784
+
785
+ expect(await backend.search(collections, 'find me by author')).toEqual({
786
+ entries: [files[0]],
787
+ });
788
+ expect(await backend.search(collections, 'find me by other')).toEqual({
789
+ entries: [files[1]],
790
+ });
791
+ });
792
+
793
+ it('should query collections by title', async () => {
794
+ const results = await backend.query(collections[0], ['title'], 'find me by title');
795
+
796
+ expect(results).toEqual({
797
+ hits: [posts[0]],
798
+ query: 'find me by title',
799
+ });
800
+ });
801
+
802
+ it('should query collections by slug', async () => {
803
+ const results = await backend.query(collections[0], ['slug'], 'find-me');
804
+
805
+ expect(results).toEqual({
806
+ hits: [posts[0]],
807
+ query: 'find-me',
808
+ });
809
+ });
810
+
811
+ it('should query collections by path', async () => {
812
+ const results = await backend.query(collections[0], ['path'], 'posts/find-me.md');
813
+
814
+ expect(results).toEqual({
815
+ hits: [posts[0]],
816
+ query: 'posts/find-me.md',
817
+ });
818
+ });
819
+
820
+ it('should query collections by nested field', async () => {
821
+ const results = await backend.query(
822
+ collections[0],
823
+ ['nested.title'],
824
+ 'find me by nested title',
825
+ );
826
+
827
+ expect(results).toEqual({
828
+ hits: [posts[0]],
829
+ query: 'find me by nested title',
830
+ });
831
+ });
832
+ });
833
+
834
+ describe('expandSearchEntries', () => {
835
+ it('should expand entry with list to multiple entries', () => {
836
+ const entry = {
837
+ data: {
838
+ field: {
839
+ nested: {
840
+ list: [
841
+ { id: 1, name: '1' },
842
+ { id: 2, name: '2' },
843
+ ],
844
+ },
845
+ },
846
+ list: [1, 2],
847
+ },
848
+ };
849
+
850
+ expect(expandSearchEntries([entry], ['list.*', 'field.nested.list.*.name'])).toEqual([
851
+ {
852
+ data: {
853
+ field: {
854
+ nested: {
855
+ list: [
856
+ { id: 1, name: '1' },
857
+ { id: 2, name: '2' },
858
+ ],
859
+ },
860
+ },
861
+ list: [1, 2],
862
+ },
863
+ field: 'list.0',
864
+ },
865
+ {
866
+ data: {
867
+ field: {
868
+ nested: {
869
+ list: [
870
+ { id: 1, name: '1' },
871
+ { id: 2, name: '2' },
872
+ ],
873
+ },
874
+ },
875
+ list: [1, 2],
876
+ },
877
+ field: 'list.1',
878
+ },
879
+ {
880
+ data: {
881
+ field: {
882
+ nested: {
883
+ list: [
884
+ { id: 1, name: '1' },
885
+ { id: 2, name: '2' },
886
+ ],
887
+ },
888
+ },
889
+ list: [1, 2],
890
+ },
891
+ field: 'field.nested.list.0.name',
892
+ },
893
+ {
894
+ data: {
895
+ field: {
896
+ nested: {
897
+ list: [
898
+ { id: 1, name: '1' },
899
+ { id: 2, name: '2' },
900
+ ],
901
+ },
902
+ },
903
+ list: [1, 2],
904
+ },
905
+ field: 'field.nested.list.1.name',
906
+ },
907
+ ]);
908
+ });
909
+ });
910
+
911
+ describe('mergeExpandedEntries', () => {
912
+ it('should merge entries and filter data', () => {
913
+ const expanded = [
914
+ {
915
+ data: {
916
+ field: {
917
+ nested: {
918
+ list: [
919
+ { id: 1, name: '1' },
920
+ { id: 2, name: '2' },
921
+ { id: 3, name: '3' },
922
+ { id: 4, name: '4' },
923
+ ],
924
+ },
925
+ },
926
+ list: [1, 2],
927
+ },
928
+ field: 'field.nested.list.0.name',
929
+ },
930
+ {
931
+ data: {
932
+ field: {
933
+ nested: {
934
+ list: [
935
+ { id: 1, name: '1' },
936
+ { id: 2, name: '2' },
937
+ { id: 3, name: '3' },
938
+ { id: 4, name: '4' },
939
+ ],
940
+ },
941
+ },
942
+ list: [1, 2],
943
+ },
944
+ field: 'field.nested.list.3.name',
945
+ },
946
+ ];
947
+
948
+ expect(mergeExpandedEntries(expanded)).toEqual([
949
+ {
950
+ data: {
951
+ field: {
952
+ nested: {
953
+ list: [
954
+ { id: 1, name: '1' },
955
+ { id: 4, name: '4' },
956
+ ],
957
+ },
958
+ },
959
+ list: [1, 2],
960
+ },
961
+ },
962
+ ]);
963
+ });
964
+
965
+ it('should merge entries and filter data based on different fields', () => {
966
+ const expanded = [
967
+ {
968
+ data: {
969
+ field: {
970
+ nested: {
971
+ list: [
972
+ { id: 1, name: '1' },
973
+ { id: 2, name: '2' },
974
+ { id: 3, name: '3' },
975
+ { id: 4, name: '4' },
976
+ ],
977
+ },
978
+ },
979
+ list: [1, 2],
980
+ },
981
+ field: 'field.nested.list.0.name',
982
+ },
983
+ {
984
+ data: {
985
+ field: {
986
+ nested: {
987
+ list: [
988
+ { id: 1, name: '1' },
989
+ { id: 2, name: '2' },
990
+ { id: 3, name: '3' },
991
+ { id: 4, name: '4' },
992
+ ],
993
+ },
994
+ },
995
+ list: [1, 2],
996
+ },
997
+ field: 'field.nested.list.3.name',
998
+ },
999
+ {
1000
+ data: {
1001
+ field: {
1002
+ nested: {
1003
+ list: [
1004
+ { id: 1, name: '1' },
1005
+ { id: 2, name: '2' },
1006
+ { id: 3, name: '3' },
1007
+ { id: 4, name: '4' },
1008
+ ],
1009
+ },
1010
+ },
1011
+ list: [1, 2],
1012
+ },
1013
+ field: 'list.1',
1014
+ },
1015
+ ];
1016
+
1017
+ expect(mergeExpandedEntries(expanded)).toEqual([
1018
+ {
1019
+ data: {
1020
+ field: {
1021
+ nested: {
1022
+ list: [
1023
+ { id: 1, name: '1' },
1024
+ { id: 4, name: '4' },
1025
+ ],
1026
+ },
1027
+ },
1028
+ list: [2],
1029
+ },
1030
+ },
1031
+ ]);
1032
+ });
1033
+
1034
+ it('should merge entries and keep sort by entry index', () => {
1035
+ const expanded = [
1036
+ {
1037
+ data: {
1038
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
1039
+ },
1040
+ field: 'list.5',
1041
+ },
1042
+ {
1043
+ data: {
1044
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
1045
+ },
1046
+ field: 'list.0',
1047
+ },
1048
+ {
1049
+ data: {
1050
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
1051
+ },
1052
+ field: 'list.11',
1053
+ },
1054
+ {
1055
+ data: {
1056
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
1057
+ },
1058
+ field: 'list.1',
1059
+ },
1060
+ ];
1061
+
1062
+ expect(mergeExpandedEntries(expanded)).toEqual([
1063
+ {
1064
+ data: {
1065
+ list: [5, 0, 11, 1],
1066
+ },
1067
+ },
1068
+ ]);
1069
+ });
1070
+ });
1071
+
1072
+ describe('persistEntry with nested collections', () => {
1073
+ it('should pass hasSubfolders=true when subfolders is true (default)', async () => {
1074
+ const implementation = {
1075
+ init: jest.fn(() => implementation),
1076
+ persistEntry: jest.fn(),
1077
+ };
1078
+
1079
+ const config = {
1080
+ backend: { commit_messages: {} },
1081
+ };
1082
+ const collection = Map({
1083
+ name: 'pages',
1084
+ type: FOLDER,
1085
+ folder: '_pages',
1086
+ create: true,
1087
+ fields: List([Map({ name: 'title', widget: 'string' })]),
1088
+ nested: Map({ depth: 10, subfolders: true }),
1089
+ meta: Map({ path: Map({ label: 'Path', widget: 'string' }) }),
1090
+ });
1091
+ const entryDraft = Map({
1092
+ entry: Map({
1093
+ data: Map({ title: 'Test' }),
1094
+ meta: Map({ path: 'blog' }),
1095
+ newRecord: true,
1096
+ }),
1097
+ });
1098
+ const user = { login: 'user', name: 'User' };
1099
+ const backend = new Backend(implementation, { config, backendName: 'test' });
1100
+
1101
+ backend.currentUser = jest.fn().mockResolvedValue(user);
1102
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
1103
+ backend.generateUniqueSlug = jest.fn().mockResolvedValue('test-slug');
1104
+ backend.invokePreSaveEvent = jest.fn().mockResolvedValue(entryDraft.get('entry'));
1105
+ backend.invokePostSaveEvent = jest.fn().mockResolvedValue();
1106
+
1107
+ await backend.persistEntry({
1108
+ config,
1109
+ collection,
1110
+ entryDraft,
1111
+ assetProxies: [],
1112
+ usedSlugs: List(),
1113
+ });
1114
+
1115
+ expect(implementation.persistEntry).toHaveBeenCalledWith(
1116
+ expect.anything(),
1117
+ expect.objectContaining({
1118
+ hasSubfolders: true,
1119
+ }),
1120
+ );
1121
+ });
1122
+
1123
+ it('should pass hasSubfolders=false when subfolders is false', async () => {
1124
+ const implementation = {
1125
+ init: jest.fn(() => implementation),
1126
+ persistEntry: jest.fn(),
1127
+ };
1128
+
1129
+ const config = {
1130
+ backend: { commit_messages: {} },
1131
+ };
1132
+ const collection = Map({
1133
+ name: 'pages',
1134
+ type: FOLDER,
1135
+ folder: '_pages',
1136
+ create: true,
1137
+ fields: List([Map({ name: 'title', widget: 'string' })]),
1138
+ nested: Map({ depth: 10, subfolders: false }),
1139
+ meta: Map({ path: Map({ label: 'Path', widget: 'string' }) }),
1140
+ });
1141
+ const entryDraft = Map({
1142
+ entry: Map({
1143
+ data: Map({ title: 'Test' }),
1144
+ meta: Map({ path: 'blog' }),
1145
+ newRecord: true,
1146
+ }),
1147
+ });
1148
+ const user = { login: 'user', name: 'User' };
1149
+ const backend = new Backend(implementation, { config, backendName: 'test' });
1150
+
1151
+ backend.currentUser = jest.fn().mockResolvedValue(user);
1152
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
1153
+ backend.generateUniqueSlug = jest.fn().mockResolvedValue('test-slug');
1154
+ backend.invokePreSaveEvent = jest.fn().mockResolvedValue(entryDraft.get('entry'));
1155
+ backend.invokePostSaveEvent = jest.fn().mockResolvedValue();
1156
+
1157
+ await backend.persistEntry({
1158
+ config,
1159
+ collection,
1160
+ entryDraft,
1161
+ assetProxies: [],
1162
+ usedSlugs: List(),
1163
+ });
1164
+
1165
+ expect(implementation.persistEntry).toHaveBeenCalledWith(
1166
+ expect.anything(),
1167
+ expect.objectContaining({
1168
+ hasSubfolders: false,
1169
+ }),
1170
+ );
1171
+ });
1172
+
1173
+ it('should default to hasSubfolders=true when subfolders is not specified', async () => {
1174
+ const implementation = {
1175
+ init: jest.fn(() => implementation),
1176
+ persistEntry: jest.fn(),
1177
+ };
1178
+
1179
+ const config = {
1180
+ backend: { commit_messages: {} },
1181
+ };
1182
+ const collection = Map({
1183
+ name: 'pages',
1184
+ type: FOLDER,
1185
+ folder: '_pages',
1186
+ create: true,
1187
+ fields: List([Map({ name: 'title', widget: 'string' })]),
1188
+ nested: Map({ depth: 10 }),
1189
+ meta: Map({ path: Map({ label: 'Path', widget: 'string' }) }),
1190
+ });
1191
+ const entryDraft = Map({
1192
+ entry: Map({
1193
+ data: Map({ title: 'Test' }),
1194
+ meta: Map({ path: 'blog' }),
1195
+ newRecord: true,
1196
+ }),
1197
+ });
1198
+ const user = { login: 'user', name: 'User' };
1199
+ const backend = new Backend(implementation, { config, backendName: 'test' });
1200
+
1201
+ backend.currentUser = jest.fn().mockResolvedValue(user);
1202
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
1203
+ backend.generateUniqueSlug = jest.fn().mockResolvedValue('test-slug');
1204
+ backend.invokePreSaveEvent = jest.fn().mockResolvedValue(entryDraft.get('entry'));
1205
+ backend.invokePostSaveEvent = jest.fn().mockResolvedValue();
1206
+
1207
+ await backend.persistEntry({
1208
+ config,
1209
+ collection,
1210
+ entryDraft,
1211
+ assetProxies: [],
1212
+ usedSlugs: List(),
1213
+ });
1214
+
1215
+ expect(implementation.persistEntry).toHaveBeenCalledWith(
1216
+ expect.anything(),
1217
+ expect.objectContaining({
1218
+ hasSubfolders: true,
1219
+ }),
1220
+ );
1221
+ });
1222
+ });
1223
+ });