@strapi/content-type-builder 5.54.0 → 5.55.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 (208) hide show
  1. package/dist/admin/components/AIChat/hooks/useAIFetch.js +6 -2
  2. package/dist/admin/components/AIChat/hooks/useAIFetch.js.map +1 -1
  3. package/dist/admin/components/AIChat/hooks/useAIFetch.mjs +7 -3
  4. package/dist/admin/components/AIChat/hooks/useAIFetch.mjs.map +1 -1
  5. package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.js +20 -0
  6. package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.js.map +1 -0
  7. package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.mjs +18 -0
  8. package/dist/admin/components/AIChat/hooks/usePrefetchAIToken.mjs.map +1 -0
  9. package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.js +83 -59
  10. package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.js.map +1 -1
  11. package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.mjs +86 -62
  12. package/dist/admin/components/ContentTypeBuilderNav/ContentTypeBuilderNav.mjs.map +1 -1
  13. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.js +112 -0
  14. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.js.map +1 -0
  15. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.mjs +110 -0
  16. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.mjs.map +1 -0
  17. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.js +159 -0
  18. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.js.map +1 -0
  19. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.mjs +157 -0
  20. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.mjs.map +1 -0
  21. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.js +311 -0
  22. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.js.map +1 -0
  23. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.mjs +309 -0
  24. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.mjs.map +1 -0
  25. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.js +379 -0
  26. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.js.map +1 -0
  27. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.mjs +376 -0
  28. package/dist/admin/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.mjs.map +1 -0
  29. package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.js +172 -0
  30. package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.js.map +1 -0
  31. package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.mjs +166 -0
  32. package/dist/admin/components/ContentTypeBuilderNav/hooks/useContentTypeBuilderMenu.mjs.map +1 -0
  33. package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.js +62 -0
  34. package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.js.map +1 -0
  35. package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.mjs +60 -0
  36. package/dist/admin/components/ContentTypeBuilderNav/hooks/useFolderActions.mjs.map +1 -0
  37. package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.js +145 -0
  38. package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.js.map +1 -0
  39. package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.mjs +143 -0
  40. package/dist/admin/components/ContentTypeBuilderNav/hooks/useSortableTree.mjs.map +1 -0
  41. package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.js +176 -0
  42. package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.js.map +1 -0
  43. package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.mjs +169 -0
  44. package/dist/admin/components/ContentTypeBuilderNav/lib/buildFolderTree.mjs.map +1 -0
  45. package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.js +277 -0
  46. package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.js.map +1 -0
  47. package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.mjs +273 -0
  48. package/dist/admin/components/ContentTypeBuilderNav/lib/dropProjection.mjs.map +1 -0
  49. package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.js +62 -0
  50. package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.js.map +1 -0
  51. package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.mjs +58 -0
  52. package/dist/admin/components/ContentTypeBuilderNav/lib/flatModel.mjs.map +1 -0
  53. package/dist/admin/components/DataManager/DataManagerContext.js.map +1 -1
  54. package/dist/admin/components/DataManager/DataManagerContext.mjs.map +1 -1
  55. package/dist/admin/components/DataManager/DataManagerProvider.js +70 -8
  56. package/dist/admin/components/DataManager/DataManagerProvider.js.map +1 -1
  57. package/dist/admin/components/DataManager/DataManagerProvider.mjs +70 -8
  58. package/dist/admin/components/DataManager/DataManagerProvider.mjs.map +1 -1
  59. package/dist/admin/components/DataManager/reducer.js +411 -33
  60. package/dist/admin/components/DataManager/reducer.js.map +1 -1
  61. package/dist/admin/components/DataManager/reducer.mjs +411 -33
  62. package/dist/admin/components/DataManager/reducer.mjs.map +1 -1
  63. package/dist/admin/components/DataManager/utils/cleanData.js +11 -2
  64. package/dist/admin/components/DataManager/utils/cleanData.js.map +1 -1
  65. package/dist/admin/components/DataManager/utils/cleanData.mjs +11 -2
  66. package/dist/admin/components/DataManager/utils/cleanData.mjs.map +1 -1
  67. package/dist/admin/components/DataManager/utils/contentStructure.js +150 -0
  68. package/dist/admin/components/DataManager/utils/contentStructure.js.map +1 -0
  69. package/dist/admin/components/DataManager/utils/contentStructure.mjs +133 -0
  70. package/dist/admin/components/DataManager/utils/contentStructure.mjs.map +1 -0
  71. package/dist/admin/components/FolderSelect.js +200 -0
  72. package/dist/admin/components/FolderSelect.js.map +1 -0
  73. package/dist/admin/components/FolderSelect.mjs +198 -0
  74. package/dist/admin/components/FolderSelect.mjs.map +1 -0
  75. package/dist/admin/components/FormModal/FormModal.js +23 -2
  76. package/dist/admin/components/FormModal/FormModal.js.map +1 -1
  77. package/dist/admin/components/FormModal/FormModal.mjs +23 -2
  78. package/dist/admin/components/FormModal/FormModal.mjs.map +1 -1
  79. package/dist/admin/components/FormModal/contentType/contentTypeForm.js +15 -18
  80. package/dist/admin/components/FormModal/contentType/contentTypeForm.js.map +1 -1
  81. package/dist/admin/components/FormModal/contentType/contentTypeForm.mjs +15 -18
  82. package/dist/admin/components/FormModal/contentType/contentTypeForm.mjs.map +1 -1
  83. package/dist/admin/components/FormModal/reducer.js +6 -0
  84. package/dist/admin/components/FormModal/reducer.js.map +1 -1
  85. package/dist/admin/components/FormModal/reducer.mjs +6 -0
  86. package/dist/admin/components/FormModal/reducer.mjs.map +1 -1
  87. package/dist/admin/pages/App/index.js +2 -5
  88. package/dist/admin/pages/App/index.js.map +1 -1
  89. package/dist/admin/pages/App/index.mjs +2 -5
  90. package/dist/admin/pages/App/index.mjs.map +1 -1
  91. package/dist/admin/src/components/AIChat/hooks/usePrefetchAIToken.d.ts +1 -0
  92. package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/DeleteFolderDialog.d.ts +16 -0
  93. package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/FolderNameField.d.ts +12 -0
  94. package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/FolderNavSection.d.ts +14 -0
  95. package/dist/admin/src/components/ContentTypeBuilderNav/components/FolderNav/SortableTreeRow.d.ts +38 -0
  96. package/dist/admin/src/components/ContentTypeBuilderNav/{useContentTypeBuilderMenu.d.ts → hooks/useContentTypeBuilderMenu.d.ts} +17 -7
  97. package/dist/admin/src/components/ContentTypeBuilderNav/hooks/useFolderActions.d.ts +42 -0
  98. package/dist/admin/src/components/ContentTypeBuilderNav/hooks/useSortableTree.d.ts +53 -0
  99. package/dist/admin/src/components/ContentTypeBuilderNav/lib/buildFolderTree.d.ts +42 -0
  100. package/dist/admin/src/components/ContentTypeBuilderNav/lib/dropProjection.d.ts +58 -0
  101. package/dist/admin/src/components/ContentTypeBuilderNav/lib/flatModel.d.ts +18 -0
  102. package/dist/admin/src/components/DataManager/DataManagerContext.d.ts +41 -0
  103. package/dist/admin/src/components/DataManager/reducer.d.ts +105 -1
  104. package/dist/admin/src/components/DataManager/utils/cleanData.d.ts +5 -1
  105. package/dist/admin/src/components/DataManager/utils/contentStructure.d.ts +68 -0
  106. package/dist/admin/src/components/FolderSelect.d.ts +18 -0
  107. package/dist/admin/src/components/FormModal/contentType/contentTypeForm.d.ts +0 -21
  108. package/dist/admin/src/components/FormModal/forms/forms.d.ts +0 -10
  109. package/dist/admin/src/components/FormModal/reducer.d.ts +7 -0
  110. package/dist/admin/src/utils/folderTelemetry.d.ts +5 -0
  111. package/dist/admin/translations/en.json.js +23 -0
  112. package/dist/admin/translations/en.json.js.map +1 -1
  113. package/dist/admin/translations/en.json.mjs +23 -0
  114. package/dist/admin/translations/en.json.mjs.map +1 -1
  115. package/dist/admin/utils/folderTelemetry.js +19 -0
  116. package/dist/admin/utils/folderTelemetry.js.map +1 -0
  117. package/dist/admin/utils/folderTelemetry.mjs +16 -0
  118. package/dist/admin/utils/folderTelemetry.mjs.map +1 -0
  119. package/dist/server/controllers/schema.js +1 -1
  120. package/dist/server/controllers/schema.js.map +1 -1
  121. package/dist/server/controllers/schema.mjs +1 -1
  122. package/dist/server/controllers/schema.mjs.map +1 -1
  123. package/dist/server/controllers/validation/common.js +3 -0
  124. package/dist/server/controllers/validation/common.js.map +1 -1
  125. package/dist/server/controllers/validation/common.mjs +3 -1
  126. package/dist/server/controllers/validation/common.mjs.map +1 -1
  127. package/dist/server/controllers/validation/content-structure.js +177 -0
  128. package/dist/server/controllers/validation/content-structure.js.map +1 -0
  129. package/dist/server/controllers/validation/content-structure.mjs +173 -0
  130. package/dist/server/controllers/validation/content-structure.mjs.map +1 -0
  131. package/dist/server/controllers/validation/schema.js +3 -1
  132. package/dist/server/controllers/validation/schema.js.map +1 -1
  133. package/dist/server/controllers/validation/schema.mjs +3 -1
  134. package/dist/server/controllers/validation/schema.mjs.map +1 -1
  135. package/dist/server/register.js +1 -1
  136. package/dist/server/register.js.map +1 -1
  137. package/dist/server/register.mjs +1 -1
  138. package/dist/server/register.mjs.map +1 -1
  139. package/dist/server/routes/content-api.js +4 -4
  140. package/dist/server/routes/content-api.js.map +1 -1
  141. package/dist/server/routes/content-api.mjs +4 -4
  142. package/dist/server/routes/content-api.mjs.map +1 -1
  143. package/dist/server/services/api-handler.js +37 -4
  144. package/dist/server/services/api-handler.js.map +1 -1
  145. package/dist/server/services/api-handler.mjs +36 -5
  146. package/dist/server/services/api-handler.mjs.map +1 -1
  147. package/dist/server/services/content-structure.js +174 -0
  148. package/dist/server/services/content-structure.js.map +1 -0
  149. package/dist/server/services/content-structure.mjs +172 -0
  150. package/dist/server/services/content-structure.mjs.map +1 -0
  151. package/dist/server/services/content-types.js +196 -44
  152. package/dist/server/services/content-types.js.map +1 -1
  153. package/dist/server/services/content-types.mjs +195 -45
  154. package/dist/server/services/content-types.mjs.map +1 -1
  155. package/dist/server/services/index.js +3 -1
  156. package/dist/server/services/index.js.map +1 -1
  157. package/dist/server/services/index.mjs +3 -1
  158. package/dist/server/services/index.mjs.map +1 -1
  159. package/dist/server/services/schema-builder/index.js +4 -2
  160. package/dist/server/services/schema-builder/index.js.map +1 -1
  161. package/dist/server/services/schema-builder/index.mjs +4 -2
  162. package/dist/server/services/schema-builder/index.mjs.map +1 -1
  163. package/dist/server/services/schema-builder/schema-handler.js +16 -16
  164. package/dist/server/services/schema-builder/schema-handler.js.map +1 -1
  165. package/dist/server/services/schema-builder/schema-handler.mjs +16 -16
  166. package/dist/server/services/schema-builder/schema-handler.mjs.map +1 -1
  167. package/dist/server/services/schema-mutation.js +46 -0
  168. package/dist/server/services/schema-mutation.js.map +1 -0
  169. package/dist/server/services/schema-mutation.mjs +43 -0
  170. package/dist/server/services/schema-mutation.mjs.map +1 -0
  171. package/dist/server/services/schema.js +134 -79
  172. package/dist/server/services/schema.js.map +1 -1
  173. package/dist/server/services/schema.mjs +135 -80
  174. package/dist/server/services/schema.mjs.map +1 -1
  175. package/dist/server/src/controllers/schema.d.ts.map +1 -1
  176. package/dist/server/src/controllers/validation/common.d.ts +1 -0
  177. package/dist/server/src/controllers/validation/common.d.ts.map +1 -1
  178. package/dist/server/src/controllers/validation/content-structure.d.ts +56 -0
  179. package/dist/server/src/controllers/validation/content-structure.d.ts.map +1 -0
  180. package/dist/server/src/controllers/validation/schema.d.ts +35 -0
  181. package/dist/server/src/controllers/validation/schema.d.ts.map +1 -1
  182. package/dist/server/src/index.d.ts +3 -0
  183. package/dist/server/src/index.d.ts.map +1 -1
  184. package/dist/server/src/routes/content-api.d.ts.map +1 -1
  185. package/dist/server/src/services/api-handler.d.ts +13 -1
  186. package/dist/server/src/services/api-handler.d.ts.map +1 -1
  187. package/dist/server/src/services/content-structure.d.ts +37 -0
  188. package/dist/server/src/services/content-structure.d.ts.map +1 -0
  189. package/dist/server/src/services/content-types.d.ts +4 -0
  190. package/dist/server/src/services/content-types.d.ts.map +1 -1
  191. package/dist/server/src/services/index.d.ts +4 -0
  192. package/dist/server/src/services/index.d.ts.map +1 -1
  193. package/dist/server/src/services/schema-builder/index.d.ts +3 -1
  194. package/dist/server/src/services/schema-builder/index.d.ts.map +1 -1
  195. package/dist/server/src/services/schema-builder/schema-handler.d.ts.map +1 -1
  196. package/dist/server/src/services/schema-mutation.d.ts +26 -0
  197. package/dist/server/src/services/schema-mutation.d.ts.map +1 -0
  198. package/dist/server/src/services/schema.d.ts +1 -0
  199. package/dist/server/src/services/schema.d.ts.map +1 -1
  200. package/dist/server/src/utils/index.d.ts +2 -0
  201. package/dist/server/src/utils/index.d.ts.map +1 -1
  202. package/dist/server/utils/index.js.map +1 -1
  203. package/dist/server/utils/index.mjs.map +1 -1
  204. package/package.json +6 -6
  205. package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js +0 -157
  206. package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js.map +0 -1
  207. package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.mjs +0 -151
  208. package/dist/admin/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.mjs.map +0 -1
