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.
- package/.depcheckrc +3 -0
- package/components/ChangesEmptyState/OfflinePage.tsx +1 -1
- package/components/ChangesItems/ChangesItems.tsx +6 -2
- package/components/CustomTypeTable/changesPage.tsx +12 -7
- package/components/DeleteSliceModal/index.tsx +37 -24
- package/components/DeleteVariationModal/index.tsx +1 -0
- package/components/Forms/CreateCustomTypeModal/CreateCustomTypeModal.tsx +65 -38
- package/components/Forms/CreateSliceModal/CreateSliceModal.tsx +3 -1
- package/components/Forms/RenameCustomTypeModal/RenameCustomTypeModal.tsx +7 -2
- package/components/Forms/RenameSliceModal/RenameSliceModal.tsx +36 -21
- package/components/Forms/RenameVariationModal/RenameVariationModal.tsx +0 -1
- package/components/LoginModal/index.tsx +40 -2
- package/components/ModalFormCard/index.tsx +0 -1
- package/components/Navigation/ChangesItem.tsx +117 -0
- package/components/Navigation/Environment.tsx +51 -4
- package/components/Navigation/index.css.ts +1 -2
- package/components/Navigation/index.test.tsx +5 -85
- package/components/Navigation/index.tsx +32 -20
- package/components/ReviewModal/ReviewForm.tsx +0 -1
- package/lib/builders/SliceBuilder/index.tsx +3 -3
- package/lib/builders/common/EditModal/Form.jsx +0 -1
- package/lib/builders/common/Zone/Card/components/NewField.tsx +0 -1
- package/lib/models/common/ModelStatus/index.ts +25 -0
- package/lib/models/common/widgets/StructuredText/index.ts +1 -1
- package/out/404.html +1 -1
- package/out/_next/static/chunks/18539003-5f78714a35fde4a1.js +97 -0
- package/out/_next/static/chunks/25-3aa4d4b24167d340.js +1 -0
- package/out/_next/static/chunks/318-19dbcae10cdb6408.js +1 -0
- package/out/_next/static/chunks/727-de03055e40815a3a.js +1 -0
- package/out/_next/static/chunks/777-8c5507e3b119b5dc.js +8 -0
- package/out/_next/static/chunks/799-0af1215eb2b3c17e.js +1 -0
- package/out/_next/static/chunks/904-40686b0426716111.js +1 -0
- package/out/_next/static/chunks/pages/_app-bca191df4ac7078e.js +434 -0
- package/out/_next/static/chunks/pages/{changelog-afd18276fe018b24.js → changelog-cb1df3a70b20f1b8.js} +1 -1
- package/out/_next/static/chunks/pages/changes-ccfd715c2bdc4f83.js +1 -0
- package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-3f862ad6111a1fc0.js → [customTypeId]-97ab3b90b85d645d.js} +1 -1
- package/out/_next/static/chunks/pages/{custom-types-94b8f8dbf7727412.js → custom-types-3c8e721ec6802127.js} +1 -1
- package/out/_next/static/chunks/pages/index-cf93d16e9c63b6db.js +1 -0
- package/out/_next/static/chunks/pages/{labs-ee206005948f8e87.js → labs-2adfb5127f9c4081.js} +1 -1
- package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-d1ffca11c42f2603.js → [pageTypeId]-84a22c5e480d3fa9.js} +1 -1
- package/out/_next/static/chunks/pages/settings-94bcd7422e8b237d.js +1 -0
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/{simulator-897ca93c114a1435.js → simulator-aaf65fbec8f5839c.js} +1 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-90bb2ea76d2abf42.js +1 -0
- package/out/_next/static/chunks/pages/slices-e0ce615c38e8338d.js +1 -0
- package/out/_next/static/css/39e089aad1742a3a.css +1 -0
- package/out/_next/static/css/{37153180c3b0dd66.css → 838fd8bb5a7c32b0.css} +1 -1
- package/out/_next/static/css/ca32882a5d2d8d88.css +1 -0
- package/out/_next/static/dS_xZnzzna6Y6uwcTgf90/_buildManifest.js +1 -0
- package/out/changelog.html +1 -1
- package/out/changes.html +1 -1
- package/out/custom-types/[customTypeId].html +1 -1
- package/out/custom-types.html +1 -1
- package/out/index.html +1 -1
- package/out/labs.html +1 -1
- package/out/page-types/[pageTypeId].html +1 -1
- package/out/settings.html +1 -0
- package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
- package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
- package/out/slices.html +1 -1
- package/package.json +9 -6
- package/pages/_app.tsx +34 -32
- package/pages/changes.tsx +27 -41
- package/pages/settings.tsx +1 -0
- package/pages/slices.tsx +20 -8
- package/postcss.config.js +10 -0
- package/src/apiClient.ts +1 -22
- package/src/components/FieldSet/FieldSet.stories.tsx +20 -11
- package/src/components/RelativeTime/RelativeTime.stories.tsx +25 -0
- package/src/components/RelativeTime/RelativeTime.tsx +40 -0
- package/src/components/RelativeTime/index.ts +1 -0
- package/src/components/SideNav/SideNav.css.ts +1 -1
- package/src/components/SideNav/SideNav.tsx +9 -5
- package/src/components/SideNav/SideNavEnvironmentSelector.tsx +31 -11
- package/src/domain/__tests__/customType.test.ts +205 -19
- package/src/domain/customType.ts +132 -7
- package/src/domain/slice.ts +38 -0
- package/src/features/autoSave/AutoSaveStatusIndicator.tsx +4 -4
- package/src/features/customTypes/PageTypeOnboarding.tsx +21 -26
- package/src/features/customTypes/actions/convertCustomToPageType.ts +14 -10
- package/src/features/customTypes/actions/createCustomType.ts +69 -0
- package/src/features/customTypes/actions/deleteCustomType.ts +13 -3
- package/src/features/customTypes/actions/renameCustomType.ts +10 -4
- package/src/features/customTypes/customTypesBuilder/CustomTypeProvider.tsx +11 -10
- package/src/features/customTypes/customTypesBuilder/CustomTypesBuilderPage.tsx +5 -3
- package/src/features/customTypes/customTypesTable/CustomTypesTable.tsx +8 -8
- package/src/features/customTypes/customTypesTable/CustomTypesTablePage.tsx +18 -17
- package/src/features/environments/actions/setEnvironment.ts +4 -4
- package/src/features/settings/SettingsPage.tsx +47 -0
- package/src/features/settings/git/ConnectGitRepository.tsx +126 -0
- package/src/features/settings/git/ConnectGitRepositoryBlankSlate.tsx +33 -0
- package/src/features/settings/git/GitOwnerSelect.tsx +71 -0
- package/src/features/settings/git/GitProvider.ts +40 -0
- package/src/features/settings/git/GitProviderConnectButtons.tsx +63 -0
- package/src/features/settings/git/GitRepositoriesList.tsx +76 -0
- package/src/features/settings/git/GitRepositoriesSearch.tsx +69 -0
- package/src/features/settings/git/GitRepositoryConnectDialog.tsx +85 -0
- package/src/features/settings/git/GitRepositoryDisconnectDialog.tsx +60 -0
- package/src/features/settings/git/useGitIntegrationExperiment.ts +8 -0
- package/src/features/settings/git/useGitOwners.ts +12 -0
- package/src/features/settings/git/useGitRepos.ts +24 -0
- package/src/features/settings/git/useLinkedGitRepos.ts +41 -0
- package/src/features/settings/git/useWriteAPIToken.ts +23 -0
- package/src/features/slices/actions/createSlice.ts +5 -29
- package/src/features/slices/actions/deleteSlice.ts +33 -0
- package/src/features/slices/actions/renameSlice.ts +37 -0
- package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewSliceDialog.tsx +0 -1
- package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewVariationDialog.tsx +0 -1
- package/src/features/slices/convertLegacySlice/ConvertLegacySliceMergeWithIdenticalDialog.tsx +0 -1
- package/src/features/slices/sliceBuilder/SliceBuilderProvider.tsx +12 -10
- package/src/features/sync/AutoSyncProvider.tsx +252 -0
- package/src/features/sync/components/AutoSyncStatusIndicator.module.css +17 -0
- package/src/features/sync/components/AutoSyncStatusIndicator.module.css.d.ts +5 -0
- package/src/features/sync/components/AutoSyncStatusIndicator.tsx +58 -0
- package/src/features/sync/fetchUnSyncChanges.ts +40 -0
- package/src/features/sync/getUnSyncChanges.ts +144 -0
- package/src/features/sync/useUnSyncChanges.ts +32 -0
- package/src/hooks/useActionQueue.tsx +177 -0
- package/src/hooks/useAuthStatus.ts +12 -0
- package/src/hooks/useUser.ts +14 -0
- package/src/icons/SettingsIcon.tsx +19 -0
- package/src/icons/SyncFailed.tsx +30 -0
- package/src/icons/Synced.tsx +19 -0
- package/src/icons/Syncing.tsx +23 -0
- package/src/modules/availableCustomTypes/index.ts +29 -135
- package/src/modules/loading/types.ts +0 -5
- package/src/modules/modal/types.ts +0 -3
- package/src/modules/slices/index.ts +27 -193
- package/src/modules/slices/selector.ts +0 -14
- package/src/modules/useSliceMachineActions.ts +70 -89
- package/src/modules/userContext/index.ts +3 -3
- package/src/redux/saga.ts +0 -4
- package/test/__testutils__/index.tsx +8 -5
- package/test/src/modules/__fixtures__/serverState.ts +1 -2
- package/test/src/modules/availableCustomTypes/index.test.ts +2 -243
- package/test/src/modules/slices.test.ts +1 -267
- package/components/Navigation/ChangesListItem.tsx +0 -92
- package/components/Navigation/ChangesRightElement.tsx +0 -33
- package/out/_next/static/chunks/18539003-5cc559566b738915.js +0 -97
- package/out/_next/static/chunks/25-37d6fa7f064be04b.js +0 -1
- package/out/_next/static/chunks/318-ce7e74f18f2d5b22.js +0 -1
- package/out/_next/static/chunks/376-7e784d6069e01654.js +0 -8
- package/out/_next/static/chunks/727-3a3bc32518778317.js +0 -1
- package/out/_next/static/chunks/731-501990ebe6a71b32.js +0 -1
- package/out/_next/static/chunks/904-9a4c1264727ae89a.js +0 -1
- package/out/_next/static/chunks/pages/_app-cd9d4c76a4abc11a.js +0 -432
- package/out/_next/static/chunks/pages/changes-2a976f9efeb719aa.js +0 -1
- package/out/_next/static/chunks/pages/index-64cfb8b0948ba8ef.js +0 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-b2a9d773ce5d7739.js +0 -1
- package/out/_next/static/chunks/pages/slices-e9c70c082ca3e3f9.js +0 -1
- package/out/_next/static/css/200244e3e0463c39.css +0 -1
- package/out/_next/static/m6psnC5u1xT7Y6osWHKjp/_buildManifest.js +0 -1
- package/src/features/autoSave/useAutoSave.tsx +0 -177
- package/src/features/customTypes/__tests__/createCustomType.test.ts +0 -159
- package/src/features/customTypes/customTypesTable/createCustomType.ts +0 -107
- package/src/features/legacyState/actions/getLegacySliceMachineState.ts +0 -53
- package/src/features/legacyState/useLegacySliceMachineState.ts +0 -7
- package/src/hooks/useModelStatus.ts +0 -78
- package/src/hooks/useUnSyncChanges.ts +0 -88
- package/test/components/CreateCustomTypeModal.test.tsx +0 -73
- package/test/components/LoginModal.test.tsx +0 -67
- package/test/src/hooks/useModelStatus.test.ts +0 -115
- /package/out/_next/static/{m6psnC5u1xT7Y6osWHKjp → dS_xZnzzna6Y6uwcTgf90}/_ssgManifest.js +0 -0
- /package/src/features/{changes → sync}/actions/pushChanges.ts +0 -0
- /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
|
|
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 =
|
|
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
|
+
}
|
|
@@ -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,
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
[
|
|
84
|
+
[setNextAction, stableSaveSliceSuccess, syncChanges],
|
|
83
85
|
);
|
|
84
86
|
|
|
85
87
|
const contextValue: SliceContext = useMemo(
|
|
86
88
|
() => ({
|
|
87
|
-
|
|
89
|
+
actionQueueStatus,
|
|
88
90
|
slice,
|
|
89
91
|
setSlice,
|
|
90
92
|
variation,
|
|
91
93
|
}),
|
|
92
|
-
[
|
|
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,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
|
+
}
|