Add number of records for each board column (#122)

This commit is contained in:
Hakan Shehu
2025-07-09 07:45:07 +02:00
committed by GitHub
parent 4469301f38
commit d33be86637
6 changed files with 218 additions and 30 deletions

View File

@@ -19,10 +19,8 @@ export class RecordFieldValueCountQueryHandler
public async handleQuery(
input: RecordFieldValueCountQueryInput
): Promise<RecordFieldValueCountQueryOutput> {
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<RecordFieldValueCount[]> {
): Promise<RecordFieldValueCountQueryOutput> {
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

View File

@@ -15,7 +15,8 @@ export type RecordFieldValueCount = {
};
export type RecordFieldValueCountQueryOutput = {
items: RecordFieldValueCount[];
values: RecordFieldValueCount[];
nullCount: number;
};
declare module '@colanode/client/queries' {

View File

@@ -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 = ({
<BoardViewColumnCollaboratorHeader
field={field}
collaborator={collaborator.value}
count={collaborator.count}
/>
),
canDrag: (record) => record.canEdit,
@@ -145,6 +147,7 @@ export const BoardViewColumnsCollaborator = ({
<BoardViewColumnCollaboratorHeader
field={field}
collaborator={null}
count={noValueCount}
/>
),
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 = ({
<div className="flex flex-row gap-2 items-center">
<CircleDashed className="size-5" />
<p className="text-muted-foreground">No {field.name}</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
@@ -221,6 +229,9 @@ const BoardViewColumnCollaboratorHeader = ({
<div className="flex flex-row gap-2 items-center">
<Spinner className="size-5" />
<p className="text-muted-foreground">Loading...</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
@@ -231,6 +242,9 @@ const BoardViewColumnCollaboratorHeader = ({
<div className="flex flex-row gap-2 items-center">
<CircleAlert className="size-5" />
<p className="text-muted-foreground">Unknown</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
@@ -244,6 +258,9 @@ const BoardViewColumnCollaboratorHeader = ({
className="size-5"
/>
<p>{user.name}</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
};

View File

@@ -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 = ({
<BoardViewColumnCreatedByHeader
field={field}
createdBy={user.value}
count={user.count}
/>
),
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 = ({
<div className="flex flex-row gap-2 items-center">
<Spinner className="size-5" />
<p className="text-muted-foreground">Loading...</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
@@ -108,6 +114,9 @@ const BoardViewColumnCreatedByHeader = ({
<div className="flex flex-row gap-2 items-center">
<CircleAlert className="size-5" />
<p className="text-muted-foreground">Unknown</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
@@ -121,6 +130,9 @@ const BoardViewColumnCreatedByHeader = ({
className="size-5"
/>
<p>{user.name}</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
};

View File

@@ -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 (
<BoardViewContext.Provider
key={option.id}
@@ -60,7 +83,11 @@ export const BoardViewColumnsMultiSelect = ({
},
dragOverClass: draggingClass,
header: (
<SelectOptionBadge name={option.name} color={option.color} />
<BoardViewColumnMultiSelectHeader
field={field}
option={option}
count={count}
/>
),
canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => {
@@ -126,7 +153,11 @@ export const BoardViewColumnsMultiSelect = ({
return null;
},
header: (
<p className="text-sm text-muted-foreground">No {field.name}</p>
<BoardViewColumnMultiSelectHeader
field={field}
option={null}
count={noValueCount}
/>
),
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 (
<div className="flex flex-row gap-2 items-center">
<CircleDashed className="size-5" />
<p className="text-muted-foreground">No {field.name}</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
return (
<div className="flex flex-row gap-2 items-center">
<SelectOptionBadge name={option.name} color={option.color} />
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
};

View File

@@ -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 (
<BoardViewContext.Provider
key={option.id}
@@ -59,7 +82,11 @@ export const BoardViewColumnsSelect = ({
},
dragOverClass: draggingClass,
header: (
<SelectOptionBadge name={option.name} color={option.color} />
<BoardViewColumnSelectHeader
field={field}
option={option}
count={count}
/>
),
canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => {
@@ -105,7 +132,11 @@ export const BoardViewColumnsSelect = ({
return null;
},
header: (
<p className="text-sm text-muted-foreground">No {field.name}</p>
<BoardViewColumnSelectHeader
field={field}
option={null}
count={noValueCount}
/>
),
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 (
<div className="flex flex-row gap-2 items-center">
<CircleDashed className="size-5" />
<p className="text-muted-foreground">No {field.name}</p>
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
}
return (
<div className="flex flex-row gap-2 items-center">
<SelectOptionBadge name={option?.name} color={option?.color} />
<p className="text-muted-foreground text-sm ml-1">
{count.toLocaleString()}
</p>
</div>
);
};