mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Add number of records for each board column (#122)
This commit is contained in:
@@ -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(
|
||||
@@ -191,9 +209,22 @@ export class RecordFieldValueCountQueryHandler
|
||||
json_each(${this.buildFieldSelector(field)})
|
||||
WHERE n.parent_id = '${databaseId}'
|
||||
AND n.type = 'record'
|
||||
AND ${this.buildFieldSelector(field)} IS NOT NULL
|
||||
${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
|
||||
|
||||
@@ -15,7 +15,8 @@ export type RecordFieldValueCount = {
|
||||
};
|
||||
|
||||
export type RecordFieldValueCountQueryOutput = {
|
||||
items: RecordFieldValueCount[];
|
||||
values: RecordFieldValueCount[];
|
||||
nullCount: number;
|
||||
};
|
||||
|
||||
declare module '@colanode/client/queries' {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user