feat(FN-2643): refine scheduled tasks modal header and toolbar layout
- Restructure ScheduledTasksModal header markup to support updated title and actions grouping - Add automation toolbar styling updates in ScriptsModal.css to match the new modal header layout - Update ScheduledTasksModal tests to assert the revised header structure and action placement
This commit is contained in:
@@ -254,30 +254,18 @@ export function ScheduledTasksModal({ onClose, addToast, projectId }: ScheduledT
|
||||
return (
|
||||
<div className="modal-overlay open" onClick={handleOverlayClick}>
|
||||
<div className="modal modal-lg" role="dialog" aria-modal="true" aria-labelledby="schedules-modal-title">
|
||||
<div className="modal-header scheduling-modal-header">
|
||||
<div className="scheduling-header-main-row">
|
||||
<div className="detail-title-row">
|
||||
<Zap size={20} className="icon-triage" />
|
||||
<h3 id="schedules-modal-title">Automations</h3>
|
||||
</div>
|
||||
<div className="modal-header-actions">
|
||||
{isShowingList && (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setRoutineView("create")}
|
||||
aria-label="Create new automation"
|
||||
>
|
||||
<Plus size={14} />
|
||||
New Automation
|
||||
</button>
|
||||
)}
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-header">
|
||||
<div className="detail-title-row">
|
||||
<Zap size={20} className="icon-triage" />
|
||||
<h3 id="schedules-modal-title">Automations</h3>
|
||||
</div>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="scheduling-header-scope-row" role="group" aria-label="Scheduling scope">
|
||||
<div className="scheduling-toolbar" aria-live="polite">
|
||||
<div className="scheduling-toolbar-left" role="group" aria-label="Scheduling scope">
|
||||
<div className="scheduling-scope-selector">
|
||||
<button
|
||||
type="button"
|
||||
@@ -300,30 +288,26 @@ export function ScheduledTasksModal({ onClose, addToast, projectId }: ScheduledT
|
||||
Project
|
||||
</button>
|
||||
</div>
|
||||
<span className="scheduling-count">
|
||||
<Zap size={14} />
|
||||
{routines.length} automation{routines.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="scheduling-toolbar-right">
|
||||
{isShowingList && (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setRoutineView("create")}
|
||||
aria-label="Create new automation"
|
||||
>
|
||||
<Plus size={14} />
|
||||
New Automation
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scheduling-summary" aria-live="polite">
|
||||
<Zap size={14} />
|
||||
<span>{routines.length} automation{routines.length === 1 ? "" : "s"}</span>
|
||||
</div>
|
||||
<div className="detail-tabs" role="tablist">
|
||||
<button
|
||||
className="detail-tab detail-tab-active"
|
||||
role="tab"
|
||||
id="tab-routines"
|
||||
aria-selected="true"
|
||||
aria-controls="scheduled-tasks-content"
|
||||
onClick={() => {
|
||||
setRoutineView("list");
|
||||
setEditingRoutine(undefined);
|
||||
}}
|
||||
>
|
||||
<Zap size={14} /> Routines
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="schedule-modal-content" role="tabpanel" id="scheduled-tasks-content">
|
||||
<div className="schedule-modal-content" id="scheduled-tasks-content">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,48 +57,43 @@
|
||||
|
||||
/* ── Scheduled Tasks ──────────────────────────────────────────────── */
|
||||
|
||||
/* Automation count summary in scheduled tasks modal */
|
||||
.scheduling-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
font-size: var(--text-xs, 12px);
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.schedule-modal-content {
|
||||
padding: var(--space-lg) 20px;
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
/* Scheduling modal header */
|
||||
.scheduling-modal-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.scheduling-header-main-row {
|
||||
/* Scheduling toolbar below modal header */
|
||||
.scheduling-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-sm) var(--modal-padding, var(--space-lg));
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--text) 10%, transparent);
|
||||
}
|
||||
|
||||
.scheduling-header-main-row .detail-title-row {
|
||||
.scheduling-toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.scheduling-header-scope-row {
|
||||
.scheduling-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: var(--space-sm);
|
||||
border-top: 1px solid var(--border);
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.scheduling-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
font-size: var(--text-xs, 12px);
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-modal-content {
|
||||
padding: var(--space-lg) var(--space-xl);
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
/* Scheduling scope selector */
|
||||
@@ -815,18 +810,19 @@
|
||||
padding-right: var(--space-lg);
|
||||
}
|
||||
|
||||
.scheduling-header-main-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.scheduling-modal-header .modal-header-actions {
|
||||
.scheduling-toolbar {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.scheduling-header-scope-row {
|
||||
padding-top: var(--space-sm);
|
||||
.scheduling-toolbar-left {
|
||||
flex: 1 1 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scheduling-toolbar-right {
|
||||
flex: 1 1 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.scheduling-scope-selector {
|
||||
@@ -838,10 +834,6 @@
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Routines ──────────────────────────────────────────────────── */
|
||||
|
||||
@@ -109,11 +109,12 @@ describe("ScheduledTasksModal", () => {
|
||||
|
||||
expect(screen.getByText("Automations")).toBeDefined();
|
||||
expect(screen.getByRole("dialog").getAttribute("aria-labelledby")).toBe("schedules-modal-title");
|
||||
expect(screen.getByRole("button", { name: "Close" })).toBeDefined();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No automations yet")).toBeDefined();
|
||||
});
|
||||
expect(screen.getByText("Create your first automation")).toBeDefined();
|
||||
expect(screen.getByText("Routines")).toBeDefined();
|
||||
expect(screen.getByText("0 automations")).toBeDefined();
|
||||
expect(mockFetchAutomations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -131,7 +132,7 @@ describe("ScheduledTasksModal", () => {
|
||||
expect(screen.getByText("New Automation")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders scope controls in a dedicated header row", async () => {
|
||||
it("renders scope controls in the toolbar below the modal header", async () => {
|
||||
mockFetchRoutines.mockResolvedValue([makeRoutine({ name: "Scoped Routine" })]);
|
||||
const { container } = render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
||||
|
||||
@@ -139,15 +140,21 @@ describe("ScheduledTasksModal", () => {
|
||||
expect(screen.getByText("Scoped Routine")).toBeDefined();
|
||||
});
|
||||
|
||||
const headerActions = container.querySelector(".scheduling-header-main-row .modal-header-actions");
|
||||
const scopeRow = container.querySelector(".scheduling-header-scope-row");
|
||||
const header = container.querySelector(".modal-header");
|
||||
const toolbar = container.querySelector(".scheduling-toolbar");
|
||||
const toolbarLeft = container.querySelector(".scheduling-toolbar-left");
|
||||
const toolbarRight = container.querySelector(".scheduling-toolbar-right");
|
||||
const scopeSelector = container.querySelector(".scheduling-scope-selector");
|
||||
const newAutomationButton = screen.getByRole("button", { name: /new automation/i });
|
||||
|
||||
expect(headerActions).toBeTruthy();
|
||||
expect(scopeRow).toBeTruthy();
|
||||
expect(header).toBeTruthy();
|
||||
expect(toolbar).toBeTruthy();
|
||||
expect(toolbarLeft).toBeTruthy();
|
||||
expect(toolbarRight).toBeTruthy();
|
||||
expect(scopeSelector).toBeTruthy();
|
||||
expect(scopeRow?.contains(scopeSelector as Node)).toBe(true);
|
||||
expect(headerActions?.contains(scopeSelector as Node)).toBe(false);
|
||||
expect(toolbarLeft?.contains(scopeSelector as Node)).toBe(true);
|
||||
expect(header?.contains(scopeSelector as Node)).toBe(false);
|
||||
expect(toolbarRight?.contains(newAutomationButton)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses routine APIs with global scope by default", async () => {
|
||||
|
||||
Reference in New Issue
Block a user