slice-machine-ui 1.23.2-beta.8 → 1.24.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 (132) 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 +3 -0
  6. package/components/DeleteVariationModal/index.tsx +1 -0
  7. package/components/Forms/CreateCustomTypeModal/CreateCustomTypeModal.tsx +4 -0
  8. package/components/Forms/CreateSliceModal/CreateSliceModal.tsx +3 -1
  9. package/components/Forms/RenameCustomTypeModal/RenameCustomTypeModal.tsx +6 -1
  10. package/components/Forms/RenameSliceModal/RenameSliceModal.tsx +3 -0
  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/out/404.html +1 -1
  25. package/out/_next/static/chunks/18539003-5f78714a35fde4a1.js +97 -0
  26. package/out/_next/static/chunks/25-3aa4d4b24167d340.js +1 -0
  27. package/out/_next/static/chunks/318-19dbcae10cdb6408.js +1 -0
  28. package/out/_next/static/chunks/727-de03055e40815a3a.js +1 -0
  29. package/out/_next/static/chunks/777-8c5507e3b119b5dc.js +8 -0
  30. package/out/_next/static/chunks/799-0af1215eb2b3c17e.js +1 -0
  31. package/out/_next/static/chunks/904-40686b0426716111.js +1 -0
  32. package/out/_next/static/chunks/pages/_app-bca191df4ac7078e.js +434 -0
  33. package/out/_next/static/chunks/pages/{changelog-afd18276fe018b24.js → changelog-cb1df3a70b20f1b8.js} +1 -1
  34. package/out/_next/static/chunks/pages/changes-ccfd715c2bdc4f83.js +1 -0
  35. package/out/_next/static/chunks/pages/{labs-ee206005948f8e87.js → labs-2adfb5127f9c4081.js} +1 -1
  36. package/out/_next/static/chunks/pages/settings-94bcd7422e8b237d.js +1 -0
  37. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/{simulator-52c3e1f9d14160b8.js → simulator-aaf65fbec8f5839c.js} +1 -1
  38. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-90bb2ea76d2abf42.js +1 -0
  39. package/out/_next/static/chunks/pages/slices-e0ce615c38e8338d.js +1 -0
  40. package/out/_next/static/css/39e089aad1742a3a.css +1 -0
  41. package/out/_next/static/css/{37153180c3b0dd66.css → 838fd8bb5a7c32b0.css} +1 -1
  42. package/out/_next/static/css/ca32882a5d2d8d88.css +1 -0
  43. package/out/_next/static/eNujlD43QIdmra7ggcNtK/_buildManifest.js +1 -0
  44. package/out/changelog.html +1 -1
  45. package/out/changes.html +1 -1
  46. package/out/custom-types/[customTypeId].html +1 -1
  47. package/out/custom-types.html +1 -1
  48. package/out/index.html +1 -1
  49. package/out/labs.html +1 -1
  50. package/out/page-types/[pageTypeId].html +1 -1
  51. package/out/settings.html +1 -0
  52. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  53. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  54. package/out/slices.html +1 -1
  55. package/package.json +10 -8
  56. package/pages/_app.tsx +34 -32
  57. package/pages/changes.tsx +27 -41
  58. package/pages/settings.tsx +1 -0
  59. package/postcss.config.js +10 -0
  60. package/src/components/FieldSet/FieldSet.stories.tsx +20 -11
  61. package/src/components/RelativeTime/RelativeTime.stories.tsx +25 -0
  62. package/src/components/RelativeTime/RelativeTime.tsx +40 -0
  63. package/src/components/RelativeTime/index.ts +1 -0
  64. package/src/components/SideNav/SideNav.css.ts +1 -1
  65. package/src/components/SideNav/SideNav.tsx +9 -5
  66. package/src/components/SideNav/SideNavEnvironmentSelector.tsx +31 -11
  67. package/src/domain/__tests__/customType.test.ts +26 -19
  68. package/src/domain/customType.ts +22 -5
  69. package/src/features/autoSave/AutoSaveStatusIndicator.tsx +4 -4
  70. package/src/features/customTypes/customTypesBuilder/CustomTypeProvider.tsx +11 -10
  71. package/src/features/customTypes/customTypesBuilder/CustomTypesBuilderPage.tsx +3 -3
  72. package/src/features/environments/actions/setEnvironment.ts +4 -4
  73. package/src/features/settings/SettingsPage.tsx +47 -0
  74. package/src/features/settings/git/ConnectGitRepository.tsx +126 -0
  75. package/src/features/settings/git/ConnectGitRepositoryBlankSlate.tsx +33 -0
  76. package/src/features/settings/git/GitOwnerSelect.tsx +71 -0
  77. package/src/features/settings/git/GitProvider.ts +40 -0
  78. package/src/features/settings/git/GitProviderConnectButtons.tsx +63 -0
  79. package/src/features/settings/git/GitRepositoriesList.tsx +76 -0
  80. package/src/features/settings/git/GitRepositoriesSearch.tsx +69 -0
  81. package/src/features/settings/git/GitRepositoryConnectDialog.tsx +85 -0
  82. package/src/features/settings/git/GitRepositoryDisconnectDialog.tsx +60 -0
  83. package/src/features/settings/git/useGitIntegrationExperiment.ts +8 -0
  84. package/src/features/settings/git/useGitOwners.ts +12 -0
  85. package/src/features/settings/git/useGitRepos.ts +24 -0
  86. package/src/features/settings/git/useLinkedGitRepos.ts +41 -0
  87. package/src/features/settings/git/useWriteAPIToken.ts +23 -0
  88. package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewSliceDialog.tsx +0 -1
  89. package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewVariationDialog.tsx +0 -1
  90. package/src/features/slices/convertLegacySlice/ConvertLegacySliceMergeWithIdenticalDialog.tsx +0 -1
  91. package/src/features/slices/sliceBuilder/SliceBuilderProvider.tsx +12 -10
  92. package/src/features/sync/AutoSyncProvider.tsx +252 -0
  93. package/src/features/sync/components/AutoSyncStatusIndicator.module.css +17 -0
  94. package/src/features/sync/components/AutoSyncStatusIndicator.module.css.d.ts +5 -0
  95. package/src/features/sync/components/AutoSyncStatusIndicator.tsx +58 -0
  96. package/src/features/sync/fetchUnSyncChanges.ts +40 -0
  97. package/src/features/sync/getUnSyncChanges.ts +144 -0
  98. package/src/features/sync/useUnSyncChanges.ts +32 -0
  99. package/src/hooks/useActionQueue.tsx +177 -0
  100. package/src/hooks/useAuthStatus.ts +12 -0
  101. package/src/hooks/useUser.ts +14 -0
  102. package/src/icons/SettingsIcon.tsx +19 -0
  103. package/src/icons/SyncFailed.tsx +30 -0
  104. package/src/icons/Synced.tsx +19 -0
  105. package/src/icons/Syncing.tsx +23 -0
  106. package/test/__testutils__/index.tsx +8 -5
  107. package/test/src/modules/__fixtures__/serverState.ts +1 -2
  108. package/components/Navigation/ChangesListItem.tsx +0 -74
  109. package/components/Navigation/ChangesRightElement.tsx +0 -33
  110. package/out/_next/static/chunks/18539003-5cc559566b738915.js +0 -97
  111. package/out/_next/static/chunks/25-01f422cea8066349.js +0 -1
  112. package/out/_next/static/chunks/318-387e3f23d717c53d.js +0 -1
  113. package/out/_next/static/chunks/376-7e784d6069e01654.js +0 -8
  114. package/out/_next/static/chunks/727-358d7fae68a880b3.js +0 -1
  115. package/out/_next/static/chunks/799-d95a93e965557aeb.js +0 -1
  116. package/out/_next/static/chunks/904-1ff7519adece2b55.js +0 -1
  117. package/out/_next/static/chunks/pages/_app-e31adcf75e9176d3.js +0 -432
  118. package/out/_next/static/chunks/pages/changes-92c84f6cac0285fd.js +0 -1
  119. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-608d206d10a719e3.js +0 -1
  120. package/out/_next/static/chunks/pages/slices-b0b58192a87df822.js +0 -1
  121. package/out/_next/static/css/200244e3e0463c39.css +0 -1
  122. package/out/_next/static/vxOhh_oqUIQ5PTSKUKgMR/_buildManifest.js +0 -1
  123. package/src/features/autoSave/useAutoSave.tsx +0 -177
  124. package/src/features/legacyState/actions/getLegacySliceMachineState.ts +0 -53
  125. package/src/features/legacyState/useLegacySliceMachineState.ts +0 -7
  126. package/src/hooks/useModelStatus.ts +0 -78
  127. package/src/hooks/useUnSyncChanges.ts +0 -88
  128. package/test/components/LoginModal.test.tsx +0 -67
  129. package/test/src/hooks/useModelStatus.test.ts +0 -115
  130. /package/out/_next/static/{vxOhh_oqUIQ5PTSKUKgMR → eNujlD43QIdmra7ggcNtK}/_ssgManifest.js +0 -0
  131. /package/src/features/{changes → sync}/actions/pushChanges.ts +0 -0
  132. /package/src/features/{changes → sync}/actions/trackPushChangesSuccess.ts +0 -0
