slice-machine-ui 1.23.2-beta.9 → 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 (130) 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/features/autoSave/AutoSaveStatusIndicator.tsx +4 -4
  68. package/src/features/customTypes/customTypesBuilder/CustomTypeProvider.tsx +11 -10
  69. package/src/features/customTypes/customTypesBuilder/CustomTypesBuilderPage.tsx +3 -3
  70. package/src/features/environments/actions/setEnvironment.ts +4 -4
  71. package/src/features/settings/SettingsPage.tsx +47 -0
  72. package/src/features/settings/git/ConnectGitRepository.tsx +126 -0
  73. package/src/features/settings/git/ConnectGitRepositoryBlankSlate.tsx +33 -0
  74. package/src/features/settings/git/GitOwnerSelect.tsx +71 -0
  75. package/src/features/settings/git/GitProvider.ts +40 -0
  76. package/src/features/settings/git/GitProviderConnectButtons.tsx +63 -0
  77. package/src/features/settings/git/GitRepositoriesList.tsx +76 -0
  78. package/src/features/settings/git/GitRepositoriesSearch.tsx +69 -0
  79. package/src/features/settings/git/GitRepositoryConnectDialog.tsx +85 -0
  80. package/src/features/settings/git/GitRepositoryDisconnectDialog.tsx +60 -0
  81. package/src/features/settings/git/useGitIntegrationExperiment.ts +8 -0
  82. package/src/features/settings/git/useGitOwners.ts +12 -0
  83. package/src/features/settings/git/useGitRepos.ts +24 -0
  84. package/src/features/settings/git/useLinkedGitRepos.ts +41 -0
  85. package/src/features/settings/git/useWriteAPIToken.ts +23 -0
  86. package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewSliceDialog.tsx +0 -1
  87. package/src/features/slices/convertLegacySlice/ConvertLegacySliceAsNewVariationDialog.tsx +0 -1
  88. package/src/features/slices/convertLegacySlice/ConvertLegacySliceMergeWithIdenticalDialog.tsx +0 -1
  89. package/src/features/slices/sliceBuilder/SliceBuilderProvider.tsx +12 -10
  90. package/src/features/sync/AutoSyncProvider.tsx +252 -0
  91. package/src/features/sync/components/AutoSyncStatusIndicator.module.css +17 -0
  92. package/src/features/sync/components/AutoSyncStatusIndicator.module.css.d.ts +5 -0
  93. package/src/features/sync/components/AutoSyncStatusIndicator.tsx +58 -0
  94. package/src/features/sync/fetchUnSyncChanges.ts +40 -0
  95. package/src/features/sync/getUnSyncChanges.ts +144 -0
  96. package/src/features/sync/useUnSyncChanges.ts +32 -0
  97. package/src/hooks/useActionQueue.tsx +177 -0
  98. package/src/hooks/useAuthStatus.ts +12 -0
  99. package/src/hooks/useUser.ts +14 -0
  100. package/src/icons/SettingsIcon.tsx +19 -0
  101. package/src/icons/SyncFailed.tsx +30 -0
  102. package/src/icons/Synced.tsx +19 -0
  103. package/src/icons/Syncing.tsx +23 -0
  104. package/test/__testutils__/index.tsx +8 -5
  105. package/test/src/modules/__fixtures__/serverState.ts +1 -2
  106. package/components/Navigation/ChangesListItem.tsx +0 -74
  107. package/components/Navigation/ChangesRightElement.tsx +0 -33
  108. package/out/_next/static/chunks/18539003-5cc559566b738915.js +0 -97
  109. package/out/_next/static/chunks/25-01f422cea8066349.js +0 -1
  110. package/out/_next/static/chunks/318-387e3f23d717c53d.js +0 -1
  111. package/out/_next/static/chunks/376-7e784d6069e01654.js +0 -8
  112. package/out/_next/static/chunks/727-358d7fae68a880b3.js +0 -1
  113. package/out/_next/static/chunks/799-9928a9413d9f0730.js +0 -1
  114. package/out/_next/static/chunks/904-1ff7519adece2b55.js +0 -1
  115. package/out/_next/static/chunks/pages/_app-9448839a9d20924d.js +0 -432
  116. package/out/_next/static/chunks/pages/changes-92c84f6cac0285fd.js +0 -1
  117. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-fecb771f91e70853.js +0 -1
  118. package/out/_next/static/chunks/pages/slices-b0b58192a87df822.js +0 -1
  119. package/out/_next/static/css/200244e3e0463c39.css +0 -1
  120. package/out/_next/static/sysU54bHKiGDh8pmVpi41/_buildManifest.js +0 -1
  121. package/src/features/autoSave/useAutoSave.tsx +0 -177
  122. package/src/features/legacyState/actions/getLegacySliceMachineState.ts +0 -53
  123. package/src/features/legacyState/useLegacySliceMachineState.ts +0 -7
  124. package/src/hooks/useModelStatus.ts +0 -78
  125. package/src/hooks/useUnSyncChanges.ts +0 -88
  126. package/test/components/LoginModal.test.tsx +0 -67
  127. package/test/src/hooks/useModelStatus.test.ts +0 -115
  128. /package/out/_next/static/{sysU54bHKiGDh8pmVpi41 → eNujlD43QIdmra7ggcNtK}/_ssgManifest.js +0 -0
  129. /package/src/features/{changes → sync}/actions/pushChanges.ts +0 -0
  130. /package/src/features/{changes → sync}/actions/trackPushChangesSuccess.ts +0 -0
