slice-machine-ui 2.18.1-beta.7 → 2.18.1-beta.8

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 (63) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/{Zy4opMpDS8I-mxQ1kA3m2 → U9WVTg1pSTJVt94SbRE5Y}/_buildManifest.js +1 -1
  3. package/out/_next/static/chunks/{248-43faecb386a16518.js → 248-a9c33f7fbe2da2f1.js} +1 -1
  4. package/out/_next/static/chunks/34-50c64778da33cff6.js +1 -0
  5. package/out/_next/static/chunks/429-aab52070cad2884b.js +3 -0
  6. package/out/_next/static/chunks/{489-234ed5471aa694b3.js → 489-a046ae0fa4f74435.js} +1 -1
  7. package/out/_next/static/chunks/{630-bb6e3db525588f16.js → 630-eae75e90b30f201a.js} +1 -1
  8. package/out/_next/static/chunks/pages/{_app-64feff311539f17b.js → _app-1b4541325b4a29ab.js} +40 -40
  9. package/out/_next/static/chunks/pages/{changelog-ba55ed247c20dc21.js → changelog-3901f2fc937d9648.js} +1 -1
  10. package/out/_next/static/chunks/pages/changes-c58a60af39167147.js +1 -0
  11. package/out/_next/static/chunks/pages/{labs-78ac01d97ab02cd7.js → labs-dd04033db23a58f3.js} +1 -1
  12. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/{simulator-b127d948a17968d3.js → simulator-f237aaa755dc9a99.js} +1 -1
  13. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-da423aa0da7bc11e.js → [variation]-c8c3e7ffb898261c.js} +1 -1
  14. package/out/_next/static/chunks/pages/{slices-59b857edc9f72d5f.js → slices-0aaae79dcc7b8c55.js} +1 -1
  15. package/out/changelog.html +1 -1
  16. package/out/changes.html +1 -1
  17. package/out/custom-types/[customTypeId].html +1 -1
  18. package/out/custom-types.html +1 -1
  19. package/out/index.html +1 -1
  20. package/out/labs.html +1 -1
  21. package/out/page-types/[pageTypeId].html +1 -1
  22. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  23. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  24. package/out/slices.html +1 -1
  25. package/package.json +3 -3
  26. package/src/features/auth/LogoutButton.tsx +42 -36
  27. package/src/features/builder/fields/contentRelationship/ContentRelationshipFieldPicker.tsx +3 -3
  28. package/src/features/changes/StatusBadge.tsx +1 -9
  29. package/src/features/customTypes/customTypesBuilder/PageSnippetDialog/PageSnippetDialog.tsx +3 -3
  30. package/src/features/customTypes/customTypesTable/CustomTypesTablePage.tsx +3 -3
  31. package/src/features/environments/actions/useSetEnvironment.ts +22 -0
  32. package/src/features/environments/useActiveEnvironment.ts +17 -9
  33. package/src/features/environments/useEnvironments.ts +11 -8
  34. package/src/features/errorBoundaries/AppStateErrorBoundary.tsx +108 -0
  35. package/src/{ErrorBoundary.tsx → features/errorBoundaries/DefaultErrorBoundary.tsx} +2 -2
  36. package/src/features/errorBoundaries/index.ts +2 -0
  37. package/src/features/labs/labsList/LabsPage.tsx +3 -3
  38. package/src/features/navigation/Navigation.tsx +7 -8
  39. package/src/features/slices/sliceBuilder/FloatingBackButton.tsx +3 -3
  40. package/src/features/sync/AutoSyncProvider.tsx +2 -2
  41. package/src/features/sync/getUnSyncChanges.ts +1 -3
  42. package/src/legacy/components/AppLayout/index.tsx +10 -85
  43. package/src/legacy/components/ChangesEmptyState/UnauthenticatedView.tsx +31 -0
  44. package/src/legacy/components/ChangesEmptyState/index.ts +1 -1
  45. package/src/legacy/components/ChangesItems/ChangesItems.tsx +3 -3
  46. package/src/legacy/components/LoginModal/index.tsx +14 -6
  47. package/src/legacy/components/Navigation/ChangesItem.tsx +2 -6
  48. package/src/legacy/components/Navigation/Environment.tsx +2 -7
  49. package/src/legacy/components/Navigation/SideNavEnvironmentSelector/SideNavEnvironmentSelector.tsx +3 -8
  50. package/src/legacy/components/Simulator/index.tsx +3 -3
  51. package/src/legacy/lib/builders/CustomTypeBuilder/TabZone/index.tsx +3 -3
  52. package/src/modules/userContext/index.ts +5 -5
  53. package/src/modules/userContext/types.ts +2 -3
  54. package/src/pages/_app.tsx +93 -95
  55. package/src/pages/changes.tsx +4 -5
  56. package/src/queryClient.tsx +24 -0
  57. package/test/__testutils__/index.tsx +13 -10
  58. package/out/_next/static/chunks/157-fa8b348c960c8283.js +0 -3
  59. package/out/_next/static/chunks/34-8d9d9b2944824750.js +0 -1
  60. package/out/_next/static/chunks/pages/changes-4c23263cdc8e59c6.js +0 -1
  61. package/src/features/environments/actions/setEnvironment.ts +0 -18
  62. package/src/legacy/components/ChangesEmptyState/AuthErrorPage.tsx +0 -44
  63. /package/out/_next/static/{Zy4opMpDS8I-mxQ1kA3m2 → U9WVTg1pSTJVt94SbRE5Y}/_ssgManifest.js +0 -0
