feat(webui): centralize contextual table actions
This commit is contained in:
@@ -2,7 +2,7 @@ import { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState, type
|
||||
import { createPortal } from "react-dom";
|
||||
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ChevronsUpDown, Filter, GripVertical, Plus, Trash2, X } from "lucide-react";
|
||||
import StatusBadge from "../StatusBadge";
|
||||
import Button from "../Button";
|
||||
import TableActionGroup from "./TableActionGroup";
|
||||
import { usePlatformLanguage, i18nMessage } from "../../i18n/LanguageContext";
|
||||
|
||||
export type DataGridSortDirection = "asc" | "desc";
|
||||
@@ -852,58 +852,44 @@ export function DataGridRowActions({
|
||||
moveUpLabel = "i18n:govoplan-core.move_row_up.b4715a19",
|
||||
moveDownLabel = "i18n:govoplan-core.move_row_down.8e2b5957"
|
||||
}: DataGridRowActionsProps) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const translatedAddLabel = translateText(addLabel);
|
||||
const translatedRemoveLabel = translateText(removeLabel);
|
||||
const translatedMoveUpLabel = translateText(moveUpLabel);
|
||||
const translatedMoveDownLabel = translateText(moveDownLabel);
|
||||
return (
|
||||
<div className="data-grid-row-actions">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="data-grid-row-action is-add"
|
||||
aria-label={translatedAddLabel}
|
||||
title={translatedAddLabel}
|
||||
disabled={disabled}
|
||||
onClick={onAddBelow}>
|
||||
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="data-grid-row-action is-reorder"
|
||||
aria-label={translatedMoveUpLabel}
|
||||
title={translatedMoveUpLabel}
|
||||
disabled={disabled || !onMoveUp}
|
||||
onClick={() => onMoveUp?.()}>
|
||||
|
||||
<ArrowUp size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="data-grid-row-action is-reorder"
|
||||
aria-label={translatedMoveDownLabel}
|
||||
title={translatedMoveDownLabel}
|
||||
disabled={disabled || !onMoveDown}
|
||||
onClick={() => onMoveDown?.()}>
|
||||
|
||||
<ArrowDown size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
className="data-grid-row-action is-remove"
|
||||
aria-label={translatedRemoveLabel}
|
||||
title={translatedRemoveLabel}
|
||||
disabled={disabled || removeDisabled}
|
||||
onClick={onRemove}>
|
||||
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>);
|
||||
<TableActionGroup
|
||||
className="data-grid-row-actions"
|
||||
actions={[
|
||||
{
|
||||
id: "add",
|
||||
label: addLabel,
|
||||
icon: <Plus size={16} aria-hidden="true" />,
|
||||
variant: "primary",
|
||||
disabled,
|
||||
onClick: onAddBelow
|
||||
},
|
||||
{
|
||||
id: "move-up",
|
||||
label: moveUpLabel,
|
||||
icon: <ArrowUp size={16} aria-hidden="true" />,
|
||||
applicable: Boolean(onMoveUp),
|
||||
disabled,
|
||||
onClick: () => onMoveUp?.()
|
||||
},
|
||||
{
|
||||
id: "move-down",
|
||||
label: moveDownLabel,
|
||||
icon: <ArrowDown size={16} aria-hidden="true" />,
|
||||
applicable: Boolean(onMoveDown),
|
||||
disabled,
|
||||
onClick: () => onMoveDown?.()
|
||||
},
|
||||
{
|
||||
id: "remove",
|
||||
label: removeLabel,
|
||||
icon: <Trash2 size={16} aria-hidden="true" />,
|
||||
variant: "danger",
|
||||
disabled: disabled || removeDisabled,
|
||||
onClick: onRemove
|
||||
}
|
||||
]}
|
||||
/>);
|
||||
|
||||
}
|
||||
|
||||
@@ -916,22 +902,18 @@ export function DataGridEmptyAction({
|
||||
|
||||
|
||||
}: {disabled?: boolean;onAdd: () => void;label?: string;}) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const translatedLabel = translateText(label);
|
||||
return (
|
||||
<div className="data-grid-row-actions data-grid-empty-row-actions">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="data-grid-row-action is-add"
|
||||
aria-label={translatedLabel}
|
||||
title={translatedLabel}
|
||||
disabled={disabled}
|
||||
onClick={onAdd}>
|
||||
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>);
|
||||
<TableActionGroup
|
||||
className="data-grid-row-actions data-grid-empty-row-actions"
|
||||
actions={[{
|
||||
id: "add",
|
||||
label,
|
||||
icon: <Plus size={16} aria-hidden="true" />,
|
||||
variant: "primary",
|
||||
disabled,
|
||||
onClick: onAdd
|
||||
}]}
|
||||
/>);
|
||||
|
||||
}
|
||||
|
||||
|
||||
80
webui/src/components/table/TableActionGroup.tsx
Normal file
80
webui/src/components/table/TableActionGroup.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { MouseEvent, ReactNode } from "react";
|
||||
import Button from "../Button";
|
||||
import { usePlatformLanguage } from "../../i18n/LanguageContext";
|
||||
|
||||
export type TableActionDefinition = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
/** Set to false when the action does not apply to this row. */
|
||||
applicable?: boolean;
|
||||
disabled?: boolean;
|
||||
variant?: "primary" | "secondary" | "ghost" | "danger";
|
||||
};
|
||||
|
||||
export type TableActionGroupProps = {
|
||||
actions: readonly (TableActionDefinition | false | null | undefined)[];
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function isApplicableAction(
|
||||
action: TableActionDefinition | false | null | undefined
|
||||
): action is TableActionDefinition {
|
||||
return Boolean(action && action.applicable !== false);
|
||||
}
|
||||
|
||||
export function runTableAction(
|
||||
event: Pick<MouseEvent<HTMLButtonElement>, "stopPropagation">,
|
||||
onClick: () => void
|
||||
) {
|
||||
event.stopPropagation();
|
||||
onClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* The standard row-level action surface for tables and DataGrids.
|
||||
*
|
||||
* Callers describe only actions that make sense for the row. An action may be
|
||||
* disabled when it applies but is temporarily unavailable; actions marked as
|
||||
* not applicable are omitted entirely. Labels remain available to assistive
|
||||
* technology and as native tooltips while the visible controls stay icon-only.
|
||||
*/
|
||||
export default function TableActionGroup({
|
||||
actions,
|
||||
label = "i18n:govoplan-core.actions.c3cd636a",
|
||||
className = ""
|
||||
}: TableActionGroupProps) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const applicableActions = actions.filter(isApplicableAction);
|
||||
if (applicableActions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`table-action-group${className ? ` ${className}` : ""}`}
|
||||
role="group"
|
||||
aria-label={translateText(label)}
|
||||
>
|
||||
{applicableActions.map((action) => {
|
||||
const translatedLabel = translateText(action.label);
|
||||
return (
|
||||
<Button
|
||||
key={action.id}
|
||||
type="button"
|
||||
variant={action.variant ?? "secondary"}
|
||||
className="table-action-button"
|
||||
aria-label={translatedLabel}
|
||||
title={translatedLabel}
|
||||
disabled={action.disabled}
|
||||
onClick={(event) => runTableAction(event, action.onClick)}
|
||||
>
|
||||
<span className="table-action-icon" aria-hidden="true">
|
||||
{action.icon}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user