@@ -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", () => {
@@ -370,15 +370,32 @@ export function renameSection(
370
370
 
371
371
  export function addField(args: AddFieldArgs): CustomType {
372
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
+ }
373
394
 
374
395
  const newCustomType = updateSection({
375
396
  customType,
376
397
  sectionId,
377
- updatedSection: {
378
- ...customType.json[sectionId],
379
- // Add the new field to the section
380
- [newFieldId]: newField,
381
- },
398
+ updatedSection,
382
399
  });
383
400
 
384
401
  return newCustomType;
@@ -1,10 +1,10 @@
1
1
  import { FC } from "react";
2
2
  import { Box, Icon, ProgressCircle, Text } from "@prismicio/editor-ui";
3
3
 
4
- import { AutoSaveStatus } from "./useAutoSave";
4
+ import { ActionQueueStatus } from "@src/hooks/useActionQueue";
5
5
 
6
6
  type AutoSaveStatusIndicatorProps = {
7
- status: AutoSaveStatus;
7
+ status: ActionQueueStatus;
8
8
  };
9
9
 
10
10
  export const AutoSaveStatusIndicator: FC<AutoSaveStatusIndicatorProps> = (
@@ -14,7 +14,7 @@ export const AutoSaveStatusIndicator: FC<AutoSaveStatusIndicatorProps> = (
14
14
  let autoSaveStatusInfo;
15
15
 
16
16
  switch (status) {
17
- case "saving":
17
+ case "pending":
18
18
  autoSaveStatusInfo = {
19
19
  icon: <ProgressCircle color="grey11" />,
20
20
  text: "Saving...",
@@ -26,7 +26,7 @@ export const AutoSaveStatusIndicator: FC<AutoSaveStatusIndicatorProps> = (
26
26
  text: "Failed to save",
27
27
  };
28
28
  break;
29
- case "saved":
29
+ case "done":
30
30
  autoSaveStatusInfo = {
31
31
  icon: <Icon name="check" color="green11" size="medium" />,
32
32
  text: "Auto-saved",
@@ -10,18 +10,16 @@ import { CustomType } from "@prismicio/types-internal/lib/customtypes";
10
10
  import { useStableCallback } from "@prismicio/editor-support/React";
11
11
 
12
12
  import useSliceMachineActions from "@src/modules/useSliceMachineActions";
13
- import {
14
- AutoSaveStatus,
15
- useAutoSave,
16
- } from "@src/features/autoSave/useAutoSave";
13
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
17
14
  import { getFormat } from "@src/domain/customType";
18
15
  import { updateCustomType } from "@src/apiClient";
16
+ import { ActionQueueStatus, useActionQueue } from "@src/hooks/useActionQueue";
19
17
 
20
18
  import { CUSTOM_TYPES_MESSAGES } from "../customTypesMessages";
21
19
 
22
20
  type CustomTypeContext = {
23
21
  customType: CustomType;
24
- autoSaveStatus: AutoSaveStatus;
22
+ actionQueueStatus: ActionQueueStatus;
25
23
  setCustomType: (customType: CustomType) => void;
26
24
  };
27
25
 
@@ -40,16 +38,17 @@ export function CustomTypeProvider(props: CustomTypeProviderProps) {
40
38
  const [customType, setCustomTypeState] = useState(initialCustomType);
41
39
  const format = getFormat(customType);
42
40
  const customTypeMessages = CUSTOM_TYPES_MESSAGES[format];
43
- const { autoSaveStatus, setNextSave } = useAutoSave({
41
+ const { actionQueueStatus, setNextAction } = useActionQueue({
44
42
  errorMessage: customTypeMessages.autoSaveFailed,
45
43
  });
46
44
  const { saveCustomTypeSuccess } = useSliceMachineActions();
47
45
  const stableSaveCustomTypeSuccess = useStableCallback(saveCustomTypeSuccess);
46
+ const { syncChanges } = useAutoSync();
48
47
 
49
48
  const setCustomType = useCallback(
50
49
  (customType: CustomType) => {
51
50
  setCustomTypeState(customType);
52
- setNextSave(async () => {
51
+ setNextAction(async () => {
53
52
  const { errors } = await updateCustomType(customType);
54
53
 
55
54
  if (errors.length > 0) {
@@ -58,18 +57,20 @@ export function CustomTypeProvider(props: CustomTypeProviderProps) {
58
57
 
59
58
  // Update available custom types store with new custom type
60
59
  stableSaveCustomTypeSuccess(customType);
60
+
61
+ syncChanges();
61
62
  });
62
63
  },
63
- [setNextSave, stableSaveCustomTypeSuccess],
64
+ [setNextAction, stableSaveCustomTypeSuccess, syncChanges],
64
65
  );
65
66
 
66
67
  const contextValue: CustomTypeContext = useMemo(
67
68
  () => ({
68
- autoSaveStatus,
69
+ actionQueueStatus,
69
70
  customType,
70
71
  setCustomType,
71
72
  }),
72
- [autoSaveStatus, customType, setCustomType],
73
+ [actionQueueStatus, customType, setCustomType],
73
74
  );
74
75
 
75
76
  return (
@@ -41,7 +41,7 @@ export const CustomTypesBuilderPage: FC = () => {
41
41
  useEffect(() => {
42
42
  // This is a fallback in case the user try to directly access a custom type
43
43
  // that don't exist
44
- // TODO(DT-1801): Implement a 404 page
44
+ // TODO(DT-1801): implement a 404 page.
45
45
  if (!selectedCustomType || !hasLocal(selectedCustomType)) {
46
46
  void router.replace("/");
47
47
  }
@@ -78,7 +78,7 @@ const CustomTypesBuilderPageWithProvider: React.FC<
78
78
  <CustomTypeProvider
79
79
  initialCustomType={CustomTypes.fromSM(customTypeFromStore)}
80
80
  >
81
- {({ autoSaveStatus, customType, setCustomType }) => {
81
+ {({ actionQueueStatus, customType, setCustomType }) => {
82
82
  const format = getFormat(customType);
83
83
  const config = CUSTOM_TYPES_CONFIG[customTypeFromStore.format];
84
84
  const messages = CUSTOM_TYPES_MESSAGES[customTypeFromStore.format];
@@ -92,7 +92,7 @@ const CustomTypesBuilderPageWithProvider: React.FC<
92
92
  page={customType.label ?? customType.id}
93
93
  />
94
94
  <AppLayoutActions>
95
- <AutoSaveStatusIndicator status={autoSaveStatus} />
95
+ <AutoSaveStatusIndicator status={actionQueueStatus} />
96
96
  {customType.format === "page" ? (
97
97
  <PageSnippetDialog model={customType} />
98
98
  ) : undefined}
@@ -1,8 +1,8 @@
1
1
  import { revalidateData } from "@prismicio/editor-support/Suspense";
2
- import { Environment } from "@slicemachine/manager/client";
3
2
 
4
- import { getLegacySliceMachineState } from "@src/features/legacyState/actions/getLegacySliceMachineState";
3
+ import { Environment } from "@slicemachine/manager/client";
5
4
  import { managerClient } from "@src/managerClient";
5
+ import { getState } from "@src/apiClient";
6
6
 
7
7
  import { getActiveEnvironment } from "./getActiveEnvironment";
8
8
 
@@ -13,6 +13,6 @@ export async function setEnvironment(
13
13
  environment: environment.domain,
14
14
  });
15
15
 
16
- revalidateData(getActiveEnvironment, []);
17
- revalidateData(getLegacySliceMachineState, []);
16
+ void revalidateData(getActiveEnvironment, []);
17
+ void revalidateData(getState, []);
18
18
  }
@@ -0,0 +1,47 @@
1
+ import { Box } from "@prismicio/editor-ui";
2
+ import Head from "next/head";
3
+ import { useRouter } from "next/router";
4
+ import { type FC, useEffect } from "react";
5
+
6
+ import {
7
+ AppLayout,
8
+ AppLayoutBreadcrumb,
9
+ AppLayoutContent,
10
+ AppLayoutHeader,
11
+ } from "@components/AppLayout";
12
+ import { ConnectGitRepository } from "@src/features/settings/git/ConnectGitRepository";
13
+ import { useGitIntegrationExperiment } from "@src/features/settings/git/useGitIntegrationExperiment";
14
+
15
+ export const SettingsPage: FC = () => {
16
+ const gitIntegrationExperiment = useGitIntegrationExperiment();
17
+ const router = useRouter();
18
+
19
+ // TODO(DT-1801): implement a 404 page.
20
+ useEffect(() => {
21
+ if (!gitIntegrationExperiment.eligible) {
22
+ void router.replace("/");
23
+ }
24
+ }, [gitIntegrationExperiment.eligible, router]);
25
+
26
+ if (!gitIntegrationExperiment.eligible) {
27
+ return null;
28
+ }
29
+
30
+ return (
31
+ <>
32
+ <Head>
33
+ <title>Settings - Slice Machine</title>
34
+ </Head>
35
+ <AppLayout>
36
+ <AppLayoutHeader>
37
+ <AppLayoutBreadcrumb folder="Settings" />
38
+ </AppLayoutHeader>
39
+ <AppLayoutContent>
40
+ <Box flexDirection="column" maxWidth={600}>
41
+ <ConnectGitRepository />
42
+ </Box>
43
+ </AppLayoutContent>
44
+ </AppLayout>
45
+ </>
46
+ );
47
+ };
@@ -0,0 +1,126 @@
1
+ import {
2
+ Button,
3
+ ErrorBoundary,
4
+ IconButton,
5
+ Tooltip,
6
+ } from "@prismicio/editor-ui";
7
+ import {
8
+ // isUnauthenticatedError,
9
+ isUnauthorizedError,
10
+ } from "@slicemachine/manager/client";
11
+ import { type FC, type ReactNode, Suspense } from "react";
12
+
13
+ import {
14
+ FieldSet,
15
+ FieldSetContent,
16
+ FieldSetFooter,
17
+ FieldSetLegend,
18
+ } from "@src/components/FieldSet";
19
+ import { ConnectGitRepositoryBlankSlate } from "@src/features/settings/git/ConnectGitRepositoryBlankSlate";
20
+ import { GitProviderConnectButtons } from "@src/features/settings/git/GitProviderConnectButtons";
21
+ import { GitRepositoriesList } from "@src/features/settings/git/GitRepositoriesList";
22
+ import {
23
+ GitRepositoriesSearch,
24
+ GitRepositoriesSearchSkeleton,
25
+ } from "@src/features/settings/git/GitRepositoriesSearch";
26
+ import { useGitOwners } from "@src/features/settings/git/useGitOwners";
27
+ import { useLinkedGitRepos } from "@src/features/settings/git/useLinkedGitRepos";
28
+ import { useUser } from "@src/hooks/useUser";
29
+ import useSliceMachineActions from "@src/modules/useSliceMachineActions";
30
+
31
+ export const ConnectGitRepository: FC = () => (
32
+ <FieldSet>
33
+ <FieldSetLegend>Connected Git Repository</FieldSetLegend>
34
+ <ErrorBoundary renderError={renderError}>
35
+ <Suspense fallback={<GitRepositoriesSearchSkeleton gitOwnerSelect />}>
36
+ <Content />
37
+ </Suspense>
38
+ </ErrorBoundary>
39
+ <FieldSetFooter
40
+ action={
41
+ <Tooltip content="Documentation is coming soon." side="bottom">
42
+ <IconButton disabled icon="openInNew" />
43
+ </Tooltip>
44
+ }
45
+ >
46
+ Learn more about Prismic for Git
47
+ </FieldSetFooter>
48
+ </FieldSet>
49
+ );
50
+
51
+ const Content: FC = () => {
52
+ // TODO(TD-1952): the `useUser` hook should be replaced with a mechanism that
53
+ // automatically revalidates the `Suspense` cache whenever the Redux
54
+ // authentication status changes.
55
+ const { isLoggedIn } = useUser();
56
+ return isLoggedIn ? <LoggedInContent /> : <UnauthenticatedErrorContent />;
57
+ };
58
+
59
+ const LoggedInContent: FC = () => {
60
+ const { linkedGitRepos } = useLinkedGitRepos();
61
+ return linkedGitRepos.length > 0 ? (
62
+ <GitRepositoriesList mode="unlink" repos={linkedGitRepos} />
63
+ ) : (
64
+ <UnlinkedRepositoryContent />
65
+ );
66
+ };
67
+
68
+ const UnlinkedRepositoryContent: FC = () => {
69
+ const owners = useGitOwners();
70
+ return owners.length > 0 ? (
71
+ <GitRepositoriesSearch owners={owners} />
72
+ ) : (
73
+ <FieldSetContent>
74
+ <GitProviderConnectButtons />
75
+ </FieldSetContent>
76
+ );
77
+ };
78
+
79
+ function renderError(error: unknown, reset: () => void): ReactNode {
80
+ // if (isUnauthenticatedError(error)) {
81
+ // return <UnauthenticatedErrorContent />;
82
+ // } else
83
+ if (isUnauthorizedError(error)) {
84
+ return <UnauthorizedErrorContent />;
85
+ } else {
86
+ return <UnknownErrorContent reset={reset} />;
87
+ }
88
+ }
89
+
90
+ const UnauthenticatedErrorContent: FC = () => {
91
+ const { openLoginModal } = useSliceMachineActions();
92
+ return (
93
+ <FieldSetContent>
94
+ <ConnectGitRepositoryBlankSlate
95
+ title="It seems like you are logged out"
96
+ description="Log in to connect a Git repository."
97
+ action={<Button onClick={openLoginModal}>Log in to Prismic</Button>}
98
+ />
99
+ </FieldSetContent>
100
+ );
101
+ };
102
+
103
+ const UnauthorizedErrorContent: FC = () => (
104
+ <FieldSetContent>
105
+ <ConnectGitRepositoryBlankSlate
106
+ title="It seems like you do not have permission"
107
+ description="An owner or admin is required to connect a Git repository."
108
+ />
109
+ </FieldSetContent>
110
+ );
111
+
112
+ type UnknownErrorContentProps = { reset: () => void };
113
+
114
+ const UnknownErrorContent: FC<UnknownErrorContentProps> = ({ reset }) => (
115
+ <FieldSetContent>
116
+ <ConnectGitRepositoryBlankSlate
117
+ title="Unable to fetch data"
118
+ description="An error occurred while fetching the list of connected Git repositories."
119
+ action={
120
+ <Button color="grey" onClick={reset}>
121
+ Retry
122
+ </Button>
123
+ }
124
+ />
125
+ </FieldSetContent>
126
+ );
@@ -0,0 +1,33 @@
1
+ import { Box, ButtonGroup, Text } from "@prismicio/editor-ui";
2
+ import type { FC, ReactNode } from "react";
3
+
4
+ type ConnectGitRepositoryBlankSlateProps = {
5
+ title: string;
6
+ description: string;
7
+ action?: ReactNode;
8
+ };
9
+
10
+ export const ConnectGitRepositoryBlankSlate: FC<
11
+ ConnectGitRepositoryBlankSlateProps
12
+ > = ({ title, description, action }) => (
13
+ <Box
14
+ flexDirection="column"
15
+ /*
16
+ * TODO: these `padding` values actually don't match Figma, but they are the
17
+ * closest allowed by the `Box` component.
18
+ */
19
+ padding={{ block: 72, inline: 100 }}
20
+ >
21
+ <Text align="center" variant="emphasized">
22
+ {title}
23
+ </Text>
24
+ <Text align="center" color="grey11">
25
+ {description}
26
+ </Text>
27
+ {Boolean(action) ? (
28
+ <ButtonGroup sx={{ alignSelf: "center", marginTop: 8 }}>
29
+ {action}
30
+ </ButtonGroup>
31
+ ) : undefined}
32
+ </Box>
33
+ );
@@ -0,0 +1,71 @@
1
+ import { Select, SelectItem, tokens } from "@prismicio/editor-ui";
2
+ import type { GitOwner } from "@slicemachine/manager";
3
+ import type { ComponentProps, FC } from "react";
4
+
5
+ import { gitProviderToConfig } from "@src/features/settings/git/GitProvider";
6
+
7
+ type GitOwnerSelectProps = {
8
+ disabled?: boolean;
9
+ owners?: GitOwner[];
10
+ selectedOwner?: GitOwner;
11
+ onSelectedOwnerChange?: (selectedOwner: GitOwner) => void;
12
+ sx?: SX;
13
+ };
14
+
15
+ export const GitOwnerSelect: FC<GitOwnerSelectProps> = ({
16
+ disabled,
17
+ owners = [],
18
+ selectedOwner,
19
+ onSelectedOwnerChange,
20
+ sx,
21
+ }) => (
22
+ <Select
23
+ color="grey"
24
+ constrainContentWidth
25
+ disabled={disabled}
26
+ flexContent
27
+ onValueChange={(value) => {
28
+ const [provider, id] = parseGitOwnerKey(value);
29
+ const selectedOwner = owners.find(
30
+ (owner) => owner.provider === provider && owner.id === id,
31
+ );
32
+ if (selectedOwner) {
33
+ onSelectedOwnerChange?.(selectedOwner);
34
+ }
35
+ }}
36
+ placeholder="Owner"
37
+ renderStartIcon={() => <GitOwnerIcon owner={selectedOwner} />}
38
+ size="large"
39
+ sx={sx}
40
+ value={selectedOwner ? formatGitOwnerKey(selectedOwner) : undefined}
41
+ >
42
+ {owners.map((owner) => (
43
+ <SelectItem
44
+ key={formatGitOwnerKey(owner)}
45
+ renderStartIcon={() => <GitOwnerIcon owner={owner} />}
46
+ size="large"
47
+ value={formatGitOwnerKey(owner)}
48
+ >
49
+ {owner.name}
50
+ </SelectItem>
51
+ ))}
52
+ </Select>
53
+ );
54
+
55
+ type GitOwnerIconProps = { owner: GitOwner | undefined };
56
+
57
+ const GitOwnerIcon: FC<GitOwnerIconProps> = ({ owner }) => {
58
+ const { Icon } = gitProviderToConfig[owner?.provider ?? "gitHub"];
59
+ return <Icon color={tokens.color.greyLight11} />;
60
+ };
61
+
62
+ function formatGitOwnerKey(owner: GitOwner): string {
63
+ return `${owner.provider}@${owner.id}`;
64
+ }
65
+
66
+ function parseGitOwnerKey(key: string): string[] {
67
+ return key.split("@");
68
+ }
69
+
70
+ // TODO(DT-1928): export the `SX` type from `@prismicio/editor-ui`.
71
+ type SX = ComponentProps<typeof Select>["sx"];