@strapi/content-type-builder 5.54.0 → 5.55.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 (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
@@ -4,12 +4,15 @@ import { applyPrivateSearchDefault } from '../../utils/applyPrivateSearchDefault
4
4
  import { getRelationType } from '../../utils/getRelationType.mjs';
5
5
  import { makeUnique } from '../../utils/makeUnique.mjs';
6
6
  import { createUndoRedoSlice } from './undoRedo.mjs';
7
+ import { sectionKeyForKind, getDeleteFolderOnlySiblingNameCollisions, MAX_FOLDER_DEPTH, isFolderNameTakenBySibling, createEmptyContentStructure, findSiblingFolderByName } from './utils/contentStructure.mjs';
7
8
 
8
9
  const initialState = {
9
10
  components: {},
10
11
  contentTypes: {},
11
12
  initialComponents: {},
12
13
  initialContentTypes: {},
14
+ contentStructure: createEmptyContentStructure(),
15
+ initialContentStructure: createEmptyContentStructure(),
13
16
  reservedNames: {
14
17
  models: [],
15
18
  attributes: []
@@ -55,6 +58,12 @@ const getNewStatus = (oldStatus, newStatus)=>{
55
58
  }
56
59
  return newStatus;
57
60
  };
61
+ const getNewGroupStatus = (oldStatus, newStatus)=>{
62
+ if (oldStatus === 'NEW') {
63
+ return oldStatus;
64
+ }
65
+ return newStatus;
66
+ };
58
67
  const setAttributeStatus = (attribute, status)=>{
59
68
  attribute.status = getNewStatus(attribute.status, status);
60
69
  };
@@ -92,6 +101,9 @@ const replaceAttributeAt = (type, index, attribute)=>{
92
101
  };
93
102
  const removeAttributeByName = (type, name)=>{
94
103
  const idx = type.attributes.findIndex((attr)=>attr.name === name);
104
+ if (idx === -1) {
105
+ return;
106
+ }
95
107
  const attr = type.attributes[idx];
96
108
  setStatus(type, 'CHANGED');
97
109
  if (attr.status === 'NEW') {
@@ -104,17 +116,156 @@ const updateType = (type, data)=>{
104
116
  merge(type, data);
105
117
  setStatus(type, 'CHANGED');
106
118
  };
119
+ const findGroup = (groups, id)=>{
120
+ return groups.find((group)=>group.id === id);
121
+ };
122
+ const setGroupStatus = (group, status)=>{
123
+ group.status = getNewGroupStatus(group.status, status);
124
+ };
125
+ /**
126
+ * Remove a content type reference from every group of a section, mark the group as changed if a reference was removed.
127
+ * @returns true if a reference was removed, false otherwise.
128
+ */ const removeContentTypeChild = (groups, uid)=>{
129
+ let removed = false;
130
+ for (const group of groups){
131
+ const groupIndex = group.children.findIndex((child)=>{
132
+ return child.type === 'contentType' && child.uid === uid;
133
+ });
134
+ if (groupIndex !== -1) {
135
+ group.children.splice(groupIndex, 1);
136
+ setGroupStatus(group, 'CHANGED');
137
+ removed = true;
138
+ }
139
+ }
140
+ return removed;
141
+ };
142
+ /**
143
+ * Assign a content type to a folder within a single section, optionally creating
144
+ * a new root folder first.
145
+ *
146
+ * Detaches the content type from any current folder then files it under the target group.
147
+ * This is also used by the create/edit content-type modal's folder picker via
148
+ * createSchema/updateSchema.
149
+ */ const applyFolderAssignment = (section, uid, folder)=>{
150
+ const { groups } = section;
151
+ removeContentTypeChild(groups, uid);
152
+ let targetGroupId;
153
+ if ('newFolderId' in folder) {
154
+ // Reuse an existing root folder of the same name instead of creating a
155
+ // duplicate. The modal picker can only ever makes root folders, so
156
+ // this makes the assumption that a same-named root folder means the
157
+ // user wants to insert into that folder.
158
+ const existing = findSiblingFolderByName(groups, null, folder.newFolderName);
159
+ if (existing) {
160
+ targetGroupId = existing.id;
161
+ } else {
162
+ groups.push({
163
+ name: folder.newFolderName.trim(),
164
+ id: folder.newFolderId,
165
+ parent: null,
166
+ children: [],
167
+ status: 'NEW'
168
+ });
169
+ targetGroupId = folder.newFolderId;
170
+ }
171
+ } else {
172
+ targetGroupId = folder.targetGroupId;
173
+ }
174
+ if (!targetGroupId) {
175
+ return;
176
+ }
177
+ const targetGroup = findGroup(groups, targetGroupId);
178
+ if (targetGroup) {
179
+ targetGroup.children.push({
180
+ type: 'contentType',
181
+ uid
182
+ });
183
+ setGroupStatus(targetGroup, 'CHANGED');
184
+ }
185
+ };
186
+ /**
187
+ * Determines whether `potentialDescendantId` is nested somewhere inside `ancestorId`'s subtree
188
+ */ const isDescendantGroup = (groups, ancestorId, potentialDescendantId)=>{
189
+ let current = findGroup(groups, potentialDescendantId);
190
+ while(current && current.parent !== null){
191
+ if (current.parent === ancestorId) {
192
+ return true;
193
+ }
194
+ current = findGroup(groups, current.parent);
195
+ }
196
+ return false;
197
+ };
198
+ const getGroupDepth = (groups, id)=>{
199
+ let depth = 1;
200
+ let current = findGroup(groups, id);
201
+ while(current && current.parent !== null){
202
+ current = findGroup(groups, current.parent);
203
+ depth += 1;
204
+ }
205
+ return depth;
206
+ };
207
+ /**
208
+ * Number of folder levels below a group e.g. a group with no sub-folders has height 0.
209
+ */ const getSubtreeHeight = (groups, id)=>{
210
+ const group = findGroup(groups, id);
211
+ if (!group) {
212
+ return 0;
213
+ }
214
+ let height = 0;
215
+ for (const child of group.children){
216
+ if (child.type !== 'group') continue;
217
+ height = Math.max(height, 1 + getSubtreeHeight(groups, child.id));
218
+ }
219
+ return height;
220
+ };
221
+ const applyDeleteContentType = (state, uid)=>{
222
+ const type = state.contentTypes[uid];
223
+ // Only application schemas belong to CTB. Folder actions can be crafted outside the UI,
224
+ // so this guard must live at the mutation boundary rather than in the dialog alone.
225
+ if (!type || !type.uid.startsWith('api::') || type.plugin) {
226
+ return;
227
+ }
228
+ // just drop new content types
229
+ if (type.status === 'NEW') {
230
+ delete state.contentTypes[uid];
231
+ } else {
232
+ setStatus(type, 'REMOVED');
233
+ }
234
+ // remove the content type from the content structure tree
235
+ Object.values(state.contentStructure.sections).forEach((section)=>{
236
+ removeContentTypeChild(section.groups, uid);
237
+ });
238
+ // remove the content type from the components
239
+ Object.keys(state.components).forEach((componentUid)=>{
240
+ const component = state.components[componentUid];
241
+ const namesToRemove = component.attributes.filter((attribute)=>attribute.type === 'relation' && attribute.target === uid).map((attribute)=>attribute.name);
242
+ namesToRemove.forEach((name)=>{
243
+ removeAttributeByName(component, name);
244
+ });
245
+ });
246
+ // remove the content type from the content types
247
+ Object.keys(state.contentTypes).forEach((contentTypeUid)=>{
248
+ const contentType = state.contentTypes[contentTypeUid];
249
+ const namesToRemove = contentType.attributes.filter((attribute)=>attribute.type === 'relation' && attribute.target === uid).map((attribute)=>attribute.name);
250
+ namesToRemove.forEach((name)=>{
251
+ removeAttributeByName(contentType, name);
252
+ });
253
+ });
254
+ };
107
255
  const slice = createUndoRedoSlice({
108
256
  name: 'data-manager',
109
257
  initialState,
110
258
  reducers: {
111
259
  init: (state, action)=>{
112
- const { components, contentTypes, reservedNames } = action.payload;
260
+ const { components, contentTypes, reservedNames, contentStructure } = action.payload;
113
261
  state.components = components;
114
262
  state.initialComponents = components;
115
263
  state.initialContentTypes = contentTypes;
116
264
  state.contentTypes = contentTypes;
117
265
  state.reservedNames = reservedNames;
266
+ const structure = contentStructure ?? createEmptyContentStructure();
267
+ state.initialContentStructure = structure;
268
+ state.contentStructure = structure;
118
269
  state.isLoading = false;
119
270
  },
120
271
  createComponentSchema: (state, action)=>{
@@ -135,7 +286,7 @@ const slice = createUndoRedoSlice({
135
286
  state.components[uid] = newSchema;
136
287
  },
137
288
  createSchema: (state, action)=>{
138
- const { uid, data } = action.payload;
289
+ const { uid, data, folder } = action.payload;
139
290
  const { displayName, singularName, pluralName, kind, draftAndPublish, pluginOptions } = data;
140
291
  const newSchema = {
141
292
  uid: uid,
@@ -158,6 +309,11 @@ const slice = createUndoRedoSlice({
158
309
  pluginOptions
159
310
  };
160
311
  state.contentTypes[uid] = newSchema;
312
+ // File the new content type into a folder in the same transaction it is created,
313
+ // so both are subject to the same undo/redo operatiion.
314
+ if (folder) {
315
+ applyFolderAssignment(state.contentStructure.sections[sectionKeyForKind(kind)], uid, folder);
316
+ }
161
317
  },
162
318
  addAttribute: (state, action)=>{
163
319
  const { attributeToSet, forTarget, targetUid } = action.payload;
@@ -419,12 +575,13 @@ const slice = createUndoRedoSlice({
419
575
  }
420
576
  },
421
577
  updateSchema: (state, action)=>{
422
- const { data, uid } = action.payload;
578
+ const { data, uid, folder } = action.payload;
423
579
  const { displayName, kind, draftAndPublish, pluginOptions } = data;
424
580
  const type = state.contentTypes[uid];
425
581
  if (!type) {
426
582
  return;
427
583
  }
584
+ const previousKind = type.kind;
428
585
  updateType(type, {
429
586
  info: {
430
587
  displayName
@@ -435,6 +592,15 @@ const slice = createUndoRedoSlice({
435
592
  },
436
593
  pluginOptions
437
594
  });
595
+ if (kind && kind !== previousKind) {
596
+ const oldSection = state.contentStructure.sections[sectionKeyForKind(previousKind)];
597
+ removeContentTypeChild(oldSection.groups, uid);
598
+ }
599
+ // Apply the folder picker's choice after any kind relocation, so the
600
+ // assignment lands in the content type's (possibly new) section.
601
+ if (folder) {
602
+ applyFolderAssignment(state.contentStructure.sections[sectionKeyForKind(kind)], uid, folder);
603
+ }
438
604
  },
439
605
  deleteComponent: (state, action)=>{
440
606
  const uid = action.payload;
@@ -454,49 +620,251 @@ const slice = createUndoRedoSlice({
454
620
  attribute.components = newComponents;
455
621
  }
456
622
  });
457
- contentType.attributes.forEach((attribute)=>{
458
- if (attribute.type === 'component' && attribute.component === uid) {
459
- removeAttributeByName(contentType, attribute.name);
460
- }
623
+ const namesToRemove = contentType.attributes.filter((attribute)=>attribute.type === 'component' && attribute.component === uid).map((attribute)=>attribute.name);
624
+ namesToRemove.forEach((name)=>{
625
+ removeAttributeByName(contentType, name);
461
626
  });
462
627
  });
463
628
  // remove the compo from other components
464
629
  Object.keys(state.components).forEach((componentUid)=>{
465
630
  const component = state.components[componentUid];
466
- component.attributes.forEach((attribute)=>{
467
- if (attribute.type === 'component' && attribute.component === uid) {
468
- removeAttributeByName(component, attribute.name);
469
- }
631
+ const namesToRemove = component.attributes.filter((attribute)=>attribute.type === 'component' && attribute.component === uid).map((attribute)=>attribute.name);
632
+ namesToRemove.forEach((name)=>{
633
+ removeAttributeByName(component, name);
470
634
  });
471
635
  });
472
636
  },
473
637
  deleteContentType: (state, action)=>{
474
- const uid = action.payload;
475
- const type = state.contentTypes[uid];
476
- // just drop new content types
477
- if (type.status === 'NEW') {
478
- delete state.contentTypes[uid];
479
- } else {
480
- setStatus(type, 'REMOVED');
638
+ applyDeleteContentType(state, action.payload);
639
+ },
640
+ createFolder: (state, action)=>{
641
+ const { section, id, name: rawName, parentId } = action.payload;
642
+ const name = rawName.trim();
643
+ const { groups } = state.contentStructure.sections[section];
644
+ if (parentId && getGroupDepth(groups, parentId) + 1 > MAX_FOLDER_DEPTH) {
645
+ return;
481
646
  }
482
- // remove the content type from the components
483
- Object.keys(state.components).forEach((componentUid)=>{
484
- const component = state.components[componentUid];
485
- component.attributes.forEach((attribute)=>{
486
- if (attribute.type === 'relation' && attribute.target === uid) {
487
- removeAttributeByName(component, attribute.name);
647
+ if (isFolderNameTakenBySibling(groups, parentId, name)) {
648
+ return;
649
+ }
650
+ groups.push({
651
+ id,
652
+ name,
653
+ parent: parentId,
654
+ children: [],
655
+ status: 'NEW'
656
+ });
657
+ if (parentId) {
658
+ const parent = findGroup(groups, parentId);
659
+ if (parent) {
660
+ parent.children.push({
661
+ type: 'group',
662
+ id
663
+ });
664
+ setGroupStatus(parent, 'CHANGED');
665
+ }
666
+ }
667
+ },
668
+ renameFolder: (state, action)=>{
669
+ const { section, id, name: rawName } = action.payload;
670
+ const name = rawName.trim();
671
+ const { groups } = state.contentStructure.sections[section];
672
+ const group = findGroup(groups, id);
673
+ if (!group) {
674
+ return;
675
+ }
676
+ if (group.name === name) {
677
+ return;
678
+ }
679
+ if (isFolderNameTakenBySibling(groups, group.parent, name, id)) {
680
+ return;
681
+ }
682
+ group.name = name;
683
+ setGroupStatus(group, 'CHANGED');
684
+ },
685
+ moveFolder: (state, action)=>{
686
+ const { section, id, newParentId, index } = action.payload;
687
+ const { groups } = state.contentStructure.sections[section];
688
+ const group = findGroup(groups, id);
689
+ if (!group) {
690
+ return;
691
+ }
692
+ // Reject no-ops and cyclic moves
693
+ if (newParentId === id || newParentId && isDescendantGroup(groups, id, newParentId)) {
694
+ return;
695
+ }
696
+ // Refuse moves that would exceed the maximum allowed depth.
697
+ const newTopDepth = newParentId ? getGroupDepth(groups, newParentId) + 1 : 1;
698
+ if (newTopDepth + getSubtreeHeight(groups, id) > MAX_FOLDER_DEPTH) {
699
+ return;
700
+ }
701
+ // Refuse a move that would collide with an identically named sibling.
702
+ if (isFolderNameTakenBySibling(groups, newParentId, group.name, id)) {
703
+ return;
704
+ }
705
+ if (group.parent) {
706
+ const oldParent = findGroup(groups, group.parent);
707
+ if (oldParent) {
708
+ // Remove from the old parent's list of children.
709
+ const idx = oldParent.children.findIndex((child)=>{
710
+ return child.type === 'group' && child.id === id;
711
+ });
712
+ if (idx !== -1) {
713
+ oldParent.children.splice(idx, 1);
488
714
  }
489
- });
715
+ setGroupStatus(oldParent, 'CHANGED');
716
+ }
717
+ }
718
+ group.parent = newParentId;
719
+ setGroupStatus(group, 'CHANGED');
720
+ if (newParentId) {
721
+ const newParent = findGroup(groups, newParentId);
722
+ if (newParent) {
723
+ const entry = {
724
+ type: 'group',
725
+ id
726
+ };
727
+ if (typeof index === 'number' && index >= 0 && index <= newParent.children.length) {
728
+ newParent.children.splice(index, 0, entry);
729
+ } else {
730
+ newParent.children.push(entry);
731
+ }
732
+ setGroupStatus(newParent, 'CHANGED');
733
+ }
734
+ } else if (typeof index === 'number') {
735
+ // If a content type is dropped into the root:
736
+ // The root display order follows the array order of the
737
+ // section's root-level groups because there's no root level
738
+ // customization yet, so reposition the moved group among them.
739
+ const currentIndex = groups.findIndex((candidate)=>candidate.id === id);
740
+ if (currentIndex !== -1) {
741
+ const [moved] = groups.splice(currentIndex, 1);
742
+ const rootIds = groups.filter((candidate)=>candidate.parent === null).map((candidate)=>candidate.id);
743
+ const anchorId = rootIds[index];
744
+ const insertAt = anchorId ? groups.findIndex((candidate)=>candidate.id === anchorId) : groups.length;
745
+ groups.splice(insertAt === -1 ? groups.length : insertAt, 0, moved);
746
+ }
747
+ }
748
+ },
749
+ deleteFolderOnly: (state, action)=>{
750
+ const { section, id: groupToDeleteId } = action.payload;
751
+ const { groups } = state.contentStructure.sections[section];
752
+ const groupToDelete = findGroup(groups, groupToDeleteId);
753
+ if (!groupToDelete) {
754
+ return;
755
+ }
756
+ const groupToDeleteParentId = groupToDelete.parent;
757
+ // Reparenting a child folder into its grandparent must preserve the same sibling-name
758
+ // invariant as create, rename, and move. Do this before mutating any group state.
759
+ if (getDeleteFolderOnlySiblingNameCollisions(groups, groupToDeleteId).length > 0) {
760
+ return;
761
+ }
762
+ // 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.
763
+ const children = groupToDelete.children.map((child)=>({
764
+ ...child
765
+ }));
766
+ // Children of a deleted folder inherit the deleted folder's parent.
767
+ children.forEach((child)=>{
768
+ if (child.type === 'group') {
769
+ const childGroup = findGroup(groups, child.id);
770
+ if (childGroup) {
771
+ childGroup.parent = groupToDeleteParentId;
772
+ }
773
+ }
490
774
  });
491
- // remove the content type from the content types
492
- Object.keys(state.contentTypes).forEach((contentTypeUid)=>{
493
- const contentType = state.contentTypes[contentTypeUid];
494
- contentType.attributes.forEach((attribute)=>{
495
- if (attribute.type === 'relation' && attribute.target === uid) {
496
- removeAttributeByName(contentType, attribute.name);
775
+ if (groupToDeleteParentId) {
776
+ const groupToDeleteParent = findGroup(groups, groupToDeleteParentId);
777
+ if (groupToDeleteParent) {
778
+ // Splice the deleted folder's children of the deleted folder into their new parent at the position of the deleted folder.
779
+ const groupToDeleteIndex = groupToDeleteParent.children.findIndex((child)=>{
780
+ return child.type === 'group' && child.id === groupToDeleteId;
781
+ });
782
+ if (groupToDeleteIndex !== -1) {
783
+ groupToDeleteParent.children.splice(groupToDeleteIndex, 1, ...children);
784
+ } else {
785
+ groupToDeleteParent.children.push(...children);
497
786
  }
498
- });
787
+ setGroupStatus(groupToDeleteParent, 'CHANGED');
788
+ }
789
+ }
790
+ // Finally, remove the deleted folder from its section's list of groups.
791
+ const groupIndex = groups.findIndex((candidate)=>{
792
+ return candidate.id === groupToDeleteId;
499
793
  });
794
+ if (groupIndex !== -1) {
795
+ groups.splice(groupIndex, 1);
796
+ }
797
+ },
798
+ deleteFolderAndContent: (state, action)=>{
799
+ const { section, id: groupToDeleteId, contentTypeUids } = action.payload;
800
+ contentTypeUids.forEach((uid)=>{
801
+ applyDeleteContentType(state, uid);
802
+ });
803
+ const { groups } = state.contentStructure.sections[section];
804
+ const groupToDelete = findGroup(groups, groupToDeleteId);
805
+ if (!groupToDelete) {
806
+ return;
807
+ }
808
+ const toRemove = new Set();
809
+ const collect = (currentGroupId)=>{
810
+ toRemove.add(currentGroupId);
811
+ const currentGroup = findGroup(groups, currentGroupId);
812
+ for (const child of currentGroup?.children ?? []){
813
+ if (child.type !== 'group') continue;
814
+ collect(child.id);
815
+ }
816
+ };
817
+ collect(groupToDeleteId);
818
+ if (groupToDelete.parent) {
819
+ const groupToDeleteParent = findGroup(groups, groupToDelete.parent);
820
+ if (groupToDeleteParent) {
821
+ const groupToDeleteIndex = groupToDeleteParent.children.findIndex((child)=>{
822
+ return child.type === 'group' && child.id === groupToDeleteId;
823
+ });
824
+ if (groupToDeleteIndex !== -1) {
825
+ groupToDeleteParent.children.splice(groupToDeleteIndex, 1);
826
+ }
827
+ setGroupStatus(groupToDeleteParent, 'CHANGED');
828
+ }
829
+ }
830
+ // Removes the group entry and all its descendants from the contentStructure.
831
+ state.contentStructure.sections[section].groups = groups.filter((candidate)=>{
832
+ return !toRemove.has(candidate.id);
833
+ });
834
+ },
835
+ assignContentTypeToFolder: (state, action)=>{
836
+ const { section, uid, targetGroupId, index } = action.payload;
837
+ const { groups } = state.contentStructure.sections[section];
838
+ removeContentTypeChild(groups, uid);
839
+ if (targetGroupId) {
840
+ const targetGroup = findGroup(groups, targetGroupId);
841
+ if (targetGroup) {
842
+ const entry = {
843
+ type: 'contentType',
844
+ uid
845
+ };
846
+ if (typeof index === 'number' && index >= 0 && index <= targetGroup.children.length) {
847
+ targetGroup.children.splice(index, 0, entry);
848
+ } else {
849
+ targetGroup.children.push(entry);
850
+ }
851
+ setGroupStatus(targetGroup, 'CHANGED');
852
+ }
853
+ }
854
+ },
855
+ reorderFolderChildren: (state, action)=>{
856
+ const { section, groupId, from, to } = action.payload;
857
+ const group = findGroup(state.contentStructure.sections[section].groups, groupId);
858
+ if (!group) {
859
+ return;
860
+ }
861
+ const child = group.children[from];
862
+ if (!child) {
863
+ return;
864
+ }
865
+ group.children.splice(from, 1);
866
+ group.children.splice(to, 0, child);
867
+ setGroupStatus(group, 'CHANGED');
500
868
  },
501
869
  applyChange (state, reducerAction) {
502
870
  const { action, schema } = reducerAction.payload;
@@ -523,10 +891,15 @@ const slice = createUndoRedoSlice({
523
891
  };
524
892
  } else {
525
893
  const contentType = state.contentTypes[uid];
894
+ const previousKind = contentType?.kind;
526
895
  state.contentTypes[uid] = {
527
896
  ...schema,
528
897
  status: contentType?.status === 'NEW' ? 'NEW' : schema.status
529
898
  };
899
+ if (previousKind && schema.kind && schema.kind !== previousKind) {
900
+ const oldSection = state.contentStructure.sections[sectionKeyForKind(previousKind)];
901
+ removeContentTypeChild(oldSection.groups, uid);
902
+ }
530
903
  }
531
904
  }
532
905
  break;
@@ -557,6 +930,9 @@ const slice = createUndoRedoSlice({
557
930
  } else {
558
931
  state.contentTypes[uid].status = 'REMOVED';
559
932
  }
933
+ Object.values(state.contentStructure.sections).forEach((section)=>{
934
+ removeContentTypeChild(section.groups, uid);
935
+ });
560
936
  }
561
937
  break;
562
938
  }
@@ -575,12 +951,14 @@ const slice = createUndoRedoSlice({
575
951
  }
576
952
  return {
577
953
  components: state.components,
578
- contentTypes: state.contentTypes
954
+ contentTypes: state.contentTypes,
955
+ contentStructure: state.contentStructure
579
956
  };
580
957
  },
581
958
  discard: (state)=>{
582
959
  state.components = state.initialComponents;
583
960
  state.contentTypes = state.initialContentTypes;
961
+ state.contentStructure = state.initialContentStructure;
584
962
  }
585
963
  });
586
964
  const { reducer, actions } = slice;