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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user