Skip to content

Commit

Permalink
Add CrudVisibility component for implementing visibility column in a …
Browse files Browse the repository at this point in the history
…Crud Grid
  • Loading branch information
nsams committed Jun 13, 2023
1 parent 1cfeffc commit 2559ff7
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-pots-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@comet/admin": minor
---

Add CrudVisibility component for implementing visibility column in a Crud Grid
177 changes: 107 additions & 70 deletions demo/admin/src/products/ProductsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from "@apollo/client";
import { useApolloClient, useQuery } from "@apollo/client";
import {
CrudContextMenu,
CrudVisibility,
GridFilterButton,
muiGridFilterToGql,
muiGridSortToGql,
Expand Down Expand Up @@ -31,6 +32,8 @@ import {
GQLProductsListFragment,
GQLProductsListQuery,
GQLProductsListQueryVariables,
GQLUpdateProductVisibilityMutation,
GQLUpdateProductVisibilityMutationVariables,
} from "./ProductsTable.generated";

function ProductsTableToolbar() {
Expand All @@ -53,80 +56,104 @@ function ProductsTableToolbar() {
);
}

const columns: GridColDef<GQLProductsListFragment>[] = [
{
field: "title",
headerName: "Title",
width: 150,
renderHeader: () => (
<div style={{ display: "flex", alignItems: "center" }}>
<Typography fontWeight={400} fontSize={14}>
Title
</Typography>
<Tooltip
trigger="click"
title={<FormattedMessage id="comet.products.productTitle.info" defaultMessage="The title/name of the product" />}
>
<IconButton>
<Info />
</IconButton>
</Tooltip>
</div>
),
},
{ field: "description", headerName: "Description", width: 150 },
{ field: "price", headerName: "Price", width: 150, type: "number" },
{ field: "type", headerName: "Type", width: 150, type: "singleSelect", valueOptions: ["Cap", "Shirt", "Tie"] },
{ field: "inStock", headerName: "In Stock", width: 50, type: "boolean" },
{
field: "action",
headerName: "",
sortable: false,
filterable: false,
renderCell: (params) => {
return (
<>
<IconButton component={StackLink} pageName="edit" payload={params.row.id}>
<Edit color="primary" />
</IconButton>
<CrudContextMenu
onPaste={async ({ input, client }) => {
await client.mutate<GQLCreateProductMutation, GQLCreateProductMutationVariables>({
mutation: createProductMutation,
variables: {
input: {
description: input.description,
// @ts-expect-error type mismatch between OneOfBlock block data and block state
image: DamImageBlock.state2Output(DamImageBlock.input2State(input.image)),
inStock: input.inStock,
price: input.price,
slug: input.slug,
title: input.title,
type: input.type,
},
function ProductsTable() {
const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") };
const sortModel = dataGridProps.sortModel;
const client = useApolloClient();

const columns: GridColDef<GQLProductsListFragment>[] = [
{
field: "title",
headerName: "Title",
width: 150,
renderHeader: () => (
<div style={{ display: "flex", alignItems: "center" }}>
<Typography fontWeight={400} fontSize={14}>
Title
</Typography>
<Tooltip
trigger="click"
title={<FormattedMessage id="comet.products.productTitle.info" defaultMessage="The title/name of the product" />}
>
<IconButton>
<Info />
</IconButton>
</Tooltip>
</div>
),
},
{ field: "description", headerName: "Description", width: 150 },
{ field: "price", headerName: "Price", width: 150, type: "number" },
{ field: "type", headerName: "Type", width: 150, type: "singleSelect", valueOptions: ["Cap", "Shirt", "Tie"] },
{ field: "inStock", headerName: "In Stock", width: 50, type: "boolean" },
{
field: "visible",
headerName: "Visible",
width: 100,
type: "boolean",
renderCell: (params) => {
return (
<CrudVisibility
visibility={params.row.visible}
onUpdateVisibility={async (visible) => {
await client.mutate<GQLUpdateProductVisibilityMutation, GQLUpdateProductVisibilityMutationVariables>({
mutation: updateProductVisibilityMutation,
variables: { id: params.row.id, visible },
optimisticResponse: {
__typename: "Mutation",
updateProductVisibility: { __typename: "Product", id: params.row.id, visible },
},
});
}}
onDelete={async ({ client }) => {
await client.mutate<GQLDeleteProductMutation, GQLDeleteProductMutationVariables>({
mutation: deleteProductMutation,
variables: { id: params.row.id },
});
}}
refetchQueries={["ProductsList"]}
copyData={() => {
return filter<GQLProductsListFragment>(productsFragment, params.row);
}}
/>
</>
);
);
},
},
},
];

function ProductsTable() {
const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") };
const sortModel = dataGridProps.sortModel;
{
field: "action",
headerName: "",
sortable: false,
filterable: false,
renderCell: (params) => {
return (
<>
<IconButton component={StackLink} pageName="edit" payload={params.row.id}>
<Edit color="primary" />
</IconButton>
<CrudContextMenu
onPaste={async ({ input }) => {
await client.mutate<GQLCreateProductMutation, GQLCreateProductMutationVariables>({
mutation: createProductMutation,
variables: {
input: {
description: input.description,
// @ts-expect-error type mismatch between OneOfBlock block data and block state
image: DamImageBlock.state2Output(DamImageBlock.input2State(input.image)),
inStock: input.inStock,
price: input.price,
slug: input.slug,
title: input.title,
type: input.type,
},
},
});
}}
onDelete={async () => {
await client.mutate<GQLDeleteProductMutation, GQLDeleteProductMutationVariables>({
mutation: deleteProductMutation,
variables: { id: params.row.id },
});
}}
refetchQueries={["ProductsList"]}
copyData={() => {
return filter<GQLProductsListFragment>(productsFragment, params.row);
}}
/>
</>
);
},
},
];

const { data, loading, error } = useQuery<GQLProductsListQuery, GQLProductsListQueryVariables>(productsQuery, {
variables: {
Expand Down Expand Up @@ -167,6 +194,7 @@ const productsFragment = gql`
type
inStock
image
visible
}
`;

Expand Down Expand Up @@ -197,4 +225,13 @@ const createProductMutation = gql`
}
`;

const updateProductVisibilityMutation = gql`
mutation UpdateProductVisibility($id: ID!, $visible: Boolean!) {
updateProductVisibility(id: $id, visible: $visible) {
id
visible
}
}
`;

export default ProductsTable;
5 changes: 4 additions & 1 deletion demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ type Mutation {
updatePageTreeNodeSlug(id: ID!, slug: String!): PageTreeNode!
updatePageTreeNodeVisibility(id: ID!, input: PageTreeNodeUpdateVisibilityInput!): PageTreeNode!
updateProduct(id: ID!, input: ProductInput!, lastUpdatedAt: DateTime): Product!
updateProductVisibility(id: ID!, visible: Boolean!): Product!
updateRedirect(id: ID!, input: RedirectInput!, lastUpdatedAt: DateTime): Redirect!
updateRedirectActiveness(id: ID!, input: RedirectUpdateActivenessInput!): Redirect!
}
Expand Down Expand Up @@ -435,7 +436,6 @@ input NewsInput {
image: DamImageBlockInput!
slug: String!
title: String!
visible: Boolean!
}

input NewsSort {
Expand Down Expand Up @@ -606,6 +606,7 @@ type Product implements DocumentInterface {
title: String!
type: ProductType!
updatedAt: DateTime!
visible: Boolean!
}

input ProductFilter {
Expand All @@ -619,6 +620,7 @@ input ProductFilter {
title: StringFilter
type: ProductTypeEnumFilter
updatedAt: DateFilter
visible: BooleanFilter
}

input ProductInput {
Expand All @@ -645,6 +647,7 @@ enum ProductSortField {
title
type
updatedAt
visible
}

enum ProductType {
Expand Down
15 changes: 15 additions & 0 deletions demo/api/src/db/migrations/Migration20230515065558.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20230515065558 extends Migration {

async up(): Promise<void> {
this.addSql('alter table "Product" add column "visible" boolean null;');
this.addSql('update "Product" set "visible"=true;');
this.addSql('alter table "Product" alter column "visible" set not null;');
}

async down(): Promise<void> {
this.addSql('alter table "Product" drop column "visible";');
}

}
7 changes: 1 addition & 6 deletions demo/api/src/news/generated/dto/news.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BlockInputInterface, isBlockInputInterface } from "@comet/blocks-api";
import { DamImageBlock, IsSlug, RootBlockInputScalar } from "@comet/cms-api";
import { Field, InputType } from "@nestjs/graphql";
import { Transform } from "class-transformer";
import { IsBoolean, IsDate, IsEnum, IsNotEmpty, IsString, ValidateNested } from "class-validator";
import { IsDate, IsEnum, IsNotEmpty, IsString, ValidateNested } from "class-validator";

import { NewsContentBlock } from "../../blocks/news-content.block";
import { NewsCategory } from "../../entities/news.entity";
Expand Down Expand Up @@ -32,11 +32,6 @@ export class NewsInput {
@Field(() => NewsCategory)
category: NewsCategory;

@IsNotEmpty()
@IsBoolean()
@Field()
visible: boolean;

@IsNotEmpty()
@Field(() => RootBlockInputScalar(DamImageBlock))
@Transform(({ value }) => (isBlockInputInterface(value) ? value : DamImageBlock.blockInputFactory(value)), { toClassOnly: true })
Expand Down
4 changes: 4 additions & 0 deletions demo/api/src/products/entities/product.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export class Product extends BaseEntity<Product, "id"> implements DocumentInterf
})
title: string;

@Property()
@Field()
visible: boolean;

@Property()
@Field()
slug: string;
Expand Down
6 changes: 6 additions & 0 deletions demo/api/src/products/generated/dto/product.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export class ProductFilter {
@Type(() => StringFilter)
title?: StringFilter;

@Field(() => BooleanFilter, { nullable: true })
@ValidateNested()
@IsOptional()
@Type(() => BooleanFilter)
visible?: BooleanFilter;

@Field(() => StringFilter, { nullable: true })
@ValidateNested()
@IsOptional()
Expand Down
1 change: 1 addition & 0 deletions demo/api/src/products/generated/dto/product.sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IsEnum } from "class-validator";

export enum ProductSortField {
title = "title",
visible = "visible",
slug = "slug",
description = "description",
type = "type",
Expand Down
17 changes: 17 additions & 0 deletions demo/api/src/products/generated/product.crud.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class ProductCrudResolver {
const product = this.repository.create({
...input,
image: input.image.transformToBlockData(),
visible: false,
});

await this.entityManager.flush();
Expand Down Expand Up @@ -92,4 +93,20 @@ export class ProductCrudResolver {
await this.entityManager.flush();
return true;
}

@Mutation(() => Product)
@SubjectEntity(Product)
async updateProductVisibility(
@Args("id", { type: () => ID }) id: string,
@Args("visible", { type: () => Boolean }) visible: boolean,
): Promise<Product> {
const product = await this.repository.findOneOrFail(id);

product.assign({
visible,
});
await this.entityManager.flush();

return product;
}
}
Loading

0 comments on commit 2559ff7

Please sign in to comment.