feat: Implement view management features with a new UI for editing and listing views
- Added a resizable panel layout in the studio app to display the view list alongside the main application. - Refactored the studio store to include new methods for fetching and managing route views. - Introduced a new DataItemForm component for configuring data items in views. - Created a ViewEditor component for adding and editing views, including data items and queries. - Enhanced the ViewList component to support searching, adding, editing, and deleting views. - Updated UI components (Button, Checkbox, Dialog, Input, Label, Table) for better styling and functionality. - Added environment configuration for API URL. - Introduced a new workspace configuration for pnpm.
This commit is contained in:
200
web/src/apps/view/components/DataItemForm.tsx
Normal file
200
web/src/apps/view/components/DataItemForm.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Query } from "@kevisual/query"
|
||||
import { QueryRouterServer } from "@kevisual/router"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker;
|
||||
type RouteViewBase = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
export type RouterViewApi = {
|
||||
type: 'api',
|
||||
api: {
|
||||
url: string,
|
||||
// 已初始化的query实例,不需要编辑配置
|
||||
query?: Query
|
||||
}
|
||||
} & RouteViewBase;
|
||||
|
||||
export type RouterViewContext = {
|
||||
type: 'context',
|
||||
context: {
|
||||
key: string,
|
||||
// 从context中获取router,不需要编辑配置
|
||||
router?: QueryRouterServer
|
||||
}
|
||||
} & RouteViewBase;
|
||||
export type RouterViewWorker = {
|
||||
type: 'worker',
|
||||
worker: {
|
||||
type: 'Worker' | 'SharedWorker' | 'serviceWorker',
|
||||
url: string,
|
||||
// 已初始化的worker实例,不需要编辑配置
|
||||
worker?: Worker | SharedWorker | ServiceWorker,
|
||||
/**
|
||||
* worker选项
|
||||
* default: { type: 'module' }
|
||||
*/
|
||||
workerOptions?: {
|
||||
type: 'module' | 'classic'
|
||||
}
|
||||
}
|
||||
} & RouteViewBase;
|
||||
interface DataItemFormProps {
|
||||
item: RouterViewItem
|
||||
onChange: (item: any) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) => {
|
||||
const handleChange = (field: string, value: any) => {
|
||||
if (field === 'type') {
|
||||
const newItem: RouterViewItem = { ...item, type: value }
|
||||
if (value === 'api' && !('api' in item)) {
|
||||
(newItem as RouterViewApi).api = { url: '' }
|
||||
} else if (value === 'context' && !('context' in item)) {
|
||||
(newItem as RouterViewContext).context = { key: '' }
|
||||
} else if (value === 'worker' && !('worker' in item)) {
|
||||
(newItem as RouterViewWorker).worker = { type: 'Worker', url: '', workerOptions: { type: 'module' } }
|
||||
}
|
||||
if (!newItem.id) {
|
||||
newItem.id = nanoid(16)
|
||||
}
|
||||
onChange(newItem)
|
||||
} else {
|
||||
onChange({ ...item, [field]: value })
|
||||
}
|
||||
}
|
||||
|
||||
const handleNestedChange = (parent: string, field: string, value: any) => {
|
||||
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
|
||||
const newParentValue: Record<string, any> = {
|
||||
...(parentValue || {}),
|
||||
[field]: value
|
||||
}
|
||||
onChange({ ...item, [parent]: newParentValue })
|
||||
}
|
||||
|
||||
const handleNestedDeepChange = (parent: string, nestedParent: string, field: string, value: any) => {
|
||||
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
|
||||
const nestedValue = parentValue?.[nestedParent] as Record<string, any> | undefined
|
||||
const newNestedValue: Record<string, any> = {
|
||||
...(nestedValue || {}),
|
||||
[field]: value
|
||||
}
|
||||
const newParentValue: Record<string, any> = {
|
||||
...(parentValue || {}),
|
||||
[nestedParent]: newNestedValue
|
||||
}
|
||||
onChange({ ...item, [parent]: newParentValue })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-4 mb-4 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-medium">数据项配置</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-sm text-red-500 hover:text-red-700"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>标题</Label>
|
||||
<Input
|
||||
value={item.title || ''}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
placeholder="输入标题"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>类型</Label>
|
||||
<select
|
||||
value={item.type}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="api">API</option>
|
||||
<option value="context">Context</option>
|
||||
<option value="worker">Worker</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="enabled"
|
||||
checked={item.enabled !== false}
|
||||
onCheckedChange={(checked) => handleChange('enabled', checked)}
|
||||
/>
|
||||
<Label htmlFor="enabled" className="cursor-pointer">启用</Label>
|
||||
</div>
|
||||
|
||||
{(item.type === 'api') && (
|
||||
<div className="space-y-2">
|
||||
<Label>API URL</Label>
|
||||
<Input
|
||||
value={item.api?.url || ''}
|
||||
onChange={(e) => handleNestedChange('api', 'url', e.target.value)}
|
||||
placeholder="输入 API 地址"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'context' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Context Key</Label>
|
||||
<Input
|
||||
value={item.context?.key || ''}
|
||||
onChange={(e) => handleNestedChange('context', 'key', e.target.value)}
|
||||
placeholder="输入 Context Key"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'worker' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Worker Type</Label>
|
||||
<select
|
||||
value={item.worker?.type || 'Worker'}
|
||||
onChange={(e) => handleNestedChange('worker', 'type', e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="Worker">Worker</option>
|
||||
<option value="SharedWorker">SharedWorker</option>
|
||||
<option value="serviceWorker">ServiceWorker</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Worker URL</Label>
|
||||
<Input
|
||||
value={item.worker?.url || ''}
|
||||
onChange={(e) => handleNestedChange('worker', 'url', e.target.value)}
|
||||
placeholder="输入 Worker URL"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Worker Options Type</Label>
|
||||
<select
|
||||
value={item.worker?.workerOptions?.type || 'module'}
|
||||
onChange={(e) => handleNestedDeepChange('worker', 'workerOptions', 'type', e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="module">Module</option>
|
||||
<option value="classic">Classic</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user