@@ -83,7 +83,7 @@ export const AutoSyncProvider: FC<PropsWithChildren> = (props) => {
83
83
 
84
84
  // We default to a full user logged in with internet access if not provider.
85
85
  // This is useful when we want to sync changes right after the user logs in.
86
- loggedIn = isOnline && authStatus === AuthStatus.AUTHORIZED,
86
+ loggedIn = isOnline && authStatus === AuthStatus.AUTHENTICATED,
87
87
  } = args;
88
88
 
89
89
  if (!loggedIn || environment?.kind !== "dev") {
@@ -240,7 +240,7 @@ function getAutoSyncStatus(args: GetAutoSyncStatusArgs): AutoSyncStatus {
240
240
  return "offline";
241
241
  }
242
242
 
243
- if (authStatus !== AuthStatus.AUTHORIZED) {
243
+ if (authStatus !== AuthStatus.AUTHENTICATED) {
244
244
  return "not-logged-in";
245
245
  }
246
246
 
@@ -149,9 +149,7 @@ export const getModelStatus = (args: GetModelStatusArgs): ModelsStatuses => {
149
149
  const { slices, customTypes, isOnline, authStatus } = args;
150
150
 
151
151
  const userHasAccessToModels =
152
- isOnline &&
153
- authStatus != AuthStatus.FORBIDDEN &&
154
- authStatus != AuthStatus.UNAUTHORIZED;
152
+ isOnline && authStatus === AuthStatus.AUTHENTICATED;
155
153
 
156
154
  const modelsStatuses = {
157
155
  slices: computeStatuses(slices, userHasAccessToModels),
@@ -1,16 +1,6 @@
1
- import {
2
- BlankSlate,
3
- BlankSlateDescription,
4
- BlankSlateIcon,
5
- BlankSlateTitle,
6
- Box,
7
- Button,
8
- ButtonGroup,
9
- Text,
10
- } from "@prismicio/editor-ui";
11
- import { isUnauthorizedError } from "@slicemachine/manager/client";
1
+ import { Box, Button, ButtonGroup } from "@prismicio/editor-ui";
12
2
  import { useRouter } from "next/router";
13
- import { FC, PropsWithChildren, Suspense, useState } from "react";
3
+ import { FC, PropsWithChildren, Suspense } from "react";
14
4
 
15
5
  import { Breadcrumb } from "@/components/Breadcrumb";
16
6
  import {
@@ -19,8 +9,6 @@ import {
19
9
  PageLayoutHeader,
20
10
  PageLayoutPane,
21
11
  } from "@/components/PageLayout";
22
- import { ErrorBoundary } from "@/ErrorBoundary";
23
- import { LogoutButton } from "@/features/auth/LogoutButton";
24
12
  import { useActiveEnvironment } from "@/features/environments/useActiveEnvironment";
25
13
  import { Navigation } from "@/features/navigation/Navigation";
26
14
 
@@ -29,80 +17,17 @@ export const AppLayout: FC<PropsWithChildren> = ({
29
17
  ...otherProps
30
18
  }) => {
31
19
  return (
32
- <ErrorBoundary
33
- renderError={(error) => {
34
- return (
35
- <Box
36
- position="absolute"
37
- top={64}
38
- width="100%"
39
- justifyContent="center"
40
- flexDirection="column"
41
- >
42
- <BlankSlate>
43
- <BlankSlateIcon
44
- lineColor="tomato11"
45
- backgroundColor="tomato3"
46
- name="alert"
47
- />
48
- <BlankSlateTitle>Failed to load Slice Machine</BlankSlateTitle>
49
- <BlankSlateDescription>
50
- <RenderError error={error} />
51
- </BlankSlateDescription>
52
- </BlankSlate>
53
- </Box>
54
- );
55
- }}
56
- >
57
- <Suspense>
58
- <PageLayoutWithActiveEnvironment {...otherProps}>
59
- <PageLayoutPane>
60
- <Navigation />
61
- </PageLayoutPane>
62
- {children}
63
- </PageLayoutWithActiveEnvironment>
64
- </Suspense>
65
- </ErrorBoundary>
20
+ <Suspense>
21
+ <PageLayoutWithActiveEnvironment {...otherProps}>
22
+ <PageLayoutPane>
23
+ <Navigation />
24
+ </PageLayoutPane>
25
+ {children}
26
+ </PageLayoutWithActiveEnvironment>
27
+ </Suspense>
66
28
  );
67
29
  };
68
30
 
69
- function RenderError(args: { error: unknown }) {
70
- const { error } = args;
71
-
72
- if (isUnauthorizedError(error)) {
73
- return <UnauthorizedErrorView />;
74
- }
75
- return <>{JSON.stringify(error)}</>;
76
- }
77
-
78
- function UnauthorizedErrorView() {
79
- const [isLoggingOut, setIsLoggingOut] = useState(false);
80
-
81
- return (
82
- <Box flexDirection="column" gap={16} margin={{ top: 8 }}>
83
- <Box flexDirection="column" gap={8} alignItems="center">
84
- <Text variant="h3" align="center">
85
- It seems like you don't have access to this repository
86
- </Text>
87
- <Text align="center">
88
- Check that the repository name is correct, then contact your
89
- repository administrator.
90
- </Text>
91
- </Box>
92
- <LogoutButton
93
- isLoading={isLoggingOut}
94
- onLogoutSuccess={() => {
95
- setIsLoggingOut(true);
96
- window.location.reload();
97
- }}
98
- sx={{ alignSelf: "center" }}
99
- >
100
- Log out
101
- </LogoutButton>
102
- </Box>
103
- );
104
- }
105
-
106
31
  const environmentTopBorderColorMap = {
107
32
  prod: "purple",
108
33
  stage: "indigo",
@@ -0,0 +1,31 @@
1
+ import { Box, Button, Text } from "@prismicio/editor-ui";
2
+
3
+ import useSliceMachineActions from "@/modules/useSliceMachineActions";
4
+
5
+ export const UnauthenticatedView = () => {
6
+ const { openLoginModal } = useSliceMachineActions();
7
+
8
+ return (
9
+ <Box
10
+ flexDirection="column"
11
+ height="100%"
12
+ alignItems="center"
13
+ justifyContent="center"
14
+ gap={8}
15
+ >
16
+ <Text variant="h3" align="center">
17
+ It seems like you are logged out
18
+ </Text>
19
+ <Text align="center">Log in to connect to your repository.</Text>
20
+ <Text align="center">
21
+ If that doesn't work, it's possible that Slice Machine is having trouble
22
+ accessing Prismic's servers.{" "}
23
+ <Text href="https://community.prismic.io/">
24
+ Contact our support team
25
+ </Text>
26
+ .
27
+ </Text>
28
+ <Button onClick={() => openLoginModal()}>Log in to Prismic</Button>
29
+ </Box>
30
+ );
31
+ };
@@ -1,2 +1,2 @@
1
- export { AuthErrorPage } from "./AuthErrorPage";
2
1
  export { OfflinePage } from "./OfflinePage";
2
+ export { UnauthenticatedView } from "./UnauthenticatedView";
@@ -3,7 +3,7 @@ import React, { Suspense } from "react";
3
3
  import { AiOutlineExclamationCircle } from "react-icons/ai";
4
4
 
5
5
  import { countMissingScreenshots } from "@/domain/slice";
6
- import { ErrorBoundary } from "@/ErrorBoundary";
6
+ import { DefaultErrorBoundary } from "@/features/errorBoundaries";
7
7
  import { SharedSliceCard } from "@/features/slices/sliceCards/SharedSliceCard";
8
8
  import { ModelsStatuses } from "@/features/sync/getUnSyncChanges";
9
9
  import { useScreenshotChangesModal } from "@/hooks/useScreenshotChangesModal";
@@ -146,11 +146,11 @@ export const ChangesItems: React.FC<ChangesItemsProps> = ({
146
146
  )}
147
147
  </Box>
148
148
 
149
- <ErrorBoundary>
149
+ <DefaultErrorBoundary>
150
150
  <Suspense>
151
151
  <DevCollaborationExperiment />
152
152
  </Suspense>
153
- </ErrorBoundary>
153
+ </DefaultErrorBoundary>
154
154
  </Box>
155
155
  </>
156
156
  );
@@ -1,3 +1,4 @@
1
+ import { useQueryClient } from "@tanstack/react-query";
1
2
  import React from "react";
2
3
  import Modal from "react-modal";
3
4
  import { useSelector } from "react-redux";
@@ -15,8 +16,8 @@ import {
15
16
 
16
17
  import { checkAuthStatus, clearAuth, getState } from "@/apiClient";
17
18
  import { getActiveEnvironment } from "@/features/environments/actions/getActiveEnvironment";
18
- import { invalidateActiveEnvironmentData } from "@/features/environments/useActiveEnvironment";
19
- import { invalidateEnvironmentsData } from "@/features/environments/useEnvironments";
19
+ import { GetActiveEnvironmentQueryKey } from "@/features/environments/useActiveEnvironment";
20
+ import { GetEnvironmentsQueryKey } from "@/features/environments/useEnvironments";
20
21
  import { useAutoSync } from "@/features/sync/AutoSyncProvider";
21
22
  import { getUnSyncedChanges } from "@/features/sync/getUnSyncChanges";
22
23
  import SliceMachineModal from "@/legacy/components/SliceMachineModal";
@@ -54,6 +55,7 @@ const LoginModal: React.FunctionComponent = () => {
54
55
  const { syncChanges } = useAutoSync();
55
56
  const { closeModals, startLoadingLogin, stopLoadingLogin, refreshState } =
56
57
  useSliceMachineActions();
58
+ const queryClient = useQueryClient();
57
59
 
58
60
  const prismicBase = preferWroomBase(env.manifest.apiEndpoint);
59
61
  const loginRedirectUrl = `${
@@ -79,8 +81,14 @@ const LoginModal: React.FunctionComponent = () => {
79
81
  );
80
82
 
81
83
  // refresh queries to update the UI
82
- invalidateEnvironmentsData();
83
- invalidateActiveEnvironmentData();
84
+ await Promise.all([
85
+ queryClient.invalidateQueries({
86
+ queryKey: GetEnvironmentsQueryKey,
87
+ }),
88
+ queryClient.invalidateQueries({
89
+ queryKey: GetActiveEnvironmentQueryKey,
90
+ }),
91
+ ]);
84
92
 
85
93
  toast.success("Logged in");
86
94
  stopLoadingLogin();
@@ -100,7 +108,7 @@ const LoginModal: React.FunctionComponent = () => {
100
108
  ),
101
109
  );
102
110
  const { changedCustomTypes, changedSlices } = getUnSyncedChanges({
103
- authStatus: AuthStatus.AUTHORIZED,
111
+ authStatus: AuthStatus.AUTHENTICATED,
104
112
  customTypes,
105
113
  isOnline: true,
106
114
  libraries: serverState.libraries,
@@ -127,7 +135,7 @@ const LoginModal: React.FunctionComponent = () => {
127
135
 
128
136
  return (
129
137
  <SliceMachineModal
130
- isOpen={isOpen}
138
+ isOpen={isLoginLoading || isOpen}
131
139
  shouldCloseOnOverlayClick
132
140
  onRequestClose={closeModals}
133
141
  contentLabel={"login_modal"}
@@ -82,16 +82,12 @@ export const ChangesCount: FC<ChangesCountProps> = ({ color }) => {
82
82
 
83
83
  if (
84
84
  !isOnline ||
85
- authStatus === AuthStatus.UNAUTHORIZED ||
86
- authStatus === AuthStatus.FORBIDDEN
85
+ authStatus !== AuthStatus.AUTHENTICATED ||
86
+ numberOfChanges === 0
87
87
  ) {
88
88
  return null;
89
89
  }
90
90
 
91
- if (numberOfChanges === 0) {
92
- return null;
93
- }
94
-
95
91
  const formattedNumberOfChanges = numberOfChanges > 9 ? "+9" : numberOfChanges;
96
92
 
97
93
  return (
@@ -5,7 +5,7 @@ import {
5
5
  import { useState } from "react";
6
6
 
7
7
  import { getState, telemetry } from "@/apiClient";
8
- import { setEnvironment } from "@/features/environments/actions/setEnvironment";
8
+ import { useSetEnvironment } from "@/features/environments/actions/useSetEnvironment";
9
9
  import { useActiveEnvironment } from "@/features/environments/useActiveEnvironment";
10
10
  import { useEnvironments } from "@/features/environments/useEnvironments";
11
11
  import { useAutoSync } from "@/features/sync/AutoSyncProvider";
@@ -28,6 +28,7 @@ export function Environment() {
28
28
  const authStatus = useAuthStatus();
29
29
  const [isSwitchingEnv, setIsSwitchingEnv] = useState(false);
30
30
  const { autoSyncStatus } = useAutoSync();
31
+ const setEnvironment = useSetEnvironment();
31
32
 
32
33
  async function onSelect(environment: EnvironmentType) {
33
34
  if (activeEnvironment?.name === environment.name) {
@@ -105,10 +106,4 @@ export function Environment() {
105
106
  />
106
107
  );
107
108
  }
108
-
109
- if (useEnvironmentsError !== undefined) {
110
- throw useEnvironmentsError;
111
- }
112
-
113
- throw activeEnvironmentError;
114
109
  }
@@ -17,9 +17,8 @@ import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
17
17
  import type { Environment } from "@slicemachine/manager/client";
18
18
  import { clsx } from "clsx";
19
19
  import type { FC, ReactNode } from "react";
20
- import { toast } from "react-toastify";
21
20
 
22
- import { LogoutButton } from "@/features/auth/LogoutButton";
21
+ import { EnvironmentLogoutButton } from "@/features/auth/LogoutButton";
23
22
  import { LoginIcon } from "@/icons/LoginIcon";
24
23
  import { LogoIcon } from "@/icons/LogoIcon";
25
24
 
@@ -30,7 +29,7 @@ type SideNavEnvironmentSelectorProps = {
30
29
  disabled?: boolean;
31
30
  environments?: Environment[];
32
31
  loading?: boolean;
33
- variant?: "default" | "offline" | "unauthorized" | "unauthenticated";
32
+ variant?: "default" | "offline" | "unauthenticated";
34
33
  onLogInClick?: () => void;
35
34
  onSelect?: (environment: Environment) => void | Promise<void>;
36
35
  };
@@ -141,11 +140,7 @@ export const SideNavEnvironmentSelector: FC<SideNavEnvironmentSelectorProps> = (
141
140
  />
142
141
  ) : undefined}
143
142
 
144
- {variant === "default" && (
145
- <LogoutButton
146
- onLogoutSuccess={() => toast.success("Logged out")}
147
- />
148
- )}
143
+ {variant === "default" && <EnvironmentLogoutButton />}
149
144
  </Box>
150
145
  </>
151
146
  )}
@@ -17,7 +17,7 @@ import { toast } from "react-toastify";
17
17
  import { BaseStyles, Box, Flex, Spinner } from "theme-ui";
18
18
 
19
19
  import { saveSliceMock, telemetry } from "@/apiClient";
20
- import { ErrorBoundary } from "@/ErrorBoundary";
20
+ import { DefaultErrorBoundary } from "@/features/errorBoundaries";
21
21
  import useThrottle from "@/hooks/useThrottle";
22
22
  import ScreenshotPreviewModal from "@/legacy/components/ScreenshotPreviewModal";
23
23
  import { ComponentUI } from "@/legacy/lib/models/common/ComponentUI";
@@ -287,7 +287,7 @@ const Simulator: FC<SimulatorProps> = ({ slice, variation }) => {
287
287
  overflowY: "auto",
288
288
  }}
289
289
  >
290
- <ErrorBoundary
290
+ <DefaultErrorBoundary
291
291
  renderError={() => (
292
292
  <DefaultErrorMessage
293
293
  title="Editor error"
@@ -315,7 +315,7 @@ const Simulator: FC<SimulatorProps> = ({ slice, variation }) => {
315
315
  />
316
316
  </QueryClientProvider>
317
317
  </Suspense>
318
- </ErrorBoundary>
318
+ </DefaultErrorBoundary>
319
319
  </Flex>
320
320
  ) : null}
321
321
  </Flex>
@@ -20,8 +20,8 @@ import {
20
20
  reorderField,
21
21
  updateField,
22
22
  } from "@/domain/customType";
23
- import { ErrorBoundary } from "@/ErrorBoundary";
24
23
  import { useCustomTypeState } from "@/features/customTypes/customTypesBuilder/CustomTypeProvider";
24
+ import { DefaultErrorBoundary } from "@/features/errorBoundaries";
25
25
  import {
26
26
  CustomTypes,
27
27
  type TabField,
@@ -246,7 +246,7 @@ const TabZone: FC<TabZoneProps> = ({ tabId }) => {
246
246
  };
247
247
 
248
248
  return (
249
- <ErrorBoundary>
249
+ <DefaultErrorBoundary>
250
250
  <Suspense
251
251
  fallback={
252
252
  <Box padding={32}>
@@ -297,7 +297,7 @@ const TabZone: FC<TabZoneProps> = ({ tabId }) => {
297
297
  />
298
298
  </List>
299
299
  </Suspense>
300
- </ErrorBoundary>
300
+ </DefaultErrorBoundary>
301
301
  );
302
302
  };
303
303
 
@@ -1,3 +1,4 @@
1
+ import { UnauthenticatedError } from "@slicemachine/manager/client";
1
2
  import { Reducer } from "redux";
2
3
  import { ActionType, createAction, getType } from "typesafe-actions";
3
4
 
@@ -90,13 +91,12 @@ const getAuthStatus = (
90
91
  ): AuthStatus => {
91
92
  switch (clientError?.status) {
92
93
  case undefined: {
93
- return AuthStatus.AUTHORIZED;
94
- }
95
- case 403: {
96
- return AuthStatus.UNAUTHORIZED;
94
+ return AuthStatus.AUTHENTICATED;
97
95
  }
98
96
  case 401: {
99
- return AuthStatus.FORBIDDEN;
97
+ if (clientError.name === new UnauthenticatedError().name) {
98
+ return AuthStatus.UNAUTHENTICATED;
99
+ }
100
100
  }
101
101
  default: {
102
102
  return AuthStatus.UNKNOWN;
@@ -1,7 +1,6 @@
1
1
  export enum AuthStatus {
2
- AUTHORIZED = "authorized",
3
- UNAUTHORIZED = "unauthorized",
4
- FORBIDDEN = "forbidden",
2
+ AUTHENTICATED = "authenticated",
3
+ UNAUTHENTICATED = "unauthenticated",
5
4
  UNKNOWN = "unknown",
6
5
  }
7
6
 
@@ -13,34 +13,33 @@ import "@/styles/starry-night.css";
13
13
  import "@/styles/tabs.css";
14
14
  import "@/styles/toaster.css";
15
15
 
16
+ import { ThemeProvider, TooltipProvider } from "@prismicio/editor-ui";
16
17
  import {
17
- Box,
18
- DefaultErrorMessage,
19
- ThemeProvider,
20
- TooltipProvider,
21
- } from "@prismicio/editor-ui";
22
- import { ConnectedRouter } from "connected-next-router";
18
+ isInvalidActiveEnvironmentError,
19
+ isUnauthorizedError,
20
+ } from "@slicemachine/manager/client";
21
+ import { useSuspenseQuery } from "@tanstack/react-query";
22
+ import { ConnectedRouter as StoreConnectedRouter } from "connected-next-router";
23
23
  import type { NextPage } from "next";
24
24
  import type { AppContext, AppInitialProps } from "next/app";
25
25
  import dynamic from "next/dynamic";
26
26
  import Head from "next/head";
27
27
  import Router from "next/router";
28
- import { type FC, type ReactNode, Suspense, useEffect, useState } from "react";
28
+ import { type FC, type ReactNode, Suspense, useEffect } from "react";
29
29
  import { Provider } from "react-redux";
30
- import type { Store } from "redux";
31
- import type { Persistor } from "redux-persist/es/types";
32
30
  import { PersistGate } from "redux-persist/integration/react";
33
31
  import { ThemeProvider as ThemeUIThemeProvider, useThemeUI } from "theme-ui";
34
32
 
35
33
  import { getState } from "@/apiClient";
36
- import { ErrorBoundary } from "@/ErrorBoundary";
34
+ import { useActiveEnvironment } from "@/features/environments/useActiveEnvironment";
35
+ import { AppStateErrorBoundary } from "@/features/errorBoundaries";
37
36
  import { AutoSyncProvider } from "@/features/sync/AutoSyncProvider";
38
37
  import { RouteChangeProvider } from "@/hooks/useRouteChange";
39
38
  import SliceMachineApp from "@/legacy/components/App";
40
39
  import LoadingPage from "@/legacy/components/LoadingPage";
41
40
  import ToastContainer from "@/legacy/components/ToasterContainer";
42
41
  import { normalizeFrontendCustomTypes } from "@/legacy/lib/models/common/normalizers/customType";
43
- import type ServerState from "@/legacy/lib/models/server/ServerState";
42
+ import { QueryClientProvider } from "@/queryClient";
44
43
  import configureStore from "@/redux/store";
45
44
  import theme from "@/theme";
46
45
 
@@ -68,50 +67,10 @@ const RemoveDarkMode: FC<RemoveDarkModeProps> = ({ children }) => {
68
67
  return <>{children}</>;
69
68
  };
70
69
 
71
- function App({
72
- Component,
73
- pageProps,
74
- }: AppContextWithComponentLayout & AppInitialProps) {
75
- const [serverState, setServerState] = useState<ServerState | null>(null);
76
- const [smStore, setSMStore] = useState<{
77
- store: Store;
78
- persistor: Persistor;
79
- } | null>(null);
70
+ function App(props: AppContextWithComponentLayout & AppInitialProps) {
71
+ const { Component } = props;
80
72
 
81
- useEffect(() => {
82
- async function getInitialState() {
83
- const serverState = await getState();
84
- setServerState(serverState);
85
- }
86
- void getInitialState();
87
- }, []);
88
-
89
- useEffect(() => {
90
- if (!serverState || smStore) {
91
- return;
92
- }
93
-
94
- const normalizedCustomTypes = normalizeFrontendCustomTypes(
95
- serverState.customTypes,
96
- serverState.remoteCustomTypes,
97
- );
98
-
99
- const { store, persistor } = configureStore({
100
- environment: serverState.env,
101
- availableCustomTypes: {
102
- ...normalizedCustomTypes,
103
- },
104
- slices: {
105
- libraries: serverState.libraries,
106
- remoteSlices: serverState.remoteSlices,
107
- },
108
- });
109
-
110
- setSMStore({ store, persistor });
111
- }, [serverState, smStore]);
112
-
113
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
114
- const ComponentLayout = Component.CustomLayout || SliceMachineApp;
73
+ const ComponentLayout = Component.CustomLayout ?? SliceMachineApp;
115
74
 
116
75
  return (
117
76
  <>
@@ -119,50 +78,89 @@ function App({
119
78
  <title>Slice Machine</title>
120
79
  </Head>
121
80
  <ThemeUIThemeProvider theme={theme}>
122
- <RemoveDarkMode>
123
- <ThemeProvider mode="light">
124
- <TooltipProvider>
125
- {!smStore || !serverState ? (
126
- <LoadingPage />
127
- ) : (
128
- <Provider store={smStore.store}>
129
- <ConnectedRouter Router={Router}>
130
- <PersistGate loading={null} persistor={smStore.persistor}>
131
- <ErrorBoundary
132
- renderError={() => (
133
- <Box
134
- justifyContent="center"
135
- width="100%"
136
- padding={80}
137
- >
138
- <DefaultErrorMessage
139
- title="Error"
140
- description="An error occurred while rendering the app."
141
- />
142
- </Box>
143
- )}
144
- >
145
- <Suspense fallback={<LoadingPage />}>
146
- <AutoSyncProvider>
147
- <RouteChangeProvider>
148
- <ComponentLayout>
149
- <Component {...pageProps} />
150
- </ComponentLayout>
151
- </RouteChangeProvider>
152
- </AutoSyncProvider>
153
- </Suspense>
154
- </ErrorBoundary>
155
- </PersistGate>
156
- </ConnectedRouter>
157
- <ToastContainer />
158
- </Provider>
159
- )}
160
- </TooltipProvider>
161
- </ThemeProvider>
162
- </RemoveDarkMode>
81
+ <QueryClientProvider>
82
+ <RemoveDarkMode>
83
+ <ThemeProvider mode="light">
84
+ <TooltipProvider>
85
+ <AppStateErrorBoundary>
86
+ <Suspense fallback={<LoadingPage />}>
87
+ <AppStateValidator>
88
+ <AppStateWrapper>
89
+ <AutoSyncProvider>
90
+ <ComponentLayout>
91
+ <Component {...props.pageProps} />
92
+ </ComponentLayout>
93
+ </AutoSyncProvider>
94
+ </AppStateWrapper>
95
+ </AppStateValidator>
96
+ </Suspense>
97
+ </AppStateErrorBoundary>
98
+ <ToastContainer />
99
+ </TooltipProvider>
100
+ </ThemeProvider>
101
+ </RemoveDarkMode>
102
+ </QueryClientProvider>
163
103
  </ThemeUIThemeProvider>
164
104
  </>
165
105
  );
166
106
  }
167
107
 
108
+ /** This is where we should check for unwanted states that should prevent the
109
+ * user from using the app, and trigger the {@link AppStateErrorBoundary} to
110
+ * display something explaining why. */
111
+ function AppStateValidator(props: { children: ReactNode }) {
112
+ const activeEnvironment = useActiveEnvironment({ suspense: true });
113
+
114
+ if (
115
+ // We're using the fetchEnvironments request to check this because it can
116
+ // return an SMUnauthorizedError or SMInvalidActiveEnvironmentError
117
+ // according to the API response, so we handle both cases with just one
118
+ // request. We also perform the request in other parts of the app, so it can
119
+ // reuse its cache.
120
+ isUnauthorizedError(activeEnvironment.error) ||
121
+ isInvalidActiveEnvironmentError(activeEnvironment.error)
122
+ ) {
123
+ throw activeEnvironment.error;
124
+ }
125
+
126
+ return <>{props.children}</>;
127
+ }
128
+
129
+ function AppStateWrapper({ children }: { children: ReactNode }) {
130
+ const { data: state } = useSuspenseQuery({
131
+ queryKey: ["getInitialState"],
132
+ queryFn: async () => {
133
+ const serverState = await getState();
134
+ const { store, persistor } = configureStore({
135
+ environment: serverState.env,
136
+ availableCustomTypes: {
137
+ ...normalizeFrontendCustomTypes(
138
+ serverState.customTypes,
139
+ serverState.remoteCustomTypes,
140
+ ),
141
+ },
142
+ slices: {
143
+ libraries: serverState.libraries,
144
+ remoteSlices: serverState.remoteSlices,
145
+ },
146
+ });
147
+
148
+ return { serverState, store, persistor };
149
+ },
150
+ // avoid refetching
151
+ staleTime: Infinity,
152
+ gcTime: Infinity,
153
+ });
154
+
155
+ return (
156
+ <Provider store={state.store}>
157
+ <StoreConnectedRouter Router={Router}>
158
+ <PersistGate loading={null} persistor={state.persistor}>
159
+ <RouteChangeProvider>{children}</RouteChangeProvider>
160
+ </PersistGate>
161
+ </StoreConnectedRouter>
162
+ </Provider>
163
+ );
164
+ }
165
+
168
166
  export default dynamic(() => Promise.resolve(App), { ssr: false });