@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,1161 @@
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
+
396
+ describe('persistMedia', () => {
397
+ it('should persist media', async () => {
398
+ const persistMediaResult = {};
399
+ const implementation = {
400
+ init: jest.fn(() => implementation),
401
+ persistMedia: jest.fn().mockResolvedValue(persistMediaResult),
402
+ };
403
+ const config = { backend: { name: 'github' } };
404
+
405
+ const backend = new Backend(implementation, { config, backendName: config.backend.name });
406
+ const user = { login: 'login', name: 'name' };
407
+ backend.currentUser = jest.fn().mockResolvedValue(user);
408
+
409
+ const file = { path: 'static/media/image.png' };
410
+
411
+ const result = await backend.persistMedia(config, file);
412
+ expect(result).toBe(persistMediaResult);
413
+ expect(implementation.persistMedia).toHaveBeenCalledTimes(1);
414
+ expect(implementation.persistMedia).toHaveBeenCalledWith(
415
+ { path: 'static/media/image.png' },
416
+ { commitMessage: 'Upload “static/media/image.png”' },
417
+ );
418
+ });
419
+ });
420
+
421
+ describe('unpublishedEntry', () => {
422
+ it('should return unpublished entry', async () => {
423
+ const unpublishedEntryResult = {
424
+ diffs: [{ path: 'src/posts/index.md', newFile: false }, { path: 'netlify.png' }],
425
+ };
426
+ const implementation = {
427
+ init: jest.fn(() => implementation),
428
+ unpublishedEntry: jest.fn().mockResolvedValue(unpublishedEntryResult),
429
+ unpublishedEntryDataFile: jest
430
+ .fn()
431
+ .mockResolvedValueOnce('---\ntitle: "Hello World"\n---\n'),
432
+ unpublishedEntryMediaFile: jest.fn().mockResolvedValueOnce({ id: '1' }),
433
+ };
434
+ const config = {
435
+ media_folder: 'static/images',
436
+ };
437
+
438
+ const backend = new Backend(implementation, { config, backendName: 'github' });
439
+
440
+ const collection = fromJS({
441
+ name: 'posts',
442
+ folder: 'src/posts',
443
+ fields: [],
444
+ });
445
+
446
+ const state = {
447
+ config,
448
+ integrations: Map({}),
449
+ mediaLibrary: Map({}),
450
+ };
451
+
452
+ const slug = 'slug';
453
+
454
+ const result = await backend.unpublishedEntry(state, collection, slug);
455
+ expect(result).toEqual({
456
+ author: '',
457
+ collection: 'posts',
458
+ slug: '',
459
+ path: 'src/posts/index.md',
460
+ partial: false,
461
+ raw: '---\ntitle: "Hello World"\n---\n',
462
+ data: { title: 'Hello World' },
463
+ meta: { path: 'src/posts/index.md' },
464
+ i18n: {},
465
+ label: null,
466
+ isModification: true,
467
+ mediaFiles: [{ id: '1', draft: true }],
468
+ status: '',
469
+ updatedOn: '',
470
+ });
471
+ });
472
+ });
473
+
474
+ describe('generateUniqueSlug', () => {
475
+ beforeEach(() => {
476
+ jest.resetAllMocks();
477
+ });
478
+
479
+ it("should return unique slug when entry doesn't exist", async () => {
480
+ const { sanitizeSlug } = require('../lib/urlHelper');
481
+ sanitizeSlug.mockReturnValue('some-post-title');
482
+
483
+ const implementation = {
484
+ init: jest.fn(() => implementation),
485
+ getEntry: jest.fn(() => Promise.resolve()),
486
+ };
487
+
488
+ const collection = fromJS({
489
+ name: 'posts',
490
+ fields: [
491
+ {
492
+ name: 'title',
493
+ },
494
+ ],
495
+ type: FOLDER,
496
+ folder: 'posts',
497
+ slug: '{{slug}}',
498
+ path: 'sub_dir/{{slug}}',
499
+ });
500
+
501
+ const entry = Map({
502
+ title: 'some post title',
503
+ });
504
+
505
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
506
+
507
+ await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe(
508
+ 'sub_dir/some-post-title',
509
+ );
510
+ });
511
+
512
+ it('should return unique slug when entry exists', async () => {
513
+ const { sanitizeSlug, sanitizeChar } = require('../lib/urlHelper');
514
+ sanitizeSlug.mockReturnValue('some-post-title');
515
+ sanitizeChar.mockReturnValue('-');
516
+
517
+ const implementation = {
518
+ init: jest.fn(() => implementation),
519
+ getEntry: jest.fn(),
520
+ };
521
+
522
+ implementation.getEntry.mockResolvedValueOnce({ data: 'data' });
523
+ implementation.getEntry.mockResolvedValueOnce();
524
+
525
+ const collection = fromJS({
526
+ name: 'posts',
527
+ fields: [
528
+ {
529
+ name: 'title',
530
+ },
531
+ ],
532
+ type: FOLDER,
533
+ folder: 'posts',
534
+ slug: '{{slug}}',
535
+ path: 'sub_dir/{{slug}}',
536
+ });
537
+
538
+ const entry = Map({
539
+ title: 'some post title',
540
+ });
541
+
542
+ const backend = new Backend(implementation, { config: {}, backendName: 'github' });
543
+
544
+ await expect(backend.generateUniqueSlug(collection, entry, Map({}), [])).resolves.toBe(
545
+ 'sub_dir/some-post-title-1',
546
+ );
547
+ });
548
+ });
549
+
550
+ describe('extractSearchFields', () => {
551
+ it('should extract slug', () => {
552
+ expect(extractSearchFields(['slug'])({ slug: 'entry-slug', data: {} })).toEqual(
553
+ ' entry-slug',
554
+ );
555
+ });
556
+
557
+ it('should extract path', () => {
558
+ expect(extractSearchFields(['path'])({ path: 'entry-path', data: {} })).toEqual(
559
+ ' entry-path',
560
+ );
561
+ });
562
+
563
+ it('should extract fields', () => {
564
+ expect(
565
+ extractSearchFields(['title', 'order'])({ data: { title: 'Entry Title', order: 5 } }),
566
+ ).toEqual(' Entry Title 5');
567
+ });
568
+
569
+ it('should extract nested fields', () => {
570
+ expect(
571
+ extractSearchFields(['nested.title'])({ data: { nested: { title: 'nested title' } } }),
572
+ ).toEqual(' nested title');
573
+ });
574
+ });
575
+
576
+ describe('search/query', () => {
577
+ const collections = [
578
+ fromJS({
579
+ name: 'posts',
580
+ folder: 'posts',
581
+ fields: [
582
+ { name: 'title', widget: 'string' },
583
+ { name: 'short_title', widget: 'string' },
584
+ { name: 'author', widget: 'string' },
585
+ { name: 'description', widget: 'string' },
586
+ { name: 'nested', widget: 'object', fields: { name: 'title', widget: 'string' } },
587
+ ],
588
+ }),
589
+ fromJS({
590
+ name: 'pages',
591
+ folder: 'pages',
592
+ fields: [
593
+ { name: 'title', widget: 'string' },
594
+ { name: 'short_title', widget: 'string' },
595
+ { name: 'author', widget: 'string' },
596
+ { name: 'description', widget: 'string' },
597
+ { name: 'nested', widget: 'object', fields: { name: 'title', widget: 'string' } },
598
+ ],
599
+ }),
600
+ ];
601
+
602
+ const posts = [
603
+ {
604
+ path: 'posts/find-me.md',
605
+ slug: 'find-me',
606
+ data: {
607
+ title: 'find me by title',
608
+ short_title: 'find me by short title',
609
+ author: 'find me by author',
610
+ description: 'find me by description',
611
+ nested: { title: 'find me by nested title' },
612
+ },
613
+ },
614
+ { path: 'posts/not-me.md', slug: 'not-me', data: { title: 'not me' } },
615
+ ];
616
+
617
+ const pages = [
618
+ {
619
+ path: 'pages/find-me.md',
620
+ slug: 'find-me',
621
+ data: {
622
+ title: 'find me by title',
623
+ short_title: 'find me by short title',
624
+ author: 'find me by author',
625
+ description: 'find me by description',
626
+ nested: { title: 'find me by nested title' },
627
+ },
628
+ },
629
+ { path: 'pages/not-me.md', slug: 'not-me', data: { title: 'not me' } },
630
+ ];
631
+
632
+ const files = [
633
+ {
634
+ path: 'files/file1.md',
635
+ slug: 'file1',
636
+ data: {
637
+ author: 'find me by author',
638
+ },
639
+ },
640
+ {
641
+ path: 'files/file2.md',
642
+ slug: 'file2',
643
+ data: {
644
+ other: 'find me by other',
645
+ },
646
+ },
647
+ ];
648
+
649
+ const implementation = {
650
+ init: jest.fn(() => implementation),
651
+ };
652
+
653
+ let backend;
654
+ beforeEach(() => {
655
+ backend = new Backend(implementation, { config: {}, backendName: 'github' });
656
+ backend.listAllEntries = jest.fn(collection => {
657
+ if (collection.get('name') === 'posts') {
658
+ return Promise.resolve(posts);
659
+ }
660
+ if (collection.get('name') === 'pages') {
661
+ return Promise.resolve(pages);
662
+ }
663
+ if (collection.get('name') === 'files') {
664
+ return Promise.resolve(files);
665
+ }
666
+ return Promise.resolve([]);
667
+ });
668
+ });
669
+
670
+ it('should search collections by title', async () => {
671
+ const results = await backend.search(collections, 'find me by title');
672
+
673
+ expect(results).toEqual({
674
+ entries: [posts[0], pages[0]],
675
+ });
676
+ });
677
+
678
+ it('should search collections by short title', async () => {
679
+ const results = await backend.search(collections, 'find me by short title');
680
+
681
+ expect(results).toEqual({
682
+ entries: [posts[0], pages[0]],
683
+ });
684
+ });
685
+
686
+ it('should search collections by author', async () => {
687
+ const results = await backend.search(collections, 'find me by author');
688
+
689
+ expect(results).toEqual({
690
+ entries: [posts[0], pages[0]],
691
+ });
692
+ });
693
+
694
+ it('should search collections by summary description', async () => {
695
+ const results = await backend.search(
696
+ collections.map(c => c.set('summary', '{{description}}')),
697
+ 'find me by description',
698
+ );
699
+
700
+ expect(results).toEqual({
701
+ entries: [posts[0], pages[0]],
702
+ });
703
+ });
704
+
705
+ it('should search in file collection using top level fields', async () => {
706
+ const collections = [
707
+ fromJS({
708
+ name: 'files',
709
+ files: [
710
+ {
711
+ name: 'file1',
712
+ fields: [{ name: 'author', widget: 'string' }],
713
+ },
714
+ {
715
+ name: 'file2',
716
+ fields: [{ name: 'other', widget: 'string' }],
717
+ },
718
+ ],
719
+ type: FILES,
720
+ }),
721
+ ];
722
+
723
+ expect(await backend.search(collections, 'find me by author')).toEqual({
724
+ entries: [files[0]],
725
+ });
726
+ expect(await backend.search(collections, 'find me by other')).toEqual({
727
+ entries: [files[1]],
728
+ });
729
+ });
730
+
731
+ it('should query collections by title', async () => {
732
+ const results = await backend.query(collections[0], ['title'], 'find me by title');
733
+
734
+ expect(results).toEqual({
735
+ hits: [posts[0]],
736
+ query: 'find me by title',
737
+ });
738
+ });
739
+
740
+ it('should query collections by slug', async () => {
741
+ const results = await backend.query(collections[0], ['slug'], 'find-me');
742
+
743
+ expect(results).toEqual({
744
+ hits: [posts[0]],
745
+ query: 'find-me',
746
+ });
747
+ });
748
+
749
+ it('should query collections by path', async () => {
750
+ const results = await backend.query(collections[0], ['path'], 'posts/find-me.md');
751
+
752
+ expect(results).toEqual({
753
+ hits: [posts[0]],
754
+ query: 'posts/find-me.md',
755
+ });
756
+ });
757
+
758
+ it('should query collections by nested field', async () => {
759
+ const results = await backend.query(
760
+ collections[0],
761
+ ['nested.title'],
762
+ 'find me by nested title',
763
+ );
764
+
765
+ expect(results).toEqual({
766
+ hits: [posts[0]],
767
+ query: 'find me by nested title',
768
+ });
769
+ });
770
+ });
771
+
772
+ describe('expandSearchEntries', () => {
773
+ it('should expand entry with list to multiple entries', () => {
774
+ const entry = {
775
+ data: {
776
+ field: {
777
+ nested: {
778
+ list: [
779
+ { id: 1, name: '1' },
780
+ { id: 2, name: '2' },
781
+ ],
782
+ },
783
+ },
784
+ list: [1, 2],
785
+ },
786
+ };
787
+
788
+ expect(expandSearchEntries([entry], ['list.*', 'field.nested.list.*.name'])).toEqual([
789
+ {
790
+ data: {
791
+ field: {
792
+ nested: {
793
+ list: [
794
+ { id: 1, name: '1' },
795
+ { id: 2, name: '2' },
796
+ ],
797
+ },
798
+ },
799
+ list: [1, 2],
800
+ },
801
+ field: 'list.0',
802
+ },
803
+ {
804
+ data: {
805
+ field: {
806
+ nested: {
807
+ list: [
808
+ { id: 1, name: '1' },
809
+ { id: 2, name: '2' },
810
+ ],
811
+ },
812
+ },
813
+ list: [1, 2],
814
+ },
815
+ field: 'list.1',
816
+ },
817
+ {
818
+ data: {
819
+ field: {
820
+ nested: {
821
+ list: [
822
+ { id: 1, name: '1' },
823
+ { id: 2, name: '2' },
824
+ ],
825
+ },
826
+ },
827
+ list: [1, 2],
828
+ },
829
+ field: 'field.nested.list.0.name',
830
+ },
831
+ {
832
+ data: {
833
+ field: {
834
+ nested: {
835
+ list: [
836
+ { id: 1, name: '1' },
837
+ { id: 2, name: '2' },
838
+ ],
839
+ },
840
+ },
841
+ list: [1, 2],
842
+ },
843
+ field: 'field.nested.list.1.name',
844
+ },
845
+ ]);
846
+ });
847
+ });
848
+
849
+ describe('mergeExpandedEntries', () => {
850
+ it('should merge entries and filter data', () => {
851
+ const expanded = [
852
+ {
853
+ data: {
854
+ field: {
855
+ nested: {
856
+ list: [
857
+ { id: 1, name: '1' },
858
+ { id: 2, name: '2' },
859
+ { id: 3, name: '3' },
860
+ { id: 4, name: '4' },
861
+ ],
862
+ },
863
+ },
864
+ list: [1, 2],
865
+ },
866
+ field: 'field.nested.list.0.name',
867
+ },
868
+ {
869
+ data: {
870
+ field: {
871
+ nested: {
872
+ list: [
873
+ { id: 1, name: '1' },
874
+ { id: 2, name: '2' },
875
+ { id: 3, name: '3' },
876
+ { id: 4, name: '4' },
877
+ ],
878
+ },
879
+ },
880
+ list: [1, 2],
881
+ },
882
+ field: 'field.nested.list.3.name',
883
+ },
884
+ ];
885
+
886
+ expect(mergeExpandedEntries(expanded)).toEqual([
887
+ {
888
+ data: {
889
+ field: {
890
+ nested: {
891
+ list: [
892
+ { id: 1, name: '1' },
893
+ { id: 4, name: '4' },
894
+ ],
895
+ },
896
+ },
897
+ list: [1, 2],
898
+ },
899
+ },
900
+ ]);
901
+ });
902
+
903
+ it('should merge entries and filter data based on different fields', () => {
904
+ const expanded = [
905
+ {
906
+ data: {
907
+ field: {
908
+ nested: {
909
+ list: [
910
+ { id: 1, name: '1' },
911
+ { id: 2, name: '2' },
912
+ { id: 3, name: '3' },
913
+ { id: 4, name: '4' },
914
+ ],
915
+ },
916
+ },
917
+ list: [1, 2],
918
+ },
919
+ field: 'field.nested.list.0.name',
920
+ },
921
+ {
922
+ data: {
923
+ field: {
924
+ nested: {
925
+ list: [
926
+ { id: 1, name: '1' },
927
+ { id: 2, name: '2' },
928
+ { id: 3, name: '3' },
929
+ { id: 4, name: '4' },
930
+ ],
931
+ },
932
+ },
933
+ list: [1, 2],
934
+ },
935
+ field: 'field.nested.list.3.name',
936
+ },
937
+ {
938
+ data: {
939
+ field: {
940
+ nested: {
941
+ list: [
942
+ { id: 1, name: '1' },
943
+ { id: 2, name: '2' },
944
+ { id: 3, name: '3' },
945
+ { id: 4, name: '4' },
946
+ ],
947
+ },
948
+ },
949
+ list: [1, 2],
950
+ },
951
+ field: 'list.1',
952
+ },
953
+ ];
954
+
955
+ expect(mergeExpandedEntries(expanded)).toEqual([
956
+ {
957
+ data: {
958
+ field: {
959
+ nested: {
960
+ list: [
961
+ { id: 1, name: '1' },
962
+ { id: 4, name: '4' },
963
+ ],
964
+ },
965
+ },
966
+ list: [2],
967
+ },
968
+ },
969
+ ]);
970
+ });
971
+
972
+ it('should merge entries and keep sort by entry index', () => {
973
+ const expanded = [
974
+ {
975
+ data: {
976
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
977
+ },
978
+ field: 'list.5',
979
+ },
980
+ {
981
+ data: {
982
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
983
+ },
984
+ field: 'list.0',
985
+ },
986
+ {
987
+ data: {
988
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
989
+ },
990
+ field: 'list.11',
991
+ },
992
+ {
993
+ data: {
994
+ list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
995
+ },
996
+ field: 'list.1',
997
+ },
998
+ ];
999
+
1000
+ expect(mergeExpandedEntries(expanded)).toEqual([
1001
+ {
1002
+ data: {
1003
+ list: [5, 0, 11, 1],
1004
+ },
1005
+ },
1006
+ ]);
1007
+ });
1008
+ });
1009
+
1010
+ describe('persistEntry with nested collections', () => {
1011
+ it('should pass hasSubfolders=true when subfolders is true (default)', async () => {
1012
+ const implementation = {
1013
+ init: jest.fn(() => implementation),
1014
+ persistEntry: jest.fn(),
1015
+ };
1016
+
1017
+ const config = {
1018
+ backend: { commit_messages: {} },
1019
+ };
1020
+ const collection = Map({
1021
+ name: 'pages',
1022
+ type: FOLDER,
1023
+ folder: '_pages',
1024
+ create: true,
1025
+ fields: List([Map({ name: 'title', widget: 'string' })]),
1026
+ nested: Map({ depth: 10, subfolders: true }),
1027
+ meta: Map({ path: Map({ label: 'Path', widget: 'string' }) }),
1028
+ });
1029
+ const entryDraft = Map({
1030
+ entry: Map({
1031
+ data: Map({ title: 'Test' }),
1032
+ meta: Map({ path: 'blog' }),
1033
+ newRecord: true,
1034
+ }),
1035
+ });
1036
+ const user = { login: 'user', name: 'User' };
1037
+ const backend = new Backend(implementation, { config, backendName: 'test' });
1038
+
1039
+ backend.currentUser = jest.fn().mockResolvedValue(user);
1040
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
1041
+ backend.generateUniqueSlug = jest.fn().mockResolvedValue('test-slug');
1042
+ backend.invokePreSaveEvent = jest.fn().mockResolvedValue(entryDraft.get('entry'));
1043
+ backend.invokePostSaveEvent = jest.fn().mockResolvedValue();
1044
+
1045
+ await backend.persistEntry({
1046
+ config,
1047
+ collection,
1048
+ entryDraft,
1049
+ assetProxies: [],
1050
+ usedSlugs: List(),
1051
+ });
1052
+
1053
+ expect(implementation.persistEntry).toHaveBeenCalledWith(
1054
+ expect.anything(),
1055
+ expect.objectContaining({
1056
+ hasSubfolders: true,
1057
+ }),
1058
+ );
1059
+ });
1060
+
1061
+ it('should pass hasSubfolders=false when subfolders is false', async () => {
1062
+ const implementation = {
1063
+ init: jest.fn(() => implementation),
1064
+ persistEntry: jest.fn(),
1065
+ };
1066
+
1067
+ const config = {
1068
+ backend: { commit_messages: {} },
1069
+ };
1070
+ const collection = Map({
1071
+ name: 'pages',
1072
+ type: FOLDER,
1073
+ folder: '_pages',
1074
+ create: true,
1075
+ fields: List([Map({ name: 'title', widget: 'string' })]),
1076
+ nested: Map({ depth: 10, subfolders: false }),
1077
+ meta: Map({ path: Map({ label: 'Path', widget: 'string' }) }),
1078
+ });
1079
+ const entryDraft = Map({
1080
+ entry: Map({
1081
+ data: Map({ title: 'Test' }),
1082
+ meta: Map({ path: 'blog' }),
1083
+ newRecord: true,
1084
+ }),
1085
+ });
1086
+ const user = { login: 'user', name: 'User' };
1087
+ const backend = new Backend(implementation, { config, backendName: 'test' });
1088
+
1089
+ backend.currentUser = jest.fn().mockResolvedValue(user);
1090
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
1091
+ backend.generateUniqueSlug = jest.fn().mockResolvedValue('test-slug');
1092
+ backend.invokePreSaveEvent = jest.fn().mockResolvedValue(entryDraft.get('entry'));
1093
+ backend.invokePostSaveEvent = jest.fn().mockResolvedValue();
1094
+
1095
+ await backend.persistEntry({
1096
+ config,
1097
+ collection,
1098
+ entryDraft,
1099
+ assetProxies: [],
1100
+ usedSlugs: List(),
1101
+ });
1102
+
1103
+ expect(implementation.persistEntry).toHaveBeenCalledWith(
1104
+ expect.anything(),
1105
+ expect.objectContaining({
1106
+ hasSubfolders: false,
1107
+ }),
1108
+ );
1109
+ });
1110
+
1111
+ it('should default to hasSubfolders=true when subfolders is not specified', async () => {
1112
+ const implementation = {
1113
+ init: jest.fn(() => implementation),
1114
+ persistEntry: jest.fn(),
1115
+ };
1116
+
1117
+ const config = {
1118
+ backend: { commit_messages: {} },
1119
+ };
1120
+ const collection = Map({
1121
+ name: 'pages',
1122
+ type: FOLDER,
1123
+ folder: '_pages',
1124
+ create: true,
1125
+ fields: List([Map({ name: 'title', widget: 'string' })]),
1126
+ nested: Map({ depth: 10 }),
1127
+ meta: Map({ path: Map({ label: 'Path', widget: 'string' }) }),
1128
+ });
1129
+ const entryDraft = Map({
1130
+ entry: Map({
1131
+ data: Map({ title: 'Test' }),
1132
+ meta: Map({ path: 'blog' }),
1133
+ newRecord: true,
1134
+ }),
1135
+ });
1136
+ const user = { login: 'user', name: 'User' };
1137
+ const backend = new Backend(implementation, { config, backendName: 'test' });
1138
+
1139
+ backend.currentUser = jest.fn().mockResolvedValue(user);
1140
+ backend.entryToRaw = jest.fn().mockReturnValue('content');
1141
+ backend.generateUniqueSlug = jest.fn().mockResolvedValue('test-slug');
1142
+ backend.invokePreSaveEvent = jest.fn().mockResolvedValue(entryDraft.get('entry'));
1143
+ backend.invokePostSaveEvent = jest.fn().mockResolvedValue();
1144
+
1145
+ await backend.persistEntry({
1146
+ config,
1147
+ collection,
1148
+ entryDraft,
1149
+ assetProxies: [],
1150
+ usedSlugs: List(),
1151
+ });
1152
+
1153
+ expect(implementation.persistEntry).toHaveBeenCalledWith(
1154
+ expect.anything(),
1155
+ expect.objectContaining({
1156
+ hasSubfolders: true,
1157
+ }),
1158
+ );
1159
+ });
1160
+ });
1161
+ });