slice-machine-ui 1.23.2-beta.1 → 1.23.2-beta.11

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 (164) hide show
  1. package/.depcheckrc +3 -0
  2. package/components/ChangesEmptyState/OfflinePage.tsx +1 -1
  3. package/components/ChangesItems/ChangesItems.tsx +6 -2
  4. package/components/CustomTypeTable/changesPage.tsx +12 -7
  5. package/components/DeleteSliceModal/index.tsx +37 -24
  6. package/components/DeleteVariationModal/index.tsx +1 -0
  7. package/components/Forms/CreateCustomTypeModal/CreateCustomTypeModal.tsx +65 -38
  8. package/components/Forms/CreateSliceModal/CreateSliceModal.tsx +3 -1
  9. package/components/Forms/RenameCustomTypeModal/RenameCustomTypeModal.tsx +7 -2
  10. package/components/Forms/RenameSliceModal/RenameSliceModal.tsx +36 -21
  11. package/components/Forms/RenameVariationModal/RenameVariationModal.tsx +0 -1
  12. package/components/LoginModal/index.tsx +40 -2
  13. package/components/ModalFormCard/index.tsx +0 -1
  14. package/components/Navigation/ChangesItem.tsx +117 -0
  15. package/components/Navigation/Environment.tsx +51 -4
  16. package/components/Navigation/index.css.ts +1 -2
  17. package/components/Navigation/index.test.tsx +5 -85
  18. package/components/Navigation/index.tsx +32 -20
  19. package/components/ReviewModal/ReviewForm.tsx +0 -1
  20. package/lib/builders/SliceBuilder/index.tsx +3 -3
  21. package/lib/builders/common/EditModal/Form.jsx +0 -1
  22. package/lib/builders/common/Zone/Card/components/NewField.tsx +0 -1
  23. package/lib/models/common/ModelStatus/index.ts +25 -0
  24. package/lib/models/common/widgets/StructuredText/index.ts +1 -1
  25. package/out/404.html +1 -1
  26. package/out/_next/static/chunks/18539003-5f78714a35fde4a1.js +97 -0
  27. package/out/_next/static/chunks/25-3aa4d4b24167d340.js +1 -0
  28. package/out/_next/static/chunks/318-19dbcae10cdb6408.js +1 -0
  29. package/out/_next/static/chunks/727-de03055e40815a3a.js +1 -0
  30. package/out/_next/static/chunks/777-8c5507e3b119b5dc.js +8 -0
  31. package/out/_next/static/chunks/799-0af1215eb2b3c17e.js +1 -0
  32. package/out/_next/static/chunks/904-40686b0426716111.js +1 -0
  33. package/out/_next/static/chunks/pages/_app-bca191df4ac7078e.js +434 -0
  34. package/out/_next/static/chunks/pages/{changelog-afd18276fe018b24.js → changelog-cb1df3a70b20f1b8.js} +1 -1
  35. package/out/_next/static/chunks/pages/changes-ccfd715c2bdc4f83.js +1 -0
  36. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-3f862ad6111a1fc0.js → [customTypeId]-97ab3b90b85d645d.js} +1 -1
  37. package/out/_next/static/chunks/pages/{custom-types-94b8f8dbf7727412.js → custom-types-3c8e721ec6802127.js} +1 -1
  38. package/out/_next/static/chunks/pages/index-cf93d16e9c63b6db.js +1 -0
  39. package/out/_next/static/chunks/pages/{labs-ee206005948f8e87.js → labs-2adfb5127f9c4081.js} +1 -1
  40. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-d1ffca11c42f2603.js → [pageTypeId]-84a22c5e480d3fa9.js} +1 -1
  41. package/out/_next/static/chunks/pages/settings-94bcd7422e8b237d.js +1 -0
  42. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/{simulator-897ca93c114a1435.js → simulator-aaf65fbec8f5839c.js} +1 -1
  43. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-90bb2ea76d2abf42.js +1 -0
  44. package/out/_next/static/chunks/pages/slices-e0ce615c38e8338d.js +1 -0
  45. package/out/_next/static/css/39e089aad1742a3a.css +1 -0
  46. package/out/_next/static/css/{37153180c3b0dd66.css → 838fd8bb5a7c32b0.css} +1 -1
  47. package/out/_next/static/css/ca32882a5d2d8d88.css +1 -0
  48. package/out/_next/static/dS_xZnzzna6Y6uwcTgf90/_buildManifest.js +1 -0
  49. package/out/changelog.html +1 -1
  50. package/out/changes.html +1 -1
  51. package/out/custom-types/[customTypeId].html +1 -1
  52. package/out/custom-types.html +1 -1
  53. package/out/index.html +1 -1
  54. package/out/labs.html +1 -1
  55. package/out/page-types/[pageTypeId].html +1 -1
  56. package/out/settings.html +1 -0
  57. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  58. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  59. package/out/slices.html +1 -1
  60. package/package.json +9 -6
  61. package/pages/_app.tsx +34 -32
  62. package/pages/changes.tsx +27 -41
  63. package/pages/settings.tsx +1 -0
  64. package/pages/slices.tsx +20 -8
  65. package/postcss.config.js +10 -0
  66. package/src/apiClient.ts +1 -22
  67. package/src/components/FieldSet/FieldSet.stories.tsx +20 -11
  68. package/src/components/RelativeTime/RelativeTime.stories.tsx +25 -0
  69. package/src/components/RelativeTime/RelativeTime.tsx +40 -0
  70. package/src/components/RelativeTime/index.ts +1 -0
  71. package/src/components/SideNav/SideNav.css.ts +1 -1
  72. package/src/components/SideNav/SideNav.tsx +9 -5
  73. package/src/components/SideNav/SideNavEnvironmentSelector.tsx +31 -11
  74. package/src/domain/__tests__/customType.test.ts +205 -19
  75. package/src/domain/customType.ts +132 -7
  76. package/src/domain/slice.ts +38 -0
  77. package/src/features/autoSave/AutoSaveStatusIndicator.tsx +4 -4
  78. package/src/features/customTypes/PageTypeOnboarding.tsx +21 -26
  79. package/src/features/customTypes/actions/convertCustomToPageType.ts +14 -10
  80. package/src/features/customTypes/actions/createCustomType.ts +69 -0
  81. package/src/features/customTypes/actions/deleteCustomType.ts +13 -3
  82. package/src/features/customTypes/actions/renameCustomType.ts +10 -4
  83. package/src/features/customTypes/customTypesBuilder/CustomTypeProvider.tsx +11 -10
  84. package/src/features/customTypes/customTypesBuilder/CustomTypesBuilderPage.tsx +5 -3
  85. package/src/features/customTypes/customTypesTable/CustomTypesTable.tsx +8 -8
  86. package/src/features/customTypes/customTypesTable/CustomTypesTablePage.tsx +18 -17
  87. package/src/features/environments/actions/setEnvironment.ts +4 -4
  88. package/src/features/settings/SettingsPage.tsx +47 -0
  89. package/src/features/settings/git/ConnectGitRepository.tsx +126 -0
  90. package/src/features/settings/git/ConnectGitRepositoryBlankSlate.tsx +33 -0
  91. package/src/features/settings/git/GitOwnerSelect.tsx +71 -0
  92. package/src/features/settings/git/GitProvider.ts +40 -0
  93. package/src/features/settings/git/GitProviderConnectButtons.tsx +63 -0
  94. package/src/features/settings/git/GitRepositoriesList.tsx +76 -0
  95. package/src/features/settings/git/GitRepositoriesSearch.tsx +69 -0
  96. package/src/features/settings/git/GitRepositoryConnectDialog.tsx +85 -0
  97. package/src/features/settings/git/GitRepositoryDisconnectDialog.tsx +60 -0
  98. package/src/features/settings/git/useGitIntegrationExperiment.ts +8 -0
  99. package/src/features/settings/git/useGitOwners.ts +12 -0
  100. package/src/features/settings/git/useGitRepos.ts +24 -0
  101. package/src/features/settings/git/useLinkedGitRepos.ts +41 -0
  102. package/src/features/settings/git/useWriteAPIToken.ts +23 -0
  103. package/src/features/slices/actions/createSlice.ts +5 -29
  104. package/src/features/slices/actions/deleteSlice.ts +33 -0
  105. package/src/features/slices/actions/renameSlice.ts +37 -0
  106. package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewSliceDialog.tsx +0 -1
  107. package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewVariationDialog.tsx +0 -1
  108. package/src/features/slices/convertLegacySlice/ConvertLegacySliceMergeWithIdenticalDialog.tsx +0 -1
  109. package/src/features/slices/sliceBuilder/SliceBuilderProvider.tsx +12 -10
  110. package/src/features/sync/AutoSyncProvider.tsx +252 -0
  111. package/src/features/sync/components/AutoSyncStatusIndicator.module.css +17 -0
  112. package/src/features/sync/components/AutoSyncStatusIndicator.module.css.d.ts +5 -0
  113. package/src/features/sync/components/AutoSyncStatusIndicator.tsx +58 -0
  114. package/src/features/sync/fetchUnSyncChanges.ts +40 -0
  115. package/src/features/sync/getUnSyncChanges.ts +144 -0
  116. package/src/features/sync/useUnSyncChanges.ts +32 -0
  117. package/src/hooks/useActionQueue.tsx +177 -0
  118. package/src/hooks/useAuthStatus.ts +12 -0
  119. package/src/hooks/useUser.ts +14 -0
  120. package/src/icons/SettingsIcon.tsx +19 -0
  121. package/src/icons/SyncFailed.tsx +30 -0
  122. package/src/icons/Synced.tsx +19 -0
  123. package/src/icons/Syncing.tsx +23 -0
  124. package/src/modules/availableCustomTypes/index.ts +29 -135
  125. package/src/modules/loading/types.ts +0 -5
  126. package/src/modules/modal/types.ts +0 -3
  127. package/src/modules/slices/index.ts +27 -193
  128. package/src/modules/slices/selector.ts +0 -14
  129. package/src/modules/useSliceMachineActions.ts +70 -89
  130. package/src/modules/userContext/index.ts +3 -3
  131. package/src/redux/saga.ts +0 -4
  132. package/test/__testutils__/index.tsx +8 -5
  133. package/test/src/modules/__fixtures__/serverState.ts +1 -2
  134. package/test/src/modules/availableCustomTypes/index.test.ts +2 -243
  135. package/test/src/modules/slices.test.ts +1 -267
  136. package/components/Navigation/ChangesListItem.tsx +0 -92
  137. package/components/Navigation/ChangesRightElement.tsx +0 -33
  138. package/out/_next/static/chunks/18539003-5cc559566b738915.js +0 -97
  139. package/out/_next/static/chunks/25-37d6fa7f064be04b.js +0 -1
  140. package/out/_next/static/chunks/318-ce7e74f18f2d5b22.js +0 -1
  141. package/out/_next/static/chunks/376-7e784d6069e01654.js +0 -8
  142. package/out/_next/static/chunks/727-3a3bc32518778317.js +0 -1
  143. package/out/_next/static/chunks/731-501990ebe6a71b32.js +0 -1
  144. package/out/_next/static/chunks/904-9a4c1264727ae89a.js +0 -1
  145. package/out/_next/static/chunks/pages/_app-cd9d4c76a4abc11a.js +0 -432
  146. package/out/_next/static/chunks/pages/changes-2a976f9efeb719aa.js +0 -1
  147. package/out/_next/static/chunks/pages/index-64cfb8b0948ba8ef.js +0 -1
  148. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-b2a9d773ce5d7739.js +0 -1
  149. package/out/_next/static/chunks/pages/slices-e9c70c082ca3e3f9.js +0 -1
  150. package/out/_next/static/css/200244e3e0463c39.css +0 -1
  151. package/out/_next/static/m6psnC5u1xT7Y6osWHKjp/_buildManifest.js +0 -1
  152. package/src/features/autoSave/useAutoSave.tsx +0 -177
  153. package/src/features/customTypes/__tests__/createCustomType.test.ts +0 -159
  154. package/src/features/customTypes/customTypesTable/createCustomType.ts +0 -107
  155. package/src/features/legacyState/actions/getLegacySliceMachineState.ts +0 -53
  156. package/src/features/legacyState/useLegacySliceMachineState.ts +0 -7
  157. package/src/hooks/useModelStatus.ts +0 -78
  158. package/src/hooks/useUnSyncChanges.ts +0 -88
  159. package/test/components/CreateCustomTypeModal.test.tsx +0 -73
  160. package/test/components/LoginModal.test.tsx +0 -67
  161. package/test/src/hooks/useModelStatus.test.ts +0 -115
  162. /package/out/_next/static/{m6psnC5u1xT7Y6osWHKjp → dS_xZnzzna6Y6uwcTgf90}/_ssgManifest.js +0 -0
  163. /package/src/features/{changes → sync}/actions/pushChanges.ts +0 -0
  164. /package/src/features/{changes → sync}/actions/trackPushChangesSuccess.ts +0 -0
