Files
headquarter/apps/web/src/components/features/config-profiles/ConfigProfilesMobileView.tsx
T
Developer d7d5baa41a 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
2026-06-05 19:09:09 +00:00

315 lines
9.7 KiB
TypeScript

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>
);
};