feat: mobile config profiles with list-detail pattern

- Add mobile list view showing all config profiles
- Add mobile detail view with profile information display
- Add mobile edit/create view with full form
- Implement list→detail→edit navigation
- Fix TypeScript errors and build issues
This commit is contained in:
Alex Blank
2026-05-25 12:27:47 +02:00
parent e8d5b16acc
commit 27c39f9cfc
2 changed files with 280 additions and 10 deletions
+270
View File
@@ -1,5 +1,10 @@
import { useCallback, useEffect, useState } from "react";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { MobileListView } from "../components/mobile-list-view";
import { MobileDetailView } from "../components/mobile-detail-view";
import { MobileEditView } from "../components/mobile-edit-view";
import { MobileFAB } from "../components/mobile-fab";
import {
createConfigProfile,
deleteConfigProfile,
@@ -16,8 +21,11 @@ import type { Project } from "../types";
import { listToolTypes, type ToolType } from "../api/tool_types";
type Status = "loading" | "ready" | "error";
type MobileView = "list" | "detail" | "edit";
export const ConfigProfilesPage = () => {
const isMobile = useMobileViewport();
const [mobileView, setMobileView] = useState<MobileView>("list");
const [status, setStatus] = useState<Status>("loading");
const [profiles, setProfiles] = useState<ConfigProfile[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
@@ -425,6 +433,268 @@ export const ConfigProfilesPage = () => {
);
}
// Mobile view rendering
if (isMobile) {
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) => {
setSelectedProfileId(id);
setIsCreating(false);
const profile = profiles.find((p) => p.id === id);
if (profile) populateForm(profile);
setMobileView("detail");
}}
onItemDelete={(id: string) => handleDelete(id)}
emptyMessage="No config profiles yet"
/>
<MobileFAB onClick={() => {
setIsCreating(true);
setSelectedProfileId(null);
resetForm();
setMobileView("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={() => setMobileView("list")}
onEdit={() => {
populateForm(selectedProfile);
setIsCreating(false);
setMobileView("edit");
}}
onDelete={() => handleDelete(selectedProfile.id)}
/>
);
}
if (mobileView === "edit") {
return (
<MobileEditView
title={isCreating ? "Create Profile" : "Edit Profile"}
onCancel={() => {
if (isCreating) {
setMobileView("list");
} else if (selectedProfile) {
setMobileView("detail");
} else {
setMobileView("list");
}
}}
onSave={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)}
isSaving={saveStatus === "saving"}
>
{/* Profile form fields */}
<div className="form-group">
<label>Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => updateFormField("name", e.target.value)}
placeholder="Profile name"
required
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={formData.description || ""}
onChange={(e) => updateFormField("description", e.target.value)}
placeholder="Optional description"
rows={3}
/>
</div>
<div className="form-group">
<label>Project</label>
<select
value={formData.project_id || ""}
onChange={(e) => updateFormField("project_id", e.target.value || undefined)}
>
<option value="">Global (all projects)</option>
{projects.map((project) => (
<option key={project.id} value={project.id}>{project.name}</option>
))}
</select>
</div>
<div className="form-group">
<label>Tool Type</label>
<select
value={formData.tool_type_id || ""}
onChange={(e) => updateFormField("tool_type_id", e.target.value || undefined)}
>
<option value="">Any tool type</option>
{toolTypes.map((toolType) => (
<option key={toolType.id} value={toolType.id}>{toolType.display_name}</option>
))}
</select>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
checked={formData.is_default || false}
onChange={(e) => updateFormField("is_default", e.target.checked)}
/>
Default Profile
</label>
</div>
{/* Environment Variables */}
<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;
updateFormField("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;
updateFormField("env_vars", newEnvVars);
}}
placeholder="value"
style={{ flex: 1 }}
/>
<button
type="button"
onClick={() => {
const newEnvVars = { ...formData.env_vars };
delete newEnvVars[key];
updateFormField("env_vars", newEnvVars);
}}
className="secondary-button"
>
<Icon name="delete" size="sm" />
</button>
</div>
))}
<button type="button" className="secondary-button" onClick={addEnvVar}>
<Icon name="add" size="sm" /> Add Variable
</button>
</div>
{/* Mounts */}
<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 };
updateFormField("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" };
updateFormField("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);
updateFormField("mounts", newMounts);
}}
className="secondary-button"
>
<Icon name="delete" size="sm" />
</button>
</div>
))}
<button type="button" className="secondary-button" onClick={addMount}>
<Icon name="add" size="sm" /> Add Mount
</button>
</div>
</MobileEditView>
);
}
// Fallback to 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) => {
setSelectedProfileId(id);
setIsCreating(false);
const profile = profiles.find((p) => p.id === id);
if (profile) populateForm(profile);
setMobileView("detail");
}}
onItemDelete={(id: string) => handleDelete(id)}
emptyMessage="No config profiles yet"
/>
<MobileFAB onClick={() => {
setIsCreating(true);
setSelectedProfileId(null);
resetForm();
setMobileView("edit");
}} />
</div>
);
}
return (
<div className="container" style={{ display: "flex", height: "calc(100vh - 4rem)", gap: 0, padding: 0 }}>
{/* Left Sidebar - Profile List */}