import { __, _n, sprintf } from '@/lib/i18n'; import { Fragment, useState } from 'react'; import { absoluteTime, relativeTime } from '@/lib/time'; import type { AppBadgeInfo, WorkflowStats, WorkflowSummary } from '@/types'; import { RowMenu } from '@/app/RowMenu'; import { AppFlow } from '@/app/AppFlow'; import { StatusSwitch } from '@/app/StatusSwitch'; import { IconDownload, IconHistory, IconPencil, IconTrash, IconType } from '@/lib/icons'; /** * The workflow list, as an actual table. * * It was a pair of CSS grids — one for the header, one per row — which cannot * align: the header's action column is empty and the rows' holds two buttons, * so the `1fr` name column resolved to a different width in each and every * heading sat left of its own numbers. A table computes one set of column * widths for all of its rows, which is the entire reason the element exists. * * A row answers the questions someone opens this screen to ask: is it live, * what does it connect, and has it been working — the switch, the logos and the * health bar, read left to right. */ /** Why going live was refused, shown under the row it belongs to. */ export interface ActivationProblem { id: number; message: string; details: string[]; } interface WorkflowTableProps { workflows: WorkflowSummary[]; canManage: boolean; onOpen: (id: number) => void; onHistory: (id: number) => void; onRename: (id: number, name: string) => void; onDelete: (id: number) => void; /** Whether the plan includes exporting. */ canExport: boolean; onExport: (id: number) => void; /** Name and logo of each integration the listed workflows use. */ apps: Record; onSetActive: (id: number, active: boolean) => void; /** Rows whose switch is waiting on the server. */ pending: ReadonlySet; problem: ActivationProblem | null; onDismissProblem: () => void; } const COLUMNS = 6; /** Completed against failed, as a bar and a sentence. */ function RunHealth({ stats }: { stats: WorkflowStats }) { const finished = stats.completedRuns + stats.failedRuns; if (finished === 0) { return {__('No runs yet', 'flowwp-workflow-automation')}; } const rate = Math.round((stats.completedRuns / finished) * 100); const tone = stats.failedRuns === 0 ? 'good' : rate >= 80 ? 'fair' : 'poor'; return ( ); } export function WorkflowTable({ workflows, canManage, onOpen, onHistory, onRename, onDelete, canExport, onExport, apps, onSetActive, pending, problem, onDismissProblem, }: WorkflowTableProps) { const [confirmingId, setConfirmingId] = useState(null); const [renamingId, setRenamingId] = useState(null); const [draftName, setDraftName] = useState(''); const commitRename = (id: number) => { const name = draftName.trim(); setRenamingId(null); // An empty name is a mistake, not an instruction to erase the title. if (name !== '') { onRename(id, name); } }; return (
{workflows.map((workflow) => ( {/* Going live checks the whole graph; when it is refused, the reasons sit under the row whose switch was flipped. */} {problem?.id === workflow.id && ( )} ))}
{__('Status', 'flowwp-workflow-automation')} {__('Workflow', 'flowwp-workflow-automation')} {__('Connects', 'flowwp-workflow-automation')} {__('Runs', 'flowwp-workflow-automation')} {__('Last edited', 'flowwp-workflow-automation')} {__('Actions', 'flowwp-workflow-automation')}
onSetActive(workflow.id, active)} />
{renamingId === workflow.id ? ( setDraftName(event.target.value)} onBlur={() => commitRename(workflow.id)} onKeyDown={(event) => { if (event.key === 'Enter') { commitRename(workflow.id); } if (event.key === 'Escape') { setRenamingId(null); } }} /> ) : ( )} {sprintf( /* translators: %d: how many triggers a workflow has. */ _n('%d trigger', '%d triggers', workflow.stats.triggers, 'flowwp-workflow-automation'), workflow.stats.triggers, )}{' '} ·{' '} {sprintf( /* translators: %d: how many steps a workflow has. */ _n('%d step', '%d steps', workflow.stats.actions, 'flowwp-workflow-automation'), workflow.stats.actions, )}
{relativeTime(workflow.updatedAt)} {workflow.stats.lastRunAt !== null && ( ran {relativeTime(workflow.stats.lastRunAt)} )}
{confirmingId === workflow.id ? (
{__('Delete permanently?', 'flowwp-workflow-automation')}
) : ( // The two actions used every day stay visible; the occasional // ones wait behind ⋯, with the destructive one set apart.
, disabled: !canManage, onSelect: () => { setRenamingId(workflow.id); setDraftName(workflow.name); }, }, { id: 'export', label: __('Export', 'flowwp-workflow-automation'), icon: , disabled: !canExport, hint: canExport ? undefined : __('Upgrade', 'flowwp-workflow-automation'), onSelect: () => onExport(workflow.id), }, { id: 'delete', label: __('Delete', 'flowwp-workflow-automation'), icon: , danger: true, separated: true, disabled: !canManage, onSelect: () => setConfirmingId(workflow.id), }, ]} />
)}
“{workflow.name}” can’t go live yet

{problem.message}

{problem.details.length > 0 && (
    {problem.details.map((detail) => (
  • {detail}
  • ))}
)}
); }