Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update predefined pages #2550

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions demo/admin/src/predefinedPage/EditPredefinedPage.gql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { gql } from "@apollo/client";

export const predefinedPageFormFragment = gql`
fragment PredefinedPageForm on PredefinedPage {
type
}
`;

export const predefinedPageQuery = gql`
query PredefinedPage($pageTreeNodeId: ID!) {
pageTreeNode(id: $pageTreeNodeId) {
id
document {
__typename
... on DocumentInterface {
id
updatedAt
}
... on PredefinedPage {
...PredefinedPageForm
}
}
}
}
${predefinedPageFormFragment}
`;

export const savePredefinedPageMutation = gql`
mutation SavePredefinedPage($id: ID!, $input: PredefinedPageInput!, $lastUpdatedAt: DateTime, $attachedPageTreeNodeId: ID!) {
savePredefinedPage(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt, attachedPageTreeNodeId: $attachedPageTreeNodeId) {
id
updatedAt
...PredefinedPageForm
}
}
${predefinedPageFormFragment}
`;
179 changes: 101 additions & 78 deletions demo/admin/src/predefinedPage/EditPredefinedPage.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,127 @@
import { gql, useMutation, useQuery } from "@apollo/client";
import { FinalForm, FinalFormSaveButton, Loading, MainContent, SelectField, Toolbar, ToolbarFillSpace, ToolbarItem, useStackApi } from "@comet/admin";
import { useApolloClient, useQuery } from "@apollo/client";
import {
FinalForm,
FinalFormSaveButton,
Loading,
MainContent,
SelectField,
Toolbar,
ToolbarFillSpace,
ToolbarItem,
useFormApiRef,
useStackApi,
} from "@comet/admin";
import { ArrowLeft } from "@comet/admin-icons";
import { PageName } from "@comet/cms-admin";
import { IconButton, MenuItem } from "@mui/material";
import { ContentScopeIndicator, PageName, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin";
import { IconButton } from "@mui/material";
import { GQLPredefinedPageType } from "@src/graphql.generated";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { v4 as uuid } from "uuid";

import { predefinedPageQuery, savePredefinedPageMutation } from "./EditPredefinedPage.gql";
import {
GQLPredefinedPageQuery,
GQLPredefinedPageQueryVariables,
GQLUpdatePredefinedPageMutation,
GQLUpdatePredefinedPageMutationVariables,
} from "./EditPredefinedPage.generated";

const getQuery = gql`
query PredefinedPage($id: ID!) {
page: pageTreeNode(id: $id) {
id
name
slug
parentId
document {
__typename
... on DocumentInterface {
id
updatedAt
}
... on PredefinedPage {
id
type
}
}
}
}
`;
GQLSavePredefinedPageMutation,
GQLSavePredefinedPageMutationVariables,
} from "./EditPredefinedPage.gql.generated";
import { predefinedPageLabels } from "./predefinedPageLabels";

const updateMutation = gql`
mutation UpdatePredefinedPage($pageId: ID!, $input: PredefinedPageInput!, $lastUpdatedAt: DateTime, $attachedPageTreeNodeId: ID!) {
savePredefinedPage(id: $pageId, input: $input, lastUpdatedAt: $lastUpdatedAt, attachedPageTreeNodeId: $attachedPageTreeNodeId) {
id
type
}
}
`;
type FormValues = {
type?: GQLPredefinedPageType;
};

interface Props {
id: string;
}

