From c350308c03107e0e479257c4439860e94da7d47e Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 24 May 2026 13:42:35 +0100 Subject: [PATCH] feat(bases): add kanban empty state with group-by picker --- .../components/views/kanban/base-kanban.tsx | 19 +++++++ .../views/kanban/kanban-empty-state.tsx | 52 +++++++++++++++++++ .../views/kanban/kanban-group-by-picker.tsx | 39 ++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 apps/client/src/features/base/components/views/kanban/kanban-empty-state.tsx create mode 100644 apps/client/src/features/base/components/views/kanban/kanban-group-by-picker.tsx diff --git a/apps/client/src/features/base/components/views/kanban/base-kanban.tsx b/apps/client/src/features/base/components/views/kanban/base-kanban.tsx index cd76ed92c..8498b2665 100644 --- a/apps/client/src/features/base/components/views/kanban/base-kanban.tsx +++ b/apps/client/src/features/base/components/views/kanban/base-kanban.tsx @@ -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 ; + } + return (
{columns.map((column) => ( diff --git a/apps/client/src/features/base/components/views/kanban/kanban-empty-state.tsx b/apps/client/src/features/base/components/views/kanban/kanban-empty-state.tsx new file mode 100644 index 000000000..0dba2c2d7 --- /dev/null +++ b/apps/client/src/features/base/components/views/kanban/kanban-empty-state.tsx @@ -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 ( + + + + {t("Choose a property to group by")} + + {hasGroupable ? ( + + ) : ( + + + {t("Create a select or status property to use the kanban view.")} + + { + // 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.) + }} + /> + + )} + + ); +} diff --git a/apps/client/src/features/base/components/views/kanban/kanban-group-by-picker.tsx b/apps/client/src/features/base/components/views/kanban/kanban-group-by-picker.tsx new file mode 100644 index 000000000..9d47198f8 --- /dev/null +++ b/apps/client/src/features/base/components/views/kanban/kanban-group-by-picker.tsx @@ -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 ( +