slice-machine-ui 1.23.2-beta.8 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +3 -0
- package/components/DeleteVariationModal/index.tsx +1 -0
- package/components/Forms/CreateCustomTypeModal/CreateCustomTypeModal.tsx +4 -0
- package/components/Forms/CreateSliceModal/CreateSliceModal.tsx +3 -1
- package/components/Forms/RenameCustomTypeModal/RenameCustomTypeModal.tsx +6 -1
- package/components/Forms/RenameSliceModal/RenameSliceModal.tsx +3 -0
- 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/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/{labs-ee206005948f8e87.js → labs-2adfb5127f9c4081.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-52c3e1f9d14160b8.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/eNujlD43QIdmra7ggcNtK/_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 +10 -8
- package/pages/_app.tsx +34 -32
- package/pages/changes.tsx +27 -41
- package/pages/settings.tsx +1 -0
- package/postcss.config.js +10 -0
- 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 +26 -19
- package/src/domain/customType.ts +22 -5
- package/src/features/autoSave/AutoSaveStatusIndicator.tsx +4 -4
- package/src/features/customTypes/customTypesBuilder/CustomTypeProvider.tsx +11 -10
- package/src/features/customTypes/customTypesBuilder/CustomTypesBuilderPage.tsx +3 -3
- 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/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/test/__testutils__/index.tsx +8 -5
- package/test/src/modules/__fixtures__/serverState.ts +1 -2
- package/components/Navigation/ChangesListItem.tsx +0 -74
- package/components/Navigation/ChangesRightElement.tsx +0 -33
- package/out/_next/static/chunks/18539003-5cc559566b738915.js +0 -97
- package/out/_next/static/chunks/25-01f422cea8066349.js +0 -1
- package/out/_next/static/chunks/318-387e3f23d717c53d.js +0 -1
- package/out/_next/static/chunks/376-7e784d6069e01654.js +0 -8
- package/out/_next/static/chunks/727-358d7fae68a880b3.js +0 -1
- package/out/_next/static/chunks/799-d95a93e965557aeb.js +0 -1
- package/out/_next/static/chunks/904-1ff7519adece2b55.js +0 -1
- package/out/_next/static/chunks/pages/_app-e31adcf75e9176d3.js +0 -432
- package/out/_next/static/chunks/pages/changes-92c84f6cac0285fd.js +0 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-608d206d10a719e3.js +0 -1
- package/out/_next/static/chunks/pages/slices-b0b58192a87df822.js +0 -1
- package/out/_next/static/css/200244e3e0463c39.css +0 -1
- package/out/_next/static/vxOhh_oqUIQ5PTSKUKgMR/_buildManifest.js +0 -1
- package/src/features/autoSave/useAutoSave.tsx +0 -177
- 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/LoginModal.test.tsx +0 -67
- package/test/src/hooks/useModelStatus.test.ts +0 -115
- /package/out/_next/static/{vxOhh_oqUIQ5PTSKUKgMR → eNujlD43QIdmra7ggcNtK}/_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,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
|
+
}
|
|
@@ -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
|
+
};
|