- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.1.0, shown as text in the browser.
Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.
import { Fragment, useEffect, useRef } from "react";
import type { CardData, ColumnData, Member } from "../types";
import { CardItem } from "./CardItem";
import type { CardItemProps } from "./CardItem";
import { IconAlert, IconInfo, IconPlus } from "../icons";
export interface CardCommon {
memberById: Map<string, Member>;
memberIndexById: Map<string, number>;
labelName: (id: string) => string;
suppressClicks: () => boolean;
onOpen: (cardId: string) => void;
onKeyDown: (event: React.KeyboardEvent<HTMLLIElement>, cardId: string) => void;
onPointerDown: (event: React.PointerEvent<HTMLLIElement>, cardId: string) => void;
onFocusCard: (cardId: string) => void;
onMoveClick: (cardId: string, anchor: HTMLElement) => void;
registerCardEl: (cardId: string, el: HTMLLIElement | null) => void;
draggedCardId: string | null;
grabbedCardId: string | null;
overdueOf: (card: CardData, columnId: string) => boolean;
}
export interface ComposerState {
columnId: string;
value: string;
error: string | null;
}
interface ColumnViewProps {
column: ColumnData;
cards: CardData[];
totalCount: number;
filtersOn: boolean;
isFull: boolean;
isDropTarget: boolean;
dropIndex: number | null;
dropBlocked: boolean;
dropHeight: number;
isBlockedFlash: boolean;
cardCommon: CardCommon;
composer: ComposerState | null;
onComposerOpen: (columnId: string) => void;
onComposerChange: (value: string) => void;
onComposerSubmit: () => void;
onComposerCancel: () => void;
registerColumnEl: (columnId: string, el: HTMLElement | null) => void;
registerListEl: (columnId: string, el: HTMLUListElement | null) => void;
registerAddButtonEl: (columnId: string, el: HTMLButtonElement | null) => void;
}
export function ColumnView({
column,
cards,
totalCount,
filtersOn,
isFull,
isDropTarget,
dropIndex,
dropBlocked,
dropHeight,
isBlockedFlash,
cardCommon,
composer,
onComposerOpen,
onComposerChange,
onComposerSubmit,
onComposerCancel,
registerColumnEl,
registerListEl,
registerAddButtonEl,
}: ColumnViewProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
const composerOpen = composer !== null && composer.columnId === column.id;
const hiddenCount = totalCount - cards.length;
useEffect(() => {
if (composerOpen) inputRef.current?.focus();
}, [composerOpen]);
const countText = column.wip_limit !== null
? `${cards.length} / ${column.wip_limit}`
: filtersOn && hiddenCount > 0
? `${cards.length} of ${totalCount}`
: `${cards.length}`;
const countTitle =
column.wip_limit !== null
? `${cards.length} visible of a limit of ${column.wip_limit} cards`
: filtersOn && hiddenCount > 0
? `${cards.length} of ${totalCount} cards match the current filters`
: `${cards.length} cards`;
const placeholder = isDropTarget && dropIndex !== null && (
<li
className={`drop-placeholder ${dropBlocked ? "is-blocked" : ""}`}
data-drag-placeholder="true"
style={{ height: dropHeight }}
aria-hidden="true"
>
{dropBlocked && (
<span className="drop-placeholder-note">
<IconAlert size={14} />
WIP limit reached
</span>
)}
</li>
);
return (
<section
className={`column ${isDropTarget ? "is-drop-target" : ""} ${
dropBlocked ? "is-drop-blocked" : ""
} ${isBlockedFlash ? "is-blocked-flash" : ""}`}
aria-labelledby={`column-${column.id}-title`}
ref={(el) => registerColumnEl(column.id, el)}
data-column-id={column.id}
>
<header className="column-head">
<h2 className="column-title" id={`column-${column.id}-title`}>
{column.title}
</h2>
<span className="column-count" title={countTitle} aria-label={countTitle}>
{countText}
</span>
<button
type="button"
className="column-add-icon"
aria-label={`Add a card to ${column.title}`}
title={isFull ? `${column.title} is at its WIP limit` : `Add a card to ${column.title}`}
disabled={isFull}
onClick={() => onComposerOpen(column.id)}
>
<IconPlus size={15} />
</button>
</header>
{column.wip_limit !== null && (
<p className={`column-limit-note ${isFull ? "is-full" : ""}`}>
<IconInfo size={13} />
{isFull
? `WIP limit reached: ${column.title} holds ${column.wip_limit} cards. Move one out to make room.`
: `Work in progress limit: ${column.wip_limit} cards.`}
</p>
)}
<ul className="card-list" ref={(el) => registerListEl(column.id, el)}>
{cards.map((card, index) => (
<Fragment key={card.id}>
{isDropTarget && dropIndex === index && placeholder}
<CardItem
card={card}
columnTitle={column.title}
member={card.assignee ? cardCommon.memberById.get(card.assignee) ?? null : null}
memberIndex={card.assignee ? cardCommon.memberIndexById.get(card.assignee) ?? 0 : 0}
labelName={cardCommon.labelName}
overdue={cardCommon.overdueOf(card, column.id)}
isDragged={cardCommon.draggedCardId === card.id}
isGrabbed={cardCommon.grabbedCardId === card.id}
suppressClicks={cardCommon.suppressClicks}
onOpen={cardCommon.onOpen}
onKeyDown={cardCommon.onKeyDown}
onPointerDown={cardCommon.onPointerDown}
onFocus={cardCommon.onFocusCard}
onMoveClick={cardCommon.onMoveClick}
registerEl={cardCommon.registerCardEl}
/>
</Fragment>
))}
{isDropTarget && dropIndex !== null && dropIndex >= cards.length && placeholder}
{cards.length === 0 && !isDropTarget && (
<li className="column-empty">
{filtersOn ? "No cards here match the filters." : "No cards yet — add the first one."}
</li>
)}
</ul>
{composerOpen && composer ? (
<form
className="card-composer"
onSubmit={(event) => {
event.preventDefault();
onComposerSubmit();
}}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
onComposerCancel();
}
}}
>
<label className="sr-only" htmlFor={`composer-${column.id}`}>
New card title in {column.title}
</label>
<input
id={`composer-${column.id}`}
ref={inputRef}
className="composer-input"
placeholder="Card title…"
value={composer.value}
aria-invalid={composer.error ? true : undefined}
aria-describedby={composer.error ? `composer-error-${column.id}` : undefined}
onChange={(event) => onComposerChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
onComposerCancel();
}
}}
/>
{composer.error && (
<p className="composer-error" id={`composer-error-${column.id}`} role="alert">
{composer.error}
</p>
)}
<div className="composer-actions">
<button type="submit" className="btn primary small">
Add card
</button>
<button type="button" className="btn ghost small" onClick={onComposerCancel}>
Cancel
</button>
</div>
</form>
) : (
<button
type="button"
className="column-add"
ref={(el) => registerAddButtonEl(column.id, el)}
disabled={isFull}
onClick={() => onComposerOpen(column.id)}
>
<IconPlus size={15} />
Add card
</button>
)}
</section>
);
}