From d33be8663779b6a1bf958c3966e3ccfc9ca62399 Mon Sep 17 00:00:00 2001 From: Hakan Shehu Date: Wed, 9 Jul 2025 07:45:07 +0200 Subject: [PATCH] Add number of records for each board column (#122) --- .../records/record-field-value-count.ts | 72 +++++++++++++------ .../records/record-field-value-count.ts | 3 +- .../board-view-columns-collaborator.tsx | 19 ++++- .../boards/board-view-columns-created-by.tsx | 14 +++- .../board-view-columns-multi-select.tsx | 70 +++++++++++++++++- .../boards/board-view-columns-select.tsx | 70 +++++++++++++++++- 6 files changed, 218 insertions(+), 30 deletions(-) diff --git a/packages/client/src/handlers/queries/records/record-field-value-count.ts b/packages/client/src/handlers/queries/records/record-field-value-count.ts index 9555fd02..df55e154 100644 --- a/packages/client/src/handlers/queries/records/record-field-value-count.ts +++ b/packages/client/src/handlers/queries/records/record-field-value-count.ts @@ -19,10 +19,8 @@ export class RecordFieldValueCountQueryHandler public async handleQuery( input: RecordFieldValueCountQueryInput ): Promise { - const counts = await this.fetchFieldValueCounts(input); - return { - items: counts, - }; + const result = await this.fetchFieldValueCounts(input); + return result; } public async checkForChanges( @@ -37,7 +35,7 @@ export class RecordFieldValueCountQueryHandler ) { return { hasChanges: true, - result: { items: [] }, + result: { values: [], nullCount: 0 }, }; } @@ -87,6 +85,16 @@ export class RecordFieldValueCountQueryHandler event.accountId === input.accountId && event.workspaceId === input.workspaceId ) { + if ( + event.node.type === 'database' && + event.node.id === input.databaseId + ) { + return { + hasChanges: true, + result: { values: [], nullCount: 0 }, + }; + } + if ( event.node.type === 'record' && event.node.attributes.databaseId === input.databaseId @@ -97,16 +105,6 @@ export class RecordFieldValueCountQueryHandler result: newResult, }; } - - if ( - event.node.type === 'database' && - event.node.id === input.databaseId - ) { - return { - hasChanges: true, - result: { items: [] }, - }; - } } return { @@ -116,12 +114,15 @@ export class RecordFieldValueCountQueryHandler private async fetchFieldValueCounts( input: RecordFieldValueCountQueryInput - ): Promise { + ): Promise { const database = await this.fetchDatabase(input); const field = database.attributes.fields[input.fieldId]; if (!field) { - return []; + return { + values: [], + nullCount: 0, + }; } const filterQuery = buildFiltersQuery( @@ -137,7 +138,24 @@ export class RecordFieldValueCountQueryHandler ); const result = await workspace.database.executeQuery(query); - return result.rows; + + const output: RecordFieldValueCountQueryOutput = { + values: [], + nullCount: 0, + }; + + for (const row of result.rows) { + if (row.value === 'null') { + output.nullCount = row.count; + } else { + output.values.push({ + value: row.value, + count: row.count, + }); + } + } + + return output; } private buildQuery( @@ -190,10 +208,23 @@ export class RecordFieldValueCountQueryHandler FROM nodes n, json_each(${this.buildFieldSelector(field)}) WHERE n.parent_id = '${databaseId}' - AND n.type = 'record' - AND ${this.buildFieldSelector(field)} IS NOT NULL + AND n.type = 'record' ${filterQuery} GROUP BY json_each.value + + UNION ALL + + SELECT + 'null' as value, + COUNT(*) as count + FROM nodes n + WHERE n.parent_id = '${databaseId}' + AND n.type = 'record' + AND (${this.buildFieldSelector(field)} IS NULL + OR ${this.buildFieldSelector(field)} = '[]' + OR json_array_length(${this.buildFieldSelector(field)}) = 0) + ${filterQuery} + ORDER BY count DESC, value ASC `; } @@ -210,7 +241,6 @@ export class RecordFieldValueCountQueryHandler FROM nodes n WHERE n.parent_id = '${databaseId}' AND n.type = 'record' - AND ${this.buildFieldSelector(field)} IS NOT NULL ${filterQuery} GROUP BY value ORDER BY count DESC, value ASC diff --git a/packages/client/src/queries/records/record-field-value-count.ts b/packages/client/src/queries/records/record-field-value-count.ts index 8d69477c..1d64a6e4 100644 --- a/packages/client/src/queries/records/record-field-value-count.ts +++ b/packages/client/src/queries/records/record-field-value-count.ts @@ -15,7 +15,8 @@ export type RecordFieldValueCount = { }; export type RecordFieldValueCountQueryOutput = { - items: RecordFieldValueCount[]; + values: RecordFieldValueCount[]; + nullCount: number; }; declare module '@colanode/client/queries' { diff --git a/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx b/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx index 9443b470..29d642c5 100644 --- a/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx +++ b/packages/ui/src/components/databases/boards/board-view-columns-collaborator.tsx @@ -39,13 +39,14 @@ export const BoardViewColumnsCollaborator = ({ return null; } - const collaborators = collaboratorCountQuery.data?.items ?? []; + const collaborators = collaboratorCountQuery.data?.values ?? []; const noValueFilter: DatabaseViewFilterAttributes = { id: '1', type: 'field', fieldId: field.id, operator: 'is_empty', }; + const noValueCount = collaboratorCountQuery.data?.nullCount ?? 0; return ( <> @@ -75,6 +76,7 @@ export const BoardViewColumnsCollaborator = ({ ), canDrag: (record) => record.canEdit, @@ -145,6 +147,7 @@ export const BoardViewColumnsCollaborator = ({ ), canDrag: () => true, @@ -187,11 +190,13 @@ export const BoardViewColumnsCollaborator = ({ interface BoardViewColumnCollaboratorHeaderProps { field: CollaboratorFieldAttributes; collaborator: string | null; + count: number; } const BoardViewColumnCollaboratorHeader = ({ field, collaborator, + count, }: BoardViewColumnCollaboratorHeaderProps) => { const workspace = useWorkspace(); @@ -212,6 +217,9 @@ const BoardViewColumnCollaboratorHeader = ({

No {field.name}

+

+ {count.toLocaleString()} +

); } @@ -221,6 +229,9 @@ const BoardViewColumnCollaboratorHeader = ({

Loading...

+

+ {count.toLocaleString()} +

); } @@ -231,6 +242,9 @@ const BoardViewColumnCollaboratorHeader = ({

Unknown

+

+ {count.toLocaleString()} +

); } @@ -244,6 +258,9 @@ const BoardViewColumnCollaboratorHeader = ({ className="size-5" />

{user.name}

+

+ {count.toLocaleString()} +

); }; diff --git a/packages/ui/src/components/databases/boards/board-view-columns-created-by.tsx b/packages/ui/src/components/databases/boards/board-view-columns-created-by.tsx index 97328e74..8c263eb7 100644 --- a/packages/ui/src/components/databases/boards/board-view-columns-created-by.tsx +++ b/packages/ui/src/components/databases/boards/board-view-columns-created-by.tsx @@ -37,7 +37,7 @@ export const BoardViewColumnsCreatedBy = ({ return null; } - const users = createdByCountQuery.data?.items ?? []; + const users = createdByCountQuery.data?.values ?? []; return ( <> @@ -62,6 +62,7 @@ export const BoardViewColumnsCreatedBy = ({ ), canDrag: () => false, @@ -79,10 +80,12 @@ export const BoardViewColumnsCreatedBy = ({ interface BoardViewColumnCreatedByHeaderProps { field: CreatedByFieldAttributes; createdBy: string; + count: number; } const BoardViewColumnCreatedByHeader = ({ createdBy, + count, }: BoardViewColumnCreatedByHeaderProps) => { const workspace = useWorkspace(); @@ -98,6 +101,9 @@ const BoardViewColumnCreatedByHeader = ({

Loading...

+

+ {count.toLocaleString()} +

); } @@ -108,6 +114,9 @@ const BoardViewColumnCreatedByHeader = ({

Unknown

+

+ {count.toLocaleString()} +

); } @@ -121,6 +130,9 @@ const BoardViewColumnCreatedByHeader = ({ className="size-5" />

{user.name}

+

+ {count.toLocaleString()} +

); }; diff --git a/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx b/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx index eb975c57..7a1a8ba7 100644 --- a/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx +++ b/packages/ui/src/components/databases/boards/board-view-columns-multi-select.tsx @@ -1,14 +1,19 @@ +import { CircleDashed } from 'lucide-react'; import { toast } from 'sonner'; import { DatabaseViewFilterAttributes, FieldValue, MultiSelectFieldAttributes, + SelectOptionAttributes, } from '@colanode/core'; import { BoardViewColumn } from '@colanode/ui/components/databases/boards/board-view-column'; import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge'; import { BoardViewContext } from '@colanode/ui/contexts/board-view'; +import { useDatabase } from '@colanode/ui/contexts/database'; +import { useDatabaseView } from '@colanode/ui/contexts/database-view'; import { useWorkspace } from '@colanode/ui/contexts/workspace'; +import { useQuery } from '@colanode/ui/hooks/use-query'; import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases'; interface BoardViewColumnsMultiSelectProps { @@ -19,8 +24,19 @@ export const BoardViewColumnsMultiSelect = ({ field, }: BoardViewColumnsMultiSelectProps) => { const workspace = useWorkspace(); - const selectOptions = Object.values(field.options ?? {}); + const database = useDatabase(); + const view = useDatabaseView(); + const selectOptionCountQuery = useQuery({ + type: 'record.field.value.count', + databaseId: database.id, + filters: view.filters, + fieldId: field.id, + accountId: workspace.accountId, + workspaceId: workspace.id, + }); + + const selectOptions = Object.values(field.options ?? {}); const noValueFilter: DatabaseViewFilterAttributes = { id: '1', type: 'field', @@ -28,6 +44,9 @@ export const BoardViewColumnsMultiSelect = ({ operator: 'is_empty', }; + const selectOptionCount = selectOptionCountQuery.data?.values ?? []; + const noValueCount = selectOptionCountQuery.data?.nullCount ?? 0; + const noValueDraggingClass = getSelectOptionLightColorClass('gray'); return ( @@ -45,6 +64,10 @@ export const BoardViewColumnsMultiSelect = ({ option.color ?? 'gray' ); + const count = + selectOptionCount.find((count) => count.value === option.id)?.count ?? + 0; + return ( + ), canDrag: (record) => record.canEdit, onDragEnd: async (record, value) => { @@ -126,7 +153,11 @@ export const BoardViewColumnsMultiSelect = ({ return null; }, header: ( -

No {field.name}

+ ), dragOverClass: noValueDraggingClass, canDrag: () => true, @@ -165,3 +196,36 @@ export const BoardViewColumnsMultiSelect = ({ ); }; + +interface BoardViewColumnMultiSelectHeaderProps { + field: MultiSelectFieldAttributes; + option: SelectOptionAttributes | null; + count: number; +} + +const BoardViewColumnMultiSelectHeader = ({ + field, + option, + count, +}: BoardViewColumnMultiSelectHeaderProps) => { + if (!option) { + return ( +
+ +

No {field.name}

+

+ {count.toLocaleString()} +

+
+ ); + } + + return ( +
+ +

+ {count.toLocaleString()} +

+
+ ); +}; diff --git a/packages/ui/src/components/databases/boards/board-view-columns-select.tsx b/packages/ui/src/components/databases/boards/board-view-columns-select.tsx index 02017454..282b5380 100644 --- a/packages/ui/src/components/databases/boards/board-view-columns-select.tsx +++ b/packages/ui/src/components/databases/boards/board-view-columns-select.tsx @@ -1,13 +1,18 @@ +import { CircleDashed } from 'lucide-react'; import { toast } from 'sonner'; import { DatabaseViewFilterAttributes, SelectFieldAttributes, + SelectOptionAttributes, } from '@colanode/core'; import { BoardViewColumn } from '@colanode/ui/components/databases/boards/board-view-column'; import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge'; import { BoardViewContext } from '@colanode/ui/contexts/board-view'; +import { useDatabase } from '@colanode/ui/contexts/database'; +import { useDatabaseView } from '@colanode/ui/contexts/database-view'; import { useWorkspace } from '@colanode/ui/contexts/workspace'; +import { useQuery } from '@colanode/ui/hooks/use-query'; import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases'; interface BoardViewColumnsSelectProps { @@ -18,8 +23,19 @@ export const BoardViewColumnsSelect = ({ field, }: BoardViewColumnsSelectProps) => { const workspace = useWorkspace(); - const selectOptions = Object.values(field.options ?? {}); + const database = useDatabase(); + const view = useDatabaseView(); + const selectOptionCountQuery = useQuery({ + type: 'record.field.value.count', + databaseId: database.id, + filters: view.filters, + fieldId: field.id, + accountId: workspace.accountId, + workspaceId: workspace.id, + }); + + const selectOptions = Object.values(field.options ?? {}); const noValueFilter: DatabaseViewFilterAttributes = { id: '1', type: 'field', @@ -27,6 +43,9 @@ export const BoardViewColumnsSelect = ({ operator: 'is_empty', }; + const selectOptionCount = selectOptionCountQuery.data?.values ?? []; + const noValueCount = selectOptionCountQuery.data?.nullCount ?? 0; + const noValueDraggingClass = getSelectOptionLightColorClass('gray'); return ( @@ -44,6 +63,10 @@ export const BoardViewColumnsSelect = ({ option.color ?? 'gray' ); + const count = + selectOptionCount.find((count) => count.value === option.id)?.count ?? + 0; + return ( + ), canDrag: (record) => record.canEdit, onDragEnd: async (record, value) => { @@ -105,7 +132,11 @@ export const BoardViewColumnsSelect = ({ return null; }, header: ( -

No {field.name}

+ ), dragOverClass: noValueDraggingClass, canDrag: () => true, @@ -144,3 +175,36 @@ export const BoardViewColumnsSelect = ({ ); }; + +interface BoardViewColumnSelectHeaderProps { + field: SelectFieldAttributes; + option: SelectOptionAttributes | null; + count: number; +} + +const BoardViewColumnSelectHeader = ({ + field, + option, + count, +}: BoardViewColumnSelectHeaderProps) => { + if (!option) { + return ( +
+ +

No {field.name}

+

+ {count.toLocaleString()} +

+
+ ); + } + + return ( +
+ +

+ {count.toLocaleString()} +

+
+ ); +};