@@ -193,11 +193,14 @@ export const WithHeaderAndBoxContent = {
193
193
  <OwnerSelect />
194
194
  </FieldSetHeader>
195
195
  <FieldSetContent>
196
- {/*
197
- * TODO: these `padding` values actually don't match Figma, but they
198
- * are the closest allowed by the `Box` component.
199
- */}
200
- <Box flexDirection="column" padding={{ block: 72, inline: 100 }}>
196
+ <Box
197
+ flexDirection="column"
198
+ /*
199
+ * TODO: these `padding` values actually don't match Figma, but they
200
+ * are the closest allowed by the `Box` component.
201
+ */
202
+ padding={{ block: 72, inline: 100 }}
203
+ >
201
204
  <Text align="center" variant="emphasized">
202
205
  No Results Found
203
206
  </Text>
@@ -230,12 +233,18 @@ function OwnerSelect(props: OwnerSelectProps) {
230
233
  size="large"
231
234
  sx={{ width: "calc(50% - 8px)" }}
232
235
  >
233
- <SelectItem size="large" value="owner-1">
234
- Owner
235
- </SelectItem>
236
- <SelectItem size="large" value="owner-2">
237
- Owner
238
- </SelectItem>
236
+ {[...Array(2).keys()].map((index) => (
237
+ <SelectItem
238
+ key={index}
239
+ renderStartIcon={() => (
240
+ <GitHubIcon color={tokens.color.greyLight11} />
241
+ )}
242
+ size="large"
243
+ value={`owner-${index}`}
244
+ >
245
+ Owner
246
+ </SelectItem>
247
+ ))}
239
248
  </Select>
240
249
  );
241
250
  }
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import { RelativeTime } from "./RelativeTime";
4
+
5
+ type Story = StoryObj<typeof meta>;
6
+
7
+ const meta = {
8
+ component: RelativeTime,
9
+ argTypes: {
10
+ date: { control: "date" },
11
+ },
12
+ } satisfies Meta<typeof RelativeTime>;
13
+
14
+ export default meta;
15
+
16
+ export const Default = {
17
+ args: {
18
+ date: new Date("1 Jan 2024"),
19
+ },
20
+ render: ({ date, ...otherArgs }) => (
21
+ // The `date` control converts the `date` arg into a UNIX timestamp. That's
22
+ // why we need to convert it back into a `Date` object.
23
+ <RelativeTime {...otherArgs} date={new Date(date)} />
24
+ ),
25
+ } satisfies Story;
@@ -0,0 +1,40 @@
1
+ import { Text } from "@prismicio/editor-ui";
2
+ import type { FC } from "react";
3
+
4
+ type RelativeTimeProps = {
5
+ date: Date;
6
+ };
7
+
8
+ export const RelativeTime: FC<RelativeTimeProps> = ({
9
+ date,
10
+ ...otherProps
11
+ }) => (
12
+ <Text {...otherProps} color="inherit" variant="inherit">
13
+ {formatRelativeTime(date)}
14
+ </Text>
15
+ );
16
+
17
+ const rtf = new Intl.RelativeTimeFormat("en", {
18
+ numeric: "auto",
19
+ style: "narrow",
20
+ });
21
+
22
+ function formatRelativeTime(date: Date, now = new Date()): string {
23
+ return rtf.format(differenceInCalendarDays(date, now), "day");
24
+ }
25
+
26
+ const millisecondsInDay = 1_000 * 60 * 60 * 24;
27
+
28
+ // Adapted from: https://github.com/date-fns/date-fns/blob/6f44a167e71053999c10aa9462d7f0c52fec0faa/src/differenceInCalendarDays/index.ts.
29
+ function differenceInCalendarDays(dateLeft: Date, dateRight: Date): number {
30
+ const startOfDayLeft = startOfDay(dateLeft);
31
+ const startOfDayRight = startOfDay(dateRight);
32
+ return Math.round((+startOfDayLeft - +startOfDayRight) / millisecondsInDay);
33
+ }
34
+
35
+ // Adapted from: https://github.com/date-fns/date-fns/blob/6f44a167e71053999c10aa9462d7f0c52fec0faa/src/startOfDay/index.ts.
36
+ function startOfDay(date: Date): Date {
37
+ const newDate = new Date(date);
38
+ newDate.setHours(0, 0, 0, 0);
39
+ return newDate;
40
+ }
@@ -0,0 +1 @@
1
+ export { RelativeTime } from "./RelativeTime";
@@ -54,7 +54,7 @@ export const repository = style([
54
54
  sprinkles({
55
55
  display: "flex",
56
56
  justifyContent: "space-between",
57
- marginBottom: 32,
57
+ marginBottom: 16,
58
58
  }),
59
59
  ]);
