slice-machine-ui 2.18.1-alpha.jp-unauthorized-error.4 → 2.18.1-alpha.jp-unauthorized-error-improvement.1

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 (76) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/{eWxsTVqoYTTRkUB_nHhoh → bWd9YPY0k1NsrjJCNbscH}/_buildManifest.js +1 -1
  3. package/out/_next/static/chunks/{248-bdbfde18c5a04eae.js → 248-bcf03aa3c62e7dfb.js} +1 -1
  4. package/out/_next/static/chunks/34-50c64778da33cff6.js +1 -0
  5. package/out/_next/static/chunks/422-c9192a1dbdd2ae0e.js +1 -0
  6. package/out/_next/static/chunks/429-1137c819c2bf6b66.js +3 -0
  7. package/out/_next/static/chunks/{489-6edb99e269996dd1.js → 489-77d1fe67b6d0f0f8.js} +1 -1
  8. package/out/_next/static/chunks/630-c34de0401b8a0375.js +1 -0
  9. package/out/_next/static/chunks/pages/{_app-d1098ab653091b72.js → _app-d298cdfd4509fb06.js} +40 -40
  10. package/out/_next/static/chunks/pages/changelog-3901f2fc937d9648.js +1 -0
  11. package/out/_next/static/chunks/pages/changes-62d4e18795217cba.js +1 -0
  12. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-7102c23f96cd1768.js → [customTypeId]-816acb31b652239b.js} +1 -1
  13. package/out/_next/static/chunks/pages/{labs-d79597003a1ff74e.js → labs-5f5c63fb1f7d4b92.js} +1 -1
  14. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-d4bc920a5efffa0a.js → [pageTypeId]-669d5479e81b638b.js} +1 -1
  15. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-eca10940c9083d4c.js +1 -0
  16. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-b4700a6332ea828c.js +1 -0
  17. package/out/_next/static/chunks/pages/slices-bf63f937b184b470.js +1 -0
  18. package/out/changelog.html +1 -1
  19. package/out/changes.html +1 -1
  20. package/out/custom-types/[customTypeId].html +1 -1
  21. package/out/custom-types.html +1 -1
  22. package/out/index.html +1 -1
  23. package/out/labs.html +1 -1
  24. package/out/page-types/[pageTypeId].html +1 -1
  25. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  26. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  27. package/out/slices.html +1 -1
  28. package/package.json +3 -6
  29. package/src/errorBoundaries.tsx +150 -0
  30. package/src/features/auth/LogoutButton.tsx +42 -36
  31. package/src/features/builder/fields/contentRelationship/ContentRelationshipFieldPicker.tsx +3 -3
  32. package/src/features/changes/StatusBadge.tsx +1 -9
  33. package/src/features/customTypes/customTypesBuilder/PageSnippetDialog/PageSnippetDialog.tsx +3 -3
  34. package/src/features/customTypes/customTypesTable/CustomTypesTablePage.tsx +3 -3
  35. package/src/features/environments/actions/useSetEnvironment.ts +22 -0
  36. package/src/features/environments/useActiveEnvironment.ts +17 -9
  37. package/src/features/environments/useEnvironments.ts +11 -8
  38. package/src/features/labs/labsList/LabsPage.tsx +3 -3
  39. package/src/features/navigation/Navigation.tsx +31 -8
  40. package/src/features/navigation/NavigationItem.tsx +9 -3
  41. package/src/features/navigation/SliceMachineVersion.tsx +1 -6
  42. package/src/features/slices/sliceBuilder/FloatingBackButton.tsx +3 -3
  43. package/src/features/slices/sliceBuilder/McpPromoLink.tsx +29 -0
  44. package/src/features/sync/actions/pushChanges.ts +1 -0
  45. package/src/features/sync/getUnSyncChanges.ts +23 -7
  46. package/src/legacy/components/AppLayout/index.tsx +10 -85
  47. package/src/legacy/components/ChangesEmptyState/UnauthenticatedView.tsx +31 -0
  48. package/src/legacy/components/ChangesEmptyState/index.ts +1 -1
  49. package/src/legacy/components/ChangesItems/ChangesItems.tsx +3 -3
  50. package/src/legacy/components/LoginModal/index.tsx +13 -5
  51. package/src/legacy/components/Navigation/ChangesItem.tsx +2 -6
  52. package/src/legacy/components/Navigation/Environment.tsx +2 -7
  53. package/src/legacy/components/Navigation/SideNavEnvironmentSelector/SideNavEnvironmentSelector.tsx +3 -8
  54. package/src/legacy/components/Simulator/index.tsx +3 -3
  55. package/src/legacy/lib/builders/CustomTypeBuilder/TabZone/index.tsx +3 -3
  56. package/src/legacy/lib/builders/SliceBuilder/index.tsx +2 -0
  57. package/src/legacy/lib/models/common/ModelStatus/index.ts +6 -1
  58. package/src/modules/userContext/index.ts +6 -3
  59. package/src/modules/userContext/types.ts +1 -1
  60. package/src/pages/_app.tsx +88 -95
  61. package/src/pages/changes.tsx +4 -5
  62. package/src/queryClient.tsx +24 -0
  63. package/test/__testutils__/index.tsx +13 -10
  64. package/out/_next/static/chunks/157-54b8336d20b41933.js +0 -3
  65. package/out/_next/static/chunks/34-8d9d9b2944824750.js +0 -1
  66. package/out/_next/static/chunks/385-b949173dfa03dd3e.js +0 -1
  67. package/out/_next/static/chunks/630-bb6e3db525588f16.js +0 -1
  68. package/out/_next/static/chunks/pages/changelog-21b960abba5abf71.js +0 -1
  69. package/out/_next/static/chunks/pages/changes-7919746009a45ad8.js +0 -1
  70. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-b127d948a17968d3.js +0 -1
  71. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-98f85d5fb8d5c704.js +0 -1
  72. package/out/_next/static/chunks/pages/slices-046e5e978ffc3a42.js +0 -1
  73. package/src/ErrorBoundary.tsx +0 -47
  74. package/src/features/environments/actions/setEnvironment.ts +0 -18
  75. package/src/legacy/components/ChangesEmptyState/AuthErrorPage.tsx +0 -44
  76. /package/out/_next/static/{eWxsTVqoYTTRkUB_nHhoh → bWd9YPY0k1NsrjJCNbscH}/_ssgManifest.js +0 -0