@@ -6,6 +6,7 @@ var applyPrivateSearchDefault = require('../../utils/applyPrivateSearchDefault.j
6
6
  var getRelationType = require('../../utils/getRelationType.js');
7
7
  var makeUnique = require('../../utils/makeUnique.js');
8
8
  var undoRedo = require('./undoRedo.js');
9
+ var contentStructure = require('./utils/contentStructure.js');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
@@ -17,6 +18,8 @@ const initialState = {
17
18
  contentTypes: {},
18
19
  initialComponents: {},
19
20
  initialContentTypes: {},
21
+ contentStructure: contentStructure.createEmptyContentStructure(),
22
+ initialContentStructure: contentStructure.createEmptyContentStructure(),
20
23
  reservedNames: {
21
24
  models: [],
22
25
  attributes: []
@@ -62,6 +65,12 @@ const getNewStatus = (oldStatus, newStatus)=>{
62
65
  }
63
66
  return newStatus;
64
67
  };
68
+ const getNewGroupStatus = (oldStatus, newStatus)=>{
69
+ if (oldStatus === 'NEW') {
70
+ return oldStatus;
71
+ }
72
+ return newStatus;
73
+ };
65
74
  const setAttributeStatus = (attribute, status)=>{
66
75
  attribute.status = getNewStatus(attribute.status, status);
67
76
  };
@@ -99,6 +108,9 @@ const replaceAttributeAt = (type, index, attribute)=>{
99
108
  };
100
109
  const removeAttributeByName = (type, name)=>{
101
110
  const idx = type.attributes.findIndex((attr)=>attr.name === name);
111
+ if (idx === -1) {
112
+ return;
113
+ }
102
114
  const attr = type.attributes[idx];
103
115
  setStatus(type, 'CHANGED');
104
116
  if (attr.status === 'NEW') {
@@ -111,17 +123,156 @@ const updateType = (type, data)=>{
111
123
  merge__default.default(type, data);
112
124
  setStatus(type, 'CHANGED');
113
125
  };
126
+ const findGroup = (groups, id)=>{
127
+ return groups.find((group)=>group.id === id);
128
+ };
129
+ const setGroupStatus = (group, status)=>{
130
+ group.status = getNewGroupStatus(group.status, status);
131
+ };
132
+ /**
133
+ * Remove a content type reference from every group of a section, mark the group as changed if a reference was removed.
134
+ * @returns true if a reference was removed, false otherwise.
135
+ */ const removeContentTypeChild = (groups, uid)=>{
136
+ let removed = false;
137
+ for (const group of groups){
138
+ const groupIndex = group.children.findIndex((child)=>{
139
+ return child.type === 'contentType' && child.uid === uid;
140
+ });
141
+ if (groupIndex !== -1) {
142
+ group.children.splice(groupIndex, 1);
143
+ setGroupStatus(group, 'CHANGED');
144
+ removed = true;
145
+ }
146
+ }
147
+ return removed;
148
+ };
149
+ /**
150
+ * Assign a content type to a folder within a single section, optionally creating
151
+ * a new root folder first.
152
+ *
153
+ * Detaches the content type from any current folder then files it under the target group.
154
+ * This is also used by the create/edit content-type modal's folder picker via
155
+ * createSchema/updateSchema.
156
+ */ const applyFolderAssignment = (section, uid, folder)=>{
157
+ const { groups } = section;
158
+ removeContentTypeChild(groups, uid);
159
+ let targetGroupId;
160
+ if ('newFolderId' in folder) {
161
+ // Reuse an existing root folder of the same name instead of creating a
162
+ // duplicate. The modal picker can only ever makes root folders, so
163
+ // this makes the assumption that a same-named root folder means the
164
+ // user wants to insert into that folder.
165
+ const existing = contentStructure.findSiblingFolderByName(groups, null, folder.newFolderName);
166
+ if (existing) {
167
+ targetGroupId = existing.id;
168
+ } else {
169
+ groups.push({
170
+ name: folder.newFolderName.trim(),
171
+ id: folder.newFolderId,
172
+ parent: null,
173
+ children: [],
174
+ status: 'NEW'
175
+ });
176
+ targetGroupId = folder.newFolderId;
177
+ }
178
+ } else {
179
+ targetGroupId = folder.targetGroupId;
180
+ }
181
+ if (!targetGroupId) {
182
+ return;
183
+ }
184
+ const targetGroup = findGroup(groups, targetGroupId);
185
+ if (targetGroup) {
186
+ targetGroup.children.push({
187
+ type: 'contentType',
188
+ uid
189
+ });
190
+ setGroupStatus(targetGroup, 'CHANGED');
191
+ }
192
+ };
193
+ /**
194
+ * Determines whether `potentialDescendantId` is nested somewhere inside `ancestorId`'s subtree
195
+ */ const isDescendantGroup = (groups, ancestorId, potentialDescendantId)=>{
196
+ let current = findGroup(groups, potentialDescendantId);
197
+ while(current && current.parent !== null){
198
+ if (current.parent === ancestorId) {
199
+ return true;
200
+ }
201
+ current = findGroup(groups, current.parent);
202
+ }
203
+ return false;
204
+ };
205
+ const getGroupDepth = (groups, id)=>{
206
+ let depth = 1;
207
+ let current = findGroup(groups, id);
208
+ while(current && current.parent !== null){
209
+ current = findGroup(groups, current.parent);
210
+ depth += 1;
211
+ }
212
+ return depth;
213
+ };
214
+ /**
215
+ * Number of folder levels below a group e.g. a group with no sub-folders has height 0.
216
+ */ const getSubtreeHeight = (groups, id)=>{
217
+ const group = findGroup(groups, id);
218
+ if (!group) {
219
+ return 0;
220
+ }
221
+ let height = 0;
222
+ for (const child of group.children){
223
+ if (child.type !== 'group') continue;
224
+ height = Math.max(height, 1 + getSubtreeHeight(groups, child.id));
225
+ }
226
+ return height;
227
+ };
228
+ const applyDeleteContentType = (state, uid)=>{
229
+ const type = state.contentTypes[uid];
230
+ // Only application schemas belong to CTB. Folder actions can be crafted outside the UI,
231
+ // so this guard must live at the mutation boundary rather than in the dialog alone.
232
+ if (!type || !type.uid.startsWith('api::') || type.plugin) {
233
+ return;
234
+ }
235
+ // just drop new content types
236
+ if (type.status === 'NEW') {
237
+ delete state.contentTypes[uid];
238
+ } else {
239
+ setStatus(type, 'REMOVED');
240
+ }
241
+ // remove the content type from the content structure tree
242
+ Object.values(state.contentStructure.sections).forEach((section)=>{
243
+ removeContentTypeChild(section.groups, uid);
244
+ });
245
+ // remove the content type from the components
246
+ Object.keys(state.components).forEach((componentUid)=>{
247
+ const component = state.components[componentUid];
248
+ const namesToRemove = component.attributes.filter((attribute)=>attribute.type === 'relation' && attribute.target === uid).map((attribute)=>attribute.name);
249
+ namesToRemove.forEach((name)=>{
250
+ removeAttributeByName(component, name);
251
+ });
252
+ });
253
+ // remove the content type from the content types
254
+ Object.keys(state.contentTypes).forEach((contentTypeUid)=>{
255
+ const contentType = state.contentTypes[contentTypeUid];
256
+ const namesToRemove = contentType.attributes.filter((attribute)=>attribute.type === 'relation' && attribute.target === uid).map((attribute)=>attribute.name);
257
+ namesToRemove.forEach((name)=>{
258
+ removeAttributeByName(contentType, name);
259
+ });
260
+ });
261
+ };
114
262
  const slice = undoRedo.createUndoRedoSlice({
115
263
  name: 'data-manager',
116
264
  initialState,
117
265
  reducers: {
118
266
  init: (state, action)=>{
119
- const { components, contentTypes, reservedNames } = action.payload;
267
+ const { components, contentTypes, reservedNames, contentStructure: contentStructure$1 } = action.payload;
120
268
  state.components = components;
121
269
  state.initialComponents = components;
122
270
  state.initialContentTypes = contentTypes;
123
271
  state.contentTypes = contentTypes;
124
272
  state.reservedNames = reservedNames;
273
+ const structure = contentStructure$1 ?? contentStructure.createEmptyContentStructure();
274
+ state.initialContentStructure = structure;
275
+ state.contentStructure = structure;
125
276
  state.isLoading = false;
126
277
  },
127
278
  createComponentSchema: (state, action)=>{
@@ -142,7 +293,7 @@ const slice = undoRedo.createUndoRedoSlice({
142
293
  state.components[uid] = newSchema;
143
294
  },
144
295
  createSchema: (state, action)=>{
145
- const { uid, data } = action.payload;
296
+ const { uid, data, folder } = action.payload;
146
297
  const { displayName, singularName, pluralName, kind, draftAndPublish, pluginOptions } = data;
147
298
  const newSchema = {
148
299
  uid: uid,
@@ -165,6 +316,11 @@ const slice = undoRedo.createUndoRedoSlice({
165
316
  pluginOptions
166
317
  };
167
318
  state.contentTypes[uid] = newSchema;
319
+ // File the new content type into a folder in the same transaction it is created,
320
+ // so both are subject to the same undo/redo operatiion.
321
+ if (folder) {
322
+ applyFolderAssignment(state.contentStructure.sections[contentStructure.sectionKeyForKind(kind)], uid, folder);
323
+ }
168
324
  },
169
325
  addAttribute: (state, action)=>{
170
326
  const { attributeToSet, forTarget, targetUid } = action.payload;
@@ -426,12 +582,13 @@ const slice = undoRedo.createUndoRedoSlice({
426
582
  }
427
583
  },
428
584
  updateSchema: (state, action)=>{
429
- const { data, uid } = action.payload;
585
+ const { data, uid, folder } = action.payload;
430
586
  const { displayName, kind, draftAndPublish, pluginOptions } = data;
431
587
  const type = state.contentTypes[uid];
432
588
  if (!type) {
433
589
  return;
434
590
  }
591
+ const previousKind = type.kind;
435
592
  updateType(type, {
436
593
  info: {
437
594
  displayName
@@ -442,6 +599,15 @@ const slice = undoRedo.createUndoRedoSlice({
442
599
  },
443
600
  pluginOptions
444
601
  });
602
+ if (kind && kind !== previousKind) {
603
+ const oldSection = state.contentStructure.sections[contentStructure.sectionKeyForKind(previousKind)];
604
+ removeContentTypeChild(oldSection.groups, uid);
605
+ }
606
+ // Apply the folder picker's choice after any kind relocation, so the
607
+ // assignment lands in the content type's (possibly new) section.
608
+ if (folder) {
609
+ applyFolderAssignment(state.contentStructure.sections[contentStructure.sectionKeyForKind(kind)], uid, folder);
610
+ }
445
611
  },
446
612
  deleteComponent: (state, action)=>{
447
613
  const uid = action.payload;
@@ -461,49 +627,251 @@ const slice = undoRedo.createUndoRedoSlice({
461
627
  attribute.components = newComponents;
462
628
  }
463
629
  });
464
- contentType.attributes.forEach((attribute)=>{
465
- if (attribute.type === 'component' && attribute.component === uid) {
466
- removeAttributeByName(contentType, attribute.name);
467
- }
630
+ const namesToRemove = contentType.attributes.filter((attribute)=>attribute.type === 'component' && attribute.component === uid).map((attribute)=>attribute.name);
631
+ namesToRemove.forEach((name)=>{
632
+ removeAttributeByName(contentType, name);
468
633
  });
469
634
  });
470
635
  // remove the compo from other components
471
636
  Object.keys(state.components).forEach((componentUid)=>{
472
637
  const component = state.components[componentUid];
473
- component.attributes.forEach((attribute)=>{
474
- if (attribute.type === 'component' && attribute.component === uid) {
475
- removeAttributeByName(component, attribute.name);
476
- }
638
+ const namesToRemove = component.attributes.filter((attribute)=>attribute.type === 'component' && attribute.component === uid).map((attribute)=>attribute.name);
639
+ namesToRemove.forEach((name)=>{
640
+ removeAttributeByName(component, name);
477
641
  });
478
642
  });
479
643
  },
480
644
  deleteContentType: (state, action)=>{
481
- const uid = action.payload;
482
- const type = state.contentTypes[uid];
483
- // just drop new content types
484
- if (type.status === 'NEW') {
485
- delete state.contentTypes[uid];
486
- } else {
487
- setStatus(type, 'REMOVED');
645
+ applyDeleteContentType(state, action.payload);
646
+ },
647
+ createFolder: (state, action)=>{
648
+ const { section, id, name: rawName, parentId } = action.payload;
649
+ const name = rawName.trim();
650
+ const { groups } = state.contentStructure.sections[section];
651
+ if (parentId && getGroupDepth(groups, parentId) + 1 > contentStructure.MAX_FOLDER_DEPTH) {
652
+ return;
488
653
  }
489
- // remove the content type from the components
490
- Object.keys(state.components).forEach((componentUid)=>{
491
- const component = state.components[componentUid];
492
- component.attributes.forEach((attribute)=>{
493
- if (attribute.type === 'relation' && attribute.target === uid) {
494
- removeAttributeByName(component, attribute.name);
654
+ if (contentStructure.isFolderNameTakenBySibling(groups, parentId, name)) {
655
+ return;
656
+ }
657
+ groups.push({
658
+ id,
659
+ name,
660
+ parent: parentId,
661
+ children: [],
662
+ status: 'NEW'
663
+ });
664
+ if (parentId) {
665
+ const parent = findGroup(groups, parentId);
666
+ if (parent) {
667
+ parent.children.push({
668
+ type: 'group',
669
+ id
670
+ });
671
+ setGroupStatus(parent, 'CHANGED');
672
+ }
673
+ }
674
+ },
675
+ renameFolder: (state, action)=>{
676
+ const { section, id, name: rawName } = action.payload;
677
+ const name = rawName.trim();
678
+ const { groups } = state.contentStructure.sections[section];
679
+ const group = findGroup(groups, id);
680
+ if (!group) {
681
+ return;
682
+ }
683
+ if (group.name === name) {
684
+ return;
685
+ }
686
+ if (contentStructure.isFolderNameTakenBySibling(groups, group.parent, name, id)) {
687
+ return;
688
+ }
689
+ group.name = name;
690
+ setGroupStatus(group, 'CHANGED');
691
+ },
692
+ moveFolder: (state, action)=>{
693
+ const { section, id, newParentId, index } = action.payload;
694
+ const { groups } = state.contentStructure.sections[section];
695
+ const group = findGroup(groups, id);
696
+ if (!group) {
697
+ return;
698
+ }
699
+ // Reject no-ops and cyclic moves
700
+ if (newParentId === id || newParentId && isDescendantGroup(groups, id, newParentId)) {
701
+ return;
702
+ }
703
+ // Refuse moves that would exceed the maximum allowed depth.
704
+ const newTopDepth = newParentId ? getGroupDepth(groups, newParentId) + 1 : 1;
705
+ if (newTopDepth + getSubtreeHeight(groups, id) > contentStructure.MAX_FOLDER_DEPTH) {
706
+ return;
707
+ }
708
+ // Refuse a move that would collide with an identically named sibling.
709
+ if (contentStructure.isFolderNameTakenBySibling(groups, newParentId, group.name, id)) {
710
+ return;
711
+ }
712
+ if (group.parent) {
713
+ const oldParent = findGroup(groups, group.parent);
714
+ if (oldParent) {
715
+ // Remove from the old parent's list of children.
716
+ const idx = oldParent.children.findIndex((child)=>{
717
+ return child.type === 'group' && child.id === id;
718
+ });
719
+ if (idx !== -1) {
720
+ oldParent.children.splice(idx, 1);
495
721
  }
496
- });
722
+ setGroupStatus(oldParent, 'CHANGED');
723
+ }
724
+ }
725
+ group.parent = newParentId;
726
+ setGroupStatus(group, 'CHANGED');
727
+ if (newParentId) {
728
+ const newParent = findGroup(groups, newParentId);
729
+ if (newParent) {
730
+ const entry = {
731
+ type: 'group',
732
+ id
733
+ };
734
+ if (typeof index === 'number' && index >= 0 && index <= newParent.children.length) {
735
+ newParent.children.splice(index, 0, entry);
736
+ } else {
737
+ newParent.children.push(entry);
738
+ }
739
+ setGroupStatus(newParent, 'CHANGED');
740
+ }
741
+ } else if (typeof index === 'number') {
742
+ // If a content type is dropped into the root:
743
+ // The root display order follows the array order of the
744
+ // section's root-level groups because there's no root level
745
+ // customization yet, so reposition the moved group among them.
746
+ const currentIndex = groups.findIndex((candidate)=>candidate.id === id);
747
+ if (currentIndex !== -1) {
748
+ const [moved] = groups.splice(currentIndex, 1);
749
+ const rootIds = groups.filter((candidate)=>candidate.parent === null).map((candidate)=>candidate.id);
750
+ const anchorId = rootIds[index];
751
+ const insertAt = anchorId ? groups.findIndex((candidate)=>candidate.id === anchorId) : groups.length;
752
+ groups.splice(insertAt === -1 ? groups.length : insertAt, 0, moved);
753
+ }
754
+ }
755
+ },
756
+ deleteFolderOnly: (state, action)=>{
757
+ const { section, id: groupToDeleteId } = action.payload;
758
+ const { groups } = state.contentStructure.sections[section];
759
+ const groupToDelete = findGroup(groups, groupToDeleteId);
760
+ if (!groupToDelete) {
761
+ return;
762
+ }
763
+ const groupToDeleteParentId = groupToDelete.parent;
764
+ // Reparenting a child folder into its grandparent must preserve the same sibling-name
765
+ // invariant as create, rename, and move. Do this before mutating any group state.
766
+ if (contentStructure.getDeleteFolderOnlySiblingNameCollisions(groups, groupToDeleteId).length > 0) {
767
+ return;
768
+ }
769
+ // It is necessary to clone the children because we must subsequently mutate the original group.children array when we splice it out of its parent.
770
+ const children = groupToDelete.children.map((child)=>({
771
+ ...child
772
+ }));
773
+ // Children of a deleted folder inherit the deleted folder's parent.
774
+ children.forEach((child)=>{
775
+ if (child.type === 'group') {
776
+ const childGroup = findGroup(groups, child.id);
777
+ if (childGroup) {
778
+ childGroup.parent = groupToDeleteParentId;
779
+ }
780
+ }
497
781
  });
498
- // remove the content type from the content types
499
- Object.keys(state.contentTypes).forEach((contentTypeUid)=>{
500
- const contentType = state.contentTypes[contentTypeUid];
501
- contentType.attributes.forEach((attribute)=>{
502
- if (attribute.type === 'relation' && attribute.target === uid) {
503
- removeAttributeByName(contentType, attribute.name);
782
+ if (groupToDeleteParentId) {
783
+ const groupToDeleteParent = findGroup(groups, groupToDeleteParentId);
784
+ if (groupToDeleteParent) {
785
+ // Splice the deleted folder's children of the deleted folder into their new parent at the position of the deleted folder.
786
+ const groupToDeleteIndex = groupToDeleteParent.children.findIndex((child)=>{
787
+ return child.type === 'group' && child.id === groupToDeleteId;
788
+ });
789
+ if (groupToDeleteIndex !== -1) {
790
+ groupToDeleteParent.children.splice(groupToDeleteIndex, 1, ...children);
791
+ } else {
792
+ groupToDeleteParent.children.push(...children);
504
793
  }
505
- });
794
+ setGroupStatus(groupToDeleteParent, 'CHANGED');
795
+ }
796
+ }
797
+ // Finally, remove the deleted folder from its section's list of groups.
798
+ const groupIndex = groups.findIndex((candidate)=>{
799
+ return candidate.id === groupToDeleteId;
506
800
  });
801
+ if (groupIndex !== -1) {
802
+ groups.splice(groupIndex, 1);
803
+ }
804
+ },
805
+ deleteFolderAndContent: (state, action)=>{
806
+ const { section, id: groupToDeleteId, contentTypeUids } = action.payload;
807
+ contentTypeUids.forEach((uid)=>{
808
+ applyDeleteContentType(state, uid);
809
+ });
810
+ const { groups } = state.contentStructure.sections[section];
811
+ const groupToDelete = findGroup(groups, groupToDeleteId);
812
+ if (!groupToDelete) {
813
+ return;
814
+ }
815
+ const toRemove = new Set();
816
+ const collect = (currentGroupId)=>{
817
+ toRemove.add(currentGroupId);
818
+ const currentGroup = findGroup(groups, currentGroupId);
819
+ for (const child of currentGroup?.children ?? []){
820
+ if (child.type !== 'group') continue;
821
+ collect(child.id);
822
+ }
823
+ };
824
+ collect(groupToDeleteId);
825
+ if (groupToDelete.parent) {
826
+ const groupToDeleteParent = findGroup(groups, groupToDelete.parent);
827
+ if (groupToDeleteParent) {
828
+ const groupToDeleteIndex = groupToDeleteParent.children.findIndex((child)=>{
829
+ return child.type === 'group' && child.id === groupToDeleteId;
830
+ });
831
+ if (groupToDeleteIndex !== -1) {
832
+ groupToDeleteParent.children.splice(groupToDeleteIndex, 1);
833
+ }
834
+ setGroupStatus(groupToDeleteParent, 'CHANGED');
835
+ }
836
+ }
837
+ // Removes the group entry and all its descendants from the contentStructure.
838
+ state.contentStructure.sections[section].groups = groups.filter((candidate)=>{
839
+ return !toRemove.has(candidate.id);
840
+ });
841
+ },
842
+ assignContentTypeToFolder: (state, action)=>{
843
+ const { section, uid, targetGroupId, index } = action.payload;
844
+ const { groups } = state.contentStructure.sections[section];
845
+ removeContentTypeChild(groups, uid);
846
+ if (targetGroupId) {
847
+ const targetGroup = findGroup(groups, targetGroupId);
848
+ if (targetGroup) {
849
+ const entry = {
850
+ type: 'contentType',
851
+ uid
852
+ };
853
+ if (typeof index === 'number' && index >= 0 && index <= targetGroup.children.length) {
854
+ targetGroup.children.splice(index, 0, entry);
855
+ } else {
856
+ targetGroup.children.push(entry);
857
+ }
858
+ setGroupStatus(targetGroup, 'CHANGED');
859
+ }
860
+ }
861
+ },
862
+ reorderFolderChildren: (state, action)=>{
863
+ const { section, groupId, from, to } = action.payload;
864
+ const group = findGroup(state.contentStructure.sections[section].groups, groupId);
865
+ if (!group) {
866
+ return;
867
+ }
868
+ const child = group.children[from];
869
+ if (!child) {
870
+ return;
871
+ }
872
+ group.children.splice(from, 1);
873
+ group.children.splice(to, 0, child);
874
+ setGroupStatus(group, 'CHANGED');
507
875
  },
508
876
  applyChange (state, reducerAction) {
509
877
  const { action, schema } = reducerAction.payload;
@@ -530,10 +898,15 @@ const slice = undoRedo.createUndoRedoSlice({
530
898
  };
531
899
  } else {
532
900
  const contentType = state.contentTypes[uid];
901
+ const previousKind = contentType?.kind;
533
902
  state.contentTypes[uid] = {
534
903
  ...schema,
535
904
  status: contentType?.status === 'NEW' ? 'NEW' : schema.status
536
905
  };
906
+ if (previousKind && schema.kind && schema.kind !== previousKind) {
907
+ const oldSection = state.contentStructure.sections[contentStructure.sectionKeyForKind(previousKind)];
908
+ removeContentTypeChild(oldSection.groups, uid);
909
+ }
537
910
  }
538
911
  }
539
912
  break;
@@ -564,6 +937,9 @@ const slice = undoRedo.createUndoRedoSlice({
564
937
  } else {
565
938
  state.contentTypes[uid].status = 'REMOVED';
566
939
  }
940
+ Object.values(state.contentStructure.sections).forEach((section)=>{
941
+ removeContentTypeChild(section.groups, uid);
942
+ });
567
943
  }
568
944
  break;
569
945
  }
@@ -582,12 +958,14 @@ const slice = undoRedo.createUndoRedoSlice({
582
958
  }
583
959
  return {
584
960
  components: state.components,
585
- contentTypes: state.contentTypes
961
+ contentTypes: state.contentTypes,
962
+ contentStructure: state.contentStructure
586
963
  };
587
964
  },
588
965
  discard: (state)=>{
589
966
  state.components = state.initialComponents;
590
967
  state.contentTypes = state.initialContentTypes;
968
+ state.contentStructure = state.initialContentStructure;
591
969
  }
592
970
  });
593
971
  const { reducer, actions } = slice;