mirror of
https://github.com/willchen96/mike.git
synced 2026-06-26 21:39:39 +02:00
Modal, header, mobile display and workflow UI updates
This commit is contained in:
parent
8a2dc05181
commit
3132e04ac0
34 changed files with 1635 additions and 1076 deletions
659
frontend/src/app/components/workflows/WorkflowDetailPage.tsx
Normal file
659
frontend/src/app/components/workflows/WorkflowDetailPage.tsx
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import dynamic from "next/dynamic";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Info,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { deleteWorkflow, getWorkflow, updateWorkflow } from "@/app/lib/mikeApi";
|
||||
import { ShareWorkflowModal } from "@/app/components/workflows/ShareWorkflowModal";
|
||||
import { WFEditColumnModal } from "@/app/components/workflows/WFEditColumnModal";
|
||||
import { WFColumnViewModal } from "@/app/components/workflows/WFColumnViewModal";
|
||||
import { AddColumnModal } from "@/app/components/tabular/AddColumnModal";
|
||||
import type { ColumnConfig, Workflow } from "@/app/components/shared/types";
|
||||
import { BUILT_IN_WORKFLOWS } from "@/app/components/workflows/builtinWorkflows";
|
||||
import { formatIcon, formatLabel } from "@/app/components/tabular/columnFormat";
|
||||
import { ConfirmPopup } from "@/app/components/shared/ConfirmPopup";
|
||||
import { HeaderActionsMenu } from "@/app/components/shared/HeaderActionsMenu";
|
||||
import { PageHeader } from "@/app/components/shared/PageHeader";
|
||||
import { WorkflowDetailsModal } from "@/app/components/workflows/WorkflowDetailsModal";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useUserProfile } from "@/contexts/UserProfileContext";
|
||||
// dynamic import keeps Tiptap (browser-only) out of the SSR bundle
|
||||
const WorkflowPromptEditor = dynamic(
|
||||
() =>
|
||||
import("@/app/components/workflows/WorkflowPromptEditor").then(
|
||||
(m) => ({ default: m.WorkflowPromptEditor }),
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
workflowType: Workflow["type"];
|
||||
}
|
||||
|
||||
type SaveStatus = "idle" | "saving" | "saved";
|
||||
type DeleteStatus = "idle" | "loading" | "complete";
|
||||
|
||||
const NAME_COL_W = "w-[332px] shrink-0";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page
|
||||
// ---------------------------------------------------------------------------
|
||||
export function WorkflowDetailPage({ id, workflowType }: Props) {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { profile } = useUserProfile();
|
||||
const stickyCellBg = "bg-[#fafbfc]";
|
||||
const builtinWorkflow =
|
||||
BUILT_IN_WORKFLOWS.find((w) => w.id === id && w.type === workflowType) ??
|
||||
null;
|
||||
const isBuiltin = builtinWorkflow !== null;
|
||||
|
||||
const [workflow, setWorkflow] = useState<Workflow | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
const readOnly =
|
||||
isBuiltin ||
|
||||
(workflow?.is_system ?? false) ||
|
||||
workflow?.allow_edit === false;
|
||||
const canShare = !readOnly && (workflow?.is_owner ?? true);
|
||||
|
||||
// Editor state
|
||||
const [promptMd, setPromptMd] = useState("");
|
||||
const [columns, setColumns] = useState<ColumnConfig[]>([]);
|
||||
|
||||
// Save status
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Column selection
|
||||
const [selectedColIndices, setSelectedColIndices] = useState<number[]>([]);
|
||||
|
||||
// Column modal
|
||||
const [addColumnOpen, setAddColumnOpen] = useState(false);
|
||||
const [editingColumn, setEditingColumn] = useState<ColumnConfig | null>(null);
|
||||
const [viewingColumn, setViewingColumn] = useState<ColumnConfig | null>(null);
|
||||
|
||||
// Share popover
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [deleteStatus, setDeleteStatus] = useState<DeleteStatus>("idle");
|
||||
|
||||
// Column actions dropdown
|
||||
const [colActionsOpen, setColActionsOpen] = useState(false);
|
||||
const colActionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (colActionsRef.current && !colActionsRef.current.contains(e.target as Node)) {
|
||||
setColActionsOpen(false);
|
||||
}
|
||||
}
|
||||
if (colActionsOpen) document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [colActionsOpen]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load workflow
|
||||
// ---------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (isBuiltin) {
|
||||
const wf = builtinWorkflow;
|
||||
if (!wf) {
|
||||
setNotFound(true);
|
||||
} else {
|
||||
setWorkflow(wf);
|
||||
setPromptMd(wf.prompt_md ?? "");
|
||||
setColumns(wf.columns_config ?? []);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
getWorkflow(id)
|
||||
.then((wf) => {
|
||||
if (wf.type !== workflowType) {
|
||||
setNotFound(true);
|
||||
return;
|
||||
}
|
||||
setWorkflow(wf);
|
||||
setPromptMd(wf.prompt_md ?? "");
|
||||
setColumns(
|
||||
(wf.columns_config ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.index - b.index),
|
||||
);
|
||||
})
|
||||
.catch(() => setNotFound(true))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id, isBuiltin, builtinWorkflow, workflowType]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Debounced auto-save for prompt
|
||||
// ---------------------------------------------------------------------------
|
||||
const save = useCallback(
|
||||
(newPromptMd: string) => {
|
||||
if (readOnly) return;
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
setSaveStatus("saving");
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
await updateWorkflow(id, { prompt_md: newPromptMd });
|
||||
setSaveStatus("saved");
|
||||
setTimeout(() => setSaveStatus("idle"), 2000);
|
||||
} catch {
|
||||
setSaveStatus("idle");
|
||||
}
|
||||
}, 800);
|
||||
},
|
||||
[id, readOnly],
|
||||
);
|
||||
|
||||
async function handleWorkflowDetailsSave(values: { title: string }) {
|
||||
if (!workflow || readOnly || !values.title) return;
|
||||
if (values.title === workflow.title) return;
|
||||
const updated = await updateWorkflow(id, { title: values.title });
|
||||
setWorkflow({
|
||||
...updated,
|
||||
shared_by_name:
|
||||
updated.shared_by_name ?? workflow.shared_by_name ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDeleteWorkflow() {
|
||||
if (!workflow || readOnly || workflow.is_owner === false) return;
|
||||
setDeleteStatus("loading");
|
||||
try {
|
||||
await deleteWorkflow(id);
|
||||
setDeleteStatus("complete");
|
||||
setTimeout(() => router.push("/workflows"), 600);
|
||||
} catch {
|
||||
setDeleteStatus("idle");
|
||||
}
|
||||
}
|
||||
|
||||
function handlePromptChange(val: string | undefined) {
|
||||
const next = val ?? "";
|
||||
setPromptMd(next);
|
||||
save(next);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Column save
|
||||
// ---------------------------------------------------------------------------
|
||||
async function saveColumns(next: ColumnConfig[]) {
|
||||
if (readOnly) return;
|
||||
setSaveStatus("saving");
|
||||
try {
|
||||
const updated = await updateWorkflow(id, { columns_config: next });
|
||||
setWorkflow(updated);
|
||||
setSaveStatus("saved");
|
||||
setTimeout(() => setSaveStatus("idle"), 2000);
|
||||
} catch {
|
||||
setSaveStatus("idle");
|
||||
}
|
||||
}
|
||||
|
||||
function handleColumnsAdded(added: ColumnConfig[]) {
|
||||
const next = [
|
||||
...columns,
|
||||
...added.map((c, i) => ({ ...c, index: columns.length + i })),
|
||||
];
|
||||
setColumns(next);
|
||||
saveColumns(next);
|
||||
setAddColumnOpen(false);
|
||||
}
|
||||
|
||||
function handleColumnSaved(updated: ColumnConfig) {
|
||||
const next = columns.map((c) =>
|
||||
c.index === updated.index ? updated : c,
|
||||
);
|
||||
setColumns(next);
|
||||
saveColumns(next);
|
||||
setEditingColumn(null);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<PageHeader
|
||||
shrink
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: "Workflows",
|
||||
onClick: () => router.push("/workflows"),
|
||||
title: "Back to Workflows",
|
||||
},
|
||||
{ loading: true, skeletonClassName: "w-40" },
|
||||
]}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{workflowType === "tabular" ? (
|
||||
<TabularWorkflowEditorSkeleton />
|
||||
) : (
|
||||
<AssistantWorkflowEditorSkeleton />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (notFound || !workflow) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-gray-400 font-serif">Workflow not found.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Page header */}
|
||||
<PageHeader
|
||||
shrink
|
||||
actionGap="md"
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: "Workflows",
|
||||
onClick: () => router.push("/workflows"),
|
||||
title: "Back to Workflows",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<span className="text-gray-900 truncate max-w-xs">
|
||||
{workflow.title}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
actions={[
|
||||
saveStatus !== "idle"
|
||||
? {
|
||||
type: "custom",
|
||||
render: (
|
||||
<span className="inline-flex h-7 items-center gap-1.5 rounded-full px-3 text-sm text-gray-500">
|
||||
{saveStatus === "saved" ? (
|
||||
<Check className="h-3.5 w-3.5 text-green-600" />
|
||||
) : null}
|
||||
{saveStatus === "saving"
|
||||
? "Saving…"
|
||||
: "Saved"}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
: null,
|
||||
canShare
|
||||
? {
|
||||
onClick: () => setShareOpen(true),
|
||||
title: "Open workflow people",
|
||||
iconOnly: true,
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
}
|
||||
: null,
|
||||
!readOnly
|
||||
? {
|
||||
type: "custom",
|
||||
render: (
|
||||
<HeaderActionsMenu
|
||||
title="Workflow actions"
|
||||
items={[
|
||||
{
|
||||
label: "Rename",
|
||||
icon: Pencil,
|
||||
onSelect: () =>
|
||||
setDetailsOpen(true),
|
||||
},
|
||||
{
|
||||
label: "Workflow Details",
|
||||
icon: Info,
|
||||
onSelect: () =>
|
||||
setDetailsOpen(true),
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: Trash2,
|
||||
variant: "danger",
|
||||
disabled:
|
||||
workflow.is_owner === false,
|
||||
onSelect: () => {
|
||||
setDeleteStatus("idle");
|
||||
setDeleteOpen(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: null,
|
||||
]}
|
||||
/>
|
||||
<WorkflowDetailsModal
|
||||
open={detailsOpen}
|
||||
workflow={workflow}
|
||||
canEdit={!readOnly}
|
||||
canShare={canShare}
|
||||
currentUserDisplayName={profile?.displayName}
|
||||
currentUserEmail={user?.email}
|
||||
onClose={() => setDetailsOpen(false)}
|
||||
onSave={handleWorkflowDetailsSave}
|
||||
onShareWorkflow={() => {
|
||||
setDetailsOpen(false);
|
||||
setShareOpen(true);
|
||||
}}
|
||||
/>
|
||||
{shareOpen && (
|
||||
<ShareWorkflowModal
|
||||
workflowId={id}
|
||||
workflowName={workflow.title}
|
||||
onClose={() => setShareOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<ConfirmPopup
|
||||
open={deleteOpen}
|
||||
title="Delete workflow?"
|
||||
message="This workflow will be permanently deleted."
|
||||
confirmLabel="Delete"
|
||||
confirmStatus={deleteStatus}
|
||||
onConfirm={() => void handleDeleteWorkflow()}
|
||||
onCancel={() => {
|
||||
if (deleteStatus === "loading") return;
|
||||
setDeleteOpen(false);
|
||||
setDeleteStatus("idle");
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
{workflow.type === "assistant" ? (
|
||||
/* ── Assistant: WYSIWYG editor ── */
|
||||
<div className="flex-1 min-h-0 px-4 pb-2 pt-0 md:px-10 md:pb-3">
|
||||
<WorkflowPromptEditor
|
||||
value={promptMd}
|
||||
onChange={readOnly ? undefined : handlePromptChange}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* ── Tabular: Column table ── */
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
{/* Toolbar */}
|
||||
{!readOnly && (
|
||||
<div className="flex items-center justify-between px-4 md:px-10 h-10 border-b border-gray-200 shrink-0">
|
||||
<button
|
||||
onClick={() => setAddColumnOpen(true)}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
Add Column
|
||||
</button>
|
||||
{selectedColIndices.length > 0 && (
|
||||
<div ref={colActionsRef} className="relative">
|
||||
<button
|
||||
onClick={() => setColActionsOpen((v) => !v)}
|
||||
className="flex items-center gap-1 text-xs font-medium text-gray-700 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
Actions
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{colActionsOpen && (
|
||||
<div className="absolute top-full right-0 mt-1 w-36 rounded-lg border border-gray-100 bg-white shadow-lg z-50 overflow-hidden">
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = columns
|
||||
.filter((c) => !selectedColIndices.includes(c.index))
|
||||
.map((c, i) => ({ ...c, index: i }));
|
||||
setColumns(next);
|
||||
saveColumns(next);
|
||||
setSelectedColIndices([]);
|
||||
setColActionsOpen(false);
|
||||
}}
|
||||
className="w-full px-3 py-1.5 text-left text-xs text-red-600 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{readOnly && (
|
||||
<div className="flex h-10 shrink-0 items-center bg-gray-50 px-4 md:px-10">
|
||||
<span className="text-xs font-medium text-gray-500">
|
||||
Read-only
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-auto">
|
||||
<div className="min-w-max flex min-h-full flex-col">
|
||||
{/* Table header */}
|
||||
<div className={`flex items-center h-8 pr-3 md:pr-10 border-b border-gray-200 text-xs text-gray-500 font-medium shrink-0 select-none ${readOnly ? "border-t" : ""}`}>
|
||||
<div className={`sticky left-0 z-[60] ${NAME_COL_W} ${stickyCellBg} flex items-center gap-4 self-stretch pl-4 pr-2 text-left`}>
|
||||
{columns.length > 0 && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={columns.length > 0 && selectedColIndices.length === columns.length}
|
||||
ref={(el) => { if (el) el.indeterminate = selectedColIndices.length > 0 && selectedColIndices.length < columns.length; }}
|
||||
onChange={() => setSelectedColIndices(selectedColIndices.length === columns.length ? [] : columns.map((c) => c.index))}
|
||||
className="h-2.5 w-2.5 rounded border-gray-200 cursor-pointer accent-black"
|
||||
/>
|
||||
)}
|
||||
<span>Column Title</span>
|
||||
</div>
|
||||
<div className="ml-auto w-36 shrink-0">Format</div>
|
||||
<div className="flex-1 min-w-0">Prompt</div>
|
||||
{!readOnly && <div className="w-8 shrink-0" />}
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
<div className="flex-1">
|
||||
{columns.length === 0 ? (
|
||||
<div className="flex flex-col items-start py-24 w-full max-w-xs mx-auto">
|
||||
<Plus className="h-8 w-8 text-gray-300 mb-4" />
|
||||
<p className="text-2xl font-medium font-serif text-gray-900">
|
||||
Columns
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-gray-400 text-left">
|
||||
Add columns to define what this tabular review workflow extracts from each document.
|
||||
</p>
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={() => setAddColumnOpen(true)}
|
||||
className="mt-4 inline-flex items-center gap-1 rounded-full bg-gray-900 px-3 py-1 text-xs font-medium text-white hover:bg-gray-700 transition-colors shadow-md"
|
||||
>
|
||||
+ Add Column
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
columns.map((col) => {
|
||||
const FormatIcon = formatIcon(col.format ?? "text");
|
||||
const isChecked = selectedColIndices.includes(col.index);
|
||||
return (
|
||||
<div
|
||||
key={col.index}
|
||||
onClick={() => readOnly ? setViewingColumn(col) : setEditingColumn(col)}
|
||||
className="group flex items-center h-10 pr-3 md:pr-10 border-b border-gray-50 hover:bg-gray-100 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className={`sticky left-0 z-[60] ${NAME_COL_W} py-2 pl-4 pr-2 ${isChecked ? "bg-gray-50" : stickyCellBg} transition-colors group-hover:bg-gray-100`}>
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => setSelectedColIndices((prev) => prev.includes(col.index) ? prev.filter((i) => i !== col.index) : [...prev, col.index])}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-2.5 w-2.5 shrink-0 rounded border-gray-200 cursor-pointer accent-black"
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-sm text-gray-800">
|
||||
{col.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto w-36 shrink-0">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-gray-600">
|
||||
<FormatIcon className="h-3.5 w-3.5 text-gray-400" />
|
||||
{formatLabel(col.format ?? "text")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
<span className="text-xs text-gray-500 truncate block">
|
||||
{col.prompt}
|
||||
</span>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="w-8 shrink-0 flex justify-end">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const next = columns
|
||||
.filter((c) => c.index !== col.index)
|
||||
.map((c, i) => ({ ...c, index: i }));
|
||||
setColumns(next);
|
||||
saveColumns(next);
|
||||
}}
|
||||
className="p-1 text-gray-300 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Read-only column view modal */}
|
||||
{viewingColumn && (
|
||||
<WFColumnViewModal col={viewingColumn} onClose={() => setViewingColumn(null)} />
|
||||
)}
|
||||
|
||||
{/* Add column modal */}
|
||||
<AddColumnModal
|
||||
open={addColumnOpen}
|
||||
existingCount={columns.length}
|
||||
onClose={() => setAddColumnOpen(false)}
|
||||
onAdd={handleColumnsAdded}
|
||||
/>
|
||||
|
||||
{/* Edit column modal */}
|
||||
{editingColumn && (
|
||||
<WFEditColumnModal
|
||||
column={editingColumn}
|
||||
onClose={() => setEditingColumn(null)}
|
||||
onSave={handleColumnSaved}
|
||||
onDelete={() => {
|
||||
const next = columns
|
||||
.filter((c) => c.index !== editingColumn.index)
|
||||
.map((c, i) => ({ ...c, index: i }));
|
||||
setColumns(next);
|
||||
saveColumns(next);
|
||||
setEditingColumn(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssistantWorkflowEditorSkeleton() {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 px-4 pb-2 pt-0 md:px-10 md:pb-3">
|
||||
<div className="h-full rounded-md border border-gray-200 bg-gray-50 px-5 py-4">
|
||||
<div className="space-y-3">
|
||||
<div className="h-3 w-24 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-5/6 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-3/4 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-4/5 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<div className="h-3 w-28 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-11/12 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-2/3 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-10/12 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<div className="h-3 w-20 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-4/6 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-3 w-5/6 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabularWorkflowEditorSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-10 shrink-0 items-center border-b border-gray-200 px-4 md:px-10">
|
||||
<div className="h-3 w-20 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
|
||||
<div className="flex h-8 shrink-0 items-center border-b border-gray-200 pr-3 md:pr-10">
|
||||
<div
|
||||
className={`${NAME_COL_W} flex shrink-0 items-center gap-4 self-stretch pl-4 pr-2`}
|
||||
>
|
||||
<div className="h-2.5 w-2.5 animate-pulse rounded bg-gray-100" />
|
||||
<div className="h-2.5 w-20 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="w-36 shrink-0">
|
||||
<div className="h-2.5 w-14 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-2.5 w-12 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="w-8 shrink-0" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-10 items-center border-b border-gray-50 pr-3 md:pr-10"
|
||||
>
|
||||
<div
|
||||
className={`${NAME_COL_W} flex shrink-0 items-center gap-4 pl-4 pr-2`}
|
||||
>
|
||||
<div className="h-2.5 w-2.5 shrink-0 animate-pulse rounded bg-gray-100" />
|
||||
<div
|
||||
className="h-3 animate-pulse rounded bg-gray-100"
|
||||
style={{ width: `${40 + (i * 13) % 35}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-36 shrink-0">
|
||||
<div className="h-3 w-16 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
<div className="flex-1 pr-4">
|
||||
<div
|
||||
className="h-3 animate-pulse rounded bg-gray-100"
|
||||
style={{ width: `${50 + (i * 17) % 35}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-8 shrink-0" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue