From 859eed86605b4a90bf893444f9d32c1d52c8d226 Mon Sep 17 00:00:00 2001
From: raphaelblum <44967610+raphaelblum@users.noreply.github.com>
Date: Mon, 14 Oct 2024 08:50:38 +0200
Subject: [PATCH 1/8] COM-957: Add basic warning entity (#2503)
In the Warning Module there should be a warning entity where warnings
are saved.
Entries into this table should later be added with a validator or via a
block, maybe even manually.
This is the start of the central warning system in order to have a basic
table and to be able to start with the validator afterwards.
---
demo/admin/crud-generator-config.ts | 5 +
demo/admin/src/common/MasterMenu.tsx | 10 ++
demo/admin/src/warnings/WarningsGrid.tsx | 152 ++++++++++++++++++
demo/admin/src/warnings/WarningsPage.tsx | 14 ++
demo/api/schema.gql | 78 +++++++++
demo/api/src/app.module.ts | 2 +
.../db/migrations/Migration20241008102605.ts | 9 ++
.../entities/warning-severity.enum.ts | 10 ++
.../warnings/entities/warning-status.enum.ts | 10 ++
.../src/warnings/entities/warning.entity.ts | 43 +++++
.../generated/dto/paginated-warnings.ts | 9 ++
.../warnings/generated/dto/warning.filter.ts | 59 +++++++
.../warnings/generated/dto/warning.input.ts | 29 ++++
.../warnings/generated/dto/warning.sort.ts | 27 ++++
.../warnings/generated/dto/warnings.args.ts | 34 ++++
.../warnings/generated/warning.resolver.ts | 81 ++++++++++
demo/api/src/warnings/warning.module.ts | 11 ++
17 files changed, 583 insertions(+)
create mode 100644 demo/admin/src/warnings/WarningsGrid.tsx
create mode 100644 demo/admin/src/warnings/WarningsPage.tsx
create mode 100644 demo/api/src/db/migrations/Migration20241008102605.ts
create mode 100644 demo/api/src/warnings/entities/warning-severity.enum.ts
create mode 100644 demo/api/src/warnings/entities/warning-status.enum.ts
create mode 100644 demo/api/src/warnings/entities/warning.entity.ts
create mode 100644 demo/api/src/warnings/generated/dto/paginated-warnings.ts
create mode 100644 demo/api/src/warnings/generated/dto/warning.filter.ts
create mode 100644 demo/api/src/warnings/generated/dto/warning.input.ts
create mode 100644 demo/api/src/warnings/generated/dto/warning.sort.ts
create mode 100644 demo/api/src/warnings/generated/dto/warnings.args.ts
create mode 100644 demo/api/src/warnings/generated/warning.resolver.ts
create mode 100644 demo/api/src/warnings/warning.module.ts
diff --git a/demo/admin/crud-generator-config.ts b/demo/admin/crud-generator-config.ts
index 2b2b8f912f..ff6b3b7dbd 100644
--- a/demo/admin/crud-generator-config.ts
+++ b/demo/admin/crud-generator-config.ts
@@ -1,4 +1,5 @@
import { CrudGeneratorConfig } from "@comet/cms-admin";
+
export default [
{
target: "src/products/generated",
@@ -8,4 +9,8 @@ export default [
target: "src/news/generated",
entityName: "News",
},
+ {
+ target: "src/warnings/generated",
+ entityName: "Warning",
+ },
] satisfies CrudGeneratorConfig[];
diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx
index 3f6b15b1a7..562c8f6691 100644
--- a/demo/admin/src/common/MasterMenu.tsx
+++ b/demo/admin/src/common/MasterMenu.tsx
@@ -32,6 +32,7 @@ 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 { WarningsPage } from "@src/warnings/WarningsPage";
import { FormattedMessage } from "react-intl";
import { Redirect, RouteComponentProps } from "react-router-dom";
@@ -175,6 +176,15 @@ export const masterMenuData: MasterMenuData = [
},
requiredPermission: "pageTree",
},
+ {
+ type: "route",
+ primary: ,
+ route: {
+ path: "/system/warnings",
+ component: WarningsPage,
+ },
+ requiredPermission: "pageTree",
+ },
],
requiredPermission: "pageTree",
},
diff --git a/demo/admin/src/warnings/WarningsGrid.tsx b/demo/admin/src/warnings/WarningsGrid.tsx
new file mode 100644
index 0000000000..3303d83268
--- /dev/null
+++ b/demo/admin/src/warnings/WarningsGrid.tsx
@@ -0,0 +1,152 @@
+import { gql, useQuery } from "@apollo/client";
+import {
+ DataGridToolbar,
+ GridColDef,
+ GridFilterButton,
+ MainContent,
+ muiGridFilterToGql,
+ muiGridSortToGql,
+ ToolbarItem,
+ useBufferedRowCount,
+ useDataGridRemote,
+ usePersistentColumnState,
+} from "@comet/admin";
+import { WarningSolid } from "@comet/admin-icons";
+import { Chip } from "@mui/material";
+import { DataGrid, GridToolbarQuickFilter } from "@mui/x-data-grid";
+import { GQLWarningSeverity } from "@src/graphql.generated";
+import * as React from "react";
+import { FormattedDate, FormattedTime, useIntl } from "react-intl";
+
+import { GQLWarningsGridQuery, GQLWarningsGridQueryVariables, GQLWarningsListFragment } from "./WarningsGrid.generated";
+
+const warningsFragment = gql`
+ fragment WarningsList on Warning {
+ id
+ createdAt
+ updatedAt
+ type
+ severity
+ status
+ }
+`;
+
+const warningsQuery = gql`
+ query WarningsGrid($offset: Int, $limit: Int, $sort: [WarningSort!], $search: String, $filter: WarningFilter) {
+ warnings(offset: $offset, limit: $limit, sort: $sort, search: $search, filter: $filter) {
+ nodes {
+ ...WarningsList
+ }
+ totalCount
+ }
+ }
+ ${warningsFragment}
+`;
+
+function WarningsGridToolbar() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export function WarningsGrid(): React.ReactElement {
+ const intl = useIntl();
+ const dataGridProps = {
+ ...useDataGridRemote({ initialFilter: { items: [{ columnField: "state", operatorValue: "is", value: "open" }] } }),
+ ...usePersistentColumnState("WarningsGrid"),
+ };
+
+ const columns: GridColDef[] = [
+ {
+ field: "createdAt",
+ headerName: intl.formatMessage({ id: "warning.dateTime", defaultMessage: "Date / Time" }),
+ type: "dateTime",
+ renderCell: (params) => (
+ <>
+
+ >
+ ),
+ width: 200,
+ },
+ {
+ field: "severity",
+ headerName: intl.formatMessage({ id: "warning.severity", defaultMessage: "Severity" }),
+ type: "singleSelect",
+ valueOptions: [
+ { value: "critical", label: intl.formatMessage({ id: "warning.severity.critical", defaultMessage: "Critical" }) },
+ { value: "high", label: intl.formatMessage({ id: "warning.severity.high", defaultMessage: "High" }) },
+ { value: "low", label: intl.formatMessage({ id: "warning.severity.low", defaultMessage: "Low" }) },
+ ],
+ width: 150,
+ renderCell: (params) => {
+ const colorMapping: Record = {
+ critical: "error",
+ high: "warning",
+ low: "default",
+ };
+ return (
+ : undefined}
+ color={colorMapping[params.value as GQLWarningSeverity]}
+ label={params.value}
+ />
+ );
+ },
+ },
+ {
+ field: "type",
+ headerName: intl.formatMessage({ id: "warning.type", defaultMessage: "Type" }),
+ width: 150,
+ renderCell: (params) => ,
+ },
+ {
+ field: "status",
+ headerName: intl.formatMessage({ id: "warning.status", defaultMessage: "Status" }),
+ type: "singleSelect",
+ valueOptions: [
+ { value: "open", label: intl.formatMessage({ id: "warning.status.open", defaultMessage: "Open" }) },
+ { value: "resolved", label: intl.formatMessage({ id: "warning.status.resolved", defaultMessage: "Resolved" }) },
+ { value: "ignored", label: intl.formatMessage({ id: "warning.status.ignored", defaultMessage: "Ignored" }) },
+ ],
+ width: 150,
+ },
+ ];
+
+ const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel);
+
+ const { data, loading, error } = useQuery(warningsQuery, {
+ variables: {
+ filter: gqlFilter,
+ search: gqlSearch,
+ offset: dataGridProps.page * dataGridProps.pageSize,
+ limit: dataGridProps.pageSize,
+ sort: muiGridSortToGql(dataGridProps.sortModel),
+ },
+ });
+ const rowCount = useBufferedRowCount(data?.warnings.totalCount);
+ if (error) throw error;
+ const rows = data?.warnings.nodes ?? [];
+
+ return (
+
+
+
+ );
+}
diff --git a/demo/admin/src/warnings/WarningsPage.tsx b/demo/admin/src/warnings/WarningsPage.tsx
new file mode 100644
index 0000000000..f1880abe57
--- /dev/null
+++ b/demo/admin/src/warnings/WarningsPage.tsx
@@ -0,0 +1,14 @@
+import { Stack } from "@comet/admin";
+import * as React from "react";
+import { useIntl } from "react-intl";
+
+import { WarningsGrid } from "./WarningsGrid";
+
+export function WarningsPage(): React.ReactElement {
+ const intl = useIntl();
+ return (
+
+
+
+ );
+}
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index a87b1f2453..f9f30bec6a 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -605,6 +605,32 @@ type RedirectScope {
domain: String!
}
+type Warning {
+ id: ID!
+ createdAt: DateTime!
+ updatedAt: DateTime!
+ type: String!
+ severity: WarningSeverity!
+ status: WarningStatus!
+}
+
+enum WarningSeverity {
+ critical
+ high
+ low
+}
+
+enum WarningStatus {
+ open
+ resolved
+ ignored
+}
+
+type PaginatedWarnings {
+ nodes: [Warning!]!
+ totalCount: Int!
+}
+
type PaginatedPageTreeNodes {
nodes: [PageTreeNode!]!
totalCount: Int!
@@ -790,6 +816,8 @@ type Query {
manufacturers(offset: Int! = 0, limit: Int! = 25, search: String, filter: ManufacturerFilter, sort: [ManufacturerSort!]): PaginatedManufacturers!
manufacturerCountry(id: ID!): ManufacturerCountry!
manufacturerCountries(offset: Int! = 0, limit: Int! = 25, search: String, filter: ManufacturerCountryFilter): PaginatedManufacturerCountries!
+ warning(id: ID!): Warning!
+ warnings(offset: Int! = 0, limit: Int! = 25, status: [WarningStatus!]! = [open], search: String, filter: WarningFilter, sort: [WarningSort!]): PaginatedWarnings!
}
input UserFilter {
@@ -1149,6 +1177,41 @@ input ManufacturerCountryFilter {
or: [ManufacturerCountryFilter!]
}
+input WarningFilter {
+ createdAt: DateTimeFilter
+ updatedAt: DateTimeFilter
+ type: StringFilter
+ severity: WarningSeverityEnumFilter
+ status: WarningStatusEnumFilter
+ and: [WarningFilter!]
+ or: [WarningFilter!]
+}
+
+input WarningSeverityEnumFilter {
+ isAnyOf: [WarningSeverity!]
+ equal: WarningSeverity
+ notEqual: WarningSeverity
+}
+
+input WarningStatusEnumFilter {
+ isAnyOf: [WarningStatus!]
+ equal: WarningStatus
+ notEqual: WarningStatus
+}
+
+input WarningSort {
+ field: WarningSortField!
+ direction: SortDirection! = ASC
+}
+
+enum WarningSortField {
+ createdAt
+ updatedAt
+ type
+ severity
+ status
+}
+
type Mutation {
currentUserSignOut: String!
userPermissionsCreatePermission(userId: String!, input: UserPermissionInput!): UserPermission!
@@ -1209,6 +1272,9 @@ type Mutation {
createManufacturer(input: ManufacturerInput!): Manufacturer!
updateManufacturer(id: ID!, input: ManufacturerUpdateInput!): Manufacturer!
deleteManufacturer(id: ID!): Boolean!
+ createWarning(input: WarningInput!): Warning!
+ updateWarning(id: ID!, input: WarningUpdateInput!): Warning!
+ deleteWarning(id: ID!): Boolean!
}
input UserPermissionInput {
@@ -1498,3 +1564,15 @@ input ManufacturerUpdateInput {
address: AddressInput
addressAsEmbeddable: AddressAsEmbeddableInput
}
+
+input WarningInput {
+ type: String!
+ severity: WarningSeverity!
+ status: WarningStatus! = open
+}
+
+input WarningUpdateInput {
+ type: String
+ severity: WarningSeverity
+ status: WarningStatus
+}
diff --git a/demo/api/src/app.module.ts b/demo/api/src/app.module.ts
index 2973d172a5..949c0df48e 100644
--- a/demo/api/src/app.module.ts
+++ b/demo/api/src/app.module.ts
@@ -49,6 +49,7 @@ import { Page } from "./pages/entities/page.entity";
import { PredefinedPageModule } from "./predefined-page/predefined-page.module";
import { ProductsModule } from "./products/products.module";
import { RedirectScope } from "./redirects/dto/redirect-scope";
+import { WarningsModule } from "./warnings/warning.module";
@Module({})
export class AppModule {
@@ -170,6 +171,7 @@ export class AppModule {
},
}),
...(config.sentry ? [SentryModule.forRootAsync(config.sentry)] : []),
+ WarningsModule,
],
};
}
diff --git a/demo/api/src/db/migrations/Migration20241008102605.ts b/demo/api/src/db/migrations/Migration20241008102605.ts
new file mode 100644
index 0000000000..f1bf384f90
--- /dev/null
+++ b/demo/api/src/db/migrations/Migration20241008102605.ts
@@ -0,0 +1,9 @@
+import { Migration } from '@mikro-orm/migrations';
+
+export class Migration20241008102605 extends Migration {
+
+ async up(): Promise {
+ this.addSql('create table "Warning" ("id" uuid not null, "createdAt" timestamptz(0) not null, "updatedAt" timestamptz(0) not null, "type" varchar(255) not null, "severity" text check ("severity" in (\'critical\', \'high\', \'low\')) not null, "status" text check ("status" in (\'open\', \'resolved\', \'ignored\')) not null, constraint "Warning_pkey" primary key ("id"));');
+ }
+
+}
diff --git a/demo/api/src/warnings/entities/warning-severity.enum.ts b/demo/api/src/warnings/entities/warning-severity.enum.ts
new file mode 100644
index 0000000000..e00efb56d9
--- /dev/null
+++ b/demo/api/src/warnings/entities/warning-severity.enum.ts
@@ -0,0 +1,10 @@
+import { registerEnumType } from "@nestjs/graphql";
+
+export enum WarningSeverity {
+ critical = "critical",
+ high = "high",
+ low = "low",
+}
+registerEnumType(WarningSeverity, {
+ name: "WarningSeverity",
+});
diff --git a/demo/api/src/warnings/entities/warning-status.enum.ts b/demo/api/src/warnings/entities/warning-status.enum.ts
new file mode 100644
index 0000000000..ddd16d4262
--- /dev/null
+++ b/demo/api/src/warnings/entities/warning-status.enum.ts
@@ -0,0 +1,10 @@
+import { registerEnumType } from "@nestjs/graphql";
+
+export enum WarningStatus {
+ open = "open",
+ resolved = "resolved",
+ ignored = "ignored",
+}
+registerEnumType(WarningStatus, {
+ name: "WarningStatus",
+});
diff --git a/demo/api/src/warnings/entities/warning.entity.ts b/demo/api/src/warnings/entities/warning.entity.ts
new file mode 100644
index 0000000000..4e506205ca
--- /dev/null
+++ b/demo/api/src/warnings/entities/warning.entity.ts
@@ -0,0 +1,43 @@
+import { RootBlockEntity } from "@comet/blocks-api";
+import { CrudField, CrudGenerator } from "@comet/cms-api";
+import { BaseEntity, Entity, Enum, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 as uuid } from "uuid";
+
+import { WarningSeverity } from "./warning-severity.enum";
+import { WarningStatus } from "./warning-status.enum";
+
+@ObjectType()
+@Entity()
+@RootBlockEntity()
+@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: ["warnings"] })
+export class Warning extends BaseEntity {
+ [OptionalProps]?: "createdAt" | "updatedAt" | "status";
+
+ @PrimaryKey({ type: "uuid" })
+ @Field(() => ID)
+ id: string = uuid();
+
+ @Property()
+ @Field()
+ createdAt: Date = new Date();
+
+ @Property({ onUpdate: () => new Date() })
+ @Field()
+ updatedAt: Date = new Date();
+
+ @Property()
+ @Field()
+ @CrudField()
+ type: string;
+
+ @Enum({ items: () => WarningSeverity })
+ @Field(() => WarningSeverity)
+ severity: WarningSeverity;
+
+ // TODO: add blockInfos with COM-958
+
+ @Enum({ items: () => WarningStatus })
+ @Field(() => WarningStatus)
+ status: WarningStatus = WarningStatus.open;
+}
diff --git a/demo/api/src/warnings/generated/dto/paginated-warnings.ts b/demo/api/src/warnings/generated/dto/paginated-warnings.ts
new file mode 100644
index 0000000000..7c2cf70c67
--- /dev/null
+++ b/demo/api/src/warnings/generated/dto/paginated-warnings.ts
@@ -0,0 +1,9 @@
+// This file has been generated by comet api-generator.
+// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
+import { PaginatedResponseFactory } from "@comet/cms-api";
+import { ObjectType } from "@nestjs/graphql";
+
+import { Warning } from "../../entities/warning.entity";
+
+@ObjectType()
+export class PaginatedWarnings extends PaginatedResponseFactory.create(Warning) {}
diff --git a/demo/api/src/warnings/generated/dto/warning.filter.ts b/demo/api/src/warnings/generated/dto/warning.filter.ts
new file mode 100644
index 0000000000..1459ae24bf
--- /dev/null
+++ b/demo/api/src/warnings/generated/dto/warning.filter.ts
@@ -0,0 +1,59 @@
+// This file has been generated by comet api-generator.
+// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
+import { createEnumFilter, DateTimeFilter, StringFilter } from "@comet/cms-api";
+import { Field, InputType } from "@nestjs/graphql";
+import { Type } from "class-transformer";
+import { IsOptional, ValidateNested } from "class-validator";
+
+import { WarningSeverity } from "../../entities/warning-severity.enum";
+import { WarningStatus } from "../../entities/warning-status.enum";
+
+@InputType()
+class WarningSeverityEnumFilter extends createEnumFilter(WarningSeverity) {}
+@InputType()
+class WarningStatusEnumFilter extends createEnumFilter(WarningStatus) {}
+
+@InputType()
+export class WarningFilter {
+ @Field(() => DateTimeFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => DateTimeFilter)
+ createdAt?: DateTimeFilter;
+
+ @Field(() => DateTimeFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => DateTimeFilter)
+ updatedAt?: DateTimeFilter;
+
+ @Field(() => StringFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => StringFilter)
+ type?: StringFilter;
+
+ @Field(() => WarningSeverityEnumFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => WarningSeverityEnumFilter)
+ severity?: WarningSeverityEnumFilter;
+
+ @Field(() => WarningStatusEnumFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => WarningStatusEnumFilter)
+ status?: WarningStatusEnumFilter;
+
+ @Field(() => [WarningFilter], { nullable: true })
+ @Type(() => WarningFilter)
+ @ValidateNested({ each: true })
+ @IsOptional()
+ and?: WarningFilter[];
+
+ @Field(() => [WarningFilter], { nullable: true })
+ @Type(() => WarningFilter)
+ @ValidateNested({ each: true })
+ @IsOptional()
+ or?: WarningFilter[];
+}
diff --git a/demo/api/src/warnings/generated/dto/warning.input.ts b/demo/api/src/warnings/generated/dto/warning.input.ts
new file mode 100644
index 0000000000..87c05b14b0
--- /dev/null
+++ b/demo/api/src/warnings/generated/dto/warning.input.ts
@@ -0,0 +1,29 @@
+// This file has been generated by comet api-generator.
+// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
+import { PartialType } from "@comet/cms-api";
+import { Field, InputType } from "@nestjs/graphql";
+import { IsEnum, IsNotEmpty, IsString } from "class-validator";
+
+import { WarningSeverity } from "../../entities/warning-severity.enum";
+import { WarningStatus } from "../../entities/warning-status.enum";
+
+@InputType()
+export class WarningInput {
+ @IsNotEmpty()
+ @IsString()
+ @Field()
+ type: string;
+
+ @IsNotEmpty()
+ @IsEnum(WarningSeverity)
+ @Field(() => WarningSeverity)
+ severity: WarningSeverity;
+
+ @IsNotEmpty()
+ @IsEnum(WarningStatus)
+ @Field(() => WarningStatus, { defaultValue: WarningStatus.open })
+ status: WarningStatus;
+}
+
+@InputType()
+export class WarningUpdateInput extends PartialType(WarningInput) {}
diff --git a/demo/api/src/warnings/generated/dto/warning.sort.ts b/demo/api/src/warnings/generated/dto/warning.sort.ts
new file mode 100644
index 0000000000..827d87a132
--- /dev/null
+++ b/demo/api/src/warnings/generated/dto/warning.sort.ts
@@ -0,0 +1,27 @@
+// This file has been generated by comet api-generator.
+// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
+import { SortDirection } from "@comet/cms-api";
+import { Field, InputType, registerEnumType } from "@nestjs/graphql";
+import { IsEnum } from "class-validator";
+
+export enum WarningSortField {
+ createdAt = "createdAt",
+ updatedAt = "updatedAt",
+ type = "type",
+ severity = "severity",
+ status = "status",
+}
+registerEnumType(WarningSortField, {
+ name: "WarningSortField",
+});
+
+@InputType()
+export class WarningSort {
+ @Field(() => WarningSortField)
+ @IsEnum(WarningSortField)
+ field: WarningSortField;
+
+ @Field(() => SortDirection, { defaultValue: SortDirection.ASC })
+ @IsEnum(SortDirection)
+ direction: SortDirection = SortDirection.ASC;
+}
diff --git a/demo/api/src/warnings/generated/dto/warnings.args.ts b/demo/api/src/warnings/generated/dto/warnings.args.ts
new file mode 100644
index 0000000000..8992ebeb75
--- /dev/null
+++ b/demo/api/src/warnings/generated/dto/warnings.args.ts
@@ -0,0 +1,34 @@
+// This file has been generated by comet api-generator.
+// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
+import { OffsetBasedPaginationArgs } from "@comet/cms-api";
+import { ArgsType, Field } from "@nestjs/graphql";
+import { Type } from "class-transformer";
+import { IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
+
+import { WarningStatus } from "../../entities/warning-status.enum";
+import { WarningFilter } from "./warning.filter";
+import { WarningSort } from "./warning.sort";
+
+@ArgsType()
+export class WarningsArgs extends OffsetBasedPaginationArgs {
+ @Field(() => [WarningStatus], { defaultValue: [WarningStatus.open] })
+ @IsEnum(WarningStatus, { each: true })
+ status: WarningStatus[];
+
+ @Field({ nullable: true })
+ @IsOptional()
+ @IsString()
+ search?: string;
+
+ @Field(() => WarningFilter, { nullable: true })
+ @ValidateNested()
+ @Type(() => WarningFilter)
+ @IsOptional()
+ filter?: WarningFilter;
+
+ @Field(() => [WarningSort], { nullable: true })
+ @ValidateNested({ each: true })
+ @Type(() => WarningSort)
+ @IsOptional()
+ sort?: WarningSort[];
+}
diff --git a/demo/api/src/warnings/generated/warning.resolver.ts b/demo/api/src/warnings/generated/warning.resolver.ts
new file mode 100644
index 0000000000..7f37964b1a
--- /dev/null
+++ b/demo/api/src/warnings/generated/warning.resolver.ts
@@ -0,0 +1,81 @@
+// This file has been generated by comet api-generator.
+// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
+import { AffectedEntity, gqlArgsToMikroOrmQuery, RequiredPermission } from "@comet/cms-api";
+import { FindOptions } from "@mikro-orm/core";
+import { InjectRepository } from "@mikro-orm/nestjs";
+import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
+import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql";
+
+import { Warning } from "../entities/warning.entity";
+import { PaginatedWarnings } from "./dto/paginated-warnings";
+import { WarningInput, WarningUpdateInput } from "./dto/warning.input";
+import { WarningsArgs } from "./dto/warnings.args";
+
+@Resolver(() => Warning)
+@RequiredPermission(["warnings"], { skipScopeCheck: true })
+export class WarningResolver {
+ constructor(private readonly entityManager: EntityManager, @InjectRepository(Warning) private readonly repository: EntityRepository) {}
+
+ @Query(() => Warning)
+ @AffectedEntity(Warning)
+ async warning(@Args("id", { type: () => ID }) id: string): Promise {
+ const warning = await this.repository.findOneOrFail(id);
+ return warning;
+ }
+
+ @Query(() => PaginatedWarnings)
+ async warnings(@Args() { status, search, filter, sort, offset, limit }: WarningsArgs): Promise {
+ const where = gqlArgsToMikroOrmQuery({ search, filter }, this.repository);
+ where.status = { $in: status };
+
+ const options: FindOptions = { offset, limit };
+
+ if (sort) {
+ options.orderBy = sort.map((sortItem) => {
+ return {
+ [sortItem.field]: sortItem.direction,
+ };
+ });
+ }
+
+ const [entities, totalCount] = await this.repository.findAndCount(where, options);
+ return new PaginatedWarnings(entities, totalCount);
+ }
+
+ @Mutation(() => Warning)
+ async createWarning(@Args("input", { type: () => WarningInput }) input: WarningInput): Promise {
+ const warning = this.repository.create({
+ ...input,
+ });
+
+ await this.entityManager.flush();
+
+ return warning;
+ }
+
+ @Mutation(() => Warning)
+ @AffectedEntity(Warning)
+ async updateWarning(
+ @Args("id", { type: () => ID }) id: string,
+ @Args("input", { type: () => WarningUpdateInput }) input: WarningUpdateInput,
+ ): Promise {
+ const warning = await this.repository.findOneOrFail(id);
+
+ warning.assign({
+ ...input,
+ });
+
+ await this.entityManager.flush();
+
+ return warning;
+ }
+
+ @Mutation(() => Boolean)
+ @AffectedEntity(Warning)
+ async deleteWarning(@Args("id", { type: () => ID }) id: string): Promise {
+ const warning = await this.repository.findOneOrFail(id);
+ this.entityManager.remove(warning);
+ await this.entityManager.flush();
+ return true;
+ }
+}
diff --git a/demo/api/src/warnings/warning.module.ts b/demo/api/src/warnings/warning.module.ts
new file mode 100644
index 0000000000..3be79caff9
--- /dev/null
+++ b/demo/api/src/warnings/warning.module.ts
@@ -0,0 +1,11 @@
+import { MikroOrmModule } from "@mikro-orm/nestjs";
+import { Module } from "@nestjs/common";
+
+import { Warning } from "./entities/warning.entity";
+import { WarningResolver } from "./generated/warning.resolver";
+
+@Module({
+ imports: [MikroOrmModule.forFeature([Warning])],
+ providers: [WarningResolver],
+})
+export class WarningsModule {}
From 7679b96a9aeba359e22970015a61953b2cd10ef3 Mon Sep 17 00:00:00 2001
From: raphaelblum <44967610+raphaelblum@users.noreply.github.com>
Date: Wed, 19 Feb 2025 12:11:55 +0100
Subject: [PATCH 2/8] Warning Module: Add console job that creates warnings
from blocks (#2656)
---
demo/admin/src/warnings/WarningsGrid.tsx | 6 +
demo/api/schema.gql | 5 +
.../db/migrations/Migration20241008102605.ts | 9 --
.../db/migrations/Migration20241108073817.ts | 9 ++
.../src/warnings/entities/warning.entity.ts | 5 +
.../warnings/generated/dto/warning.filter.ts | 6 +
.../warnings/generated/dto/warning.input.ts | 5 +
.../warnings/generated/dto/warning.sort.ts | 1 +
.../src/warnings/warning-checker.console.ts | 125 ++++++++++++++++++
demo/api/src/warnings/warning.module.ts | 6 +-
packages/api/blocks-api/src/blocks/block.ts | 19 +++
packages/api/blocks-api/src/index.ts | 1 +
.../api/cms-api/src/blocks/createSeoBlock.ts | 9 ++
13 files changed, 195 insertions(+), 11 deletions(-)
delete mode 100644 demo/api/src/db/migrations/Migration20241008102605.ts
create mode 100644 demo/api/src/db/migrations/Migration20241108073817.ts
create mode 100644 demo/api/src/warnings/warning-checker.console.ts
diff --git a/demo/admin/src/warnings/WarningsGrid.tsx b/demo/admin/src/warnings/WarningsGrid.tsx
index 3303d83268..d9569c791b 100644
--- a/demo/admin/src/warnings/WarningsGrid.tsx
+++ b/demo/admin/src/warnings/WarningsGrid.tsx
@@ -25,6 +25,7 @@ const warningsFragment = gql`
id
createdAt
updatedAt
+ message
type
severity
status
@@ -106,6 +107,11 @@ export function WarningsGrid(): React.ReactElement {
width: 150,
renderCell: (params) => ,
},
+ {
+ field: "message",
+ headerName: intl.formatMessage({ id: "warning.message", defaultMessage: "Message" }),
+ flex: 1,
+ },
{
field: "status",
headerName: intl.formatMessage({ id: "warning.status", defaultMessage: "Status" }),
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index f9f30bec6a..bc1d76cad4 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -609,6 +609,7 @@ type Warning {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
+ message: String!
type: String!
severity: WarningSeverity!
status: WarningStatus!
@@ -1180,6 +1181,7 @@ input ManufacturerCountryFilter {
input WarningFilter {
createdAt: DateTimeFilter
updatedAt: DateTimeFilter
+ message: StringFilter
type: StringFilter
severity: WarningSeverityEnumFilter
status: WarningStatusEnumFilter
@@ -1207,6 +1209,7 @@ input WarningSort {
enum WarningSortField {
createdAt
updatedAt
+ message
type
severity
status
@@ -1566,12 +1569,14 @@ input ManufacturerUpdateInput {
}
input WarningInput {
+ message: String!
type: String!
severity: WarningSeverity!
status: WarningStatus! = open
}
input WarningUpdateInput {
+ message: String
type: String
severity: WarningSeverity
status: WarningStatus
diff --git a/demo/api/src/db/migrations/Migration20241008102605.ts b/demo/api/src/db/migrations/Migration20241008102605.ts
deleted file mode 100644
index f1bf384f90..0000000000
--- a/demo/api/src/db/migrations/Migration20241008102605.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Migration } from '@mikro-orm/migrations';
-
-export class Migration20241008102605 extends Migration {
-
- async up(): Promise {
- this.addSql('create table "Warning" ("id" uuid not null, "createdAt" timestamptz(0) not null, "updatedAt" timestamptz(0) not null, "type" varchar(255) not null, "severity" text check ("severity" in (\'critical\', \'high\', \'low\')) not null, "status" text check ("status" in (\'open\', \'resolved\', \'ignored\')) not null, constraint "Warning_pkey" primary key ("id"));');
- }
-
-}
diff --git a/demo/api/src/db/migrations/Migration20241108073817.ts b/demo/api/src/db/migrations/Migration20241108073817.ts
new file mode 100644
index 0000000000..479d1d3406
--- /dev/null
+++ b/demo/api/src/db/migrations/Migration20241108073817.ts
@@ -0,0 +1,9 @@
+import { Migration } from '@mikro-orm/migrations';
+
+export class Migration20241108073817 extends Migration {
+
+ async up(): Promise {
+ this.addSql('create table "Warning" ("id" uuid not null, "createdAt" timestamptz(0) not null, "updatedAt" timestamptz(0) not null, "message" text not null, "type" varchar(255) not null, "severity" text check ("severity" in (\'critical\', \'high\', \'low\')) not null, "status" text check ("status" in (\'open\', \'resolved\', \'ignored\')) not null default \'open\', constraint "Warning_pkey" primary key ("id"));');
+ }
+
+}
diff --git a/demo/api/src/warnings/entities/warning.entity.ts b/demo/api/src/warnings/entities/warning.entity.ts
index 4e506205ca..98cb46b0bb 100644
--- a/demo/api/src/warnings/entities/warning.entity.ts
+++ b/demo/api/src/warnings/entities/warning.entity.ts
@@ -26,6 +26,11 @@ export class Warning extends BaseEntity {
@Field()
updatedAt: Date = new Date();
+ @Property({ columnType: "text" })
+ @Field()
+ @CrudField()
+ message: string;
+
@Property()
@Field()
@CrudField()
diff --git a/demo/api/src/warnings/generated/dto/warning.filter.ts b/demo/api/src/warnings/generated/dto/warning.filter.ts
index 1459ae24bf..76ab3ba6ef 100644
--- a/demo/api/src/warnings/generated/dto/warning.filter.ts
+++ b/demo/api/src/warnings/generated/dto/warning.filter.ts
@@ -27,6 +27,12 @@ export class WarningFilter {
@Type(() => DateTimeFilter)
updatedAt?: DateTimeFilter;
+ @Field(() => StringFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => StringFilter)
+ message?: StringFilter;
+
@Field(() => StringFilter, { nullable: true })
@ValidateNested()
@IsOptional()
diff --git a/demo/api/src/warnings/generated/dto/warning.input.ts b/demo/api/src/warnings/generated/dto/warning.input.ts
index 87c05b14b0..bdfdcfeadd 100644
--- a/demo/api/src/warnings/generated/dto/warning.input.ts
+++ b/demo/api/src/warnings/generated/dto/warning.input.ts
@@ -9,6 +9,11 @@ import { WarningStatus } from "../../entities/warning-status.enum";
@InputType()
export class WarningInput {
+ @IsNotEmpty()
+ @IsString()
+ @Field()
+ message: string;
+
@IsNotEmpty()
@IsString()
@Field()
diff --git a/demo/api/src/warnings/generated/dto/warning.sort.ts b/demo/api/src/warnings/generated/dto/warning.sort.ts
index 827d87a132..7cbd43ef2c 100644
--- a/demo/api/src/warnings/generated/dto/warning.sort.ts
+++ b/demo/api/src/warnings/generated/dto/warning.sort.ts
@@ -7,6 +7,7 @@ import { IsEnum } from "class-validator";
export enum WarningSortField {
createdAt = "createdAt",
updatedAt = "updatedAt",
+ message = "message",
type = "type",
severity = "severity",
status = "status",
diff --git a/demo/api/src/warnings/warning-checker.console.ts b/demo/api/src/warnings/warning-checker.console.ts
new file mode 100644
index 0000000000..ce62d28e9f
--- /dev/null
+++ b/demo/api/src/warnings/warning-checker.console.ts
@@ -0,0 +1,125 @@
+import { Block, BlockData, FlatBlocks } from "@comet/blocks-api";
+import { DiscoverService } from "@comet/cms-api/lib/dependencies/discover.service";
+import { CreateRequestContext, MikroORM } from "@mikro-orm/core";
+import { InjectRepository } from "@mikro-orm/nestjs";
+import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
+import { Injectable } from "@nestjs/common";
+import { Command, Console } from "nestjs-console";
+import { v5 } from "uuid";
+
+import { Warning } from "./entities/warning.entity";
+import { WarningSeverity } from "./entities/warning-severity.enum";
+
+interface RootBlockEntityData {
+ tableName: string;
+ className: string;
+ rootBlockData: Array<{ block: Block; column: string }>;
+}
+
+@Injectable()
+@Console()
+export class WarningCheckerConsole {
+ constructor(
+ private readonly orm: MikroORM,
+ private readonly discoverService: DiscoverService,
+ private readonly entityManager: EntityManager,
+ @InjectRepository(Warning) private readonly warningsRepository: EntityRepository,
+ ) {}
+
+ @Command({
+ command: "check-warnings",
+ description: "Checks for warnings",
+ })
+ @CreateRequestContext()
+ async execute(): Promise {
+ // TODO: (in the next PRs) Check if data itself is valid in the database. (Maybe some data was put into database and is not correct or a migration was done wrong)
+ for (const data of this.groupRootBlockDataByEntity()) {
+ const { tableName, className, rootBlockData } = data;
+
+ const queryBuilderLimit = 100;
+ const baseQueryBuilder = this.entityManager.createQueryBuilder(className);
+ baseQueryBuilder
+ .select(["id", ...rootBlockData.map(({ column }) => column)])
+ .from(tableName)
+ .limit(queryBuilderLimit);
+ let rootBlocks: Array<{ [key: string]: BlockData }> = [];
+ let offset = 0;
+
+ do {
+ const queryBuilder = baseQueryBuilder.clone();
+ queryBuilder.offset(offset);
+ rootBlocks = (await queryBuilder.getResult()) as Array<{ [key: string]: BlockData }>;
+
+ for (const { column, block } of rootBlockData) {
+ for (const rootBlock of rootBlocks) {
+ const blockData = rootBlock[column];
+
+ const flatBlocks = new FlatBlocks(blockData, {
+ name: block.name,
+ visible: true,
+ rootPath: "root",
+ });
+ for (const node of flatBlocks.depthFirst()) {
+ const warnings = node.block.warnings();
+
+ if (warnings.length > 0) {
+ // TODO: (in the next PRs) auto resolve warnings
+ for (const warning of warnings) {
+ const type = "Block";
+ const staticNamespace = "4e099212-0341-4bc8-8f4a-1f31c7a639ae";
+ const id = v5(`${tableName}${rootBlock["id"]};${warning.message}`, staticNamespace);
+ // TODO: (in the next PRs) add blockInfos/metadata
+
+ await this.entityManager.upsert(
+ Warning,
+ {
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ id,
+ type,
+ message: warning.message,
+ severity: WarningSeverity[warning.severity],
+ },
+ { onConflictExcludeFields: ["createdAt"] },
+ );
+ }
+ }
+ }
+ }
+ }
+
+ offset += queryBuilderLimit;
+ } while (rootBlocks.length > 0);
+ }
+ await this.entityManager.flush();
+ }
+
+ // Group root block data by tableName and className to reduce database calls.
+ // This allows the query builder to efficiently load all root blocks of an entity in one database call
+ private groupRootBlockDataByEntity() {
+ const rootBlockEntityData = new Map();
+
+ for (const {
+ metadata: { tableName, className },
+ block,
+ column,
+ } of this.discoverService.discoverRootBlocks()) {
+ const key = `${tableName}:${className}`;
+
+ if (!rootBlockEntityData.has(key)) {
+ rootBlockEntityData.set(key, {
+ tableName,
+ className,
+ rootBlockData: [],
+ });
+ }
+
+ const discoveredData = rootBlockEntityData.get(key);
+ if (discoveredData) {
+ discoveredData.rootBlockData.push({ block, column });
+ }
+ }
+
+ return rootBlockEntityData.values();
+ }
+}
diff --git a/demo/api/src/warnings/warning.module.ts b/demo/api/src/warnings/warning.module.ts
index 3be79caff9..0aa7bd4ae8 100644
--- a/demo/api/src/warnings/warning.module.ts
+++ b/demo/api/src/warnings/warning.module.ts
@@ -1,11 +1,13 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
+import { PagesModule } from "@src/pages/pages.module";
import { Warning } from "./entities/warning.entity";
import { WarningResolver } from "./generated/warning.resolver";
+import { WarningCheckerConsole } from "./warning-checker.console";
@Module({
- imports: [MikroOrmModule.forFeature([Warning])],
- providers: [WarningResolver],
+ imports: [MikroOrmModule.forFeature([Warning]), PagesModule],
+ providers: [WarningResolver, WarningCheckerConsole],
})
export class WarningsModule {}
diff --git a/packages/api/blocks-api/src/blocks/block.ts b/packages/api/blocks-api/src/blocks/block.ts
index 5d969b6d6c..58c299d969 100644
--- a/packages/api/blocks-api/src/blocks/block.ts
+++ b/packages/api/blocks-api/src/blocks/block.ts
@@ -53,10 +53,24 @@ export declare type BlockIndexItem = {
} & BlockIndexData;
export declare type BlockIndex = Array;
+// TODO: import from WarningModule after the warning module is in the package (this enum is currently duplicated)
+enum WarningEntityWarningSeverity {
+ critical = "critical",
+ high = "high",
+ low = "low",
+}
+export type WarningSeverity = `${WarningEntityWarningSeverity}`;
+
+interface BlockWarning {
+ message: string;
+ severity: WarningSeverity;
+}
+
export interface BlockDataInterface {
transformToPlain(context: BlockContext): Promise | TraversableTransformResponse>;
transformToSave(): TraversableTransformResponse;
indexData(): BlockIndexData;
+ warnings(): BlockWarning[];
searchText(): SearchText[];
childBlocksInfo(): ChildBlockInfo[]; // @TODO: better name for method and Type, maybe ReflectChildBlocks ?
previewImageUrlTemplate(dependencies: Record, context: BlockContext): Promise;
@@ -74,6 +88,11 @@ export abstract class BlockData implements BlockDataInterface {
indexData(): BlockIndexData {
return {};
}
+
+ warnings(): BlockWarning[] {
+ return [];
+ }
+
childBlocksInfo(): ChildBlockInfo[] {
const ret: ChildBlockInfo[] = [];
for (const key of getFieldKeys({ prototype: Object.getPrototypeOf(this) })) {
diff --git a/packages/api/blocks-api/src/index.ts b/packages/api/blocks-api/src/index.ts
index 11ad20f379..87a3138405 100644
--- a/packages/api/blocks-api/src/index.ts
+++ b/packages/api/blocks-api/src/index.ts
@@ -31,6 +31,7 @@ export {
TraversableTransformResponse,
TraversableTransformResponseArray,
} from "./blocks/block";
+export { WarningSeverity } from "./blocks/block";
export { createRichTextBlock } from "./blocks/createRichTextBlock";
export { createSpaceBlock } from "./blocks/createSpaceBlock";
export { createTextLinkBlock } from "./blocks/createTextLinkBlock";
diff --git a/packages/api/cms-api/src/blocks/createSeoBlock.ts b/packages/api/cms-api/src/blocks/createSeoBlock.ts
index d4f233e286..83d6eece7e 100644
--- a/packages/api/cms-api/src/blocks/createSeoBlock.ts
+++ b/packages/api/cms-api/src/blocks/createSeoBlock.ts
@@ -17,6 +17,7 @@ import {
OptionalBlockInputInterface,
SimpleBlockInputInterface,
TraversableTransformResponse,
+ WarningSeverity,
} from "@comet/blocks-api";
import { Type } from "class-transformer";
import { IsBoolean, IsEnum, IsJSON, IsOptional, IsString, IsUrl, ValidateNested } from "class-validator";
@@ -137,6 +138,14 @@ export function createSeoBlock
Date: Tue, 25 Feb 2025 13:25:58 +0100
Subject: [PATCH 3/8] auto resolve block warnings
---
demo/api/src/warnings/warning-checker.command.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/demo/api/src/warnings/warning-checker.command.ts b/demo/api/src/warnings/warning-checker.command.ts
index 421300a075..f4096eb4b4 100644
--- a/demo/api/src/warnings/warning-checker.command.ts
+++ b/demo/api/src/warnings/warning-checker.command.ts
@@ -33,6 +33,8 @@ export class WarningCheckerCommand extends CommandRunner {
@CreateRequestContext()
async run(): Promise {
+ const startDate = new Date();
+
// TODO: (in the next PRs) Check if data itself is valid in the database. (Maybe some data was put into database and is not correct or a migration was done wrong)
for (const data of this.groupRootBlockDataByEntity()) {
const { tableName, className, rootBlockData } = data;
@@ -64,7 +66,6 @@ export class WarningCheckerCommand extends CommandRunner {
const warnings = node.block.warnings();
if (warnings.length > 0) {
- // TODO: (in the next PRs) auto resolve warnings
for (const warning of warnings) {
const type = "Block";
const staticNamespace = "4e099212-0341-4bc8-8f4a-1f31c7a639ae";
@@ -93,6 +94,10 @@ export class WarningCheckerCommand extends CommandRunner {
} while (rootBlocks.length > 0);
}
await this.entityManager.flush();
+
+ // remove all Block-Warnings that are not present anymore
+ const bufferTime = startDate.setSeconds(startDate.getSeconds() - 1); // Create a buffer time by subtracting 1 second from the startDate to avoid deleting records inserted in the same second
+ await this.entityManager.nativeDelete(Warning, { type: "Block", updatedAt: { $lt: new Date(bufferTime) } });
}
// Group root block data by tableName and className to reduce database calls.
From fc5a2ab3c829f0e3fe3d26dc13bd707153e8b9c4 Mon Sep 17 00:00:00 2001
From: Raphael Blum
Date: Tue, 25 Feb 2025 13:42:41 +0100
Subject: [PATCH 4/8] switch createdAt and updatedAt to timestamp with time
zone instead of timestamptz(0) - so we have milisecond precision
---
demo/api/src/db/migrations/Migration20250225123757.ts | 11 +++++++++++
demo/api/src/warnings/entities/warning.entity.ts | 4 ++--
demo/api/src/warnings/warning-checker.command.ts | 3 +--
3 files changed, 14 insertions(+), 4 deletions(-)
create mode 100644 demo/api/src/db/migrations/Migration20250225123757.ts
diff --git a/demo/api/src/db/migrations/Migration20250225123757.ts b/demo/api/src/db/migrations/Migration20250225123757.ts
new file mode 100644
index 0000000000..77fd8ce788
--- /dev/null
+++ b/demo/api/src/db/migrations/Migration20250225123757.ts
@@ -0,0 +1,11 @@
+import { Migration } from '@mikro-orm/migrations';
+
+export class Migration20250225123757 extends Migration {
+
+ override async up(): Promise {
+
+ this.addSql(`alter table "Warning" alter column "createdAt" type timestamp with time zone using ("createdAt"::timestamp with time zone);`);
+ this.addSql(`alter table "Warning" alter column "updatedAt" type timestamp with time zone using ("updatedAt"::timestamp with time zone);`);
+ }
+
+}
diff --git a/demo/api/src/warnings/entities/warning.entity.ts b/demo/api/src/warnings/entities/warning.entity.ts
index cc2334de0f..748f082a55 100644
--- a/demo/api/src/warnings/entities/warning.entity.ts
+++ b/demo/api/src/warnings/entities/warning.entity.ts
@@ -17,11 +17,11 @@ export class Warning extends BaseEntity {
@Field(() => ID)
id: string = uuid();
- @Property()
+ @Property({ columnType: "timestamp with time zone" })
@Field()
createdAt: Date = new Date();
- @Property({ onUpdate: () => new Date() })
+ @Property({ onUpdate: () => new Date(), columnType: "timestamp with time zone" })
@Field()
updatedAt: Date = new Date();
diff --git a/demo/api/src/warnings/warning-checker.command.ts b/demo/api/src/warnings/warning-checker.command.ts
index f4096eb4b4..f45b7db199 100644
--- a/demo/api/src/warnings/warning-checker.command.ts
+++ b/demo/api/src/warnings/warning-checker.command.ts
@@ -96,8 +96,7 @@ export class WarningCheckerCommand extends CommandRunner {
await this.entityManager.flush();
// remove all Block-Warnings that are not present anymore
- const bufferTime = startDate.setSeconds(startDate.getSeconds() - 1); // Create a buffer time by subtracting 1 second from the startDate to avoid deleting records inserted in the same second
- await this.entityManager.nativeDelete(Warning, { type: "Block", updatedAt: { $lt: new Date(bufferTime) } });
+ await this.entityManager.nativeDelete(Warning, { type: "Block", updatedAt: { $lt: startDate } });
}
// Group root block data by tableName and className to reduce database calls.
From d0df826e65ec7ab0bfcdaa436040c41df367fec5 Mon Sep 17 00:00:00 2001
From: Raphael Blum
Date: Wed, 26 Feb 2025 14:42:47 +0100
Subject: [PATCH 5/8] use type instead of columnType
---
demo/api/src/warnings/entities/warning.entity.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/demo/api/src/warnings/entities/warning.entity.ts b/demo/api/src/warnings/entities/warning.entity.ts
index 748f082a55..fd77e0056b 100644
--- a/demo/api/src/warnings/entities/warning.entity.ts
+++ b/demo/api/src/warnings/entities/warning.entity.ts
@@ -17,11 +17,11 @@ export class Warning extends BaseEntity {
@Field(() => ID)
id: string = uuid();
- @Property({ columnType: "timestamp with time zone" })
+ @Property({ type: "timestamp with time zone" })
@Field()
createdAt: Date = new Date();
- @Property({ onUpdate: () => new Date(), columnType: "timestamp with time zone" })
+ @Property({ onUpdate: () => new Date(), type: "timestamp with time zone" })
@Field()
updatedAt: Date = new Date();
From 6bcd819ee98596171d781e6a7d87547067b4cce1 Mon Sep 17 00:00:00 2001
From: Raphael Blum
Date: Wed, 26 Feb 2025 15:02:29 +0100
Subject: [PATCH 6/8] rerun api generator
---
.../src/warnings/generated/dto/warning.filter.ts | 14 +-------------
.../api/src/warnings/generated/dto/warning.sort.ts | 2 --
2 files changed, 1 insertion(+), 15 deletions(-)
diff --git a/demo/api/src/warnings/generated/dto/warning.filter.ts b/demo/api/src/warnings/generated/dto/warning.filter.ts
index 76ab3ba6ef..138e255942 100644
--- a/demo/api/src/warnings/generated/dto/warning.filter.ts
+++ b/demo/api/src/warnings/generated/dto/warning.filter.ts
@@ -1,6 +1,6 @@
// This file has been generated by comet api-generator.
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
-import { createEnumFilter, DateTimeFilter, StringFilter } from "@comet/cms-api";
+import { createEnumFilter, StringFilter } from "@comet/cms-api";
import { Field, InputType } from "@nestjs/graphql";
import { Type } from "class-transformer";
import { IsOptional, ValidateNested } from "class-validator";
@@ -15,18 +15,6 @@ class WarningStatusEnumFilter extends createEnumFilter(WarningStatus) {}
@InputType()
export class WarningFilter {
- @Field(() => DateTimeFilter, { nullable: true })
- @ValidateNested()
- @IsOptional()
- @Type(() => DateTimeFilter)
- createdAt?: DateTimeFilter;
-
- @Field(() => DateTimeFilter, { nullable: true })
- @ValidateNested()
- @IsOptional()
- @Type(() => DateTimeFilter)
- updatedAt?: DateTimeFilter;
-
@Field(() => StringFilter, { nullable: true })
@ValidateNested()
@IsOptional()
diff --git a/demo/api/src/warnings/generated/dto/warning.sort.ts b/demo/api/src/warnings/generated/dto/warning.sort.ts
index 7cbd43ef2c..5cb27c1883 100644
--- a/demo/api/src/warnings/generated/dto/warning.sort.ts
+++ b/demo/api/src/warnings/generated/dto/warning.sort.ts
@@ -5,8 +5,6 @@ import { Field, InputType, registerEnumType } from "@nestjs/graphql";
import { IsEnum } from "class-validator";
export enum WarningSortField {
- createdAt = "createdAt",
- updatedAt = "updatedAt",
message = "message",
type = "type",
severity = "severity",
From 66c785946aec95423e57d92704efb7832ee21ea4 Mon Sep 17 00:00:00 2001
From: Raphael Blum
Date: Thu, 27 Feb 2025 09:06:46 +0100
Subject: [PATCH 7/8] Implement updating warnings on save
---
demo/api/schema.gql | 10 +++
.../db/migrations/Migration20250218090446.ts | 8 +++
demo/api/src/news/entities/news.entity.ts | 2 -
.../warnings/dto/warning-dependency-info.ts | 26 ++++++++
.../src/warnings/entities/warning.entity.ts | 8 ++-
.../warnings/generated/dto/warning.input.ts | 10 ++-
.../src/warnings/warning-checker.command.ts | 46 ++++++-------
.../src/warnings/warning-event-subscriber.ts | 66 +++++++++++++++++++
demo/api/src/warnings/warning.module.ts | 5 +-
demo/api/src/warnings/warning.service.ts | 52 +++++++++++++++
10 files changed, 203 insertions(+), 30 deletions(-)
create mode 100644 demo/api/src/db/migrations/Migration20250218090446.ts
create mode 100644 demo/api/src/warnings/dto/warning-dependency-info.ts
create mode 100644 demo/api/src/warnings/warning-event-subscriber.ts
create mode 100644 demo/api/src/warnings/warning.service.ts
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index b1ae792a5e..6f086b8b8a 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -1616,12 +1616,22 @@ input WarningInput {
message: String!
type: String!
severity: WarningSeverity!
+ dependencyInfo: WarningDependencyInfoInput!
status: WarningStatus! = open
}
+input WarningDependencyInfoInput {
+ rootEntityName: String!
+ rootColumnName: String!
+ rootPrimaryKey: String!
+ targetId: String!
+ jsonPath: String!
+}
+
input WarningUpdateInput {
message: String
type: String
severity: WarningSeverity
+ dependencyInfo: WarningDependencyInfoInput
status: WarningStatus
}
\ No newline at end of file
diff --git a/demo/api/src/db/migrations/Migration20250218090446.ts b/demo/api/src/db/migrations/Migration20250218090446.ts
new file mode 100644
index 0000000000..db7d5dded6
--- /dev/null
+++ b/demo/api/src/db/migrations/Migration20250218090446.ts
@@ -0,0 +1,8 @@
+import { Migration } from '@mikro-orm/migrations';
+
+export class Migration20250218090446 extends Migration {
+
+ async up(): Promise {
+ this.addSql('alter table "Warning" add column "dependencyInfo" jsonb not null;');
+ }
+}
diff --git a/demo/api/src/news/entities/news.entity.ts b/demo/api/src/news/entities/news.entity.ts
index a63cb2f93e..9c238e0c93 100644
--- a/demo/api/src/news/entities/news.entity.ts
+++ b/demo/api/src/news/entities/news.entity.ts
@@ -6,7 +6,6 @@ import {
EntityInfo,
RootBlock,
RootBlockDataScalar,
- RootBlockEntity,
RootBlockType,
} from "@comet/cms-api";
import { BaseEntity, Collection, Embeddable, Embedded, Entity, Enum, OneToMany, OptionalProps, PrimaryKey, Property } from "@mikro-orm/postgresql";
@@ -48,7 +47,6 @@ export class NewsContentScope {
}
@EntityInfo((news) => ({ name: news.title, secondaryInformation: news.slug }))
-@RootBlockEntity()
@ObjectType()
@Entity()
@CrudGenerator({ targetDirectory: `${__dirname}/../generated/` })
diff --git a/demo/api/src/warnings/dto/warning-dependency-info.ts b/demo/api/src/warnings/dto/warning-dependency-info.ts
new file mode 100644
index 0000000000..fb2ffcd446
--- /dev/null
+++ b/demo/api/src/warnings/dto/warning-dependency-info.ts
@@ -0,0 +1,26 @@
+import { Property } from "@mikro-orm/core";
+import { Field, InputType, ObjectType } from "@nestjs/graphql";
+
+@ObjectType()
+@InputType("WarningDependencyInfoInput")
+export class WarningDependencyInfo {
+ @Property()
+ @Field()
+ rootEntityName: string;
+
+ @Property()
+ @Field()
+ rootColumnName: string;
+
+ @Property()
+ @Field()
+ rootPrimaryKey: string;
+
+ @Property()
+ @Field()
+ targetId: string;
+
+ @Property()
+ @Field()
+ jsonPath: string;
+}
diff --git a/demo/api/src/warnings/entities/warning.entity.ts b/demo/api/src/warnings/entities/warning.entity.ts
index fd77e0056b..37dc16f82c 100644
--- a/demo/api/src/warnings/entities/warning.entity.ts
+++ b/demo/api/src/warnings/entities/warning.entity.ts
@@ -1,14 +1,14 @@
-import { CrudField, CrudGenerator, RootBlockEntity } from "@comet/cms-api";
+import { CrudField, CrudGenerator } from "@comet/cms-api";
import { BaseEntity, Entity, Enum, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { Field, ID, ObjectType } from "@nestjs/graphql";
import { v4 as uuid } from "uuid";
+import { WarningDependencyInfo } from "../dto/warning-dependency-info";
import { WarningSeverity } from "./warning-severity.enum";
import { WarningStatus } from "./warning-status.enum";
@ObjectType()
@Entity()
-@RootBlockEntity()
@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: ["warnings"] })
export class Warning extends BaseEntity {
[OptionalProps]?: "createdAt" | "updatedAt" | "status";
@@ -39,7 +39,9 @@ export class Warning extends BaseEntity {
@Field(() => WarningSeverity)
severity: WarningSeverity;
- // TODO: add blockInfos with COM-958
+ @Property({ type: "jsonb" })
+ @CrudField()
+ dependencyInfo: WarningDependencyInfo;
@Enum({ items: () => WarningStatus })
@Field(() => WarningStatus)
diff --git a/demo/api/src/warnings/generated/dto/warning.input.ts b/demo/api/src/warnings/generated/dto/warning.input.ts
index bdfdcfeadd..df6c54589a 100644
--- a/demo/api/src/warnings/generated/dto/warning.input.ts
+++ b/demo/api/src/warnings/generated/dto/warning.input.ts
@@ -2,8 +2,10 @@
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
import { PartialType } from "@comet/cms-api";
import { Field, InputType } from "@nestjs/graphql";
-import { IsEnum, IsNotEmpty, IsString } from "class-validator";
+import { Type } from "class-transformer";
+import { IsEnum, IsNotEmpty, IsString, ValidateNested } from "class-validator";
+import { WarningDependencyInfo } from "../../dto/warning-dependency-info";
import { WarningSeverity } from "../../entities/warning-severity.enum";
import { WarningStatus } from "../../entities/warning-status.enum";
@@ -24,6 +26,12 @@ export class WarningInput {
@Field(() => WarningSeverity)
severity: WarningSeverity;
+ @IsNotEmpty()
+ @ValidateNested()
+ @Type(() => WarningDependencyInfo)
+ @Field(() => WarningDependencyInfo)
+ dependencyInfo: WarningDependencyInfo;
+
@IsNotEmpty()
@IsEnum(WarningStatus)
@Field(() => WarningStatus, { defaultValue: WarningStatus.open })
diff --git a/demo/api/src/warnings/warning-checker.command.ts b/demo/api/src/warnings/warning-checker.command.ts
index f45b7db199..a36f998164 100644
--- a/demo/api/src/warnings/warning-checker.command.ts
+++ b/demo/api/src/warnings/warning-checker.command.ts
@@ -5,17 +5,22 @@ import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
import { Injectable } from "@nestjs/common";
import { Command, CommandRunner } from "nest-commander";
-import { v5 } from "uuid";
import { Warning } from "./entities/warning.entity";
-import { WarningSeverity } from "./entities/warning-severity.enum";
+import { WarningService } from "./warning.service";
interface RootBlockEntityData {
+ primaryKey: string;
tableName: string;
className: string;
rootBlockData: Array<{ block: Block; column: string }>;
}
+interface RootBlockData {
+ id: string;
+ [key: string]: BlockData | string;
+}
+
@Injectable()
@Command({
name: "check-warnings",
@@ -27,6 +32,7 @@ export class WarningCheckerCommand extends CommandRunner {
private readonly discoverService: DiscoverService,
private readonly entityManager: EntityManager,
@InjectRepository(Warning) private readonly warningsRepository: EntityRepository,
+ private readonly warningService: WarningService,
) {
super();
}
@@ -42,19 +48,19 @@ export class WarningCheckerCommand extends CommandRunner {
const queryBuilderLimit = 100;
const baseQueryBuilder = this.entityManager.createQueryBuilder(className);
baseQueryBuilder
- .select(["id", ...rootBlockData.map(({ column }) => column)])
+ .select([`${data.primaryKey} as id`, ...rootBlockData.map(({ column }) => column)])
.from(tableName)
.limit(queryBuilderLimit);
- let rootBlocks: Array<{ [key: string]: BlockData }> = [];
+ let rootBlocks: RootBlockData[] = [];
let offset = 0;
do {
const queryBuilder = baseQueryBuilder.clone();
queryBuilder.offset(offset);
- rootBlocks = (await queryBuilder.getResult()) as Array<{ [key: string]: BlockData }>;
-
+ rootBlocks = (await queryBuilder.getResult()) as RootBlockData[];
for (const { column, block } of rootBlockData) {
for (const rootBlock of rootBlocks) {
+ if (typeof rootBlock[column] === "string") continue;
const blockData = rootBlock[column];
const flatBlocks = new FlatBlocks(blockData, {
@@ -67,23 +73,16 @@ export class WarningCheckerCommand extends CommandRunner {
if (warnings.length > 0) {
for (const warning of warnings) {
- const type = "Block";
- const staticNamespace = "4e099212-0341-4bc8-8f4a-1f31c7a639ae";
- const id = v5(`${tableName}${rootBlock["id"]};${warning.message}`, staticNamespace);
- // TODO: (in the next PRs) add blockInfos/metadata
-
- await this.entityManager.upsert(
- Warning,
- {
- createdAt: new Date(),
- updatedAt: new Date(),
- id,
- type,
- message: warning.message,
- severity: WarningSeverity[warning.severity],
+ this.warningService.saveWarning({
+ warning,
+ dependencyInfo: {
+ rootEntityName: tableName,
+ rootColumnName: column,
+ rootPrimaryKey: data.primaryKey,
+ targetId: rootBlock.id,
+ jsonPath: node.pathToString(),
},
- { onConflictExcludeFields: ["createdAt"] },
- );
+ });
}
}
}
@@ -105,7 +104,7 @@ export class WarningCheckerCommand extends CommandRunner {
const rootBlockEntityData = new Map();
for (const {
- metadata: { tableName, className },
+ metadata: { tableName, className, primaryKeys },
block,
column,
} of this.discoverService.discoverRootBlocks()) {
@@ -116,6 +115,7 @@ export class WarningCheckerCommand extends CommandRunner {
tableName,
className,
rootBlockData: [],
+ primaryKey: primaryKeys[0],
});
}
diff --git a/demo/api/src/warnings/warning-event-subscriber.ts b/demo/api/src/warnings/warning-event-subscriber.ts
new file mode 100644
index 0000000000..70654995c4
--- /dev/null
+++ b/demo/api/src/warnings/warning-event-subscriber.ts
@@ -0,0 +1,66 @@
+import { FlatBlocks } from "@comet/cms-api";
+import { EntityName, EventArgs, EventSubscriber } from "@mikro-orm/core";
+import { EntityClass, EntityManager, MikroORM } from "@mikro-orm/postgresql";
+import { Injectable } from "@nestjs/common";
+import { WarningService } from "@src/warnings/warning.service";
+
+@Injectable()
+export class WarningEventSubscriber implements EventSubscriber {
+ constructor(
+ readonly entityManager: EntityManager,
+ private readonly orm: MikroORM,
+ private readonly warningService: WarningService,
+ ) {
+ entityManager.getEventManager().registerSubscriber(this);
+ }
+
+ getSubscribedEntities(): EntityName[] {
+ const rootBlockEntities: EntityName[] = [];
+
+ const entities = this.orm.config.get("entities") as EntityClass[];
+ for (const entity of entities) {
+ const rootBlockEntityOptions = Reflect.getMetadata(`data:rootBlockEntityOptions`, entity);
+
+ if (rootBlockEntityOptions) {
+ rootBlockEntities.push(entity);
+ }
+ }
+ return rootBlockEntities;
+ }
+
+ async afterUpdate(args: EventArgs): Promise {
+ return this.handleUpdateAndCreate(args);
+ }
+
+ async afterCreate(args: EventArgs): Promise {
+ return this.handleUpdateAndCreate(args);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private async handleUpdateAndCreate(args: EventArgs): Promise {
+ const entity = args.meta.class;
+
+ if (entity) {
+ const keys = Reflect.getMetadata(`keys:rootBlock`, entity.prototype) || [];
+ for (const key of keys) {
+ const block = Reflect.getMetadata(`data:rootBlock`, entity.prototype, key);
+
+ const flatBlocks = new FlatBlocks(args.entity[key], {
+ name: block.name,
+ visible: true,
+ rootPath: "root",
+ });
+ for (const node of flatBlocks.depthFirst()) {
+ const warnings = node.block.warnings();
+ await this.warningService.updateWarningsForBlock(warnings, {
+ rootEntityName: entity.name,
+ rootColumnName: key,
+ targetId: args.entity.id,
+ rootPrimaryKey: args.meta.primaryKeys[0],
+ jsonPath: node.pathToString(),
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/demo/api/src/warnings/warning.module.ts b/demo/api/src/warnings/warning.module.ts
index 24c4313f4e..accb19c42f 100644
--- a/demo/api/src/warnings/warning.module.ts
+++ b/demo/api/src/warnings/warning.module.ts
@@ -3,10 +3,13 @@ import { Module } from "@nestjs/common";
import { Warning } from "./entities/warning.entity";
import { WarningResolver } from "./generated/warning.resolver";
+import { WarningService } from "./warning.service";
import { WarningCheckerCommand } from "./warning-checker.command";
+import { WarningEventSubscriber } from "./warning-event-subscriber";
@Module({
imports: [MikroOrmModule.forFeature([Warning])],
- providers: [WarningResolver, WarningCheckerCommand],
+ providers: [WarningResolver, WarningCheckerCommand, WarningService, WarningEventSubscriber],
+ exports: [WarningService],
})
export class WarningsModule {}
diff --git a/demo/api/src/warnings/warning.service.ts b/demo/api/src/warnings/warning.service.ts
new file mode 100644
index 0000000000..2d6cb67572
--- /dev/null
+++ b/demo/api/src/warnings/warning.service.ts
@@ -0,0 +1,52 @@
+import { BlockWarning } from "@comet/cms-api";
+import { MikroORM } from "@mikro-orm/core";
+import { EntityManager } from "@mikro-orm/postgresql";
+import { Injectable } from "@nestjs/common";
+import { v5 } from "uuid";
+
+import { WarningDependencyInfo } from "./dto/warning-dependency-info";
+import { Warning } from "./entities/warning.entity";
+import { WarningSeverity } from "./entities/warning-severity.enum";
+
+@Injectable()
+export class WarningService {
+ constructor(
+ private readonly orm: MikroORM,
+ private readonly entityManager: EntityManager,
+ ) {}
+
+ public async saveWarning({ warning, dependencyInfo }: { warning: BlockWarning; dependencyInfo: WarningDependencyInfo }): Promise {
+ const type = "Block";
+ const staticNamespace = "4e099212-0341-4bc8-8f4a-1f31c7a639ae";
+ const id = v5(`${dependencyInfo.rootEntityName}${dependencyInfo.targetId};${warning.message}`, staticNamespace);
+
+ await this.entityManager.upsert(
+ Warning,
+ {
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ id,
+ type,
+ message: warning.message,
+ severity: WarningSeverity[warning.severity as keyof typeof WarningSeverity],
+ dependencyInfo,
+ },
+ { onConflictExcludeFields: ["createdAt"] },
+ );
+ }
+
+ public async updateWarningsForBlock(warnings: BlockWarning[], dependencyInfo: WarningDependencyInfo): Promise {
+ const startDate = new Date();
+ for (const warning of warnings) {
+ await this.saveWarning({
+ warning,
+ dependencyInfo,
+ });
+ }
+ this.deleteOutdatedWarnings(startDate, dependencyInfo);
+ }
+
+ public async deleteOutdatedWarnings(date: Date, dependencyInfo: WarningDependencyInfo): Promise {
+ await this.entityManager.nativeDelete(Warning, { type: "Block", updatedAt: { $lt: date }, dependencyInfo });
+ }
+}
From de1102d3fffc64885249ebdb46879b4c199a55e0 Mon Sep 17 00:00:00 2001
From: Raphael Blum
Date: Fri, 28 Feb 2025 10:53:28 +0100
Subject: [PATCH 8/8] Add linking to warning in admin
---
demo/admin/src/warnings/WarningsGrid.tsx | 70 +++++++++++++++++--
demo/api/schema.gql | 31 ++++----
.../warnings/dto/warning-dependency-info.ts | 4 ++
.../src/warnings/entities/warning.entity.ts | 1 +
.../src/warnings/warning-checker.command.ts | 1 +
.../src/warnings/warning-event-subscriber.ts | 1 +
.../createDocumentDependencyMethods.ts | 7 +-
7 files changed, 98 insertions(+), 17 deletions(-)
diff --git a/demo/admin/src/warnings/WarningsGrid.tsx b/demo/admin/src/warnings/WarningsGrid.tsx
index 07125dbdb1..c8d3f9d59b 100644
--- a/demo/admin/src/warnings/WarningsGrid.tsx
+++ b/demo/admin/src/warnings/WarningsGrid.tsx
@@ -1,4 +1,4 @@
-import { gql, useQuery } from "@apollo/client";
+import { gql, useApolloClient, useQuery } from "@apollo/client";
import {
dataGridDateTimeColumn,
DataGridToolbar,
@@ -12,11 +12,14 @@ import {
useDataGridRemote,
usePersistentColumnState,
} from "@comet/admin";
-import { WarningSolid } from "@comet/admin-icons";
-import { Chip } from "@mui/material";
+import { ArrowRight, OpenNewTab, WarningSolid } from "@comet/admin-icons";
+import { type DependencyInterface, useDependenciesConfig } from "@comet/cms-admin";
+import { Chip, IconButton } from "@mui/material";
import { DataGrid, GridToolbarQuickFilter } from "@mui/x-data-grid";
+import { useContentScope } from "@src/common/ContentScopeProvider";
import { type GQLWarningSeverity } from "@src/graphql.generated";
-import { useIntl } from "react-intl";
+import { FormattedMessage, useIntl } from "react-intl";
+import { useHistory } from "react-router";
import { type GQLWarningsGridQuery, type GQLWarningsGridQueryVariables, type GQLWarningsListFragment } from "./WarningsGrid.generated";
@@ -29,6 +32,12 @@ const warningsFragment = gql`
type
severity
status
+ dependencyInfo {
+ rootColumnName
+ targetId
+ graphqlObjectType
+ jsonPath
+ }
}
`;
@@ -63,6 +72,10 @@ export function WarningsGrid() {
...useDataGridRemote({ initialFilter: { items: [{ field: "state", operator: "is", value: "open" }] } }),
...usePersistentColumnState("WarningsGrid"),
};
+ const history = useHistory();
+ const entityDependencyMap = useDependenciesConfig();
+ const apolloClient = useApolloClient();
+ const contentScope = useContentScope();
const columns: GridColDef[] = [
{
@@ -118,6 +131,55 @@ export function WarningsGrid() {
],
width: 150,
},
+ {
+ field: "actions",
+ headerName: "",
+ sortable: false,
+ renderCell: ({ row }) => {
+ const dependencyObject = entityDependencyMap[row.dependencyInfo.graphqlObjectType] as DependencyInterface | undefined;
+
+ if (dependencyObject === undefined) {
+ if (process.env.NODE_ENV === "development") {
+ console.warn(
+ `Cannot load URL because no implementation of DependencyInterface for ${row.dependencyInfo.graphqlObjectType} was provided via the DependenciesConfig`,
+ );
+ }
+ return ;
+ }
+
+ const loadUrl = async () => {
+ const path = await dependencyObject.resolvePath({
+ rootColumnName: row.dependencyInfo.rootColumnName,
+ jsonPath: row.dependencyInfo.jsonPath,
+ apolloClient,
+ id: row.dependencyInfo.targetId,
+ });
+ return contentScope.match.url + path;
+ };
+
+ return (
+
+
{
+ const url = await loadUrl();
+ window.open(url, "_blank");
+ }}
+ >
+
+
+
{
+ const url = await loadUrl();
+
+ history.push(url);
+ }}
+ >
+
+
+
+ );
+ },
+ },
];
const { filter: gqlFilter, search: gqlSearch } = muiGridFilterToGql(columns, dataGridProps.filterModel);
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index 6f086b8b8a..15568cc8ef 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -627,6 +627,15 @@ type RedirectScope {
domain: String!
}
+type WarningDependencyInfo {
+ rootEntityName: String!
+ rootColumnName: String!
+ rootPrimaryKey: String!
+ targetId: String!
+ jsonPath: String!
+ graphqlObjectType: String!
+}
+
type Warning {
id: ID!
createdAt: DateTime!
@@ -634,6 +643,7 @@ type Warning {
message: String!
type: String!
severity: WarningSeverity!
+ dependencyInfo: WarningDependencyInfo!
status: WarningStatus!
}
@@ -782,6 +792,15 @@ input RedirectScopeInput {
domain: String!
}
+input WarningDependencyInfoInput {
+ rootEntityName: String!
+ rootColumnName: String!
+ rootPrimaryKey: String!
+ targetId: String!
+ jsonPath: String!
+ graphqlObjectType: String!
+}
+
type Query {
currentUser: CurrentUser!
userPermissionsUserById(id: String!): UserPermissionsUser!
@@ -1210,8 +1229,6 @@ input ManufacturerCountryFilter {
}
input WarningFilter {
- createdAt: DateTimeFilter
- updatedAt: DateTimeFilter
message: StringFilter
type: StringFilter
severity: WarningSeverityEnumFilter
@@ -1238,8 +1255,6 @@ input WarningSort {
}
enum WarningSortField {
- createdAt
- updatedAt
message
type
severity
@@ -1620,14 +1635,6 @@ input WarningInput {
status: WarningStatus! = open
}
-input WarningDependencyInfoInput {
- rootEntityName: String!
- rootColumnName: String!
- rootPrimaryKey: String!
- targetId: String!
- jsonPath: String!
-}
-
input WarningUpdateInput {
message: String
type: String
diff --git a/demo/api/src/warnings/dto/warning-dependency-info.ts b/demo/api/src/warnings/dto/warning-dependency-info.ts
index fb2ffcd446..39b72ea377 100644
--- a/demo/api/src/warnings/dto/warning-dependency-info.ts
+++ b/demo/api/src/warnings/dto/warning-dependency-info.ts
@@ -23,4 +23,8 @@ export class WarningDependencyInfo {
@Property()
@Field()
jsonPath: string;
+
+ @Property()
+ @Field()
+ graphqlObjectType: string; // do i need this??
}
diff --git a/demo/api/src/warnings/entities/warning.entity.ts b/demo/api/src/warnings/entities/warning.entity.ts
index 37dc16f82c..01d9472b12 100644
--- a/demo/api/src/warnings/entities/warning.entity.ts
+++ b/demo/api/src/warnings/entities/warning.entity.ts
@@ -40,6 +40,7 @@ export class Warning extends BaseEntity {
severity: WarningSeverity;
@Property({ type: "jsonb" })
+ @Field()
@CrudField()
dependencyInfo: WarningDependencyInfo;
diff --git a/demo/api/src/warnings/warning-checker.command.ts b/demo/api/src/warnings/warning-checker.command.ts
index a36f998164..31b16ea670 100644
--- a/demo/api/src/warnings/warning-checker.command.ts
+++ b/demo/api/src/warnings/warning-checker.command.ts
@@ -81,6 +81,7 @@ export class WarningCheckerCommand extends CommandRunner {
rootPrimaryKey: data.primaryKey,
targetId: rootBlock.id,
jsonPath: node.pathToString(),
+ graphqlObjectType: tableName,
},
});
}
diff --git a/demo/api/src/warnings/warning-event-subscriber.ts b/demo/api/src/warnings/warning-event-subscriber.ts
index 70654995c4..4cf08619ee 100644
--- a/demo/api/src/warnings/warning-event-subscriber.ts
+++ b/demo/api/src/warnings/warning-event-subscriber.ts
@@ -58,6 +58,7 @@ export class WarningEventSubscriber implements EventSubscriber {
targetId: args.entity.id,
rootPrimaryKey: args.meta.primaryKeys[0],
jsonPath: node.pathToString(),
+ graphqlObjectType: entity.name,
});
}
}
diff --git a/packages/admin/cms-admin/src/dependencies/createDocumentDependencyMethods.ts b/packages/admin/cms-admin/src/dependencies/createDocumentDependencyMethods.ts
index 2a4cd05077..66f1d206e2 100644
--- a/packages/admin/cms-admin/src/dependencies/createDocumentDependencyMethods.ts
+++ b/packages/admin/cms-admin/src/dependencies/createDocumentDependencyMethods.ts
@@ -79,7 +79,12 @@ export function createDocumentDependencyMethods