export const EditPredefinedPage = ({ id }: Props) => {
export const EditPredefinedPage = ({ id: pageTreeNodeId }: Props) => {
const stackApi = useStackApi();
const client = useApolloClient();
const formApiRef = useFormApiRef<FormValues>();

const { data, loading } = useQuery<GQLPredefinedPageQuery, GQLPredefinedPageQueryVariables>(getQuery, {
variables: { id },
const { data, error, loading, refetch } = useQuery<GQLPredefinedPageQuery, GQLPredefinedPageQueryVariables>(predefinedPageQuery, {
variables: { pageTreeNodeId },
});

const [mutation] = useMutation<GQLUpdatePredefinedPageMutation, GQLUpdatePredefinedPageMutationVariables>(updateMutation, {
refetchQueries: ["PredefinedPage"],
const initialValues = useMemo<Partial<FormValues>>(() => {
if (data?.pageTreeNode?.document) {
if (data.pageTreeNode.document.__typename !== "PredefinedPage") {
throw new Error(`Invalid document type: ${data.pageTreeNode.document.__typename}`);
}

return {
type: data.pageTreeNode.document.type ?? undefined,
};
nsams marked this conversation as resolved.
Show resolved Hide resolved
}

return {};
}, [data]);

const saveConflict = useFormSaveConflict({
checkConflict: async () => {
const updatedAt = await queryUpdatedAt(client, "predefinedPage", data?.pageTreeNode?.document?.id);
return resolveHasSaveConflict(data?.pageTreeNode?.document?.updatedAt, updatedAt);
},
formApiRef,
loadLatestVersion: async () => {
await refetch();
},
});

const handleSubmit = async (formValues: FormValues) => {
if (await saveConflict.checkForConflicts()) {
throw new Error("Conflicts detected");
}

const output = {
type: formValues.type ?? null,
};

const id = data?.pageTreeNode?.document?.id ?? uuid();

await client.mutate<GQLSavePredefinedPageMutation, GQLSavePredefinedPageMutationVariables>({
mutation: savePredefinedPageMutation,
variables: { id, input: output, attachedPageTreeNodeId: pageTreeNodeId },
});
};

if (error) {
throw error;
}

if (loading) {
return <Loading behavior="fillPageHeight" />;
}

return (
<FinalForm
mode="edit"
initialValues={{ type: data?.page?.document?.__typename === "PredefinedPage" ? data.page.document.type : undefined }}
onSubmit={() => {
mutation({ variables: { pageId: id, input: { type: "News" }, attachedPageTreeNodeId: id } });
}}
>
{({ pristine, hasValidationErrors, submitting, handleSubmit, hasSubmitErrors }) => {
return (
<>
<Toolbar>
<ToolbarItem>
<IconButton onClick={stackApi?.goBack}>
<ArrowLeft />
</IconButton>
</ToolbarItem>
<PageName pageId={id} />
<ToolbarFillSpace />
<ToolbarItem>
<FinalFormSaveButton />
</ToolbarItem>
</Toolbar>
<MainContent>
<SelectField label={<FormattedMessage id="structuredContent.type" defaultMessage="Type" />} name="type" fullWidth>
{predefinedPageOptions.map((item, index) => (
<MenuItem value={item.value} key={index}>
{item.name}
</MenuItem>
))}
</SelectField>
</MainContent>
</>
);
}}
<FinalForm apiRef={formApiRef} onSubmit={handleSubmit} mode="edit" initialValues={initialValues}>
{() => (
<>
{saveConflict.dialogs}
<Toolbar scopeIndicator={<ContentScopeIndicator />}>
<ToolbarItem>
<IconButton onClick={stackApi?.goBack}>
<ArrowLeft />
</IconButton>
</ToolbarItem>
<PageName pageId={pageTreeNodeId} />
<ToolbarFillSpace />
<ToolbarItem>
<FinalFormSaveButton hasConflict={saveConflict.hasConflict} />
</ToolbarItem>
</Toolbar>
<MainContent>
<SelectField
name="type"
label={<FormattedMessage id="predefinedPages.type.label" defaultMessage="Type" />}
options={[{ value: "News", label: predefinedPageLabels.News }]}
fullWidth
/>
</MainContent>
</>
)}
</FinalForm>
);
};

const predefinedPageOptions = [{ value: "News", name: <FormattedMessage id="structuredContent.news" defaultMessage="News" /> }];
6 changes: 4 additions & 2 deletions demo/admin/src/predefinedPage/PredefinedPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useQuery } from "@apollo/client";
import { FileData, FileDataNotMenu } from "@comet/admin-icons";
import { DocumentInterface } from "@comet/cms-admin";
import { Chip } from "@mui/material";
import { GQLPredefinedPage, GQLPredefinedPageInput } from "@src/graphql.generated";
import { EditPredefinedPage } from "@src/predefinedPage/EditPredefinedPage";
import gql from "graphql-tag";
import { FormattedMessage } from "react-intl";

import { GQLPredefinedPageInfoTagQuery, GQLPredefinedPageInfoTagQueryVariables } from "./PredefinedPage.generated";
import { predefinedPageLabels } from "./predefinedPageLabels";

const predefinedPageInfoTagQuery = gql`
query PredefinedPageInfoTag($id: ID!) {
Expand All @@ -23,7 +25,7 @@ const predefinedPageInfoTagQuery = gql`
`;

export const PredefinedPage: DocumentInterface<Pick<GQLPredefinedPage, "type">, GQLPredefinedPageInput> = {
displayName: <FormattedMessage id="predefinedPage" defaultMessage="Predefined Page" />,
displayName: <FormattedMessage id="predefinedPages.displayName" defaultMessage="Predefined Page" />,
editComponent: EditPredefinedPage,
getQuery: gql`
query PredefinedPageDocument($id: ID!) {
Expand Down Expand Up @@ -64,7 +66,7 @@ export const PredefinedPage: DocumentInterface<Pick<GQLPredefinedPage, "type">,

if (data?.page?.document != null) {
const { type } = data.page.document as GQLPredefinedPage;
return type ? <>{type}</> : null;
return type ? <Chip label={predefinedPageLabels[type]} /> : null;
} else {
return null;
}
Expand Down
7 changes: 7 additions & 0 deletions demo/admin/src/predefinedPage/predefinedPageLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GQLPredefinedPageType } from "@src/graphql.generated";
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";

export const predefinedPageLabels: Record<GQLPredefinedPageType, ReactNode> = {
News: <FormattedMessage id="predefinedPages.news" defaultMessage="News" />,
};
11 changes: 7 additions & 4 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,11 @@ type PredefinedPage implements DocumentInterface {
id: ID!
updatedAt: DateTime!
createdAt: DateTime!
type: String
type: PredefinedPageType
}

enum PredefinedPageType {
News
}

type DamScope {
Expand Down Expand Up @@ -770,8 +774,7 @@ type Query {
topMenu(scope: PageTreeNodeScopeInput!): [PageTreeNode!]!
mainMenuItem(pageTreeNodeId: ID!): MainMenuItem!
footer(scope: FooterContentScopeInput!): Footer
predefinedPage(id: ID!): PredefinedPage
pageTreeNodeForPredefinedPage(type: String!, scope: PageTreeNodeScopeInput!): PageTreeNode
predefinedPage(id: ID!): PredefinedPage!
kubernetesCronJobs: [KubernetesCronJob!]!
kubernetesCronJob(name: String!): KubernetesCronJob!
kubernetesJobs(cronJobName: String!): [KubernetesJob!]!
Expand Down Expand Up @@ -1384,7 +1387,7 @@ input FooterInput {
scalar FooterContentBlockInput @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")

input PredefinedPageInput {
type: String
type: PredefinedPageType
}

input ProductInput {
Expand Down
10 changes: 6 additions & 4 deletions demo/api/src/predefined-page/dto/predefined-page.input.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Field, InputType } from "@nestjs/graphql";
import { IsOptional, IsString } from "class-validator";
import { IsEnum, IsOptional } from "class-validator";

import { PredefinedPageType } from "../entities/predefined-page.entity";

@InputType()
export class PredefinedPageInput {
@Field(() => String, { nullable: true })
@IsString()
@Field(() => PredefinedPageType, { nullable: true })
@IsEnum(PredefinedPageType)
@IsOptional()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nsams type can be both optional (not changed) and nullable (unset). Should I use @IsUndefinable and @IsNullable() instead? And should the TS type reflect that as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, @IsOptional should not be used anymore. And in that case @IsUndefinable and @IsNullable() together are correct.

And should the TS type reflect that as well?

yes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: be85243

type?: string;
type?: PredefinedPageType;
}
14 changes: 11 additions & 3 deletions demo/api/src/predefined-page/entities/predefined-page.entity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { DocumentInterface, PageTreeNodeDocumentEntityScopeService, ScopedEntity } from "@comet/cms-api";
import { BaseEntity, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { Field, ID, ObjectType } from "@nestjs/graphql";
import { Field, ID, ObjectType, registerEnumType } from "@nestjs/graphql";
import { v4 as uuid } from "uuid";

export enum PredefinedPageType {
News = "News",
}

registerEnumType(PredefinedPageType, {
name: "PredefinedPageType",
});

@Entity()
@ObjectType({
implements: () => [DocumentInterface],
Expand All @@ -29,6 +37,6 @@ export class PredefinedPage extends BaseEntity<PredefinedPage, "id"> implements
updatedAt: Date = new Date();

@Property({ columnType: "text", nullable: true })
@Field({ nullable: true })
type?: string;
@Field(() => PredefinedPageType, { nullable: true })
type?: PredefinedPageType;
}
4 changes: 1 addition & 3 deletions demo/api/src/predefined-page/predefined-page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import { PagesModule } from "@src/pages/pages.module";

import { PredefinedPage } from "./entities/predefined-page.entity";
import { PredefinedPageResolver } from "./predefined-page.resolver";
import { PredefinedPageService } from "./predefined-page.service";

@Module({
imports: [PagesModule, MikroOrmModule.forFeature([PredefinedPage])],
providers: [PredefinedPageResolver, PredefinedPageService],
exports: [PredefinedPageService],
providers: [PredefinedPageResolver],
})
export class PredefinedPageModule {}
Loading
Loading