remove the Automation page
Deletes the Automation and NewAutomation screens, their /automation and /new-automation routes, the Dashboard barrel exports, the dock item (+ default dock path), and the page-title rule. TaskRunnerModal is kept — the file browser still uses it to run tasks on files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,8 +50,6 @@ export function App() {
|
||||
/>
|
||||
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
|
||||
<Route path="/settings/apps" element={<Dashboard.AppsSettings />} />
|
||||
<Route path="/automation" element={<Dashboard.Automation />} />
|
||||
<Route path="/new-automation" element={<Dashboard.NewAutomation />} />
|
||||
<Route path="/chat" element={<Dashboard.SessionListPage />} />
|
||||
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
|
||||
<Route path="/chat/saved/:id" element={<Dashboard.SessionListPage />} />
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { Play, Trash2, Terminal, Bot, Workflow } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { Card } from '@/components/Card';
|
||||
import { TaskRunnerModal } from 'officerdev';
|
||||
import type { TaskSummary } from 'officerdev';
|
||||
|
||||
type TaskDetail = {
|
||||
dirName: string;
|
||||
name: string;
|
||||
description: string;
|
||||
mode: string;
|
||||
language: string | null;
|
||||
body: string | null;
|
||||
inputs: Record<string, unknown> | null;
|
||||
config: { steps?: Array<{ task: string; foreach?: string }> } | null;
|
||||
version: number | null;
|
||||
};
|
||||
|
||||
const modeLabels: Record<string, { label: string; icon: typeof Terminal; color: string }> = {
|
||||
script: { label: 'Script', icon: Terminal, color: 'bg-emerald-500/10 text-emerald-600' },
|
||||
agentic: { label: 'Agentic', icon: Bot, color: 'bg-violet-500/10 text-violet-600' },
|
||||
pipeline: { label: 'Pipeline', icon: Workflow, color: 'bg-amber-500/10 text-amber-600' },
|
||||
};
|
||||
|
||||
export const AutomationDetail = () => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const [selected, setSelected] = usePanelChannel<TaskSummary | null>('automation:selected-task', null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||||
const [runTask, setRunTask] = useState<TaskSummary | null>(null);
|
||||
|
||||
// Reset delete confirm when selection changes
|
||||
useEffect(() => {
|
||||
setDeleteConfirm(false);
|
||||
}, [selected?.dirName]);
|
||||
|
||||
const { data: detail } = useQuery<TaskDetail>({
|
||||
queryKey: ['tasks', selected?.dirName],
|
||||
queryFn: () => client.get<TaskDetail>(`/tasks/${selected!.dirName}`),
|
||||
enabled: !!selected?.dirName,
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selected) return;
|
||||
try {
|
||||
await client.delete(`/tasks/${selected.dirName}`);
|
||||
await qc.invalidateQueries({ queryKey: ['tasks'] });
|
||||
setDeleteConfirm(false);
|
||||
setSelected(null);
|
||||
toast.success('Automation deleted');
|
||||
} catch {
|
||||
toast.error('Failed to delete automation');
|
||||
}
|
||||
};
|
||||
|
||||
if (!selected) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<p className="text-sm text-duck-dark/40 dark:text-foreground/40">Select an automation to view details</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mode = modeLabels[selected.mode] ?? modeLabels.agentic!;
|
||||
const ModeIcon = mode.icon;
|
||||
|
||||
const hasSteps = detail?.config?.steps && detail.config.steps.length > 0;
|
||||
const hasInputs = detail?.inputs && Object.keys(detail.inputs).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full overflow-hidden flex flex-col min-h-0">
|
||||
{/* Header */}
|
||||
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60 flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70 flex-1 truncate">
|
||||
{detail?.name ?? selected.name}
|
||||
</span>
|
||||
<span
|
||||
className={`shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium ${mode.color}`}
|
||||
>
|
||||
<ModeIcon className="h-2.5 w-2.5" />
|
||||
{mode.label}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setRunTask(selected)}
|
||||
className="p-1 rounded hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer transition-colors"
|
||||
title="Run"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(true)}
|
||||
className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-500/10 cursor-pointer transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-duck-dark/50 dark:text-foreground/50 hover:text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="overflow-y-auto flex-1 p-6">
|
||||
{/* Description */}
|
||||
{detail?.description && (
|
||||
<p className="text-sm text-duck-dark/60 dark:text-foreground/60 mb-4">{detail.description}</p>
|
||||
)}
|
||||
|
||||
{/* Meta badges */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{detail?.language && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-duck-dark/8 dark:bg-foreground/8 text-duck-dark/50 dark:text-foreground/50 font-medium">
|
||||
{detail.language}
|
||||
</span>
|
||||
)}
|
||||
{detail?.version && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-duck-dark/8 dark:bg-foreground/8 text-duck-dark/50 dark:text-foreground/50 font-medium">
|
||||
v{detail.version}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pipeline steps */}
|
||||
{hasSteps && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-medium text-duck-dark/50 dark:text-foreground/50 uppercase tracking-wider mb-2">
|
||||
Pipeline Steps
|
||||
</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
{detail!.config!.steps!.map((step, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 text-sm text-duck-dark/70 dark:text-foreground/70 px-2 py-1 rounded bg-duck-dark/3 dark:bg-foreground/3"
|
||||
>
|
||||
<span className="w-5 text-center font-mono text-xs text-duck-dark/40 dark:text-foreground/40">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="font-medium">{step.task}</span>
|
||||
{step.foreach && (
|
||||
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">
|
||||
(foreach: {step.foreach})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inputs */}
|
||||
{hasInputs && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-medium text-duck-dark/50 dark:text-foreground/50 uppercase tracking-wider mb-2">
|
||||
Inputs
|
||||
</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
{Object.entries(detail!.inputs!).map(([key, def]) => {
|
||||
const d = def as { type?: string; description?: string; default?: string };
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-baseline gap-2 text-sm px-2 py-1 rounded bg-duck-dark/3 dark:bg-foreground/3"
|
||||
>
|
||||
<span className="font-mono text-xs text-duck-teal">{key}</span>
|
||||
{d.type && (
|
||||
<span className="text-[10px] text-duck-dark/40 dark:text-foreground/40">{d.type}</span>
|
||||
)}
|
||||
{d.description && (
|
||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50 flex-1">
|
||||
{d.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body (agent instructions) */}
|
||||
{detail?.body ? (
|
||||
<article className="skill-md">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||
{detail.body}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
) : detail && !hasSteps ? (
|
||||
<p className="text-sm text-duck-dark/40 dark:text-foreground/40 text-center mt-12">No instructions</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Delete confirmation */}
|
||||
<Dialog open={deleteConfirm} onOpenChange={setDeleteConfirm}>
|
||||
<DialogContent className="sm:max-w-md z-[700]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Automation</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete "{selected.name}"? This cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<Button variant="outline" onClick={() => setDeleteConfirm(false)} className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDelete} className="cursor-pointer">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Run modal */}
|
||||
{runTask && (
|
||||
<TaskRunnerModal
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setRunTask(null);
|
||||
}}
|
||||
task={runTask}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,202 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Plus, Search, Play, Terminal, Bot, Workflow } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { TaskRunnerModal } from 'officerdev';
|
||||
import type { TaskSummary } from 'officerdev';
|
||||
|
||||
const modeIcons = {
|
||||
script: Terminal,
|
||||
agentic: Bot,
|
||||
pipeline: Workflow,
|
||||
} as const;
|
||||
|
||||
const modeColors = {
|
||||
script: 'bg-emerald-500/10 text-emerald-600',
|
||||
agentic: 'bg-violet-500/10 text-violet-600',
|
||||
pipeline: 'bg-amber-500/10 text-amber-600',
|
||||
} as const;
|
||||
|
||||
export const AutomationList = () => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const [selected, setSelected] = usePanelChannel<TaskSummary | null>('automation:selected-task', null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [createName, setCreateName] = useState('');
|
||||
const [createDesc, setCreateDesc] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [runTask, setRunTask] = useState<TaskSummary | null>(null);
|
||||
|
||||
const { data: tasks = [] } = useQuery<TaskSummary[]>({
|
||||
queryKey: ['tasks'],
|
||||
queryFn: () => client.get('/tasks'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const q = search.toLowerCase();
|
||||
const filtered = search
|
||||
? tasks.filter((t) => t.name.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q))
|
||||
: tasks;
|
||||
|
||||
const handleCreate = async () => {
|
||||
const name = createName.trim();
|
||||
if (!name) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
await client.post('/tasks', { name, description: createDesc.trim() || undefined });
|
||||
await qc.invalidateQueries({ queryKey: ['tasks'] });
|
||||
setShowCreate(false);
|
||||
setCreateName('');
|
||||
setCreateDesc('');
|
||||
toast.success('Automation created');
|
||||
} catch {
|
||||
toast.error('Failed to create automation');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60 flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70 flex-1">Automations</span>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="p-1 rounded hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer transition-colors"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 text-duck-dark/50 dark:text-foreground/50" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="px-3 pt-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/30 dark:text-foreground/30" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
placeholder="Search..."
|
||||
className="w-full rounded border border-duck-dark/15 dark:border-foreground/15 bg-background/80 pl-7 pr-2 py-1.5 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list */}
|
||||
<div className="flex-1 overflow-y-auto px-3 pt-2 pb-3">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{filtered.map((task) => {
|
||||
const ModeIcon = modeIcons[task.mode] ?? Bot;
|
||||
const modeColor = modeColors[task.mode] ?? modeColors.agentic;
|
||||
const isSelected = selected?.dirName === task.dirName;
|
||||
return (
|
||||
<div
|
||||
key={task.dirName}
|
||||
className={`group flex items-center gap-2 w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
isSelected
|
||||
? 'bg-duck-teal/10 text-duck-dark dark:text-foreground'
|
||||
: 'text-duck-dark/60 dark:text-foreground/60 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setSelected(task)}
|
||||
className="flex-1 min-w-0 text-left cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium truncate">{task.name}</span>
|
||||
<span className={`shrink-0 inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-[9px] font-medium ${modeColor}`}>
|
||||
<ModeIcon className="h-2.5 w-2.5" />
|
||||
</span>
|
||||
</div>
|
||||
{task.description && (
|
||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40 mt-0.5 line-clamp-1">{task.description}</p>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={(ev) => { ev.stopPropagation(); setRunTask(task); }}
|
||||
className="p-1 rounded hover:bg-duck-teal/10 transition-colors cursor-pointer opacity-0 group-hover:opacity-100 shrink-0"
|
||||
title="Run"
|
||||
>
|
||||
<Play className="h-3 w-3 text-duck-teal" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{tasks.length === 0 && (
|
||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40 px-3 py-4 text-center">
|
||||
No automations yet
|
||||
</p>
|
||||
)}
|
||||
{tasks.length > 0 && filtered.length === 0 && (
|
||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40 px-3 py-4 text-center">
|
||||
No matches
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create dialog */}
|
||||
<Dialog open={showCreate} onOpenChange={setShowCreate}>
|
||||
<DialogContent className="sm:max-w-md z-[700]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Automation</DialogTitle>
|
||||
<DialogDescription>Give your automation a name and optional description.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(ev) => { ev.preventDefault(); handleCreate(); }}
|
||||
className="flex flex-col gap-3 mt-1"
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Name</label>
|
||||
<input
|
||||
value={createName}
|
||||
onChange={(ev) => setCreateName(ev.target.value)}
|
||||
placeholder="e.g. Batch Resize Images"
|
||||
autoFocus
|
||||
className="rounded border border-duck-dark/20 dark:border-foreground/20 bg-background px-3 py-2 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Description</label>
|
||||
<textarea
|
||||
value={createDesc}
|
||||
onChange={(ev) => setCreateDesc(ev.target.value)}
|
||||
placeholder="What does this automation do?"
|
||||
rows={3}
|
||||
className="rounded border border-duck-dark/20 dark:border-foreground/20 bg-background px-3 py-2 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-1">
|
||||
<Button type="button" variant="outline" onClick={() => setShowCreate(false)} className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!createName.trim() || creating}
|
||||
className="bg-duck-teal text-white hover:bg-duck-teal/90 cursor-pointer"
|
||||
>
|
||||
{creating ? 'Creating...' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Run modal */}
|
||||
{runTask && (
|
||||
<TaskRunnerModal
|
||||
open
|
||||
onOpenChange={(open) => { if (!open) setRunTask(null); }}
|
||||
task={runTask}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
# Automation
|
||||
|
||||
## Overview
|
||||
|
||||
The `/automation` route shows all user automations (tasks) in a standard `WorkspaceView` layout: left panel for the list, right panel for task details.
|
||||
|
||||
## Route
|
||||
|
||||
```tsx
|
||||
<Route path="/automation" element={<Dashboard.Automation />} />
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
+------------------+------------------------------------------+
|
||||
| List (30%) | Detail (70%) |
|
||||
| | |
|
||||
| Search | Task name, mode, description |
|
||||
| Task items | Pipeline steps / Inputs / Body |
|
||||
| + Create | Run / Delete actions |
|
||||
+------------------+------------------------------------------+
|
||||
```
|
||||
|
||||
Uses `useDashboardState('screens/automation')` for persistent layout and `usePanelChannel<TaskSummary>('automation:selected-task')` for cross-panel selection.
|
||||
|
||||
## Components
|
||||
|
||||
### `index.tsx`
|
||||
Entry point. Sets up `WorkspaceView` with two panels, manages mobile panel switching.
|
||||
|
||||
### `AutomationList.tsx`
|
||||
Left panel. Fetches tasks from `GET /tasks`, shows searchable list with mode badges (Script/Agentic/Pipeline). Has create dialog and run button per task.
|
||||
|
||||
### `AutomationDetail.tsx`
|
||||
Right panel. Fetches task detail from `GET /tasks/:name`. Shows description, pipeline steps, input definitions, and markdown body. Run and delete actions.
|
||||
|
||||
## Task Data
|
||||
|
||||
Tasks come from `officerdb` database. Modes:
|
||||
- **script** (green) — deterministic bash/python/ts
|
||||
- **agentic** (purple) — AI-powered with MCP tools
|
||||
- **pipeline** (amber) — multi-step orchestration
|
||||
|
||||
## Related
|
||||
|
||||
- `TaskRunnerModal` in `officerdev` workspace handles execution for all modes
|
||||
- `CapabilityPage.tsx` — legacy standalone pages for `/skills`, `/tasks`, `/processes`
|
||||
- File browser context menu triggers via `useTasks` hook
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
||||
import { WorkspaceView } from 'officerdev';
|
||||
import { useIsMobile } from 'hooks/useIsMobile';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import type { TaskSummary } from 'officerdev';
|
||||
|
||||
import { AutomationList } from './AutomationList';
|
||||
import { AutomationDetail } from './AutomationDetail';
|
||||
|
||||
export type { TaskSummary as SelectedTask };
|
||||
|
||||
const defaultLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'automation-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'automation-list', appType: null }, size: 30 },
|
||||
{ node: { type: 'panel', id: 'automation-detail', appType: null }, size: 70 },
|
||||
],
|
||||
};
|
||||
|
||||
export const Automation = () => {
|
||||
const workspace = useDashboardState<LayoutNode>('screens/automation', defaultLayout);
|
||||
const [selected] = usePanelChannel<TaskSummary | null>('automation:selected-task', null);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const panelComponents: PanelComponents = useMemo(
|
||||
() => ({
|
||||
'automation-list': AutomationList,
|
||||
'automation-detail': AutomationDetail,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const mobilePanelId = isMobile && selected ? 'automation-detail' : undefined;
|
||||
const onMobileBack = useCallback(() => {}, []);
|
||||
|
||||
if (!workspace.isLoaded) return null;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
locked
|
||||
components={panelComponents}
|
||||
mobilePanelId={mobilePanelId}
|
||||
onMobilePanelChange={mobilePanelId ? () => onMobileBack() : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -110,7 +110,7 @@ export const Dock = ({ items, className }: DockProps) => {
|
||||
};
|
||||
|
||||
|
||||
import { Home, MessageCircle, FileText, FolderOpen, Code, LayoutGrid, Bot, ScrollText, FolderKanban, Monitor, Mail, Globe, MonitorSmartphone, Workflow } from 'lucide-react';
|
||||
import { Home, MessageCircle, FileText, FolderOpen, Code, LayoutGrid, ScrollText, FolderKanban, Monitor, Mail, Globe, MonitorSmartphone, Workflow } from 'lucide-react';
|
||||
|
||||
export const ALL_DOCK_ITEMS: DockItem[] = [
|
||||
{ label: 'Home', to: '/', icon: Home, color: '#f59e0b' },
|
||||
@@ -119,7 +119,6 @@ export const ALL_DOCK_ITEMS: DockItem[] = [
|
||||
{ label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
|
||||
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
|
||||
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
|
||||
{ label: 'Automation', to: '/automation', icon: Bot, color: '#2dd4bf' },
|
||||
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
|
||||
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
|
||||
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
|
||||
@@ -129,4 +128,4 @@ export const ALL_DOCK_ITEMS: DockItem[] = [
|
||||
{ label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' },
|
||||
];
|
||||
|
||||
export const DEFAULT_DOCK_PATHS = ['/', '/files', '/automation', '/projects', '/dashboards', '/chat'];
|
||||
export const DEFAULT_DOCK_PATHS = ['/', '/files', '/projects', '/dashboards', '/chat'];
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
||||
import { WorkspaceLayout } from 'officerdev';
|
||||
|
||||
const LeftPanel = () => {
|
||||
return <div className="h-full w-full p-4" />;
|
||||
};
|
||||
|
||||
const RightPanel = () => {
|
||||
return <div className="h-full w-full p-4" />;
|
||||
};
|
||||
|
||||
const layout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'new-automation-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'new-automation-left', appType: null }, size: 30 },
|
||||
{ node: { type: 'panel', id: 'new-automation-right', appType: null }, size: 70 },
|
||||
],
|
||||
};
|
||||
|
||||
export const NewAutomation = () => {
|
||||
const panelComponents: PanelComponents = useMemo(
|
||||
() => ({
|
||||
'new-automation-left': LeftPanel,
|
||||
'new-automation-right': RightPanel,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,8 +6,6 @@ export * from './Plans';
|
||||
export * from './Processes';
|
||||
export * from './CapabilityPage';
|
||||
export * from './Settings';
|
||||
export * from './Automation';
|
||||
export * from './NewAutomation';
|
||||
export * from './Skills';
|
||||
export * from './TaskLogs';
|
||||
export * from './Tasks';
|
||||
|
||||
@@ -23,7 +23,6 @@ const RULES: TitleRule[] = [
|
||||
{ match: (p) => p.startsWith('/skills'), title: 'Skills' },
|
||||
{ match: (p) => p.startsWith('/processes'), title: 'Processes' },
|
||||
{ match: (p) => p.startsWith('/dashboards'), title: 'Dashboards' },
|
||||
{ match: (p) => p.startsWith('/automation') || p.startsWith('/new-automation'), title: 'Automation' },
|
||||
{ match: (p) => p.startsWith('/terminal'), title: 'Terminal' },
|
||||
{ match: (p) => p.startsWith('/browser'), title: 'Browser' },
|
||||
{ match: (p) => p.startsWith('/desktop'), title: 'Desktop' },
|
||||
|
||||
Reference in New Issue
Block a user