fix(FN-1801): plugin settings UX and theme awareness improvements
- Add new input types for plugin settings (password, textarea, array fields) - Render toggle switch inside .toggle-switch label for theme-aware styling - Update STATE_COLORS to use CSS variable references instead of hardcoded hex values - Add SSE live updates for plugin lifecycle events (enabled, disabled, uninstalled, settings-updated) - Project-scoped SSE subscription when projectId is provided - Expand PluginManager tests with 32 passing tests covering new functionality
This commit is contained in:
@@ -39,12 +39,12 @@ interface PluginManagerProps {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATE_COLORS: Record<string, string> = {
|
export const STATE_COLORS: Record<string, string> = {
|
||||||
started: "var(--color-success, #22c55e)",
|
started: "var(--color-success)",
|
||||||
loaded: "var(--color-warning, #eab308)",
|
loaded: "var(--color-warning)",
|
||||||
error: "var(--color-error, #ef4444)",
|
error: "var(--color-error)",
|
||||||
stopped: "var(--color-muted, #6b7280)",
|
stopped: "var(--color-muted)",
|
||||||
installed: "var(--color-info, #3b82f6)",
|
installed: "var(--color-info)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
export function PluginManager({ addToast, projectId }: PluginManagerProps) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ vi.mock("../../api", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Import after vi.mock so the mock is in place
|
// Import after vi.mock so the mock is in place
|
||||||
import { PluginManager } from "../PluginManager";
|
import { PluginManager, STATE_COLORS } from "../PluginManager";
|
||||||
import {
|
import {
|
||||||
fetchPlugins,
|
fetchPlugins,
|
||||||
installPlugin,
|
installPlugin,
|
||||||
@@ -886,4 +886,52 @@ describe("PluginManager", () => {
|
|||||||
expect(numberInputs.length).toBeGreaterThanOrEqual(3);
|
expect(numberInputs.length).toBeGreaterThanOrEqual(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("toggle switch rendering", () => {
|
||||||
|
it("renders enabled toggle switch inside .toggle-switch label", async () => {
|
||||||
|
vi.mocked(fetchPlugins).mockResolvedValueOnce([{ ...mockPlugins[0], enabled: true }]);
|
||||||
|
|
||||||
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the toggle-switch label
|
||||||
|
const toggleSwitch = screen.getByText("Test Plugin A").closest(".plugin-item")?.querySelector(".toggle-switch");
|
||||||
|
expect(toggleSwitch).toBeTruthy();
|
||||||
|
|
||||||
|
// Find the checkbox inside
|
||||||
|
const checkbox = toggleSwitch?.querySelector('input[type="checkbox"]');
|
||||||
|
expect(checkbox).toBeTruthy();
|
||||||
|
expect(checkbox?.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders disabled toggle switch inside .toggle-switch label", async () => {
|
||||||
|
vi.mocked(fetchPlugins).mockResolvedValueOnce([{ ...mockPlugins[0], enabled: false }]);
|
||||||
|
|
||||||
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Test Plugin A")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the toggle-switch label
|
||||||
|
const toggleSwitch = screen.getByText("Test Plugin A").closest(".plugin-item")?.querySelector(".toggle-switch");
|
||||||
|
expect(toggleSwitch).toBeTruthy();
|
||||||
|
|
||||||
|
// Find the checkbox inside
|
||||||
|
const checkbox = toggleSwitch?.querySelector('input[type="checkbox"]');
|
||||||
|
expect(checkbox).toBeTruthy();
|
||||||
|
expect(checkbox?.checked).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("STATE_COLORS", () => {
|
||||||
|
it("contains only CSS variable references with no hardcoded hex values", () => {
|
||||||
|
const values = Object.values(STATE_COLORS);
|
||||||
|
const allUseCssVars = values.every((v) => /^var\(--[a-z-]+\)$/.test(v));
|
||||||
|
expect(allUseCssVars).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
--color-error-dark: #da3633;
|
--color-error-dark: #da3633;
|
||||||
--color-muted: #8b949e;
|
--color-muted: #8b949e;
|
||||||
--color-info: #1f6feb;
|
--color-info: #1f6feb;
|
||||||
|
--color-warning: #ca8a04;
|
||||||
|
|
||||||
/* Workflow Step type / state colors */
|
/* Workflow Step type / state colors */
|
||||||
--ws-pre-merge: #3b82f6;
|
--ws-pre-merge: #3b82f6;
|
||||||
@@ -10270,6 +10271,7 @@ html .column.drag-over * {
|
|||||||
--color-success: #1a7f37;
|
--color-success: #1a7f37;
|
||||||
--color-error: #cf222e;
|
--color-error: #cf222e;
|
||||||
--color-muted: #6e7781;
|
--color-muted: #6e7781;
|
||||||
|
--color-warning: #9a6700;
|
||||||
|
|
||||||
/* Executor status bar state accents (lower intensity for light backgrounds) */
|
/* Executor status bar state accents (lower intensity for light backgrounds) */
|
||||||
--executor-status-error-bg: color-mix(in srgb, var(--color-error) 6%, transparent);
|
--executor-status-error-bg: color-mix(in srgb, var(--color-error) 6%, transparent);
|
||||||
@@ -29974,7 +29976,7 @@ html .column.drag-over * {
|
|||||||
|
|
||||||
.plugin-settings-form .form-group {
|
.plugin-settings-form .form-group {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-top: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-settings-array {
|
.plugin-settings-array {
|
||||||
@@ -30059,6 +30061,46 @@ html .column.drag-over * {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Plugin toggle switch */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch .toggle-slider {
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: background var(--transition-normal);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch .toggle-slider::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider {
|
||||||
|
background: var(--in-review);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Roadmaps view */
|
/* Roadmaps view */
|
||||||
.roadmaps-view {
|
.roadmaps-view {
|
||||||
|
|||||||
Reference in New Issue
Block a user