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
@@ -0,0 +1,41 @@
1
+ import { revalidateData, useRequest } from "@prismicio/editor-support/Suspense";
2
+ import type { GitRepoSpecifier } from "@slicemachine/manager";
3
+
4
+ import { useSliceMachineConfig } from "@src/hooks/useSliceMachineConfig";
5
+ import { managerClient } from "@src/managerClient";
6
+
7
+ type UseLinkedGitReposReturnType = {
8
+ linkedGitRepos: GitRepoSpecifier[];
9
+ linkRepo: (git: GitRepoSpecifier) => Promise<void>;
10
+ unlinkRepo: (git: GitRepoSpecifier) => Promise<void>;
11
+ };
12
+
13
+ export function useLinkedGitRepos(): UseLinkedGitReposReturnType {
14
+ const [config] = useSliceMachineConfig();
15
+ const linkedGitRepos = useRequest(getLinkedGitRepos, [config.repositoryName]);
16
+ return {
17
+ linkedGitRepos,
18
+ linkRepo: async (git) => {
19
+ await managerClient.git.linkRepo({
20
+ prismic: { domain: config.repositoryName },
21
+ git,
22
+ });
23
+ await revalidateData(getLinkedGitRepos, [config.repositoryName]);
24
+ },
25
+ unlinkRepo: async (git) => {
26
+ await managerClient.git.unlinkRepo({
27
+ prismic: { domain: config.repositoryName },
28
+ git,
29
+ });
30
+ await revalidateData(getLinkedGitRepos, [config.repositoryName]);
31
+ },
32
+ };
33
+ }
34
+
35
+ async function getLinkedGitRepos(
36
+ prismicDomain: string,
37
+ ): Promise<GitRepoSpecifier[]> {
38
+ return await managerClient.git.fetchLinkedRepos({
39
+ prismic: { domain: prismicDomain },
40
+ });
41
+ }
@@ -0,0 +1,23 @@
1
+ import type { GitRepoSpecifier } from "@slicemachine/manager";
2
+
3
+ import { useSliceMachineConfig } from "@src/hooks/useSliceMachineConfig";
4
+ import { managerClient } from "@src/managerClient";
5
+
6
+ type UseWriteAPITokenReturnType = {
7
+ updateToken: (token: string) => Promise<void>;
8
+ };
9
+
10
+ export function useWriteAPIToken(args: {
11
+ git: GitRepoSpecifier;
12
+ }): UseWriteAPITokenReturnType {
13
+ const [config] = useSliceMachineConfig();
14
+ return {
15
+ updateToken: async (token) => {
16
+ await managerClient.git.updateWriteAPIToken({
17
+ prismic: { domain: config.repositoryName },
18
+ git: args.git,
19
+ token,
20
+ });
21
+ },
22
+ };
23
+ }
@@ -1,9 +1,10 @@
1
1
  import { toast } from "react-toastify";
2
2
  import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
3
3
 
4
- import { pascalize, snakelize } from "@lib/utils/str";
4
+ import { pascalize } from "@lib/utils/str";
5
5
  import { managerClient } from "@src/managerClient";
6
6
  import { telemetry } from "@src/apiClient";
7
+ import { buildEmptySliceModel } from "@src/domain/slice";
7
8
 
