FN-6034: fix mobile workflow canvas width and project controls
Keep workflow editing usable on mobile while preserving related dashboard controls. - allow the standalone project selector to expose a Manage Projects action in single-project layouts - improve the mailbox resize handle contrast for better visibility - make the workflow editor canvas and React Flow surface shrink and fill correctly without mobile horizontal overflow - add CSS contract coverage for desktop and mobile workflow editor width behavior Files changed: packages/dashboard/app/components/Header.tsx | 4 +++- packages/dashboard/app/components/MailboxModal.css | 2 +- .../dashboard/app/components/ProjectSelector.tsx | 15 +++-- .../app/components/WorkflowNodeEditor.css | 28 +++++++++ .../__tests__/WorkflowNodeEditor.css.test.ts | 66 ++++++++++++++++++++++ 5 files changed, 109 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-6034 Fusion-Task-Lineage: e7a784ff-188a-4538-a2be-f75484f10aa4
This commit is contained in:
@@ -836,12 +836,14 @@ export function Header({
|
||||
)}
|
||||
|
||||
{/* Project Selector - Back button when project selected, dropdown when 2+ projects (tablet + desktop) */}
|
||||
{!isMobile && projects.length >= 1 && onViewAllProjects && onSelectProject && (
|
||||
{!isMobile && projects.length >= 1 && onViewAllProjects && (
|
||||
<StandaloneProjectSelector
|
||||
projects={projects}
|
||||
currentProject={currentProject ?? null}
|
||||
onViewAll={onViewAllProjects}
|
||||
onSelect={onSelectProject}
|
||||
allowSingleProject
|
||||
viewAllLabel={t("header.manageProjects", "Manage Projects")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -673,7 +673,7 @@
|
||||
width: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
background: color-mix(in srgb, var(--border) 70%, transparent);
|
||||
touch-action: none;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface ProjectSelectorProps {
|
||||
onSelect?: (project: ProjectInfo) => void;
|
||||
onViewAll: () => void;
|
||||
recentProjectIds?: string[];
|
||||
allowSingleProject?: boolean;
|
||||
viewAllLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +79,8 @@ export function ProjectSelector({
|
||||
onSelect,
|
||||
onViewAll,
|
||||
recentProjectIds = [],
|
||||
allowSingleProject = false,
|
||||
viewAllLabel,
|
||||
}: ProjectSelectorProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -369,8 +373,9 @@ export function ProjectSelector({
|
||||
);
|
||||
};
|
||||
|
||||
// Don't render if only one project (single-project mode)
|
||||
if (projects.length <= 1) {
|
||||
// Standalone project switching stays hidden in single/no-project contexts unless
|
||||
// a host uses the trigger as an explicit Manage Projects affordance.
|
||||
if (!allowSingleProject && projects.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -384,11 +389,12 @@ export function ProjectSelector({
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
aria-label={t("projectSelector.ariaLabel", "Select project")}
|
||||
title={currentProject?.name ? t("projectSelector.switchProjectTitle", "Switch project (current: {{name}})", { name: currentProject.name }) : t("projectSelector.projectsTitle", "Projects")}
|
||||
data-testid="project-selector-trigger"
|
||||
>
|
||||
<Folder size={16} className="project-selector__trigger-icon" />
|
||||
<span className="project-selector__trigger-text">
|
||||
{currentProject?.name || t("projectSelector.selectProject", "Select Project")}
|
||||
{currentProject?.name || t("projectSelector.projects", "Projects")}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
@@ -583,9 +589,10 @@ export function ProjectSelector({
|
||||
highlightedIndex === totalItems - 1 ? "highlighted" : ""
|
||||
}`}
|
||||
onClick={handleViewAll}
|
||||
data-testid="manage-projects-action"
|
||||
>
|
||||
<Grid3X3 size={14} />
|
||||
<span>{t("projectSelector.viewAll", "View All Projects")}</span>
|
||||
<span>{viewAllLabel ?? t("projectSelector.viewAll", "View All Projects")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
.wf-editor-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -537,7 +538,10 @@
|
||||
|
||||
.wf-editor-canvas {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1230,6 +1234,9 @@
|
||||
|
||||
.wf-editor-body {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.wf-editor-body--list-stage .wf-editor-sidebar {
|
||||
@@ -1254,7 +1261,10 @@
|
||||
.wf-editor-body--editor-stage .wf-editor-canvas-wrap {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wf-editor-body--editor-stage .wf-editor-canvas {
|
||||
@@ -1305,9 +1315,27 @@
|
||||
|
||||
.wf-editor-canvas-wrap {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wf-editor-canvas {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wf-editor-canvas .react-flow,
|
||||
.wf-editor-canvas .react-flow__renderer,
|
||||
.wf-editor-canvas .react-flow__pane,
|
||||
.wf-editor-canvas .react-flow__viewport {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.wf-editor-list-item,
|
||||
.wf-editor-new,
|
||||
.wf-editor-import,
|
||||
|
||||
@@ -103,6 +103,72 @@ describe("WorkflowNodeEditor mobile CSS contract", () => {
|
||||
expect(collapsedToggleRule).toMatch(/bottom\s*:\s*var\(--space-sm\)\s*;/);
|
||||
});
|
||||
|
||||
it("FN-6034 keeps the desktop graph canvas shrinkable without removing the modal minimum", () => {
|
||||
const baseCss = loadAllAppCssBaseOnly();
|
||||
|
||||
const desktopModalRule = findRule([baseCss], /\.wf-editor-modal\s*\{[^}]*\}/);
|
||||
expect(desktopModalRule).toMatch(/min-width\s*:\s*640px\s*;/);
|
||||
|
||||
const bodyRule = findRule([baseCss], /\.wf-editor-body\s*\{[^}]*\}/);
|
||||
expect(bodyRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
|
||||
const canvasWrapRule = findRule([baseCss], /\.wf-editor-canvas-wrap\s*\{[^}]*\}/);
|
||||
expect(canvasWrapRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
|
||||
const canvasRule = findRule([baseCss], /\.wf-editor-canvas\s*\{[^}]*\}/);
|
||||
expect(canvasRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(canvasRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||
expect(canvasRule).toMatch(/overflow\s*:\s*hidden\s*;/);
|
||||
});
|
||||
|
||||
it("FN-6034 makes the mobile React Flow surface fill the editor stage without horizontal overflow", () => {
|
||||
const editorCss = readComponentCss("WorkflowNodeEditor.css");
|
||||
const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)");
|
||||
|
||||
const editorBodyRule = findRule(mobileBlocks, /\.wf-editor-body\s*\{[^}]*\}/);
|
||||
expect(editorBodyRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||
expect(editorBodyRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(editorBodyRule).toMatch(/overflow-x\s*:\s*hidden\s*;/);
|
||||
|
||||
const editorStageWrapRule = findRule(mobileBlocks, /\.wf-editor-body--editor-stage \.wf-editor-canvas-wrap\s*\{[^}]*\}/);
|
||||
expect(editorStageWrapRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||
expect(editorStageWrapRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(editorStageWrapRule).toMatch(/overflow\s*:\s*hidden\s*;/);
|
||||
|
||||
const canvasRule = findRule(mobileBlocks, /\.wf-editor-canvas\s*\{[^}]*\}/);
|
||||
expect(canvasRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||
expect(canvasRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(canvasRule).toMatch(/max-width\s*:\s*100%\s*;/);
|
||||
expect(canvasRule).toMatch(/overflow\s*:\s*hidden\s*;/);
|
||||
|
||||
const reactFlowSurfaceRule = findRule(
|
||||
mobileBlocks,
|
||||
/\.wf-editor-canvas \.react-flow,\s*\.wf-editor-canvas \.react-flow__renderer,\s*\.wf-editor-canvas \.react-flow__pane,\s*\.wf-editor-canvas \.react-flow__viewport\s*\{[^}]*\}/,
|
||||
);
|
||||
expect(reactFlowSurfaceRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||
expect(reactFlowSurfaceRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(reactFlowSurfaceRule).toMatch(/max-width\s*:\s*100%\s*;/);
|
||||
});
|
||||
|
||||
it("FN-6034 preserves mobile staged editor visibility and inspector stacking", () => {
|
||||
const editorCss = readComponentCss("WorkflowNodeEditor.css");
|
||||
const mobileBlocks = extractMediaBlocks(editorCss, "(max-width: 768px)");
|
||||
|
||||
const listStageHiddenRule = findRule(
|
||||
mobileBlocks,
|
||||
/\.wf-editor-body--list-stage \.wf-editor-canvas-wrap,\s*\.wf-editor-body--list-stage \.wf-editor-inspector\s*\{[^}]*\}/,
|
||||
);
|
||||
expect(listStageHiddenRule).toMatch(/display\s*:\s*none\s*;/);
|
||||
|
||||
const editorStageSidebarRule = findRule(mobileBlocks, /\.wf-editor-body--editor-stage \.wf-editor-sidebar\s*\{[^}]*\}/);
|
||||
expect(editorStageSidebarRule).toMatch(/display\s*:\s*none\s*;/);
|
||||
|
||||
const inspectorRule = findRule(mobileBlocks, /\.wf-editor-body--editor-stage \.wf-editor-inspector\s*\{[^}]*\}/);
|
||||
expect(inspectorRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||
expect(inspectorRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||
expect(inspectorRule).toMatch(/border-top\s*:\s*1px solid var\(--border\)\s*;/);
|
||||
});
|
||||
|
||||
it("FN-6033 keeps workflow editor touch target increases mobile-scoped", () => {
|
||||
const baseCss = loadAllAppCssBaseOnly();
|
||||
const editorCss = readComponentCss("WorkflowNodeEditor.css");
|
||||
|
||||
Reference in New Issue
Block a user