feat: mobile tool workshop with list-detail pattern
- Add mobile viewport detection to ToolWorkshopPage - Implement mobile list view with MobileListView component - Implement mobile detail view with MobileDetailView component - Implement mobile edit view with MobileEditView component - Add MobileFAB for creating new tool types - Fix IconName type issues in mobile components - TypeScript check passes, build succeeds
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 {
|
||||
createToolType,
|
||||
deleteToolType,
|
||||
@@ -31,7 +36,11 @@ import {
|
||||
type RightPanelTab = "details" | "configs" | "folders";
|
||||
type Status = "loading" | "ready" | "error";
|
||||
|
||||
type MobileView = "list" | "detail" | "edit";
|
||||
|
||||
export const ToolWorkshopPage = () => {
|
||||
const isMobile = useMobileViewport();
|
||||
const [mobileView, setMobileView] = useState<MobileView>("list");
|
||||
const [status, setStatus] = useState<Status>("loading");
|
||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||
const [configs, setConfigs] = useState<ToolConfig[]>([]);
|
||||
@@ -507,6 +516,259 @@ export const ToolWorkshopPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
if (mobileView === "list") {
|
||||
return (
|
||||
<div className="mobile-page">
|
||||
<div className="mobile-page-header">
|
||||
<h1>Tool Workshop</h1>
|
||||
<span className="muted">{toolTypes.length} tool types</span>
|
||||
</div>
|
||||
<MobileListView
|
||||
items={toolTypes.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.display_name,
|
||||
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||
}))}
|
||||
onItemClick={(id) => {
|
||||
setSelectedToolTypeId(id);
|
||||
setIsCreating(false);
|
||||
setMobileView("detail");
|
||||
}}
|
||||
emptyMessage="No tool types yet"
|
||||
/>
|
||||
<MobileFAB onClick={() => {
|
||||
setSelectedToolTypeId(null);
|
||||
setIsCreating(true);
|
||||
resetToolTypeForm();
|
||||
setMobileView("edit");
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "detail" && selectedToolType) {
|
||||
return (
|
||||
<MobileDetailView
|
||||
title={selectedToolType.display_name}
|
||||
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
|
||||
fields={[
|
||||
{ label: "Name", value: selectedToolType.name },
|
||||
{ label: "Display Name", value: selectedToolType.display_name },
|
||||
{ label: "Description", value: selectedToolType.description },
|
||||
{ label: "Category", value: selectedToolType.category },
|
||||
{ label: "Interface Type", value: selectedToolType.interface_type },
|
||||
{ label: "Requires Port", value: selectedToolType.requires_port, type: "boolean" },
|
||||
{ label: "Default Port", value: selectedToolType.default_port },
|
||||
{ label: "Definition Type", value: selectedToolType.definition_type },
|
||||
{ label: "Startup Command", value: selectedToolType.startup_command },
|
||||
{ label: "Readiness Command", value: selectedToolType.readiness_probe?.command ?? null },
|
||||
{ label: "Readiness Timeout", value: selectedToolType.readiness_probe?.timeout ?? null },
|
||||
{ label: "Readiness Interval", value: selectedToolType.readiness_probe?.interval ?? null },
|
||||
{ label: "Required Variables", value: selectedToolType.required_variables?.join(", ") ?? null },
|
||||
{ label: "Compose Template", value: selectedToolType.compose_template, type: "code" },
|
||||
{ label: "Dockerfile Template", value: selectedToolType.dockerfile_template, type: "code" },
|
||||
]}
|
||||
onEdit={() => {
|
||||
populateToolTypeForm(selectedToolType);
|
||||
setIsCreating(false);
|
||||
setMobileView("edit");
|
||||
}}
|
||||
onDelete={() => {
|
||||
handleDeleteToolType(selectedToolType.id);
|
||||
setMobileView("list");
|
||||
}}
|
||||
onBack={() => {
|
||||
setSelectedToolTypeId(null);
|
||||
setMobileView("list");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileView === "edit") {
|
||||
return (
|
||||
<MobileEditView
|
||||
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
||||
onCancel={() => {
|
||||
if (toolTypeDirty) {
|
||||
if (!window.confirm("You have unsaved changes. Discard them?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setMobileView(isCreating ? "list" : "detail");
|
||||
}}
|
||||
onSave={() => {
|
||||
// Create a synthetic form event to call handleToolTypeSubmit
|
||||
const syntheticEvent = { preventDefault: () => {} } as React.FormEvent;
|
||||
void handleToolTypeSubmit(syntheticEvent);
|
||||
if (!toolTypeError) {
|
||||
setMobileView("list");
|
||||
}
|
||||
}}
|
||||
isSaving={false}
|
||||
>
|
||||
{/* Tool Type Form Fields */}
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.name}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, name: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., my-tool"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Display Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.display_name}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, display_name: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., My Tool"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Description</label>
|
||||
<textarea
|
||||
value={toolTypeForm.description}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, description: e.target.value })}
|
||||
className="mobile-form-textarea"
|
||||
placeholder="What does this tool do?"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.category}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, category: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., development"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Interface Type</label>
|
||||
<select
|
||||
value={toolTypeForm.interface_type}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, interface_type: e.target.value as "web" | "terminal" })}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="web">Web</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Requires Port</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toolTypeForm.requires_port}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, requires_port: e.target.checked })}
|
||||
className="mobile-form-checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Default Port</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.default_port}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, default_port: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., 8080"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Definition Type</label>
|
||||
<select
|
||||
value={toolTypeForm.definition_type}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, definition_type: e.target.value as "compose" | "dockerfile" })}
|
||||
className="mobile-form-select"
|
||||
>
|
||||
<option value="compose">Compose</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Startup Command</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.startup_command}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, startup_command: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="Command to run on startup"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Command</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_command}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, readiness_command: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="e.g., curl -f http://localhost:8080/health"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Timeout</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_timeout}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, readiness_timeout: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="30"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Readiness Interval</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.readiness_interval}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, readiness_interval: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Required Variables</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolTypeForm.required_variables}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, required_variables: e.target.value })}
|
||||
className="mobile-form-input"
|
||||
placeholder="VAR1, VAR2, VAR3"
|
||||
/>
|
||||
</div>
|
||||
{toolTypeForm.definition_type === "compose" && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Compose Template</label>
|
||||
<textarea
|
||||
value={toolTypeForm.compose_template}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, compose_template: e.target.value })}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder="version: '3'"
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{toolTypeForm.definition_type === "dockerfile" && (
|
||||
<div className="mobile-form-group">
|
||||
<label className="mobile-form-label">Dockerfile Template</label>
|
||||
<textarea
|
||||
value={toolTypeForm.dockerfile_template}
|
||||
onChange={(e) => setToolTypeForm({ ...toolTypeForm, dockerfile_template: e.target.value })}
|
||||
className="mobile-form-textarea mobile-form-code"
|
||||
placeholder="FROM ubuntu:22.04"
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</MobileEditView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container" style={{ display: "flex", height: "calc(100vh - 4rem)", gap: 0, padding: 0 }}>
|
||||
{/* Left Sidebar - Tool List */}
|
||||
|
||||
Reference in New Issue
Block a user