package/.depcheckrc CHANGED
@@ -6,6 +6,9 @@ ignores:
6
6
  "@vitest/coverage-v8",
7
7
  "monocle-ts",
8
8
  "newtype-ts",
9
+ "postcss",
10
+ "postcss-flexbugs-fixes",
11
+ "postcss-preset-env",
9
12
  "semver",
10
13
  "start-slicemachine",
11
14
  "stylelint-config-css-modules",
@@ -28,7 +28,7 @@ export const OfflinePage = () => {
28
28
  <Text
29
29
  sx={{ fontSize: "13px", lineHeight: "24px", textAlign: "center" }}
30
30
  >
31
- You need a stable internet connection to review and push changes
31
+ Connect to the internet to sync your changes.
32
32
  </Text>
33
33
  </Flex>
34
34
  </Flex>
@@ -6,19 +6,23 @@ import { ChangesSectionHeader } from "@components/ChangesSectionHeader";
6
6
  import { CustomTypeTable } from "@components/CustomTypeTable/changesPage";
7
7
  import Grid from "@components/Grid";
8
8
  import { ComponentUI } from "@lib/models/common/ComponentUI";
9
- import { ModelStatusInformation } from "@src/hooks/useModelStatus";
10
9
  import ScreenshotChangesModal from "@components/ScreenshotChangesModal";
11
10
  import { countMissingScreenshots } from "@src/domain/slice";
12
11
  import { useScreenshotChangesModal } from "@src/hooks/useScreenshotChangesModal";
13
12
  import { ModelStatus } from "@lib/models/common/ModelStatus";
14
13
  import { LocalOrRemoteCustomType } from "@lib/models/common/ModelData";
15
14
  import { SharedSliceCard } from "@src/features/slices/sliceCards/SharedSliceCard";
15
+ import { AuthStatus } from "@src/modules/userContext/types";
16
+ import { ModelsStatuses } from "@src/features/sync/getUnSyncChanges";
16
17
 
17
18
  import { DevCollaborationExperiment } from "./DevCollaborationExperiment";
18
19
 
19
- interface ChangesItemsProps extends ModelStatusInformation {
20
+ interface ChangesItemsProps {
20
21
  unSyncedCustomTypes: LocalOrRemoteCustomType[];
21
22
  unSyncedSlices: ComponentUI[];
23
+ modelsStatuses: ModelsStatuses;
24
+ authStatus: AuthStatus;
25
+ isOnline: boolean;
22
26
  }
23
27
 
24
28
  export const ChangesItems: React.FC<ChangesItemsProps> = ({
@@ -1,7 +1,6 @@
1
1
  import Link from "next/link";
2
2
  import React from "react";
3
3
  import { Box, Text } from "theme-ui";
4
- import { ModelStatusInformation } from "@src/hooks/useModelStatus";
5
4
  import { CustomTypeSM } from "@lib/models/common/CustomType";
6
5
  import { ModelStatus } from "@lib/models/common/ModelStatus";
7
6
  import {
@@ -10,18 +9,26 @@ import {
10
9
  } from "@lib/models/common/ModelData";
11
10
  import { StatusBadge } from "@src/features/changes/StatusBadge";
12
11
  import { CUSTOM_TYPES_CONFIG } from "@src/features/customTypes/customTypesConfig";
12
+ import { AuthStatus } from "@src/modules/userContext/types";
13
+ import { ModelsStatuses } from "@src/features/sync/getUnSyncChanges";
13
14
 
14
- interface CustomTypeTableProps extends ModelStatusInformation {
15
+ interface CustomTypeTableProps {
15
16
  customTypes: LocalOrRemoteCustomType[];
17
+ modelsStatuses: ModelsStatuses;
18
+ authStatus: AuthStatus;
19
+ isOnline: boolean;
16
20
  }
17
21
 
18
22
  const firstColumnWidth = "40%";
19
23
  const secondColumnWidth = "40%";
20
24
  const thirdColumnWidth = "20%";
21
25
 
22
- const CustomTypeChangeRow: React.FC<
23
- { ct: CustomTypeSM; status: ModelStatus } & ModelStatusInformation
24
- > = ({ ct, status, authStatus, isOnline }) => {
26
+ const CustomTypeChangeRow: React.FC<{
27
+ ct: CustomTypeSM;
28
+ status: ModelStatus;
29
+ authStatus: AuthStatus;
30
+ isOnline: boolean;
31
+ }> = ({ ct, status, authStatus, isOnline }) => {
25
32
  return (
26
33
  <>
27
34
  <Box as={"td"} style={{ width: firstColumnWidth }}>
@@ -80,7 +87,6 @@ export const CustomTypeTable: React.FC<CustomTypeTableProps> = ({
80
87
  status={modelsStatuses.customTypes[customType.local.id]}
81
88
  authStatus={authStatus}
82
89
  isOnline={isOnline}
83
- modelsStatuses={modelsStatuses}
84
90
  key={customType.local.id}
85
91
  />
86
92
  </tr>
@@ -97,7 +103,6 @@ export const CustomTypeTable: React.FC<CustomTypeTableProps> = ({
97
103
  status={modelsStatuses.customTypes[customType.remote.id]}
98
104
  authStatus={authStatus}
99
105
  isOnline={isOnline}
100
- modelsStatuses={modelsStatuses}
101
106
  key={customType.remote.id}
102
107
  />
103
108
  </tr>
@@ -7,6 +7,7 @@ import useSliceMachineActions from "@src/modules/useSliceMachineActions";
7
7
  import Card from "@components/Card";
8
8
  import { Button } from "@components/Button";
9
9
  import { deleteSlice } from "@src/features/slices/actions/deleteSlice";
10
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
10
11
 
11
12
  type DeleteSliceModalProps = {
12
13
  isOpen: boolean;
@@ -21,6 +22,7 @@ export const DeleteSliceModal: React.FunctionComponent<
21
22
  > = ({ sliceId, sliceName, libName, isOpen, onClose }) => {
22
23
  const [isDeleting, setIsDeleting] = useState(false);
23
24
  const { deleteSliceSuccess } = useSliceMachineActions();
25
+ const { syncChanges } = useAutoSync();
24
26
  const { theme } = useThemeUI();
25
27
 
26
28
  const onDelete = async () => {
@@ -32,6 +34,7 @@ export const DeleteSliceModal: React.FunctionComponent<
32
34
  libraryID: libName,
33
35
  onSuccess: () => {
34
36
  deleteSliceSuccess(sliceId, libName);
37
+ syncChanges();
35
38
  },
36
39
  });
37
40
 
@@ -52,6 +52,7 @@ export const DeleteVariationModal: FC<DeleteVariationModalProps> = ({
52
52
  <DialogActions
53
53
  ok={{
54
54
  text: "Delete",
55
+ color: "tomato",
55
56
  onClick: () => {
56
57
  if (!variation) return;
57
58
  void (async () => {
@@ -21,6 +21,7 @@ import {
21
21
  } from "@src/features/customTypes/actions/createCustomType";
22
22
  import { CUSTOM_TYPES_CONFIG } from "@src/features/customTypes/customTypesConfig";
23
23
  import { getFormat } from "@src/domain/customType";
24
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
24
25
 
25
26
  import { InputBox } from "../components/InputBox";
26
27
  import { SelectRepeatable } from "../components/SelectRepeatable";
@@ -58,6 +59,7 @@ export const CreateCustomTypeModal: React.FC<CreateCustomTypeModalProps> = ({
58
59
  );
59
60
  const customTypesMessages = CUSTOM_TYPES_MESSAGES[format];
60
61
  const [isIdFieldPristine, setIsIdFieldPristine] = useState(true);
62
+ const { syncChanges } = useAutoSync();
61
63
  const router = useRouter();
62
64
 
63
65
  const onSubmit = async ({ id, label, repeatable }: FormValues) => {
@@ -86,6 +88,8 @@ export const CreateCustomTypeModal: React.FC<CreateCustomTypeModalProps> = ({
86
88
  }
87
89
  : undefined,
88
90
  });
91
+
92
+ syncChanges();
89
93
  },
90
94
  });
91
95
 
@@ -9,6 +9,7 @@ import ModalFormCard from "@components/ModalFormCard";
9
9
  import { createSlice } from "@src/features/slices/actions/createSlice";
10
10
  import useSliceMachineActions from "@src/modules/useSliceMachineActions";
11
11
  import { getState } from "@src/apiClient";
12
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
12
13
 
13
14
  import { validateSliceModalValues } from "../formsValidator";
14
15
  import { InputBox } from "../components/InputBox";
@@ -30,6 +31,7 @@ export const CreateSliceModal: FC<CreateSliceModalProps> = ({
30
31
  }) => {
31
32
  const { createSliceSuccess } = useSliceMachineActions();
32
33
  const [isCreatingSlice, setIsCreatingSlice] = useState(false);
34
+ const { syncChanges } = useAutoSync();
33
35
 
34
36
  const onSubmit = async (values: FormValues) => {
35
37
  const sliceName = values.sliceName;
@@ -45,8 +47,8 @@ export const CreateSliceModal: FC<CreateSliceModalProps> = ({
45
47
  const serverState = await getState();
46
48
  // Update Redux store
47
49
  createSliceSuccess(serverState.libraries);
48
-
49
50
  onSuccess(newSlice, libraryName);
51
+ syncChanges();
50
52
  },
51
53
  });
52
54
  };
@@ -13,6 +13,7 @@ import { CustomTypeFormat } from "@slicemachine/manager";
13
13
  import { CUSTOM_TYPES_MESSAGES } from "@src/features/customTypes/customTypesMessages";
14
14
 
15
15
  import { renameCustomType } from "@src/features/customTypes/actions/renameCustomType";
16
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
16
17
 
17
18
  interface RenameCustomTypeModalProps {
18
19
  isChangesLocal: boolean;
@@ -32,6 +33,7 @@ export const RenameCustomTypeModal: React.FC<RenameCustomTypeModalProps> = ({
32
33
  const customTypeName = customType?.label ?? "";
33
34
  const customTypeId = customType?.id ?? "";
34
35
  const { renameCustomTypeSuccess } = useSliceMachineActions();
36
+ const { syncChanges } = useAutoSync();
35
37
 
36
38
  const [isRenaming, setIsRenaming] = useState(false);
37
39
 
@@ -46,7 +48,10 @@ export const RenameCustomTypeModal: React.FC<RenameCustomTypeModalProps> = ({
46
48
  await renameCustomType({
47
49
  model: customType,
48
50
  newLabel: values.customTypeName,
49
- onSuccess: renameCustomTypeSuccess,
51
+ onSuccess: (renamedCustomType) => {
52
+ renameCustomTypeSuccess(renamedCustomType);
53
+ syncChanges();
54
+ },
50
55
  });
51
56
  }
52
57
  setIsRenaming(false);
@@ -6,6 +6,7 @@ import { SliceMachineStoreType } from "@src/redux/type";
6
6
  import { getLibraries, getRemoteSlices } from "@src/modules/slices";
7
7
  import { ComponentUI } from "@lib/models/common/ComponentUI";
8
8
  import { renameSlice } from "@src/features/slices/actions/renameSlice";
9
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
9
10
 
10
11
  import { InputBox } from "../components/InputBox";
11
12
  import ModalFormCard from "../../ModalFormCard";
@@ -24,6 +25,7 @@ export const RenameSliceModal: React.FC<RenameSliceModalProps> = ({
24
25
  onClose,
25
26
  }) => {
26
27
  const { renameSliceSuccess } = useSliceMachineActions();
28
+ const { syncChanges } = useAutoSync();
27
29
  const { localLibs, remoteLibs } = useSelector(
28
30
  (store: SliceMachineStoreType) => ({
29
31
  localLibs: getLibraries(store),
@@ -39,6 +41,7 @@ export const RenameSliceModal: React.FC<RenameSliceModalProps> = ({
39
41
  newSliceName: values.sliceName,
40
42
  onSuccess: (renamedSlice) => {
41
43
  renameSliceSuccess(renamedSlice.from, renamedSlice.model);
44
+ syncChanges();
42
45
  },
43
46
  });
44
47
 
@@ -45,7 +45,6 @@ export const RenameVariationModal: FC<RenameVariationModalProps> = ({
45
45
  <DialogHeader icon="edit" title="Rename variation" />
46
46
  <DialogContent>
47
47
  <Formik
48
- validateOnChange
49
48
  validateOnMount
50
49
  initialValues={{ variationName: variation?.name ?? "" }}
51
50
  validate={(values) => {
@@ -11,7 +11,7 @@ import {
11
11
  Text,
12
12
  } from "theme-ui";
13
13
  import SliceMachineModal from "@components/SliceMachineModal";
14
- import { checkAuthStatus, startAuth } from "@src/apiClient";
14
+ import { checkAuthStatus, getState, startAuth } from "@src/apiClient";
15
15
  import { buildEndpoints } from "@lib/prismic/endpoints";
16
16
  import { startPolling } from "@lib/utils/poll";
17
17
  import { CheckAuthStatusResponse } from "@models/common/Auth";
@@ -25,6 +25,12 @@ import { getEnvironment } from "@src/modules/environment";
25
25
  import useSliceMachineActions from "@src/modules/useSliceMachineActions";
26
26
  import preferWroomBase from "@lib/utils/preferWroomBase";
27
27
  import { ToasterType } from "@src/modules/toaster";
28
+ import { getUnSyncedChanges } from "@src/features/sync/getUnSyncChanges";
29
+ import { normalizeFrontendCustomTypes } from "@lib/models/common/normalizers/customType";
30
+ import { normalizeFrontendSlices } from "@lib/models/common/normalizers/slices";
31
+ import { AuthStatus } from "@src/modules/userContext/types";
32
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
33
+ import { getActiveEnvironment } from "@src/features/environments/actions/getActiveEnvironment";
28
34
 
29
35
  interface ValidAuthStatus extends CheckAuthStatusResponse {
30
36
  status: "ok";
@@ -42,7 +48,7 @@ const LoginModal: React.FunctionComponent = () => {
42
48
  env: getEnvironment(store),
43
49
  }),
44
50
  );
45
-
51
+ const { syncChanges } = useAutoSync();
46
52
  const { closeModals, startLoadingLogin, stopLoadingLogin, openToaster } =
47
53
  useSliceMachineActions();
48
54
 
@@ -72,6 +78,38 @@ const LoginModal: React.FunctionComponent = () => {
72
78
  openToaster("Logged in", ToasterType.SUCCESS);
73
79
  stopLoadingLogin();
74
80
  closeModals();
81
+
82
+ const serverState = await getState();
83
+ const slices = normalizeFrontendSlices(
84
+ serverState.libraries,
85
+ serverState.remoteSlices,
86
+ );
87
+ const customTypes = Object.values(
88
+ normalizeFrontendCustomTypes(
89
+ serverState.customTypes,
90
+ serverState.remoteCustomTypes,
91
+ ),
92
+ );
93
+ const { changedCustomTypes, changedSlices } = getUnSyncedChanges({
94
+ authStatus: AuthStatus.AUTHORIZED,
95
+ customTypes,
96
+ isOnline: true,
97
+ libraries: serverState.libraries,
98
+ slices,
99
+ });
100
+ const { activeEnvironment } = await getActiveEnvironment();
101
+
102
+ if (
103
+ activeEnvironment?.kind === "dev" &&
104
+ (changedCustomTypes.length > 0 || changedSlices.length > 0)
105
+ ) {
106
+ syncChanges({
107
+ environment: activeEnvironment,
108
+ loggedIn: true,
109
+ changedCustomTypes,
110
+ changedSlices,
111
+ });
112
+ }
75
113
  } catch (e) {
76
114
  stopLoadingLogin();
77
115
  openToaster("Login failed", ToasterType.ERROR);
@@ -81,7 +81,6 @@ function ModalCard<Values extends FormikValues>({
81
81
  }}
82
82
  >
83
83
  <Formik
84
- validateOnChange
85
84
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
86
85
  initialValues={initialValues}
87
86
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
@@ -0,0 +1,117 @@
1
+ import { type FC } from "react";
2
+ import { useRouter } from "next/router";
3
+ import { Box, Button, Text } from "@prismicio/editor-ui";
4
+
5
+ import {
6
+ HoverCard,
7
+ HoverCardCloseButton,
8
+ HoverCardDescription,
9
+ HoverCardMedia,
10
+ HoverCardTitle,
11
+ } from "@src/components/HoverCard";
12
+ import useSliceMachineActions from "@src/modules/useSliceMachineActions";
13
+ import { useNetwork } from "@src/hooks/useNetwork";
14
+ import { useAuthStatus } from "@src/hooks/useAuthStatus";
15
+ import { useUnSyncChanges } from "@src/features/sync/useUnSyncChanges";
16
+ import { AuthStatus } from "@src/modules/userContext/types";
17
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
18
+ import { AutoSyncStatusIndicator } from "@src/features/sync/components/AutoSyncStatusIndicator";
19
+
20
+ export const ChangesItem: FC = () => {
21
+ const { setSeenChangesToolTip } = useSliceMachineActions();
22
+ const open = useOpenChangesHoverCard();
23
+ const router = useRouter();
24
+ const { autoSyncStatus } = useAutoSync();
25
+
26
+ const onClose = () => {
27
+ setSeenChangesToolTip();
28
+ };
29
+
30
+ return (
31
+ <HoverCard
32
+ open={open}
33
+ openDelay={3000}
34
+ onClose={onClose}
35
+ side="right"
36
+ sideOffset={24}
37
+ collisionPadding={280}
38
+ trigger={
39
+ <Box padding={{ bottom: 24 }}>
40
+ {autoSyncStatus === "failed" ||
41
+ autoSyncStatus === "synced" ||
42
+ autoSyncStatus === "syncing" ? (
43
+ <AutoSyncStatusIndicator autoSyncStatus={autoSyncStatus} />
44
+ ) : (
45
+ // TODO(DT-1942): This should be a Button with a link component for
46
+ // accessibility
47
+ <Button
48
+ color="grey"
49
+ onClick={() => {
50
+ void router.push("/changes");
51
+ }}
52
+ sx={{ width: "100%" }}
53
+ >
54
+ <Box alignItems="center" gap={4}>
55
+ <Text variant="bold">Review changes</Text> <ChangesCount />
56
+ </Box>
57
+ </Button>
58
+ )}
59
+ </Box>
60
+ }
61
+ >
62
+ <HoverCardTitle>Push your changes</HoverCardTitle>
63
+ <HoverCardMedia component="image" src="/push.png" />
64
+ <HoverCardDescription>
65
+ When you click Save, your changes are saved locally. Then, you can push
66
+ your models to Prismic from the Changes page.
67
+ </HoverCardDescription>
68
+ <HoverCardCloseButton>Got it</HoverCardCloseButton>
69
+ </HoverCard>
70
+ );
71
+ };
72
+
73
+ export const ChangesCount: FC = () => {
74
+ const isOnline = useNetwork();
75
+ const authStatus = useAuthStatus();
76
+ const { unSyncedSlices, unSyncedCustomTypes } = useUnSyncChanges();
77
+ const numberOfChanges = unSyncedSlices.length + unSyncedCustomTypes.length;
78
+
79
+ if (
80
+ !isOnline ||
81
+ authStatus === AuthStatus.UNAUTHORIZED ||
82
+ authStatus === AuthStatus.FORBIDDEN
83
+ ) {
84
+ return null;
85
+ }
86
+
87
+ if (numberOfChanges === 0) {
88
+ return null;
89
+ }
90
+
91
+ const formattedNumberOfChanges = numberOfChanges > 9 ? "+9" : numberOfChanges;
92
+
93
+ return (
94
+ <Box padding={{ inline: 6 }} borderRadius={10} backgroundColor="grey5">
95
+ <Text color="grey11" variant="small">
96
+ {formattedNumberOfChanges}
97
+ </Text>
98
+ </Box>
99
+ );
100
+ };
101
+
102
+ // TODO(DT-1925): Reactivate this feature
103
+ const useOpenChangesHoverCard = () => {
104
+ // const { hasSeenChangesToolTip, hasSeenSimulatorToolTip } = useSelector(
105
+ // (store: SliceMachineStoreType) => ({
106
+ // hasSeenChangesToolTip: userHasSeenChangesToolTip(store),
107
+ // hasSeenSimulatorToolTip: userHasSeenSimulatorToolTip(store),
108
+ // }),
109
+ // );
110
+
111
+ // return (
112
+ // !hasSeenChangesToolTip &&
113
+ // hasSeenSimulatorToolTip
114
+ // );
115
+
116
+ return false;
117
+ };
@@ -1,25 +1,40 @@
1
+ import { useState } from "react";
1
2
  import {
2
3
  Environment as EnvironmentType,
3
4
  isUnauthenticatedError,
4
5
  isUnauthorizedError,
5
6
  } from "@slicemachine/manager/client";
6
7
 
7
- import { telemetry } from "@src/apiClient";
8
+ import { getState, telemetry } from "@src/apiClient";
8
9
  import { useEnvironments } from "@src/features/environments/useEnvironments";
9
10
  import { setEnvironment } from "@src/features/environments/actions/setEnvironment";
10
11
  import { useActiveEnvironment } from "@src/features/environments/useActiveEnvironment";
11
- import { getLegacySliceMachineState } from "@src/features/legacyState/actions/getLegacySliceMachineState";
12
12
  import useSliceMachineActions from "@src/modules/useSliceMachineActions";
13
13
  import { SideNavEnvironmentSelector } from "@src/components/SideNav";
14
14
  import { useNetwork } from "@src/hooks/useNetwork";
15
+ import { useAutoSync } from "@src/features/sync/AutoSyncProvider";
16
+ import { normalizeFrontendSlices } from "@lib/models/common/normalizers/slices";
17
+ import { normalizeFrontendCustomTypes } from "@lib/models/common/normalizers/customType";
18
+ import { getUnSyncedChanges } from "@src/features/sync/getUnSyncChanges";
19
+ import { useAuthStatus } from "@src/hooks/useAuthStatus";
15
20
 
16
21
  export function Environment() {
17
22
  const { environments, error: useEnvironmentsError } = useEnvironments();
18
23
  const { activeEnvironment } = useActiveEnvironment();
19
24
  const { refreshState, openLoginModal } = useSliceMachineActions();
25
+ const { syncChanges } = useAutoSync();
20
26
  const isOnline = useNetwork();
27
+ const authStatus = useAuthStatus();
28
+ const [isSwitchingEnv, setIsSwitchingEnv] = useState(false);
29
+ const { autoSyncStatus } = useAutoSync();
21
30
 
22
31
  async function onSelect(environment: EnvironmentType) {
32
+ if (activeEnvironment?.name === environment.name) {
33
+ return;
34
+ }
35
+
36
+ setIsSwitchingEnv(true);
37
+
23
38
  void telemetry.track({
24
39
  event: "environment:switch",
25
40
  domain: environment.domain,
@@ -27,9 +42,39 @@ export function Environment() {
27
42
 
28
43
  await setEnvironment(environment);
29
44
 
30
- const legacySliceMachineState = await getLegacySliceMachineState();
45
+ const serverState = await getState();
46
+ refreshState(serverState);
47
+
48
+ const slices = normalizeFrontendSlices(
49
+ serverState.libraries,
50
+ serverState.remoteSlices,
51
+ );
52
+ const customTypes = Object.values(
53
+ normalizeFrontendCustomTypes(
54
+ serverState.customTypes,
55
+ serverState.remoteCustomTypes,
56
+ ),
57
+ );
58
+ const { changedCustomTypes, changedSlices } = getUnSyncedChanges({
59
+ authStatus,
60
+ customTypes,
61
+ isOnline,
62
+ libraries: serverState.libraries,
63
+ slices,
64
+ });
65
+
66
+ if (
67
+ environment.kind === "dev" &&
68
+ (changedCustomTypes.length > 0 || changedSlices.length > 0)
69
+ ) {
70
+ syncChanges({
71
+ environment,
72
+ changedCustomTypes,
73
+ changedSlices,
74
+ });
75
+ }
31
76
 
32
- refreshState(legacySliceMachineState);
77
+ setIsSwitchingEnv(false);
33
78
  }
34
79
 
35
80
  if (!isOnline) {
@@ -42,6 +87,8 @@ export function Environment() {
42
87
  environments={environments}
43
88
  activeEnvironment={activeEnvironment}
44
89
  onSelect={onSelect}
90
+ disabled={isSwitchingEnv || autoSyncStatus === "syncing"}
91
+ loading={isSwitchingEnv}
45
92
  />
46
93
  );
47
94
  }
@@ -1,6 +1,5 @@
1
1
  import { sprinkles } from "@prismicio/editor-ui";
2
2
 
3
3
  export const environmentDivider = sprinkles({
4
- marginTop: 16,
5
- marginBottom: 32,
4
+ marginBlock: 16,
6
5
  });
@@ -131,10 +131,8 @@ describe("Side Navigation", () => {
131
131
  test.skip.each([
132
132
  ["Page types", "/"],
133
133
  ["Custom types", "/custom-types"],
134
- ["Changes", "/changes"],
135
-
136
- ["Changelog", "/changelog"],
137
134
  ["Slices", "/slices"],
135
+ ["Changelog", "/changelog"],
138
136
  // TODO: once we have a plan fo how to display individual libraries change this
139
137
  // ["Slices/a", "/slices#slices/a"],
140
138
  // ["Slices/b", "/slices#slices/b"]
@@ -156,95 +154,17 @@ describe("Side Navigation", () => {
156
154
  },
157
155
  );
158
156
 
159
- test("should display the number of changes on the 'Changes' item", async () => {
157
+ test("should display the number of changes on the 'Review changes' item", async () => {
160
158
  renderSideNavigation({ canUpdate: true, hasChanges: true });
161
159
 
162
- expect(await screen.findByText("Changes")).toBeVisible();
160
+ expect(await screen.findByText("Review changes")).toBeVisible();
163
161
  const changesItem = screen
164
- .getByText("Changes")
165
- .closest("a") as HTMLAnchorElement;
162
+ .getByText("Review changes")
163
+ .closest("button") as HTMLButtonElement;
166
164
 
167
165
  expect(within(changesItem).getByText("1")).toBeVisible();
168
166
  });
169
167
 
170
- test("should not display the number of changes or 'Disconnect' on the 'Changes' item", async () => {
171
- renderSideNavigation({ canUpdate: true, hasChanges: false });
172
-
173
- expect(await screen.findByText("Changes")).toBeVisible();
174
- const changesItem = screen
175
- .getByText("Changes")
176
- .closest("a") as HTMLAnchorElement;
177
-
178
- expect(within(changesItem).queryByText("1")).not.toBeInTheDocument();
179
- expect(
180
- within(changesItem).queryByText("Logged out"),
181
- ).not.toBeInTheDocument();
182
- });
183
-
184
- test("should display the information that user is disconnected on the 'Changes' item when unauthorized", async () => {
185
- renderSideNavigation({
186
- canUpdate: true,
187
- hasChanges: true,
188
- authStatus: AuthStatus.UNAUTHORIZED,
189
- });
190
-
191
- expect(await screen.findByText("Changes")).toBeVisible();
192
- const changesItem = screen
193
- .getByText("Changes")
194
- .closest("a") as HTMLAnchorElement;
195
-
196
- expect(within(changesItem).getByText("Logged out")).toBeVisible();
197
- });
198
-
199
- test("should display the information that user is disconnected on the 'Changes' item when forbidden", async () => {
200
- renderSideNavigation({
201
- canUpdate: true,
202
- hasChanges: true,
203
- authStatus: AuthStatus.FORBIDDEN,
204
- });
205
-
206
- expect(await screen.findByText("Changes")).toBeVisible();
207
- const changesItem = screen
208
- .getByText("Changes")
209
- .closest("a") as HTMLAnchorElement;
210
-
211
- expect(within(changesItem).getByText("Logged out")).toBeVisible();
212
- });
213
-
214
- test("should display the information that user is disconnected on the 'Changes' item when offline", async () => {
215
- vi.spyOn(navigator, "onLine", "get").mockReturnValueOnce(false);
216
-
217
- renderSideNavigation({
218
- canUpdate: true,
219
- hasChanges: true,
220
- });
221
-
222
- expect(await screen.findByText("Changes")).toBeVisible();
223
- const changesItem = screen
224
- .getByText("Changes")
225
- .closest("a") as HTMLAnchorElement;
226
-
227
- expect(within(changesItem).getByText("Logged out")).toBeVisible();
228
- });
229
-
230
- test("should not display the information that user is disconnected when user is online", async () => {
231
- vi.spyOn(navigator, "onLine", "get").mockReturnValueOnce(true);
232
-
233
- renderSideNavigation({
234
- canUpdate: true,
235
- hasChanges: true,
236
- });
237
-
238
- expect(await screen.findByText("Changes")).toBeVisible();
239
- const changesItem = screen
240
- .getByText("Changes")
241
- .closest("a") as HTMLAnchorElement;
242
-
243
- expect(
244
- within(changesItem).queryByText("Logged out"),
245
- ).not.toBeInTheDocument();
246
- });
247
-
248
168
  test("Video Item with next", async (ctx) => {
249
169
  const adapter = createTestPlugin({
250
170
  meta: {