feat(bases): add kanban empty state with group-by picker

This commit is contained in:
Philipinho
2026-05-24 13:42:35 +01:00
parent f36df26d75
commit c350308c03
3 changed files with 110 additions and 0 deletions
@@ -5,7 +5,9 @@ import {
IBaseView,
} from "@/features/base/types/base.types";
import { useKanbanGroups } from "@/features/base/hooks/use-kanban-groups";
import { useUpdateViewMutation } from "@/features/base/queries/base-view-query";
import { KanbanColumn } from "./kanban-column";
import { KanbanEmptyState } from "./kanban-empty-state";
import classes from "@/features/base/styles/kanban.module.css";
type BaseKanbanProps = {
@@ -34,6 +36,10 @@ export function BaseKanban({
[base.properties],
);
const isGroupable = property?.type === "select" || property?.type === "status";
const updateViewMutation = useUpdateViewMutation();
// Rules of Hooks: call useKanbanGroups unconditionally with `undefined`
// when not groupable; switch the render path on isGroupable below.
const { columns } = useKanbanGroups(
rows,
isGroupable ? property : undefined,
@@ -41,6 +47,19 @@ export function BaseKanban({
effectiveView?.config?.choiceOrder,
);
const handlePickProperty = (propertyId: string) => {
if (!effectiveView) return;
updateViewMutation.mutate({
viewId: effectiveView.id,
pageId: base.id,
config: { ...effectiveView.config, groupByPropertyId: propertyId },
});
};
if (!isGroupable) {
return <KanbanEmptyState base={base} onPick={handlePickProperty} />;
}
return (
<div className={classes.board}>
{columns.map((column) => (
@@ -0,0 +1,52 @@
import { Stack, Text } from "@mantine/core";
import { IconColumns3 } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { IBase } from "@/features/base/types/base.types";
import { KanbanGroupByPicker } from "./kanban-group-by-picker";
import { CreatePropertyPopover } from "@/features/base/components/property/create-property-popover";
type KanbanEmptyStateProps = {
base: IBase;
onPick: (propertyId: string) => void;
};
export function KanbanEmptyState({ base, onPick }: KanbanEmptyStateProps) {
const { t } = useTranslation();
const hasGroupable = base.properties.some(
(p) => p.type === "select" || p.type === "status",
);
return (
<Stack align="center" gap="md" p="xl" mt={48}>
<IconColumns3 size={48} color="var(--mantine-color-gray-5)" />
<Text size="lg" fw={500}>
{t("Choose a property to group by")}
</Text>
{hasGroupable ? (
<KanbanGroupByPicker
properties={base.properties}
value={null}
onChange={onPick}
/>
) : (
<Stack align="center" gap="xs">
<Text size="sm" c="dimmed">
{t("Create a select or status property to use the kanban view.")}
</Text>
<CreatePropertyPopover
pageId={base.id}
properties={base.properties}
onPropertyCreated={() => {
// The base query invalidates on property create — the empty
// state will re-render with the picker variant. The user
// then picks the new property explicitly. (Auto-picking the
// new property requires receiving its id from the create
// mutation, which the current popover doesn't expose. Keep
// the explicit step for now.)
}}
/>
</Stack>
)}
</Stack>
);
}
@@ -0,0 +1,39 @@
import { useMemo } from "react";
import { Select } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { IBaseProperty } from "@/features/base/types/base.types";
type KanbanGroupByPickerProps = {
properties: IBaseProperty[];
value: string | null;
onChange: (propertyId: string) => void;
// Allows the toolbar variant to render compact / narrow.
size?: "xs" | "sm" | "md";
};
export function KanbanGroupByPicker({
properties,
value,
onChange,
size = "sm",
}: KanbanGroupByPickerProps) {
const { t } = useTranslation();
const data = useMemo(
() =>
properties
.filter((p) => p.type === "select" || p.type === "status")
.map((p) => ({ value: p.id, label: p.name || t("Untitled") })),
[properties, t],
);
return (
<Select
placeholder={t("Group by…")}
data={data}
value={value}
onChange={(v) => v && onChange(v)}
size={size}
allowDeselect={false}
searchable
/>
);
}