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
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import { MobileListView } from "../mobile/mobile-list-view";
|
||||
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
||||
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||
import { MobileFAB } from "../mobile/mobile-fab";
|
||||
import { Icon } from "../../icon";
|
||||
import type { ConfigProfile, CreateConfigProfileRequest } from "../../../api/config-profiles";
|
||||
|
||||
type MobileView = "list" | "detail" | "edit";
|
||||
|
||||
interface Props {
|
||||
profiles: ConfigProfile[];
|
||||
selectedProfile: ConfigProfile | null;
|
||||
mobileView: MobileView;
|
||||
isCreating: boolean;
|
||||
formData: CreateConfigProfileRequest;
|
||||
saveStatus: "idle" | "saving" | "saved" | "error";
|
||||
onViewChange: (view: MobileView) => void;
|
||||
onSelect: (profile: ConfigProfile) => void;
|
||||
onCreate: () => void;
|
||||
onDelete: (id: string) => void;
|
||||
onFormChange: <K extends keyof CreateConfigProfileRequest>(key: K, value: CreateConfigProfileRequest[K]) => void;
|
||||
onSubmit: () => void;
|
||||
getScopeLabel: (profile: ConfigProfile) => string;
|
||||
}
|
||||
|
||||
export const ConfigProfilesMobileView = ({
|
||||
profiles,
|
||||
selectedProfile,
|
||||
mobileView,
|
||||
isCreating,
|
||||
formData,
|
||||
saveStatus,
|
||||
onViewChange,
|
||||
onSelect,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onFormChange,
|
||||
onSubmit,
|
||||
getScopeLabel,
|
||||
}: Props) => {
|
||||
if (mobileView === "list") {
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Config Profiles</h1>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={profiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
title: profile.name,
|
||||
subtitle: profile.description || getScopeLabel(profile),
|
||||
}))}
|
||||
onItemClick={(id: string) => {
|
||||
const profile = profiles.find((p) => p.id === id);
|
||||
if (profile) {
|
||||
onSelect(profile);
|
||||
onViewChange("detail");
|
||||
}
|
||||
}}
|
||||
onItemDelete={(id: string) => onDelete(id)}
|
||||
emptyMessage="No config profiles yet"
|
||||
/>
|
||||
<MobileFAB
|
||||
onClick={() => {
|
||||
onCreate();
|
||||
onViewChange("edit");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "detail" && selectedProfile) {
|
||||
const fields = [
|
||||
{ label: "Name", value: selectedProfile.name },
|
||||
{ label: "Description", value: selectedProfile.description || "-" },
|
||||
{ label: "Scope", value: getScopeLabel(selectedProfile) },
|
||||
{ label: "Default", value: selectedProfile.is_default ? "Yes" : "No" },
|
||||
{
|
||||
label: "Environment Variables",
|
||||
value:
|
||||
Object.keys(selectedProfile.env_vars).length > 0
|
||||
? Object.entries(selectedProfile.env_vars)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(", ")
|
||||
: "-",
|
||||
},
|
||||
{
|
||||
label: "Mounts",
|
||||
value:
|
||||
selectedProfile.mounts.length > 0
|
||||
? selectedProfile.mounts.map((m) => `${m.target} (${m.mode})`).join(", ")
|
||||
: "-",
|
||||
},
|
||||
{
|
||||
label: "Includes",
|
||||
value:
|
||||
selectedProfile.includes.length > 0
|
||||
? `${selectedProfile.includes.length} profile(s)`
|
||||
: "-",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<MobileDetailView
|
||||
title={selectedProfile.name}
|
||||
subtitle={getScopeLabel(selectedProfile)}
|
||||
fields={fields}
|
||||
onBack={() => onViewChange("list")}
|
||||
onEdit={() => {
|
||||
onSelect(selectedProfile);
|
||||
onViewChange("edit");
|
||||
}}
|
||||
onDelete={() => onDelete(selectedProfile.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "edit") {
|
||||
return (
|
||||
<MobileEditView
|
||||
title={isCreating ? "Create Profile" : "Edit Profile"}
|
||||
onCancel={() => {
|
||||
onViewChange(isCreating ? "list" : "detail");
|
||||
}}
|
||||
onSave={onSubmit}
|
||||
isSaving={saveStatus === "saving"}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => onFormChange("name", e.target.value)}
|
||||
placeholder="Profile name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => onFormChange("description", e.target.value || undefined)}
|
||||
placeholder="Optional description"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Project</label>
|
||||
<select
|
||||
value={formData.project_id || ""}
|
||||
onChange={(e) => onFormChange("project_id", e.target.value || undefined)}
|
||||
>
|
||||
<option value="">Global (all projects)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Tool Type</label>
|
||||
<select
|
||||
value={formData.tool_type_id || ""}
|
||||
onChange={(e) => onFormChange("tool_type_id", e.target.value || undefined)}
|
||||
>
|
||||
<option value="">Any tool type</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_default || false}
|
||||
onChange={(e) => onFormChange("is_default", e.target.checked)}
|
||||
/>
|
||||
Default Profile
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Environment Variables</label>
|
||||
{Object.entries(formData.env_vars || {}).map(([key, value], index) => (
|
||||
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newEnvVars = { ...formData.env_vars };
|
||||
delete newEnvVars[key];
|
||||
newEnvVars[e.target.value] = value;
|
||||
onFormChange("env_vars", newEnvVars);
|
||||
}}
|
||||
placeholder="KEY"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const newEnvVars = { ...formData.env_vars };
|
||||
newEnvVars[key] = e.target.value;
|
||||
onFormChange("env_vars", newEnvVars);
|
||||
}}
|
||||
placeholder="value"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newEnvVars = { ...formData.env_vars };
|
||||
delete newEnvVars[key];
|
||||
onFormChange("env_vars", newEnvVars);
|
||||
}}
|
||||
className="secondary-button"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
onFormChange("env_vars", { ...formData.env_vars, "": "" });
|
||||
}}
|
||||
>
|
||||
<Icon name="add" size="sm" /> Add Variable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Mounts</label>
|
||||
{(formData.mounts || []).map((mount, index) => (
|
||||
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={mount.target}
|
||||
onChange={(e) => {
|
||||
const newMounts = [...(formData.mounts || [])];
|
||||
newMounts[index] = { ...mount, target: e.target.value };
|
||||
onFormChange("mounts", newMounts);
|
||||
}}
|
||||
placeholder="Target path"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<select
|
||||
value={mount.mode}
|
||||
onChange={(e) => {
|
||||
const newMounts = [...(formData.mounts || [])];
|
||||
newMounts[index] = { ...mount, mode: e.target.value as "ro" | "rw" };
|
||||
onFormChange("mounts", newMounts);
|
||||
}}
|
||||
style={{ width: "80px" }}
|
||||
>
|
||||
<option value="ro">Read</option>
|
||||
<option value="rw">Write</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newMounts = (formData.mounts || []).filter((_, i) => i !== index);
|
||||
onFormChange("mounts", newMounts);
|
||||
}}
|
||||
className="secondary-button"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
onFormChange("mounts", [...(formData.mounts || []), { target: "/", mode: "rw", files: {} }]);
|
||||
}}
|
||||
>
|
||||
<Icon name="add" size="sm" /> Add Mount
|
||||
</button>
|
||||
</div>
|
||||
</MobileEditView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Config Profiles</h1>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={profiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
title: profile.name,
|
||||
subtitle: profile.description || getScopeLabel(profile),
|
||||
}))}
|
||||
onItemClick={(id: string) => {
|
||||
const profile = profiles.find((p) => p.id === id);
|
||||
if (profile) {
|
||||
onSelect(profile);
|
||||
onViewChange("detail");
|
||||
}
|
||||
}}
|
||||
onItemDelete={(id: string) => onDelete(id)}
|
||||
emptyMessage="No config profiles yet"
|
||||
/>
|
||||
<MobileFAB
|
||||
onClick={() => {
|
||||
onCreate();
|
||||
onViewChange("edit");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user