chore: auto-commit (28 archivos)
- app.md - auth.go - chat.go - chat.log - db.go - frontend/package.json - frontend/pnpm-lock.yaml - frontend/src/App.tsx - frontend/src/Root.tsx - frontend/src/api.ts - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,10 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Menu,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Popover,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
@@ -17,7 +20,12 @@ import {
|
||||
import {
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconAlertTriangle,
|
||||
IconCheck,
|
||||
IconCheckbox,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconDotsVertical,
|
||||
IconGripVertical,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
@@ -25,7 +33,7 @@ import {
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { memo, MouseEvent, useEffect, useRef, useState } from "react";
|
||||
import type { Card, CardColor, Column } from "../types";
|
||||
import type { Card, CardColor, Column, User } from "../types";
|
||||
import { KanbanCard } from "./KanbanCard";
|
||||
|
||||
interface Props {
|
||||
@@ -38,10 +46,16 @@ interface Props {
|
||||
onResizeColumn: (id: string, width: number) => void;
|
||||
onMoveColumnLocation: (id: string, location: "board" | "sidebar") => void;
|
||||
onDeleteColumn: (id: string) => void;
|
||||
onSetWIPLimit: (id: string, limit: number) => void;
|
||||
onToggleDone: (id: string, is_done: boolean) => void;
|
||||
onEditCard: (card: Card) => void;
|
||||
onDeleteCard: (id: string) => void;
|
||||
onChangeCardColor: (id: string, color: CardColor) => void;
|
||||
onShowHistory: (card: Card) => void;
|
||||
onToggleCardLock: (id: string, locked: boolean) => void;
|
||||
onAssignCard: (id: string, assignee_id: string | null) => void;
|
||||
users: User[];
|
||||
usersById: Map<string, User>;
|
||||
}
|
||||
|
||||
function KanbanColumnImpl({
|
||||
@@ -54,14 +68,34 @@ function KanbanColumnImpl({
|
||||
onResizeColumn,
|
||||
onMoveColumnLocation,
|
||||
onDeleteColumn,
|
||||
onSetWIPLimit,
|
||||
onToggleDone,
|
||||
onEditCard,
|
||||
onDeleteCard,
|
||||
onChangeCardColor,
|
||||
onShowHistory,
|
||||
onToggleCardLock,
|
||||
onAssignCard,
|
||||
users,
|
||||
usersById,
|
||||
}: Props) {
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [name, setName] = useState(column.name);
|
||||
const [localWidth, setLocalWidth] = useState<number | null>(null);
|
||||
const [wipPopOpen, setWipPopOpen] = useState(false);
|
||||
const [wipDraft, setWipDraft] = useState<number | string>(column.wip_limit);
|
||||
const [bodyHidden, setBodyHidden] = useState(() => {
|
||||
if (!collapsed) return false;
|
||||
return localStorage.getItem(`kanban_col_body_${column.id}`) === "1";
|
||||
});
|
||||
useEffect(() => {
|
||||
if (collapsed) {
|
||||
localStorage.setItem(`kanban_col_body_${column.id}`, bodyHidden ? "1" : "0");
|
||||
}
|
||||
}, [bodyHidden, collapsed, column.id]);
|
||||
|
||||
const wipLimit = column.wip_limit;
|
||||
const overLimit = wipLimit > 0 && cards.length > wipLimit;
|
||||
|
||||
// sync local width when column.width changes from outside (other clients).
|
||||
useEffect(() => {
|
||||
@@ -73,20 +107,32 @@ function KanbanColumnImpl({
|
||||
data: { type: "column", columnId: column.id, location: column.location },
|
||||
});
|
||||
|
||||
const effectiveWidth = collapsed ? 220 : localWidth ?? column.width;
|
||||
const effectiveWidth = collapsed ? "100%" : localWidth ?? column.width;
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
width: effectiveWidth,
|
||||
minWidth: effectiveWidth,
|
||||
maxWidth: effectiveWidth,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
};
|
||||
const style: React.CSSProperties = collapsed
|
||||
? {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
flex: bodyHidden ? "0 0 auto" : "1 1 auto",
|
||||
minHeight: 0,
|
||||
}
|
||||
: {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
width: effectiveWidth,
|
||||
minWidth: effectiveWidth,
|
||||
maxWidth: effectiveWidth,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
};
|
||||
|
||||
const cardIds = cards.map((c) => c.id);
|
||||
|
||||
@@ -135,8 +181,24 @@ function KanbanColumnImpl({
|
||||
const archiveLabel = isInSidebar ? "Restaurar al board" : "Mover al sidebar";
|
||||
const ArchiveIcon = isInSidebar ? IconArchiveOff : IconArchive;
|
||||
|
||||
const submitWIP = () => {
|
||||
const n = typeof wipDraft === "number" ? wipDraft : parseInt(String(wipDraft), 10);
|
||||
const safe = Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
||||
if (safe !== column.wip_limit) onSetWIPLimit(column.id, safe);
|
||||
setWipPopOpen(false);
|
||||
};
|
||||
|
||||
const paperBg = overLimit ? "var(--mantine-color-red-9)" : "var(--mantine-color-dark-7)";
|
||||
const paperBorderColor = overLimit ? "var(--mantine-color-red-6)" : undefined;
|
||||
|
||||
return (
|
||||
<Paper ref={setNodeRef} style={style} withBorder radius="md" p="sm" bg="dark.7">
|
||||
<Paper
|
||||
ref={setNodeRef}
|
||||
style={{ ...style, background: paperBg, borderColor: paperBorderColor, borderWidth: overLimit ? 2 : 1 }}
|
||||
withBorder
|
||||
radius="md"
|
||||
p="sm"
|
||||
>
|
||||
<Group justify="space-between" mb="xs" wrap="nowrap">
|
||||
<Group gap={4} wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
|
||||
<ActionIcon
|
||||
@@ -181,9 +243,65 @@ function KanbanColumnImpl({
|
||||
{column.name}
|
||||
</Text>
|
||||
)}
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{cards.length}
|
||||
</Badge>
|
||||
<Popover
|
||||
opened={wipPopOpen}
|
||||
onChange={(o) => {
|
||||
setWipPopOpen(o);
|
||||
if (o) setWipDraft(column.wip_limit);
|
||||
}}
|
||||
position="bottom"
|
||||
withArrow
|
||||
shadow="md"
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip
|
||||
label={
|
||||
wipLimit > 0
|
||||
? `WIP ${cards.length}/${wipLimit}${overLimit ? " (excedido)" : ""}`
|
||||
: "Click para limitar WIP"
|
||||
}
|
||||
withArrow
|
||||
>
|
||||
<Badge
|
||||
size="xs"
|
||||
variant={overLimit ? "filled" : "light"}
|
||||
color={overLimit ? "red" : wipLimit > 0 ? "yellow" : "gray"}
|
||||
leftSection={overLimit ? <IconAlertTriangle size={10} /> : null}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => setWipPopOpen((v) => !v)}
|
||||
>
|
||||
{wipLimit > 0 ? `${cards.length}/${wipLimit}` : cards.length}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p="xs">
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
Maximo de tarjetas (0 = sin limite)
|
||||
</Text>
|
||||
<NumberInput
|
||||
size="xs"
|
||||
value={wipDraft}
|
||||
onChange={setWipDraft}
|
||||
min={0}
|
||||
max={999}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") submitWIP();
|
||||
if (e.key === "Escape") setWipPopOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Group justify="flex-end" gap={4}>
|
||||
<Button size="xs" variant="subtle" onClick={() => setWipPopOpen(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button size="xs" onClick={submitWIP}>
|
||||
Guardar
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Group>
|
||||
<Group gap={2} wrap="nowrap">
|
||||
{renaming ? (
|
||||
@@ -206,72 +324,109 @@ function KanbanColumnImpl({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setName(column.name);
|
||||
setRenaming(true);
|
||||
}}
|
||||
aria-label="Rename"
|
||||
>
|
||||
<IconPencil size={14} />
|
||||
</ActionIcon>
|
||||
<Tooltip label={archiveLabel} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={() => onMoveColumnLocation(column.id, isInSidebar ? "board" : "sidebar")}
|
||||
aria-label={archiveLabel}
|
||||
>
|
||||
<ArchiveIcon size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => onDeleteColumn(column.id)}
|
||||
aria-label="Delete column"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</ActionIcon>
|
||||
{collapsed && (
|
||||
<Tooltip label={bodyHidden ? "Expandir" : "Colapsar"} withArrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
onClick={() => setBodyHidden((v) => !v)}
|
||||
aria-label={bodyHidden ? "Expandir columna" : "Colapsar columna"}
|
||||
>
|
||||
{bodyHidden ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{column.is_done && (
|
||||
<Tooltip label="Columna Done" withArrow>
|
||||
<Badge size="xs" color="green" variant="filled" leftSection={<IconCheckbox size={10} />}>
|
||||
done
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Menu position="bottom-end" shadow="md" withArrow>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray" size="sm" aria-label="Acciones columna">
|
||||
<IconDotsVertical size={14} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Columna</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={<IconPencil size={14} />}
|
||||
onClick={() => {
|
||||
setName(column.name);
|
||||
setRenaming(true);
|
||||
}}
|
||||
>
|
||||
Renombrar
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconCheckbox size={14} />}
|
||||
color={column.is_done ? "yellow" : "green"}
|
||||
onClick={() => onToggleDone(column.id, !column.is_done)}
|
||||
>
|
||||
{column.is_done ? "Quitar marca Done" : "Marcar como Done"}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<ArchiveIcon size={14} />}
|
||||
onClick={() => onMoveColumnLocation(column.id, isInSidebar ? "board" : "sidebar")}
|
||||
>
|
||||
{archiveLabel}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
color="red"
|
||||
onClick={() => onDeleteColumn(column.id)}
|
||||
>
|
||||
Borrar columna
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<ScrollArea style={{ flex: 1 }} type="auto">
|
||||
<SortableContext items={cardIds} strategy={verticalListSortingStrategy}>
|
||||
<Stack gap="xs" pb="xs" style={{ minHeight: 40 }}>
|
||||
{cards.map((c) => (
|
||||
<KanbanCard
|
||||
key={c.id}
|
||||
card={c}
|
||||
now={now}
|
||||
onDelete={onDeleteCard}
|
||||
onEdit={onEditCard}
|
||||
onChangeColor={onChangeCardColor}
|
||||
onShowHistory={onShowHistory}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
</ScrollArea>
|
||||
{!(collapsed && bodyHidden) && (
|
||||
<>
|
||||
<ScrollArea style={{ flex: 1 }} type="auto">
|
||||
<SortableContext items={cardIds} strategy={verticalListSortingStrategy}>
|
||||
<Stack gap="xs" pb="xs" style={{ minHeight: 40 }}>
|
||||
{cards.map((c) => (
|
||||
<KanbanCard
|
||||
key={c.id}
|
||||
card={c}
|
||||
now={now}
|
||||
onDelete={onDeleteCard}
|
||||
onEdit={onEditCard}
|
||||
onChangeColor={onChangeCardColor}
|
||||
onShowHistory={onShowHistory}
|
||||
onToggleLock={onToggleCardLock}
|
||||
onAssign={onAssignCard}
|
||||
users={users}
|
||||
assignee={c.assignee_id ? usersById.get(c.assignee_id) : undefined}
|
||||
inDoneColumn={column.is_done}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
</ScrollArea>
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={() => onAddCard(column.id)}
|
||||
mt="xs"
|
||||
fullWidth
|
||||
>
|
||||
Anadir tarjeta
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="xs"
|
||||
leftSection={<IconPlus size={14} />}
|
||||
onClick={() => onAddCard(column.id)}
|
||||
mt="xs"
|
||||
fullWidth
|
||||
>
|
||||
Anadir tarjeta
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Resize handle (only on board, not sidebar) */}
|
||||
{!isInSidebar && (
|
||||
|
||||
Reference in New Issue
Block a user