Show the count and a list of records that have no value for calendar group field (#124)

This commit is contained in:
Hakan Shehu
2025-07-09 18:01:08 +02:00
committed by GitHub
parent d33be86637
commit 7c0d421bc2
10 changed files with 157 additions and 12 deletions

View File

@@ -35,7 +35,7 @@ export class RecordFieldValueCountQueryHandler
) { ) {
return { return {
hasChanges: true, hasChanges: true,
result: { values: [], nullCount: 0 }, result: { values: [], noValueCount: 0 },
}; };
} }
@@ -91,7 +91,7 @@ export class RecordFieldValueCountQueryHandler
) { ) {
return { return {
hasChanges: true, hasChanges: true,
result: { values: [], nullCount: 0 }, result: { values: [], noValueCount: 0 },
}; };
} }
@@ -121,7 +121,7 @@ export class RecordFieldValueCountQueryHandler
if (!field) { if (!field) {
return { return {
values: [], values: [],
nullCount: 0, noValueCount: 0,
}; };
} }
@@ -141,12 +141,12 @@ export class RecordFieldValueCountQueryHandler
const output: RecordFieldValueCountQueryOutput = { const output: RecordFieldValueCountQueryOutput = {
values: [], values: [],
nullCount: 0, noValueCount: 0,
}; };
for (const row of result.rows) { for (const row of result.rows) {
if (row.value === 'null') { if (row.value === 'null') {
output.nullCount = row.count; output.noValueCount = row.count;
} else { } else {
output.values.push({ output.values.push({
value: row.value, value: row.value,

View File

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

View File

@@ -46,7 +46,7 @@ export const BoardViewColumnsCollaborator = ({
fieldId: field.id, fieldId: field.id,
operator: 'is_empty', operator: 'is_empty',
}; };
const noValueCount = collaboratorCountQuery.data?.nullCount ?? 0; const noValueCount = collaboratorCountQuery.data?.noValueCount ?? 0;
return ( return (
<> <>

View File

@@ -45,7 +45,7 @@ export const BoardViewColumnsMultiSelect = ({
}; };
const selectOptionCount = selectOptionCountQuery.data?.values ?? []; const selectOptionCount = selectOptionCountQuery.data?.values ?? [];
const noValueCount = selectOptionCountQuery.data?.nullCount ?? 0; const noValueCount = selectOptionCountQuery.data?.noValueCount ?? 0;
const noValueDraggingClass = getSelectOptionLightColorClass('gray'); const noValueDraggingClass = getSelectOptionLightColorClass('gray');

View File

@@ -44,7 +44,7 @@ export const BoardViewColumnsSelect = ({
}; };
const selectOptionCount = selectOptionCountQuery.data?.values ?? []; const selectOptionCount = selectOptionCountQuery.data?.values ?? [];
const noValueCount = selectOptionCountQuery.data?.nullCount ?? 0; const noValueCount = selectOptionCountQuery.data?.noValueCount ?? 0;
const noValueDraggingClass = getSelectOptionLightColorClass('gray'); const noValueDraggingClass = getSelectOptionLightColorClass('gray');

View File

@@ -42,7 +42,11 @@ export const CalendarViewDay = ({
/> />
</div> </div>
)} )}
<p className={isToday ? 'rounded-md bg-red-500 p-0.5 text-white' : ''}> <p
className={
isToday ? 'rounded-md bg-red-500 py-1 px-2 text-white' : ''
}
>
{date.getDate()} {date.getDate()}
</p> </p>
</div> </div>

View File

@@ -76,12 +76,12 @@ export const CalendarViewGrid = ({ field }: CalendarViewGridProps) => {
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: 'ghost' }), buttonVariants({ variant: 'ghost' }),
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none size-7',
defaultClassNames.button_previous defaultClassNames.button_previous
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: 'ghost' }), buttonVariants({ variant: 'ghost' }),
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none size-7',
defaultClassNames.button_next defaultClassNames.button_next
), ),
month_caption: cn( month_caption: cn(

View File

@@ -0,0 +1,76 @@
import { CircleDashed } from 'lucide-react';
import { DatabaseViewFilterAttributes, FieldAttributes } from '@colanode/core';
import { CalendarViewNoValueList } from '@colanode/ui/components/databases/calendars/calendar-view-no-value-list';
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@colanode/ui/components/ui/dialog';
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';
interface CalendarViewNoValueCountProps {
field: FieldAttributes;
}
export const CalendarViewNoValueCount = ({
field,
}: CalendarViewNoValueCountProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const view = useDatabaseView();
const filters: DatabaseViewFilterAttributes[] = [
...view.filters,
{
id: 'no_value',
type: 'field',
fieldId: field.id,
operator: 'is_empty',
},
];
const noValueCountQuery = useQuery({
type: 'record.field.value.count',
databaseId: database.id,
filters: filters,
fieldId: field.id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const noValueCount = noValueCountQuery.data?.noValueCount ?? 0;
if (noValueCount === 0) {
return null;
}
return (
<Dialog>
<DialogTrigger asChild>
<div
className="flex flex-row items-center gap-1 text-sm mr-2 hover:bg-muted hover:cursor-pointer rounded-md p-1"
role="presentation"
>
<CircleDashed className="size-4" />
<p>No {field.name}</p>
<p>({noValueCount.toLocaleString()})</p>
</div>
</DialogTrigger>
<DialogContent className="w-128 min-w-128 min-h-40 max-h-128 overflow-auto p-4">
<DialogHeader>
<DialogTitle>{view.name}</DialogTitle>
<DialogDescription>
Record with no {field.name} value ({noValueCount.toLocaleString()})
</DialogDescription>
</DialogHeader>
<CalendarViewNoValueList filters={filters} field={field} />
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,63 @@
import { InView } from 'react-intersection-observer';
import { DatabaseViewFilterAttributes, FieldAttributes } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useLayout } from '@colanode/ui/contexts/layout';
import { useRecordsQuery } from '@colanode/ui/hooks/use-records-query';
interface CalendarViewNoValueListProps {
filters: DatabaseViewFilterAttributes[];
field: FieldAttributes;
}
export const CalendarViewNoValueList = ({
filters,
field,
}: CalendarViewNoValueListProps) => {
const view = useDatabaseView();
const layout = useLayout();
const { records, hasMore, loadMore, isPending } = useRecordsQuery(
filters,
view.sorts
);
return (
<div className="flex flex-col gap-2 overflow-y-auto">
{records.length === 0 && (
<div className="text-center text-sm text-muted-foreground">
No records with no {field.name} value
</div>
)}
{records.map((record) => {
const name = record.attributes.name ?? 'Unnamed';
return (
<div
key={record.id}
className="flex flex-row items-center border rounded-md p-1 gap-2 cursor-pointer hover:bg-muted"
onClick={() => {
layout.previewLeft(record.id, true);
}}
>
<Avatar
id={record.id}
name={name}
avatar={record.attributes.avatar}
size="small"
/>
<p>{name}</p>
</div>
);
})}
<InView
rootMargin="200px"
onChange={(inView) => {
if (inView && hasMore && !isPending) {
loadMore();
}
}}
></InView>
</div>
);
};

View File

@@ -2,6 +2,7 @@ import { Fragment } from 'react';
import { CalendarViewGrid } from '@colanode/ui/components/databases/calendars/calendar-view-grid'; import { CalendarViewGrid } from '@colanode/ui/components/databases/calendars/calendar-view-grid';
import { CalendarViewNoGroup } from '@colanode/ui/components/databases/calendars/calendar-view-no-group'; import { CalendarViewNoGroup } from '@colanode/ui/components/databases/calendars/calendar-view-no-group';
import { CalendarViewNoValueCount } from '@colanode/ui/components/databases/calendars/calendar-view-no-value-count';
import { CalendarViewSettings } from '@colanode/ui/components/databases/calendars/calendar-view-settings'; import { CalendarViewSettings } from '@colanode/ui/components/databases/calendars/calendar-view-settings';
import { ViewFilterButton } from '@colanode/ui/components/databases/search/view-filter-button'; import { ViewFilterButton } from '@colanode/ui/components/databases/search/view-filter-button';
import { ViewSearchBar } from '@colanode/ui/components/databases/search/view-search-bar'; import { ViewSearchBar } from '@colanode/ui/components/databases/search/view-search-bar';
@@ -23,6 +24,7 @@ export const CalendarView = () => {
<div className="flex flex-row justify-between border-b"> <div className="flex flex-row justify-between border-b">
<ViewTabs /> <ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible"> <div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
{groupByField && <CalendarViewNoValueCount field={groupByField} />}
<CalendarViewSettings /> <CalendarViewSettings />
<ViewSortButton /> <ViewSortButton />
<ViewFilterButton /> <ViewFilterButton />