8
9
  type CreateSliceArgs = {
9
10
  sliceName: string;
@@ -12,8 +13,9 @@ type CreateSliceArgs = {
12
13
  };
13
14
 
14
15
  export async function createSlice(args: CreateSliceArgs) {
16
+ const { sliceName, libraryName, onSuccess } = args;
17
+
15
18
  try {
16
- const { sliceName, libraryName, onSuccess } = args;
17
19
  const newSlice = buildEmptySliceModel(sliceName);
18
20
  const { errors } = await managerClient.slices.createSlice({
19
21
  libraryID: libraryName,
@@ -33,34 +35,8 @@ export async function createSlice(args: CreateSliceArgs) {
33
35
 
34
36
  await onSuccess(newSlice);
35
37
  } catch (e) {
36
- const errorMessage = "Internal Error: Slice not created";
38
+ const errorMessage = `An unexpected error happened while creating slice ${sliceName}.`;
37
39
  console.error(errorMessage, e);
38
40
  toast.error(errorMessage);
39
41
  }
40
42
  }
41
-
42
- const DEFAULT_VARIATION_ID = "default";
43
-
44
- function buildEmptySliceModel(sliceName: string): SharedSlice {
45
- return {
46
- id: snakelize(sliceName),
47
- type: "SharedSlice",
48
- name: sliceName,
49
- description: sliceName,
50
- variations: [
51
- {
52
- id: DEFAULT_VARIATION_ID,
53
- name: pascalize(DEFAULT_VARIATION_ID),
54
- // Property not used yet. Fallback to "...".
55
- docURL: "...",
56
- // "initial" is fine here as default value.
57
- version: "initial",
58
- description: pascalize(DEFAULT_VARIATION_ID),
59
- // Empty string is fine, we don't want to save imageUrl.
60
- // We don't want to compare local and remote image with imageUrl.
61
- // It will be striped anyway when doing the comparison.
62
- imageUrl: "",
63
- },
64
- ],
65
- };
66
- }
@@ -0,0 +1,33 @@
1
+ import { toast } from "react-toastify";
2
+
3
+ import { managerClient } from "@src/managerClient";
4
+
5
+ type DeleteSliceArgs = {
6
+ libraryID: string;
7
+ sliceID: string;
8
+ sliceName: string;
9
+ onSuccess: () => void;
10
+ };
11
+
12
+ export async function deleteSlice(args: DeleteSliceArgs) {
13
+ const { sliceName, sliceID, libraryID, onSuccess } = args;
14
+
15
+ try {
16
+ const { errors } = await managerClient.slices.deleteSlice({
17
+ libraryID,
18
+ sliceID,
19
+ });
20
+
21
+ if (errors.length > 0) {
22
+ throw errors;
23
+ }
24
+
25
+ onSuccess();
26
+
27
+ toast.success(`Successfully deleted slice “${sliceName}”`);
28
+ } catch (e) {
29
+ const errorMessage = `An unexpected error happened while deleting slice “${sliceName}”.`;
30
+ console.error(errorMessage, e);
31
+ toast.error(errorMessage);
32
+ }
33
+ }
@@ -0,0 +1,37 @@
1
+ import { toast } from "react-toastify";
2
+
3
+ import { managerClient } from "@src/managerClient";
4
+ import { ComponentUI } from "@lib/models/common/ComponentUI";
5
+ import { rename } from "@src/domain/slice";
6
+ import { Slices } from "@lib/models/common/Slice";
7
+
8
+ type DeleteSliceArgs = {
9
+ newSliceName: string;
10
+ slice: ComponentUI;
11
+ onSuccess: (renamedSlice: ComponentUI) => void;
12
+ };
13
+
14
+ export async function renameSlice(args: DeleteSliceArgs) {
15
+ const { slice, newSliceName, onSuccess } = args;
16
+
17
+ try {
18
+ const renamedSlice = rename(slice, newSliceName);
19
+
20
+ const { errors } = await managerClient.slices.renameSlice({
21
+ libraryID: slice.from,
22
+ model: Slices.fromSM(renamedSlice.model),
23
+ });
24
+
25
+ if (errors.length > 0) {
26
+ throw errors;
27
+ }
28
+
29
+ onSuccess(renamedSlice);
30
+
31
+ toast.success("Slice name updated");
32
+ } catch (e) {
33
+ const errorMessage = `An unexpected error happened while renaming “${slice.model.name}”.`;
34
+ console.error(errorMessage, e);
35
+ toast.error(errorMessage);
36
+ }
37
+ }
@@ -43,7 +43,6 @@ export const ConvertLegacySliceAsNewSliceDialog: FC<DialogProps> = ({
43
43
  <DialogHeader title="Upgrade slice" />
44
44
  <DialogContent>
45
45
  <Formik
46
- validateOnChange
47
46
  validateOnMount
48
47
  initialValues={{
49
48
  from: libraries[0]?.name,
@@ -47,7 +47,6 @@ export const ConvertLegacySliceAsNewVariationDialog: FC<DialogProps> = ({
47
47
  <DialogHeader title="Convert to slice variation" />
48
48
  <DialogContent>
49
49
  <Formik
50
- validateOnChange
51
50
  initialValues={{
52
51
  libraryID: localSharedSlices[0]?.from,
53
52
  sliceID: localSharedSlices[0]?.model.id,
@@ -31,7 +31,6 @@ export const ConvertLegacySliceMergeWithIdenticalDialog: FC<DialogProps> = ({
31
31
  <DialogHeader title="Merge with an existing slice" />
32
32
  <DialogContent>
33
33
  <Formik
34
- validateOnChange
35
34
  initialValues={{
36
35
  path: identicalSlices[0]?.path ?? "",
37
36
  }}
@@ -9,18 +9,16 @@ import {
9
9
  import { useStableCallback } from "@prismicio/editor-support/React";
10
10
  import { useRouter } from "next/router";
11
11
 
12
- import {
13
- AutoSaveStatus,
14
- useAutoSave,
15
- } from "@src/features/autoSave/useAutoSave";
16
12
  import { ComponentUI } from "@lib/models/common/ComponentUI";
17
13
  import { readSliceMocks, updateSlice } from "@src/apiClient";
18
14
  import useSliceMachineActions from "@src/modules/useSliceMachineActions";
19
15
  import { VariationSM } from "@lib/models/common/Slice";
16
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
17
+ import { ActionQueueStatus, useActionQueue } from "@src/hooks/useActionQueue";
20
18
 
21
19
  type SliceContext = {
22
20
  slice: ComponentUI;
23
- autoSaveStatus: AutoSaveStatus;
21
+ actionQueueStatus: ActionQueueStatus;
24
22
  setSlice: (slice: ComponentUI) => void;
25
23
  variation: VariationSM;
26
24
  };
@@ -37,12 +35,13 @@ export function SliceBuilderProvider(props: SliceBuilderProviderProps) {
37
35
 
38
36
  const router = useRouter();
39
37
  const [slice, setSliceState] = useState(initialSlice);
40
- const { autoSaveStatus, setNextSave } = useAutoSave({
38
+ const { actionQueueStatus, setNextAction } = useActionQueue({
41
39
  errorMessage:
42
40
  "Failed to save slice. Check your browser's console for more information.",
43
41
  });
44
42
  const { saveSliceSuccess } = useSliceMachineActions();
45
43
  const stableSaveSliceSuccess = useStableCallback(saveSliceSuccess);
44
+ const { syncChanges } = useAutoSync();
46
45
 
47
46
  const variation = useMemo(() => {
48
47
  const variationName = router.query.variation;
@@ -60,7 +59,7 @@ export function SliceBuilderProvider(props: SliceBuilderProviderProps) {
60
59
  const setSlice = useCallback(
61
60
  (slice: ComponentUI) => {
62
61
  setSliceState(slice);
63
- setNextSave(async () => {
62
+ setNextAction(async () => {
64
63
  const { errors: updateSliceErrors } = await updateSlice(slice);
65
64
 
66
65
  if (updateSliceErrors.length > 0) {
@@ -76,20 +75,23 @@ export function SliceBuilderProvider(props: SliceBuilderProviderProps) {
76
75
  throw readSliceMockErrors;
77
76
  }
78
77
 
78
+ // Update slices store with new slice
79
79
  stableSaveSliceSuccess({ ...slice, mocks });
80
+
81
+ syncChanges();
80
82
  });
81
83
  },
82
- [setNextSave, stableSaveSliceSuccess],
84
+ [setNextAction, stableSaveSliceSuccess, syncChanges],
83
85
  );
84
86
 
85
87
  const contextValue: SliceContext = useMemo(
86
88
  () => ({
87
- autoSaveStatus,
89
+ actionQueueStatus,
88
90
  slice,
89
91
  setSlice,
90
92
  variation,
91
93
  }),
92
- [autoSaveStatus, slice, setSlice, variation],
94
+ [actionQueueStatus, slice, setSlice, variation],
93
95
  );
94
96
 
95
97
  return (
@@ -0,0 +1,252 @@
1
+ import {
2
+ FC,
3
+ PropsWithChildren,
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ } from "react";
10
+ import {
11
+ useOnChange,
12
+ useStableCallback,
13
+ } from "@prismicio/editor-support/React";
14
+
15
+ import { AuthStatus } from "@src/modules/userContext/types";
16
+ import useSliceMachineActions from "@src/modules/useSliceMachineActions";
17
+ import { getState } from "@src/apiClient";
18
+ import {
19
+ Environment,
20
+ isUnauthenticatedError,
21
+ } from "@slicemachine/manager/client";
22
+ import { useNetwork } from "@src/hooks/useNetwork";
23
+ import { useAuthStatus } from "@src/hooks/useAuthStatus";
24
+ import {
25
+ ChangedCustomType,
26
+ ChangedSlice,
27
+ } from "@lib/models/common/ModelStatus";
28
+
29
+ import { useActiveEnvironment } from "../environments/useActiveEnvironment";
30
+ import { ActionQueueStatus, useActionQueue } from "../../hooks/useActionQueue";
31
+ import { pushChanges } from "./actions/pushChanges";
32
+ import { useUnSyncChanges } from "./useUnSyncChanges";
33
+ import { fetchUnSyncChanges } from "./fetchUnSyncChanges";
34
+
35
+ export type AutoSyncStatus =
36
+ | "not-active"
37
+ | "offline"
38
+ | "not-logged-in"
39
+ | "syncing"
40
+ | "synced"
41
+ | "failed";
42
+
43
+ type AutoSyncContext = {
44
+ autoSyncStatus: AutoSyncStatus;
45
+ syncChanges: (args?: SyncChangesArgs) => void;
46
+ };
47
+
48
+ type SyncChangesArgs = {
49
+ environment?: Environment;
50
+ loggedIn?: boolean;
51
+ changedCustomTypes?: ChangedCustomType[];
52
+ changedSlices?: ChangedSlice[];
53
+ };
54
+
55
+ const AutoSyncContextValue = createContext<AutoSyncContext | undefined>(
56
+ undefined,
57
+ );
58
+
59
+ export const AutoSyncProvider: FC<PropsWithChildren> = (props) => {
60
+ const { children } = props;
61
+ const { unSyncedCustomTypes, unSyncedSlices } = useUnSyncChanges();
62
+ const isOnline = useNetwork();
63
+ const authStatus = useAuthStatus();
64
+ const { refreshState, pushChangesSuccess } = useSliceMachineActions();
65
+ const stableRefreshState = useStableCallback(refreshState);
66
+ const stablePushChangesSuccess = useStableCallback(pushChangesSuccess);
67
+ const { activeEnvironment } = useActiveEnvironment();
68
+ const { setNextAction, actionQueueStatus } = useActionQueue({
69
+ actionQueueStatusDelay: 0,
70
+ errorMessage:
71
+ "Failed to sync changes. Check your browser's console for more information.",
72
+ });
73
+
74
+ const syncChanges = useCallback(
75
+ (args: SyncChangesArgs = {}) => {
76
+ const {
77
+ // We default to the active environment if not provided.
78
+ // This is useful when we want to sync changes right after an environment switch.
79
+ environment = activeEnvironment,
80
+
81
+ // We default to a full user logged in with internet access if not provider.
82
+ // This is useful when we want to sync changes right after the user logs in.
83
+ loggedIn = isOnline && authStatus === AuthStatus.AUTHORIZED,
84
+ } = args;
85
+
86
+ if (!loggedIn || environment?.kind !== "dev") {
87
+ return;
88
+ }
89
+
90
+ setNextAction(async () => {
91
+ // We first get the remote models and local models to ensure we need
92
+ // to sync changes.
93
+ const { changedCustomTypes, changedSlices } = await fetchUnSyncChanges({
94
+ isOnline,
95
+ authStatus,
96
+ });
97
+
98
+ if (changedCustomTypes.length === 0 && changedSlices.length === 0) {
99
+ return;
100
+ }
101
+
102
+ try {
103
+ const response = await pushChanges({
104
+ changedCustomTypes,
105
+ changedSlices,
106
+ // We force the deletion of documents as the auto-sync is only for
107
+ // the dev environment.
108
+ confirmDeleteDocuments: true,
109
+ });
110
+
111
+ // Hard limit reached is the only limit that can be reached as we set
112
+ // confirmDeleteDocuments to true.
113
+ if (response && response.type === "HARD") {
114
+ console.error(
115
+ `Manual action required. ${
116
+ response.details.customTypes.length > 1
117
+ ? "These types have"
118
+ : "This type has"
119
+ } too many associated Documents. Archive and delete these documents
120
+ manually and then try deleting the types again.`,
121
+ );
122
+
123
+ throw new Error();
124
+ }
125
+
126
+ // Now that the changes have been pushed, we need to update redux with
127
+ // the new remote models.
128
+ const serverState = await getState();
129
+ stableRefreshState(serverState);
130
+
131
+ // Update last sync value in local storage
132
+ stablePushChangesSuccess();
133
+ } catch (error) {
134
+ if (isUnauthenticatedError(error)) {
135
+ // If the user is not authenticated, we don't want to let the user
136
+ // retry the sync. We just stop the sync and let the user
137
+ // know that they need to login again.
138
+ // This can easily happen if the the token expires when the user is
139
+ // offline.
140
+ return;
141
+ }
142
+
143
+ // If the sync failed, we want to display the error message with
144
+ // the retry button.
145
+ throw error;
146
+ }
147
+ });
148
+ },
149
+ [
150
+ stableRefreshState,
151
+ stablePushChangesSuccess,
152
+ setNextAction,
153
+ isOnline,
154
+ authStatus,
155
+ activeEnvironment,
156
+ ],
157
+ );
158
+
159
+ // We want to sync changes when the user loads the page and there are unsynced changes
160
+ useEffect(
161
+ () => {
162
+ if (unSyncedCustomTypes.length > 0 || unSyncedSlices.length > 0) {
163
+ syncChanges();
164
+ }
165
+ },
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ [],
168
+ );
169
+
170
+ // We want to sync changes when the user comes back online and there are unsynced changes
171
+ useOnChange(isOnline, () => {
172
+ if (
173
+ isOnline &&
174
+ (unSyncedCustomTypes.length > 0 || unSyncedSlices.length > 0)
175
+ ) {
176
+ syncChanges();
177
+ }
178
+ });
179
+
180
+ const autoSyncStatus = useMemo(
181
+ () =>
182
+ getAutoSyncStatus({
183
+ activeEnvironment,
184
+ isOnline,
185
+ authStatus,
186
+ actionQueueStatus,
187
+ }),
188
+ [actionQueueStatus, authStatus, isOnline, activeEnvironment],
189
+ );
190
+
191
+ const contextValue = useMemo(
192
+ () => ({
193
+ syncChanges,
194
+ autoSyncStatus,
195
+ }),
196
+ [syncChanges, autoSyncStatus],
197
+ );
198
+
199
+ return (
200
+ <AutoSyncContextValue.Provider value={contextValue}>
201
+ {children}
202
+ </AutoSyncContextValue.Provider>
203
+ );
204
+ };
205
+
206
+ export function useAutoSync(): AutoSyncContext {
207
+ const context = useContext(AutoSyncContextValue);
208
+
209
+ // Prevent introducing a lot of implementation details from redux in tests
210
+ if (process.env.NODE_ENV === "test") {
211
+ return { syncChanges: () => void 0, autoSyncStatus: "not-active" };
212
+ }
213
+
214
+ if (context === undefined) {
215
+ throw new Error("useAutoSync must be used within a AutoSyncProvider");
216
+ }
217
+
218
+ return context;
219
+ }
220
+
221
+ type GetAutoSyncStatusArgs = {
222
+ activeEnvironment?: Environment;
223
+ isOnline: boolean;
224
+ authStatus: AuthStatus;
225
+ actionQueueStatus: ActionQueueStatus;
226
+ };
227
+
228
+ function getAutoSyncStatus(args: GetAutoSyncStatusArgs): AutoSyncStatus {
229
+ const { activeEnvironment, isOnline, authStatus, actionQueueStatus } = args;
230
+
231
+ if (activeEnvironment === undefined || activeEnvironment.kind !== "dev") {
232
+ return "not-active";
233
+ }
234
+
235
+ if (!isOnline) {
236
+ return "offline";
237
+ }
238
+
239
+ if (authStatus !== AuthStatus.AUTHORIZED) {
240
+ return "not-logged-in";
241
+ }
242
+
243
+ if (actionQueueStatus === "failed") {
244
+ return "failed";
245
+ }
246
+
247
+ if (actionQueueStatus === "pending") {
248
+ return "syncing";
249
+ }
250
+
251
+ return "synced";
252
+ }
@@ -0,0 +1,17 @@
1
+ .root {
2
+ align-items: center;
3
+ border-radius: 6px;
4
+ border: solid 1px transparent;
5
+ border-image: linear-gradient(
6
+ to right,
7
+ color-mix(in srgb, var(--grey6), transparent 100%),
8
+ var(--grey6),
9
+ color-mix(in srgb, var(--grey6), transparent 100%)
10
+ )
11
+ 1 stretch;
12
+ display: flex;
13
+ gap: 4px;
14
+ height: 32px;
15
+ justify-content: center;
16
+ width: 100%;
17
+ }
@@ -0,0 +1,5 @@
1
+ declare const styles: {
2
+ readonly "root": string;
3
+ };
4
+ export = styles;
5
+
@@ -0,0 +1,58 @@
1
+ import { FC } from "react";
2
+ import { Text, Tooltip } from "@prismicio/editor-ui";
3
+
4
+ import { Syncing } from "@src/icons/Syncing";
5
+ import { Synced } from "@src/icons/Synced";
6
+ import { SyncFailed } from "@src/icons/SyncFailed";
7
+
8
+ import styles from "./AutoSyncStatusIndicator.module.css";
9
+
10
+ type AutoSyncStatusIndicatorProps = {
11
+ autoSyncStatus: "syncing" | "synced" | "failed";
12
+ };
13
+
14
+ export const AutoSyncStatusIndicator: FC<AutoSyncStatusIndicatorProps> = (
15
+ props,
16
+ ) => {
17
+ const { autoSyncStatus } = props;
18
+
19
+ let autoSaveStatusInfo;
20
+
21
+ switch (autoSyncStatus) {
22
+ case "syncing":
23
+ autoSaveStatusInfo = {
24
+ icon: <Syncing />,
25
+ text: "Syncing...",
26
+ tooltipText:
27
+ "Your local changes are currently syncing with your personal sandbox.",
28
+ };
29
+ break;
30
+ case "synced":
31
+ autoSaveStatusInfo = {
32
+ icon: <Synced />,
33
+ text: "Synced",
34
+ tooltipText:
35
+ "Your local changes are in sync with your personal sandbox.",
36
+ };
37
+ break;
38
+ case "failed":
39
+ autoSaveStatusInfo = {
40
+ icon: <SyncFailed />,
41
+ text: "Sync failed",
42
+ tooltipText:
43
+ "An error occurred while syncing your changes with your personal sandbox.",
44
+ };
45
+ break;
46
+ }
47
+
48
+ return autoSaveStatusInfo.tooltipText !== undefined ? (
49
+ <Tooltip content={autoSaveStatusInfo.tooltipText} side="bottom">
50
+ <div className={styles.root}>
51
+ {autoSaveStatusInfo.icon}
52
+ <Text color="grey11">{autoSaveStatusInfo.text}</Text>
53
+ </div>
54
+ </Tooltip>
55
+ ) : (
56
+ autoSaveStatusInfo.icon
57
+ );
58
+ };
@@ -0,0 +1,40 @@
1
+ import { normalizeFrontendCustomTypes } from "@lib/models/common/normalizers/customType";
2
+ import { normalizeFrontendSlices } from "@lib/models/common/normalizers/slices";
3
+ import { getState } from "@src/apiClient";
4
+ import { AuthStatus } from "@src/modules/userContext/types";
5
+
6
+ import { UnSyncedChanges, getUnSyncedChanges } from "./getUnSyncChanges";
7
+
8
+ type FetchUnSyncChangesArgs = {
9
+ isOnline: boolean;
10
+ authStatus: AuthStatus;
11
+ };
12
+
13
+ export async function fetchUnSyncChanges(
14
+ args: FetchUnSyncChangesArgs,
15
+ ): Promise<UnSyncedChanges> {
16
+ const { isOnline, authStatus } = args;
17
+
18
+ const serverState = await getState();
19
+
20
+ const slices = normalizeFrontendSlices(
21
+ serverState.libraries,
22
+ serverState.remoteSlices,
23
+ );
24
+ const customTypes = Object.values(
25
+ normalizeFrontendCustomTypes(
26
+ serverState.customTypes,
27
+ serverState.remoteCustomTypes,
28
+ ),
29
+ );
30
+
31
+ const unSyncedChanges = getUnSyncedChanges({
32
+ customTypes,
33
+ libraries: serverState.libraries,
34
+ slices,
35
+ isOnline,
36
+ authStatus,
37
+ });
38
+
39
+ return unSyncedChanges;
40
+ }