@@ -0,0 +1,150 @@
1
+ import {
2
+ BlankSlate,
3
+ BlankSlateDescription,
4
+ BlankSlateIcon,
5
+ BlankSlateTitle,
6
+ Box,
7
+ ErrorBoundary as EditorUiErrorBoundary,
8
+ Text,
9
+ } from "@prismicio/editor-ui";
10
+ import {
11
+ isInvalidActiveEnvironmentError,
12
+ isUnauthenticatedError,
13
+ isUnauthorizedError,
14
+ } from "@slicemachine/manager/client";
15
+ import Link from "next/link";
16
+ import {
17
+ type ComponentPropsWithoutRef,
18
+ type FC,
19
+ PropsWithChildren,
20
+ useCallback,
21
+ useRef,
22
+ } from "react";
23
+
24
+ import { ReloadLogoutButton } from "@/features/auth/LogoutButton";
25
+ import { useAuthStatus } from "@/hooks/useAuthStatus";
26
+
27
+ type DefaultErrorBoundaryProps = Pick<
28
+ // TODO(DT-1979): Export the `ErrorBoundaryProps` type from `@prismicio/editor-ui`.
29
+ ComponentPropsWithoutRef<typeof EditorUiErrorBoundary>,
30
+ "children" | "renderError"
31
+ >;
32
+
33
+ export const DefaultErrorBoundary: FC<DefaultErrorBoundaryProps> = (props) => {
34
+ const errorRef = useRef<unknown>();
35
+ const authStatus = useAuthStatus();
36
+ return (
37
+ <EditorUiErrorBoundary
38
+ {...props}
39
+ onError={(error) => {
40
+ errorRef.current = error;
41
+ }}
42
+ ref={useCallback(
43
+ (errorBoundary: EditorUiErrorBoundary | null) => {
44
+ if (errorBoundary === null) return;
45
+ const error = errorRef.current;
46
+ if (isUnauthenticatedError(error) || isUnauthorizedError(error)) {
47
+ errorRef.current = undefined;
48
+ errorBoundary.reset();
49
+ }
50
+ },
51
+ /* eslint-disable-next-line react-hooks/exhaustive-deps --
52
+ * Whenever `authStatus` changes, we want to `reset` the `errorBoundary`
53
+ * if an authentication or authorization error has been caught.
54
+ **/
55
+ [authStatus],
56
+ )}
57
+ />
58
+ );
59
+ };
60
+
61
+ export function AppStateErrorBoundary(props: PropsWithChildren) {
62
+ return (
63
+ <EditorUiErrorBoundary
64
+ renderError={(error) => {
65
+ return (
66
+ <Box
67
+ position="absolute"
68
+ top={64}
69
+ width="100%"
70
+ justifyContent="center"
71
+ flexDirection="column"
72
+ >
73
+ <BlankSlate>
74
+ <BlankSlateIcon
75
+ lineColor="tomato11"
76
+ backgroundColor="tomato3"
77
+ name="alert"
78
+ />
79
+ <BlankSlateTitle>Failed to load Slice Machine</BlankSlateTitle>
80
+ <RenderErrorDescription error={error} />
81
+ </BlankSlate>
82
+ </Box>
83
+ );
84
+ }}
85
+ >
86
+ {props.children}
87
+ </EditorUiErrorBoundary>
88
+ );
89
+ }
90
+
91
+ function RenderErrorDescription(args: { error: unknown }) {
92
+ const { error } = args;
93
+
94
+ if (isUnauthorizedError(error)) {
95
+ return (
96
+ <CommonErrorBox>
97
+ <Box flexDirection="column" gap={8} alignItems="center">
98
+ <Text variant="h3" align="center">
99
+ You don't have access to this repository.
100
+ </Text>
101
+ <Text align="center">
102
+ Check that the repository name is correct, then contact your
103
+ repository administrator.
104
+ </Text>
105
+ </Box>
106
+ <ReloadLogoutButton sx={{ alignSelf: "center" }} />
107
+ </CommonErrorBox>
108
+ );
109
+ }
110
+
111
+ if (isInvalidActiveEnvironmentError(error)) {
112
+ return (
113
+ <CommonErrorBox>
114
+ <Box flexDirection="column" gap={8} alignItems="center">
115
+ <Text variant="h3" align="center">
116
+ Your current environment is invalid.
117
+ </Text>
118
+ <Text align="center">
119
+ Check with your repository administrator that you have permissions
120
+ and correctly configured your environment for the current
121
+ repository. For more details, consult the{" "}
122
+ <Link href="https://prismic.io/docs/environments" target="_blank">
123
+ documentation
124
+ </Link>
125
+ .
126
+ </Text>
127
+ </Box>
128
+ <ReloadLogoutButton sx={{ alignSelf: "center" }} />
129
+ </CommonErrorBox>
130
+ );
131
+ }
132
+
133
+ return <BlankSlateDescription>{JSON.stringify(error)}</BlankSlateDescription>;
134
+ }
135
+
136
+ function CommonErrorBox(args: { children: React.ReactNode }) {
137
+ const { children } = args;
138
+
139
+ return (
140
+ <Box
141
+ flexDirection="column"
142
+ alignItems="center"
143
+ gap={16}
144
+ margin={{ top: 8 }}
145
+ maxWidth={768}
146
+ >
147
+ {children}
148
+ </Box>
149
+ );
150
+ }
@@ -1,52 +1,34 @@
1
1
  import { Button, Icon, IconButton, Tooltip } from "@prismicio/editor-ui";
