From d7d5baa41aa9e9310a47329fd1542770f7328771 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 5 Jun 2026 19:09:09 +0000 Subject: [PATCH] refactor: extract ConfigProfilesPage components - Extract use-config-profiles hook for state management - Extract ConfigProfileListSidebar, ConfigProfileEditorPanel, ConfigProfilesMobileView - Slim ConfigProfilesPage from 1,611 to 170 lines Quality gates: tsc --noEmit passes, npm run build passes --- .../ConfigProfileEditorPanel.tsx | 305 +++ .../ConfigProfileListSidebar.tsx | 174 ++ .../ConfigProfilesMobileView.tsx | 314 +++ apps/web/src/hooks/use-config-profiles.ts | 431 +++++ apps/web/src/pages/ConfigProfilesPage.tsx | 1707 ++--------------- 5 files changed, 1357 insertions(+), 1574 deletions(-) create mode 100644 apps/web/src/components/features/config-profiles/ConfigProfileEditorPanel.tsx create mode 100644 apps/web/src/components/features/config-profiles/ConfigProfileListSidebar.tsx create mode 100644 apps/web/src/components/features/config-profiles/ConfigProfilesMobileView.tsx create mode 100644 apps/web/src/hooks/use-config-profiles.ts diff --git a/apps/web/src/components/features/config-profiles/ConfigProfileEditorPanel.tsx b/apps/web/src/components/features/config-profiles/ConfigProfileEditorPanel.tsx new file mode 100644 index 0000000..16fd544 --- /dev/null +++ b/apps/web/src/components/features/config-profiles/ConfigProfileEditorPanel.tsx @@ -0,0 +1,305 @@ +import { Icon } from "../../icon"; +import { GitMountEditor } from "../git/git-mount-editor"; +import type { ConfigProfile, CreateConfigProfileRequest, ResolvedProfile } from "../../../api/config-profiles"; +import type { ProjectWithRepos } from "../../../types"; +import type { ToolType } from "../../../api/tool-types"; + +interface Props { + isCreating: boolean; + selectedProfile: ConfigProfile | null; + formData: CreateConfigProfileRequest; + includedProfileIds: string[]; + dragOverIndex: number | null; + error: string | null; + saveStatus: "idle" | "saving" | "saved" | "error"; + previewData: ResolvedProfile | null; + previewingId: string | null; + projects: ProjectWithRepos[]; + toolTypes: ToolType[]; + availableProfiles: ConfigProfile[]; + getIncludedProfile: (id: string) => ConfigProfile | undefined; + getScopeLabel: (profile: ConfigProfile) => string; + onFormChange: (key: K, value: CreateConfigProfileRequest[K]) => void; + onSubmit: (e?: React.FormEvent) => void; + onReset: () => void; + onPreview: () => void; + onAddInclude: (id: string) => void; + onRemoveInclude: (index: number) => void; + onDragStart: (e: React.DragEvent, index: number) => void; + onDragOver: (e: React.DragEvent, index: number) => void; + onDragLeave: () => void; + onDrop: (e: React.DragEvent, index: number) => void; + onAddEnvVar: () => void; + onUpdateEnvVar: (oldKey: string, newKey: string, value: string) => void; + onRemoveEnvVar: (key: string) => void; + onAddFile: () => void; + onUpdateFile: (oldPath: string, newPath: string, content: string) => void; + onRemoveFile: (path: string) => void; + onAddMount: () => void; + onUpdateMount: (index: number, updates: Partial) => void; + onRemoveMount: (index: number) => void; + onAddMountFile: (mountIndex: number) => void; + onUpdateMountFile: (mountIndex: number, oldPath: string, newPath: string, content: string) => void; + onRemoveMountFile: (mountIndex: number, path: string) => void; + onClosePreview: () => void; +} + +export const ConfigProfileEditorPanel = ({ + isCreating, + selectedProfile, + formData, + includedProfileIds, + dragOverIndex, + error, + saveStatus, + previewData, + previewingId, + projects, + toolTypes, + availableProfiles, + getIncludedProfile, + getScopeLabel, + onFormChange, + onSubmit, + onReset, + onPreview, + onAddInclude, + onRemoveInclude, + onDragStart, + onDragOver, + onDragLeave, + onDrop, + onAddEnvVar, + onUpdateEnvVar, + onRemoveEnvVar, + onAddFile, + onUpdateFile, + onRemoveFile, + onAddMount, + onUpdateMount, + onRemoveMount, + onAddMountFile, + onUpdateMountFile, + onRemoveMountFile, + onClosePreview, +}: Props) => { + const hasSelection = isCreating || selectedProfile; + + return ( +
+ {!hasSelection ? ( +
+
+ +
+

Select a config profile

+

Choose a profile from the list to edit, or create a new one.

+
+ ) : ( +
+
+
+

+ {isCreating ? "Create Profile" : selectedProfile?.name} +

+ {!isCreating && selectedProfile && ( +

+ {selectedProfile.project_id && + `Project: ${projects.find((p) => p.id === selectedProfile.project_id)?.name || selectedProfile.project_id}`} + {selectedProfile.project_id && selectedProfile.tool_type_id && " · "} + {selectedProfile.tool_type_id && + `Tool: ${toolTypes.find((t) => t.id === selectedProfile.tool_type_id)?.display_name || selectedProfile.tool_type_id}`} +

+ )} +
+ {!isCreating && selectedProfile && ( +
+ +
+ )} +
+ + {error &&
{error}
} + + {saveStatus === "saved" && ( +
+ + Profile saved successfully +
+ )} + +
+
+ + onFormChange("name", e.target.value)} placeholder="e.g., Development Environment" className="form-input" required /> +
+ +
+ + onFormChange("description", e.target.value || undefined)} placeholder="Optional description" className="form-input" /> +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+

Includes

+ {includedProfileIds.length} included +
+ {includedProfileIds.length === 0 ? ( +

No profiles included. Add profiles to compose configurations.

+ ) : ( +
+ {includedProfileIds.map((profileId, index) => { + const profile = getIncludedProfile(profileId); + if (!profile) return null; + return ( +
onDragStart(e, index)} + onDragOver={(e) => onDragOver(e, index)} + onDragLeave={onDragLeave} + onDrop={(e) => onDrop(e, index)} + style={{ display: "flex", alignItems: "center", gap: "0.5rem", padding: "0.5rem 0.75rem", background: dragOverIndex === index ? "var(--brand-bg, #e0e7ff)" : "var(--panel)", border: "1px solid var(--border)", borderRadius: "0.375rem", marginBottom: "0.25rem", cursor: "grab", transition: "background 0.15s" }} + > + + {profile.name} + {getScopeLabel(profile)} + +
+ ); + })} +
+ )} + {availableProfiles.length > 0 && ( +
+ +
+ )} +
+ +
+

Environment Variables

+ {Object.entries(formData.env_vars || {}).map(([key, value], idx) => ( +
+ onUpdateEnvVar(key, e.target.value, value)} placeholder="VAR_NAME" className="form-input" style={{ flex: 1 }} /> + onUpdateEnvVar(key, key, e.target.value)} placeholder="value" className="form-input" style={{ flex: 1 }} /> + +
+ ))} + +
+ +
+

Runtime Hints

+