From 4c730f39f53f34cb2a452896188070c15f718091 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Mon, 2 Mar 2026 17:18:50 +0100 Subject: [PATCH 01/10] Add filtering by name, secondaryInformation, type and visibility to DependentsList and DependenciesList --- .changeset/deps-list-filter-sort.md | 9 + demo/admin/src/documents/pages/EditPage.tsx | 24 ++- demo/api/schema.gql | 40 ++++- packages/admin/admin/src/index.ts | 2 +- .../src/dam/FileForm/EditFile.gql.ts | 12 +- .../src/dependencies/DependenciesList.tsx | 55 ++++++- .../src/dependencies/DependentsList.tsx | 55 ++++++- packages/api/cms-api/schema.gql | 70 +++++--- .../dependencies.resolver.factory.ts | 4 +- .../src/dependencies/dependencies.service.ts | 155 +++++++++++++----- .../dependents.resolver.factory.ts | 4 +- .../src/dependencies/dto/dependencies.args.ts | 15 +- .../dependencies/dto/dependencies.filter.ts | 65 +++++++- .../src/dependencies/dto/dependency-sort.ts | 25 +++ packages/api/cms-api/src/index.ts | 1 + 15 files changed, 440 insertions(+), 96 deletions(-) create mode 100644 .changeset/deps-list-filter-sort.md create mode 100644 packages/api/cms-api/src/dependencies/dto/dependency-sort.ts diff --git a/.changeset/deps-list-filter-sort.md b/.changeset/deps-list-filter-sort.md new file mode 100644 index 00000000000..8c7a2a78b41 --- /dev/null +++ b/.changeset/deps-list-filter-sort.md @@ -0,0 +1,9 @@ +--- +"@comet/cms-admin": minor +"@comet/cms-api": minor +"@comet/admin": minor +--- + +Add filtering and sorting to `DependenciesList` and `DependentsList` + +Users can now filter dependencies/dependents by name, type, secondary information, and visibility, and sort by all columns. A default filter shows only visible items. The `GqlFilter` type is now exported from `@comet/admin`. diff --git a/demo/admin/src/documents/pages/EditPage.tsx b/demo/admin/src/documents/pages/EditPage.tsx index 249b5a19961..7109edb3ea2 100644 --- a/demo/admin/src/documents/pages/EditPage.tsx +++ b/demo/admin/src/documents/pages/EditPage.tsx @@ -37,10 +37,17 @@ interface Props { } const pageTreeNodeDependentsQuery = gql` - query PageTreeNodeDependents($id: ID!, $offset: Int!, $limit: Int!, $forceRefresh: Boolean = false) { + query PageTreeNodeDependents( + $id: ID! + $offset: Int! + $limit: Int! + $forceRefresh: Boolean = false + $filter: DependentFilter + $sort: [DependencySort!] + ) { item: pageTreeNode(id: $id) { id - dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh) { + dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh, filter: $filter, sort: $sort) { nodes { rootGraphqlObjectType rootId @@ -48,6 +55,7 @@ const pageTreeNodeDependentsQuery = gql` jsonPath name secondaryInformation + visible } totalCount } @@ -56,10 +64,17 @@ const pageTreeNodeDependentsQuery = gql` `; const pageTreeNodeDependenciesQuery = gql` - query PageTreeNodeDependencies($id: ID!, $offset: Int!, $limit: Int!, $forceRefresh: Boolean = false) { + query PageTreeNodeDependencies( + $id: ID! + $offset: Int! + $limit: Int! + $forceRefresh: Boolean = false + $filter: DependencyFilter + $sort: [DependencySort!] + ) { item: page(id: $id) { id - dependencies(offset: $offset, limit: $limit, forceRefresh: $forceRefresh) { + dependencies(offset: $offset, limit: $limit, forceRefresh: $forceRefresh, filter: $filter, sort: $sort) { nodes { targetGraphqlObjectType targetId @@ -67,6 +82,7 @@ const pageTreeNodeDependenciesQuery = gql` jsonPath name secondaryInformation + visible } totalCount } diff --git a/demo/api/schema.gql b/demo/api/schema.gql index d7ad43df410..99d31f5b8b3 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -381,7 +381,7 @@ type DamFile { contentHash: String! createdAt: DateTime! damPath: String! - dependents(filter: DependentFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependents(filter: DependentFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! duplicates: [DamFile!]! fileUrl: String! folder: DamFolder @@ -542,15 +542,37 @@ type Dependency { } input DependencyFilter { + and: [DependencyFilter!] + name: StringFilter + or: [DependencyFilter!] rootColumnName: String + secondaryInformation: StringFilter targetGraphqlObjectType: String targetId: String + visible: BooleanFilter +} + +input DependencySort { + direction: SortDirection! = ASC + field: DependencySortField! +} + +enum DependencySortField { + graphqlObjectType + name + secondaryInformation + visible } input DependentFilter { + and: [DependentFilter!] + name: StringFilter + or: [DependentFilter!] rootColumnName: String rootGraphqlObjectType: String rootId: String + secondaryInformation: StringFilter + visible: BooleanFilter } interface DocumentInterface { @@ -669,7 +691,7 @@ input FolderFilterInput { type Footer { content: FooterContentBlockData! createdAt: DateTime! - dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! id: ID! scope: FooterScope! updatedAt: DateTime! @@ -799,7 +821,7 @@ type MainMenu { type MainMenuItem { content: RichTextBlockData createdAt: DateTime! - dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! id: ID! node: PageTreeNode! updatedAt: DateTime! @@ -1011,8 +1033,8 @@ type News { content: NewsContentBlockData! createdAt: DateTime! date: DateTime! - dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! - dependents(filter: DependentFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! + dependents(filter: DependentFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! id: ID! image: DamImageBlockData! scope: NewsContentScope! @@ -1143,7 +1165,7 @@ input OneToManyFilter { type Page implements DocumentInterface { content: PageContentBlockData! createdAt: DateTime! - dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! id: ID! pageTreeNode: PageTreeNode seo: SeoBlockData! @@ -1168,8 +1190,8 @@ input PageInput { type PageTreeNode { category: PageTreeNodeCategory! childNodes: [PageTreeNode!]! - dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! - dependents(filter: DependentFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! + dependents(filter: DependentFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! document: PageContentUnion documentType: String! hideInMenu: Boolean! @@ -1978,7 +2000,7 @@ type Redirect { active: Boolean! comment: String createdAt: DateTime! - dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0): PaginatedDependencies! + dependencies(filter: DependencyFilter, forceRefresh: Boolean! = false, limit: Int! = 25, offset: Int! = 0, sort: [DependencySort!]): PaginatedDependencies! generationType: RedirectGenerationType! id: ID! scope: RedirectScope! diff --git a/packages/admin/admin/src/index.ts b/packages/admin/admin/src/index.ts index de8d226cb4e..139e811baa7 100644 --- a/packages/admin/admin/src/index.ts +++ b/packages/admin/admin/src/index.ts @@ -84,7 +84,7 @@ export { dataGridOneToManyColumn, } from "./dataGrid/gridColumnTypes"; export { GridFilterButton } from "./dataGrid/GridFilterButton"; -export { muiGridFilterToGql } from "./dataGrid/muiGridFilterToGql"; +export { type GqlFilter, muiGridFilterToGql } from "./dataGrid/muiGridFilterToGql"; export { muiGridPagingToGql } from "./dataGrid/muiGridPagingToGql"; export { muiGridSortToGql } from "./dataGrid/muiGridSortToGql"; export { DataGridPagination, type DataGridPaginationClassKey, type DataGridPaginationProps } from "./dataGrid/pagination/DataGridPagination"; diff --git a/packages/admin/cms-admin/src/dam/FileForm/EditFile.gql.ts b/packages/admin/cms-admin/src/dam/FileForm/EditFile.gql.ts index f9b77872143..16fd7018e8f 100644 --- a/packages/admin/cms-admin/src/dam/FileForm/EditFile.gql.ts +++ b/packages/admin/cms-admin/src/dam/FileForm/EditFile.gql.ts @@ -59,10 +59,17 @@ export const updateDamFileMutation = gql` `; export const damFileDependentsQuery = gql` - query DamFileDependencies($id: ID!, $offset: Int!, $limit: Int!, $forceRefresh: Boolean = false) { + query DamFileDependencies( + $id: ID! + $offset: Int! + $limit: Int! + $forceRefresh: Boolean = false + $filter: DependentFilter + $sort: [DependencySort!] + ) { item: damFile(id: $id) { id - dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh) { + dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh, filter: $filter, sort: $sort) { nodes { rootGraphqlObjectType rootId @@ -70,6 +77,7 @@ export const damFileDependentsQuery = gql` jsonPath name secondaryInformation + visible } totalCount } diff --git a/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx b/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx index c250fa08d0e..e0d58277a75 100644 --- a/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx +++ b/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx @@ -3,9 +3,13 @@ import { Alert, DataGridToolbar, FillSpace, + type GqlFilter, GridCellContent, type GridColDef, + GridFilterButton, messages, + muiGridFilterToGql, + muiGridSortToGql, Tooltip, useBufferedRowCount, useDataGridRemote, @@ -23,12 +27,15 @@ import { type GQLDependency } from "../graphql.generated"; import { useDependenciesConfig } from "./dependenciesConfig"; import { type DependencyInterface } from "./types"; -type DependencyItem = Pick & { +type DependencyItem = Pick & { id: string; graphqlObjectType: string; }; -type Dependency = Pick; +type Dependency = Pick< + GQLDependency, + "targetGraphqlObjectType" | "targetId" | "rootColumnName" | "jsonPath" | "name" | "secondaryInformation" | "visible" +>; interface DependenciesListQuery { item: { @@ -41,6 +48,8 @@ type QueryVariables = { offset: number; limit: number; forceRefresh?: boolean; + filter?: GqlFilter; + sort?: Array<{ field: string; direction: "ASC" | "DESC" }>; }; interface DependenciesListGridToolbarProps extends GridToolbarProps { @@ -51,6 +60,7 @@ function DependenciesListGridToolbar({ refetch }: DependenciesListGridToolbarPro return ( + }> ...useDataGridRemote({ queryParamsPrefix: "dependencies", pageSize, + initialFilter: { + items: [{ field: "visible", operator: "is", value: "true" }], + }, }), ...usePersistentColumnState("DependenciesList"), }; const columns: GridColDef[] = [ { - field: "nameInfo", + field: "name", headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.nameAndInfo", defaultMessage: "Name/Info" }), - sortable: false, flex: 1, + sortBy: "name", renderCell: ({ row }) => ( } secondaryText={row.secondaryInformation} /> ), }, + { + field: "secondaryInformation", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.secondaryInformation", defaultMessage: "Secondary information" }), + sortBy: "secondaryInformation", + visible: false, + }, { field: "type", headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.type", defaultMessage: "Type" }), - sortable: false, + type: "singleSelect", + valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ + value, + label: dep.displayName as string, + })), + sortBy: "graphqlObjectType", + toGqlFilter: (filterItem) => { + if (!filterItem.value) return {}; + return { targetGraphqlObjectType: filterItem.value }; + }, renderCell: ({ row }) => , }, + { + field: "visible", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.visible", defaultMessage: "Visible" }), + type: "boolean", + visible: false, + sortBy: "visible", + toGqlFilter: (filterItem) => { + if (filterItem.value === undefined || filterItem.value === "") return {}; + return { visible: { equal: filterItem.value === "true" || filterItem.value === true } } as unknown as GqlFilter; + }, + }, { field: "actions", type: "actions", headerName: "", + filterable: false, sortable: false, renderCell: ({ row }) => { const dependencyObject = entityDependencyMap[row.graphqlObjectType] as DependencyInterface | undefined; @@ -161,10 +201,15 @@ export const DependenciesList = ({ query, variables }: DependenciesListProps) => }, ]; + const { filter: gqlFilter } = muiGridFilterToGql(columns, dataGridProps.filterModel); + const sort = muiGridSortToGql(dataGridProps.sortModel, columns); + const { data, loading, error, refetch } = useQuery(query, { variables: { offset: dataGridProps.paginationModel.page * dataGridProps.paginationModel.pageSize, limit: dataGridProps.paginationModel.pageSize, + filter: gqlFilter, + sort, ...variables, }, }); diff --git a/packages/admin/cms-admin/src/dependencies/DependentsList.tsx b/packages/admin/cms-admin/src/dependencies/DependentsList.tsx index e9a554d78fb..897911a3509 100644 --- a/packages/admin/cms-admin/src/dependencies/DependentsList.tsx +++ b/packages/admin/cms-admin/src/dependencies/DependentsList.tsx @@ -3,9 +3,13 @@ import { Alert, DataGridToolbar, FillSpace, + type GqlFilter, GridCellContent, type GridColDef, + GridFilterButton, messages, + muiGridFilterToGql, + muiGridSortToGql, Tooltip, useBufferedRowCount, useDataGridRemote, @@ -23,12 +27,15 @@ import { type GQLDependency } from "../graphql.generated"; import { useDependenciesConfig } from "./dependenciesConfig"; import { type DependencyInterface } from "./types"; -type DependencyItem = Pick & { +type DependencyItem = Pick & { id: string; graphqlObjectType: string; }; -type Dependent = Pick; +type Dependent = Pick< + GQLDependency, + "rootGraphqlObjectType" | "rootId" | "rootColumnName" | "jsonPath" | "name" | "secondaryInformation" | "visible" +>; interface DependentsListQuery { item: { @@ -41,6 +48,8 @@ type QueryVariables = { offset: number; limit: number; forceRefresh?: boolean; + filter?: GqlFilter; + sort?: Array<{ field: string; direction: "ASC" | "DESC" }>; }; interface DependentsListGridToolbarProps extends GridToolbarProps { @@ -51,6 +60,7 @@ function DependentsListGridToolbar({ refetch }: DependentsListGridToolbarProps) return ( + }> { ...useDataGridRemote({ queryParamsPrefix: "dependents", pageSize, + initialFilter: { + items: [{ field: "visible", operator: "is", value: "true" }], + }, }), ...usePersistentColumnState("DependentsList"), }; const columns: GridColDef[] = [ { - field: "nameInfo", + field: "name", headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.nameAndInfo", defaultMessage: "Name/Info" }), - sortable: false, flex: 1, + sortBy: "name", renderCell: ({ row }) => ( } secondaryText={row.secondaryInformation} /> ), }, + { + field: "secondaryInformation", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.secondaryInformation", defaultMessage: "Secondary information" }), + sortBy: "secondaryInformation", + visible: false, + }, { field: "type", headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.type", defaultMessage: "Type" }), - sortable: false, + type: "singleSelect", + valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ + value, + label: dep.displayName as string, + })), + sortBy: "graphqlObjectType", + toGqlFilter: (filterItem) => { + if (!filterItem.value) return {}; + return { rootGraphqlObjectType: filterItem.value }; + }, renderCell: ({ row }) => , }, + { + field: "visible", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.visible", defaultMessage: "Visible" }), + type: "boolean", + visible: false, + sortBy: "visible", + toGqlFilter: (filterItem) => { + if (filterItem.value === undefined || filterItem.value === "") return {}; + return { visible: { equal: filterItem.value === "true" || filterItem.value === true } } as unknown as GqlFilter; + }, + }, { field: "actions", type: "actions", headerName: "", + filterable: false, sortable: false, renderCell: ({ row }) => { const dependencyObject = entityDependencyMap[row.graphqlObjectType] as DependencyInterface | undefined; @@ -161,10 +201,15 @@ export const DependentsList = ({ query, variables }: DependentsListProps) => { }, ]; + const { filter: gqlFilter } = muiGridFilterToGql(columns, dataGridProps.filterModel); + const sort = muiGridSortToGql(dataGridProps.sortModel, columns); + const { data, loading, error, refetch } = useQuery(query, { variables: { offset: dataGridProps.paginationModel.page * dataGridProps.paginationModel.pageSize, limit: dataGridProps.paginationModel.pageSize, + filter: gqlFilter, + sort, ...variables, }, }); diff --git a/packages/api/cms-api/schema.gql b/packages/api/cms-api/schema.gql index f74dc7aff11..21409b62dc9 100644 --- a/packages/api/cms-api/schema.gql +++ b/packages/api/cms-api/schema.gql @@ -314,7 +314,7 @@ type PageTreeNode { path: String! parentNodes: [PageTreeNode!]! document: PageContentUnion - dependents(offset: Int! = 0, limit: Int! = 25, filter: DependentFilter, forceRefresh: Boolean! = false): PaginatedDependencies! + dependents(offset: Int! = 0, limit: Int! = 25, filter: DependentFilter, sort: [DependencySort!], forceRefresh: Boolean! = false): PaginatedDependencies! } enum PageTreeNodeVisibility { @@ -339,6 +339,44 @@ input DependentFilter { rootGraphqlObjectType: String rootId: String rootColumnName: String + name: StringFilter + secondaryInformation: StringFilter + visible: BooleanFilter + and: [DependentFilter!] + or: [DependentFilter!] +} + +input StringFilter { + contains: String + notContains: String + startsWith: String + endsWith: String + equal: String + notEqual: String + isAnyOf: [String!] + isEmpty: Boolean + isNotEmpty: Boolean +} + +input BooleanFilter { + equal: Boolean +} + +input DependencySort { + field: DependencySortField! + direction: SortDirection! = ASC +} + +enum DependencySortField { + name + secondaryInformation + graphqlObjectType + visible +} + +enum SortDirection { + ASC + DESC } type Redirect { @@ -352,7 +390,7 @@ type Redirect { generationType: RedirectGenerationType! createdAt: DateTime! updatedAt: DateTime! - dependencies(offset: Int! = 0, limit: Int! = 25, filter: DependencyFilter, forceRefresh: Boolean! = false): PaginatedDependencies! + dependencies(offset: Int! = 0, limit: Int! = 25, filter: DependencyFilter, sort: [DependencySort!], forceRefresh: Boolean! = false): PaginatedDependencies! } enum RedirectSourceTypeValues { @@ -368,6 +406,11 @@ input DependencyFilter { targetGraphqlObjectType: String targetId: String rootColumnName: String + name: StringFilter + secondaryInformation: StringFilter + visible: BooleanFilter + and: [DependencyFilter!] + or: [DependencyFilter!] } type PaginatedRedirects { @@ -415,7 +458,7 @@ type DamFile { damPath: String! alternativesForThisFile: [DamMediaAlternative!]! thisFileIsAlternativeFor: [DamMediaAlternative!]! - dependents(offset: Int! = 0, limit: Int! = 25, filter: DependentFilter, forceRefresh: Boolean! = false): PaginatedDependencies! + dependents(offset: Int! = 0, limit: Int! = 25, filter: DependentFilter, sort: [DependencySort!], forceRefresh: Boolean! = false): PaginatedDependencies! } type PaginatedDamItems { @@ -504,11 +547,6 @@ input RedirectScopeInput { thisScopeHasNoFields____: String } -enum SortDirection { - ASC - DESC -} - input RedirectFilter { generationType: StringFilter source: StringFilter @@ -521,22 +559,6 @@ input RedirectFilter { or: [RedirectFilter!] } -input StringFilter { - contains: String - notContains: String - startsWith: String - endsWith: String - equal: String - notEqual: String - isAnyOf: [String!] - isEmpty: Boolean - isNotEmpty: Boolean -} - -input BooleanFilter { - equal: Boolean -} - input DateTimeFilter { equal: DateTime lowerThan: DateTime diff --git a/packages/api/cms-api/src/dependencies/dependencies.resolver.factory.ts b/packages/api/cms-api/src/dependencies/dependencies.resolver.factory.ts index d5c4e9fbba7..7a9da1fef95 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.resolver.factory.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.resolver.factory.ts @@ -17,9 +17,9 @@ export class DependenciesResolverFactory { @ResolveField(() => PaginatedDependencies) async dependencies( @Parent() node: AnyEntity<{ id: string }>, - @Args() { filter, offset, limit, forceRefresh }: DependenciesArgs, + @Args() { filter, sort, offset, limit, forceRefresh }: DependenciesArgs, ): Promise { - return this.dependenciesService.getDependencies(node, filter, { offset, limit }, { forceRefresh }); + return this.dependenciesService.getDependencies(node, filter, { offset, limit }, { forceRefresh, sort }); } } diff --git a/packages/api/cms-api/src/dependencies/dependencies.service.ts b/packages/api/cms-api/src/dependencies/dependencies.service.ts index 1a8d9f23a52..dcd3d9a5024 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.service.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.service.ts @@ -3,10 +3,12 @@ import { Injectable, Logger } from "@nestjs/common"; import { subMinutes } from "date-fns"; import { v4 as uuid } from "uuid"; +import { StringFilter } from "../common/filter/string.filter"; import { EntityInfoService } from "../entity-info/entity-info.service"; import { DiscoverService } from "./discover.service"; import { DependencyFilter, DependentFilter } from "./dto/dependencies.filter"; import { Dependency } from "./dto/dependency"; +import { DependencySort, DependencySortField } from "./dto/dependency-sort"; import { PaginatedDependencies } from "./dto/paginated-dependencies"; import { BlockIndexRefresh } from "./entities/block-index-refresh.entity"; @@ -223,26 +225,22 @@ export class DependenciesService { rootEntityName?: string; }, paginationArgs?: { offset: number; limit: number }, - options?: { forceRefresh: boolean }, + options?: { forceRefresh: boolean; sort?: DependencySort[] }, ): Promise { await this.refreshViews({ force: options?.forceRefresh }); const entityName = "entityName" in target ? target.entityName : target.constructor.name; - const qb = this.getQueryBuilderWithFilters( - { - ...filter, - targetEntityName: entityName, - targetId: target.id, - }, - paginationArgs, - ).join("EntityInfo", (join) => { + const qb = this.getBaseQueryBuilder({ targetEntityName: entityName, targetId: target.id }, paginationArgs).join("EntityInfo", (join) => { join.on("idx.rootEntityName", "EntityInfo.entityName").andOn( "EntityInfo.id", this.entityManager.getKnex("read").raw('"idx"."rootId"::text'), ); }); + this.applyFilter(qb, filter); + this.applySort(qb, options?.sort, "dependents"); + const results: Dependency[] = await qb; const countResult: Array<{ count: string | number }> = await this.withCount(qb).select("targetId").groupBy(["targetId", "targetEntityName"]); @@ -257,26 +255,22 @@ export class DependenciesService { targetEntityName?: string; }, paginationArgs?: { offset: number; limit: number }, - options?: { forceRefresh: boolean }, + options?: { forceRefresh: boolean; sort?: DependencySort[] }, ): Promise { await this.refreshViews({ force: options?.forceRefresh }); const entityName = "entityName" in root ? root.entityName : root.constructor.name; - const qb = this.getQueryBuilderWithFilters( - { - ...filter, - rootEntityName: entityName, - rootId: root.id, - }, - paginationArgs, - ).join("EntityInfo", (join) => { + const qb = this.getBaseQueryBuilder({ rootEntityName: entityName, rootId: root.id }, paginationArgs).join("EntityInfo", (join) => { join.on("idx.targetEntityName", "EntityInfo.entityName").andOn( "EntityInfo.id", this.entityManager.getKnex("read").raw('"idx"."targetId"::text'), ); }); + this.applyFilter(qb, filter); + this.applySort(qb, options?.sort, "dependencies"); + const results: Dependency[] = await qb; const countResult: Array<{ count: string | number }> = await this.withCount(qb).select("rootId").groupBy(["rootId", "rootEntityName"]); @@ -285,12 +279,8 @@ export class DependenciesService { return new PaginatedDependencies(results, Number(totalCount)); } - private getQueryBuilderWithFilters( - filter: DependentFilter & - DependencyFilter & { - targetEntityName?: string; - rootEntityName?: string; - }, + private getBaseQueryBuilder( + internalFilter: { targetEntityName?: string; rootEntityName?: string; targetId?: string; rootId?: string }, paginationArgs?: { offset: number; limit: number }, ) { const qb = this.entityManager.getKnex("read").select("*").from({ idx: "block_index_dependencies" }); @@ -299,34 +289,119 @@ export class DependenciesService { qb.offset(paginationArgs.offset).limit(paginationArgs.limit); } - if (filter.targetEntityName) { - qb.andWhere({ targetEntityName: filter.targetEntityName }); + if (internalFilter.targetEntityName) { + qb.andWhere({ targetEntityName: internalFilter.targetEntityName }); } - if (filter?.targetGraphqlObjectType) { - qb.andWhere({ targetGraphqlObjectType: filter.targetGraphqlObjectType }); + if (internalFilter.targetId) { + qb.andWhere({ targetId: internalFilter.targetId }); } - if (filter.targetId) { - qb.andWhere({ targetId: filter.targetId }); + if (internalFilter.rootEntityName) { + qb.andWhere({ rootEntityName: internalFilter.rootEntityName }); } + if (internalFilter.rootId) { + qb.andWhere({ rootId: internalFilter.rootId }); + } + + return qb; + } + + private applyFilter(qb: Knex.QueryBuilder, filter?: DependencyFilter | DependentFilter): void { + if (!filter) return; - if (filter.rootEntityName) { - qb.andWhere({ rootEntityName: filter.rootEntityName }); + if ("rootGraphqlObjectType" in filter && filter.rootGraphqlObjectType) { + qb.andWhere({ "idx.rootGraphqlObjectType": filter.rootGraphqlObjectType }); } - if (filter.rootGraphqlObjectType) { - qb.andWhere({ rootGraphqlObjectType: filter.rootGraphqlObjectType }); + if ("targetGraphqlObjectType" in filter && filter.targetGraphqlObjectType) { + qb.andWhere({ "idx.targetGraphqlObjectType": filter.targetGraphqlObjectType }); + } + if (filter.rootColumnName) { + qb.andWhere({ "idx.rootColumnName": filter.rootColumnName }); + } + if (filter.name) { + this.applyStringFilterToKnex(qb, '"EntityInfo"."name"', filter.name); + } + if (filter.secondaryInformation) { + this.applyStringFilterToKnex(qb, '"EntityInfo"."secondaryInformation"', filter.secondaryInformation); + } + if (filter.visible) { + if (filter.visible.equal !== undefined) { + qb.andWhere(this.entityManager.getKnex("read").raw('"idx"."visible" = ?', [filter.visible.equal])); + } + } + if (filter.and) { + for (const subFilter of filter.and) { + qb.andWhere((sub) => { + this.applyFilter(sub, subFilter); + }); + } } - if (filter.rootId) { - qb.andWhere({ rootId: filter.rootId }); + if (filter.or) { + const orFilters = filter.or; + qb.andWhere((outer) => { + for (const subFilter of orFilters) { + outer.orWhere((sub) => { + this.applyFilter(sub, subFilter); + }); + } + }); } + } + private applyStringFilterToKnex(qb: Knex.QueryBuilder, column: string, filter: StringFilter): void { + const knex = this.entityManager.getKnex("read"); - if (filter?.rootColumnName) { - qb.andWhere({ rootColumnName: filter.rootColumnName }); + if (filter.contains !== undefined) { + qb.andWhere(knex.raw(`${column} ILIKE ?`, [`%${filter.contains}%`])); + } + if (filter.notContains !== undefined) { + qb.andWhere(knex.raw(`${column} NOT ILIKE ?`, [`%${filter.notContains}%`])); + } + if (filter.startsWith !== undefined) { + qb.andWhere(knex.raw(`${column} ILIKE ?`, [`${filter.startsWith}%`])); + } + if (filter.endsWith !== undefined) { + qb.andWhere(knex.raw(`${column} ILIKE ?`, [`%${filter.endsWith}`])); + } + if (filter.equal !== undefined) { + qb.andWhere(knex.raw(`${column} = ?`, [filter.equal])); + } + if (filter.notEqual !== undefined) { + qb.andWhere(knex.raw(`${column} != ?`, [filter.notEqual])); + } + if (filter.isAnyOf !== undefined && filter.isAnyOf.length > 0) { + qb.andWhere(knex.raw(`${column} IN (${filter.isAnyOf.map(() => "?").join(", ")})`, filter.isAnyOf)); } + if (filter.isEmpty) { + qb.andWhere(knex.raw(`(${column} IS NULL OR ${column} = '')`)); + } + if (filter.isNotEmpty) { + qb.andWhere(knex.raw(`(${column} IS NOT NULL AND ${column} != '')`)); + } + } - return qb; + private applySort(qb: Knex.QueryBuilder, sort: DependencySort[] | undefined, context: "dependencies" | "dependents"): void { + if (!sort || sort.length === 0) return; + + for (const sortEntry of sort) { + let column: string; + switch (sortEntry.field) { + case DependencySortField.name: + column = '"EntityInfo"."name"'; + break; + case DependencySortField.secondaryInformation: + column = '"EntityInfo"."secondaryInformation"'; + break; + case DependencySortField.graphqlObjectType: + column = context === "dependencies" ? '"idx"."targetGraphqlObjectType"' : '"idx"."rootGraphqlObjectType"'; + break; + case DependencySortField.visible: + column = '"idx"."visible"'; + break; + } + qb.orderByRaw(`${column} ${sortEntry.direction}`); + } } private withCount(qb: Knex.QueryBuilder) { - return qb.offset(0).clearSelect().count(); + return qb.offset(0).clearSelect().clearOrder().count(); } } diff --git a/packages/api/cms-api/src/dependencies/dependents.resolver.factory.ts b/packages/api/cms-api/src/dependencies/dependents.resolver.factory.ts index aa3267066b8..6893d86f48b 100644 --- a/packages/api/cms-api/src/dependencies/dependents.resolver.factory.ts +++ b/packages/api/cms-api/src/dependencies/dependents.resolver.factory.ts @@ -17,9 +17,9 @@ export class DependentsResolverFactory { @ResolveField(() => PaginatedDependencies) async dependents( @Parent() node: AnyEntity<{ id: string }>, - @Args() { filter, offset, limit, forceRefresh }: DependentsArgs, + @Args() { filter, sort, offset, limit, forceRefresh }: DependentsArgs, ): Promise { - return this.dependenciesService.getDependents(node, filter, { offset, limit }, { forceRefresh }); + return this.dependenciesService.getDependents(node, filter, { offset, limit }, { forceRefresh, sort }); } } diff --git a/packages/api/cms-api/src/dependencies/dto/dependencies.args.ts b/packages/api/cms-api/src/dependencies/dto/dependencies.args.ts index 4ad740088a2..304e33cedf9 100644 --- a/packages/api/cms-api/src/dependencies/dto/dependencies.args.ts +++ b/packages/api/cms-api/src/dependencies/dto/dependencies.args.ts @@ -1,10 +1,11 @@ import { ArgsType, Field } from "@nestjs/graphql"; import { Type } from "class-transformer"; -import { IsBoolean, ValidateNested } from "class-validator"; +import { IsBoolean, IsOptional, ValidateNested } from "class-validator"; import { OffsetBasedPaginationArgs } from "../../common/pagination/offset-based.args"; import { IsUndefinable } from "../../common/validators/is-undefinable"; import { DependencyFilter, DependentFilter } from "./dependencies.filter"; +import { DependencySort } from "./dependency-sort"; @ArgsType() export class DependenciesArgs extends OffsetBasedPaginationArgs { @@ -14,6 +15,12 @@ export class DependenciesArgs extends OffsetBasedPaginationArgs { @IsUndefinable() filter?: DependencyFilter; + @Field(() => [DependencySort], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => DependencySort) + @IsOptional() + sort?: DependencySort[]; + @Field(() => Boolean, { defaultValue: false }) @IsBoolean() forceRefresh: boolean; @@ -27,6 +34,12 @@ export class DependentsArgs extends OffsetBasedPaginationArgs { @IsUndefinable() filter?: DependentFilter; + @Field(() => [DependencySort], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => DependencySort) + @IsOptional() + sort?: DependencySort[]; + @Field(() => Boolean, { defaultValue: false }) @IsBoolean() forceRefresh: boolean; diff --git a/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts b/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts index 0c63807e37b..fd9ed50c2b0 100644 --- a/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts +++ b/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts @@ -1,6 +1,9 @@ import { Field, InputType } from "@nestjs/graphql"; -import { IsString } from "class-validator"; +import { Type } from "class-transformer"; +import { IsOptional, IsString, ValidateNested } from "class-validator"; +import { BooleanFilter } from "../../common/filter/boolean.filter"; +import { StringFilter } from "../../common/filter/string.filter"; import { IsUndefinable } from "../../common/validators/is-undefinable"; @InputType() @@ -19,6 +22,36 @@ export class DependencyFilter { @IsString() @IsUndefinable() rootColumnName?: string; + + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @Type(() => StringFilter) + @IsOptional() + name?: StringFilter; + + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @Type(() => StringFilter) + @IsOptional() + secondaryInformation?: StringFilter; + + @Field(() => BooleanFilter, { nullable: true }) + @ValidateNested() + @Type(() => BooleanFilter) + @IsOptional() + visible?: BooleanFilter; + + @Field(() => [DependencyFilter], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => DependencyFilter) + @IsOptional() + and?: DependencyFilter[]; + + @Field(() => [DependencyFilter], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => DependencyFilter) + @IsOptional() + or?: DependencyFilter[]; } @InputType() @@ -37,4 +70,34 @@ export class DependentFilter { @IsString() @IsUndefinable() rootColumnName?: string; + + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @Type(() => StringFilter) + @IsOptional() + name?: StringFilter; + + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @Type(() => StringFilter) + @IsOptional() + secondaryInformation?: StringFilter; + + @Field(() => BooleanFilter, { nullable: true }) + @ValidateNested() + @Type(() => BooleanFilter) + @IsOptional() + visible?: BooleanFilter; + + @Field(() => [DependentFilter], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => DependentFilter) + @IsOptional() + and?: DependentFilter[]; + + @Field(() => [DependentFilter], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => DependentFilter) + @IsOptional() + or?: DependentFilter[]; } diff --git a/packages/api/cms-api/src/dependencies/dto/dependency-sort.ts b/packages/api/cms-api/src/dependencies/dto/dependency-sort.ts new file mode 100644 index 00000000000..dc0edbb083c --- /dev/null +++ b/packages/api/cms-api/src/dependencies/dto/dependency-sort.ts @@ -0,0 +1,25 @@ +import { Field, InputType, registerEnumType } from "@nestjs/graphql"; +import { IsEnum } from "class-validator"; + +import { SortDirection } from "../../common/sorting/sort-direction.enum"; + +export enum DependencySortField { + name = "name", + secondaryInformation = "secondaryInformation", + graphqlObjectType = "graphqlObjectType", + visible = "visible", +} +registerEnumType(DependencySortField, { + name: "DependencySortField", +}); + +@InputType() +export class DependencySort { + @Field(() => DependencySortField) + @IsEnum(DependencySortField) + field: DependencySortField; + + @Field(() => SortDirection, { defaultValue: SortDirection.ASC }) + @IsEnum(SortDirection) + direction: SortDirection = SortDirection.ASC; +} diff --git a/packages/api/cms-api/src/index.ts b/packages/api/cms-api/src/index.ts index 315b3bc8b16..7635ce5b4e8 100644 --- a/packages/api/cms-api/src/index.ts +++ b/packages/api/cms-api/src/index.ts @@ -203,6 +203,7 @@ export { DependenciesService } from "./dependencies/dependencies.service"; export { DependentsResolverFactory } from "./dependencies/dependents.resolver.factory"; export { BaseDependencyInterface } from "./dependencies/dto/base-dependency.interface"; export { Dependency } from "./dependencies/dto/dependency"; +export { DependencySort, DependencySortField } from "./dependencies/dto/dependency-sort"; export { DocumentInterface } from "./document/dto/document-interface"; export { SaveDocument } from "./document/dto/save-document"; export { validateNotModified } from "./document/validateNotModified"; From 233fe550f37e3cd9f4c66a01b5ce8bc21b377e59 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Tue, 17 Mar 2026 23:15:18 +0100 Subject: [PATCH 02/10] Fix type filters --- demo/api/schema.gql | 4 ++-- .../src/dependencies/DependenciesList.tsx | 21 ++++++++++++++++--- .../src/dependencies/DependentsList.tsx | 21 ++++++++++++++++--- packages/api/cms-api/schema.gql | 4 ++-- .../src/dependencies/dependencies.service.ts | 4 ++-- .../dependencies/dto/dependencies.filter.ts | 18 +++++++++------- 6 files changed, 52 insertions(+), 20 deletions(-) diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 99d31f5b8b3..831fa36e807 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -547,7 +547,7 @@ input DependencyFilter { or: [DependencyFilter!] rootColumnName: String secondaryInformation: StringFilter - targetGraphqlObjectType: String + targetGraphqlObjectType: StringFilter targetId: String visible: BooleanFilter } @@ -569,7 +569,7 @@ input DependentFilter { name: StringFilter or: [DependentFilter!] rootColumnName: String - rootGraphqlObjectType: String + rootGraphqlObjectType: StringFilter rootId: String secondaryInformation: StringFilter visible: BooleanFilter diff --git a/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx b/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx index e0d58277a75..cca2600f4b7 100644 --- a/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx +++ b/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx @@ -18,7 +18,7 @@ import { import { ArrowRight, OpenNewTab, Reload, ThreeDotSaving } from "@comet/admin-icons"; import { Chip, IconButton } from "@mui/material"; import { DataGrid, type GridSlotsComponent, type GridToolbarProps } from "@mui/x-data-grid"; -import { useState } from "react"; +import { isValidElement, type ReactNode, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useHistory } from "react-router"; @@ -81,6 +81,15 @@ function DependenciesListGridToolbar({ refetch }: DependenciesListGridToolbarPro ); } +function getDisplayNameString(displayName: ReactNode, fallback: string): string { + if (typeof displayName === "string") return displayName; + if (isValidElement(displayName)) { + const { defaultMessage } = displayName.props as { defaultMessage?: string }; + if (typeof defaultMessage === "string") return defaultMessage; + } + return fallback; +} + const pageSize = 10; export interface DependenciesListProps { @@ -128,12 +137,18 @@ export const DependenciesList = ({ query, variables }: DependenciesListProps) => type: "singleSelect", valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ value, - label: dep.displayName as string, + label: getDisplayNameString(dep.displayName, value), })), sortBy: "graphqlObjectType", toGqlFilter: (filterItem) => { if (!filterItem.value) return {}; - return { targetGraphqlObjectType: filterItem.value }; + if (filterItem.operator === "isAnyOf") { + return { targetGraphqlObjectType: { isAnyOf: filterItem.value } }; + } + if (filterItem.operator === "not") { + return { targetGraphqlObjectType: { notEqual: filterItem.value } }; + } + return { targetGraphqlObjectType: { equal: filterItem.value } }; }, renderCell: ({ row }) => , }, diff --git a/packages/admin/cms-admin/src/dependencies/DependentsList.tsx b/packages/admin/cms-admin/src/dependencies/DependentsList.tsx index 897911a3509..9c7848ed4f4 100644 --- a/packages/admin/cms-admin/src/dependencies/DependentsList.tsx +++ b/packages/admin/cms-admin/src/dependencies/DependentsList.tsx @@ -18,7 +18,7 @@ import { import { ArrowRight, OpenNewTab, Reload, ThreeDotSaving } from "@comet/admin-icons"; import { Chip, IconButton } from "@mui/material"; import { DataGrid, type GridSlotsComponent, type GridToolbarProps } from "@mui/x-data-grid"; -import { useState } from "react"; +import { isValidElement, type ReactNode, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useHistory } from "react-router"; @@ -81,6 +81,15 @@ function DependentsListGridToolbar({ refetch }: DependentsListGridToolbarProps) ); } +function getDisplayNameString(displayName: ReactNode, fallback: string): string { + if (typeof displayName === "string") return displayName; + if (isValidElement(displayName)) { + const { defaultMessage } = displayName.props as { defaultMessage?: string }; + if (typeof defaultMessage === "string") return defaultMessage; + } + return fallback; +} + const pageSize = 10; export interface DependentsListProps { @@ -128,12 +137,18 @@ export const DependentsList = ({ query, variables }: DependentsListProps) => { type: "singleSelect", valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ value, - label: dep.displayName as string, + label: getDisplayNameString(dep.displayName, value), })), sortBy: "graphqlObjectType", toGqlFilter: (filterItem) => { if (!filterItem.value) return {}; - return { rootGraphqlObjectType: filterItem.value }; + if (filterItem.operator === "isAnyOf") { + return { rootGraphqlObjectType: { isAnyOf: filterItem.value } }; + } + if (filterItem.operator === "not") { + return { rootGraphqlObjectType: { notEqual: filterItem.value } }; + } + return { rootGraphqlObjectType: { equal: filterItem.value } }; }, renderCell: ({ row }) => , }, diff --git a/packages/api/cms-api/schema.gql b/packages/api/cms-api/schema.gql index 21409b62dc9..16845aa6899 100644 --- a/packages/api/cms-api/schema.gql +++ b/packages/api/cms-api/schema.gql @@ -336,7 +336,7 @@ interface DocumentInterface { } input DependentFilter { - rootGraphqlObjectType: String + rootGraphqlObjectType: StringFilter rootId: String rootColumnName: String name: StringFilter @@ -403,7 +403,7 @@ enum RedirectGenerationType { } input DependencyFilter { - targetGraphqlObjectType: String + targetGraphqlObjectType: StringFilter targetId: String rootColumnName: String name: StringFilter diff --git a/packages/api/cms-api/src/dependencies/dependencies.service.ts b/packages/api/cms-api/src/dependencies/dependencies.service.ts index dcd3d9a5024..f20770d40fc 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.service.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.service.ts @@ -309,10 +309,10 @@ export class DependenciesService { if (!filter) return; if ("rootGraphqlObjectType" in filter && filter.rootGraphqlObjectType) { - qb.andWhere({ "idx.rootGraphqlObjectType": filter.rootGraphqlObjectType }); + this.applyStringFilterToKnex(qb, '"idx"."rootGraphqlObjectType"', filter.rootGraphqlObjectType); } if ("targetGraphqlObjectType" in filter && filter.targetGraphqlObjectType) { - qb.andWhere({ "idx.targetGraphqlObjectType": filter.targetGraphqlObjectType }); + this.applyStringFilterToKnex(qb, '"idx"."targetGraphqlObjectType"', filter.targetGraphqlObjectType); } if (filter.rootColumnName) { qb.andWhere({ "idx.rootColumnName": filter.rootColumnName }); diff --git a/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts b/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts index fd9ed50c2b0..840f6cd9312 100644 --- a/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts +++ b/packages/api/cms-api/src/dependencies/dto/dependencies.filter.ts @@ -8,10 +8,11 @@ import { IsUndefinable } from "../../common/validators/is-undefinable"; @InputType() export class DependencyFilter { - @Field({ nullable: true }) - @IsString() - @IsUndefinable() - targetGraphqlObjectType?: string; + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @Type(() => StringFilter) + @IsOptional() + targetGraphqlObjectType?: StringFilter; @Field({ nullable: true }) @IsString() @@ -56,10 +57,11 @@ export class DependencyFilter { @InputType() export class DependentFilter { - @Field({ nullable: true }) - @IsString() - @IsUndefinable() - rootGraphqlObjectType?: string; + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @Type(() => StringFilter) + @IsOptional() + rootGraphqlObjectType?: StringFilter; @Field({ nullable: true }) @IsString() From 56e09743e050cd9d5f1a3d7605e0745fa0b3327e Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Tue, 17 Mar 2026 23:44:36 +0100 Subject: [PATCH 03/10] Wrap columns in useMemo as done by generator --- .../src/dependencies/DependenciesList.tsx | 189 +++++++++--------- .../src/dependencies/DependentsList.tsx | 189 +++++++++--------- 2 files changed, 192 insertions(+), 186 deletions(-) diff --git a/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx b/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx index cca2600f4b7..c211283aada 100644 --- a/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx +++ b/packages/admin/cms-admin/src/dependencies/DependenciesList.tsx @@ -18,7 +18,7 @@ import { import { ArrowRight, OpenNewTab, Reload, ThreeDotSaving } from "@comet/admin-icons"; import { Chip, IconButton } from "@mui/material"; import { DataGrid, type GridSlotsComponent, type GridToolbarProps } from "@mui/x-data-grid"; -import { isValidElement, type ReactNode, useState } from "react"; +import { isValidElement, type ReactNode, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useHistory } from "react-router"; @@ -115,106 +115,109 @@ export const DependenciesList = ({ query, variables }: DependenciesListProps) => ...usePersistentColumnState("DependenciesList"), }; - const columns: GridColDef[] = [ - { - field: "name", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.nameAndInfo", defaultMessage: "Name/Info" }), - flex: 1, - sortBy: "name", - renderCell: ({ row }) => ( - } secondaryText={row.secondaryInformation} /> - ), - }, - { - field: "secondaryInformation", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.secondaryInformation", defaultMessage: "Secondary information" }), - sortBy: "secondaryInformation", - visible: false, - }, - { - field: "type", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.type", defaultMessage: "Type" }), - type: "singleSelect", - valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ - value, - label: getDisplayNameString(dep.displayName, value), - })), - sortBy: "graphqlObjectType", - toGqlFilter: (filterItem) => { - if (!filterItem.value) return {}; - if (filterItem.operator === "isAnyOf") { - return { targetGraphqlObjectType: { isAnyOf: filterItem.value } }; - } - if (filterItem.operator === "not") { - return { targetGraphqlObjectType: { notEqual: filterItem.value } }; - } - return { targetGraphqlObjectType: { equal: filterItem.value } }; + const columns: GridColDef[] = useMemo( + () => [ + { + field: "name", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.nameAndInfo", defaultMessage: "Name/Info" }), + flex: 1, + sortBy: "name", + renderCell: ({ row }) => ( + } secondaryText={row.secondaryInformation} /> + ), }, - renderCell: ({ row }) => , - }, - { - field: "visible", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.visible", defaultMessage: "Visible" }), - type: "boolean", - visible: false, - sortBy: "visible", - toGqlFilter: (filterItem) => { - if (filterItem.value === undefined || filterItem.value === "") return {}; - return { visible: { equal: filterItem.value === "true" || filterItem.value === true } } as unknown as GqlFilter; + { + field: "secondaryInformation", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.secondaryInformation", defaultMessage: "Secondary information" }), + sortBy: "secondaryInformation", + visible: false, }, - }, - { - field: "actions", - type: "actions", - headerName: "", - filterable: false, - sortable: false, - renderCell: ({ row }) => { - const dependencyObject = entityDependencyMap[row.graphqlObjectType] as DependencyInterface | undefined; + { + field: "type", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.type", defaultMessage: "Type" }), + type: "singleSelect", + valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ + value, + label: getDisplayNameString(dep.displayName, value), + })), + sortBy: "graphqlObjectType", + toGqlFilter: (filterItem) => { + if (!filterItem.value) return {}; + if (filterItem.operator === "isAnyOf") { + return { targetGraphqlObjectType: { isAnyOf: filterItem.value } }; + } + if (filterItem.operator === "not") { + return { targetGraphqlObjectType: { notEqual: filterItem.value } }; + } + return { targetGraphqlObjectType: { equal: filterItem.value } }; + }, + renderCell: ({ row }) => , + }, + { + field: "visible", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.visible", defaultMessage: "Visible" }), + type: "boolean", + visible: false, + sortBy: "visible", + toGqlFilter: (filterItem) => { + if (filterItem.value === undefined || filterItem.value === "") return {}; + return { visible: { equal: filterItem.value === "true" || filterItem.value === true } } as unknown as GqlFilter; + }, + }, + { + field: "actions", + type: "actions", + headerName: "", + filterable: false, + sortable: false, + renderCell: ({ row }) => { + const dependencyObject = entityDependencyMap[row.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.graphqlObjectType} was provided via the DependenciesConfig`, - ); + if (dependencyObject === undefined) { + if (process.env.NODE_ENV === "development") { + console.warn( + `Cannot load URL because no implementation of DependencyInterface for ${row.graphqlObjectType} was provided via the DependenciesConfig`, + ); + } + return ; } - return ; - } - const loadUrl = async () => { - const path = await dependencyObject.resolvePath({ - rootColumnName: row.rootColumnName, - jsonPath: row.jsonPath, - apolloClient, - id: row.id, - }); - return contentScope.match.url + path; - }; + const loadUrl = async () => { + const path = await dependencyObject.resolvePath({ + rootColumnName: row.rootColumnName, + jsonPath: row.jsonPath, + apolloClient, + id: row.id, + }); + return contentScope.match.url + path; + }; - return ( -
- { - const url = await loadUrl(); - window.open(url, "_blank"); - }} - > - - - { - const url = await loadUrl(); + return ( +
+ { + const url = await loadUrl(); + window.open(url, "_blank"); + }} + > + + + { + const url = await loadUrl(); - history.push(url); - }} - > - - -
- ); + history.push(url); + }} + > + +
+
+ ); + }, }, - }, - ]; + ], + [intl, entityDependencyMap, apolloClient, contentScope, history], + ); const { filter: gqlFilter } = muiGridFilterToGql(columns, dataGridProps.filterModel); const sort = muiGridSortToGql(dataGridProps.sortModel, columns); diff --git a/packages/admin/cms-admin/src/dependencies/DependentsList.tsx b/packages/admin/cms-admin/src/dependencies/DependentsList.tsx index 9c7848ed4f4..5aed9f7f22e 100644 --- a/packages/admin/cms-admin/src/dependencies/DependentsList.tsx +++ b/packages/admin/cms-admin/src/dependencies/DependentsList.tsx @@ -18,7 +18,7 @@ import { import { ArrowRight, OpenNewTab, Reload, ThreeDotSaving } from "@comet/admin-icons"; import { Chip, IconButton } from "@mui/material"; import { DataGrid, type GridSlotsComponent, type GridToolbarProps } from "@mui/x-data-grid"; -import { isValidElement, type ReactNode, useState } from "react"; +import { isValidElement, type ReactNode, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useHistory } from "react-router"; @@ -115,106 +115,109 @@ export const DependentsList = ({ query, variables }: DependentsListProps) => { ...usePersistentColumnState("DependentsList"), }; - const columns: GridColDef[] = [ - { - field: "name", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.nameAndInfo", defaultMessage: "Name/Info" }), - flex: 1, - sortBy: "name", - renderCell: ({ row }) => ( - } secondaryText={row.secondaryInformation} /> - ), - }, - { - field: "secondaryInformation", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.secondaryInformation", defaultMessage: "Secondary information" }), - sortBy: "secondaryInformation", - visible: false, - }, - { - field: "type", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.type", defaultMessage: "Type" }), - type: "singleSelect", - valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ - value, - label: getDisplayNameString(dep.displayName, value), - })), - sortBy: "graphqlObjectType", - toGqlFilter: (filterItem) => { - if (!filterItem.value) return {}; - if (filterItem.operator === "isAnyOf") { - return { rootGraphqlObjectType: { isAnyOf: filterItem.value } }; - } - if (filterItem.operator === "not") { - return { rootGraphqlObjectType: { notEqual: filterItem.value } }; - } - return { rootGraphqlObjectType: { equal: filterItem.value } }; + const columns: GridColDef[] = useMemo( + () => [ + { + field: "name", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.nameAndInfo", defaultMessage: "Name/Info" }), + flex: 1, + sortBy: "name", + renderCell: ({ row }) => ( + } secondaryText={row.secondaryInformation} /> + ), }, - renderCell: ({ row }) => , - }, - { - field: "visible", - headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.visible", defaultMessage: "Visible" }), - type: "boolean", - visible: false, - sortBy: "visible", - toGqlFilter: (filterItem) => { - if (filterItem.value === undefined || filterItem.value === "") return {}; - return { visible: { equal: filterItem.value === "true" || filterItem.value === true } } as unknown as GqlFilter; + { + field: "secondaryInformation", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.secondaryInformation", defaultMessage: "Secondary information" }), + sortBy: "secondaryInformation", + visible: false, }, - }, - { - field: "actions", - type: "actions", - headerName: "", - filterable: false, - sortable: false, - renderCell: ({ row }) => { - const dependencyObject = entityDependencyMap[row.graphqlObjectType] as DependencyInterface | undefined; + { + field: "type", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.type", defaultMessage: "Type" }), + type: "singleSelect", + valueOptions: Object.entries(entityDependencyMap).map(([value, dep]) => ({ + value, + label: getDisplayNameString(dep.displayName, value), + })), + sortBy: "graphqlObjectType", + toGqlFilter: (filterItem) => { + if (!filterItem.value) return {}; + if (filterItem.operator === "isAnyOf") { + return { rootGraphqlObjectType: { isAnyOf: filterItem.value } }; + } + if (filterItem.operator === "not") { + return { rootGraphqlObjectType: { notEqual: filterItem.value } }; + } + return { rootGraphqlObjectType: { equal: filterItem.value } }; + }, + renderCell: ({ row }) => , + }, + { + field: "visible", + headerName: intl.formatMessage({ id: "comet.dependencies.dataGrid.visible", defaultMessage: "Visible" }), + type: "boolean", + visible: false, + sortBy: "visible", + toGqlFilter: (filterItem) => { + if (filterItem.value === undefined || filterItem.value === "") return {}; + return { visible: { equal: filterItem.value === "true" || filterItem.value === true } } as unknown as GqlFilter; + }, + }, + { + field: "actions", + type: "actions", + headerName: "", + filterable: false, + sortable: false, + renderCell: ({ row }) => { + const dependencyObject = entityDependencyMap[row.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.graphqlObjectType} was provided via the DependenciesConfig`, - ); + if (dependencyObject === undefined) { + if (process.env.NODE_ENV === "development") { + console.warn( + `Cannot load URL because no implementation of DependencyInterface for ${row.graphqlObjectType} was provided via the DependenciesConfig`, + ); + } + return ; } - return ; - } - const loadUrl = async () => { - const path = await dependencyObject.resolvePath({ - rootColumnName: row.rootColumnName, - jsonPath: row.jsonPath, - apolloClient, - id: row.id, - }); - return contentScope.match.url + path; - }; + const loadUrl = async () => { + const path = await dependencyObject.resolvePath({ + rootColumnName: row.rootColumnName, + jsonPath: row.jsonPath, + apolloClient, + id: row.id, + }); + return contentScope.match.url + path; + }; - return ( -
- { - const url = await loadUrl(); - window.open(url, "_blank"); - }} - > - - - { - const url = await loadUrl(); + return ( +
+ { + const url = await loadUrl(); + window.open(url, "_blank"); + }} + > + + + { + const url = await loadUrl(); - history.push(url); - }} - > - - -
- ); + history.push(url); + }} + > + +
+
+ ); + }, }, - }, - ]; + ], + [intl, entityDependencyMap, apolloClient, contentScope, history], + ); const { filter: gqlFilter } = muiGridFilterToGql(columns, dataGridProps.filterModel); const sort = muiGridSortToGql(dataGridProps.sortModel, columns); From a781dc5e3df79529f3b2f006488cf4a42a3f2146 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Tue, 17 Mar 2026 23:59:17 +0100 Subject: [PATCH 04/10] Update docs --- docs/docs/2-core-concepts/7-dependencies/index.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/docs/2-core-concepts/7-dependencies/index.md b/docs/docs/2-core-concepts/7-dependencies/index.md index 4f219e82c62..8f3bf2b5c21 100644 --- a/docs/docs/2-core-concepts/7-dependencies/index.md +++ b/docs/docs/2-core-concepts/7-dependencies/index.md @@ -306,10 +306,12 @@ Each component requires two props: $offset: Int! $limit: Int! $forceRefresh: Boolean = false + $filter: DependentFilter + $sort: [DependencySort!] ) { item: damFile(id: $id) { id - dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh) { + dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh, filter: $filter, sort: $sort) { nodes { rootGraphqlObjectType rootId @@ -317,6 +319,7 @@ Each component requires two props: jsonPath name secondaryInformation + visible } totalCount } From ad343bcd4685ae8bf09b512ad3099d0100c9cab0 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Wed, 18 Mar 2026 00:10:36 +0100 Subject: [PATCH 05/10] Fix filter for dependents and dependencies --- .../src/dependencies/dependencies.service.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/api/cms-api/src/dependencies/dependencies.service.ts b/packages/api/cms-api/src/dependencies/dependencies.service.ts index f20770d40fc..3903bdd5739 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.service.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.service.ts @@ -221,17 +221,21 @@ export class DependenciesService { async getDependents( target: AnyEntity<{ id: string }> | { entityName: string; id: string }, - filter?: DependentFilter & { + passedFilter?: DependentFilter & { rootEntityName?: string; }, paginationArgs?: { offset: number; limit: number }, options?: { forceRefresh: boolean; sort?: DependencySort[] }, ): Promise { await this.refreshViews({ force: options?.forceRefresh }); + const { rootEntityName, ...filter } = passedFilter ?? {}; const entityName = "entityName" in target ? target.entityName : target.constructor.name; - const qb = this.getBaseQueryBuilder({ targetEntityName: entityName, targetId: target.id }, paginationArgs).join("EntityInfo", (join) => { + const qb = this.getBaseQueryBuilder( + { targetEntityName: entityName, targetId: target.id, rootEntityName: rootEntityName }, + paginationArgs, + ).join("EntityInfo", (join) => { join.on("idx.rootEntityName", "EntityInfo.entityName").andOn( "EntityInfo.id", this.entityManager.getKnex("read").raw('"idx"."rootId"::text'), @@ -251,22 +255,26 @@ export class DependenciesService { async getDependencies( root: AnyEntity<{ id: string }> | { entityName: string; id: string }, - filter?: DependencyFilter & { + passedFilter?: DependencyFilter & { targetEntityName?: string; }, paginationArgs?: { offset: number; limit: number }, options?: { forceRefresh: boolean; sort?: DependencySort[] }, ): Promise { await this.refreshViews({ force: options?.forceRefresh }); + const { targetEntityName, ...filter } = passedFilter ?? {}; const entityName = "entityName" in root ? root.entityName : root.constructor.name; - const qb = this.getBaseQueryBuilder({ rootEntityName: entityName, rootId: root.id }, paginationArgs).join("EntityInfo", (join) => { - join.on("idx.targetEntityName", "EntityInfo.entityName").andOn( - "EntityInfo.id", - this.entityManager.getKnex("read").raw('"idx"."targetId"::text'), - ); - }); + const qb = this.getBaseQueryBuilder({ rootEntityName: entityName, rootId: root.id, targetEntityName: targetEntityName }, paginationArgs).join( + "EntityInfo", + (join) => { + join.on("idx.targetEntityName", "EntityInfo.entityName").andOn( + "EntityInfo.id", + this.entityManager.getKnex("read").raw('"idx"."targetId"::text'), + ); + }, + ); this.applyFilter(qb, filter); this.applySort(qb, options?.sort, "dependencies"); @@ -311,9 +319,15 @@ export class DependenciesService { if ("rootGraphqlObjectType" in filter && filter.rootGraphqlObjectType) { this.applyStringFilterToKnex(qb, '"idx"."rootGraphqlObjectType"', filter.rootGraphqlObjectType); } + if ("rootId" in filter && filter.rootId) { + qb.andWhere({ "idx.rootId": filter.rootId }); + } if ("targetGraphqlObjectType" in filter && filter.targetGraphqlObjectType) { this.applyStringFilterToKnex(qb, '"idx"."targetGraphqlObjectType"', filter.targetGraphqlObjectType); } + if ("targetId" in filter && filter.targetId) { + qb.andWhere({ "idx.targetId": filter.targetId }); + } if (filter.rootColumnName) { qb.andWhere({ "idx.rootColumnName": filter.rootColumnName }); } From e95d45a84a5a7d9b9acfcc200aec30fb94419137 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Wed, 18 Mar 2026 00:20:06 +0100 Subject: [PATCH 06/10] Remove unnecessary library export --- packages/api/cms-api/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/api/cms-api/src/index.ts b/packages/api/cms-api/src/index.ts index 7635ce5b4e8..315b3bc8b16 100644 --- a/packages/api/cms-api/src/index.ts +++ b/packages/api/cms-api/src/index.ts @@ -203,7 +203,6 @@ export { DependenciesService } from "./dependencies/dependencies.service"; export { DependentsResolverFactory } from "./dependencies/dependents.resolver.factory"; export { BaseDependencyInterface } from "./dependencies/dto/base-dependency.interface"; export { Dependency } from "./dependencies/dto/dependency"; -export { DependencySort, DependencySortField } from "./dependencies/dto/dependency-sort"; export { DocumentInterface } from "./document/dto/document-interface"; export { SaveDocument } from "./document/dto/save-document"; export { validateNotModified } from "./document/validateNotModified"; From b5458c5146fa0a028356aae6155fbf5c27563dbc Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Wed, 18 Mar 2026 00:45:18 +0100 Subject: [PATCH 07/10] Add breaking changes to changeset --- .changeset/deps-list-filter-sort.md | 50 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/.changeset/deps-list-filter-sort.md b/.changeset/deps-list-filter-sort.md index 8c7a2a78b41..62e789bc8f0 100644 --- a/.changeset/deps-list-filter-sort.md +++ b/.changeset/deps-list-filter-sort.md @@ -1,9 +1,55 @@ --- -"@comet/cms-admin": minor -"@comet/cms-api": minor +"@comet/cms-admin": major +"@comet/cms-api": major "@comet/admin": minor --- Add filtering and sorting to `DependenciesList` and `DependentsList` Users can now filter dependencies/dependents by name, type, secondary information, and visibility, and sort by all columns. A default filter shows only visible items. The `GqlFilter` type is now exported from `@comet/admin`. + +**Breaking changes:** + +**`@comet/cms-api`:** `DependencyFilter.targetGraphqlObjectType` and `DependentFilter.rootGraphqlObjectType` changed from `string` to `StringFilter`. Update any code passing a plain string to use `{ equal: "..." }` instead. + +**`@comet/cms-admin`:** The GQL queries passed to `DependenciesList` and `DependentsList` must now accept `$filter` and `$sort` variables and forward them to the `dependencies`/`dependents` field. Update your queries as follows: + +```graphql +# DependentsList +query MyDependents($id: ID!, $offset: Int!, $limit: Int!, $forceRefresh: Boolean = false, $filter: DependentFilter, $sort: [DependencySort!]) { + item: myEntity(id: $id) { + id + dependents(offset: $offset, limit: $limit, forceRefresh: $forceRefresh, filter: $filter, sort: $sort) { + nodes { + rootGraphqlObjectType + rootId + rootColumnName + jsonPath + name + secondaryInformation + visible + } + totalCount + } + } +} + +# DependenciesList +query MyDependencies($id: ID!, $offset: Int!, $limit: Int!, $forceRefresh: Boolean = false, $filter: DependencyFilter, $sort: [DependencySort!]) { + item: myEntity(id: $id) { + id + dependencies(offset: $offset, limit: $limit, forceRefresh: $forceRefresh, filter: $filter, sort: $sort) { + nodes { + targetGraphqlObjectType + targetId + rootColumnName + jsonPath + name + secondaryInformation + visible + } + totalCount + } + } +} +``` From 2cdb9409a51f7eae9cd6f6e50b6b8f0039d32024 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Wed, 18 Mar 2026 12:32:01 +0100 Subject: [PATCH 08/10] Check string filters for undefined and null --- .../src/dependencies/dependencies.service.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/api/cms-api/src/dependencies/dependencies.service.ts b/packages/api/cms-api/src/dependencies/dependencies.service.ts index 3903bdd5739..dddac07cd7d 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.service.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.service.ts @@ -363,25 +363,25 @@ export class DependenciesService { private applyStringFilterToKnex(qb: Knex.QueryBuilder, column: string, filter: StringFilter): void { const knex = this.entityManager.getKnex("read"); - if (filter.contains !== undefined) { + if (filter.contains !== undefined && filter.contains !== null) { qb.andWhere(knex.raw(`${column} ILIKE ?`, [`%${filter.contains}%`])); } - if (filter.notContains !== undefined) { + if (filter.notContains !== undefined && filter.notContains !== null) { qb.andWhere(knex.raw(`${column} NOT ILIKE ?`, [`%${filter.notContains}%`])); } - if (filter.startsWith !== undefined) { + if (filter.startsWith !== undefined && filter.startsWith !== null) { qb.andWhere(knex.raw(`${column} ILIKE ?`, [`${filter.startsWith}%`])); } - if (filter.endsWith !== undefined) { + if (filter.endsWith !== undefined && filter.endsWith !== null) { qb.andWhere(knex.raw(`${column} ILIKE ?`, [`%${filter.endsWith}`])); } - if (filter.equal !== undefined) { + if (filter.equal !== undefined && filter.equal !== null) { qb.andWhere(knex.raw(`${column} = ?`, [filter.equal])); } - if (filter.notEqual !== undefined) { + if (filter.notEqual !== undefined && filter.notEqual !== null) { qb.andWhere(knex.raw(`${column} != ?`, [filter.notEqual])); } - if (filter.isAnyOf !== undefined && filter.isAnyOf.length > 0) { + if (filter.isAnyOf !== undefined && filter.isAnyOf !== null && filter.isAnyOf.length > 0) { qb.andWhere(knex.raw(`${column} IN (${filter.isAnyOf.map(() => "?").join(", ")})`, filter.isAnyOf)); } if (filter.isEmpty) { From f3c9e9f084baede8e8a6d95c4037273b32ab6ee1 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Wed, 18 Mar 2026 12:34:33 +0100 Subject: [PATCH 09/10] Check visible filter for undefined and null --- packages/api/cms-api/src/dependencies/dependencies.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/cms-api/src/dependencies/dependencies.service.ts b/packages/api/cms-api/src/dependencies/dependencies.service.ts index dddac07cd7d..4960fb11832 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.service.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.service.ts @@ -338,7 +338,7 @@ export class DependenciesService { this.applyStringFilterToKnex(qb, '"EntityInfo"."secondaryInformation"', filter.secondaryInformation); } if (filter.visible) { - if (filter.visible.equal !== undefined) { + if (filter.visible.equal !== undefined && filter.visible.equal !== null) { qb.andWhere(this.entityManager.getKnex("read").raw('"idx"."visible" = ?', [filter.visible.equal])); } } From 690f5a0dcb19b514615de01ede6a132ce3886c87 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Wed, 18 Mar 2026 12:50:15 +0100 Subject: [PATCH 10/10] Skip empty and/or filter arrays in dependencies query --- packages/api/cms-api/src/dependencies/dependencies.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/cms-api/src/dependencies/dependencies.service.ts b/packages/api/cms-api/src/dependencies/dependencies.service.ts index 4960fb11832..c0c2d53ced8 100644 --- a/packages/api/cms-api/src/dependencies/dependencies.service.ts +++ b/packages/api/cms-api/src/dependencies/dependencies.service.ts @@ -342,14 +342,14 @@ export class DependenciesService { qb.andWhere(this.entityManager.getKnex("read").raw('"idx"."visible" = ?', [filter.visible.equal])); } } - if (filter.and) { + if (filter.and?.length) { for (const subFilter of filter.and) { qb.andWhere((sub) => { this.applyFilter(sub, subFilter); }); } } - if (filter.or) { + if (filter.or?.length) { const orFilters = filter.or; qb.andWhere((outer) => { for (const subFilter of orFilters) {