2
2
  import { SX } from "@prismicio/editor-ui/dist/theme";
3
3
  import * as Sentry from "@sentry/nextjs";
4
- import { ReactNode } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { useRouter } from "next/router";
6
+ import { toast } from "react-toastify";
5
7
 
6
- import { clearAuth as managerLogout, getState } from "@/apiClient";
7
- import { invalidateActiveEnvironmentData } from "@/features/environments/useActiveEnvironment";
8
- import { invalidateEnvironmentsData } from "@/features/environments/useEnvironments";
8
+ import { clearAuth, getState } from "@/apiClient";
9
+ import { GetActiveEnvironmentQueryKey } from "@/features/environments/useActiveEnvironment";
10
+ import { GetEnvironmentsQueryKey } from "@/features/environments/useEnvironments";
9
11
  import useSliceMachineActions from "@/modules/useSliceMachineActions";
10
12
 
11
- interface LogoutButtonProps {
12
- children?: ReactNode;
13
- onLogoutSuccess?: () => void;
14
- isLoading?: boolean;
15
- sx?: SX;
16
- }
17
-
18
- export function LogoutButton(props: LogoutButtonProps) {
19
- const { children, onLogoutSuccess, isLoading, sx } = props;
20
-
13
+ export function EnvironmentLogoutButton() {
21
14
  const { refreshState } = useSliceMachineActions();
15
+ const queryClient = useQueryClient();
22
16
 
23
17
  async function onClick() {
24
- await managerLogout();
18
+ await clearAuth();
25
19
 
26
20
  const serverState = await getState();
27
21
  refreshState(serverState);
28
-
29
22
  Sentry.setUser({ id: serverState.env.shortId });
30
23
 
31
- // refresh queries to update the UI
32
- invalidateEnvironmentsData();
33
- invalidateActiveEnvironmentData();
24
+ await Promise.all([
25
+ queryClient.invalidateQueries({ queryKey: GetEnvironmentsQueryKey }),
26
+ queryClient.invalidateQueries({
27
+ queryKey: GetActiveEnvironmentQueryKey,
28
+ }),
29
+ ]);
34
30
 
35
- onLogoutSuccess?.();
36
- }
37
-
38
- if (children !== undefined) {
39
- return (
40
- <Button
41
- onClick={() => void onClick()}
42
- renderEndIcon={() => <Icon name="logout" size="extraSmall" />}
43
- color="grey"
44
- loading={isLoading}
45
- sx={sx}
46
- >
47
- {children}
48
- </Button>
49
- );
31
+ toast.success("Logged out");
50
32
  }
51
33
 
52
34
  return (
@@ -55,9 +37,33 @@ export function LogoutButton(props: LogoutButtonProps) {
55
37
  icon="logout"
56
38
  onClick={() => void onClick()}
57
39
  hiddenLabel="Log out"
58
- loading={isLoading}
59
- sx={sx}
60
40
  />
61
41
  </Tooltip>
62
42
  );
63
43
  }
44
+
45
+ interface ReloadLogoutButtonProps {
46
+ sx?: SX;
47
+ }
48
+
49
+ export function ReloadLogoutButton(props: ReloadLogoutButtonProps) {
50
+ const { sx } = props;
51
+ const router = useRouter();
52
+
53
+ async function onClick() {
54
+ await clearAuth();
55
+ Sentry.setUser({ id: undefined });
56
+ router.reload();
57
+ }
58
+
59
+ return (
60
+ <Button
61
+ onClick={() => void onClick()}
62
+ renderEndIcon={() => <Icon name="logout" size="extraSmall" />}
63
+ color="grey"
64
+ sx={sx}
65
+ >
66
+ Log out
67
+ </Button>
68
+ );
69
+ }
@@ -30,7 +30,7 @@ import {
30
30
  } from "@prismicio/types-internal/lib/customtypes";
31
31
  import { useEffect } from "react";
32
32
 
33
- import { ErrorBoundary } from "@/ErrorBoundary";
33
+ import { DefaultErrorBoundary } from "@/errorBoundaries";
34
34
  import {
35
35
  revalidateGetCustomTypes,
36
36
  useCustomTypes as useCustomTypesRequest,
@@ -226,7 +226,7 @@ export function ContentRelationshipFieldPicker(
226
226
  props: ContentRelationshipFieldPickerProps,
227
227
  ) {
228
228
  return (
229
- <ErrorBoundary
229
+ <DefaultErrorBoundary
230
230
  renderError={() => (
231
231
  <Box alignItems="center" gap={8}>
232
232
  <Icon name="alert" size="small" color="tomato10" />
@@ -254,7 +254,7 @@ export function ContentRelationshipFieldPicker(
254
254
  >
255
255
  <ContentRelationshipFieldPickerContent {...props} />
256
256
  </AnimatedSuspense>
257
- </ErrorBoundary>
257
+ </DefaultErrorBoundary>
258
258
  );
259
259
  }
260
260
 
@@ -73,7 +73,7 @@ function getStatusBadgeContent(
73
73
  "Data from the remote repository could not be fetched (no internet connection).",
74
74
  };
75
75
  }
76
- if (args.authStatus === AuthStatus.UNAUTHORIZED) {
76
+ if (args.authStatus === AuthStatus.UNAUTHENTICATED) {
77
77
  return {
78
78
  badgeColor: "grey",
79
79
  badgeTitle: "Unknown",
@@ -81,14 +81,6 @@ function getStatusBadgeContent(
81
81
  "Data from the remote repository could not be fetched (not connected to Prismic).",
82
82
  };
83
83
  }
84
- if (args.authStatus === AuthStatus.FORBIDDEN) {
85
- return {
86
- badgeColor: "grey",
87
- badgeTitle: "Unknown",
88
- tooltipContent:
89
- "Data from the remote repository could not be fetched (you don't have access to the repository).",
90
- };
91
- }
92
84
  }
93
85
  default: {
94
86
  return {
@@ -4,7 +4,7 @@ import { FC, Suspense } from "react";
4
4
 
5
5
  import { telemetry } from "@/apiClient";
6
6
  import { ContentTabs } from "@/components/ContentTabs";
7
- import { ErrorBoundary } from "@/ErrorBoundary";
7
+ import { DefaultErrorBoundary } from "@/errorBoundaries";
8
8
  import { MarkdownRenderer } from "@/features/documentation/MarkdownRenderer";
9
9
  import { useDocumentation } from "@/features/documentation/useDocumentation";
10
10
  import { useOnboarding } from "@/features/onboarding/useOnboarding";
@@ -68,7 +68,7 @@ type PageSnippetDialogProps = { model: CustomType };
68
68
  export const PageSnippetDialog: FC<PageSnippetDialogProps> = ({ model }) => {
69
69
  return (
70
70
  <div>
71
- <ErrorBoundary>
71
+ <DefaultErrorBoundary>
72
72
  <Suspense
73
73
  fallback={
74
74
  <Button color="grey" startIcon="code">
@@ -78,7 +78,7 @@ export const PageSnippetDialog: FC<PageSnippetDialogProps> = ({ model }) => {
78
78
  >
79
79
  <PageSnippetContent model={model} />
80
80
  </Suspense>
81
- </ErrorBoundary>
81
+ </DefaultErrorBoundary>
82
82
  </div>
83
83
  );
84
84
  };
@@ -9,7 +9,7 @@ import Head from "next/head";
9
9
  import { type FC, Suspense, useState } from "react";
10
10
 
11
11
  import { BreadcrumbItem } from "@/components/Breadcrumb";
12
- import { ErrorBoundary } from "@/ErrorBoundary";
12
+ import { DefaultErrorBoundary } from "@/errorBoundaries";
13
13
  import { CUSTOM_TYPES_MESSAGES } from "@/features/customTypes/customTypesMessages";
14
14
  import {
15
15
  AppLayout,
@@ -42,7 +42,7 @@ export const CustomTypesTablePage: FC<CustomTypesTablePageProps> = ({
42
42
  Machine
43
43
  </title>
44
44
  </Head>
45
- <ErrorBoundary
45
+ <DefaultErrorBoundary
46
46
  renderError={() => (
47
47
  <AppLayout>
48
48
  <AppLayoutContent>
@@ -119,7 +119,7 @@ export const CustomTypesTablePage: FC<CustomTypesTablePageProps> = ({
119
119
  </AppLayoutContent>
120
120
  </AppLayout>
121
121
  </Suspense>
122
- </ErrorBoundary>
122
+ </DefaultErrorBoundary>
123
123
  </>
124
124
  );
125
125
  };
@@ -0,0 +1,22 @@
1
+ import { revalidateData } from "@prismicio/editor-support/Suspense";
2
+ import { Environment } from "@slicemachine/manager/client";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+
5
+ import { getState } from "@/apiClient";
6
+ import { GetActiveEnvironmentQueryKey } from "@/features/environments/useActiveEnvironment";
7
+ import { managerClient } from "@/managerClient";
8
+
9
+ export function useSetEnvironment() {
10
+ const queryClient = useQueryClient();
11
+
12
+ return async (environment: Pick<Environment, "domain">) => {
13
+ await managerClient.project.updateEnvironment({
14
+ environment: environment.domain,
15
+ });
16
+
17
+ void Promise.all([
18
+ queryClient.invalidateQueries({ queryKey: GetActiveEnvironmentQueryKey }),
19
+ revalidateData(getState, []),
20
+ ]);
21
+ };
22
+ }
@@ -1,14 +1,22 @@
1
- import {
2
- invalidateFetcherData,
3
- useRequest,
4
- } from "@prismicio/editor-support/Suspense";
1
+ import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
5
2
 
6
3
  import { getActiveEnvironment } from "./actions/getActiveEnvironment";
7
4
 
8
- export function invalidateActiveEnvironmentData() {
9
- invalidateFetcherData(getActiveEnvironment);
10
- }
5
+ export const GetActiveEnvironmentQueryKey = ["getActiveEnvironment"];
6
+
7
+ export function useActiveEnvironment(options?: { suspense?: boolean }) {
8
+ const { suspense } = options ?? {};
9
+
10
+ const hook = suspense === true ? useSuspenseQuery : useQuery;
11
+
12
+ const { data, error, ...rest } = hook({
13
+ queryKey: GetActiveEnvironmentQueryKey,
14
+ queryFn: () => getActiveEnvironment(),
15
+ });
11
16
 
12
- export function useActiveEnvironment() {
13
- return useRequest(getActiveEnvironment, []);
17
+ return {
18
+ activeEnvironment: data?.activeEnvironment,
19
+ error: data?.error ?? error ?? undefined,
20
+ ...rest,
21
+ };
14
22
  }
@@ -1,14 +1,17 @@
1
- import {
2
- invalidateFetcherData,
3
- useRequest,
4
- } from "@prismicio/editor-support/Suspense";
1
+ import { useQuery } from "@tanstack/react-query";
5
2
 
6
3
  import { getEnvironments } from "./actions/getEnvironments";
7
4
 
8
- export function invalidateEnvironmentsData() {
9
- invalidateFetcherData(getEnvironments);
10
- }
5
+ export const GetEnvironmentsQueryKey = ["getEnvironments"];
11
6
 
12
7
  export function useEnvironments() {
13
- return useRequest(getEnvironments, []);
8
+ const { data, error, ...rest } = useQuery({
9
+ queryKey: GetEnvironmentsQueryKey,
10
+ queryFn: () => getEnvironments(),
11
+ });
12
+ return {
13
+ environments: data?.environments,
14
+ error: data?.error ?? error ?? undefined,
15
+ ...rest,
16
+ };
14
17
  }
@@ -3,7 +3,7 @@ import Head from "next/head";
3
3
  import { type FC, ReactNode, Suspense } from "react";
4
4
 
5
5
  import { BreadcrumbItem } from "@/components/Breadcrumb";
6
- import { ErrorBoundary } from "@/ErrorBoundary";
6
+ import { DefaultErrorBoundary } from "@/errorBoundaries";
7
7
  import {
8
8
  AppLayout,
9
9
  AppLayoutBreadcrumb,
@@ -19,7 +19,7 @@ export const LabsPage: FC = () => {
19
19
  <Head>
20
20
  <title>Labs - Slice Machine</title>
21
21
  </Head>
22
- <ErrorBoundary
22
+ <DefaultErrorBoundary
23
23
  renderError={() => (
24
24
  <LabsPageLayout>
25
25
  <Box alignItems="center" justifyContent="center">
@@ -42,7 +42,7 @@ export const LabsPage: FC = () => {
42
42
  <LabsList />
43
43
  </LabsPageLayout>
44
44
  </Suspense>
45
- </ErrorBoundary>
45
+ </DefaultErrorBoundary>
46
46
  </>
47
47
  );
48
48
  };
@@ -1,15 +1,22 @@
1
- import { ActionList, Box, Separator, Skeleton } from "@prismicio/editor-ui";
1
+ import {
2
+ ActionList,
3
+ Badge,
4
+ Box,
5
+ Separator,
6
+ Skeleton,
7
+ } from "@prismicio/editor-ui";
2
8
  import { useRouter } from "next/router";
3
9
  import { Suspense } from "react";
4
10
 
5
11
  import { telemetry } from "@/apiClient";
6
- import { ErrorBoundary } from "@/ErrorBoundary";
12
+ import { DefaultErrorBoundary } from "@/errorBoundaries";
7
13
  import { CUSTOM_TYPES_CONFIG } from "@/features/customTypes/customTypesConfig";
8
14
  import { CUSTOM_TYPES_MESSAGES } from "@/features/customTypes/customTypesMessages";
9
15
  import { RepositoryInfo } from "@/features/navigation/RepositoryInfo";
10
16
  import { OnboardingGuide } from "@/features/onboarding";
11
17
  import { useAdapterName } from "@/hooks/useAdapterName";
12
18
  import { useMarketingContent } from "@/hooks/useMarketingContent";
19
+ import { CodeIcon } from "@/icons/CodeIcon";
13
20
  import { FolderIcon } from "@/icons/FolderIcon";
14
21
  import { LightningIcon } from "@/icons/Lightning";
15
22
  import { MenuBookIcon } from "@/icons/MenuBookIcon";
@@ -79,20 +86,20 @@ export function Navigation() {
79
86
  />
80
87
  </ActionList>
81
88
 
82
- <ErrorBoundary>
89
+ <DefaultErrorBoundary>
83
90
  <Suspense>
84
91
  <UpdateInfo />
85
92
  </Suspense>
86
- </ErrorBoundary>
93
+ </DefaultErrorBoundary>
87
94
  </Box>
88
95
 
89
96
  <Box flexDirection="column">
90
97
  <ActionList variant="compact">
91
- <ErrorBoundary>
98
+ <DefaultErrorBoundary>
92
99
  <Suspense>
93
100
  <OnboardingGuide />
94
101
  </Suspense>
95
- </ErrorBoundary>
102
+ </DefaultErrorBoundary>
96
103
  <NavigationItem
97
104
  title="Documentation"
98
105
  href={documentationLink}
@@ -106,17 +113,33 @@ export function Navigation() {
106
113
  }}
107
114
  />
108
115
 
116
+ <NavigationItem
117
+ title="Prismic MCP"
118
+ href="https://prismic.io/docs/ai#code-with-prismics-mcp-server"
119
+ target="_blank"
120
+ Icon={CodeIcon}
121
+ RightElement={<Badge title="New" color="indigo" />}
122
+ tooltip="Connect Prismic MCP to your editor (like Cursor) for AI-powered coding."
123
+ onClick={() => {
124
+ void telemetry.track({
125
+ event: "sidebar:link-clicked",
126
+ link_name: "prismic_mcp",
127
+ source: "slice_machine_sidebar",
128
+ });
129
+ }}
130
+ />
131
+
109
132
  <NavigationItem
110
133
  title="Changelog"
111
134
  href="/changelog"
112
135
  Icon={LightningIcon}
113
136
  active={router.asPath.startsWith("/changelog")}
114
137
  RightElement={
115
- <ErrorBoundary>
138
+ <DefaultErrorBoundary>
116
139
  <Suspense fallback={<Skeleton height={16} />}>
117
140
  <SliceMachineVersion />
118
141
  </Suspense>
119
- </ErrorBoundary>
142
+ </DefaultErrorBoundary>
120
143
  }
121
144
  />
122
145
  </ActionList>
@@ -13,6 +13,7 @@ type NavigationItemPropsBase = {
13
13
  active?: boolean;
14
14
  Icon: FC<SVGProps<SVGSVGElement>>;
15
15
  RightElement?: ReactNode;
16
+ tooltip?: string;
16
17
  };
17
18
 
18
19
  type NavigationLinkItemProps = NavigationItemPropsBase & {
@@ -32,7 +33,8 @@ type NavigationButtonItemProps = NavigationItemPropsBase & {
32
33
  type NavigationItemProps = NavigationLinkItemProps | NavigationButtonItemProps;
33
34
 
34
35
  export function NavigationItem(props: NavigationItemProps) {
35
- const { title, href, target, active, Icon, RightElement, onClick } = props;
36
+ const { title, href, target, active, Icon, RightElement, onClick, tooltip } =
37
+ props;
36
38
 
37
39
  const isCollapsed = useMediaQuery({ max: "medium" });
38
40
 
@@ -45,7 +47,7 @@ export function NavigationItem(props: NavigationItemProps) {
45
47
  textVariant="normal"
46
48
  backgroundColor="transparent"
47
49
  renderStartIcon={ItemIcon}
48
- endAdornment={RightElement}
50
+ endAdornment={isCollapsed ? undefined : RightElement}
49
51
  selected={active}
50
52
  >
51
53
  {isCollapsed ? null : title}
@@ -53,7 +55,11 @@ export function NavigationItem(props: NavigationItemProps) {
53
55
  );
54
56
 
55
57
  return (
56
- <Tooltip content={title} side="right" visible={isCollapsed}>
58
+ <Tooltip
59
+ content={Boolean(tooltip) ? tooltip ?? "" : title}
60
+ side="right"
61
+ visible={Boolean(tooltip) ? undefined : isCollapsed}
62
+ >
57
63
  {href !== undefined ? (
58
64
  <Link
59
65
  href={href}
@@ -1,14 +1,9 @@
1
- import { Text, useMediaQuery } from "@prismicio/editor-ui";
1
+ import { Text } from "@prismicio/editor-ui";
2
2
 
3
3
  import { useSliceMachineRunningVersion } from "@/hooks/useSliceMachineRunningVersion";
4
4
 
5
5
  export function SliceMachineVersion() {
6
6
  const sliceMachineRunningVersion = useSliceMachineRunningVersion();
7
- const isCollapsed = useMediaQuery({ max: "medium" });
8
-
9
- if (isCollapsed) {
10
- return null;
11
- }
12
7
 
13
8
  return (
14
9
  <Text
@@ -2,7 +2,7 @@ import { Box, Button, ButtonGroup } from "@prismicio/editor-ui";
2
2
  import { useRouter } from "next/router";
3
3
  import { type FC, Suspense, useState } from "react";
4
4
 
5
- import { ErrorBoundary } from "@/ErrorBoundary";
5
+ import { DefaultErrorBoundary } from "@/errorBoundaries";
6
6
  import { useCustomType } from "@/features/customTypes/customTypesBuilder/useCustomType";
7
7
  import {
8
8
  CUSTOM_TYPES_CONFIG,
@@ -17,7 +17,7 @@ export const FloatingBackButton: FC = () => {
17
17
  const { source } = useRouteChange();
18
18
  const sourceCustomTypeId = getSourceCustomTypeId(source);
19
19
  return sourceCustomTypeId !== undefined ? (
20
- <ErrorBoundary>
20
+ <DefaultErrorBoundary>
21
21
  <Suspense>
22
22
  <Box
23
23
  bottom={32}
@@ -29,7 +29,7 @@ export const FloatingBackButton: FC = () => {
29
29
  <BackButton sourceCustomTypeId={sourceCustomTypeId} />
30
30
  </Box>
31
31
  </Suspense>
32
- </ErrorBoundary>
32
+ </DefaultErrorBoundary>
33
33
  ) : null;
34
34
  };
35
35
 
@@ -0,0 +1,29 @@
1
+ import { Button } from "@prismicio/editor-ui";
2
+
3
+ import { telemetry } from "@/apiClient";
4
+
5
+ export const McpPromoLink = () => {
6
+ return (
7
+ <Button
8
+ asChild
9
+ invisible
10
+ color="grey"
11
+ endIcon="openInNew"
12
+ sx={{ alignSelf: "center" }}
13
+ onClick={() => {
14
+ void telemetry.track({
15
+ event: "mcp:promo-link-clicked",
16
+ source: "slice_editor",
17
+ target: "docs",
18
+ });
19
+ }}
20
+ >
21
+ <a
22
+ href="https://prismic.io/docs/ai#code-with-prismics-mcp-server"
23
+ target="_blank"
24
+ >
25
+ Boost your workflow in Cursor with Prismic MCP
26
+ </a>
27
+ </Button>
28
+ );
29
+ };
@@ -29,6 +29,7 @@ export async function pushChanges(
29
29
  type: "Slice" as const,
30
30
  libraryID: sliceChange.slice.from,
31
31
  status: sliceChange.status,
32
+ variationImageUrlMap: sliceChange.variationImageUrlMap,
32
33
  }));
33
34
  const customTypeChanges = changedCustomTypes.map((customTypeChange) => ({
34
35
  id: customTypeChange.customType.id,