feat(FN-1978): rename schedule actions to Automation and polish mobile layout
- Rename Schedules/Scheduled Tasks labels to Automation in header and mobile navigation actions - Update desktop, tablet, and mobile header tests to assert the new Automation wording and behavior - Improve mobile schedule and routine modal spacing with token-based padding/margin updates for cards, forms, and empty states - Refresh the UX audit report to reference the Automation label in the header control inventory
This commit is contained in:
@@ -57,7 +57,7 @@ This audit acknowledges and does not duplicate the following existing backlog it
|
||||
### 1.1 Header Overload on Desktop
|
||||
|
||||
- **Component:** `packages/dashboard/app/components/Header.tsx` (lines ~200-650)
|
||||
- **Current behavior:** The header displays 15+ icon buttons without labels on desktop, including: Usage, Activity Log, Mailbox, GitHub Import, Planning, Schedules, Terminal, Files, Git Manager, Nodes, Workflow Steps, Scripts, Pause, Stop, Settings, plus view toggle buttons and project selector. Users must hover over each icon to discover its function.
|
||||
- **Current behavior:** The header displays 15+ icon buttons without labels on desktop, including: Usage, Activity Log, Mailbox, GitHub Import, Planning, Automation, Terminal, Files, Git Manager, Nodes, Workflow Steps, Scripts, Pause, Stop, Settings, plus view toggle buttons and project selector. Users must hover over each icon to discover its function.
|
||||
- **Recommended fix:** Group related actions into collapsible sections or a hamburger menu. Primary actions (Settings, Planning, Usage) should remain visible; secondary actions (Nodes, Workflow Steps, Scripts) should move to an overflow menu. Consider a "compact mode" toggle for users who want maximum screen space.
|
||||
- **Impact:** All users are affected. New users cannot discover functionality, and power users waste time finding actions.
|
||||
- **Effort estimate:** M
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const workspaceRoot = join(__dirname, "..", "..", "..", "..");
|
||||
|
||||
function listSourceFiles(dir: string): string[] {
|
||||
const entries = readdirSync(dir);
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const path = join(dir, entry);
|
||||
const stat = statSync(path);
|
||||
const path = join(dir, entry.name);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (entry === "__tests__" || entry === "dist" || entry === "node_modules") {
|
||||
if (entry.isDirectory()) {
|
||||
if (
|
||||
entry.name === "__tests__" ||
|
||||
entry.name === "dist" ||
|
||||
entry.name === "node_modules" ||
|
||||
entry.name.startsWith(".")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
files.push(...listSourceFiles(path));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!/\.(ts|tsx)$/.test(entry) || /\.test\.(ts|tsx)$/.test(entry)) {
|
||||
if (!/\.(ts|tsx)$/.test(entry.name) || /\.test\.(ts|tsx)$/.test(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -40,13 +44,18 @@ describe("architecture hot-path contracts", () => {
|
||||
];
|
||||
const bareListTaskCalls: string[] = [];
|
||||
|
||||
const bareListTasksPattern = /\.\s*listTasks\(\)/;
|
||||
|
||||
for (const root of sourceRoots) {
|
||||
for (const file of listSourceFiles(join(workspaceRoot, root))) {
|
||||
const content = readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
if (!bareListTasksPattern.test(content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lines = content.split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
if (/\.\s*listTasks\(\)/.test(line)) {
|
||||
if (bareListTasksPattern.test(line)) {
|
||||
bareListTaskCalls.push(`${relative(workspaceRoot, file)}:${index + 1}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -123,9 +123,9 @@ describe("tablet header controls", () => {
|
||||
expect(screen.queryByTitle("Open Terminal")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render schedules button inline on tablet", () => {
|
||||
it("does not render automation button inline on tablet", () => {
|
||||
renderTabletHeader({ onOpenSchedules: noop });
|
||||
expect(screen.queryByTitle("Scheduled tasks")).toBeNull();
|
||||
expect(screen.queryByTitle("Automation")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render usage button inline on tablet", () => {
|
||||
@@ -194,10 +194,10 @@ describe("tablet header controls", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("overflow menu contains scheduled tasks on tablet", () => {
|
||||
it("overflow menu contains automation on tablet", () => {
|
||||
renderTabletHeader({ onOpenSchedules: noop });
|
||||
fireEvent.click(screen.getByTitle("More header actions"));
|
||||
expect(screen.getByText("Scheduled Tasks")).toBeDefined();
|
||||
expect(screen.getByText("Automation")).toBeDefined();
|
||||
});
|
||||
|
||||
it("overflow menu contains usage on tablet when provided", () => {
|
||||
|
||||
@@ -799,21 +799,21 @@ describe("Header", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("schedules button", () => {
|
||||
it("renders schedules button on desktop", () => {
|
||||
describe("automation button", () => {
|
||||
it("renders automation button on desktop", () => {
|
||||
renderHeader({ onOpenSchedules: vi.fn() }, "desktop");
|
||||
expect(screen.getByTitle("Scheduled tasks")).toBeDefined();
|
||||
expect(screen.getByTitle("Automation")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not render schedules button inline on mobile", () => {
|
||||
it("does not render automation button inline on mobile", () => {
|
||||
renderHeader({ onOpenSchedules: vi.fn() }, "mobile");
|
||||
expect(screen.queryByTitle("Scheduled tasks")).toBeNull();
|
||||
expect(screen.queryByTitle("Automation")).toBeNull();
|
||||
});
|
||||
|
||||
it("calls onOpenSchedules when schedules button is clicked", () => {
|
||||
it("calls onOpenSchedules when automation button is clicked", () => {
|
||||
const onOpenSchedules = vi.fn();
|
||||
renderHeader({ onOpenSchedules }, "desktop");
|
||||
fireEvent.click(screen.getByTitle("Scheduled tasks"));
|
||||
fireEvent.click(screen.getByTitle("Automation"));
|
||||
expect(onOpenSchedules).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -822,10 +822,10 @@ describe("Header", () => {
|
||||
expect(screen.getByTestId("schedules-btn")).toBeDefined();
|
||||
});
|
||||
|
||||
it("includes scheduled tasks in overflow menu on mobile", () => {
|
||||
it("includes automation in overflow menu on mobile", () => {
|
||||
renderHeader({ onOpenSchedules: vi.fn() }, "mobile");
|
||||
fireEvent.click(screen.getByTitle("More header actions"));
|
||||
expect(screen.getByText("Scheduled Tasks")).toBeDefined();
|
||||
expect(screen.getByText("Automation")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onOpenSchedules from mobile overflow menu", () => {
|
||||
|
||||
@@ -829,7 +829,7 @@ export function Header({
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={onOpenSchedules}
|
||||
title="Scheduled tasks"
|
||||
title="Automation"
|
||||
data-testid="schedules-btn"
|
||||
>
|
||||
<Clock size={16} />
|
||||
@@ -1120,7 +1120,7 @@ export function Header({
|
||||
data-testid="overflow-schedules-btn"
|
||||
>
|
||||
<Clock size={16} />
|
||||
<span>Scheduled Tasks</span>
|
||||
<span>Automation</span>
|
||||
</button>
|
||||
{/* Activity Log - in overflow on mobile */}
|
||||
{onOpenActivityLog && (
|
||||
|
||||
@@ -447,7 +447,7 @@ export function MobileNavBar({
|
||||
onClick={() => handleMoreAction(onOpenSchedules)}
|
||||
>
|
||||
<Clock />
|
||||
<span>Schedules</span>
|
||||
<span>Automation</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -17149,7 +17149,12 @@ html .column.drag-over * {
|
||||
@media (max-width: 768px) {
|
||||
.schedule-modal-content {
|
||||
max-height: none;
|
||||
padding: var(--space-md) 14px;
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
margin: 0;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.schedule-card-header {
|
||||
@@ -17162,6 +17167,28 @@ html .column.drag-over * {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.schedule-empty-state {
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.schedule-form {
|
||||
padding-left: var(--space-lg);
|
||||
padding-right: var(--space-lg);
|
||||
}
|
||||
|
||||
.scheduling-scope-selector {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-header-actions {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Routines ──────────────────────────────────────────────────── */
|
||||
@@ -17405,6 +17432,11 @@ html .column.drag-over * {
|
||||
|
||||
/* Routine mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.routine-card {
|
||||
margin: 0;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.routine-card-header {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
@@ -17416,6 +17448,15 @@ html .column.drag-over * {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.routine-empty-state {
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.routine-form {
|
||||
padding-left: var(--space-lg);
|
||||
padding-right: var(--space-lg);
|
||||
}
|
||||
|
||||
.routine-trigger-type-selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user