From df42cc1ce5d6d414b31c2eb76401446c3ae1cccc Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 7 Sep 2023 18:55:42 +0200 Subject: [PATCH] feat: ui conflicts --- gui/src/App.tsx | 102 +++++++++-------- gui/src/components/AppFooter.tsx | 15 +++ gui/src/components/AppHeader.tsx | 42 +++++++ gui/src/components/CustomTypeCard.tsx | 145 +++++++++++++++++++++++++ gui/src/components/CustomTypeList.tsx | 23 ++++ gui/src/components/SharedSliceList.tsx | 20 ++++ gui/src/components/StatusDot.tsx | 36 ++++++ gui/src/index.css | 42 ++++++- gui/src/main.tsx | 8 +- gui/src/store/useRepository.ts | 47 ++++++++ gui/src/store/useSliceConflicts.ts | 21 ++++ gui/src/store/useSliceMachineConfig.ts | 8 +- gui/vite.config.ts | 8 ++ package-lock.json | 79 +++++++++++++- package.json | 1 + 15 files changed, 537 insertions(+), 60 deletions(-) create mode 100644 gui/src/components/AppFooter.tsx create mode 100644 gui/src/components/AppHeader.tsx create mode 100644 gui/src/components/CustomTypeCard.tsx create mode 100644 gui/src/components/CustomTypeList.tsx create mode 100644 gui/src/components/SharedSliceList.tsx create mode 100644 gui/src/components/StatusDot.tsx create mode 100644 gui/src/store/useRepository.ts create mode 100644 gui/src/store/useSliceConflicts.ts diff --git a/gui/src/App.tsx b/gui/src/App.tsx index c4f202d..a056139 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -1,66 +1,64 @@ -import { DocumentStatusBar, Separator, Skeleton } from "@prismicio/editor-ui"; -import { useMemo } from "react"; +import { AnimatedElement, DocumentStatusBar, Tab } from "@prismicio/editor-ui"; +import { useEffect, useState } from "react"; +import { AppFooter } from "./components/AppFooter"; +import { AppHeader } from "./components/AppHeader"; +import { CustomTypeList } from "./components/CustomTypeList"; +import { SharedSliceList } from "./components/SharedSliceList"; +import { useRepository } from "./store/useRepository"; import { useSliceMachineConfig } from "./store/useSliceMachineConfig"; import { useUser } from "./store/useUser"; -function Header(): JSX.Element { - const { config } = useSliceMachineConfig(); - const { profile } = useUser(); - - const apiEndpoint = useMemo(() => { - if (!config) { - return null; - } +export function App(): JSX.Element { + const repository = useRepository(); + const sliceMachineConfig = useSliceMachineConfig(); + const user = useUser(); - return config.apiEndpoint - ? config.apiEndpoint.replace(/^https?:\/\//i, "") - : `${config.repositoryName}.prismic.io`; - }, [config]); + const tabs = ["Slices", "Custom Types", "Upgrade Summary"] as const; + const [activeTab, setActiveTab] = + useState<(typeof tabs)[number]>("Custom Types"); - return ( -
- {config ? ( -
-

{config.repositoryName}

- - {apiEndpoint} - -
- ) : ( -
- -
- )} - {profile ? ( -

- Logged in as {profile.email} -

- ) : ( -
- -
- )} -
- ); -} - -export function App(): JSX.Element { - useSliceMachineConfig((state) => state.fetch)(); - useUser((state) => state.fetch)(); + useEffect(() => { + repository.fetch(); + sliceMachineConfig.fetch(); + user.fetch(); + }, []); return ( <> -
-
- + +
+
+ + + {activeTab === "Slices" ? ( +
+ TODO: Slices +
+ ) : activeTab === "Custom Types" ? ( +
+ + +
+ ) : ( +
+ TODO: Upgrade Summary +
+ )} +
+
-
+ ); } diff --git a/gui/src/components/AppFooter.tsx b/gui/src/components/AppFooter.tsx new file mode 100644 index 0000000..3f121d9 --- /dev/null +++ b/gui/src/components/AppFooter.tsx @@ -0,0 +1,15 @@ +export function AppFooter(): JSX.Element { + return ( + + ); +} diff --git a/gui/src/components/AppHeader.tsx b/gui/src/components/AppHeader.tsx new file mode 100644 index 0000000..2889d2b --- /dev/null +++ b/gui/src/components/AppHeader.tsx @@ -0,0 +1,42 @@ +import { Icon, Skeleton } from "@prismicio/editor-ui"; + +import { useSliceMachineConfig } from "../store/useSliceMachineConfig"; +import { useUser } from "../store/useUser"; + +export function AppHeader(): JSX.Element { + const [config, dashboard] = useSliceMachineConfig((state) => [ + state.config, + state.dashboard, + ]); + const profile = useUser((state) => state.profile); + + return ( +
+ {config ? ( +
+

{config.repositoryName}

+ +
+ ) : ( +
+ +
+ )} + {profile ? ( + + + Logged in as {profile.email} + + ) : ( +
+ +
+ )} +
+ ); +} diff --git a/gui/src/components/CustomTypeCard.tsx b/gui/src/components/CustomTypeCard.tsx new file mode 100644 index 0000000..01f9773 --- /dev/null +++ b/gui/src/components/CustomTypeCard.tsx @@ -0,0 +1,145 @@ +import { Button, Icon, Tooltip } from "@prismicio/editor-ui"; +import { useMemo } from "react"; + +import { useSliceConflicts } from "../store/useSliceConflicts"; +import { useSliceMachineConfig } from "../store/useSliceMachineConfig"; + +import { StatusDot } from "./StatusDot"; + +import { CompositeSlice } from "../../../src/models/CompositeSlice"; +import { CustomType } from "../../../src/models/CustomType"; + +export function CustomTypeCard({ + customType, +}: { + customType: CustomType; +}): JSX.Element { + const dashboard = useSliceMachineConfig((state) => state.dashboard); + const conflicts = useSliceConflicts((state) => state.conflicts); + + const sliceZones = useMemo(() => { + const sliceZoneMap: Record< + string, + { id: string; tabID: string; compositeSlices: CompositeSlice[] } + > = {}; + const compositeSlices = customType.getAllCompositeSlices(); + + for (const compositeSlice of compositeSlices) { + sliceZoneMap[compositeSlice.meta.path.sliceZoneID] ||= { + id: compositeSlice.meta.path.sliceZoneID, + tabID: compositeSlice.meta.path.tabID, + compositeSlices: [], + }; + sliceZoneMap[compositeSlice.meta.path.sliceZoneID].compositeSlices.push( + compositeSlice, + ); + } + + return Object.values(sliceZoneMap); + }, [customType]); + + const hasConflicts = useMemo(() => { + return Object.values(conflicts ?? {}).some((slices) => { + return slices.some((slice) => { + return ( + slice instanceof CompositeSlice && + slice.meta.parent.id === customType.id + ); + }); + }); + }, [conflicts, customType]); + + return ( +
+
+
+

+ + {customType.definition.label} +

+ + ID: {customType.id} + +
+ +
+
+
+
    + {sliceZones.length ? ( + sliceZones.map((sliceZone) => ( +
  • +
    +

    + + {sliceZone.id} +

    + + {sliceZone.compositeSlices.length} legacy slice + {sliceZone.compositeSlices.length > 1 ? "s" : ""} in this + slice zone. + +
    +
      + {sliceZone.compositeSlices.map((compositeSlice) => ( +
    • +
      +
      + + + + {compositeSlice.definition.fieldset} + + +
      + + ID:{" "} + {compositeSlice.id} + +
      +
    • + ))} +
    +
  • + )) + ) : ( +
  • + + No slice zone in this custom type. + +
  • + )} +
+
+
+ ); +} diff --git a/gui/src/components/CustomTypeList.tsx b/gui/src/components/CustomTypeList.tsx new file mode 100644 index 0000000..a033f2d --- /dev/null +++ b/gui/src/components/CustomTypeList.tsx @@ -0,0 +1,23 @@ +import { useRepository } from "../store/useRepository"; + +import { CustomTypeCard } from "./CustomTypeCard"; + +export function CustomTypeList(): JSX.Element { + const customTypes = useRepository((state) => state.customTypes); + + return ( +
+

Custom Types ({customTypes?.length})

+
    + {customTypes?.map((customType) => ( +
  • + +
  • + ))} + {customTypes?.length === 0 ? ( +
  • No Shared Slices found.
  • + ) : null} +
+
+ ); +} diff --git a/gui/src/components/SharedSliceList.tsx b/gui/src/components/SharedSliceList.tsx new file mode 100644 index 0000000..5ed40e8 --- /dev/null +++ b/gui/src/components/SharedSliceList.tsx @@ -0,0 +1,20 @@ +import { useRepository } from "../store/useRepository"; + +// TODO: Rendering +export function SharedSliceList(): JSX.Element { + const sharedSlices = useRepository((state) => state.sharedSlices); + + return ( +
+

Shared Slices ({sharedSlices?.length})

+
    + {sharedSlices?.map((sharedSlice) => ( +
  • {sharedSlice.id}
  • + ))} + {sharedSlices?.length === 0 ? ( +
  • No Shared Slices found.
  • + ) : null} +
+
+ ); +} diff --git a/gui/src/components/StatusDot.tsx b/gui/src/components/StatusDot.tsx new file mode 100644 index 0000000..77a3b21 --- /dev/null +++ b/gui/src/components/StatusDot.tsx @@ -0,0 +1,36 @@ +import clsx from "clsx"; +import { forwardRef } from "react"; + +type StatusDotProps = { + type: "unknown" | "success" | "warn" | "error"; +}; + +export const StatusDot = forwardRef( + function StatusDot({ type }: StatusDotProps, ref): JSX.Element { + return ( + + {["warn", "error"].includes(type) ? ( + + ) : null} + + + ); + }, +); diff --git a/gui/src/index.css b/gui/src/index.css index 8fab608..3e3a143 100644 --- a/gui/src/index.css +++ b/gui/src/index.css @@ -2,4 +2,44 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +html { + @apply overflow-y-scroll; +} + +.container { + @apply px-4 max-w-screen-lg mx-auto; +} + +.heading-1 { + @apply font-medium text-2xl; +} + +.heading-2 { + @apply font-medium text-xl; +} + +.heading-3 { + @apply font-medium; +} + +.heading-4 { + @apply font-medium; +} + +.heading-5 { + @apply font-medium; +} + +.badge { + @apply text-xs rounded bg-stone-200/50 cursor-default inline-flex items-center gap-2 px-2 py-1 text-stone-500 not-italic; +} + +.badge.badge--medium { + @apply py-1.5 +} + +.badge:hover { + @apply bg-stone-200; +} diff --git a/gui/src/main.tsx b/gui/src/main.tsx index 5b3d75d..3569b19 100644 --- a/gui/src/main.tsx +++ b/gui/src/main.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider } from "@prismicio/editor-ui"; +import { ThemeProvider, TooltipProvider } from "@prismicio/editor-ui"; import React from "react"; import ReactDOM from "react-dom/client"; @@ -7,8 +7,10 @@ import "./index.css"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - + + + + , ); diff --git a/gui/src/store/useRepository.ts b/gui/src/store/useRepository.ts new file mode 100644 index 0000000..b6d7e61 --- /dev/null +++ b/gui/src/store/useRepository.ts @@ -0,0 +1,47 @@ +import { create } from "zustand"; + +import { useManager } from "./useManager"; +import { useSliceConflicts } from "./useSliceConflicts"; + +import { CompositeSlice } from "../../../src/models/CompositeSlice"; +import { CustomType } from "../../../src/models/CustomType"; +import { SharedSlice } from "../../../src/models/SharedSlice"; +import { checkSliceConflicts } from "../../../src/models/checkSliceConflicts"; + +type RepositoryState = { + customTypes: null | CustomType[]; + sharedSlices: null | SharedSlice[]; + compositeSlices: null | CompositeSlice[]; + fetch: () => Promise; +}; + +export const useRepository = create((set) => ({ + customTypes: null, + sharedSlices: null, + compositeSlices: null, + fetch: async () => { + const [rawCustomTypes, rawSharedSlices] = await Promise.all([ + useManager().customTypes.fetchRemoteCustomTypes(), + useManager().slices.fetchRemoteSlices(), + ]); + const customTypes = rawCustomTypes.map( + (customType) => new CustomType(customType), + ); + const sharedSlices = rawSharedSlices.map( + (sharedSlice) => new SharedSlice(sharedSlice), + ); + const compositeSlices = customTypes + .map((customType) => customType.getAllCompositeSlices()) + .flat(); + + set({ + customTypes, + sharedSlices, + compositeSlices, + }); + + useSliceConflicts.setState((_state) => ({ + conflicts: checkSliceConflicts([...sharedSlices, ...compositeSlices]), + })); + }, +})); diff --git a/gui/src/store/useSliceConflicts.ts b/gui/src/store/useSliceConflicts.ts new file mode 100644 index 0000000..c399c6a --- /dev/null +++ b/gui/src/store/useSliceConflicts.ts @@ -0,0 +1,21 @@ +import { create } from "zustand"; + +import { CompositeSlice } from "../../../src/models/CompositeSlice"; +import { SharedSlice } from "../../../src/models/SharedSlice"; +import { + SliceConflicts, + checkSliceConflicts, +} from "../../../src/models/checkSliceConflicts"; + +type SliceConflictsState = { + conflicts: null | SliceConflicts; +}; + +export const useSliceConflicts = create((set) => ({ + conflicts: null, + check: (slices: (CompositeSlice | SharedSlice)[]) => { + const conflicts = checkSliceConflicts(slices); + + set({ conflicts }); + }, +})); diff --git a/gui/src/store/useSliceMachineConfig.ts b/gui/src/store/useSliceMachineConfig.ts index 4df1a0b..b4466b4 100644 --- a/gui/src/store/useSliceMachineConfig.ts +++ b/gui/src/store/useSliceMachineConfig.ts @@ -5,14 +5,20 @@ import { useManager } from "./useManager"; type SliceMachineConfigState = { config: null | SliceMachineConfig; + dashboard: null | string; fetch: () => Promise; }; export const useSliceMachineConfig = create((set) => ({ config: null, + dashboard: null, fetch: async () => { const config = await useManager().project.getSliceMachineConfig(); - set({ config }); + const dashboard = config.apiEndpoint + ? config.apiEndpoint.replace(".cdn", "") + : `https://${config.repositoryName}.prismic.io`; + + set({ config, dashboard }); }, })); diff --git a/gui/vite.config.ts b/gui/vite.config.ts index 8c85c5f..7e095b5 100644 --- a/gui/vite.config.ts +++ b/gui/vite.config.ts @@ -4,6 +4,14 @@ import { defineConfig } from "vite"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: [ + { + find: /@cli\/(.*)/, + replacement: "../src/$1", + }, + ], + }, root: __dirname, build: { outDir: "../dist/gui", diff --git a/package-lock.json b/package-lock.json index 1bce07b..fab87a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@vitejs/plugin-react": "^4.0.4", "@vitest/coverage-v8": "^0.34.3", "autoprefixer": "^10.4.15", + "clsx": "^2.0.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", @@ -1843,6 +1844,15 @@ "outdent": "^0.8.0" } }, + "node_modules/@prismicio/editor-ui/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@prismicio/editor-ui/node_modules/css-what": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", @@ -3877,6 +3887,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@react-aria/dnd/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-aria/focus": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.12.0.tgz", @@ -3893,6 +3912,15 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@react-aria/focus/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-aria/i18n": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.7.0.tgz", @@ -4070,6 +4098,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@react-aria/overlays/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-aria/slider": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.6.0.tgz", @@ -4145,6 +4182,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@react-aria/slider/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-aria/spinbutton": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.5.1.tgz", @@ -4266,6 +4312,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@react-aria/textfield/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-aria/utils": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.19.0.tgz", @@ -4291,6 +4346,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@react-aria/utils/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-aria/visually-hidden": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.0.tgz", @@ -4307,6 +4371,15 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@react-aria/visually-hidden/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@react-stately/calendar": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.1.0.tgz", @@ -6498,9 +6571,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "dev": true, "engines": { "node": ">=6" diff --git a/package.json b/package.json index 2363cb4..da49cda 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "@vitejs/plugin-react": "^4.0.4", "@vitest/coverage-v8": "^0.34.3", "autoprefixer": "^10.4.15", + "clsx": "^2.0.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0",