mirror of
https://github.com/docmost/docmost.git
synced 2026-07-24 08:22:49 +10:00
feat(bases): scaffold kanban renderer (no DnD yet)
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
IBase,
|
||||||
|
IBaseRow,
|
||||||
|
IBaseView,
|
||||||
|
} from "@/features/base/types/base.types";
|
||||||
|
import { useKanbanGroups } from "@/features/base/hooks/use-kanban-groups";
|
||||||
|
import { KanbanColumn } from "./kanban-column";
|
||||||
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
|
||||||
|
type BaseKanbanProps = {
|
||||||
|
base: IBase;
|
||||||
|
rows: IBaseRow[];
|
||||||
|
effectiveView: IBaseView | undefined;
|
||||||
|
onCardClick: (rowId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BaseKanban({
|
||||||
|
base,
|
||||||
|
rows,
|
||||||
|
effectiveView,
|
||||||
|
onCardClick,
|
||||||
|
}: BaseKanbanProps) {
|
||||||
|
const groupByPropertyId = effectiveView?.config?.groupByPropertyId;
|
||||||
|
const property = useMemo(
|
||||||
|
() =>
|
||||||
|
groupByPropertyId
|
||||||
|
? base.properties.find((p) => p.id === groupByPropertyId)
|
||||||
|
: undefined,
|
||||||
|
[groupByPropertyId, base.properties],
|
||||||
|
);
|
||||||
|
const primaryProperty = useMemo(
|
||||||
|
() => base.properties.find((p) => p.isPrimary),
|
||||||
|
[base.properties],
|
||||||
|
);
|
||||||
|
const isGroupable = property?.type === "select" || property?.type === "status";
|
||||||
|
const { columns } = useKanbanGroups(
|
||||||
|
rows,
|
||||||
|
isGroupable ? property : undefined,
|
||||||
|
effectiveView?.config?.hiddenChoiceIds,
|
||||||
|
effectiveView?.config?.choiceOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.board}>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<KanbanColumn
|
||||||
|
key={column.key}
|
||||||
|
column={column}
|
||||||
|
primaryProperty={primaryProperty}
|
||||||
|
onCardClick={onCardClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { IBaseProperty, IBaseRow } from "@/features/base/types/base.types";
|
||||||
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type KanbanCardProps = {
|
||||||
|
row: IBaseRow;
|
||||||
|
primaryProperty: IBaseProperty | undefined;
|
||||||
|
onClick: (rowId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KanbanCard({ row, primaryProperty, onClick }: KanbanCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const titleValue =
|
||||||
|
primaryProperty
|
||||||
|
? ((row.cells ?? {})[primaryProperty.id] as string | undefined)
|
||||||
|
: undefined;
|
||||||
|
const titleText = titleValue?.trim().length ? titleValue : t("Untitled");
|
||||||
|
const isEmpty = !titleValue?.trim().length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classes.card}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => onClick(row.id)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(row.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
classes.cardTitle + (isEmpty ? " " + classes.cardTitleEmpty : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{titleText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Badge, Text } from "@mantine/core";
|
||||||
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
|
||||||
|
type KanbanColumnHeaderProps = {
|
||||||
|
name: string;
|
||||||
|
color: string | null;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KanbanColumnHeader({
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
count,
|
||||||
|
}: KanbanColumnHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className={classes.columnHeader}>
|
||||||
|
<div className={classes.columnHeaderLeft}>
|
||||||
|
{color ? (
|
||||||
|
<Badge color={color} variant="light" size="sm">
|
||||||
|
{name}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<span className={classes.columnCount}>{count}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { KanbanColumn as KanbanColumnData } from "@/features/base/hooks/use-kanban-groups";
|
||||||
|
import { IBaseProperty } from "@/features/base/types/base.types";
|
||||||
|
import { KanbanCard } from "./kanban-card";
|
||||||
|
import { KanbanColumnHeader } from "./kanban-column-header";
|
||||||
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
|
||||||
|
type KanbanColumnProps = {
|
||||||
|
column: KanbanColumnData;
|
||||||
|
primaryProperty: IBaseProperty | undefined;
|
||||||
|
onCardClick: (rowId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KanbanColumn({
|
||||||
|
column,
|
||||||
|
primaryProperty,
|
||||||
|
onCardClick,
|
||||||
|
}: KanbanColumnProps) {
|
||||||
|
return (
|
||||||
|
<div className={classes.column} data-column-key={column.key}>
|
||||||
|
<KanbanColumnHeader
|
||||||
|
name={column.name}
|
||||||
|
color={column.color}
|
||||||
|
count={column.rows.length}
|
||||||
|
/>
|
||||||
|
<div className={classes.columnBody} data-column-body={column.key}>
|
||||||
|
{column.rows.map((row) => (
|
||||||
|
<KanbanCard
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
primaryProperty={primaryProperty}
|
||||||
|
onClick={onCardClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
.board {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 280px;
|
||||||
|
width: 280px;
|
||||||
|
background: var(--mantine-color-default-hover);
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||||
|
position: relative; /* anchor for BaseDropEdgeIndicator on column reorder */
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnHeaderLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnCount {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mantine-color-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnBody {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--mantine-color-body);
|
||||||
|
border: 1px solid var(--mantine-color-default-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card[data-dragging="true"] {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitleEmpty {
|
||||||
|
color: var(--mantine-color-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addCardButton {
|
||||||
|
margin-top: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortHint {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mantine-color-dimmed);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user