d7d5baa41a
- 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
315 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
};
|