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
@@ -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
+ }
@@ -0,0 +1,144 @@
1
+ import { ComponentUI } from "@lib/models/common/ComponentUI";
2
+ import { LibraryUI } from "@lib/models/common/LibraryUI";
3
+ import {
4
+ LocalOrRemoteCustomType,
5
+ LocalOrRemoteSlice,
6
+ RemoteOnlySlice,
7
+ getModelId,
8
+ hasLocal,
9
+ isRemoteOnly,
10
+ } from "@lib/models/common/ModelData";
11
+ import {
12
+ ChangedCustomType,
13
+ ChangedSlice,
14
+ ModelStatus,
15
+ computeStatuses,
16
+ } from "@lib/models/common/ModelStatus";
17
+ import { AuthStatus } from "@src/modules/userContext/types";
18
+
19
+ type GetUnSyncedChangesArgs = {
20
+ customTypes: LocalOrRemoteCustomType[];
21
+ libraries: Readonly<LibraryUI[]>;
22
+ slices: LocalOrRemoteSlice[];
23
+ isOnline: boolean;
24
+ authStatus: AuthStatus;
25
+ };
26
+
27
+ const unSyncStatuses = [
28
+ ModelStatus.New,
29
+ ModelStatus.Modified,
30
+ ModelStatus.Deleted,
31
+ ];
32
+
33
+ export type UnSyncedChanges = {
34
+ changedCustomTypes: ChangedCustomType[];
35
+ unSyncedCustomTypes: LocalOrRemoteCustomType[];
36
+ changedSlices: ChangedSlice[];
37
+ unSyncedSlices: ComponentUI[];
38
+ modelsStatuses: ModelsStatuses;
39
+ };
40
+
41
+ export function getUnSyncedChanges(
42
+ args: GetUnSyncedChangesArgs,
43
+ ): UnSyncedChanges {
44
+ const { customTypes, libraries, slices, isOnline, authStatus } = args;
45
+
46
+ const modelsStatuses = getModelStatus({
47
+ slices,
48
+ customTypes,
49
+ isOnline,
50
+ authStatus,
51
+ });
52
+
53
+ const localComponents: ComponentUI[] = libraries.flatMap(
54
+ (lib) => lib.components,
55
+ );
56
+ const deletedComponents: ComponentUI[] = slices
57
+ .filter(isRemoteOnly)
58
+ .map(wrapDeletedSlice);
59
+ const components: ComponentUI[] = localComponents
60
+ .concat(deletedComponents)
61
+ .sort((s1, s2) => (s1.model.name > s2.model.name ? 1 : -1));
62
+
63
+ const unSyncedSlices = components.filter(
64
+ (component) =>
65
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
66
+ modelsStatuses.slices[component.model.id] &&
67
+ unSyncStatuses.includes(modelsStatuses.slices[component.model.id]),
68
+ );
69
+ const unSyncedCustomTypes = customTypes.filter(
70
+ (customType) =>
71
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
72
+ modelsStatuses.customTypes[getModelId(customType)] &&
73
+ unSyncStatuses.includes(
74
+ modelsStatuses.customTypes[getModelId(customType)],
75
+ ),
76
+ );
77
+
78
+ const changedSlices = unSyncedSlices
79
+ .map((s) => ({
80
+ slice: s,
81
+ status: modelsStatuses.slices[s.model.id],
82
+ }))
83
+ .filter((s): s is ChangedSlice => unSyncStatuses.includes(s.status));
84
+ const changedCustomTypes = unSyncedCustomTypes
85
+ .map((model) => (hasLocal(model) ? model.local : model.remote))
86
+ .map((ct) => ({
87
+ customType: ct,
88
+ status: modelsStatuses.customTypes[ct.id],
89
+ }))
90
+ .filter((c): c is ChangedCustomType => unSyncStatuses.includes(c.status));
91
+
92
+ return {
93
+ changedCustomTypes,
94
+ changedSlices,
95
+ unSyncedCustomTypes,
96
+ unSyncedSlices,
97
+ modelsStatuses,
98
+ };
99
+ }
100
+
101
+ // ComponentUI are manipulated on all the relevant pages
102
+ // But the data is not available for remote only slices
103
+ // which have been deleted locally
104
+ // Should revisit this with the sync improvements
105
+ function wrapDeletedSlice(s: RemoteOnlySlice): ComponentUI {
106
+ return {
107
+ model: s.remote,
108
+ screenshots: {},
109
+ from: "",
110
+ href: "",
111
+ pathToSlice: "",
112
+ fileName: "",
113
+ extension: "",
114
+ };
115
+ }
116
+
117
+ type GetModelStatusArgs = {
118
+ slices: LocalOrRemoteSlice[];
119
+ customTypes: LocalOrRemoteCustomType[];
120
+ isOnline: boolean;
121
+ authStatus: AuthStatus;
122
+ };
123
+
124
+ // Slices and Custom Types needs to be separated as Ids are not unique amongst each others.
125
+ export type ModelsStatuses = {
126
+ slices: { [sliceId: string]: ModelStatus };
127
+ customTypes: { [ctId: string]: ModelStatus };
128
+ };
129
+
130
+ export const getModelStatus = (args: GetModelStatusArgs): ModelsStatuses => {
131
+ const { slices, customTypes, isOnline, authStatus } = args;
132
+
133
+ const userHasAccessToModels =
134
+ isOnline &&
135
+ authStatus != AuthStatus.FORBIDDEN &&
136
+ authStatus != AuthStatus.UNAUTHORIZED;
137
+
138
+ const modelsStatuses = {
139
+ slices: computeStatuses(slices, userHasAccessToModels),
140
+ customTypes: computeStatuses(customTypes, userHasAccessToModels),
141
+ };
142
+
143
+ return modelsStatuses;
144
+ };
@@ -0,0 +1,32 @@
1
+ import { useMemo } from "react";
2
+ import { useSelector } from "react-redux";
3
+
4
+ import { selectAllCustomTypes } from "@src/modules/availableCustomTypes";
5
+ import { getFrontendSlices, getLibraries } from "@src/modules/slices";
6
+ import { SliceMachineStoreType } from "@src/redux/type";
7
+ import { useAuthStatus } from "@src/hooks/useAuthStatus";
8
+
9
+ import { useNetwork } from "../../hooks/useNetwork";
10
+ import { UnSyncedChanges, getUnSyncedChanges } from "./getUnSyncChanges";
11
+
12
+ export const useUnSyncChanges = (): UnSyncedChanges => {
13
+ const { customTypes, slices, libraries } = useSelector(
14
+ (store: SliceMachineStoreType) => ({
15
+ customTypes: selectAllCustomTypes(store),
16
+ slices: getFrontendSlices(store),
17
+ libraries: getLibraries(store),
18
+ }),
19
+ );
20
+ const isOnline = useNetwork();
21
+ const authStatus = useAuthStatus();
22
+
23
+ const unSyncedChange = getUnSyncedChanges({
24
+ authStatus,
25
+ customTypes,
26
+ isOnline,
27
+ libraries,
28
+ slices,
29
+ });
30
+
31
+ return useMemo(() => unSyncedChange, [unSyncedChange]);
32
+ };