60
60
 
@@ -173,10 +173,14 @@ export const UpdateInfo: FC<UpdateInfoProps> = ({
173
173
  Some updates of Slice Machine are available.
174
174
  </p>
175
175
 
176
- {createElement(
177
- component,
178
- { ...{ className: styles.updateInfoLink, onClick }, href },
179
- "Learn more",
180
- )}
176
+ {
177
+ // TODO(DT-1942): This should be a Button with a link component for
178
+ // accessibility
179
+ createElement(
180
+ component,
181
+ { ...{ className: styles.updateInfoLink, onClick }, href },
182
+ "Learn more",
183
+ )
184
+ }
181
185
  </div>
182
186
  );
@@ -8,6 +8,7 @@ import {
8
8
  Icon,
9
9
  IconButton,
10
10
  InvisibleButton,
11
+ ProgressCircle,
11
12
  Text,
12
13
  } from "@prismicio/editor-ui";
13
14
  import { Environment } from "@slicemachine/manager/client";
@@ -21,22 +22,26 @@ import * as styles from "./SideNavEnvironmentSelector.css";
21
22
  import { LoginIcon } from "@src/icons/LoginIcon";
22
23
 
23
24
  type SideNavEnvironmentSelectorProps = {
24
- variant?: "default" | "offline" | "unauthorized" | "unauthenticated";
25
- environments?: Environment[];
26
25
  activeEnvironment?: Environment;
27
- onSelect?: (environment: Environment) => void | Promise<void>;
26
+ disabled?: boolean;
27
+ environments?: Environment[];
28
+ loading?: boolean;
29
+ variant?: "default" | "offline" | "unauthorized" | "unauthenticated";
28
30
  onLogInClick?: () => void;
31
+ onSelect?: (environment: Environment) => void | Promise<void>;
29
32
  };
30
33
 
31
34
  export const SideNavEnvironmentSelector: FC<SideNavEnvironmentSelectorProps> = (
32
35
  props,
33
36
  ) => {
34
37
  const {
35
- variant = "default",
36
- environments = [],
37
38
  activeEnvironment,
38
- onSelect,
39
+ disabled = false,
40
+ environments = [],
41
+ loading = false,
42
+ variant = "default",
39
43
  onLogInClick,
44
+ onSelect,
40
45
  } = props;
41
46
 
42
47
  const isProductionEnvironmentActive = activeEnvironment?.kind === "prod";
@@ -60,7 +65,7 @@ export const SideNavEnvironmentSelector: FC<SideNavEnvironmentSelectorProps> = (
60
65
  overflow="hidden"
61
66
  alignItems="flex-start"
62
67
  >
63
- {variant === "default" || variant === "unauthenticated" ? (
68
+ {variant === "default" ? (
64
69
  <Text component="span" variant="small" color="grey11">
65
70
  Environment
66
71
  </Text>
@@ -70,6 +75,12 @@ export const SideNavEnvironmentSelector: FC<SideNavEnvironmentSelectorProps> = (
70
75
  <InvisibleButton buttonText="Login required" onClick={onLogInClick} />
71
76
  ) : undefined}
72
77
 
78
+ {variant === "offline" ? (
79
+ <Text component="span" variant="bold">
80
+ Offline
81
+ </Text>
82
+ ) : undefined}
83
+
73
84
  {variant === "default" ? (
74
85
  <Text
75
86
  component="span"
@@ -93,8 +104,10 @@ export const SideNavEnvironmentSelector: FC<SideNavEnvironmentSelectorProps> = (
93
104
 
94
105
  {environments.length > 1 ? (
95
106
  <EnvironmentDropdownMenu
96
- environments={environments}
97
107
  activeEnvironment={activeEnvironment}
108
+ disabled={disabled}
109
+ environments={environments}
110
+ loading={loading}
98
111
  onSelect={onSelect}
99
112
  />
100
113
  ) : undefined}
@@ -107,11 +120,14 @@ type EnvironmentDropdownMenuProps = Pick<
107
120
  SideNavEnvironmentSelectorProps,
108
121
  "activeEnvironment" | "onSelect"
109
122
  > & {
123
+ disabled: boolean;
110
124
  environments: Environment[];
125
+ loading: boolean;
111
126
  };
112
127
 
113
128
  const EnvironmentDropdownMenu: FC<EnvironmentDropdownMenuProps> = (props) => {
114
- const { environments, activeEnvironment, onSelect } = props;
129
+ const { activeEnvironment, disabled, environments, loading, onSelect } =
130
+ props;
115
131
 
116
132
  const nonPersonalEnvironments = environments.filter(
117
133
  (environment) => environment.kind !== "dev",
@@ -122,8 +138,12 @@ const EnvironmentDropdownMenu: FC<EnvironmentDropdownMenuProps> = (props) => {
122
138
 
123
139
  return (
124
140
  <DropdownMenu modal>
125
- <DropdownMenuTrigger disabled={environments.length < 2}>
126
- <IconButton icon="unfoldMore" hiddenLabel="Select environment" />
141
+ <DropdownMenuTrigger disabled={disabled}>
142
+ <IconButton
143
+ icon={loading ? <ProgressCircle /> : "unfoldMore"}
144
+ hiddenLabel="Select environment"
145
+ disabled={disabled}
146
+ />
127
147
  </DropdownMenuTrigger>
128
148
  <DropdownMenuContent align="end" minWidth={256}>
129
149
  {personalEnvironment ? (
@@ -393,32 +393,39 @@ describe("CustomTypeModel test suite", () => {
393
393
 
394
394
  it("addField should return the given custom type with the field added to the section", () => {
395
395
  expect(
396
- CustomTypeModel.addField({
397
- customType: mockCustomType,
398
- sectionId: "mainSection",
399
- newFieldId: "newField",
400
- newField: {
401
- config: {
402
- label: "NewField",
403
- },
404
- type: "UID",
405
- },
406
- }),
407
- ).toEqual({
408
- ...mockCustomType,
409
- json: {
410
- ...mockCustomType.json,
411
- mainSection: {
412
- ...mainSection,
396
+ JSON.stringify(
397
+ CustomTypeModel.addField({
398
+ customType: mockCustomType,
399
+ sectionId: "mainSection",
400
+ newFieldId: "newField",
413
401
  newField: {
414
402
  config: {
415
403
  label: "NewField",
416
404
  },
417
405
  type: "UID",
418
406
  },
407
+ }),
408
+ ),
409
+ ).toEqual(
410
+ JSON.stringify({
411
+ ...mockCustomType,
412
+ json: {
413
+ ...mockCustomType.json,
414
+ mainSection: {
415
+ uid: mainSection.uid,
416
+ booleanField: mainSection.booleanField,
417
+ groupField: mainSection.groupField,
418
+ newField: {
419
+ config: {
420
+ label: "NewField",
421
+ },
422
+ type: "UID",
423
+ },
424
+ slices: mainSection.slices,
425
+ },
419
426
  },
420
- },
421
- });
427
+ }),
428
+ );
422
429
  });
423
430
 
424
431
  it("deleteField should return the given custom type without the field", () => {
@@ -782,4 +789,183 @@ describe("CustomTypeModel test suite", () => {
782
789
  },
783
790
  });
784
791
  });
792
+
793
+ it("should create a custom type with repeatable true", () => {
794
+ expect(
795
+ CustomTypeModel.create({
796
+ id: "id",
797
+ label: "lama",
798
+ repeatable: true,
799
+ format: "custom",
800
+ }),
801
+ ).toMatchInlineSnapshot(`
802
+ {
803
+ "format": "custom",
804
+ "id": "id",
805
+ "json": {
806
+ "Main": {
807
+ "uid": {
808
+ "config": {
809
+ "label": "UID",
810
+ },
811
+ "type": "UID",
812
+ },
813
+ },
814
+ },
815
+ "label": "lama",
816
+ "repeatable": true,
817
+ "status": true,
818
+ }
819
+ `);
820
+ });
821
+
822
+ it("should create a custom type with repeatable false", () => {
823
+ const result = CustomTypeModel.create({
824
+ id: "id",
825
+ label: "lama",
826
+ repeatable: false,
827
+ format: "custom",
828
+ });
829
+
830
+ expect(result.id).toBe("id");
831
+ expect(result.label).toBe("lama");
832
+ expect(result.format).toBe("custom");
833
+ expect(result.json).toHaveProperty("Main");
834
+ expect(result).toMatchInlineSnapshot(`
835
+ {
836
+ "format": "custom",
837
+ "id": "id",
838
+ "json": {
839
+ "Main": {},
840
+ },
841
+ "label": "lama",
842
+ "repeatable": false,
843
+ "status": true,
844
+ }
845
+ `);
846
+ });
847
+
848
+ it("should create a page type with a slice zone", () => {
849
+ const result = CustomTypeModel.create({
850
+ id: "🥪",
851
+ label: "label",
852
+ repeatable: true,
853
+ format: "page",
854
+ });
855
+
856
+ expect(result.format).toBe("page");
857
+ expect(result.json).toHaveProperty("Main");
858
+ expect(result.json.Main).toHaveProperty("slices");
859
+ expect(result.json).toHaveProperty("SEO & Metadata");
860
+ expect(result).toMatchInlineSnapshot(`
861
+ {
862
+ "format": "page",
863
+ "id": "🥪",
864
+ "json": {
865
+ "Main": {
866
+ "slices": {
867
+ "config": {
868
+ "choices": {},
869
+ },
870
+ "fieldset": "Slice Zone",
871
+ "type": "Slices",
872
+ },
873
+ "uid": {
874
+ "config": {
875
+ "label": "UID",
876
+ },
877
+ "type": "UID",
878
+ },
879
+ },
880
+ "SEO & Metadata": {
881
+ "meta_description": {
882
+ "config": {
883
+ "label": "Meta Description",
884
+ "placeholder": "A brief summary of the page",
885
+ },
886
+ "type": "Text",
887
+ },
888
+ "meta_image": {
889
+ "config": {
890
+ "constraint": {
891
+ "height": 1260,
892
+ "width": 2400,
893
+ },
894
+ "label": "Meta Image",
895
+ "thumbnails": [],
896
+ },
897
+ "type": "Image",
898
+ },
899
+ "meta_title": {
900
+ "config": {
901
+ "label": "Meta Title",
902
+ "placeholder": "A title of the page used for social media and search engines",
903
+ },
904
+ "type": "Text",
905
+ },
906
+ },
907
+ },
908
+ "label": "label",
909
+ "repeatable": true,
910
+ "status": true,
911
+ }
912
+ `);
913
+ });
914
+ });
915
+
916
+ it("when non repeatable page type is should contain Main with a slice-zone, no uid, and a SEO tab", () => {
917
+ const result = CustomTypeModel.create({
918
+ id: "foo",
919
+ label: "bar",
920
+ repeatable: false,
921
+ format: "page",
922
+ });
923
+
924
+ expect(result).toMatchInlineSnapshot(`
925
+ {
926
+ "format": "page",
927
+ "id": "foo",
928
+ "json": {
929
+ "Main": {
930
+ "slices": {
931
+ "config": {
932
+ "choices": {},
933
+ },
934
+ "fieldset": "Slice Zone",
935
+ "type": "Slices",
936
+ },
937
+ },
938
+ "SEO & Metadata": {
939
+ "meta_description": {
940
+ "config": {
941
+ "label": "Meta Description",
942
+ "placeholder": "A brief summary of the page",
943
+ },
944
+ "type": "Text",
945
+ },
946
+ "meta_image": {
947
+ "config": {
948
+ "constraint": {
949
+ "height": 1260,
950
+ "width": 2400,
951
+ },
952
+ "label": "Meta Image",
953
+ "thumbnails": [],
954
+ },
955
+ "type": "Image",
956
+ },
957
+ "meta_title": {
958
+ "config": {
959
+ "label": "Meta Title",
960
+ "placeholder": "A title of the page used for social media and search engines",
961
+ },
962
+ "type": "Text",
963
+ },
964
+ },
965
+ },
966
+ "label": "bar",
967
+ "repeatable": false,
968
+ "status": true,
969
+ }
970
+ `);
785
971
  });
@@ -104,8 +104,15 @@ type GetGroupFieldArgs = {
104
104
  groupFieldId: string;
105
105
  };
106
106
 
107
- export function getFormat(custom: CustomType): CustomTypeFormat {
108
- return custom.format ?? "custom";
107
+ type CreateArgs = {
108
+ format: CustomTypeFormat;
109
+ id: string;
110
+ label: string;
111
+ repeatable: boolean;
112
+ };
113
+
114
+ export function getFormat(customType: CustomType): CustomTypeFormat {
115
+ return customType.format ?? "custom";
109
116
  }
110
117
 
111
118
  export function getSectionEntries(
@@ -363,15 +370,32 @@ export function renameSection(
363
370
 
364
371
  export function addField(args: AddFieldArgs): CustomType {
365
372
  const { customType, sectionId, newField, newFieldId } = args;
373
+ const sectionJson = customType.json[sectionId];
374
+ const maybeSliceZoneEntry = getSectionSliceZoneEntry(customType, sectionId);
375
+
376
+ // Separate the fields into slices and non-slices
377
+ const sectionFields = Object.fromEntries(
378
+ Object.entries(sectionJson).filter(
379
+ ([_, value]) => value.type !== SlicesFieldType,
380
+ ),
381
+ );
382
+
383
+ const updatedSection = {
384
+ ...sectionFields,
385
+ // Add the new field to the section
386
+ [newFieldId]: newField,
387
+ };
388
+
389
+ // Merge the SlicesFieldType field back into the section, if it exists
390
+ if (maybeSliceZoneEntry !== undefined) {
391
+ const [sliceZoneKey, sliceZoneField] = maybeSliceZoneEntry;
392
+ updatedSection[sliceZoneKey] = sliceZoneField;
393
+ }
366
394
 
367
395
  const newCustomType = updateSection({
368
396
  customType,
369
397
  sectionId,
370
- updatedSection: {
371
- ...customType.json[sectionId],
372
- // Add the new field to the section
373
- [newFieldId]: newField,
374
- },
398
+ updatedSection,
375
399
  });
376
400
 
377
401
  return newCustomType;
@@ -633,3 +657,104 @@ export function updateGroupFields(
633
657
  },
634
658
  };
635
659
  }
660
+
661
+ export function create(args: CreateArgs) {
662
+ const { id, label, repeatable, format } = args;
663
+ const mainTab = makeMainTab(repeatable, format);
664
+
665
+ return {
666
+ format,
667
+ id,
668
+ json: mainTab,
669
+ label,
670
+ repeatable,
671
+ status: true,
672
+ };
673
+ }
674
+
675
+ function makeMainTab(
676
+ repeatable: boolean,
677
+ format: CustomTypeFormat,
678
+ ): CustomType["json"] {
679
+ if (repeatable === false && format === "page") {
680
+ return { ...DEFAULT_MAIN_WITH_SLICE_ZONE, ...DEFAULT_SEO_TAB };
681
+ }
682
+
683
+ if (repeatable === false) {
684
+ return DEFAULT_MAIN;
685
+ }
686
+
687
+ if (format === "page") {
688
+ return {
689
+ ...DEFAULT_MAIN_WITH_UID_AND_SLICE_ZONE,
690
+ ...DEFAULT_SEO_TAB,
691
+ };
692
+ }
693
+
694
+ return DEFAULT_MAIN_WITH_UID;
695
+ }
696
+
697
+ const DEFAULT_MAIN: CustomType["json"] = {
698
+ Main: {},
699
+ };
700
+
701
+ const DEFAULT_MAIN_WITH_SLICE_ZONE: CustomType["json"] = {
702
+ Main: {
703
+ slices: {
704
+ config: {
705
+ choices: {},
706
+ },
707
+ fieldset: "Slice Zone",
708
+ type: "Slices",
709
+ },
710
+ },
711
+ };
712
+
713
+ const DEFAULT_MAIN_WITH_UID: CustomType["json"] = {
714
+ Main: {
715
+ uid: {
716
+ config: {
717
+ label: "UID",
718
+ },
719
+ type: "UID",
720
+ },
721
+ },
722
+ };
723
+
724
+ const DEFAULT_MAIN_WITH_UID_AND_SLICE_ZONE: CustomType["json"] = {
725
+ Main: {
726
+ ...DEFAULT_MAIN_WITH_UID.Main,
727
+ ...DEFAULT_MAIN_WITH_SLICE_ZONE.Main,
728
+ },
729
+ };
730
+
731
+ const DEFAULT_SEO_TAB: CustomType["json"] = {
732
+ "SEO & Metadata": {
733
+ meta_description: {
734
+ config: {
735
+ label: "Meta Description",
736
+ placeholder: "A brief summary of the page",
737
+ },
738
+ type: "Text",
739
+ },
740
+ meta_image: {
741
+ config: {
742
+ constraint: {
743
+ height: 1260,
744
+ width: 2400,
745
+ },
746
+ label: "Meta Image",
747
+ thumbnails: [],
748
+ },
749
+ type: "Image",
750
+ },
751
+ meta_title: {
752
+ config: {
753
+ label: "Meta Title",
754
+ placeholder:
755
+ "A title of the page used for social media and search engines",
756
+ },
757
+ type: "Text",
758
+ },
759
+ },
760
+ };