diff --git a/demo/admin/crud-generator-config.ts b/demo/admin/crud-generator-config.ts index f0d7356725..1b71110f32 100644 --- a/demo/admin/crud-generator-config.ts +++ b/demo/admin/crud-generator-config.ts @@ -5,8 +5,4 @@ export default [ target: "src/products/generated", entityName: "Product", }, - { - target: "src/news/generated", - entityName: "News", - }, ] satisfies CrudGeneratorConfig[]; diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx index 0ff1b45532..d1b64136d7 100644 --- a/demo/admin/src/common/MasterMenu.tsx +++ b/demo/admin/src/common/MasterMenu.tsx @@ -19,7 +19,7 @@ import { PredefinedPage } from "@src/documents/predefinedPages/PredefinedPage"; 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 { NewsPage } from "@src/news/NewsPage"; import MainMenu from "@src/pages/mainMenu/MainMenu"; import { Page } from "@src/pages/Page"; import { categoryToUrlParam, pageTreeCategories, urlParamToCategory } from "@src/pageTree/pageTreeCategories"; diff --git a/demo/admin/src/news/NewsForm.cometGen.ts b/demo/admin/src/news/NewsForm.cometGen.ts new file mode 100644 index 0000000000..2076f679f5 --- /dev/null +++ b/demo/admin/src/news/NewsForm.cometGen.ts @@ -0,0 +1,48 @@ +import { future_FormConfig as FormConfig } from "@comet/cms-admin"; +import { GQLNews } from "@src/graphql.generated"; + +export const NewsForm: FormConfig = { + type: "form", + gqlType: "News", + fragmentName: "NewsForm", + fields: [ + { + type: "text", + name: "slug", + label: "Slug", + required: true, + }, + { + type: "text", + name: "title", + label: "Title", + required: true, + }, + { + type: "date", + name: "date", + label: "Date", + required: true, + }, + { + type: "staticSelect", + name: "category", + label: "Category", + required: true, + inputType: "radio", + values: ["Events", "Company", "Awards"], + }, + { + type: "block", + name: "image", + label: "Image", + block: { name: "DamImageBlock", import: "@comet/cms-admin" }, + }, + { + type: "block", + name: "content", + label: "Content", + block: { name: "NewsContentBlock", import: "../blocks/NewsContentBlock" }, + }, + ], +}; diff --git a/demo/admin/src/news/NewsGrid.cometGen.ts b/demo/admin/src/news/NewsGrid.cometGen.ts new file mode 100644 index 0000000000..bd3adbe514 --- /dev/null +++ b/demo/admin/src/news/NewsGrid.cometGen.ts @@ -0,0 +1,38 @@ +import { future_GridConfig as GridConfig } from "@comet/cms-admin"; +import { GQLNews } from "@src/graphql.generated"; + +export const NewsGrid: GridConfig = { + type: "grid", + gqlType: "News", + fragmentName: "NewsGrid", + columns: [ + { + type: "text", + name: "title", + headerName: "Title", + }, + { + type: "date", + name: "date", + headerName: "Date", + }, + { + type: "staticSelect", + name: "category", + headerName: "Category", + values: ["Events", "Company", "Awards"], + }, + { + type: "block", + name: "image", + headerName: "Image", + block: { name: "DamImageBlock", import: "@comet/cms-admin" }, + }, + { + type: "block", + name: "content", + headerName: "Content", + block: { name: "NewsContentBlock", import: "../blocks/NewsContentBlock" }, + }, + ], +}; diff --git a/demo/admin/src/news/NewsPage.tsx b/demo/admin/src/news/NewsPage.tsx new file mode 100644 index 0000000000..2a5816529a --- /dev/null +++ b/demo/admin/src/news/NewsPage.tsx @@ -0,0 +1,65 @@ +import { + MainContent, + SaveBoundary, + SaveBoundarySaveButton, + Stack, + StackPage, + StackSwitch, + StackToolbar, + ToolbarActions, + ToolbarAutomaticTitleItem, + ToolbarBackButton, + ToolbarFillSpace, +} from "@comet/admin"; +import { ContentScopeIndicator } from "@comet/cms-admin"; +import { useContentScope } from "@src/common/ContentScopeProvider"; +import { useIntl } from "react-intl"; + +import { NewsForm } from "./generated/NewsForm"; +import { NewsGrid } from "./generated/NewsGrid"; + +const FormToolbar = () => ( + }> + + + + + + + +); + +export function NewsPage() { + const intl = useIntl(); + const { scope } = useContentScope(); + return ( + + + + } /> + + + + + + {(selectedNewsId) => ( + + + + + + + )} + + + + + + + + + + + + ); +} diff --git a/demo/admin/src/news/generated/NewsForm.gql.ts b/demo/admin/src/news/generated/NewsForm.gql.tsx similarity index 80% rename from demo/admin/src/news/generated/NewsForm.gql.ts rename to demo/admin/src/news/generated/NewsForm.gql.tsx index 1bd9172e08..c675e18777 100644 --- a/demo/admin/src/news/generated/NewsForm.gql.ts +++ b/demo/admin/src/news/generated/NewsForm.gql.tsx @@ -1,22 +1,19 @@ // 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 newsFormFragment = gql` fragment NewsForm on News { slug title - status date category image content } `; - -export const newsFormQuery = gql` - query NewsForm($id: ID!) { +export const newsQuery = gql` + query News($id: ID!) { news(id: $id) { id updatedAt @@ -25,15 +22,6 @@ export const newsFormQuery = gql` } ${newsFormFragment} `; - -export const newsFormCheckForChangesQuery = gql` - query NewsFormCheckForChanges($id: ID!) { - news(id: $id) { - updatedAt - } - } -`; - export const createNewsMutation = gql` mutation CreateNews($scope: NewsContentScopeInput!, $input: NewsInput!) { createNews(scope: $scope, input: $input) { @@ -44,7 +32,6 @@ export const createNewsMutation = gql` } ${newsFormFragment} `; - export const updateNewsMutation = gql` mutation UpdateNews($id: ID!, $input: NewsUpdateInput!) { updateNews(id: $id, input: $input) { diff --git a/demo/admin/src/news/generated/NewsForm.tsx b/demo/admin/src/news/generated/NewsForm.tsx index b3d1196f8f..bce42af277 100644 --- a/demo/admin/src/news/generated/NewsForm.tsx +++ b/demo/admin/src/news/generated/NewsForm.tsx @@ -1,45 +1,34 @@ // 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, filterByFragment, FinalForm, - FinalFormSaveButton, - FinalFormSelect, FinalFormSubmitEvent, Loading, - MainContent, + RadioGroupField, TextField, - Toolbar, - ToolbarActions, - ToolbarFillSpace, - ToolbarItem, - ToolbarTitleItem, useFormApiRef, - useStackApi, useStackSwitchApi, } from "@comet/admin"; -import { DateField } from "@comet/admin-date-time"; -import { ArrowLeft } from "@comet/admin-icons"; +import { FinalFormDatePicker } from "@comet/admin-date-time"; import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; -import { ContentScopeIndicator, DamImageBlock, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; -import { IconButton, MenuItem } from "@mui/material"; -import { useContentScope } from "@src/common/ContentScopeProvider"; +import { DamImageBlock, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { GQLNewsContentScopeInput } from "@src/graphql.generated"; import { FormApi } from "final-form"; import isEqual from "lodash.isequal"; import React from "react"; import { FormattedMessage } from "react-intl"; import { NewsContentBlock } from "../blocks/NewsContentBlock"; -import { createNewsMutation, newsFormFragment, newsFormQuery, updateNewsMutation } from "./NewsForm.gql"; +import { createNewsMutation, newsFormFragment, newsQuery, updateNewsMutation } from "./NewsForm.gql"; import { GQLCreateNewsMutation, GQLCreateNewsMutationVariables, GQLNewsFormFragment, - GQLNewsFormQuery, - GQLNewsFormQueryVariables, + GQLNewsQuery, + GQLNewsQueryVariables, GQLUpdateNewsMutation, GQLUpdateNewsMutationVariables, } from "./NewsForm.gql.generated"; @@ -56,27 +45,23 @@ type FormValues = GQLNewsFormFragment & { interface FormProps { id?: string; + scope: GQLNewsContentScopeInput; } -export function NewsForm({ id }: FormProps): React.ReactElement { - const stackApi = useStackApi(); +export function NewsForm({ id, scope }: FormProps): React.ReactElement { const client = useApolloClient(); const mode = id ? "edit" : "add"; const formApiRef = useFormApiRef(); const stackSwitchApi = useStackSwitchApi(); - const { scope } = useContentScope(); - const { data, error, loading, refetch } = useQuery( - newsFormQuery, - id ? { variables: { id } } : { skip: true }, - ); + const { data, error, loading, refetch } = useQuery(newsQuery, id ? { variables: { id } } : { skip: true }); const initialValues = React.useMemo>( () => data?.news ? { ...filterByFragment(newsFormFragment, data.news), - + date: data.news.date ? new Date(data.news.date) : undefined, image: rootBlocks.image.input2State(data.news.image), content: rootBlocks.content.input2State(data.news.content), } @@ -98,36 +83,30 @@ export function NewsForm({ id }: FormProps): React.ReactElement { }, }); - const handleSubmit = async (state: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { - if (await saveConflict.checkForConflicts()) { - throw new Error("Conflicts detected"); - } - + const handleSubmit = async (formValues: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { - ...state, - - image: rootBlocks.image.state2Output(state.image), - content: rootBlocks.content.state2Output(state.content), + ...formValues, + image: rootBlocks.image.state2Output(formValues.image), + content: rootBlocks.content.state2Output(formValues.content), }; - if (mode === "edit") { - if (!id) { - throw new Error("Missing id in edit mode"); - } + if (!id) throw new Error(); + const { ...updateInput } = output; await client.mutate({ mutation: updateNewsMutation, - variables: { id, input: output }, + variables: { id, input: updateInput }, }); } else { const { data: mutationResponse } = await client.mutate({ mutation: createNewsMutation, - variables: { scope, input: output }, + variables: { input: output, scope }, }); if (!event.navigatingBack) { const id = mutationResponse?.createNews.id; if (id) { setTimeout(() => { - stackSwitchApi.activatePage("edit", id); + stackSwitchApi.activatePage(`edit`, id); }); } } @@ -141,25 +120,18 @@ export function NewsForm({ id }: FormProps): React.ReactElement { } return ( - apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}> - {({ values }) => ( + + apiRef={formApiRef} + onSubmit={handleSubmit} + mode={mode} + initialValues={initialValues} + initialValuesEqual={isEqual} //required to compare block data correctly + subscription={{}} + > + {() => ( <> {saveConflict.dialogs} - }> - - - - - - - - - - - - - - + <> } /> + } /> - }> - {(props) => ( - - - - - - - - - )} - - } /> - } + options={[ + { + label: , + value: "Events", + }, + { + label: , + value: "Company", + }, + { + label: , + value: "Awards", + }, + ]} + /> + } + variant="horizontal" + fullWidth > - {(props) => ( - - - - - - - - - - - - )} - - {createFinalFormBlock(rootBlocks.image)} - + } + variant="horizontal" + fullWidth + > {createFinalFormBlock(rootBlocks.content)} - + )} diff --git a/demo/admin/src/news/generated/NewsGrid.tsx b/demo/admin/src/news/generated/NewsGrid.tsx index 929bccb852..123fd54a8b 100644 --- a/demo/admin/src/news/generated/NewsGrid.tsx +++ b/demo/admin/src/news/generated/NewsGrid.tsx @@ -4,11 +4,12 @@ import { gql, useApolloClient, useQuery } from "@apollo/client"; import { CrudContextMenu, DataGridToolbar, + filterByFragment, GridColDef, GridFilterButton, - MainContent, muiGridFilterToGql, muiGridSortToGql, + renderStaticSelectCell, StackLink, ToolbarActions, ToolbarFillSpace, @@ -17,7 +18,7 @@ import { useDataGridRemote, usePersistentColumnState, } from "@comet/admin"; -import { Add as AddIcon, Edit } from "@comet/admin-icons"; +import { Add as AddIcon, Edit as EditIcon } from "@comet/admin-icons"; import { BlockPreviewContent } from "@comet/blocks-admin"; import { DamImageBlock } from "@comet/cms-admin"; import { Button, IconButton } from "@mui/material"; @@ -32,31 +33,27 @@ import { GQLCreateNewsMutationVariables, GQLDeleteNewsMutation, GQLDeleteNewsMutationVariables, + GQLNewsGridFragment, GQLNewsGridQuery, GQLNewsGridQueryVariables, - GQLNewsListFragment, } from "./NewsGrid.generated"; const newsFragment = gql` - fragment NewsList on News { + fragment NewsGrid on News { id - slug title - status date category image content - createdAt - updatedAt } `; const newsQuery = gql` - query NewsGrid($offset: Int, $limit: Int, $sort: [NewsSort!], $search: String, $filter: NewsFilter, $scope: NewsContentScopeInput!) { + query NewsGrid($offset: Int!, $limit: Int!, $sort: [NewsSort!], $search: String, $filter: NewsFilter, $scope: NewsContentScopeInput!) { newsList(offset: $offset, limit: $limit, sort: $sort, search: $search, filter: $filter, scope: $scope) { nodes { - ...NewsList + ...NewsGrid } totalCount } @@ -103,70 +100,61 @@ export function NewsGrid(): React.ReactElement { const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("NewsGrid") }; 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: "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, - }, + const columns: GridColDef[] = [ + { field: "title", headerName: intl.formatMessage({ id: "news.title", defaultMessage: "Title" }), flex: 1, minWidth: 150 }, { field: "date", headerName: intl.formatMessage({ id: "news.date", defaultMessage: "Date" }), - type: "dateTime", - valueGetter: ({ value }) => value && new Date(value), - width: 150, + type: "date", + valueGetter: ({ row }) => row.date && new Date(row.date), + valueFormatter: ({ value }) => (value ? intl.formatDate(value) : ""), + flex: 1, + minWidth: 150, }, { field: "category", headerName: intl.formatMessage({ id: "news.category", defaultMessage: "Category" }), type: "singleSelect", + valueFormatter: ({ value }) => value?.toString(), valueOptions: [ - { value: "Events", label: intl.formatMessage({ id: "news.category.events", defaultMessage: "Events" }) }, - { value: "Company", label: intl.formatMessage({ id: "news.category.company", defaultMessage: "Company" }) }, - { value: "Awards", label: intl.formatMessage({ id: "news.category.awards", defaultMessage: "Awards" }) }, + { + value: "Events", + label: intl.formatMessage({ id: "news.category.events", defaultMessage: "Events" }), + }, + { + value: "Company", + label: intl.formatMessage({ id: "news.category.company", defaultMessage: "Company" }), + }, + { + value: "Awards", + label: intl.formatMessage({ id: "news.category.awards", defaultMessage: "Awards" }), + }, ], - width: 150, + renderCell: renderStaticSelectCell, + flex: 1, + minWidth: 150, }, { field: "image", headerName: intl.formatMessage({ id: "news.image", defaultMessage: "Image" }), filterable: false, sortable: false, - width: 150, renderCell: (params) => { return ; }, + flex: 1, + minWidth: 150, }, { field: "content", headerName: intl.formatMessage({ id: "news.content", defaultMessage: "Content" }), filterable: false, sortable: false, - width: 150, renderCell: (params) => { return ; }, - }, - { - field: "createdAt", - headerName: intl.formatMessage({ id: "news.createdAt", defaultMessage: "Created At" }), - type: "dateTime", - 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, + flex: 1, + minWidth: 150, }, { field: "actions", @@ -174,23 +162,23 @@ export function NewsGrid(): React.ReactElement { sortable: false, filterable: false, type: "actions", + align: "right", + pinned: "right", + width: 84, renderCell: (params) => { return ( <> - - + + { - const row = params.row; + // Don't copy id, because we want to create a new entity with this data + const { id, ...filteredData } = filterByFragment(newsFragment, params.row); return { - slug: row.slug, - title: row.title, - status: row.status, - date: row.date, - category: row.category, - image: DamImageBlock.state2Output(DamImageBlock.input2State(row.image)), - content: NewsContentBlock.state2Output(NewsContentBlock.input2State(row.content)), + ...filteredData, + image: DamImageBlock.state2Output(DamImageBlock.input2State(filteredData.image)), + content: NewsContentBlock.state2Output(NewsContentBlock.input2State(filteredData.content)), }; }} onPaste={async ({ input }) => { @@ -230,18 +218,16 @@ export function NewsGrid(): React.ReactElement { const rows = data?.newsList.nodes ?? []; return ( - - - + ); } diff --git a/demo/admin/src/news/generated/NewsPage.tsx b/demo/admin/src/news/generated/NewsPage.tsx deleted file mode 100644 index be3a73e2f1..0000000000 --- a/demo/admin/src/news/generated/NewsPage.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// 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 { Stack, StackPage, StackSwitch, StackToolbar } from "@comet/admin"; -import { ContentScopeIndicator } from "@comet/cms-admin"; -import * as React from "react"; -import { useIntl } from "react-intl"; - -import { NewsForm } from "./NewsForm"; -import { NewsGrid } from "./NewsGrid"; - -export function NewsPage(): React.ReactElement { - const intl = useIntl(); - return ( - - - - } /> - - - - {(selectedId) => } - - - - - - - ); -}