diff --git a/.changeset/atlantic-scallop-fisherman b/.changeset/atlantic-scallop-fisherman new file mode 100644 index 0000000000..27a8255a0f --- /dev/null +++ b/.changeset/atlantic-scallop-fisherman @@ -0,0 +1,5 @@ +--- +"@comet/cms-api": patch +--- + +API Generator: Disable input generation if `create` and `update` are set to `false` in the decorator diff --git a/.changeset/beige-chefs-confess.md b/.changeset/beige-chefs-confess.md new file mode 100644 index 0000000000..421b0cb446 --- /dev/null +++ b/.changeset/beige-chefs-confess.md @@ -0,0 +1,16 @@ +--- +"@comet/eslint-config": major +--- + +Enforce PascalCase for enums + +Changing the casing of an existing enum can be problematic, e.g., if the enum values are persisted in the database. +In such cases, the rule can be disabled like so + +```diff ++ /* eslint-disable @typescript-eslint/naming-convention */ + export enum ExampleEnum { + attr1 = "attr1", + } ++ /* eslint-enable @typescript-eslint/naming-convention */ +``` diff --git a/.changeset/brave-carpets-talk.md b/.changeset/brave-carpets-talk.md new file mode 100644 index 0000000000..d43440e4d9 --- /dev/null +++ b/.changeset/brave-carpets-talk.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Add `@comet/admin-theme` as a peer dependency + +`@comet/admin` now uses the custom `Typography` variants `list` and `listItem` defined in `@comet/admin-theme`. diff --git a/.changeset/brave-kiwis-pay.md b/.changeset/brave-kiwis-pay.md new file mode 100644 index 0000000000..aa46ea5c70 --- /dev/null +++ b/.changeset/brave-kiwis-pay.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Add `StackToolbar`, a variant of `Toolbar` component that hides itself in a nested stack diff --git a/.changeset/bright-dolls-live.md b/.changeset/bright-dolls-live.md new file mode 100644 index 0000000000..f3bebda792 --- /dev/null +++ b/.changeset/bright-dolls-live.md @@ -0,0 +1,57 @@ +--- +"@comet/cms-api": major +--- + +Remove CDN config from DAM + +It was a bad idea to introduce this in the first place, because `@comet/cms-api` should not be opinionated about how the CDN works. + +Modern applications require all traffic to be routed through a CDN. Cloudflare offers a tunnel, which made the origin-check obsolete, so we introduced a flag to disable the origin check. + +Also changes the behavior of the `FilesService::createFileUrl()`-method which now expects an options-object as second argument. + +## How to migrate (only required if CDN is used): + +Remove the following env vars from the API + +``` +DAM_CDN_ENABLED= +DAM_CDN_DOMAIN= +DAM_CDN_ORIGIN_HEADER= +DAM_DISABLE_CDN_ORIGIN_HEADER_CHECK=false +``` + +If you want to enable the origin check: + +1. Set the following env vars for the API + +``` +CDN_ORIGIN_CHECK_SECRET="Use value from DAM_CDN_ORIGIN_HEADER to avoid downtime" +``` + +_environment-variables.ts_ + +``` +@IsOptional() +@IsString() +CDN_ORIGIN_CHECK_SECRET: string; +``` + +_config.ts_ + +``` +cdn: { + originCheckSecret: envVars.CDN_ORIGIN_CHECK_SECRET, +}, +``` + +2. Add CdnGuard + +``` +// if CDN is enabled, make sure all traffic is either coming from the CDN or internal sources +if (config.cdn.originCheckSecret) { + app.useGlobalGuards(new CdnGuard({ headerName: "x-cdn-origin-check", headerValue: config.cdn.originCheckSecret })); +} +``` + +3. DNS changes might be required. `api.example.com` should point to CDN, CDN should point to internal API domain diff --git a/.changeset/brown-kids-develop.md b/.changeset/brown-kids-develop.md new file mode 100644 index 0000000000..883bfcd1e0 --- /dev/null +++ b/.changeset/brown-kids-develop.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Create a subroute by default in `SaveBoundary` + +The default path is `./save`, you can change it using the `subRoutePath` prop. diff --git a/.changeset/calm-plums-taste.md b/.changeset/calm-plums-taste.md new file mode 100644 index 0000000000..7b9b735953 --- /dev/null +++ b/.changeset/calm-plums-taste.md @@ -0,0 +1,6 @@ +--- +"@comet/cms-admin": major +"@comet/cms-api": major +--- + +Change language field in User and CurrentUser to locale diff --git a/.changeset/chilled-walls-shop.md b/.changeset/chilled-walls-shop.md new file mode 100644 index 0000000000..e250315fce --- /dev/null +++ b/.changeset/chilled-walls-shop.md @@ -0,0 +1,21 @@ +--- +"@comet/eslint-config": major +--- + +Add the rule `@typescript-eslint/prefer-enum-initializers` to require enum initializers + +```ts +// ✅ +enum ExampleEnum { + One = "One", + Two = "Two" +} +``` + +```ts +// ❌ +enum ExampleEnum { + One, + Two +} +``` \ No newline at end of file diff --git a/.changeset/clever-clocks-smile.md b/.changeset/clever-clocks-smile.md new file mode 100644 index 0000000000..2fcd094778 --- /dev/null +++ b/.changeset/clever-clocks-smile.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-color-picker": major +--- + +Replace the `componentsProps` prop with `slotProps` in `ColorPicker` and remove the `ColorPickerComponentsProps` type diff --git a/.changeset/cold-humans-pull.md b/.changeset/cold-humans-pull.md new file mode 100644 index 0000000000..2aed94ccf1 --- /dev/null +++ b/.changeset/cold-humans-pull.md @@ -0,0 +1,8 @@ +--- +"@comet/cms-admin": major +--- + +Add `@comet/admin-theme` as a peer dependency + +`@comet/cms-admin` now uses the custom `Typography` variants `list` and `listItem` defined in `@comet/admin-theme`. + diff --git a/.changeset/config.json b/.changeset/config.json index e5bd42918c..e6d4199baa 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,7 +7,7 @@ "access": "restricted", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["comet-admin-stories", "comet-demo-api", "comet-demo-admin", "comet-demo-site", "comet-docs"], + "ignore": ["comet-storybook", "comet-demo-api", "comet-demo-admin", "comet-demo-site", "comet-docs"], "snapshot": { "useCalculatedVersion": true } diff --git a/.changeset/cool-kiwis-shop.md b/.changeset/cool-kiwis-shop.md new file mode 100644 index 0000000000..8003fdf773 --- /dev/null +++ b/.changeset/cool-kiwis-shop.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-theme": major +--- + +Rework theme of MUI's `Chip` to match the updated Comet CI diff --git a/.changeset/curvy-moles-punch.md b/.changeset/curvy-moles-punch.md new file mode 100644 index 0000000000..72e1ce219d --- /dev/null +++ b/.changeset/curvy-moles-punch.md @@ -0,0 +1,8 @@ +--- +"@comet/admin-theme": major +--- + +Rework `typographyOptions` + +- Replace `typographyOptions` with `createTypographyOptions()` to enable using the theme's breakpoints for media queries +- Add new styles for `button` variant diff --git a/.changeset/cyan-drinks-enjoy.md b/.changeset/cyan-drinks-enjoy.md new file mode 100644 index 0000000000..7679ef251f --- /dev/null +++ b/.changeset/cyan-drinks-enjoy.md @@ -0,0 +1,8 @@ +--- +"@comet/admin": major +--- + +Change theming method of `Menu` + +- Rename `permanent` class-key to `permanentDrawer` and `temporary` class-key to `temporaryDrawer` +- Replace the `permanentDrawerProps` and `temporaryDrawerProps` props with `slotProps` diff --git a/.changeset/cyan-ladybugs-bathe.md b/.changeset/cyan-ladybugs-bathe.md new file mode 100644 index 0000000000..6618eed645 --- /dev/null +++ b/.changeset/cyan-ladybugs-bathe.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-api": minor +--- + +API Generator: Add `list` option to `@CrudGenerator()` to allow disabling the list query + +Related DTO classes will still be generated as they might be useful for application code. diff --git a/.changeset/early-news-attend.md b/.changeset/early-news-attend.md new file mode 100644 index 0000000000..78e46dc704 --- /dev/null +++ b/.changeset/early-news-attend.md @@ -0,0 +1,6 @@ +--- +"@comet/cms-admin": major +"@comet/cms-api": major +--- + +CRUD Generator: Remove `lastUpdatedAt` argument from update mutations diff --git a/.changeset/eighty-owls-cheat.md b/.changeset/eighty-owls-cheat.md new file mode 100644 index 0000000000..ebca055b90 --- /dev/null +++ b/.changeset/eighty-owls-cheat.md @@ -0,0 +1,21 @@ +--- +"@comet/admin-theme": major +--- + +Rework colors + +- Rename `bluePalette` to `primaryPalette` +- Rename `neutrals` to `greyPalette` +- Remove `greenPalette` +- Remove `secondary` from `paletteOptions` +- Change colors in all palettes +- Change `text` colors +- Add `highlight` colors `purple`, `green`, `orange`, `yellow` and `red` to palette + +Hint: To use the `highlight` colors without getting a type error, you must adjust the `vendors.d.ts` in your project: + +```diff ++ /// + +// ... +``` diff --git a/.changeset/fair-clouds-poke.md b/.changeset/fair-clouds-poke.md deleted file mode 100644 index b88df66dc8..0000000000 --- a/.changeset/fair-clouds-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@comet/cms-api": patch ---- - -API Generator: Use correct type for `where` when `getFindCondition` service method is not used diff --git a/.changeset/fast-pens-laugh.md b/.changeset/fast-pens-laugh.md new file mode 100644 index 0000000000..b7f143b603 --- /dev/null +++ b/.changeset/fast-pens-laugh.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-site": minor +--- + +Deprecate `InternalLinkBlock` component, instead there should be a copy of this component in the application for flexibility (e.g., support for internationalized routing) diff --git a/.changeset/fluffy-oranges-approve.md b/.changeset/fluffy-oranges-approve.md new file mode 100644 index 0000000000..b84740a057 --- /dev/null +++ b/.changeset/fluffy-oranges-approve.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-api": major +--- + +Make `@nestjs/platform-express` a peer dependency + +Make sure that `@nestjs/platform-express` is installed in the application. diff --git a/.changeset/fluffy-sheep-hammer.md b/.changeset/fluffy-sheep-hammer.md new file mode 100644 index 0000000000..2311bc9865 --- /dev/null +++ b/.changeset/fluffy-sheep-hammer.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-site": major +--- + +Bump styled-components peer dependency to v6 + +Follow the official [migration guide](https://styled-components.com/docs/faqs#what-do-i-need-to-do-to-migrate-to-v6) to upgrade. diff --git a/.changeset/forty-hounds-work.md b/.changeset/forty-hounds-work.md new file mode 100644 index 0000000000..2aafc07200 --- /dev/null +++ b/.changeset/forty-hounds-work.md @@ -0,0 +1,59 @@ +--- +"@comet/blocks-api": major +"@comet/cms-api": major +--- + +Support "real" dependency injection in `BlockData#transformToPlain` + +Previously we supported poor man's dependency injection using the `TransformDependencies` object in `transformToPlain`. +This is now replaced by a technique that allows actual dependency injection. + +**Example** + +```ts +class NewsLinkBlockData extends BlockData { + @BlockField({ nullable: true }) + id?: string; + + transformToPlain() { + // Return service that does the transformation + return NewsLinkBlockTransformerService; + } +} + +type TransformResponse = { + news?: { + id: string; + slug: string; + }; +}; + +@Injectable() +class NewsLinkBlockTransformerService implements BlockTransformerServiceInterface { + // Use dependency injection here + constructor(@InjectRepository(News) private readonly repository: EntityRepository) {} + + async transformToPlain(block: NewsLinkBlockData, context: BlockContext) { + if (!block.id) { + return {}; + } + + const news = await this.repository.findOneOrFail(block.id); + + return { + news: { + id: news.id, + slug: news.slug, + }, + }; + } +} +``` + +Adding this new technique results in a few breaking changes: + +- Remove dynamic registration of `BlocksModule` +- Pass `moduleRef` to `BlocksTransformerMiddlewareFactory` instead of `dependencies` +- Remove `dependencies` from `BlockData#transformToPlain` + +See the [migration guide](https://docs.comet-dxp.com/docs/migration/migration-from-v6-to-v7) on how to migrate. diff --git a/.changeset/fresh-buckets-begin.md b/.changeset/fresh-buckets-begin.md new file mode 100644 index 0000000000..11fc53ef72 --- /dev/null +++ b/.changeset/fresh-buckets-begin.md @@ -0,0 +1,31 @@ +--- +"@comet/admin-date-time": major +--- + +Change `DatePicker` and `DateRangePicker` values from `Date` to `string` + +This affects the `value` prop and the value returned by the `onChange` event. + +The value of `DatePicker` is a string in the format `YYYY-MM-DD`. +The value of `DateRangePicker` is an object with `start` and `end` keys, each as a string in the format `YYYY-MM-DD`. + +The code that handles values from these components may need to be adjusted. +This may include how the values are stored in or sent to the database. + +```diff +- const [date, setDate] = useState(new Date("2024-03-10")); ++ const [date, setDate] = useState("2024-03-10"); + return ; +``` + +```diff + const [dateRange, setDateRange] = useState({ +- start: new Date("2024-03-10"), +- end: new Date("2024-03-16"), ++ start: "2024-03-10", ++ end: "2024-03-16", + }); + return ; +``` + +The reason for this change is that when selecting a date like `2024-04-10` in a timezone ahead of UTC, it would be stored in a `Date` object as e.g. `2024-04-09T22:00:00.000Z`. When only the date is saved to the database, without the time, it would be saved as `2024-04-09`, which differs from the selected date. diff --git a/.changeset/fresh-sheep-remember.md b/.changeset/fresh-sheep-remember.md new file mode 100644 index 0000000000..1479c75e68 --- /dev/null +++ b/.changeset/fresh-sheep-remember.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-site": major +--- + +Upgrade to Next 14 and React 18 + +Add "use client" directive to components that currently require it (as they use styled-components or a context) diff --git a/.changeset/gentle-chefs-sell.md b/.changeset/gentle-chefs-sell.md new file mode 100644 index 0000000000..0878d53118 --- /dev/null +++ b/.changeset/gentle-chefs-sell.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove the `requiredSymbol` prop from `FieldContainer` and use MUIs native implementation + +This prop was used to display a custom required symbol next to the label of the field. We now use the native implementation of the required attribute of MUI to ensure better accessibility and compatibility with screen readers. diff --git a/.changeset/gentle-pots-perform.md b/.changeset/gentle-pots-perform.md new file mode 100644 index 0000000000..cb360cbcea --- /dev/null +++ b/.changeset/gentle-pots-perform.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-theme": minor +--- + +Slightly increase the default size of dialogs diff --git a/.changeset/giant-apples-cheer.md b/.changeset/giant-apples-cheer.md new file mode 100644 index 0000000000..ca8f0310fb --- /dev/null +++ b/.changeset/giant-apples-cheer.md @@ -0,0 +1,8 @@ +--- +"@comet/cms-api": major +--- + +API Generator: Generate better API for Many-to-one-relations with `orphanRemoval` activated where the reverse side has its own API generated + +- Add `id` as argument to create mutation +- Add `id` as argument to list query diff --git a/.changeset/good-squids-join.md b/.changeset/good-squids-join.md new file mode 100644 index 0000000000..f4b76b6338 --- /dev/null +++ b/.changeset/good-squids-join.md @@ -0,0 +1,19 @@ +--- +"@comet/cms-site": minor +--- + +Store site preview scope in cookie and add `previewParams()` helper to access it + +- Requires the new `SITE_PREVIEW_SECRET` environment variable that must contain a random secret (not required for local development) +- Requires a Route Handler located at `app/api/site-preview/route.ts`: + + ```ts + import { sitePreviewRoute } from "@comet/cms-site"; + import { createGraphQLFetch } from "@src/util/graphQLClient"; + import { type NextRequest } from "next/server"; + + export const dynamic = "force-dynamic"; + + export async function GET(request: NextRequest) { + return sitePreviewRoute(request, createGraphQLFetch()); + } \ No newline at end of file diff --git a/.changeset/great-hotels-unite.md b/.changeset/great-hotels-unite.md new file mode 100644 index 0000000000..091f339369 --- /dev/null +++ b/.changeset/great-hotels-unite.md @@ -0,0 +1,10 @@ +--- +"@comet/cms-api": major +--- + +Refactor auth-decorators + +- Remove `@PublicApi()`-decorator +- Rename `@DisableGlobalGuard()`-decorator to `@DisableCometGuards()` + +The `@DisableCometGuards()`-decorator will only disable the AuthGuard when no `x-include-invisible-content`-header is set. diff --git a/.changeset/great-insects-reply.md b/.changeset/great-insects-reply.md new file mode 100644 index 0000000000..32634b6656 --- /dev/null +++ b/.changeset/great-insects-reply.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove the `FieldContainerComponent` component + +`FieldContainerComponent` was never intended to be exported, use `FieldContainer` instead. diff --git a/.changeset/grumpy-poets-own.md b/.changeset/grumpy-poets-own.md new file mode 100644 index 0000000000..f821545876 --- /dev/null +++ b/.changeset/grumpy-poets-own.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-theme": major +--- + +Change `Link` text styling diff --git a/.changeset/happy-fans-pump.md b/.changeset/happy-fans-pump.md new file mode 100644 index 0000000000..a220014ade --- /dev/null +++ b/.changeset/happy-fans-pump.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-admin": minor +--- + +Add future Admin Generator that works with configuration files diff --git a/.changeset/happy-ladybugs-smile.md b/.changeset/happy-ladybugs-smile.md new file mode 100644 index 0000000000..13dec62322 --- /dev/null +++ b/.changeset/happy-ladybugs-smile.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": major +--- + +Rename the `FilterBarMoveFilersClassKey` type to `FilterBarMoreFiltersClassKey` diff --git a/.changeset/hot-drinks-crash.md b/.changeset/hot-drinks-crash.md new file mode 100644 index 0000000000..cb8d114b3c --- /dev/null +++ b/.changeset/hot-drinks-crash.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-theme": minor +--- + +Adapt styling of filter panel in `DataGrid` for mobile devices (<900px) diff --git a/.changeset/khaki-cobras-trade.md b/.changeset/khaki-cobras-trade.md new file mode 100644 index 0000000000..048ca36b65 --- /dev/null +++ b/.changeset/khaki-cobras-trade.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove the `disabled` class-key in `TabScrollButton` + +Use the `:disabled` selector instead when styling the disabled state. diff --git a/.changeset/khaki-eels-try.md b/.changeset/khaki-eels-try.md new file mode 100644 index 0000000000..41be238033 --- /dev/null +++ b/.changeset/khaki-eels-try.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Only use horizontal layout in `FieldContainer` when it exceeds `600px` in width diff --git a/.changeset/long-crabs-bake.md b/.changeset/long-crabs-bake.md new file mode 100644 index 0000000000..ae00ec0ebf --- /dev/null +++ b/.changeset/long-crabs-bake.md @@ -0,0 +1,8 @@ +--- +"@comet/admin": major +--- + +Remove the `components` and `componentProps` props from `CopyToClipboardButton` + +Instead, for the icons, use the `copyIcon` and `successIcon` props to pass a `ReactNode` instead of separately passing in values to the `components` and `componentProps` objects. +Use `slotPops` to pass props to the remaining elements. \ No newline at end of file diff --git a/.changeset/long-items-sell.md b/.changeset/long-items-sell.md new file mode 100644 index 0000000000..bb3decacc7 --- /dev/null +++ b/.changeset/long-items-sell.md @@ -0,0 +1,8 @@ +--- +"@comet/cms-admin": minor +--- + +Remove "Re-login"-button from `CurrentUserProvider` + +The button is already implemented in `createErrorDialogApolloLink()`. The correct arrangement of +the components in `App.tsx` (see migration guide) makes the double implementation needless. diff --git a/.changeset/metal-seas-listen.md b/.changeset/metal-seas-listen.md new file mode 100644 index 0000000000..abad6b7023 --- /dev/null +++ b/.changeset/metal-seas-listen.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Router Prompt: actively reset form state when choosing discard in the prompt dialog diff --git a/.changeset/nervous-sloths-thank.md b/.changeset/nervous-sloths-thank.md new file mode 100644 index 0000000000..f36e4963fc --- /dev/null +++ b/.changeset/nervous-sloths-thank.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Add setting `signInUrl` to `createErrorDialogApolloLink` diff --git a/.changeset/nervous-windows-grin.md b/.changeset/nervous-windows-grin.md new file mode 100644 index 0000000000..0b785a9d9a --- /dev/null +++ b/.changeset/nervous-windows-grin.md @@ -0,0 +1,13 @@ +--- +"@comet/cms-admin": major +"@comet/blocks-api": major +"@comet/cms-api": major +--- + +Remove unused/unnecessary peer dependencies + +Some dependencies were incorrectly marked as peer dependencies. +If you don't use them in your application, you may remove the following dependencies: + +- Admin: `axios` +- API: `@aws-sdk/client-s3`, `@azure/storage-blob` and `pg-error-constants` diff --git a/.changeset/new-chicken-search.md b/.changeset/new-chicken-search.md new file mode 100644 index 0000000000..24e818c89b --- /dev/null +++ b/.changeset/new-chicken-search.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-api": minor +--- + +InternalLinkBlock: add scope to targetPage in output + +This allows for example using the language from scope as url prefix in a multi-language site diff --git a/.changeset/plenty-cougars-warn.md b/.changeset/plenty-cougars-warn.md new file mode 100644 index 0000000000..a664a60ab6 --- /dev/null +++ b/.changeset/plenty-cougars-warn.md @@ -0,0 +1,34 @@ +--- +"@comet/cms-site": minor +--- + +Add GraphQL fetch client + +- `createGraphQLFetch`: simple graphql client around fetch, usage: createGraphQLFetch(fetch, url)(gql, variables) +- `type GraphQLFetch = (query: string, variables?: V, init?: RequestInit) => Promise` +- `gql` for tagging queries +- `createFetchWithDefaults` fetch decorator that adds default values (eg. headers or next.revalidate) +- `createFetchWithPreviewHeaders` fetch decorator that adds comet preview headers (based on SitePreviewData) + +Example helper in application: +``` +export const graphQLApiUrl = `${typeof window === "undefined" ? process.env.API_URL_INTERNAL : process.env.NEXT_PUBLIC_API_URL}/graphql`; +export function createGraphQLFetch(previewData?: SitePreviewData) { + return createGraphQLFetchLibrary( + createFetchWithDefaults(createFetchWithPreviewHeaders(fetch, previewData), { next: { revalidate: 15 * 60 } }), + graphQLApiUrl, + ); + +} +``` + +Usage example: +``` +const graphqlFetch = createGraphQLFetch(previewData); +const data = await graphqlFetch( + exampleQuery, + { + exampleVariable: "foo" + } +); +``` \ No newline at end of file diff --git a/.changeset/polite-needles-unite.md b/.changeset/polite-needles-unite.md new file mode 100644 index 0000000000..97f204b36e --- /dev/null +++ b/.changeset/polite-needles-unite.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-theme": minor +--- + +Add `breakpointsOptions` to theme diff --git a/.changeset/poor-dragons-sing.md b/.changeset/poor-dragons-sing.md new file mode 100644 index 0000000000..4fafa61d0d --- /dev/null +++ b/.changeset/poor-dragons-sing.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Support relative paths in `SubRoute` component using `./subroute` notation diff --git a/.changeset/proud-cheetahs-sleep.md b/.changeset/proud-cheetahs-sleep.md new file mode 100644 index 0000000000..79c548ace2 --- /dev/null +++ b/.changeset/proud-cheetahs-sleep.md @@ -0,0 +1,9 @@ +--- +"@comet/admin-color-picker": patch +"@comet/admin-react-select": patch +"@comet/admin-date-time": patch +"@comet/admin-rte": patch +"@comet/admin": patch +--- + +Allow partial props in the theme's `defaultProps` instead of requiring all props when setting the `defaultProps` of a component diff --git a/.changeset/quick-foxes-notice.md b/.changeset/quick-foxes-notice.md new file mode 100644 index 0000000000..437da160b5 --- /dev/null +++ b/.changeset/quick-foxes-notice.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": major +--- + +Replace the `componentsProps` prop with `slotProps` in `FieldSet` diff --git a/.changeset/rare-boxes-guess.md b/.changeset/rare-boxes-guess.md new file mode 100644 index 0000000000..ec52553b6e --- /dev/null +++ b/.changeset/rare-boxes-guess.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-admin": minor +--- + +Adapt `Header` and `UserHeaderItem` used in `AppHeader` for mobile devices (<900px) diff --git a/.changeset/selfish-books-fail.md b/.changeset/selfish-books-fail.md new file mode 100644 index 0000000000..c187025397 --- /dev/null +++ b/.changeset/selfish-books-fail.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": minor +--- + +Pass `required` prop to children of `Field` component diff --git a/.changeset/selfish-dolls-beg.md b/.changeset/selfish-dolls-beg.md new file mode 100644 index 0000000000..fe70825e6b --- /dev/null +++ b/.changeset/selfish-dolls-beg.md @@ -0,0 +1,14 @@ +--- +"@comet/cms-site": minor +--- + +Add new technique for blocks to load additional data at page level when using SSR + +This works both server-side (SSR, SSG) and client-side (block preview). + +New Apis: + +- `recursivelyLoadBlockData`: used to call loaders for a block data tree +- `BlockLoader`: type of a loader function that is responsible for one block +- `useBlockPreviewFetch`: helper hook for block preview that creates client-side caching graphQLFetch/fetch +- `BlockLoaderDependencies`: interface with dependencies that get passed through recursivelyLoadBlockData into loader functions. Can be extended using module augmentation in application to inject eg. pageTreeNodeId. \ No newline at end of file diff --git a/.changeset/serious-ravens-matter.md b/.changeset/serious-ravens-matter.md new file mode 100644 index 0000000000..306d89aa76 --- /dev/null +++ b/.changeset/serious-ravens-matter.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": major +--- + +Replace the `componentsProps` prop with `slotProps` in `InputWithPopper` and remove the `InputWithPopperComponentsProps` type diff --git a/.changeset/shy-scissors-joke.md b/.changeset/shy-scissors-joke.md new file mode 100644 index 0000000000..410ceb8ece --- /dev/null +++ b/.changeset/shy-scissors-joke.md @@ -0,0 +1,9 @@ +--- +"@comet/cms-api": major +--- + +API Generator: Remove support for `visible` boolean, use `status` enum instead. + +Recommended enum values: Published/Unpublished or Visible/Invisible or Active/Deleted or Active/Archived + +Remove support for update visibility mutation, use existing generic update instead diff --git a/.changeset/small-snails-mate.md b/.changeset/small-snails-mate.md new file mode 100644 index 0000000000..f77b840307 --- /dev/null +++ b/.changeset/small-snails-mate.md @@ -0,0 +1,9 @@ +--- +"@comet/cms-admin": major +"@comet/cms-site": major +--- + +Support single host for block preview + +The content scope is passed through the iframe-bridge in the admin and accessible in the site in the IFrameBridgeProvider. +Breaking: `previewUrl`-property of `SiteConfig` has changed to `blockPreviewBaseUrl` diff --git a/.changeset/smooth-chefs-flow.md b/.changeset/smooth-chefs-flow.md new file mode 100644 index 0000000000..365c7ae2ec --- /dev/null +++ b/.changeset/smooth-chefs-flow.md @@ -0,0 +1,5 @@ +--- +"@comet/admin-theme": major +--- + +Adapt `Typography` headlines for mobile devices (<900px) diff --git a/.changeset/strange-steaks-pull.md b/.changeset/strange-steaks-pull.md new file mode 100644 index 0000000000..d0f4dff674 --- /dev/null +++ b/.changeset/strange-steaks-pull.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove the `message` class-key from `Alert` + +Use the `.MuiAlert-message` selector instead to style the message of the underlying MUI `Alert` component. diff --git a/.changeset/ten-turkeys-poke.md b/.changeset/ten-turkeys-poke.md new file mode 100644 index 0000000000..2d0f065aa4 --- /dev/null +++ b/.changeset/ten-turkeys-poke.md @@ -0,0 +1,29 @@ +--- +"@comet/admin-date-time": major +--- + +Rename multiple props and class-keys and remove the `componentsProps` types: + +- `DatePicker`: + + - Replace the `componentsProps` prop with `slotProps` + - Remove the `DatePickerComponentsProps` type + +- `DateRangePicker`: + + - Replace the `componentsProps` prop with `slotProps` + - Remove the `DateRangePickerComponentsProps` type + - Rename the `calendar` class-key to `dateRange` + +- `DateTimePicker`: + + - Replace the `componentsProps` prop with `slotProps` + - Remove the `DateTimePickerComponentsProps` type + - Replace the `formControl` class-key with two separate class-keys: `dateFormControl` and `timeFormControl` + +- `TimeRangePicker`: + + - Replace the `componentsProps` prop with `slotProps` + - Remove the `TimeRangePickerComponentsProps` and `TimeRangePickerIndividualPickerProps` types + - Replace the `formControl` class-key with two separate class-keys: `startFormControl` and `endFormControl` + - Replace the `timePicker` class-key with two separate class-keys: `startTimePicker` and `endTimePicker` diff --git a/.changeset/tender-dots-warn.md b/.changeset/tender-dots-warn.md new file mode 100644 index 0000000000..4ce98620f7 --- /dev/null +++ b/.changeset/tender-dots-warn.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove the `paper` class-key from `FilterBarPopoverFilter` + +Instead of using `styleOverrides` for `paper` in the theme, use the `slotProps` and `sx` props. diff --git a/.changeset/tender-pigs-rest.md b/.changeset/tender-pigs-rest.md new file mode 100644 index 0000000000..716d36c023 --- /dev/null +++ b/.changeset/tender-pigs-rest.md @@ -0,0 +1,21 @@ +--- +"@comet/admin-theme": minor +--- + +Add custom `Typography` variants for displaying inline lists + +```tsx + + Lorem ipsum + Lorem ipsum + Lorem ipsum + +``` + +Hint: To use the custom variants without getting a type error, you must adjust the `vendors.d.ts` in your project: + +```diff ++ /// + +// ... +``` \ No newline at end of file diff --git a/.changeset/thin-ligers-pull.md b/.changeset/thin-ligers-pull.md new file mode 100644 index 0000000000..6f2236cc91 --- /dev/null +++ b/.changeset/thin-ligers-pull.md @@ -0,0 +1,7 @@ +--- +"@comet/admin-react-select": major +--- + +Remove `ControlInput` component + +`ControlInput` was never intended to be exported, use MUI's `InputBase` instead. diff --git a/.changeset/tidy-impalas-refuse.md b/.changeset/tidy-impalas-refuse.md new file mode 100644 index 0000000000..113f173e47 --- /dev/null +++ b/.changeset/tidy-impalas-refuse.md @@ -0,0 +1,7 @@ +--- +"@comet/admin-date-time": major +--- + +Remove `clearable` prop from `DatePicker` + +The clear button will automatically be shown for all optional fields. diff --git a/.changeset/tough-zoos-refuse.md b/.changeset/tough-zoos-refuse.md new file mode 100644 index 0000000000..7a4e60cc4e --- /dev/null +++ b/.changeset/tough-zoos-refuse.md @@ -0,0 +1,8 @@ +--- +"@comet/admin": major +--- + +Remove the `disabled` and `focusVisible` class-keys and rename the `inner` class-key to `content` in `AppHeaderButton` + +Use the `:disabled` selector instead when styling the disabled state. +Use the `:focus` selector instead when styling the focus state. diff --git a/.changeset/tricky-paws-march.md b/.changeset/tricky-paws-march.md new file mode 100644 index 0000000000..35bfd67fe8 --- /dev/null +++ b/.changeset/tricky-paws-march.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove `endAdornment` prop from `FinalFormSelect` component + +The reason were conflicts with the clearable prop. This decision was based on the fact that MUI doesn't support endAdornment on selects (see: [mui/material-ui#17799](https://github.com/mui/material-ui/issues/17799)), and that there are no common use cases where both `endAdornment` and `clearable` are needed simultaneously. diff --git a/.changeset/twelve-spoons-play.md b/.changeset/twelve-spoons-play.md new file mode 100644 index 0000000000..077fd13a40 --- /dev/null +++ b/.changeset/twelve-spoons-play.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": major +--- + +Remove the `disabled` class-key in `ClearInputButton` + +Use the `:disabled` selector instead when styling the disabled state. diff --git a/.changeset/two-lions-promise.md b/.changeset/two-lions-promise.md new file mode 100644 index 0000000000..4a57683e17 --- /dev/null +++ b/.changeset/two-lions-promise.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": major +--- + +`MenuItem` no longer supports props from MUI's `ListItem` but those from `ListItemButton` instead diff --git a/.changeset/unlucky-files-compare.md b/.changeset/unlucky-files-compare.md deleted file mode 100644 index 2455678e2d..0000000000 --- a/.changeset/unlucky-files-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@comet/eslint-config": major ---- - -Enable no-other-module-relative-import rule by default diff --git a/.changeset/violet-wombats-try.md b/.changeset/violet-wombats-try.md new file mode 100644 index 0000000000..ba2f8739fb --- /dev/null +++ b/.changeset/violet-wombats-try.md @@ -0,0 +1,8 @@ +--- +"@comet/admin": major +--- + +Remove the `popoverPaper` class-key and rename the `popoverRoot` class-key to `popover` in `AppHeaderDropdown` + +Instead of using `styleOverrides` for `popoverPaper` in the theme, use the `slotProps` and `sx` props. +Use the `popover` prop instead of `popoverRoot` to override styles. diff --git a/.changeset/weak-moles-destroy.md b/.changeset/weak-moles-destroy.md new file mode 100644 index 0000000000..0df4d8e361 --- /dev/null +++ b/.changeset/weak-moles-destroy.md @@ -0,0 +1,5 @@ +--- +"@comet/admin": patch +--- + +Fix a bug where the `disabled` prop would not be passed to the children of `Field` diff --git a/.changeset/yellow-houses-talk.md b/.changeset/yellow-houses-talk.md new file mode 100644 index 0000000000..82ab535d20 --- /dev/null +++ b/.changeset/yellow-houses-talk.md @@ -0,0 +1,106 @@ +--- +"@comet/admin-color-picker": major +"@comet/admin-react-select": major +"@comet/admin-date-time": major +"@comet/blocks-admin": major +"@comet/admin-theme": major +"@comet/admin-rte": major +"@comet/cms-admin": major +"@comet/admin": major +--- + +Change the method of overriding the styling of Admin components + +- Remove dependency on the legacy `@mui/styles` package in favor of `@mui/material/styles`. +- Add the ability to style components using [MUI's `sx` prop](https://mui.com/system/getting-started/the-sx-prop/). +- Add the ability to style individual elements (slots) of a component using the `slotProps` and `sx` props. +- The `$` syntax in the theme's `styleOverrides` is no longer supported, see: https://mui.com/material-ui/migration/v5-style-changes/#migrate-theme-styleoverrides-to-emotion + +```diff + const theme = createCometTheme({ + components: { + CometAdminMyComponent: { + styleOverrides: { +- root: { +- "&$hasShadow": { +- boxShadow: "2px 2px 5px 0 rgba(0, 0, 0, 0.25)", +- }, +- "& $header": { +- backgroundColor: "lime", +- }, +- }, ++ hasShadow: { ++ boxShadow: "2px 2px 5px 0 rgba(0, 0, 0, 0.25)", ++ }, ++ header: { ++ backgroundColor: "lime", ++ }, + }, + }, + }, + }); +``` + +- Overriding a component's styles using `withStyles` is no longer supported. Use the `sx` and `slotProps` props instead: + +```diff +-import { withStyles } from "@mui/styles"; +- +-const StyledMyComponent = withStyles({ +- root: { +- backgroundColor: "lime", +- }, +- header: { +- backgroundColor: "fuchsia", +- }, +-})(MyComponent); +- +-// ... +- +-; ++ +``` + +- The module augmentation for the `DefaultTheme` type from `@mui/styles/defaultTheme` is no longer needed and needs to be removed from the admins theme file, usually located in `admin/src/theme.ts`: + +```diff +-declare module "@mui/styles/defaultTheme" { +- // eslint-disable-next-line @typescript-eslint/no-empty-interface +- export interface DefaultTheme extends Theme {} +-} +``` + +- Class-keys originating from MUI components have been removed from Comet Admin components, causing certain class-names and `styleOverrides` to no longer be applied. + The components `root` class-key is not affected. Other class-keys will retain the class-names and `styleOverrides` from the underlying MUI component. + For example, in `ClearInputAdornment` (when used with `position="end"`) the class-name `CometAdminClearInputAdornment-positionEnd` and the `styleOverrides` for `CometAdminClearInputAdornment.positionEnd` will no longer be applied. + The component will retain the class-names `MuiInputAdornment-positionEnd`, `MuiInputAdornment-root`, and `CometAdminClearInputAdornment-root`. + Also, the `styleOverrides` for `MuiInputAdornment.positionEnd`, `MuiInputAdornment.root`, and `CometAdminClearInputAdornment.root` will continue to be applied. + + This affects the following components: + + - `AppHeader` + - `AppHeaderMenuButton` + - `ClearInputAdornment` + - `Tooltip` + - `CancelButton` + - `DeleteButton` + - `OkayButton` + - `SaveButton` + - `StackBackButton` + - `DatePicker` + - `DateRangePicker` + - `TimePicker` + +- For more details, see MUI's migration guide: https://mui.com/material-ui/migration/v5-style-changes/#mui-styles diff --git a/.changeset/yellow-seahorses-lick.md b/.changeset/yellow-seahorses-lick.md index 71ca251373..245ed4b6f4 100644 --- a/.changeset/yellow-seahorses-lick.md +++ b/.changeset/yellow-seahorses-lick.md @@ -5,7 +5,12 @@ "@comet/cms-site": minor --- -Migrate site preview to Next.js Preview Mode +Uses the Next.JS Preview mode for the site preview + +The preview is entered by navigating to an API-Route in the site, which has to be executed in a secured environment. +In the API-Routes the current scope is checked (and possibly stored), then the client is redirected to the Preview. + +// TODO Move the following introduction to the migration guide before releasing Requires following changes to site: @@ -16,8 +21,12 @@ Requires following changes to site: - Just implement `getStaticProps`/`getServerSideProps` (Preview Mode will SSR automatically) - Get `previewData` from `context` and use it to configure the GraphQL Client - Add `SitePreviewProvider` to `App` (typically in `src/pages/_app.tsx`) -- Add `/api/preview` Next API route (see demo) +- Provide a protected environment for the site + - Make sure that a Authorization-Header is present in this environment + - Add a Next.JS API-Route for the site preview (eg. `/api/site-preview`) + - Call `getValidatedSitePreviewParams()` in the API-Route (calls the API which checks the Authorization-Header with the submitted scope) + - Use the `path`-part of the return value to redirect to the preview -Requires following changes to API: +Requires following changes to admin -- Set `sitePreviewSecret` in `PageTreeModule`-options (make sure it's the same across multiple API-instances) +- The `SitesConfig` must provide a `sitePreviewApiUrl` diff --git a/.changeset/young-lies-cry.md b/.changeset/young-lies-cry.md new file mode 100644 index 0000000000..8ede23a758 --- /dev/null +++ b/.changeset/young-lies-cry.md @@ -0,0 +1,7 @@ +--- +"@comet/admin-theme": major +--- + +Rework shadows + +- Change shadows 1 - 4 diff --git a/.env b/.env index 5e98ce65ce..b941576b81 100644 --- a/.env +++ b/.env @@ -49,10 +49,6 @@ BLOB_STORAGE_DIRECTORY_PREFIX="comet-demo" # dam DAM_SECRET=6a9e8a185b513363bc89ec0b96eed8f70c759bc86b97319f60365c4b7f8593dc -DAM_CDN_ENABLED="false" -DAM_CDN_DOMAIN= -DAM_CDN_ORIGIN_HEADER= -DAM_DISABLE_CDN_ORIGIN_HEADER_CHECK=false # admin ADMIN_PORT=8000 @@ -64,11 +60,8 @@ SITE_PORT=3000 SITE_URL=http://localhost:$SITE_PORT SITE_PRELOGIN_ENABLED=false SITE_PRELOGIN_PASSWORD=password -# no gtm in dev mode -NEXT_PUBLIC_GTM_ID= NEXT_PUBLIC_SITE_DOMAIN=main -NEXT_PUBLIC_SITE_LANGUAGES=en -NEXT_PUBLIC_SITE_DEFAULT_LANGUAGE=en +NEXT_PUBLIC_API_URL=$API_URL # jaegertracing JAEGER_UI_PORT=16686 diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000000..145ba47897 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,10 @@ +addAssignees: author + +addReviewers: true + +reviewers: + - johnnyomair + +skipKeywords: + - Merge main into next + - Version Packages diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ea3b1ec5f7..07641a2cfd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,14 @@ +--- + +## PR Checklist +- [ ] Verify if the change requires a changeset. See [CONTRIBUTING.md](https://github.com/vivid-planet/comet/blob/HEAD/CONTRIBUTING.md) +- [ ] Link to the respective task if one exists: +- [ ] Provide screenshots/screencasts if the change contains visual changes - -- [ ] Add changeset (if necessary) \ No newline at end of file +
+ Screenshots/screencasts + +
diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index 2925647ac3..ebe7872f56 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -25,13 +25,6 @@ jobs: token: ${{ secrets.VIVID_PLANET_BOT_TOKEN }} path: "demo/admin/lang/comet-lang" - - name: "Demo: Clone translations" - uses: actions/checkout@v3 - with: - repository: vivid-planet/comet-demo-lang - token: ${{ secrets.VIVID_PLANET_BOT_TOKEN }} - path: "demo/admin/lang/comet-demo-lang" - - uses: pnpm/action-setup@v2 with: version: 8 @@ -50,18 +43,43 @@ jobs: pnpm run intl:extract cp -r lang/* demo/admin/lang/comet-lang/ - - name: "Library: Update translateable strings" + - name: "Library: Update translatable strings" uses: EndBug/add-and-commit@v9 with: cwd: "./demo/admin/lang/comet-lang" - - name: "Demo: Extract i18n strings" + - name: "Demo Admin: Clone translations" + uses: actions/checkout@v3 + with: + repository: vivid-planet/comet-demo-lang + token: ${{ secrets.VIVID_PLANET_BOT_TOKEN }} + path: "demo/admin/lang/comet-demo-lang" + + - name: "Demo Admin: Extract i18n strings" run: | cd demo/admin pnpm run intl:extract - cp -r lang-extracted/* lang/comet-demo-lang + cp -r lang-extracted/* lang/comet-demo-lang/admin - - name: "Demo: Update translateable strings" + - name: "Demo Admin: Update translatable strings" uses: EndBug/add-and-commit@v9 with: cwd: "./demo/admin/lang/comet-demo-lang" + + - name: "Demo Site: Clone translations" + uses: actions/checkout@v3 + with: + repository: vivid-planet/comet-demo-lang + token: ${{ secrets.VIVID_PLANET_BOT_TOKEN }} + path: "demo/site/lang/comet-demo-lang" + + - name: "Demo Site: Extract i18n strings" + run: | + cd demo/site + pnpm run intl:extract + cp -r lang-extracted/* lang/comet-demo-lang/site + + - name: "Demo Site: Update translatable strings" + uses: EndBug/add-and-commit@v9 + with: + cwd: "./demo/site/lang/comet-demo-lang" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 79c555768a..bbd5bf9d70 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,6 @@ on: - opened - synchronize - reopened - - closed push: branches: - main @@ -40,6 +39,15 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} path: "demo/admin/lang/comet-demo-lang" + - name: Initial lang content (TODO remove this step after all lang files are in the repo) + run: | + mkdir -p demo/admin/lang/comet-demo-lang/admin + test -f demo/admin/lang/comet-demo-lang/admin/en.json || echo "{}" > demo/admin/lang/comet-demo-lang/admin/en.json + test -f demo/admin/lang/comet-demo-lang/admin/de.json || echo "{}" > demo/admin/lang/comet-demo-lang/admin/de.json + mkdir -p demo/site/lang/comet-demo-lang/site + test -f demo/site/lang/comet-demo-lang/site/en.json || echo "{}" > demo/site/lang/comet-demo-lang/site/en.json + test -f demo/site/lang/comet-demo-lang/site/de.json || echo "{}" > demo/site/lang/comet-demo-lang/site/de.json + - uses: pnpm/action-setup@v2 with: version: 8 @@ -57,7 +65,7 @@ jobs: run: pnpm run copy-schema-files - name: Build - run: pnpm run build:lib + run: pnpm run build:packages - name: Lint run: | @@ -65,13 +73,3 @@ jobs: pnpm run lint # check for duplicate ids of formatted messages pnpm run intl:extract - - - name: Test - run: pnpm run test - - - name: Upload test results - uses: actions/upload-artifact@v3 - if: success() || failure() - with: - name: test-results - path: packages/**/junit.xml diff --git a/.github/workflows/main-into-next-pr.yml b/.github/workflows/main-into-next-pr.yml index ca9a473789..990189733e 100644 --- a/.github/workflows/main-into-next-pr.yml +++ b/.github/workflows/main-into-next-pr.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # Needed to also fetch next branch @@ -36,9 +36,9 @@ jobs: echo 'PR_BODY=This is an automated pull request to merge changes from `main` into `next`. It has merge conflicts. To resolve conflicts, check out the branch `merge-main-into-next` locally, make any necessary changes to conflicting files, and commit and publish your changes.' >> $GITHUB_ENV - name: Create pull request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.VIVID_PLANET_BOT_TOKEN }} title: ${{ env.PR_TITLE }} body: ${{ env.PR_BODY }} base: next diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml index bc92d115f8..3bf69649fc 100644 --- a/.github/workflows/test-report.yml +++ b/.github/workflows/test-report.yml @@ -1,7 +1,7 @@ name: "Test Report" on: workflow_run: - workflows: ["Lint"] + workflows: ["Test"] types: - completed jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..44d1242e46 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,55 @@ +name: Test + +on: + pull_request: + types: + - opened + - synchronize + - reopened + push: + branches: + - main + - next + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - run: echo "${{ github.actor }}" + + - uses: actions/checkout@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - run: | + git config user.name github-actions + git config user.email github-actions@github.com + + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org" + cache: "pnpm" # https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-dependencies + + - run: pnpm install --frozen-lockfile + + - name: Copy schema files + run: pnpm run copy-schema-files + + - name: Build + run: pnpm run build:packages + + - name: Test + run: pnpm run test + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: success() || failure() + with: + name: test-results + path: packages/**/junit.xml diff --git a/.gitignore b/.gitignore index 1fee62d94c..1096bee992 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ lang/ .pnp.* junit.xml .env.local +**/.idea diff --git a/README.md b/README.md index d4f0c68353..183cd64ef8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,11 @@ Visit https://docs.comet-dxp.com/ to view the documentation. -## Getting started +## Create a new Comet DXP project + +Use `@comet/create-app` to create a new Comet DXP project. More information can be found in the [docs](https://docs.comet-dxp.com/docs/getting-started/). + +## Development ### Prerequisites @@ -30,32 +34,76 @@ sh install.sh _It is recommended to run `install.sh` every time you switch to the `main` branch._ +### Build packages + +Before starting individual development processes, build all Comet packages at least once. + +```bash +pnpm run build:packages +``` + +_It is recommended to build all packages every time you switch to the `main` branch._ + ### Start development processes [dev-process-manager](https://github.com/vivid-planet/dev-process-manager) is used for local development. +We recommend only running the development process you will need. +Typically, you will need a subset of the available development processes. -Start Comet Admin packages +Here are a few examples: -```bash -pnpm run dev:admin -``` +1. You want to add a new component to `@comet/admin` -Start CMS packages + Start the development process for `@comet/admin`: -```bash -pnpm run dev:cms -``` + ```bash + npx dev-pm start @comet-admin + ``` -It is also possbile to start specific microservices + Create a development story in Storybook: -```bash -pnpm run dev:cms:api # (api|admin|site) -``` + ```bash + pnpm run storybook + ``` + +2. You want to add a CMS feature to the API + + Start the development process for `@comet/cms-api`: + + ```bash + npx dev-pm start @cms-api + ``` + + Start Demo API: + + ```bash + npx dev-pm start @demo-api + ``` + + The Demo API will be available at [http://localhost:4000/](http://localhost:4000/) + +3. You want to add a CMS feature to the Admin + + Start the development process for `@comet/cms-admin`: + + ```bash + npx dev-pm start @cms-admin + ``` + + Start Demo API and Admin: + + ```bash + npx dev-pm start @demo-api @demo-admin + ``` + + The Demo Admin will be available at [http://localhost:8000/](http://localhost:8000/) + +See [dev-pm.config.js](/dev-pm.config.js) for a list of all available processes and process groups. #### Start Demo ```bash -pnpm run dev:demo +npx dev-pm start @demo ``` Demo will be available at @@ -64,10 +112,10 @@ Demo will be available at - API: [http://localhost:4000/](http://localhost:4000/) - Site: [http://localhost:3000/](http://localhost:3000/) -It is also possbile to start specific microservices +It is also possible to start specific microservices ```bash -pnpm run dev:demo:api # (api|admin|site) +npx dev-pm start @demo-api # (@demo-api|@demo-admin|@demo-site) ``` #### Start Storybook @@ -86,12 +134,25 @@ pnpm run docs The docs will be available at [http://localhost:3000/](http://localhost:3000/) -### Stop Processes +### Stop processes ```bash npx dev-pm shutdown ``` +### Dev scripts + +We provide `dev:*` scripts for the most common use cases. +For example, to start the Demo, you can also run: + +```bash +pnpm run dev:demo +``` + +However, we recommend directly using dev-process-manager for greater control over which development processes to start. + +See [package.json](/package.json) for a list of all available dev scripts. + ## Develop in a project ### additional Requirements diff --git a/SECURITY.md b/SECURITY.md index 657ada47de..e842f38ad0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,12 +4,12 @@ The versions of the project that are currently supported with security updates. -| Version | Supported | -| ------: | :---------------------------------- | -| 5.x | :white_check_mark: | -| 4.x | :white_check_mark: | -| 3.x | :white_check_mark: (until March 24) | -| < 3.0 | :x: | +| Version | Supported | +| ------: | :------------------------------------ | +| 6.x | :white_check_mark: | +| 5.x | :white_check_mark: (until 2025-01-30) | +| 4.x | :white_check_mark: (until 2024-11-21) | +| < 4.x | :x: | ## Reporting a vulnerability diff --git a/copy-schema-files.js b/copy-schema-files.js index d2a25e5c41..c6f9f8d1f7 100644 --- a/copy-schema-files.js +++ b/copy-schema-files.js @@ -11,7 +11,7 @@ const fs = require("fs"); fs.promises.copyFile("demo/api/block-meta.json", "demo/site/block-meta.json"), fs.promises.copyFile("demo/api/schema.gql", "demo/admin/schema.gql"), fs.promises.copyFile("demo/api/schema.gql", "demo/site/schema.gql"), - fs.promises.copyFile("demo/api/comet-config.json", "demo/site/comet-config.json"), - fs.promises.copyFile("demo/api/comet-config.json", "demo/admin/comet-config.json"), + fs.promises.copyFile("demo/api/src/comet-config.json", "demo/site/src/comet-config.json"), + fs.promises.copyFile("demo/api/src/comet-config.json", "demo/admin/src/comet-config.json"), ]); })(); diff --git a/demo/admin/.gitignore b/demo/admin/.gitignore index 170cb27250..2f88443846 100644 --- a/demo/admin/.gitignore +++ b/demo/admin/.gitignore @@ -12,5 +12,6 @@ lang-compiled graphql.generated.ts block-meta.json src/blocks.generated.ts -comet-config.json +src/comet-config.json src/**/*.generated.ts +.swc/ \ No newline at end of file diff --git a/demo/admin/public/index.ejs b/demo/admin/index.html similarity index 65% rename from demo/admin/public/index.ejs rename to demo/admin/index.html index 07769481ff..28e24576b3 100644 --- a/demo/admin/public/index.ejs +++ b/demo/admin/index.html @@ -6,13 +6,11 @@ Comet Demo Admin -
- -
+
diff --git a/demo/admin/package.json b/demo/admin/package.json index 638e99d1a6..3562eccbe4 100644 --- a/demo/admin/package.json +++ b/demo/admin/package.json @@ -3,22 +3,22 @@ "version": "1.0.0", "private": true, "scripts": { - "admin-generator": "rimraf 'src/*/generated' && comet-admin-generator generate crud-generator-config.ts", - "build": "run-s intl:compile && run-p gql:types generate-block-types && cross-env BABEL_ENV=production webpack --config ./webpack.config.ts --env production --mode production --progress", + "admin-generator": "rimraf 'src/*/generated' && comet-admin-generator generate crud-generator-config.ts && comet-admin-generator future-generate", + "build": "run-s intl:compile && run-p gql:types generate-block-types && NODE_ENV=production vite build", + "preview": "npm run build && vite preview", "generate-block-types": "comet generate-block-types --inputs", "generate-block-types:watch": "chokidar -s \"**/block-meta.json\" -c \"npm run generate-block-types\"", "gql:types": "graphql-codegen", "gql:watch": "graphql-codegen --watch", - "postinstall": "cd server && npm install", "intl:compile": "run-p intl:compile:comet intl:compile:comet-demo", "intl:compile:comet": "formatjs compile-folder --format simple --ast lang/comet-lang lang-compiled/comet-lang", - "intl:compile:comet-demo": "formatjs compile-folder --format simple --ast lang/comet-demo-lang lang-compiled/comet-demo-lang", + "intl:compile:comet-demo": "formatjs compile-folder --format simple --ast lang/comet-demo-lang/admin lang-compiled/comet-demo-lang-admin", "intl:extract": "formatjs extract \"src/**/*.ts*\" --ignore ./**.d.ts --out-file lang-extracted/en.json --format simple", "lint": "run-s intl:compile && run-p gql:types generate-block-types && run-p lint:eslint lint:tsc && $npm_execpath lint:generated-files-not-modified", "lint:eslint": "eslint --max-warnings 0 --config ./.eslintrc.cli.js --ext .ts,.tsx,.js,.jsx,.json,.md src/ package.json", "lint:tsc": "tsc --project .", "lint:generated-files-not-modified": "$npm_execpath admin-generator && git diff --exit-code HEAD --", - "start": "run-s intl:compile && run-p gql:types generate-block-types && dotenv -- cross-env BABEL_ENV=development webpack serve --config ./webpack.config.ts --mode development" + "start": "run-s intl:compile && run-p gql:types generate-block-types && dotenv -- chokidar --initial -s \"../../packages/admin/*/src/**\" -c \"kill-port $ADMIN_PORT && vite --force\"" }, "dependencies": { "@apollo/client": "^3.7.0", @@ -37,11 +37,10 @@ "@mui/icons-material": "^5.0.0", "@mui/lab": "^5.0.0-alpha.76", "@mui/material": "^5.0.0", - "@mui/styles": "^5.0.0", "@mui/system": "^5.0.0", "@mui/x-data-grid": "^5.15.2", "@mui/x-data-grid-pro": "^5.15.2", - "axios": "^0.21.0", + "change-case": "^5.2.0", "date-fns": "^2.0.0", "dnd-core": "^10.0.2", "draft-js": "^0.11.0", @@ -90,7 +89,6 @@ "devDependencies": { "@comet/cli": "workspace:*", "@comet/eslint-config": "workspace:*", - "@emotion/babel-plugin": "^11.0.0", "@formatjs/cli": "^2.0.0", "@gitbeaker/node": "^25.0.0", "@graphql-codegen/add": "^3.0.0", @@ -102,6 +100,7 @@ "@graphql-codegen/typescript": "^2.0.0", "@graphql-codegen/typescript-operations": "^2.0.0", "@ory/hydra-client": "^1.0.0", + "@swc/plugin-emotion": "^2.5.120", "@types/draft-js": "^0.11.10", "@types/final-form-set-field-touched": "^1.0.0", "@types/lodash.escaperegexp": "^4.0.0", @@ -118,28 +117,20 @@ "@types/react-router-dom": "^5.0.0", "@types/react-select": "^3.0.0", "@types/uuid": "^7.0.0", - "@types/webpack-env": "^1.0.0", "@types/zen-observable": "^0.8.0", - "babel-loader": "^8.0.0", - "babel-plugin-inline-react-svg": "^2.0.0", - "babel-preset-react-app": "^10.0.0", + "@vitejs/plugin-react-swc": "^3.6.0", "chokidar-cli": "^2.0.0", "cosmiconfig-toml-loader": "^1.0.0", - "cross-env": "^7.0.0", - "css-loader": "^6.0.0", "dotenv-cli": "^4.0.0", "eslint": "^8.0.0", "eslint-plugin-graphql": "^4.0.0", - "graphql-request": "^3.0.0", - "html-webpack-plugin": "^5.0.0", + "kill-port": "^2.0.1", "npm-run-all": "^4.1.5", "pascal-case": "^3.0.0", "prettier": "^2.0.0", - "style-loader": "^3.0.0", "ts-node": "^10.0.0", "typescript": "^4.0.0", - "webpack": "^5.0.0", - "webpack-cli": "^4.0.0", - "webpack-dev-server": "^4.0.0" + "vite": "^5.1.6", + "vite-plugin-html": "^3.2.2" } } diff --git a/demo/admin/server/package-lock.json b/demo/admin/server/package-lock.json deleted file mode 100644 index e345ac25e1..0000000000 --- a/demo/admin/server/package-lock.json +++ /dev/null @@ -1,1153 +0,0 @@ -{ - "name": "server", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "server", - "version": "1.0.0", - "dependencies": { - "compression": "^1.7.4", - "express": "^4.18.2" - }, - "engines": { - "node": "18", - "npm": "9" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "engines": { - "node": ">= 0.8" - } - } - }, - "dependencies": { - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - } - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - } - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - } - } -} diff --git a/demo/admin/server/package.json b/demo/admin/server/package.json index 027a4e92be..62f1a66a09 100644 --- a/demo/admin/server/package.json +++ b/demo/admin/server/package.json @@ -8,9 +8,5 @@ "scripts": { "preserve": "envsubst < \"../build/index.html\" > \"/tmp/index.html\" && mv /tmp/index.html ../build/index.html", "serve": "node server.js" - }, - "engines": { - "node": "18", - "npm": "9" } } diff --git a/demo/admin/src/App.tsx b/demo/admin/src/App.tsx index 45074e3b0f..ed2162eb99 100644 --- a/demo/admin/src/App.tsx +++ b/demo/admin/src/App.tsx @@ -4,55 +4,44 @@ import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "material-design-icons/iconfont/material-icons.css"; import "typeface-open-sans"; +import "@src/polyfills"; import { ApolloProvider } from "@apollo/client"; -import { ErrorDialogHandler, MasterLayout, MuiThemeProvider, RouterBrowserRouter, RouteWithErrorBoundary, SnackbarProvider } from "@comet/admin"; +import { ErrorDialogHandler, MasterLayout, MuiThemeProvider, RouterBrowserRouter, SnackbarProvider } from "@comet/admin"; import { - AllCategories, CmsBlockContextProvider, + ContentScopeInterface, + createDamFileDependency, createHttpClient, - createRedirectsPage, - CronJobsPage, + CurrentUserProvider, DamConfigProvider, - DamPage, + DependenciesConfigProvider, LocaleProvider, - PagesPage, - PublisherPage, - SiteConfig, + MasterMenuRoutes, SitePreview, SitesConfigProvider, - UserPermissionsPage, } from "@comet/cms-admin"; import { css, Global } from "@emotion/react"; import { createApolloClient } from "@src/common/apollo/createApolloClient"; import ContentScopeProvider, { ContentScope } from "@src/common/ContentScopeProvider"; -import { additionalPageTreeNodeFieldsFragment, EditPageNode } from "@src/common/EditPageNode"; -import MasterHeader from "@src/common/MasterHeader"; -import MasterMenu from "@src/common/MasterMenu"; +import { additionalPageTreeNodeFieldsFragment } from "@src/common/EditPageNode"; import { createConfig } from "@src/config"; -import Dashboard from "@src/dashboard/Dashboard"; -import { PredefinedPage } from "@src/predefinedPage/PredefinedPage"; +import { ImportFromUnsplash } from "@src/dam/ImportFromUnsplash"; +import { pageTreeCategories } from "@src/pageTree/pageTreeCategories"; import theme from "@src/theme"; import * as React from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import * as ReactDOM from "react-dom"; import { FormattedMessage, IntlProvider } from "react-intl"; -import { Redirect, Route, Switch } from "react-router-dom"; +import { Route, Switch } from "react-router-dom"; -import { ComponentDemo } from "./common/ComponentDemo"; -import { ContentScopeIndicator } from "./common/ContentScopeIndicator"; +import MasterHeader from "./common/MasterHeader"; +import MasterMenu, { masterMenuData, pageTreeDocumentTypes } from "./common/MasterMenu"; import { getMessages } from "./lang"; import { Link } from "./links/Link"; -import { NewsLinkBlock } from "./news/blocks/NewsLinkBlock"; -import { NewsPage } from "./news/generated/NewsPage"; -import MainMenu from "./pages/mainMenu/MainMenu"; +import { NewsDependency } from "./news/dependencies/NewsDependency"; import { Page } from "./pages/Page"; -import ProductCategoriesPage from "./products/categories/ProductCategoriesPage"; -import { ProductsPage } from "./products/generated/ProductsPage"; -import ProductsHandmadePage from "./products/ProductsPage"; -import ProductTagsPage from "./products/tags/ProductTagsPage"; -import { urlParamToCategory } from "./utils/pageTreeNodeCategoryMapping"; const GlobalStyle = () => ( , - }, - { - category: "TopMenu", - label: , - }, -]; - -const pageTreeDocumentTypes = { - Page, - Link, - PredefinedPage, -}; - -const RedirectsPage = createRedirectsPage({ customTargets: { news: NewsLinkBlock }, scopeParts: ["domain"] }); - class App extends React.Component { public static render(baseEl: Element): void { ReactDOM.render(, baseEl); @@ -98,155 +68,88 @@ class App extends React.Component { , scope: ContentScope) => configs[scope.domain], + resolveSiteConfigForScope: (configs, scope: ContentScope) => configs[scope.domain], }} > - - - scope.domain}> - - - - - - - - - {({ match }) => ( - - {/* @TODO: add preview to contentScope once site is capable of contentScope */} - } - /> - ( - - - - - { - const category = urlParamToCategory(params.category); - - if (category === undefined) { - return ; - } - - return ( - ( - - )} - /> - ); + , + importSources: { + unsplash: { + label: , + }, + }, + }} + > + + + scope.domain}> + + + + + + + + + + + {({ match }) => ( + + {/* @TODO: add preview to contentScope once site is capable of contentScope */} + ( + { + return `/${scope.language}${path}`; }} + {...props} /> - - ( - ( - - )} - /> - )} - /> - - - - - ( - - )} - /> - - - - - - - - - - - - - - )} - /> - - )} - - - - - - - - - - + )} + /> + ( + + + + )} + /> + + )} + + + + + + + + + + + diff --git a/demo/admin/src/common/ComponentDemo.tsx b/demo/admin/src/common/ComponentDemo.tsx index a73aa8b369..5296c6cf21 100644 --- a/demo/admin/src/common/ComponentDemo.tsx +++ b/demo/admin/src/common/ComponentDemo.tsx @@ -1,14 +1,15 @@ import { + CheckboxField, Field, FieldContainer, FinalForm, - FinalFormCheckbox, - FinalFormInput, FinalFormRadio, - FinalFormSelect, - FinalFormSwitch, MainContent, + SelectField, Stack, + SwitchField, + TextAreaField, + TextField, } from "@comet/admin"; import { Add, FocusPointCenter, FocusPointNortheast, FocusPointNorthwest, FocusPointSoutheast, FocusPointSouthwest, Snips } from "@comet/admin-icons"; import { @@ -128,51 +129,43 @@ export function ComponentDemo(): React.ReactElement { }} initialValues={{ richText: RichTextBlock.defaultValues() }} > - - - - {(props) => ( - - Option 1 - Option 2 - Option 3 - - )} - - - - - - {(props) => ( - - Option 1 - Option 2 - Option 3 - - )} - - - - {(props) => ( - - Option 1 - Option 2 - Option 3 - - )} - - - - {(props) => ( - - Option 1 - Option 2 - Option 3 - - )} - - - + + + Option 1 + Option 2 + Option 3 + + + + + + + + Option 1 + Option 2 + Option 3 + + + + Option 1 + Option 2 + Option 3 + + + + Option 1 + Option 2 + Option 3 + + + + Option 1 + Option 2 + Option 3 + + + @@ -183,32 +176,24 @@ export function ComponentDemo(): React.ReactElement { } > - {(props) => ( - - 1 - 2 - 3 - - )} - - - - {(props) => ( - - - } primary="Option 1" secondary="Secondary text" /> - - - } primary="Option 2" secondary="Secondary text" /> - - - } primary="Option 3" secondary="Secondary text" /> - - - )} - - - + 1 + 2 + 3 + + + + + } primary="Option 1" secondary="Secondary text" /> + + + } primary="Option 2" secondary="Secondary text" /> + + + } primary="Option 3" secondary="Secondary text" /> + + + + @@ -222,26 +207,22 @@ export function ComponentDemo(): React.ReactElement { {(props) => } />} + + {(props) => } />} + - - {(props) => } />} - - - {(props) => } />} - - - {(props) => } />} - + + + + - - {(props) => } />} - + + + - - {(props) => } />} - + - - {(props) => ( - - 2:3 - 4:3 - 16:9 - - )} - - - {(props) => ( - - 0% - 10% - 20% - - )} - - - {(props) => } />} - + + 2:3 + 4:3 + 16:9 + + + 0% + 10% + 20% + + + diff --git a/demo/admin/src/common/ContentScopeProvider.tsx b/demo/admin/src/common/ContentScopeProvider.tsx index b66c29b4ba..c247369d8b 100644 --- a/demo/admin/src/common/ContentScopeProvider.tsx +++ b/demo/admin/src/common/ContentScopeProvider.tsx @@ -1,5 +1,3 @@ -import { gql, useQuery } from "@apollo/client"; -import { Loading } from "@comet/admin"; import { Domain as DomainIcon } from "@comet/admin-icons"; import { ContentScopeConfigProps, @@ -11,12 +9,12 @@ import { useContentScope as useContentScopeLibrary, UseContentScopeApi, useContentScopeConfig as useContentScopeConfigLibrary, + useCurrentUser, useSitesConfig, } from "@comet/cms-admin"; +import { SitesConfig } from "@src/config"; import React from "react"; -import { GQLCurrentUserScopeQuery } from "./ContentScopeProvider.generated"; - type Domain = "main" | "secondary" | string; type Language = "en" | string; export interface ContentScope { @@ -52,29 +50,21 @@ export function useContentScopeConfig(p: ContentScopeConfigProps): void { return useContentScopeConfigLibrary(p); } -const currentUserQuery = gql` - query CurrentUserScope { - currentUser { - role - domains - } - } -`; - const ContentScopeProvider: React.FC> = ({ children }) => { - const sitesConfig = useSitesConfig(); - const { loading, data } = useQuery(currentUserQuery); - - if (loading || !data) return ; + const sitesConfig = useSitesConfig(); + const user = useCurrentUser(); - const allowedUserDomains = data.currentUser.domains; + const allowedUserDomains = user.allowedContentScopes.map((scope) => scope.domain); const allowedSiteConfigs = Object.fromEntries( Object.entries(sitesConfig.configs).filter(([siteKey, siteConfig]) => allowedUserDomains.includes(siteKey)), ); const values: ContentScopeValues = { domain: Object.keys(allowedSiteConfigs).map((key) => ({ value: key })), - language: [{ label: "English", value: "en" }], + language: [ + { label: "English", value: "en" }, + { label: "German", value: "de" }, + ], }; return ( diff --git a/demo/admin/src/common/EditPageNode.tsx b/demo/admin/src/common/EditPageNode.tsx index d4cc1b48bf..0c7dc1b4b4 100644 --- a/demo/admin/src/common/EditPageNode.tsx +++ b/demo/admin/src/common/EditPageNode.tsx @@ -1,9 +1,10 @@ import { gql } from "@apollo/client"; -import { Field, FinalFormSelect } from "@comet/admin"; +import { SelectField } from "@comet/admin"; import { createEditPageNode } from "@comet/cms-admin"; import { Box, Divider, MenuItem } from "@mui/material"; import * as React from "react"; import { FormattedMessage } from "react-intl"; + export type { GQLPageTreeNodeAdditionalFieldsFragment } from "./EditPageNode.generated"; //re-export const userGroupOptions = [ @@ -42,22 +43,18 @@ export const EditPageNode = createEditPageNode({ - } name="userGroup" variant="horizontal" fullWidth > - {(props) => ( - - {userGroupOptions.map((option) => ( - - {option.label} - - ))} - - )} - + {userGroupOptions.map((option) => ( + + {option.label} + + ))} + ), }); diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx index 0eda220556..29f1b8f3f1 100644 --- a/demo/admin/src/common/MasterMenu.tsx +++ b/demo/admin/src/common/MasterMenu.tsx @@ -1,113 +1,242 @@ -import { Menu, MenuCollapsibleItem, MenuContext, MenuItemGroup, MenuItemRouterLink, useWindowSize } from "@comet/admin"; -import { Assets, Dashboard, Data, PageTree, Snips, Wrench } from "@comet/admin-icons"; -import { useContentScope } from "@comet/cms-admin"; -import { capitalize } from "@mui/material"; +import { Assets, Dashboard as DashboardIcon, Data, PageTree, Snips, Wrench } from "@comet/admin-icons"; +import { + createRedirectsPage, + CronJobsPage, + DamPage, + MasterMenu as CometMasterMenu, + MasterMenuData, + PagesPage, + PublisherPage, + UserPermissionsPage, +} from "@comet/cms-admin"; +import { ImportFromUnsplash } from "@src/dam/ImportFromUnsplash"; +import Dashboard from "@src/dashboard/Dashboard"; +import { GQLPageTreeNodeCategory } from "@src/graphql.generated"; +import { Link } from "@src/links/Link"; +import { NewsLinkBlock } from "@src/news/blocks/NewsLinkBlock"; +import { NewsPage } from "@src/news/generated/NewsPage"; +import MainMenu from "@src/pages/mainMenu/MainMenu"; +import { Page } from "@src/pages/Page"; +import { categoryToUrlParam, pageTreeCategories, urlParamToCategory } from "@src/pageTree/pageTreeCategories"; +import { PredefinedPage } from "@src/predefinedPage/PredefinedPage"; +import ProductCategoriesPage from "@src/products/categories/ProductCategoriesPage"; +import { ManufacturersPage as FutureManufacturersPage } from "@src/products/future/ManufacturersPage"; +import { ProductsPage as FutureProductsPage } from "@src/products/future/ProductsPage"; +import { ProductsWithLowPricePage as FutureProductsWithLowPricePage } from "@src/products/future/ProductsWithLowPricePage"; +import { ProductsPage } from "@src/products/generated/ProductsPage"; +import { ManufacturersPage as ManufacturersHandmadePage } from "@src/products/ManufacturersPage"; +import ProductsHandmadePage from "@src/products/ProductsPage"; +import ProductTagsPage from "@src/products/tags/ProductTagsPage"; import * as React from "react"; -import { useIntl } from "react-intl"; -import { useRouteMatch } from "react-router"; +import { FormattedMessage } from "react-intl"; +import { Redirect, RouteComponentProps } from "react-router-dom"; -const PERMANENT_MENU_MIN_WIDTH = 1024; +import { ComponentDemo } from "./ComponentDemo"; +import { ContentScopeIndicator } from "./ContentScopeIndicator"; +import { EditPageNode } from "./EditPageNode"; -const MasterMenu: React.FC = () => { - const { open, toggleOpen } = React.useContext(MenuContext); - const windowSize = useWindowSize(); - const intl = useIntl(); - const match = useRouteMatch(); - const { scope, values } = useContentScope(); - const useTemporaryMenu: boolean = windowSize.width < PERMANENT_MENU_MIN_WIDTH; - - // Open menu when changing to permanent variant and close when changing to temporary variant. - React.useEffect(() => { - if ((useTemporaryMenu && open) || (!useTemporaryMenu && !open)) { - toggleOpen(); - } - // useEffect dependencies must only include `location`, because the function should only be called once after changing the location. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location]); +export const pageTreeDocumentTypes = { + Page, + Link, + PredefinedPage, +}; - const scopeLangLabel = values.language.find(({ value }) => value === scope?.language)?.label; - const capitalizedScopeDomain = capitalize(scope?.domain); - const scopeLang = scopeLangLabel ?? scope?.language?.toUpperCase(); - const sectionScopeTitle = `${capitalizedScopeDomain} - ${scopeLang}`; +const RedirectsPage = createRedirectsPage({ customTargets: { news: NewsLinkBlock }, scopeParts: ["domain"] }); - return ( - - - } - to={`${match.url}/dashboard`} - /> - }> - - - +export const masterMenuData: MasterMenuData = [ + { + primary: , + icon: , + route: { + path: "/dashboard", + component: Dashboard, + }, + }, + { + primary: , + icon: , + submenu: pageTreeCategories.map((category) => ({ + primary: category.label, + to: `/pages/pagetree/${categoryToUrlParam(category.category as GQLPageTreeNodeCategory)}`, + })), + route: { + path: "/pages/pagetree/:category", + render: ({ match }: RouteComponentProps<{ category: string }>) => { + const category = urlParamToCategory(match.params.category); - } - > - - + if (category === undefined) { + return ; + } - }> - } /> - - }> - } /> - } /> - } /> - } /> - - - - } - to={`${match.url}/assets`} + ); + }, + }, + requiredPermission: "pageTree", + }, + { + primary: , + icon: , + submenu: [ + { + primary: , + route: { + path: "/structured-content/news", + component: NewsPage, + }, + }, + ], + requiredPermission: "news", + }, + { + primary: , + icon: , + route: { + path: "/assets", + render: () => ( + } + additionalToolbarItems={} /> - }> - - - - - } - /> - } - /> - - - ); -}; + ), + }, + requiredPermission: "dam", + }, + { + primary: , + icon: , + submenu: [ + { + primary: , + route: { + path: "/project-snips/main-menu", + component: MainMenu, + }, + }, + ], + requiredPermission: "pageTree", + }, + { + primary: , + icon: , + submenu: [ + { + primary: , + route: { + path: "/system/publisher", + component: PublisherPage, + }, + requiredPermission: "builds", + }, + { + primary: , + route: { + path: "/system/cron-jobs", + component: CronJobsPage, + }, + requiredPermission: "cronJobs", + }, + { + primary: , + route: { + path: "/system/redirects", + render: () => , + }, + requiredPermission: "pageTree", + }, + ], + requiredPermission: "pageTree", + }, + { + primary: , + icon: , + route: { + path: "/component-demo", + component: ComponentDemo, + }, + requiredPermission: "pageTree", + }, + { + primary: , + icon: , + submenu: [ + { + primary: , + route: { + path: "/products-future", + component: FutureProductsPage, + }, + }, + { + primary: , + route: { + path: "/manufacturers-future", + component: FutureManufacturersPage, + }, + }, + { + primary: , + route: { + path: "/products-with-low-price-future", + component: FutureProductsWithLowPricePage, + }, + }, + { + primary: , + route: { + path: "/products", + component: ProductsPage, + }, + }, + { + primary: , + route: { + path: "/product-categories", + component: ProductCategoriesPage, + }, + }, + { + primary: , + route: { + path: "/product-tags", + component: ProductTagsPage, + }, + }, + { + primary: , + route: { + path: "/products-handmade", + component: ProductsHandmadePage, + }, + }, + { + primary: , + route: { + path: "/manufacturers-handmade", + component: ManufacturersHandmadePage, + }, + }, + ], + requiredPermission: "products", + }, + { + primary: , + icon: , + route: { + path: "/user-permissions", + component: UserPermissionsPage, + }, + requiredPermission: "userPermissions", + }, +]; +const MasterMenu = () => ; export default MasterMenu; diff --git a/demo/admin/src/common/blocks/HeadlineBlock.tsx b/demo/admin/src/common/blocks/HeadlineBlock.tsx index df86eabf51..6c93a3393a 100644 --- a/demo/admin/src/common/blocks/HeadlineBlock.tsx +++ b/demo/admin/src/common/blocks/HeadlineBlock.tsx @@ -1,10 +1,7 @@ -import { Field, FinalFormInput, FinalFormSelect } from "@comet/admin"; -import { BlockCategory, BlocksFinalForm, createCompositeBlock, createCompositeSetting } from "@comet/blocks-admin"; +import { BlockCategory, createCompositeBlock, createCompositeBlockSelectField, createCompositeBlockTextField } from "@comet/blocks-admin"; import { createRichTextBlock } from "@comet/cms-admin"; -import { MenuItem } from "@mui/material"; import { HeadlineBlockData } from "@src/blocks.generated"; import { LinkBlock } from "@src/common/blocks/LinkBlock"; -import * as React from "react"; const RichTextBlock = createRichTextBlock({ link: LinkBlock, @@ -22,16 +19,8 @@ export const HeadlineBlock = createCompositeBlock( displayName: "Headline", blocks: { eyebrow: { - block: createCompositeSetting({ - defaultValue: undefined, - AdminComponent: ({ state, updateState }) => ( - > - onSubmit={({ eyebrow }) => updateState(eyebrow)} - initialValues={{ eyebrow: state }} - > - - - ), + block: createCompositeBlockTextField({ + fieldProps: { label: "Eyebrow" }, }), }, headline: { @@ -39,27 +28,17 @@ export const HeadlineBlock = createCompositeBlock( title: "Headline", }, level: { - block: createCompositeSetting({ + block: createCompositeBlockSelectField({ defaultValue: "header-one", - AdminComponent: ({ state, updateState }) => ( - > - onSubmit={({ level }) => updateState(level)} - initialValues={{ level: state }} - > - - {(props) => ( - - Header One - Header Two - Header Three - Header Four - Header Five - Header Six - - )} - - - ), + fieldProps: { label: "Level", fullWidth: true }, + options: [ + { value: "header-one", label: "Header One" }, + { value: "header-two", label: "Header Two" }, + { value: "header-three", label: "Header Three" }, + { value: "header-four", label: "Header Four" }, + { value: "header-five", label: "Header Five" }, + { value: "header-six", label: "Header Six" }, + ], }), }, }, diff --git a/demo/admin/src/common/blocks/LinkListBlock.tsx b/demo/admin/src/common/blocks/LinkListBlock.tsx index 9a1ce34f68..0b183c681b 100644 --- a/demo/admin/src/common/blocks/LinkListBlock.tsx +++ b/demo/admin/src/common/blocks/LinkListBlock.tsx @@ -17,13 +17,9 @@ export const LinkListBlock = createListBlock({ ...userGroupAdditionalItemFields, }, AdditionalItemContextMenuItems: ({ item, onChange, onMenuClose }) => { - // TODO fix typing: infer additional fields somehow - // @ts-expect-error missing additional field return ; }, AdditionalItemContent: ({ item }) => { - // TODO fix typing: infer additional fields somehow - // @ts-expect-error missing additional field return ; }, }); diff --git a/demo/admin/src/common/blocks/customBlockCategories.tsx b/demo/admin/src/common/blocks/customBlockCategories.tsx new file mode 100644 index 0000000000..8e323212af --- /dev/null +++ b/demo/admin/src/common/blocks/customBlockCategories.tsx @@ -0,0 +1,11 @@ +import { BlockCategory } from "@comet/blocks-admin"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +const customBlockCategory = { + id: "Custom", + label: , + insertBefore: BlockCategory.Media, +}; + +export { customBlockCategory }; diff --git a/demo/admin/src/config.ts b/demo/admin/src/config.ts index cd49ec3d6a..a2539a8278 100644 --- a/demo/admin/src/config.ts +++ b/demo/admin/src/config.ts @@ -1,5 +1,7 @@ -import cometConfig from "../comet-config.json"; -import environment from "./environment"; +import { SiteConfig } from "@comet/cms-admin"; + +import cometConfig from "./comet-config.json"; +import { environment } from "./environment"; export function createConfig() { const environmentVariables = {} as Record<(typeof environment)[number], string>; @@ -18,8 +20,10 @@ export function createConfig() { ...cometConfig, apiUrl: environmentVariables.API_URL, adminUrl: environmentVariables.ADMIN_URL, - sitesConfig: JSON.parse(environmentVariables.SITES_CONFIG), + sitesConfig: JSON.parse(environmentVariables.SITES_CONFIG) as SitesConfig, }; } +export type SitesConfig = Record; + export type Config = ReturnType; diff --git a/demo/admin/src/dam/ImportFromUnsplash.tsx b/demo/admin/src/dam/ImportFromUnsplash.tsx new file mode 100644 index 0000000000..3bb707b455 --- /dev/null +++ b/demo/admin/src/dam/ImportFromUnsplash.tsx @@ -0,0 +1,79 @@ +import { CancelButton, messages, SaveButton } from "@comet/admin"; +import { useCurrentDamFolder, useDamAcceptedMimeTypes, useDamFileUpload } from "@comet/cms-admin"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; +import { FormattedMessage } from "react-intl"; + +import { getRandomUnsplashImage, UnsplashImage } from "./getRandomUnsplashImage"; +import UnsplashIcon from "./UnsplashIcon"; + +export const ImportFromUnsplash: React.FC = () => { + const { allAcceptedMimeTypes } = useDamAcceptedMimeTypes(); + const { folderId } = useCurrentDamFolder(); + const [isOpen, setIsOpen] = React.useState(false); + const [unsplashImage, setUnsplashImage] = React.useState(); + + const { uploadFiles } = useDamFileUpload({ + acceptedMimetypes: allAcceptedMimeTypes, + }); + + const handleOpenDialog = async () => { + const image = await getRandomUnsplashImage(); + setUnsplashImage(image); + setIsOpen(true); + }; + + const handleCloseDialog = () => { + setIsOpen(false); + }; + + const handleSave = async () => { + if (unsplashImage === undefined) return; + await uploadFiles( + { acceptedFiles: [unsplashImage.file], fileRejections: [] }, + { + folderId, + importSource: { + importSourceId: unsplashImage.url, + importSourceType: "unsplash", + }, + }, + ); + handleCloseDialog(); + }; + + const handleShuffle = async () => { + const image = await getRandomUnsplashImage(); + setUnsplashImage(image); + }; + + return ( + <> + + +
+ Import from Unsplash + + + + + + + + + + +
+
+ + ); +}; + +const ImagePreview = styled("img")` + max-width: 100%; +`; diff --git a/demo/admin/src/dam/UnsplashIcon.tsx b/demo/admin/src/dam/UnsplashIcon.tsx new file mode 100644 index 0000000000..c0781c0bd9 --- /dev/null +++ b/demo/admin/src/dam/UnsplashIcon.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; +import * as React from "react"; + +export default function UnsplashIcon(props: SvgIconProps): JSX.Element { + return ( + + + + ); +} diff --git a/demo/admin/src/dam/getRandomUnsplashImage.ts b/demo/admin/src/dam/getRandomUnsplashImage.ts new file mode 100644 index 0000000000..d056e9c60e --- /dev/null +++ b/demo/admin/src/dam/getRandomUnsplashImage.ts @@ -0,0 +1,46 @@ +export interface UnsplashImage { + file: File; + url: string; +} + +async function fetchUnsplashImage(url: string) { + const response = await fetch(url); + + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + + return { + blob: await response.blob(), + origin: response.url, + }; +} + +function extractFileNameFromUrl(url: string): string { + const fileNameWithQuery = url.split("?")[0]; + const fileName = fileNameWithQuery.split("/").pop(); + return fileName ? `${fileName}.jpeg` : "unnamed.jpeg"; +} + +export async function getRandomUnsplashImage(): Promise { + const imageUrl = "https://source.unsplash.com/all/"; + + try { + const image = await fetchUnsplashImage(imageUrl); + const mimeType = image.blob.type; + + if (mimeType !== "image/jpeg") { + return getRandomUnsplashImage(); + } + + const fileName = extractFileNameFromUrl(image.origin); + const acceptedFile = new File([image.blob], fileName, { type: mimeType }); + + return { + file: acceptedFile, + url: image.origin, + }; + } catch (error) { + throw new Error(`Failed to fetch image: ${error}`); + } +} diff --git a/demo/admin/src/dashboard/Dashboard.tsx b/demo/admin/src/dashboard/Dashboard.tsx index 340171810f..378dcb9afa 100644 --- a/demo/admin/src/dashboard/Dashboard.tsx +++ b/demo/admin/src/dashboard/Dashboard.tsx @@ -1,5 +1,5 @@ import { MainContent, Stack } from "@comet/admin"; -import { DashboardHeader, LatestBuildsDashboardWidget } from "@comet/cms-admin"; +import { DashboardHeader, LatestBuildsDashboardWidget, useUserPermissionCheck } from "@comet/cms-admin"; import { Grid } from "@mui/material"; import { ContentScopeIndicator } from "@src/common/ContentScopeIndicator"; import * as React from "react"; @@ -11,7 +11,7 @@ import { LatestContentUpdates } from "./LatestContentUpdates"; const Dashboard: React.FC = () => { const intl = useIntl(); - + const isAllowed = useUserPermissionCheck(); return ( { - - {process.env.NODE_ENV !== "development" && } + {isAllowed("pageTree") && } + {import.meta.env.MODE !== "development" && } diff --git a/demo/admin/src/dashboard/LatestContentUpdates.tsx b/demo/admin/src/dashboard/LatestContentUpdates.tsx index 222e0faa6a..f68a08e495 100644 --- a/demo/admin/src/dashboard/LatestContentUpdates.tsx +++ b/demo/admin/src/dashboard/LatestContentUpdates.tsx @@ -2,7 +2,7 @@ import { gql, useQuery } from "@apollo/client"; import { LatestContentUpdatesDashboardWidget } from "@comet/cms-admin"; import { useContentScope } from "@src/common/ContentScopeProvider"; import { GQLLatestContentUpdatesQueryVariables } from "@src/dashboard/LatestContentUpdates.generated"; -import { categoryToUrlParam } from "@src/utils/pageTreeNodeCategoryMapping"; +import { categoryToUrlParam } from "@src/pageTree/pageTreeCategories"; import React from "react"; import { GQLLatestContentUpdatesQuery } from "./LatestContentUpdates.generated"; diff --git a/demo/admin/src/environment.ts b/demo/admin/src/environment.ts index 20ee12eca1..fa98f71839 100644 --- a/demo/admin/src/environment.ts +++ b/demo/admin/src/environment.ts @@ -1 +1 @@ -export default ["API_URL", "ADMIN_URL", "SITES_CONFIG", "COMET_DEMO_API_URL", "BUILD_DATE", "BUILD_NUMBER", "COMMIT_SHA"] as const; +export const environment = ["API_URL", "ADMIN_URL", "SITES_CONFIG", "COMET_DEMO_API_URL", "BUILD_DATE", "BUILD_NUMBER", "COMMIT_SHA"] as const; diff --git a/demo/admin/src/lang.ts b/demo/admin/src/lang.ts index a9864850b5..97e770ad3f 100644 --- a/demo/admin/src/lang.ts +++ b/demo/admin/src/lang.ts @@ -1,7 +1,7 @@ import { ResolvedIntlConfig } from "react-intl"; -import comet_demo_messages_de from "../lang-compiled/comet-demo-lang/de.json"; -import comet_demo_messages_en from "../lang-compiled/comet-demo-lang/en.json"; +import comet_demo_messages_de from "../lang-compiled/comet-demo-lang-admin/de.json"; +import comet_demo_messages_en from "../lang-compiled/comet-demo-lang-admin/en.json"; import comet_messages_de from "../lang-compiled/comet-lang/de.json"; import comet_messages_en from "../lang-compiled/comet-lang/en.json"; @@ -17,7 +17,7 @@ const cometDemoMessages = { export const getMessages = (): ResolvedIntlConfig["messages"] => { // in dev mode we use the default messages to have immediate changes - if (process.env.NODE_ENV === "development") { + if (import.meta.env.MODE === "development") { return {}; } diff --git a/demo/admin/src/links/Link.tsx b/demo/admin/src/links/Link.tsx index 0bf4dc3e6b..c998bc7dd5 100644 --- a/demo/admin/src/links/Link.tsx +++ b/demo/admin/src/links/Link.tsx @@ -1,17 +1,22 @@ import { messages } from "@comet/admin"; import { Link as LinkIcon } from "@comet/admin-icons"; -import { createDocumentRootBlocksMethods, DocumentInterface } from "@comet/cms-admin"; +import { createDocumentDependencyMethods, createDocumentRootBlocksMethods, DependencyInterface, DocumentInterface } from "@comet/cms-admin"; import { PageTreePage } from "@comet/cms-admin/lib/pages/pageTree/usePageTree"; import { Chip } from "@mui/material"; import { LinkBlock } from "@src/common/blocks/LinkBlock"; import { GQLPageTreeNodeAdditionalFieldsFragment } from "@src/common/EditPageNode"; import { GQLLink, GQLLinkInput } from "@src/graphql.generated"; import { EditLink } from "@src/links/EditLink"; +import { categoryToUrlParam } from "@src/pageTree/pageTreeCategories"; import gql from "graphql-tag"; import * as React from "react"; import { FormattedMessage } from "react-intl"; -export const Link: DocumentInterface, GQLLinkInput> = { +const rootBlocks = { + content: LinkBlock, +}; + +export const Link: DocumentInterface, GQLLinkInput> & DependencyInterface = { displayName: , editComponent: EditLink, getQuery: gql` @@ -50,7 +55,10 @@ export const Link: DocumentInterface, GQLLinkInput> = { return null; }, menuIcon: LinkIcon, - ...createDocumentRootBlocksMethods({ - content: LinkBlock, + ...createDocumentRootBlocksMethods(rootBlocks), + ...createDocumentDependencyMethods({ + rootQueryName: "link", + rootBlocks, + basePath: ({ pageTreeNode }) => `/pages/pagetree/${categoryToUrlParam(pageTreeNode.category)}/${pageTreeNode.id}/edit`, }), }; diff --git a/demo/admin/src/loader.ts b/demo/admin/src/loader.ts index 3b27b055c2..603557ab55 100644 --- a/demo/admin/src/loader.ts +++ b/demo/admin/src/loader.ts @@ -1,10 +1,10 @@ import App from "./App"; const loadHtml = () => { - const baseEl = document.querySelector("comet-demo-admin"); - if (!baseEl) return false; + const rootElement = document.querySelector("#root"); + if (!rootElement) return false; - App.render(baseEl); + App.render(rootElement); }; if (["interactive", "complete"].indexOf(document.readyState) !== -1) { diff --git a/demo/admin/src/news/blocks/NewsContentBlock.tsx b/demo/admin/src/news/blocks/NewsContentBlock.tsx index cf623e26c0..9c8867116d 100644 --- a/demo/admin/src/news/blocks/NewsContentBlock.tsx +++ b/demo/admin/src/news/blocks/NewsContentBlock.tsx @@ -7,7 +7,7 @@ import { TextImageBlock } from "@src/common/blocks/TextImageBlock"; export const NewsContentBlock = createBlocksBlock({ name: "NewsContentBlock", supportedBlocks: { - heading: HeadlineBlock, + headline: HeadlineBlock, richtext: RichTextBlock, image: DamImageBlock, textImage: TextImageBlock, diff --git a/demo/admin/src/news/blocks/NewsDetailBlock.tsx b/demo/admin/src/news/blocks/NewsDetailBlock.tsx new file mode 100644 index 0000000000..e2c8ad88eb --- /dev/null +++ b/demo/admin/src/news/blocks/NewsDetailBlock.tsx @@ -0,0 +1,28 @@ +import { Field, FinalFormInput } from "@comet/admin"; +import { BlockInterface, BlocksFinalForm, createBlockSkeleton } from "@comet/blocks-admin"; +import { NewsDetailBlockData, NewsDetailBlockInput } from "@src/blocks.generated"; +import * as React from "react"; + +type State = NewsDetailBlockData; + +const NewsDetailBlock: BlockInterface = { + ...createBlockSkeleton(), + + name: "NewsDetail", + + displayName: "News Detail", + + defaultValues: () => ({}), + + AdminComponent: ({ state, updateState }) => { + return ( + + + + ); + }, + + previewContent: (state) => (state.id !== undefined ? [{ type: "text", content: state.id }] : []), +}; + +export { NewsDetailBlock }; diff --git a/demo/admin/src/news/blocks/NewsLinkBlock.tsx b/demo/admin/src/news/blocks/NewsLinkBlock.tsx index 5ab7c2389f..a572fe0f99 100644 --- a/demo/admin/src/news/blocks/NewsLinkBlock.tsx +++ b/demo/admin/src/news/blocks/NewsLinkBlock.tsx @@ -1,4 +1,4 @@ -import { Field, FinalFormInput } from "@comet/admin"; +import { TextField } from "@comet/admin"; import { BlockInterface, BlocksFinalForm, createBlockSkeleton, LinkBlockInterface } from "@comet/blocks-admin"; import { NewsLinkBlockData, NewsLinkBlockInput } from "@src/blocks.generated"; import * as React from "react"; @@ -17,7 +17,7 @@ const NewsLinkBlock: BlockInterface { return ( - + ); }, diff --git a/demo/admin/src/news/dependencies/NewsDependency.tsx b/demo/admin/src/news/dependencies/NewsDependency.tsx new file mode 100644 index 0000000000..b55c3cffc8 --- /dev/null +++ b/demo/admin/src/news/dependencies/NewsDependency.tsx @@ -0,0 +1,13 @@ +import { createDependencyMethods, DamImageBlock, DependencyInterface } from "@comet/cms-admin"; +import { NewsContentBlock } from "@src/news/blocks/NewsContentBlock"; +import * as React from "react"; +import { FormattedMessage } from "react-intl"; + +export const NewsDependency: DependencyInterface = { + displayName: , + ...createDependencyMethods({ + rootQueryName: "news", + rootBlocks: { content: { block: NewsContentBlock, path: "/form" }, image: DamImageBlock }, + basePath: ({ id }) => `/structured-content/news/${id}/edit`, + }), +}; diff --git a/demo/admin/src/news/generated/NewsForm.gql.tsx b/demo/admin/src/news/generated/NewsForm.gql.ts similarity index 87% rename from demo/admin/src/news/generated/NewsForm.gql.tsx rename to demo/admin/src/news/generated/NewsForm.gql.ts index 61fda9a4d9..1bd9172e08 100644 --- a/demo/admin/src/news/generated/NewsForm.gql.tsx +++ b/demo/admin/src/news/generated/NewsForm.gql.ts @@ -7,6 +7,7 @@ export const newsFormFragment = gql` fragment NewsForm on News { slug title + status date category image @@ -45,8 +46,8 @@ export const createNewsMutation = gql` `; export const updateNewsMutation = gql` - mutation UpdateNews($id: ID!, $input: NewsUpdateInput!, $lastUpdatedAt: DateTime) { - updateNews(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + mutation UpdateNews($id: ID!, $input: NewsUpdateInput!) { + updateNews(id: $id, input: $input) { id updatedAt ...NewsForm diff --git a/demo/admin/src/news/generated/NewsForm.tsx b/demo/admin/src/news/generated/NewsForm.tsx index 6279b4ddf9..4be3ea7844 100644 --- a/demo/admin/src/news/generated/NewsForm.tsx +++ b/demo/admin/src/news/generated/NewsForm.tsx @@ -5,12 +5,12 @@ import { useApolloClient, useQuery } from "@apollo/client"; import { Field, FinalForm, - FinalFormInput, FinalFormSaveSplitButton, FinalFormSelect, FinalFormSubmitEvent, Loading, MainContent, + TextField, Toolbar, ToolbarActions, ToolbarFillSpace, @@ -20,7 +20,7 @@ import { useStackApi, useStackSwitchApi, } from "@comet/admin"; -import { FinalFormDatePicker } from "@comet/admin-date-time"; +import { DateField } from "@comet/admin-date-time"; import { ArrowLeft } from "@comet/admin-icons"; import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; @@ -116,15 +116,15 @@ export function NewsForm({ id }: FormProps): React.ReactElement { } await client.mutate({ mutation: updateNewsMutation, - variables: { id, input: output, lastUpdatedAt: data?.news?.updatedAt }, + variables: { id, input: output }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: createNewsMutation, variables: { scope, input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createNews.id; + const id = mutationResponse?.createNews.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage("edit", id); @@ -141,15 +141,7 @@ export function NewsForm({ id }: FormProps): React.ReactElement { } return ( - - apiRef={formApiRef} - onSubmit={handleSubmit} - mode={mode} - initialValues={initialValues} - onAfterSubmit={(values, form) => { - //don't go back automatically - }} - > + apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}> {({ values }) => ( {saveConflict.dialogs} @@ -164,31 +156,25 @@ export function NewsForm({ id }: FormProps): React.ReactElement { - + - } - /> - } - /> - } - /> + } /> + } /> + }> + {(props) => ( + + + + + + + + + )} + + } /> }> {(props) => ( diff --git a/demo/admin/src/news/generated/NewsGrid.tsx b/demo/admin/src/news/generated/NewsGrid.tsx index aa031eef19..8865e8e42d 100644 --- a/demo/admin/src/news/generated/NewsGrid.tsx +++ b/demo/admin/src/news/generated/NewsGrid.tsx @@ -1,6 +1,6 @@ // This file has been generated by comet admin-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. -import { useApolloClient, useQuery } from "@apollo/client"; +import { gql, useApolloClient, useQuery } from "@apollo/client"; import { CrudContextMenu, GridFilterButton, @@ -23,7 +23,6 @@ import { DamImageBlock } from "@comet/cms-admin"; import { Button, IconButton } from "@mui/material"; import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; import { useContentScope } from "@src/common/ContentScopeProvider"; -import gql from "graphql-tag"; import * as React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -41,15 +40,15 @@ import { const newsFragment = gql` fragment NewsList on News { id - updatedAt slug title + status date category - visible image content createdAt + updatedAt } `; @@ -106,15 +105,18 @@ export function NewsGrid(): React.ReactElement { const { scope } = useContentScope(); const columns: GridColDef[] = [ + { field: "slug", headerName: intl.formatMessage({ id: "news.slug", defaultMessage: "Slug" }), width: 150 }, + { field: "title", headerName: intl.formatMessage({ id: "news.title", defaultMessage: "Title" }), width: 150 }, { - field: "updatedAt", - headerName: intl.formatMessage({ id: "news.updatedAt", defaultMessage: "Updated At" }), - type: "dateTime", - valueGetter: ({ value }) => value && new Date(value), + field: "status", + headerName: intl.formatMessage({ id: "news.status", defaultMessage: "Status" }), + type: "singleSelect", + valueOptions: [ + { value: "Active", label: intl.formatMessage({ id: "news.status.active", defaultMessage: "Active" }) }, + { value: "Deleted", label: intl.formatMessage({ id: "news.status.deleted", defaultMessage: "Deleted" }) }, + ], width: 150, }, - { field: "slug", headerName: intl.formatMessage({ id: "news.slug", defaultMessage: "Slug" }), width: 150 }, - { field: "title", headerName: intl.formatMessage({ id: "news.title", defaultMessage: "Title" }), width: 150 }, { field: "date", headerName: intl.formatMessage({ id: "news.date", defaultMessage: "Date" }), @@ -133,7 +135,6 @@ export function NewsGrid(): React.ReactElement { ], width: 150, }, - { field: "visible", headerName: intl.formatMessage({ id: "news.visible", defaultMessage: "Visible" }), type: "boolean", width: 150 }, { field: "image", headerName: intl.formatMessage({ id: "news.image", defaultMessage: "Image" }), @@ -161,6 +162,13 @@ export function NewsGrid(): React.ReactElement { valueGetter: ({ value }) => value && new Date(value), width: 150, }, + { + field: "updatedAt", + headerName: intl.formatMessage({ id: "news.updatedAt", defaultMessage: "Updated At" }), + type: "dateTime", + valueGetter: ({ value }) => value && new Date(value), + width: 150, + }, { field: "actions", headerName: "", @@ -179,6 +187,7 @@ export function NewsGrid(): React.ReactElement { return { slug: row.slug, title: row.title, + status: row.status, date: row.date, category: row.category, image: DamImageBlock.state2Output(DamImageBlock.input2State(row.image)), diff --git a/demo/admin/src/pageTree/pageTreeCategories.tsx b/demo/admin/src/pageTree/pageTreeCategories.tsx new file mode 100644 index 0000000000..7b8942fe7c --- /dev/null +++ b/demo/admin/src/pageTree/pageTreeCategories.tsx @@ -0,0 +1,29 @@ +import { AllCategories } from "@comet/cms-admin"; +import { GQLPageTreeNodeCategory } from "@src/graphql.generated"; +import { kebabCase, pascalCase } from "change-case"; +import * as React from "react"; +import { FormattedMessage } from "react-intl"; + +export const pageTreeCategories: AllCategories = [ + { + category: "MainNavigation", + label: , + }, + { + category: "TopMenu", + label: , + }, +]; + +const isCategory = (category: string): category is GQLPageTreeNodeCategory => { + return pageTreeCategories.some((c) => c.category === category); +}; + +export function categoryToUrlParam(category: GQLPageTreeNodeCategory | string): string { + return kebabCase(category); +} + +export function urlParamToCategory(param: string): GQLPageTreeNodeCategory | undefined { + const category = pascalCase(param); + return isCategory(category) ? category : undefined; +} diff --git a/demo/admin/src/pages/EditPage.tsx b/demo/admin/src/pages/EditPage.tsx index 3da0fe1e49..4dd6f968f4 100644 --- a/demo/admin/src/pages/EditPage.tsx +++ b/demo/admin/src/pages/EditPage.tsx @@ -5,6 +5,7 @@ import { AdminComponentRoot, AdminTabLabel } from "@comet/blocks-admin"; import { BlockPreviewWithTabs, createUsePage, + DependencyList, EditPageLayout, openSitePreviewWindow, PageName, @@ -23,6 +24,25 @@ import { useRouteMatch } from "react-router"; import { GQLEditPageQuery, GQLEditPageQueryVariables, GQLUpdatePageMutation, GQLUpdatePageMutationVariables } from "./EditPage.generated"; import { PageContentBlock } from "./PageContentBlock"; +const pageDependenciesQuery = gql` + query PageDependencies($id: ID!, $offset: Int!, $limit: Int!, $forceRefresh: Boolean = false) { + item: page(id: $id) { + id + dependencies(offset: $offset, limit: $limit, forceRefresh: $forceRefresh) { + nodes { + targetGraphqlObjectType + targetId + rootColumnName + jsonPath + name + secondaryInformation + } + totalCount + } + } + } +`; + interface Props { id: string; category: GQLPageTreeNodeCategory; @@ -139,7 +159,7 @@ export const EditPage: React.FC = ({ id, category }) => { - + {[ { key: "content", @@ -161,6 +181,22 @@ export const EditPage: React.FC = ({ id, category }) => { ), content: rootBlocksApi.seo.adminUI, }, + { + key: "dependencies", + label: ( + + + + ), + content: ( + + ), + }, ]} diff --git a/demo/admin/src/pages/Page.tsx b/demo/admin/src/pages/Page.tsx index a19f58e522..ce32f1e948 100644 --- a/demo/admin/src/pages/Page.tsx +++ b/demo/admin/src/pages/Page.tsx @@ -1,11 +1,12 @@ import { messages } from "@comet/admin"; import { File, FileNotMenu } from "@comet/admin-icons"; -import { createDocumentRootBlocksMethods, DocumentInterface } from "@comet/cms-admin"; +import { createDocumentDependencyMethods, createDocumentRootBlocksMethods, DependencyInterface, DocumentInterface } from "@comet/cms-admin"; import { PageTreePage } from "@comet/cms-admin/lib/pages/pageTree/usePageTree"; import { Chip } from "@mui/material"; import { SeoBlock } from "@src/common/blocks/SeoBlock"; import { GQLPageTreeNodeAdditionalFieldsFragment } from "@src/common/EditPageNode"; import { GQLPage, GQLPageInput } from "@src/graphql.generated"; +import { categoryToUrlParam } from "@src/pageTree/pageTreeCategories"; import gql from "graphql-tag"; import * as React from "react"; import { FormattedMessage } from "react-intl"; @@ -13,7 +14,12 @@ import { FormattedMessage } from "react-intl"; import { EditPage } from "./EditPage"; import { PageContentBlock } from "./PageContentBlock"; -export const Page: DocumentInterface, GQLPageInput> = { +const rootBlocks = { + content: PageContentBlock, + seo: SeoBlock, +}; + +export const Page: DocumentInterface, GQLPageInput> & DependencyInterface = { displayName: , editComponent: EditPage, getQuery: gql` @@ -54,8 +60,13 @@ export const Page: DocumentInterface, GQLPageIn }, menuIcon: File, hideInMenuIcon: FileNotMenu, - ...createDocumentRootBlocksMethods({ - content: PageContentBlock, - seo: SeoBlock, + ...createDocumentRootBlocksMethods(rootBlocks), + ...createDocumentDependencyMethods({ + rootQueryName: "page", + rootBlocks: { + content: PageContentBlock, + seo: { block: SeoBlock, path: "/config" }, + }, + basePath: ({ pageTreeNode }) => `/pages/pagetree/${categoryToUrlParam(pageTreeNode.category)}/${pageTreeNode.id}/edit`, }), }; diff --git a/demo/admin/src/pages/PageContentBlock.tsx b/demo/admin/src/pages/PageContentBlock.tsx index f8a1714d6b..f915f374c3 100644 --- a/demo/admin/src/pages/PageContentBlock.tsx +++ b/demo/admin/src/pages/PageContentBlock.tsx @@ -4,6 +4,7 @@ import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock"; import { LinkListBlock } from "@src/common/blocks/LinkListBlock"; import { RichTextBlock } from "@src/common/blocks/RichTextBlock"; import { TextImageBlock } from "@src/common/blocks/TextImageBlock"; +import { NewsDetailBlock } from "@src/news/blocks/NewsDetailBlock"; import { userGroupAdditionalItemFields } from "@src/userGroups/userGroupAdditionalItemFields"; import { UserGroupChip } from "@src/userGroups/UserGroupChip"; import { UserGroupContextMenuItem } from "@src/userGroups/UserGroupContextMenuItem"; @@ -12,6 +13,7 @@ import * as React from "react"; import { ColumnsBlock } from "./blocks/ColumnsBlock"; import { FullWidthImageBlock } from "./blocks/FullWidthImageBlock"; import { MediaBlock } from "./blocks/MediaBlock"; +import { TeaserBlock } from "./blocks/TeaserBlock"; import { TwoListsBlock } from "./blocks/TwoListsBlock"; import { VideoBlock } from "./blocks/VideoBlock"; @@ -32,18 +34,16 @@ export const PageContentBlock = createBlocksBlock({ anchor: AnchorBlock, twoLists: TwoListsBlock, media: MediaBlock, + teaser: TeaserBlock, + newsDetail: NewsDetailBlock, }, additionalItemFields: { ...userGroupAdditionalItemFields, }, AdditionalItemContextMenuItems: ({ item, onChange, onMenuClose }) => { - // TODO fix typing: infer additional fields somehow - // @ts-expect-error missing additional field return ; }, AdditionalItemContent: ({ item }) => { - // TODO fix typing: infer additional fields somehow - // @ts-expect-error missing additional field return ; }, }); diff --git a/demo/admin/src/pages/blocks/ColumnsBlock.tsx b/demo/admin/src/pages/blocks/ColumnsBlock.tsx index b26d82c619..718655fe50 100644 --- a/demo/admin/src/pages/blocks/ColumnsBlock.tsx +++ b/demo/admin/src/pages/blocks/ColumnsBlock.tsx @@ -49,6 +49,20 @@ const ColumnsBlock = createColumnsBlock({ ), }, + { + name: "two-columns-12-6", + label: "Two columns 12-6", + columns: 2, + preview: ( + + + + + + + + ), + }, ], contentBlock: ColumnsContentBlock, }); diff --git a/demo/admin/src/pages/blocks/FullWidthImageBlock.tsx b/demo/admin/src/pages/blocks/FullWidthImageBlock.tsx index a9a9e10198..f038cc485e 100644 --- a/demo/admin/src/pages/blocks/FullWidthImageBlock.tsx +++ b/demo/admin/src/pages/blocks/FullWidthImageBlock.tsx @@ -1,6 +1,7 @@ import { messages } from "@comet/admin"; -import { BlockCategory, createCompositeBlock, createOptionalBlock } from "@comet/blocks-admin"; +import { createCompositeBlock, createOptionalBlock } from "@comet/blocks-admin"; import { DamImageBlock } from "@comet/cms-admin"; +import { customBlockCategory } from "@src/common/blocks/customBlockCategories"; import { RichTextBlock } from "@src/common/blocks/RichTextBlock"; import * as React from "react"; import { FormattedMessage } from "react-intl"; @@ -9,19 +10,24 @@ const FullWidthImageContentBlock = createOptionalBlock(RichTextBlock, { title: , }); -export const FullWidthImageBlock = createCompositeBlock({ - name: "FullWidthImage", - displayName: , - category: BlockCategory.Media, - blocks: { - image: { - block: DamImageBlock, - title: , - paper: true, - }, - content: { - block: FullWidthImageContentBlock, - title: , +export const FullWidthImageBlock = createCompositeBlock( + { + name: "FullWidthImage", + displayName: , + blocks: { + image: { + block: DamImageBlock, + title: , + paper: true, + }, + content: { + block: FullWidthImageContentBlock, + title: , + }, }, }, -}); + (block) => { + block.category = customBlockCategory; + return block; + }, +); diff --git a/demo/admin/src/pages/blocks/TeaserBlock.tsx b/demo/admin/src/pages/blocks/TeaserBlock.tsx new file mode 100644 index 0000000000..cf57887c38 --- /dev/null +++ b/demo/admin/src/pages/blocks/TeaserBlock.tsx @@ -0,0 +1,43 @@ +import { BlockCategory, createCompositeBlock } from "@comet/blocks-admin"; +import { DamImageBlock } from "@comet/cms-admin"; +import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock"; +import { LinkListBlock } from "@src/common/blocks/LinkListBlock"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +const TeaserBlock = createCompositeBlock( + { + name: "Teaser", + displayName: , + blocks: { + // Normal + headline: { + block: HeadlineBlock, + title: , + }, + // Nested + image: { + block: DamImageBlock, + title: , + nested: true, + }, + // Subroutes + links: { + block: LinkListBlock, + title: , + }, + // Nested inner subroutes + buttons: { + block: LinkListBlock, + title: , + nested: true, + }, + }, + }, + (block) => { + block.category = BlockCategory.Teaser; + return block; + }, +); + +export { TeaserBlock }; diff --git a/demo/admin/src/pages/blocks/TwoListsBlock.tsx b/demo/admin/src/pages/blocks/TwoListsBlock.tsx index 71d6aed6b0..1d42d9b793 100644 --- a/demo/admin/src/pages/blocks/TwoListsBlock.tsx +++ b/demo/admin/src/pages/blocks/TwoListsBlock.tsx @@ -1,4 +1,5 @@ import { createCompositeBlock, createListBlock } from "@comet/blocks-admin"; +import { customBlockCategory } from "@src/common/blocks/customBlockCategories"; import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock"; const TwoListsListBlock = createListBlock({ @@ -6,17 +7,23 @@ const TwoListsListBlock = createListBlock({ block: HeadlineBlock, }); -export const TwoListsBlock = createCompositeBlock({ - name: "TwoLists", - displayName: "Two Lists", - blocks: { - list1: { - block: TwoListsListBlock, - title: "List 1", - }, - list2: { - block: TwoListsListBlock, - title: "List 2", +export const TwoListsBlock = createCompositeBlock( + { + name: "TwoLists", + displayName: "Two Lists", + blocks: { + list1: { + block: TwoListsListBlock, + title: "List 1", + }, + list2: { + block: TwoListsListBlock, + title: "List 2", + }, }, }, -}); + (block) => { + block.category = customBlockCategory; + return block; + }, +); diff --git a/demo/admin/src/pages/mainMenu/components/EditMainMenuItem.tsx b/demo/admin/src/pages/mainMenu/components/EditMainMenuItem.tsx index c38f9996d1..3e86994d8e 100644 --- a/demo/admin/src/pages/mainMenu/components/EditMainMenuItem.tsx +++ b/demo/admin/src/pages/mainMenu/components/EditMainMenuItem.tsx @@ -24,6 +24,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useRouteMatch } from "react-router-dom"; import { GQLEditMainMenuItemFragment, GQLUpdateMainMenuItemMutation, GQLUpdateMainMenuItemMutationVariables } from "./EditMainMenuItem.generated"; + export type { GQLEditMainMenuItemFragment } from "./EditMainMenuItem.generated"; // re-export export const editMainMenuItemFragment = gql` @@ -174,7 +175,7 @@ const EditMainMenuItem: React.FunctionComponent = ({ item )} - +
{content ? ( diff --git a/demo/admin/src/pages/mainMenu/components/MainMenuItems.tsx b/demo/admin/src/pages/mainMenu/components/MainMenuItems.tsx index 5db5890a24..88dc8d1f3e 100644 --- a/demo/admin/src/pages/mainMenu/components/MainMenuItems.tsx +++ b/demo/admin/src/pages/mainMenu/components/MainMenuItems.tsx @@ -1,5 +1,5 @@ import { gql } from "@apollo/client"; -import { MainContent, Table, TableQuery, Toolbar, ToolbarAutomaticTitleItem, useStackSwitchApi, useTableQuery } from "@comet/admin"; +import { MainContent, StackLink, Table, TableQuery, Toolbar, ToolbarAutomaticTitleItem, useTableQuery } from "@comet/admin"; import { Edit } from "@comet/admin-icons"; import { IconButton } from "@mui/material"; import { ContentScopeIndicator } from "@src/common/ContentScopeIndicator"; @@ -25,7 +25,6 @@ const mainMenuQuery = gql` `; const MainMenuItems: React.FunctionComponent = () => { - const stackApi = useStackSwitchApi(); const { scope } = useContentScope(); const { tableData, api, loading, error } = useTableQuery()(mainMenuQuery, { @@ -64,7 +63,7 @@ const MainMenuItems: React.FunctionComponent = () => { header: "", cellProps: { align: "right" }, render: (item) => ( - stackApi.activatePage("edit", item.node.id)}> + ), diff --git a/demo/admin/src/pre-loader.ts b/demo/admin/src/pre-loader.ts deleted file mode 100644 index 5dc2a3b5d6..0000000000 --- a/demo/admin/src/pre-loader.ts +++ /dev/null @@ -1,9 +0,0 @@ -const scripts = document.getElementsByTagName("script"); -[].every.call(scripts, (script: HTMLScriptElement) => { - const m = script.src.match(/^(.*)?\/build\/comet-demo-admin/); - if (m) { - __webpack_public_path__ = m[1] + __webpack_public_path__; - return false; - } - return true; -}); diff --git a/demo/admin/src/predefinedPage/EditPredefinedPage.tsx b/demo/admin/src/predefinedPage/EditPredefinedPage.tsx index ac2040a16f..479dcd5175 100644 --- a/demo/admin/src/predefinedPage/EditPredefinedPage.tsx +++ b/demo/admin/src/predefinedPage/EditPredefinedPage.tsx @@ -1,12 +1,11 @@ import { gql, useMutation, useQuery } from "@apollo/client"; import { - Field, FinalForm, - FinalFormSelect, Loading, MainContent, messages, SaveButton, + SelectField, SplitButton, Toolbar, ToolbarFillSpace, @@ -118,17 +117,13 @@ export const EditPredefinedPage: React.FC = ({ id }) => { - } name="type" fullWidth> - {(props) => ( - - {predefinedPageOptions.map((item, index) => ( - - {item.name} - - ))} - - )} - + } name="type" fullWidth> + {predefinedPageOptions.map((item, index) => ( + + {item.name} + + ))} + ); diff --git a/demo/admin/src/products/ManufacturerForm.gql.tsx b/demo/admin/src/products/ManufacturerForm.gql.tsx new file mode 100644 index 0000000000..a3625d4d5e --- /dev/null +++ b/demo/admin/src/products/ManufacturerForm.gql.tsx @@ -0,0 +1,60 @@ +import { gql } from "@apollo/client"; + +export const manufacturerFormFragment = gql` + fragment ManufacturerFormDetails on Manufacturer { + address { + street + streetNumber + zip + country + alternativeAddress { + street + streetNumber + zip + country + } + } + addressAsEmbeddable { + street + streetNumber + zip + country + alternativeAddress { + street + streetNumber + zip + country + } + } + } +`; +export const manufacturerQuery = gql` + query Manufacturer($id: ID!) { + manufacturer(id: $id) { + id + updatedAt + ...ManufacturerFormDetails + } + } + ${manufacturerFormFragment} +`; +export const createManufacturerMutation = gql` + mutation CreateManufacturer($input: ManufacturerInput!) { + createManufacturer(input: $input) { + id + updatedAt + ...ManufacturerFormDetails + } + } + ${manufacturerFormFragment} +`; +export const updateManufacturerMutation = gql` + mutation UpdateManufacturer($id: ID!, $input: ManufacturerUpdateInput!) { + updateManufacturer(id: $id, input: $input) { + id + updatedAt + ...ManufacturerFormDetails + } + } + ${manufacturerFormFragment} +`; diff --git a/demo/admin/src/products/ManufacturerForm.tsx b/demo/admin/src/products/ManufacturerForm.tsx new file mode 100644 index 0000000000..b8907fb160 --- /dev/null +++ b/demo/admin/src/products/ManufacturerForm.tsx @@ -0,0 +1,379 @@ +import { useApolloClient, useQuery } from "@apollo/client"; +import { + Field, + FieldSet, + FinalForm, + FinalFormInput, + FinalFormSaveSplitButton, + FinalFormSubmitEvent, + Loading, + MainContent, + TextField, + Toolbar, + ToolbarActions, + ToolbarFillSpace, + ToolbarItem, + ToolbarTitleItem, + useFormApiRef, + useStackApi, + useStackSwitchApi, +} from "@comet/admin"; +import { ArrowLeft } from "@comet/admin-icons"; +import { EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { IconButton } from "@mui/material"; +import { FormApi } from "final-form"; +import { filter } from "graphql-anywhere"; +import isEqual from "lodash.isequal"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { createManufacturerMutation, manufacturerFormFragment, manufacturerQuery, updateManufacturerMutation } from "./ManufacturerForm.gql"; +import { + GQLCreateManufacturerMutation, + GQLCreateManufacturerMutationVariables, + GQLManufacturerFormDetailsFragment, + GQLManufacturerQuery, + GQLManufacturerQueryVariables, + GQLUpdateManufacturerMutation, + GQLUpdateManufacturerMutationVariables, +} from "./ManufacturerForm.gql.generated"; + +type FormValues = Omit & { + address: + | (Omit, "streetNumber" | "zip" | "alternativeAddress"> & { + streetNumber: string | null; + zip: string; + alternativeAddress: + | (Omit["alternativeAddress"]>, "streetNumber" | "zip"> & { + streetNumber: string | null; + zip: string; + }) + | null; + }) + | null; + addressAsEmbeddable: + | Omit, "streetNumber" | "zip" | "alternativeAddress"> & { + streetNumber: string | null; + zip: string; + alternativeAddress: + | Omit< + NonNullable["alternativeAddress"]>, + "streetNumber" | "zip" + > & { + streetNumber: string | null; + zip: string; + }; + }; +}; + +interface FormProps { + id?: string; +} + +export function ManufacturerForm({ id }: FormProps): React.ReactElement { + const stackApi = useStackApi(); + const client = useApolloClient(); + const mode = id ? "edit" : "add"; + const formApiRef = useFormApiRef(); + const stackSwitchApi = useStackSwitchApi(); + + const { data, error, loading, refetch } = useQuery( + manufacturerQuery, + id ? { variables: { id } } : { skip: true }, + ); + + const initialValues = React.useMemo>(() => { + const filteredData = data ? filter(manufacturerFormFragment, data.manufacturer) : undefined; + if (!filteredData) return {}; + return { + ...filteredData, + address: filteredData.address + ? { + ...filteredData.address, + streetNumber: filteredData.address.streetNumber ? String(filteredData.address.streetNumber) : null, + zip: String(filteredData.address.zip), + alternativeAddress: filteredData.address.alternativeAddress + ? { + ...filteredData.address.alternativeAddress, + streetNumber: filteredData.address.alternativeAddress.streetNumber + ? String(filteredData.address.alternativeAddress.streetNumber) + : null, + zip: String(filteredData.address.alternativeAddress.zip), + } + : undefined, + } + : undefined, + addressAsEmbeddable: { + ...filteredData.addressAsEmbeddable, + streetNumber: filteredData.addressAsEmbeddable.streetNumber ? String(filteredData.addressAsEmbeddable.streetNumber) : null, + zip: String(filteredData.addressAsEmbeddable.zip), + alternativeAddress: { + ...filteredData.addressAsEmbeddable.alternativeAddress, + streetNumber: filteredData.addressAsEmbeddable.alternativeAddress.streetNumber + ? String(filteredData.addressAsEmbeddable.alternativeAddress.streetNumber) + : null, + zip: String(filteredData.addressAsEmbeddable.alternativeAddress.zip), + }, + }, + }; + }, [data]); + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "manufacturer", id); + return resolveHasSaveConflict(data?.manufacturer.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); + const output = { + ...formValues, + address: formValues.address + ? { + ...formValues.address, + streetNumber: formValues.address?.streetNumber ? parseInt(formValues.address.streetNumber) : null, + zip: parseInt(formValues.address.zip), + alternativeAddress: formValues.address?.alternativeAddress + ? { + ...formValues.address.alternativeAddress, + streetNumber: formValues.address?.alternativeAddress.streetNumber + ? parseInt(formValues.address.alternativeAddress.streetNumber) + : null, + zip: parseInt(formValues.address.alternativeAddress.zip), + } + : undefined, + } + : undefined, + addressAsEmbeddable: { + ...formValues.addressAsEmbeddable, + streetNumber: formValues.addressAsEmbeddable?.streetNumber ? parseInt(formValues.addressAsEmbeddable.streetNumber) : null, + zip: parseInt(formValues.addressAsEmbeddable.zip), + alternativeAddress: { + ...formValues.addressAsEmbeddable.alternativeAddress, + streetNumber: formValues.addressAsEmbeddable?.alternativeAddress.streetNumber + ? parseInt(formValues.addressAsEmbeddable.alternativeAddress.streetNumber) + : null, + zip: parseInt(formValues.addressAsEmbeddable.alternativeAddress.zip), + }, + }, + }; + if (mode === "edit") { + if (!id) throw new Error(); + await client.mutate({ + mutation: updateManufacturerMutation, + variables: { id, input: output }, + }); + } else { + const { data: mutationResponse } = await client.mutate({ + mutation: createManufacturerMutation, + variables: { input: output }, + }); + if (!event.navigatingBack) { + const id = mutationResponse?.createManufacturer.id; + if (id) { + setTimeout(() => { + stackSwitchApi.activatePage(`edit`, id); + }); + } + } + } + }; + + if (error) throw error; + + if (loading) { + return ; + } + + return ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode={mode} + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + subscription={{}} + > + {() => ( + + {saveConflict.dialogs} + + + + + + + + + {({ input }) => + input.value ? ( + input.value + ) : ( + + ) + } + + + + + + + + +
} + supportText={} + collapsible={true} + initiallyExpanded={true} + > + } + /> + } + /> + } + /> + } + /> +
} + supportText={ + + } + > + + } + /> + + } + /> + } + /> + + } + /> +
+
+
} + > + } + /> + } + /> + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> +
+
+
+ )} + + ); +} diff --git a/demo/admin/src/products/ManufacturersGrid.tsx b/demo/admin/src/products/ManufacturersGrid.tsx new file mode 100644 index 0000000000..03b6536702 --- /dev/null +++ b/demo/admin/src/products/ManufacturersGrid.tsx @@ -0,0 +1,247 @@ +import { useApolloClient, useQuery } from "@apollo/client"; +import { + CrudContextMenu, + GridFilterButton, + MainContent, + muiGridFilterToGql, + muiGridSortToGql, + StackLink, + Toolbar, + ToolbarAutomaticTitleItem, + ToolbarFillSpace, + ToolbarItem, + Tooltip, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { Add as AddIcon, Edit, Info } from "@comet/admin-icons"; +import { Button, IconButton, Typography } from "@mui/material"; +import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { + GQLCreateManufacturerMutation, + GQLCreateManufacturerMutationVariables, + GQLDeleteManufacturerMutation, + GQLDeleteManufacturerMutationVariables, + GQLManufacturersListQuery, + GQLManufacturersListQueryVariables, +} from "@src/products/ManufacturersGrid.generated"; +import { filter } from "graphql-anywhere"; +import gql from "graphql-tag"; +import * as React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +function ManufacturersGridToolbar() { + return ( + + + + + + + + + + + + + + ); +} + +type GridValues = GQLManufacturersListQuery["manufacturers"]["nodes"][0]; + +export function ManufacturersGrid() { + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ManufacturersGrid") }; + const sortModel = dataGridProps.sortModel; + const client = useApolloClient(); + const intl = useIntl(); + + const columns: GridColDef[] = [ + { + field: "id", + width: 150, + renderHeader: () => ( +
+ + {intl.formatMessage({ id: "manufacturers.id", defaultMessage: "ID" })} + + } + > + + + + +
+ ), + }, + { + field: "address.street", + headerName: intl.formatMessage({ id: "manufacturers.street", defaultMessage: "Street" }), + valueGetter: ({ row }) => `${row.address?.street} ${row.address?.streetNumber}`, + }, + { + field: "address.zip", + headerName: intl.formatMessage({ id: "manufacturers.zip", defaultMessage: "ZIP" }), + valueGetter: ({ row }) => row.address?.zip, + }, + { + field: "address.alternativeAddress.street", + headerName: intl.formatMessage({ id: "manufacturers.alternativeAddressStreet", defaultMessage: "alt. Street" }), + valueGetter: ({ row }) => `${row.address?.alternativeAddress?.street} ${row.address?.alternativeAddress?.streetNumber}`, + }, + { + field: "address.alternativeAddress.zip", + headerName: intl.formatMessage({ id: "manufacturers.alternativeAddressZip", defaultMessage: "alt. ZIP" }), + valueGetter: ({ row }) => row.address?.alternativeAddress?.zip, + }, + { + field: "addressAsEmbeddable.street", + headerName: intl.formatMessage({ id: "manufacturers.street2", defaultMessage: "Street2" }), + valueGetter: ({ row }) => `${row.addressAsEmbeddable?.street} ${row.addressAsEmbeddable?.streetNumber}`, + }, + { + field: "addressAsEmbeddable.zip", + headerName: intl.formatMessage({ id: "manufacturers.zip2", defaultMessage: "ZIP2" }), + valueGetter: ({ row }) => row.addressAsEmbeddable?.zip, + }, + { + field: "addressAsEmbeddable.alternativeAddress.street", + headerName: intl.formatMessage({ id: "manufacturers.alternativeAddressStreet2", defaultMessage: "alt. Street2" }), + valueGetter: ({ row }) => + `${row.addressAsEmbeddable?.alternativeAddress?.street} ${row.addressAsEmbeddable?.alternativeAddress?.streetNumber}`, + }, + { + field: "addressAsEmbeddable.alternativeAddress.zip", + headerName: intl.formatMessage({ id: "manufacturers.alternativeAddressZip", defaultMessage: "alt. ZIP2" }), + valueGetter: ({ row }) => row.addressAsEmbeddable?.alternativeAddress?.zip, + }, + { + field: "action", + headerName: "", + sortable: false, + filterable: false, + renderCell: (params) => { + return ( + <> + + + + { + await client.mutate({ + mutation: createManufacturerMutation, + variables: { + input: { + address: input.address, + addressAsEmbeddable: input.addressAsEmbeddable, + }, + }, + }); + }} + onDelete={async () => { + await client.mutate({ + mutation: deleteManufacturerMutation, + variables: { id: params.row.id }, + }); + }} + refetchQueries={["ManufacturersList"]} + copyData={() => { + return filter(manufacturersFragment, params.row); + }} + /> + + ); + }, + }, + ]; + + const { data, loading, error } = useQuery(manufacturersQuery, { + variables: { + ...muiGridFilterToGql(columns, dataGridProps.filterModel), + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(sortModel), + }, + }); + if (error) throw error; + + const rows = data?.manufacturers.nodes ?? []; + const rowCount = useBufferedRowCount(data?.manufacturers.totalCount); + + return ( + + + + ); +} + +const manufacturersFragment = gql` + fragment ManufacturersListManual on Manufacturer { + address { + street + streetNumber + zip + country + alternativeAddress { + street + streetNumber + zip + country + } + } + addressAsEmbeddable { + street + streetNumber + zip + country + alternativeAddress { + street + streetNumber + zip + country + } + } + } +`; + +const manufacturersQuery = gql` + query ManufacturersList($offset: Int!, $limit: Int!, $sort: [ManufacturerSort!], $filter: ManufacturerFilter, $search: String) { + manufacturers(offset: $offset, limit: $limit, sort: $sort, filter: $filter, search: $search) { + nodes { + id + ...ManufacturersListManual + } + totalCount + } + } + ${manufacturersFragment} +`; + +const deleteManufacturerMutation = gql` + mutation DeleteManufacturer($id: ID!) { + deleteManufacturer(id: $id) + } +`; + +const createManufacturerMutation = gql` + mutation CreateManufacturer($input: ManufacturerInput!) { + createManufacturer(input: $input) { + id + } + } +`; diff --git a/demo/admin/src/products/ManufacturersPage.tsx b/demo/admin/src/products/ManufacturersPage.tsx new file mode 100644 index 0000000000..ede2c581fd --- /dev/null +++ b/demo/admin/src/products/ManufacturersPage.tsx @@ -0,0 +1,24 @@ +import { Stack, StackPage, StackSwitch } from "@comet/admin"; +import { ManufacturerForm } from "@src/products/ManufacturerForm"; +import { ManufacturersGrid } from "@src/products/ManufacturersGrid"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +export function ManufacturersPage(): React.ReactElement { + const intl = useIntl(); + return ( + + + + + + + {(selectedId) => } + + + + + + + ); +} diff --git a/demo/admin/src/products/ProductForm.gql.ts b/demo/admin/src/products/ProductForm.gql.ts index 311d38a87d..283ff8a1b8 100644 --- a/demo/admin/src/products/ProductForm.gql.ts +++ b/demo/admin/src/products/ProductForm.gql.ts @@ -5,7 +5,6 @@ export const productFormFragment = gql` title slug description - price type inStock image @@ -43,8 +42,8 @@ export const createProductMutation = gql` `; export const updateProductMutation = gql` - mutation UpdateProduct($id: ID!, $input: ProductUpdateInput!, $lastUpdatedAt: DateTime) { - updateProduct(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + mutation UpdateProduct($id: ID!, $input: ProductUpdateInput!) { + updateProduct(id: $id, input: $input) { id updatedAt ...ProductFormManual diff --git a/demo/admin/src/products/ProductForm.tsx b/demo/admin/src/products/ProductForm.tsx index 6f94e2b1f6..c8fa8b2fb4 100644 --- a/demo/admin/src/products/ProductForm.tsx +++ b/demo/admin/src/products/ProductForm.tsx @@ -1,28 +1,22 @@ import { useApolloClient, useQuery } from "@apollo/client"; import { + CheckboxField, Field, FinalForm, - FinalFormCheckbox, - FinalFormInput, - FinalFormSaveSplitButton, FinalFormSelect, FinalFormSubmitEvent, Loading, MainContent, - Toolbar, - ToolbarActions, - ToolbarFillSpace, - ToolbarItem, - ToolbarTitleItem, + SelectField, + TextAreaField, + TextField, useAsyncOptionsProps, useFormApiRef, - useStackApi, useStackSwitchApi, } from "@comet/admin"; -import { ArrowLeft } from "@comet/admin-icons"; import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; -import { FormControlLabel, IconButton, MenuItem } from "@mui/material"; +import { MenuItem } from "@mui/material"; import { GQLProductType } from "@src/graphql.generated"; import { FormApi } from "final-form"; import { filter } from "graphql-anywhere"; @@ -48,11 +42,11 @@ import { GQLProductQuery, GQLProductQueryVariables, GQLProductTagsQuery, + GQLProductTagsQueryVariables, GQLProductTagsSelectFragment, GQLUpdateProductMutation, GQLUpdateProductMutationVariables, } from "./ProductForm.gql.generated"; -import { GQLProductTagsListQueryVariables } from "./tags/ProductTagTable.generated"; interface FormProps { id?: string; @@ -62,13 +56,11 @@ const rootBlocks = { image: DamImageBlock, }; -type FormValues = Omit & { - price: string; +type FormValues = Omit & { image: BlockState; }; -function ProductForm({ id }: FormProps): React.ReactElement { - const stackApi = useStackApi(); +export function ProductForm({ id }: FormProps): React.ReactElement { const client = useApolloClient(); const mode = id ? "edit" : "add"; const formApiRef = useFormApiRef(); @@ -82,7 +74,6 @@ function ProductForm({ id }: FormProps): React.ReactElement { const initialValues: Partial = data?.product ? { ...filter(productFormFragment, data.product), - price: String(data.product.price), image: rootBlocks.image.input2State(data.product.image), } : { @@ -106,30 +97,27 @@ function ProductForm({ id }: FormProps): React.ReactElement { if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { ...formValues, - price: parseFloat(formValues.price), image: rootBlocks.image.state2Output(formValues.image), type: formValues.type as GQLProductType, category: formValues.category?.id, tags: formValues.tags.map((i) => i.id), - variants: [], articleNumbers: [], discounts: [], - packageDimensions: { width: 0, height: 0, depth: 0 }, statistics: { views: 0 }, }; if (mode === "edit") { if (!id) throw new Error(); await client.mutate({ mutation: updateProductMutation, - variables: { id, input: output, lastUpdatedAt: data?.product.updatedAt }, + variables: { id, input: output }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: createProductMutation, variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProduct.id; + const id = mutationResponse?.createProduct.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage(`edit`, id); @@ -144,13 +132,11 @@ function ProductForm({ id }: FormProps): React.ReactElement { return categories.data.productCategories.nodes; }); const tagsSelectAsyncProps = useAsyncOptionsProps(async () => { - const tags = await client.query({ query: productTagsQuery }); + const tags = await client.query({ query: productTagsQuery }); return tags.data.productTags.nodes; }); - if (error) { - return ; - } + if (error) throw error; if (loading) { return ; @@ -163,65 +149,25 @@ function ProductForm({ id }: FormProps): React.ReactElement { mode={mode} initialValues={initialValues} initialValuesEqual={isEqual} //required to compare block data correctly - onAfterSubmit={(values, form) => { - //don't go back automatically TODO remove this automatismn - }} subscription={{}} > {() => ( {saveConflict.dialogs} - - - - - - - - - {({ input }) => - input.value ? input.value : - } - - - - - - - - } - /> - } - /> - } /> + } /> + } /> - - {(props) => ( - - Cap - Shirt - Tie - - )} - + + Cap + Shirt + Tie + option.title} /> - } - /> - - {(props) => ( - } - control={} - /> - )} - + } fullWidth /> {createFinalFormBlock(rootBlocks.image)} @@ -263,5 +195,3 @@ function ProductForm({ id }: FormProps): React.ReactElement { ); } - -export default ProductForm; diff --git a/demo/admin/src/products/ProductPriceForm.gql.ts b/demo/admin/src/products/ProductPriceForm.gql.ts new file mode 100644 index 0000000000..dbc6cd7345 --- /dev/null +++ b/demo/admin/src/products/ProductPriceForm.gql.ts @@ -0,0 +1,29 @@ +import { gql } from "@apollo/client"; + +export const productPriceFormFragment = gql` + fragment ProductPriceForm on Product { + price + } +`; + +export const productPriceFormQuery = gql` + query ProductPriceForm($id: ID!) { + product(id: $id) { + id + updatedAt + ...ProductPriceForm + } + } + ${productPriceFormFragment} +`; + +export const updateProductPriceFormMutation = gql` + mutation ProductPriceFormUpdateProduct($id: ID!, $input: ProductUpdateInput!) { + updateProduct(id: $id, input: $input) { + id + updatedAt + ...ProductPriceForm + } + } + ${productPriceFormFragment} +`; diff --git a/demo/admin/src/products/ProductPriceForm.tsx b/demo/admin/src/products/ProductPriceForm.tsx new file mode 100644 index 0000000000..ca940ff1cb --- /dev/null +++ b/demo/admin/src/products/ProductPriceForm.tsx @@ -0,0 +1,102 @@ +import { useApolloClient, useQuery } from "@apollo/client"; +import { Field, FinalForm, FinalFormInput, FinalFormSubmitEvent, MainContent, useFormApiRef } from "@comet/admin"; +import { EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { CircularProgress } from "@mui/material"; +import { FormApi } from "final-form"; +import { filter } from "graphql-anywhere"; +import isEqual from "lodash.isequal"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { productPriceFormFragment, productPriceFormQuery, updateProductPriceFormMutation } from "./ProductPriceForm.gql"; +import { + GQLProductPriceFormFragment, + GQLProductPriceFormQuery, + GQLProductPriceFormQueryVariables, + GQLProductPriceFormUpdateProductMutation, + GQLProductPriceFormUpdateProductMutationVariables, +} from "./ProductPriceForm.gql.generated"; + +interface FormProps { + id: string; +} + +type FormValues = Omit & { + price?: string; +}; + +export function ProductPriceForm({ id }: FormProps): React.ReactElement { + const client = useApolloClient(); + const formApiRef = useFormApiRef(); + + const { data, error, loading, refetch } = useQuery(productPriceFormQuery, { + variables: { id }, + }); + + const initialValues: Partial = data?.product + ? { + ...filter(productPriceFormFragment, data.product), + price: data.product.price ? String(data.product.price) : undefined, + } + : {}; + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "product", id); + return resolveHasSaveConflict(data?.product.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); + const output = { + ...formValues, + price: formValues.price ? parseFloat(formValues.price) : null, + }; + await client.mutate({ + mutation: updateProductPriceFormMutation, + variables: { id, input: output }, + }); + }; + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode="edit" + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + onAfterSubmit={(values, form) => { + //don't go back automatically TODO remove this automatismn + }} + subscription={{}} + > + {() => ( + + {saveConflict.dialogs} + + } + /> + + + )} + + ); +} diff --git a/demo/admin/src/products/ProductVariantForm.gql.ts b/demo/admin/src/products/ProductVariantForm.gql.ts new file mode 100644 index 0000000000..290b186639 --- /dev/null +++ b/demo/admin/src/products/ProductVariantForm.gql.ts @@ -0,0 +1,41 @@ +import { gql } from "@apollo/client"; + +export const productVariantFormFragment = gql` + fragment ProductVariantForm on ProductVariant { + name + image + } +`; + +export const productVariantFormQuery = gql` + query ProductVariantForm($id: ID!) { + productVariant(id: $id) { + id + updatedAt + ...ProductVariantForm + } + } + ${productVariantFormFragment} +`; + +export const createProductVariantFormMutation = gql` + mutation CreateProductVariant($product: ID!, $input: ProductVariantInput!) { + createProductVariant(product: $product, input: $input) { + id + updatedAt + ...ProductVariantForm + } + } + ${productVariantFormFragment} +`; + +export const updateProductVariantFormMutation = gql` + mutation UpdateProductVariant($id: ID!, $input: ProductVariantUpdateInput!) { + updateProductVariant(id: $id, input: $input) { + id + updatedAt + ...ProductVariantForm + } + } + ${productVariantFormFragment} +`; diff --git a/demo/admin/src/products/ProductVariantForm.tsx b/demo/admin/src/products/ProductVariantForm.tsx new file mode 100644 index 0000000000..3fe0e917b9 --- /dev/null +++ b/demo/admin/src/products/ProductVariantForm.tsx @@ -0,0 +1,132 @@ +import { useApolloClient, useQuery } from "@apollo/client"; +import { Field, FinalForm, FinalFormInput, FinalFormSubmitEvent, Loading, MainContent, useFormApiRef, useStackSwitchApi } from "@comet/admin"; +import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; +import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { FormApi } from "final-form"; +import { filter } from "graphql-anywhere"; +import isEqual from "lodash.isequal"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { + createProductVariantFormMutation, + productVariantFormFragment, + productVariantFormQuery, + updateProductVariantFormMutation, +} from "./ProductVariantForm.gql"; +import { + GQLCreateProductVariantMutation, + GQLCreateProductVariantMutationVariables, + GQLProductVariantFormFragment, + GQLProductVariantFormQuery, + GQLProductVariantFormQueryVariables, + GQLUpdateProductVariantMutation, + GQLUpdateProductVariantMutationVariables, +} from "./ProductVariantForm.gql.generated"; + +interface FormProps { + id?: string; + productId: string; +} + +const rootBlocks = { + image: DamImageBlock, +}; + +type FormValues = Omit & { + image: BlockState; +}; + +export function ProductVariantForm({ id, productId }: FormProps): React.ReactElement { + const client = useApolloClient(); + const mode = id ? "edit" : "add"; + const formApiRef = useFormApiRef(); + const stackSwitchApi = useStackSwitchApi(); + + const { data, error, loading, refetch } = useQuery( + productVariantFormQuery, + id ? { variables: { id } } : { skip: true }, + ); + + const initialValues: Partial = data?.productVariant + ? { + ...filter(productVariantFormFragment, data.productVariant), + image: rootBlocks.image.input2State(data.productVariant.image), + } + : { + image: rootBlocks.image.defaultValues(), + }; + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "productVariant", id); + return resolveHasSaveConflict(data?.productVariant.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); + const output = { + ...formValues, + image: rootBlocks.image.state2Output(formValues.image), + }; + if (mode === "edit") { + if (!id) throw new Error(); + await client.mutate({ + mutation: updateProductVariantFormMutation, + variables: { id, input: output }, + }); + } else { + const { data: mutationReponse } = await client.mutate({ + mutation: createProductVariantFormMutation, + variables: { product: productId, input: output }, + }); + if (!event.navigatingBack) { + const id = mutationReponse?.createProductVariant.id; + if (id) { + setTimeout(() => { + stackSwitchApi.activatePage(`edit`, id); + }); + } + } + } + }; + + if (error) throw error; + + if (loading) { + return ; + } + + return ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode="edit" + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + subscription={{}} + > + {() => ( + + {saveConflict.dialogs} + + } + /> + + {createFinalFormBlock(rootBlocks.image)} + + + + )} + + ); +} diff --git a/demo/admin/src/products/ProductVariantsGrid.tsx b/demo/admin/src/products/ProductVariantsGrid.tsx new file mode 100644 index 0000000000..64956fef56 --- /dev/null +++ b/demo/admin/src/products/ProductVariantsGrid.tsx @@ -0,0 +1,185 @@ +import { useQuery } from "@apollo/client"; +import { + GridFilterButton, + muiGridFilterToGql, + muiGridSortToGql, + StackLink, + Toolbar, + ToolbarAutomaticTitleItem, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { Add as AddIcon, Edit } from "@comet/admin-icons"; +import { Box, Button, IconButton } from "@mui/material"; +import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import gql from "graphql-tag"; +import * as React from "react"; +import { FormattedMessage } from "react-intl"; + +import { + //GQLCreateProductMutation, + //GQLCreateProductMutationVariables, + //GQLDeleteProductMutation, + //GQLDeleteProductMutationVariables, + GQLProductVariantsListFragment, + GQLProductVariantsListQuery, + GQLProductVariantsListQueryVariables, + //GQLUpdateProductVisibilityMutation, + //GQLUpdateProductVisibilityMutationVariables, +} from "./ProductVariantsGrid.generated"; + +function ProductVariantsGridToolbar() { + return ( + + + + + + + + + + + + + + ); +} + +export function ProductVariantsGrid({ productId }: { productId: string }) { + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductVariantsGrid") }; + const sortModel = dataGridProps.sortModel; + //const client = useApolloClient(); + + const columns: GridColDef[] = [ + { field: "name", headerName: "Name", width: 150 }, + /* + { + field: "visible", + headerName: "Visible", + width: 100, + type: "boolean", + renderCell: (params) => { + return ( + { + await client.mutate({ + mutation: updateProductVisibilityMutation, + variables: { id: params.row.id, visible }, + optimisticResponse: { + __typename: "Mutation", + updateProductVisibility: { __typename: "Product", id: params.row.id, visible }, + }, + }); + }} + /> + ); + }, + }, + */ + { + field: "action", + headerName: "", + sortable: false, + filterable: false, + renderCell: (params) => { + return ( + <> + + + + {/* + + */} + + ); + }, + }, + ]; + + const { data, loading, error } = useQuery(productVariantsQuery, { + variables: { + product: productId, + ...muiGridFilterToGql(columns, dataGridProps.filterModel), + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(sortModel), + }, + }); + const rows = data?.productVariants.nodes ?? []; + const rowCount = useBufferedRowCount(data?.productVariants.totalCount); + + return ( + + + + ); +} +const productVariantsFragment = gql` + fragment ProductVariantsList on ProductVariant { + id + name + } +`; + +const productVariantsQuery = gql` + query ProductVariantsList( + $product: ID! + $offset: Int + $limit: Int + $sort: [ProductVariantSort!] + $filter: ProductVariantFilter + $search: String + ) { + productVariants(product: $product, offset: $offset, limit: $limit, sort: $sort, filter: $filter, search: $search) { + nodes { + ...ProductVariantsList + } + totalCount + } + } + ${productVariantsFragment} +`; +/* +const deleteProductMutation = gql` + mutation DeleteProductVariant($id: ID!) { + deleteProduct(id: $id) + } +`; +const createProductMutation = gql` + mutation CreateProductVariant($input: ProductVariantInput!) { + createProduct(input: $input) { + id + } + } +`; +*/ +/* +const updateProductVisibilityMutation = gql` + mutation UpdateProductVisibility($id: ID!, $visible: Boolean!) { + updateProductVariantVisibility(id: $id, visible: $visible) { + id + visible + } + } +`; +*/ diff --git a/demo/admin/src/products/ProductsGrid.tsx b/demo/admin/src/products/ProductsGrid.tsx index 75c760de09..a6a0b948b2 100644 --- a/demo/admin/src/products/ProductsGrid.tsx +++ b/demo/admin/src/products/ProductsGrid.tsx @@ -35,8 +35,8 @@ import { GQLProductsListManualFragment, GQLProductsListQuery, GQLProductsListQueryVariables, - GQLUpdateProductVisibilityMutation, - GQLUpdateProductVisibilityMutationVariables, + GQLUpdateProductStatusMutation, + GQLUpdateProductStatusMutationVariables, } from "./ProductsGrid.generated"; function ProductsGridToolbar() { @@ -59,7 +59,7 @@ function ProductsGridToolbar() { ); } -function ProductsGrid() { +export function ProductsGrid() { const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; const sortModel = dataGridProps.sortModel; const client = useApolloClient(); @@ -111,21 +111,22 @@ function ProductsGrid() { }, { field: "inStock", headerName: "In Stock", width: 50, type: "boolean" }, { - field: "visible", - headerName: "Visible", + field: "status", + headerName: "Status", width: 100, type: "boolean", + valueGetter: (params) => params.row.status == "Published", renderCell: (params) => { return ( { - await client.mutate({ - mutation: updateProductVisibilityMutation, - variables: { id: params.row.id, visible }, + visibility={params.row.status == "Published"} + onUpdateVisibility={async (status) => { + await client.mutate({ + mutation: updateProductStatusMutation, + variables: { id: params.row.id, status: status ? "Published" : "Unpublished" }, optimisticResponse: { __typename: "Mutation", - updateProductVisibility: { __typename: "Product", id: params.row.id, visible }, + updateProduct: { __typename: "Product", id: params.row.id, status: status ? "Published" : "Unpublished" }, }, }); }} @@ -159,13 +160,9 @@ function ProductsGrid() { type: input.type, category: input.category?.id, tags: input.tags.map((tag) => tag.id), - variants: input.variants.map((variant) => ({ - name: variant.name, - image: DamImageBlock.state2Output(DamImageBlock.input2State(variant.image)), - })), + colors: input.colors, articleNumbers: input.articleNumbers, discounts: input.discounts, - packageDimensions: input.packageDimensions, statistics: { views: 0 }, }, }, @@ -227,7 +224,7 @@ const productsFragment = gql` type inStock image - visible + status category { id title @@ -236,20 +233,18 @@ const productsFragment = gql` id title } - variants { - image + colors { name + hexCode + } + variants { + id } articleNumbers discounts { quantity price } - packageDimensions { - width - height - depth - } } `; @@ -291,13 +286,11 @@ const createProductMutation = gql` } `; -const updateProductVisibilityMutation = gql` - mutation UpdateProductVisibility($id: ID!, $visible: Boolean!) { - updateProductVisibility(id: $id, visible: $visible) { +const updateProductStatusMutation = gql` + mutation UpdateProductStatus($id: ID!, $status: ProductStatus!) { + updateProduct(id: $id, input: { status: $status }) { id - visible + status } } `; - -export default ProductsGrid; diff --git a/demo/admin/src/products/ProductsPage.tsx b/demo/admin/src/products/ProductsPage.tsx index 3592cc7973..12d7432579 100644 --- a/demo/admin/src/products/ProductsPage.tsx +++ b/demo/admin/src/products/ProductsPage.tsx @@ -1,9 +1,25 @@ -import { Stack, StackPage, StackSwitch } from "@comet/admin"; +import { + RouterTab, + RouterTabs, + SaveBoundary, + SaveBoundarySaveButton, + Stack, + StackPage, + StackSwitch, + StackToolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarBackButton, + ToolbarFillSpace, +} from "@comet/admin"; import React from "react"; import { useIntl } from "react-intl"; -import ProductForm from "./ProductForm"; -import ProductsGrid from "./ProductsGrid"; +import { ProductForm } from "./ProductForm"; +import { ProductPriceForm } from "./ProductPriceForm"; +import { ProductsGrid } from "./ProductsGrid"; +import { ProductVariantForm } from "./ProductVariantForm"; +import { ProductVariantsGrid } from "./ProductVariantsGrid"; const ProductsPage: React.FC = () => { const intl = useIntl(); @@ -15,10 +31,88 @@ const ProductsPage: React.FC = () => { - {(selectedId) => } + {(selectedProductId) => ( + + + + + + + + + + + + + + + + + + + + + + + {(selectedProductVariantId) => ( + + + + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + )} - + + + + + + + + + + + diff --git a/demo/admin/src/products/categories/ProductCategoriesTable.tsx b/demo/admin/src/products/categories/ProductCategoriesTable.tsx index 3a2682669f..7faf9028c5 100644 --- a/demo/admin/src/products/categories/ProductCategoriesTable.tsx +++ b/demo/admin/src/products/categories/ProductCategoriesTable.tsx @@ -74,7 +74,12 @@ const columns: GridColDef[] = [ onPaste={async ({ input, client }) => { await client.mutate({ mutation: createProductMutation, - variables: { input: { ...input, products: [] } }, + variables: { + input: { + title: input.title, + slug: input.slug, + }, + }, }); }} onDelete={async ({ client }) => { diff --git a/demo/admin/src/products/categories/ProductCategoryForm.gql.ts b/demo/admin/src/products/categories/ProductCategoryForm.gql.ts index 6965d1ea62..fb02d4a08c 100644 --- a/demo/admin/src/products/categories/ProductCategoryForm.gql.ts +++ b/demo/admin/src/products/categories/ProductCategoryForm.gql.ts @@ -38,8 +38,8 @@ export const createProductCategoryMutation = gql` `; export const updateProductCategoryMutation = gql` - mutation ProductCategoryFormUpdateProductCategory($id: ID!, $input: ProductCategoryUpdateInput!, $lastUpdatedAt: DateTime) { - updateProductCategory(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + mutation ProductCategoryFormUpdateProductCategory($id: ID!, $input: ProductCategoryUpdateInput!) { + updateProductCategory(id: $id, input: $input) { id updatedAt ...ProductCategoryForm diff --git a/demo/admin/src/products/categories/ProductCategoryForm.tsx b/demo/admin/src/products/categories/ProductCategoryForm.tsx index 2913b33904..7327059c8c 100644 --- a/demo/admin/src/products/categories/ProductCategoryForm.tsx +++ b/demo/admin/src/products/categories/ProductCategoryForm.tsx @@ -2,10 +2,10 @@ import { useApolloClient, useQuery } from "@apollo/client"; import { Field, FinalForm, - FinalFormInput, FinalFormSaveSplitButton, FinalFormSubmitEvent, MainContent, + TextField, Toolbar, ToolbarActions, ToolbarFillSpace, @@ -95,10 +95,10 @@ function ProductCategoryForm({ id }: FormProps): React.ReactElement { if (!id) throw new Error(); await client.mutate({ mutation: updateProductCategoryMutation, - variables: { id, input: output, lastUpdatedAt: data?.productCategory.updatedAt }, + variables: { id, input: output }, }); } else { - const { data: mutationReponse } = await client.mutate< + const { data: mutationResponse } = await client.mutate< GQLProductCategoryFormCreateProductCategoryMutation, GQLProductCategoryFormCreateProductCategoryMutationVariables >({ @@ -106,7 +106,7 @@ function ProductCategoryForm({ id }: FormProps): React.ReactElement { variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProductCategory.id; + const id = mutationResponse?.createProductCategory.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage(`edit`, id); @@ -125,16 +125,7 @@ function ProductCategoryForm({ id }: FormProps): React.ReactElement { } return ( - - apiRef={formApiRef} - onSubmit={handleSubmit} - mode={mode} - initialValues={initialValues} - onAfterSubmit={(values, form) => { - //don't go back automatically TODO remove this automatismn - }} - subscription={{}} - > + apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues} subscription={{}}> {() => ( {saveConflict.dialogs} @@ -157,24 +148,12 @@ function ProductCategoryForm({ id }: FormProps): React.ReactElement { - + - } - /> - } - /> + } /> + } /> )} diff --git a/demo/admin/src/products/future/ManufacturersGrid.cometGen.ts b/demo/admin/src/products/future/ManufacturersGrid.cometGen.ts new file mode 100644 index 0000000000..da6f3bfaae --- /dev/null +++ b/demo/admin/src/products/future/ManufacturersGrid.cometGen.ts @@ -0,0 +1,19 @@ +import { future_GridConfig as GridConfig } from "@comet/cms-admin"; +import { GQLManufacturer } from "@src/graphql.generated"; + +export const ManufacturersGrid: GridConfig = { + type: "grid", + gqlType: "Manufacturer", + fragmentName: "ManufacturersGridFuture", // configurable as it must be unique across project + columns: [ + { type: "text", name: "id", headerName: "ID" }, + { type: "text", name: "address.street", headerName: "Street" }, + { type: "number", name: "address.streetNumber", headerName: "Street number" }, + { type: "text", name: "address.alternativeAddress.street", headerName: "Alt-Street" }, + { type: "number", name: "address.alternativeAddress.streetNumber", headerName: "Alt-Street number" }, + { type: "text", name: "addressAsEmbeddable.street", headerName: "Street 2" }, + { type: "number", name: "addressAsEmbeddable.streetNumber", headerName: "Street number 2" }, + { type: "text", name: "addressAsEmbeddable.alternativeAddress.street", headerName: "Alt-Street 2" }, + { type: "number", name: "addressAsEmbeddable.alternativeAddress.streetNumber", headerName: "Alt-Street number 2" }, + ], +}; diff --git a/demo/admin/src/products/future/ManufacturersPage.tsx b/demo/admin/src/products/future/ManufacturersPage.tsx new file mode 100644 index 0000000000..000d427bed --- /dev/null +++ b/demo/admin/src/products/future/ManufacturersPage.tsx @@ -0,0 +1,23 @@ +import { Stack, StackPage, StackSwitch } from "@comet/admin"; +import { ManufacturersGrid } from "@src/products/future/generated/ManufacturersGrid"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +export function ManufacturersPage(): React.ReactElement { + const intl = useIntl(); + return ( + + + + + + +
Add Manufacturer
+
+ + {(selectedId) =>
Edit Manufacturer
} +
+
+
+ ); +} diff --git a/demo/admin/src/products/future/ProductForm.cometGen.ts b/demo/admin/src/products/future/ProductForm.cometGen.ts new file mode 100644 index 0000000000..896891cb63 --- /dev/null +++ b/demo/admin/src/products/future/ProductForm.cometGen.ts @@ -0,0 +1,57 @@ +import { future_FormConfig as FormConfig } from "@comet/cms-admin"; +import { GQLProduct } from "@src/graphql.generated"; + +export const ProductForm: FormConfig = { + type: "form", + gqlType: "Product", + fragmentName: "ProductFormDetails", // configurable as it must be unique across project + fields: [ + { + type: "text", + name: "title", + label: "Titel", // default is generated from name (camelCaseToHumanReadable) + required: true, // default is inferred from gql schema + validate: { name: "validateTitle", import: "./validateTitle" }, + }, + { type: "text", name: "slug" }, + { type: "date", name: "createdAt", label: "Created", readOnly: true }, + { type: "text", name: "description", label: "Description", multiline: true }, + { type: "staticSelect", name: "type", label: "Type" /*, values: from gql schema (TODO overridable)*/ }, + { type: "asyncSelect", name: "category", rootQuery: "productCategories" }, + { type: "number", name: "price", helperText: "Enter price in this format: 123,45" }, + { type: "boolean", name: "inStock" }, + { type: "date", name: "availableSince" }, + { type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } }, + ], +}; + +/* +TODO +export const tabsConfig: TabsConfig = { + type: "tabs", + tabs: [{ name: "form", content: formConfig }], +}; + +//alternative syntax for the above +export const tabsConfig2: TabsConfig = { + type: "tabs", + tabs: [ + { + name: "form", + content: { + type: "form", + gqlType: "Product", + fields: [ + { type: "text", name: "title", label: "Titel" }, + { type: "text", name: "slug", label: "Slug" }, + { type: "text", name: "description", label: "Description", multiline: true }, + { type: "staticSelect", name: "type", label: "Type" / *, values: from gql schema (overridable)* / }, + { type: "asyncSelect", name: "type", label: "Type" / *, endpoint: from gql schema (overridable)* / }, + { type: "block", name: "image", label: "Image", block: PixelImageBlock }, + ], + } satisfies FormConfig, + }, + ], +}; + +*/ diff --git a/demo/admin/src/products/future/ProductsGrid.cometGen.ts b/demo/admin/src/products/future/ProductsGrid.cometGen.ts new file mode 100644 index 0000000000..8a920f42ec --- /dev/null +++ b/demo/admin/src/products/future/ProductsGrid.cometGen.ts @@ -0,0 +1,18 @@ +import { future_GridConfig as GridConfig } from "@comet/cms-admin"; +import { GQLProduct } from "@src/graphql.generated"; + +export const ProductsGrid: GridConfig = { + type: "grid", + gqlType: "Product", + fragmentName: "ProductsGridFuture", // configurable as it must be unique across project + filterProp: true, + columns: [ + { type: "boolean", name: "inStock", headerName: "In stock", width: 90 }, + { type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250 }, + { type: "text", name: "description", headerName: "Description" }, + { type: "number", name: "price", headerName: "Price", maxWidth: 150 }, + { type: "staticSelect", name: "type", maxWidth: 150 /*, values: from gql schema (TODO overridable)*/ }, + { type: "date", name: "availableSince", width: 140 }, + { type: "dateTime", name: "createdAt", width: 170 }, + ], +}; diff --git a/demo/admin/src/products/future/ProductsPage.tsx b/demo/admin/src/products/future/ProductsPage.tsx new file mode 100644 index 0000000000..fd5223721d --- /dev/null +++ b/demo/admin/src/products/future/ProductsPage.tsx @@ -0,0 +1,48 @@ +import { + SaveBoundary, + SaveBoundarySaveButton, + Stack, + StackPage, + StackSwitch, + StackToolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarBackButton, + ToolbarFillSpace, +} from "@comet/admin"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +import { ProductForm } from "./generated/ProductForm"; +import { ProductsGrid } from "./generated/ProductsGrid"; + +export function ProductsPage(): React.ReactElement { + const intl = useIntl(); + return ( + + + + + + + {(selectedProductId) => ( + + + + + + + + + + + + )} + + + + + + + ); +} diff --git a/demo/admin/src/products/future/ProductsWithLowPricePage.tsx b/demo/admin/src/products/future/ProductsWithLowPricePage.tsx new file mode 100644 index 0000000000..01e3c373eb --- /dev/null +++ b/demo/admin/src/products/future/ProductsWithLowPricePage.tsx @@ -0,0 +1,25 @@ +import { Stack, StackPage, StackSwitch } from "@comet/admin"; +import * as React from "react"; +import { useIntl } from "react-intl"; + +import { ProductForm } from "./generated/ProductForm"; +import { ProductsGrid } from "./generated/ProductsGrid"; + +export function ProductsWithLowPricePage(): React.ReactElement { + const intl = useIntl(); + return ( + + + + + + + {(selectedId) => } + + + + + + + ); +} diff --git a/demo/admin/src/products/future/generated/ManufacturersGrid.tsx b/demo/admin/src/products/future/generated/ManufacturersGrid.tsx new file mode 100644 index 0000000000..a7e1760d14 --- /dev/null +++ b/demo/admin/src/products/future/generated/ManufacturersGrid.tsx @@ -0,0 +1,259 @@ +// This file has been generated by comet admin-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { gql, useApolloClient, useQuery } from "@apollo/client"; +import { + CrudContextMenu, + GridFilterButton, + MainContent, + muiGridFilterToGql, + muiGridSortToGql, + StackLink, + Toolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { Add as AddIcon, Edit } from "@comet/admin-icons"; +import { Button, IconButton } from "@mui/material"; +import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { filter as filterByFragment } from "graphql-anywhere"; +import * as React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { + GQLCreateManufacturerMutation, + GQLCreateManufacturerMutationVariables, + GQLDeleteManufacturerMutation, + GQLDeleteManufacturerMutationVariables, + GQLManufacturersGridFutureFragment, + GQLManufacturersGridQuery, + GQLManufacturersGridQueryVariables, +} from "./ManufacturersGrid.generated"; + +const manufacturersFragment = gql` + fragment ManufacturersGridFuture on Manufacturer { + id + address { + street + streetNumber + alternativeAddress { + street + streetNumber + } + } + addressAsEmbeddable { + street + streetNumber + alternativeAddress { + street + streetNumber + } + } + } +`; + +const manufacturersQuery = gql` + query ManufacturersGrid($offset: Int, $limit: Int, $sort: [ManufacturerSort!], $search: String, $filter: ManufacturerFilter) { + manufacturers(offset: $offset, limit: $limit, sort: $sort, search: $search, filter: $filter) { + nodes { + ...ManufacturersGridFuture + } + totalCount + } + } + ${manufacturersFragment} +`; + +const deleteManufacturerMutation = gql` + mutation DeleteManufacturer($id: ID!) { + deleteManufacturer(id: $id) + } +`; + +const createManufacturerMutation = gql` + mutation CreateManufacturer($input: ManufacturerInput!) { + createManufacturer(input: $input) { + id + } + } +`; + +function ManufacturersGridToolbar() { + return ( + + + + + + + + + + + + + + ); +} + +export function ManufacturersGrid(): React.ReactElement { + const client = useApolloClient(); + const intl = useIntl(); + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ManufacturersGrid") }; + + const columns: GridColDef[] = [ + { + field: "id", + headerName: intl.formatMessage({ id: "manufacturer.id", defaultMessage: "ID" }), + filterable: false, + sortable: false, + flex: 1, + minWidth: 150, + }, + { + field: "address_street", + headerName: intl.formatMessage({ id: "manufacturer.address.street", defaultMessage: "Street" }), + filterable: false, + sortable: false, + valueGetter: ({ row }) => row.address?.street, + flex: 1, + minWidth: 150, + }, + { + field: "address_streetNumber", + headerName: intl.formatMessage({ id: "manufacturer.address.streetNumber", defaultMessage: "Street number" }), + type: "number", + filterable: false, + sortable: false, + valueGetter: ({ row }) => row.address?.streetNumber, + flex: 1, + minWidth: 150, + }, + { + field: "address_alternativeAddress_street", + headerName: intl.formatMessage({ id: "manufacturer.address.alternativeAddress.street", defaultMessage: "Alt-Street" }), + filterable: false, + sortable: false, + valueGetter: ({ row }) => row.address?.alternativeAddress?.street, + flex: 1, + minWidth: 150, + }, + { + field: "address_alternativeAddress_streetNumber", + headerName: intl.formatMessage({ id: "manufacturer.address.alternativeAddress.streetNumber", defaultMessage: "Alt-Street number" }), + type: "number", + filterable: false, + sortable: false, + valueGetter: ({ row }) => row.address?.alternativeAddress?.streetNumber, + flex: 1, + minWidth: 150, + }, + { + field: "addressAsEmbeddable_street", + headerName: intl.formatMessage({ id: "manufacturer.addressAsEmbeddable.street", defaultMessage: "Street 2" }), + valueGetter: ({ row }) => row.addressAsEmbeddable?.street, + flex: 1, + minWidth: 150, + }, + { + field: "addressAsEmbeddable_streetNumber", + headerName: intl.formatMessage({ id: "manufacturer.addressAsEmbeddable.streetNumber", defaultMessage: "Street number 2" }), + type: "number", + valueGetter: ({ row }) => row.addressAsEmbeddable?.streetNumber, + flex: 1, + minWidth: 150, + }, + { + field: "addressAsEmbeddable_alternativeAddress_street", + headerName: intl.formatMessage({ id: "manufacturer.addressAsEmbeddable.alternativeAddress.street", defaultMessage: "Alt-Street 2" }), + valueGetter: ({ row }) => row.addressAsEmbeddable?.alternativeAddress?.street, + flex: 1, + minWidth: 150, + }, + { + field: "addressAsEmbeddable_alternativeAddress_streetNumber", + headerName: intl.formatMessage({ + id: "manufacturer.addressAsEmbeddable.alternativeAddress.streetNumber", + defaultMessage: "Alt-Street number 2", + }), + type: "number", + valueGetter: ({ row }) => row.addressAsEmbeddable?.alternativeAddress?.streetNumber, + flex: 1, + minWidth: 150, + }, + { + field: "actions", + headerName: "", + sortable: false, + filterable: false, + type: "actions", + align: "right", + renderCell: (params) => { + return ( + <> + + + + { + // Don't copy id, because we want to create a new entity with this data + const { id, ...filteredData } = filterByFragment(manufacturersFragment, params.row); + return filteredData; + }} + onPaste={async ({ input }) => { + await client.mutate({ + mutation: createManufacturerMutation, + variables: { input }, + }); + }} + onDelete={async () => { + await client.mutate({ + mutation: deleteManufacturerMutation, + variables: { id: params.row.id }, + }); + }} + refetchQueries={[manufacturersQuery]} + /> + + ); + }, + }, + ]; + + const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel); + + const { data, loading, error } = useQuery(manufacturersQuery, { + variables: { + filter: { and: [gqlFilter] }, + search: gqlSearch, + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(dataGridProps.sortModel), + }, + }); + const rowCount = useBufferedRowCount(data?.manufacturers.totalCount); + if (error) throw error; + const rows = data?.manufacturers.nodes ?? []; + + return ( + + + + ); +} diff --git a/demo/admin/src/products/future/generated/ProductForm.gql.tsx b/demo/admin/src/products/future/generated/ProductForm.gql.tsx new file mode 100644 index 0000000000..9caa828a72 --- /dev/null +++ b/demo/admin/src/products/future/generated/ProductForm.gql.tsx @@ -0,0 +1,67 @@ +// This file has been generated by comet admin-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { gql } from "@apollo/client"; + +export const productCategoriesSelectFragment = gql` + fragment ProductCategorySelect on ProductCategory { + id + title + } +`; +export const productCategoriesQuery = gql` + query ProductCategoriesSelect { + productCategories { + nodes { + ...ProductCategorySelect + } + } + } + ${productCategoriesSelectFragment} +`; +export const productFormFragment = gql` + fragment ProductFormDetails on Product { + title + slug + createdAt + description + type + category { + id + title + } + price + inStock + availableSince + image + } +`; +export const productQuery = gql` + query Product($id: ID!) { + product(id: $id) { + id + updatedAt + ...ProductFormDetails + } + } + ${productFormFragment} +`; +export const createProductMutation = gql` + mutation CreateProduct($input: ProductInput!) { + createProduct(input: $input) { + id + updatedAt + ...ProductFormDetails + } + } + ${productFormFragment} +`; +export const updateProductMutation = gql` + mutation UpdateProduct($id: ID!, $input: ProductUpdateInput!) { + updateProduct(id: $id, input: $input) { + id + updatedAt + ...ProductFormDetails + } + } + ${productFormFragment} +`; diff --git a/demo/admin/src/products/future/generated/ProductForm.tsx b/demo/admin/src/products/future/generated/ProductForm.tsx new file mode 100644 index 0000000000..994e6c0b09 --- /dev/null +++ b/demo/admin/src/products/future/generated/ProductForm.tsx @@ -0,0 +1,239 @@ +// This file has been generated by comet admin-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { useApolloClient, useQuery } from "@apollo/client"; +import { + Field, + FinalForm, + FinalFormCheckbox, + FinalFormInput, + FinalFormSelect, + FinalFormSubmitEvent, + Loading, + MainContent, + TextAreaField, + TextField, + useAsyncOptionsProps, + useFormApiRef, + useStackSwitchApi, +} from "@comet/admin"; +import { FinalFormDatePicker } from "@comet/admin-date-time"; +import { Lock } from "@comet/admin-icons"; +import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; +import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { FormControlLabel, InputAdornment, MenuItem } from "@mui/material"; +import { FormApi } from "final-form"; +import { filter } from "graphql-anywhere"; +import isEqual from "lodash.isequal"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { validateTitle } from "../validateTitle"; +import { createProductMutation, productCategoriesQuery, productFormFragment, productQuery, updateProductMutation } from "./ProductForm.gql"; +import { + GQLCreateProductMutation, + GQLCreateProductMutationVariables, + GQLProductCategoriesSelectQuery, + GQLProductCategoriesSelectQueryVariables, + GQLProductCategorySelectFragment, + GQLProductFormDetailsFragment, + GQLProductQuery, + GQLProductQueryVariables, + GQLUpdateProductMutation, + GQLUpdateProductMutationVariables, +} from "./ProductForm.gql.generated"; + +const rootBlocks = { + image: DamImageBlock, +}; + +type FormValues = Omit & { + price?: string; + image: BlockState; +}; + +interface FormProps { + id?: string; +} + +export function ProductForm({ id }: FormProps): React.ReactElement { + const client = useApolloClient(); + const mode = id ? "edit" : "add"; + const formApiRef = useFormApiRef(); + const stackSwitchApi = useStackSwitchApi(); + + const { data, error, loading, refetch } = useQuery( + productQuery, + id ? { variables: { id } } : { skip: true }, + ); + + const initialValues = React.useMemo>( + () => + data?.product + ? { + ...filter(productFormFragment, data.product), + price: data.product.price ? String(data.product.price) : undefined, + createdAt: data.product.createdAt ? new Date(data.product.createdAt) : undefined, + availableSince: data.product.availableSince ? new Date(data.product.availableSince) : undefined, + image: rootBlocks.image.input2State(data.product.image), + } + : { + inStock: false, + image: rootBlocks.image.defaultValues(), + }, + [data], + ); + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "product", id); + return resolveHasSaveConflict(data?.product.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); + const output = { + ...formValues, + category: formValues.category?.id, + price: formValues.price ? parseFloat(formValues.price) : null, + image: rootBlocks.image.state2Output(formValues.image), + }; + if (mode === "edit") { + if (!id) throw new Error(); + const { createdAt, ...updateInput } = output; + await client.mutate({ + mutation: updateProductMutation, + variables: { id, input: updateInput }, + }); + } else { + const { data: mutationResponse } = await client.mutate({ + mutation: createProductMutation, + variables: { input: output }, + }); + if (!event.navigatingBack) { + const id = mutationResponse?.createProduct.id; + if (id) { + setTimeout(() => { + stackSwitchApi.activatePage(`edit`, id); + }); + } + } + } + }; + + const categorySelectAsyncProps = useAsyncOptionsProps(async () => { + const result = await client.query({ + query: productCategoriesQuery, + }); + return result.data.productCategories.nodes; + }); + + if (error) throw error; + + if (loading) { + return ; + } + + return ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode={mode} + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + subscription={{}} + > + {() => ( + + {saveConflict.dialogs} + + } + validate={validateTitle} + /> + + } /> + + + + + } + fullWidth + name="createdAt" + component={FinalFormDatePicker} + label={} + /> + + } + /> + }> + {(props) => ( + + + + + + + + + + + + )} + + } + component={FinalFormSelect} + {...categorySelectAsyncProps} + getOptionLabel={(option: GQLProductCategorySelectFragment) => option.title} + /> + + } + helperText={} + /> + + {(props) => ( + } + control={} + /> + )} + + + } + /> + + {createFinalFormBlock(rootBlocks.image)} + + + + )} + + ); +} diff --git a/demo/admin/src/products/future/generated/ProductsGrid.tsx b/demo/admin/src/products/future/generated/ProductsGrid.tsx new file mode 100644 index 0000000000..c1a24f8d8a --- /dev/null +++ b/demo/admin/src/products/future/generated/ProductsGrid.tsx @@ -0,0 +1,223 @@ +// This file has been generated by comet admin-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { gql, useApolloClient, useQuery } from "@apollo/client"; +import { + CrudContextMenu, + GridFilterButton, + MainContent, + muiGridFilterToGql, + muiGridSortToGql, + StackLink, + Toolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarFillSpace, + ToolbarItem, + useBufferedRowCount, + useDataGridRemote, + usePersistentColumnState, +} from "@comet/admin"; +import { Add as AddIcon, Edit } from "@comet/admin-icons"; +import { DamImageBlock } from "@comet/cms-admin"; +import { Button, IconButton } from "@mui/material"; +import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; +import { GQLProductFilter } from "@src/graphql.generated"; +import { filter as filterByFragment } from "graphql-anywhere"; +import * as React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { + GQLCreateProductMutation, + GQLCreateProductMutationVariables, + GQLDeleteProductMutation, + GQLDeleteProductMutationVariables, + GQLProductsGridFutureFragment, + GQLProductsGridQuery, + GQLProductsGridQueryVariables, +} from "./ProductsGrid.generated"; + +const productsFragment = gql` + fragment ProductsGridFuture on Product { + id + inStock + title + description + price + type + availableSince + createdAt + } +`; + +const productsQuery = gql` + query ProductsGrid($offset: Int, $limit: Int, $sort: [ProductSort!], $search: String, $filter: ProductFilter) { + products(offset: $offset, limit: $limit, sort: $sort, search: $search, filter: $filter) { + nodes { + ...ProductsGridFuture + } + totalCount + } + } + ${productsFragment} +`; + +const deleteProductMutation = gql` + mutation DeleteProduct($id: ID!) { + deleteProduct(id: $id) + } +`; + +const createProductMutation = gql` + mutation CreateProduct($input: ProductInput!) { + createProduct(input: $input) { + id + } + } +`; + +function ProductsGridToolbar() { + return ( + + + + + + + + + + + + + + ); +} + +type Props = { + filter?: GQLProductFilter; +}; + +export function ProductsGrid({ filter }: Props): React.ReactElement { + const client = useApolloClient(); + const intl = useIntl(); + const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; + + const columns: GridColDef[] = [ + { field: "inStock", headerName: intl.formatMessage({ id: "product.inStock", defaultMessage: "In stock" }), type: "boolean", width: 90 }, + { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), flex: 1, maxWidth: 250, minWidth: 200 }, + { + field: "description", + headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), + flex: 1, + minWidth: 150, + }, + { + field: "price", + headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), + type: "number", + flex: 1, + maxWidth: 150, + minWidth: 150, + }, + { + field: "type", + headerName: intl.formatMessage({ id: "product.type", defaultMessage: "Type" }), + type: "singleSelect", + valueOptions: [ + { value: "Cap", label: intl.formatMessage({ id: "product.type.cap", defaultMessage: "Cap" }) }, + { value: "Shirt", label: intl.formatMessage({ id: "product.type.shirt", defaultMessage: "Shirt" }) }, + { value: "Tie", label: intl.formatMessage({ id: "product.type.tie", defaultMessage: "Tie" }) }, + ], + flex: 1, + maxWidth: 150, + minWidth: 150, + }, + { + field: "availableSince", + headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), + type: "date", + valueGetter: ({ row }) => row.availableSince && new Date(row.availableSince), + width: 140, + }, + { + field: "createdAt", + headerName: intl.formatMessage({ id: "product.createdAt", defaultMessage: "Created At" }), + type: "dateTime", + valueGetter: ({ row }) => row.createdAt && new Date(row.createdAt), + width: 170, + }, + { + field: "actions", + headerName: "", + sortable: false, + filterable: false, + type: "actions", + align: "right", + renderCell: (params) => { + return ( + <> + + + + { + // Don't copy id, because we want to create a new entity with this data + const { id, ...filteredData } = filterByFragment(productsFragment, params.row); + return { + ...filteredData, + image: DamImageBlock.state2Output(DamImageBlock.input2State(filteredData.image)), + }; + }} + onPaste={async ({ input }) => { + await client.mutate({ + mutation: createProductMutation, + variables: { input }, + }); + }} + onDelete={async () => { + await client.mutate({ + mutation: deleteProductMutation, + variables: { id: params.row.id }, + }); + }} + refetchQueries={[productsQuery]} + /> + + ); + }, + }, + ]; + + const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel); + + const { data, loading, error } = useQuery(productsQuery, { + variables: { + filter: { and: [gqlFilter, ...(filter ? [filter] : [])] }, + search: gqlSearch, + offset: dataGridProps.page * dataGridProps.pageSize, + limit: dataGridProps.pageSize, + sort: muiGridSortToGql(dataGridProps.sortModel), + }, + }); + const rowCount = useBufferedRowCount(data?.products.totalCount); + if (error) throw error; + const rows = data?.products.nodes ?? []; + + return ( + + + + ); +} diff --git a/demo/admin/src/products/future/validateTitle.tsx b/demo/admin/src/products/future/validateTitle.tsx new file mode 100644 index 0000000000..f507bb005b --- /dev/null +++ b/demo/admin/src/products/future/validateTitle.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +export function validateTitle(value: string) { + return value.length < 3 ? ( + + ) : undefined; +} diff --git a/demo/admin/src/products/generated/ProductForm.gql.tsx b/demo/admin/src/products/generated/ProductForm.gql.ts similarity index 91% rename from demo/admin/src/products/generated/ProductForm.gql.tsx rename to demo/admin/src/products/generated/ProductForm.gql.ts index c3bafd48a6..3b851ef4af 100644 --- a/demo/admin/src/products/generated/ProductForm.gql.tsx +++ b/demo/admin/src/products/generated/ProductForm.gql.ts @@ -6,11 +6,13 @@ import { gql } from "@apollo/client"; export const productFormFragment = gql` fragment ProductForm on Product { title + status slug description type price inStock + availableSince image } `; @@ -46,8 +48,8 @@ export const createProductMutation = gql` `; export const updateProductMutation = gql` - mutation UpdateProduct($id: ID!, $input: ProductUpdateInput!, $lastUpdatedAt: DateTime) { - updateProduct(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + mutation UpdateProduct($id: ID!, $input: ProductUpdateInput!) { + updateProduct(id: $id, input: $input) { id updatedAt ...ProductForm diff --git a/demo/admin/src/products/generated/ProductForm.tsx b/demo/admin/src/products/generated/ProductForm.tsx index 8dccbd0ce3..b1869d5a08 100644 --- a/demo/admin/src/products/generated/ProductForm.tsx +++ b/demo/admin/src/products/generated/ProductForm.tsx @@ -12,6 +12,7 @@ import { FinalFormSubmitEvent, Loading, MainContent, + TextField, Toolbar, ToolbarActions, ToolbarFillSpace, @@ -21,6 +22,7 @@ import { useStackApi, useStackSwitchApi, } from "@comet/admin"; +import { DateField } from "@comet/admin-date-time"; import { ArrowLeft } from "@comet/admin-icons"; import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; @@ -110,15 +112,15 @@ export function ProductForm({ id }: FormProps): React.ReactElement { } await client.mutate({ mutation: updateProductMutation, - variables: { id, input: output, lastUpdatedAt: data?.product?.updatedAt }, + variables: { id, input: output }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: createProductMutation, variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProduct.id; + const id = mutationResponse?.createProduct.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage("edit", id); @@ -135,15 +137,7 @@ export function ProductForm({ id }: FormProps): React.ReactElement { } return ( - - apiRef={formApiRef} - onSubmit={handleSubmit} - mode={mode} - initialValues={initialValues} - onAfterSubmit={(values, form) => { - //don't go back automatically - }} - > + apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}> {({ values }) => ( {saveConflict.dialogs} @@ -158,29 +152,28 @@ export function ProductForm({ id }: FormProps): React.ReactElement { - + - } - /> - } - /> - } /> + }> + {(props) => ( + + + + + + + + + )} + + } /> + } /> }> @@ -213,6 +206,11 @@ export function ProductForm({ id }: FormProps): React.ReactElement { /> )} + } + /> {createFinalFormBlock(rootBlocks.image)} diff --git a/demo/admin/src/products/generated/ProductsGrid.tsx b/demo/admin/src/products/generated/ProductsGrid.tsx index cd43612f0c..d01e43bbb1 100644 --- a/demo/admin/src/products/generated/ProductsGrid.tsx +++ b/demo/admin/src/products/generated/ProductsGrid.tsx @@ -1,6 +1,6 @@ // This file has been generated by comet admin-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. -import { useApolloClient, useQuery } from "@apollo/client"; +import { gql, useApolloClient, useQuery } from "@apollo/client"; import { CrudContextMenu, GridFilterButton, @@ -22,7 +22,6 @@ import { BlockPreviewContent } from "@comet/blocks-admin"; import { DamImageBlock } from "@comet/cms-admin"; import { Button, IconButton } from "@mui/material"; import { DataGridPro, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid-pro"; -import gql from "graphql-tag"; import * as React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -39,17 +38,18 @@ import { const productsFragment = gql` fragment ProductsList on Product { id - updatedAt title - visible + status slug description type price inStock soldCount + availableSince image createdAt + updatedAt } `; @@ -105,15 +105,17 @@ export function ProductsGrid(): React.ReactElement { const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; const columns: GridColDef[] = [ + { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Title" }), width: 150 }, { - field: "updatedAt", - headerName: intl.formatMessage({ id: "product.updatedAt", defaultMessage: "Updated At" }), - type: "dateTime", - valueGetter: ({ value }) => value && new Date(value), + field: "status", + headerName: intl.formatMessage({ id: "product.status", defaultMessage: "Status" }), + type: "singleSelect", + valueOptions: [ + { value: "Published", label: intl.formatMessage({ id: "product.status.published", defaultMessage: "Published" }) }, + { value: "Unpublished", label: intl.formatMessage({ id: "product.status.unpublished", defaultMessage: "Unpublished" }) }, + ], width: 150, }, - { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Title" }), width: 150 }, - { field: "visible", headerName: intl.formatMessage({ id: "product.visible", defaultMessage: "Visible" }), type: "boolean", width: 150 }, { field: "slug", headerName: intl.formatMessage({ id: "product.slug", defaultMessage: "Slug" }), width: 150 }, { field: "description", headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), width: 150 }, { @@ -130,6 +132,13 @@ export function ProductsGrid(): React.ReactElement { { field: "price", headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), type: "number", width: 150 }, { field: "inStock", headerName: intl.formatMessage({ id: "product.inStock", defaultMessage: "In Stock" }), type: "boolean", width: 150 }, { field: "soldCount", headerName: intl.formatMessage({ id: "product.soldCount", defaultMessage: "Sold Count" }), type: "number", width: 150 }, + { + field: "availableSince", + headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), + type: "dateTime", + valueGetter: ({ value }) => value && new Date(value), + width: 150, + }, { field: "image", headerName: intl.formatMessage({ id: "product.image", defaultMessage: "Image" }), @@ -147,6 +156,13 @@ export function ProductsGrid(): React.ReactElement { valueGetter: ({ value }) => value && new Date(value), width: 150, }, + { + field: "updatedAt", + headerName: intl.formatMessage({ id: "product.updatedAt", defaultMessage: "Updated At" }), + type: "dateTime", + valueGetter: ({ value }) => value && new Date(value), + width: 150, + }, { field: "actions", headerName: "", @@ -164,11 +180,13 @@ export function ProductsGrid(): React.ReactElement { const row = params.row; return { title: row.title, + status: row.status, slug: row.slug, description: row.description, type: row.type, price: row.price, inStock: row.inStock, + availableSince: row.availableSince, image: DamImageBlock.state2Output(DamImageBlock.input2State(row.image)), }; }} diff --git a/demo/admin/src/products/tags/ProductTagForm.gql.ts b/demo/admin/src/products/tags/ProductTagForm.gql.ts index 4176eb847b..e9fdad6870 100644 --- a/demo/admin/src/products/tags/ProductTagForm.gql.ts +++ b/demo/admin/src/products/tags/ProductTagForm.gql.ts @@ -37,8 +37,8 @@ export const createProductTagMutation = gql` `; export const updateProductTagMutation = gql` - mutation ProductTagFormUpdateProductTag($id: ID!, $input: ProductTagUpdateInput!, $lastUpdatedAt: DateTime) { - updateProductTag(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + mutation ProductTagFormUpdateProductTag($id: ID!, $input: ProductTagUpdateInput!) { + updateProductTag(id: $id, input: $input) { id updatedAt ...ProductTagForm diff --git a/demo/admin/src/products/tags/ProductTagForm.tsx b/demo/admin/src/products/tags/ProductTagForm.tsx index 21704a7e61..cda49e8e30 100644 --- a/demo/admin/src/products/tags/ProductTagForm.tsx +++ b/demo/admin/src/products/tags/ProductTagForm.tsx @@ -2,10 +2,10 @@ import { useApolloClient, useQuery } from "@apollo/client"; import { Field, FinalForm, - FinalFormInput, FinalFormSaveSplitButton, FinalFormSubmitEvent, MainContent, + TextField, Toolbar, ToolbarActions, ToolbarFillSpace, @@ -92,10 +92,10 @@ function ProductTagForm({ id }: FormProps): React.ReactElement { if (!id) throw new Error(); await client.mutate({ mutation: updateProductTagMutation, - variables: { id, input: output, lastUpdatedAt: data?.productTag.updatedAt }, + variables: { id, input: output }, }); } else { - const { data: mutationReponse } = await client.mutate< + const { data: mutationResponse } = await client.mutate< GQLProductTagFormCreateProductTagMutation, GQLProductTagFormCreateProductTagMutationVariables >({ @@ -103,7 +103,7 @@ function ProductTagForm({ id }: FormProps): React.ReactElement { variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProductTag.id; + const id = mutationResponse?.createProductTag.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage(`edit`, id); @@ -122,16 +122,7 @@ function ProductTagForm({ id }: FormProps): React.ReactElement { } return ( - - apiRef={formApiRef} - onSubmit={handleSubmit} - mode={mode} - initialValues={initialValues} - onAfterSubmit={(values, form) => { - //don't go back automatically TODO remove this automatismn - }} - subscription={{}} - > + apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues} subscription={{}}> {() => ( {saveConflict.dialogs} @@ -154,17 +145,11 @@ function ProductTagForm({ id }: FormProps): React.ReactElement { - + - } - /> + } /> )} diff --git a/demo/admin/src/theme.ts b/demo/admin/src/theme.ts index 62add536d5..7326dcf750 100644 --- a/demo/admin/src/theme.ts +++ b/demo/admin/src/theme.ts @@ -1,10 +1,4 @@ import { createCometTheme } from "@comet/admin-theme"; import type {} from "@mui/lab/themeAugmentation"; -import { Theme } from "@mui/material"; export default createCometTheme(); - -declare module "@mui/styles/defaultTheme" { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface DefaultTheme extends Theme {} -} diff --git a/demo/admin/src/userGroups/UserGroupContextMenuItem.tsx b/demo/admin/src/userGroups/UserGroupContextMenuItem.tsx index d2be7f8cea..600ba891ff 100644 --- a/demo/admin/src/userGroups/UserGroupContextMenuItem.tsx +++ b/demo/admin/src/userGroups/UserGroupContextMenuItem.tsx @@ -1,4 +1,4 @@ -import { Field, FinalFormSelect, messages } from "@comet/admin"; +import { messages, SelectField } from "@comet/admin"; import { Account } from "@comet/admin-icons"; import { Button, Dialog, DialogActions, DialogContent, DialogTitle, ListItemIcon, MenuItem } from "@mui/material"; import { GQLUserGroup } from "@src/graphql.generated"; @@ -62,17 +62,13 @@ function UserGroupContextMenuItem({ item, onChange, onMenuClose }: Props): JSX.E {({ handleSubmit }) => (
- - {(props) => ( - - {userGroupOptions.map((option) => ( - - {option.label} - - ))} - - )} - + + {userGroupOptions.map((option) => ( + + {option.label} + + ))} +