mirror of
https://github.com/docmost/docmost.git
synced 2026-08-31 02:46:27 +08:00
feat(bases): add kanban empty state with group-by picker
This commit is contained in:
@@ -5,7 +5,9 @@ import {
|
|||||||
IBaseView,
|
IBaseView,
|
||||||
} from "@/features/base/types/base.types";
|
} from "@/features/base/types/base.types";
|
||||||
import { useKanbanGroups } from "@/features/base/hooks/use-kanban-groups";
|
import { useKanbanGroups } from "@/features/base/hooks/use-kanban-groups";
|
||||||
|
import { useUpdateViewMutation } from "@/features/base/queries/base-view-query";
|
||||||
import { KanbanColumn } from "./kanban-column";
|
import { KanbanColumn } from "./kanban-column";
|
||||||
|
import { KanbanEmptyState } from "./kanban-empty-state";
|
||||||
import classes from "@/features/base/styles/kanban.module.css";
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
|
||||||
type BaseKanbanProps = {
|
type BaseKanbanProps = {
|
||||||
@@ -34,6 +36,10 @@ export function BaseKanban({
|
|||||||
[base.properties],
|
[base.properties],
|
||||||
);
|
);
|
||||||
const isGroupable = property?.type === "select" || property?.type === "status";
|
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(
|
const { columns } = useKanbanGroups(
|
||||||
rows,
|
rows,
|
||||||
isGroupable ? property : undefined,
|
isGroupable ? property : undefined,
|
||||||
@@ -41,6 +47,19 @@ export function BaseKanban({
|
|||||||
effectiveView?.config?.choiceOrder,
|
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 (
|
return (
|
||||||
<div className={classes.board}>
|
<div className={classes.board}>
|
||||||
{columns.map((column) => (
|
{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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user