// View/Schema Designer page (US-033) // Visual schema designer for creating custom views import { useState, useEffect, useCallback, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom' import apiClient from '@/api/client' import { useWorkspace } from '@/contexts/WorkspaceContext' import { useRealtimeDocuments } from '@/hooks/useRealtimeDocuments' import Button from '@/components/Button' import Input from '@/components/Input' import ViewPermissionsEditor from '@/components/ViewPermissionsEditor' // Field type definitions matching backend const FIELD_TYPES = [ { value: 'text', label: 'Text', icon: 'T' }, { value: 'number', label: 'Number', icon: '#' }, { value: 'email', label: 'Email', icon: '@' }, { value: 'url', label: 'URL', icon: '🔗' }, { value: 'date', label: 'Date', icon: '📅' }, { value: 'datetime', label: 'Date & Time', icon: '🕐' }, { value: 'boolean', label: 'Boolean', icon: '✓' }, { value: 'select', label: 'Select', icon: '▼' }, { value: 'reference', label: 'Reference', icon: '→' }, { value: 'color', label: 'Color', icon: '🎨' }, ] as const // Widget types per field type - only show relevant widgets const WIDGET_TYPES_BY_FIELD: Record> = { text: [ { value: '', label: 'Auto (Single line)' }, { value: 'textarea', label: 'Text Area (Multi-line)' }, ], number: [ { value: '', label: 'Auto (Number input)' }, { value: 'slider', label: 'Slider' }, ], email: [ { value: '', label: 'Auto (Email input)' }, ], url: [ { value: '', label: 'Auto (URL input)' }, ], date: [ { value: '', label: 'Auto (Date picker)' }, ], datetime: [ { value: '', label: 'Auto (Date & Time picker)' }, ], boolean: [ { value: '', label: 'Auto (Checkbox)' }, { value: 'switch', label: 'Toggle Switch' }, ], select: [ { value: '', label: 'Auto (Dropdown)' }, { value: 'radio', label: 'Radio Buttons' }, ], reference: [ { value: '', label: 'Auto (Dropdown)' }, { value: 'autocomplete', label: 'Autocomplete' }, ], color: [ { value: '', label: 'Auto (Color picker)' }, ], } // Get widgets for a field type const getWidgetsForType = (fieldType: string) => { return WIDGET_TYPES_BY_FIELD[fieldType] || [{ value: '', label: 'Auto' }] } // Convert camelCase or PascalCase to Title Case // e.g., kisCica -> Kis Cica, thisIsAnExample -> This Is An Example const camelToTitleCase = (str: string): string => { if (!str) return '' // Insert space before uppercase letters, then capitalize first letter of each word return str .replace(/([A-Z])/g, ' $1') // Add space before uppercase .replace(/^./, (c) => c.toUpperCase()) // Capitalize first letter .trim() } // Built-in system fields that can be displayed in views const SYSTEM_FIELDS = [ { name: '_id', label: 'Document ID', type: 'text', description: 'Unique identifier' }, { name: '_created_at', label: 'Created At', type: 'datetime', description: 'Creation timestamp' }, { name: '_updated_at', label: 'Updated At', type: 'datetime', description: 'Last update timestamp' }, { name: '_created_by_name', label: 'Created By', type: 'text', description: 'Name of user who created' }, { name: '_updated_by_name', label: 'Updated By', type: 'text', description: 'Name of user who last updated' }, ] as const interface SchemaField { _id: string // Client-side ID for React keys name: string type: string label: string description: string required: boolean display_order: number widget: string group: string default_value?: unknown options?: string[] reference_collection?: string markdown?: boolean // Enable markdown editing/rendering for textarea fields // List view display options hidden?: boolean // Hide field from list/table views (still visible in edit/create forms) maxLength?: number // Maximum characters to display in list views (truncate with ellipsis) // Detail view display options hidden_in_detail?: boolean // Hide field from single item detail view detail_widget?: string // Widget to use in detail view (defaults to same as edit widget) } interface ViewSchema { title?: string description?: string layout?: unknown fields?: Omit[] // Full view detail has fields field_count?: number // List view has field_count } interface View { id: string name: string collection_name: string workspace_id: string schema: ViewSchema settings?: Record created_at: string updated_at: string } interface Collection { name: string schema?: Record is_system: boolean } // Field editor component function FieldEditor({ field, onUpdate, onDelete, onMoveUp, onMoveDown, isFirst, isLast, collections, }: { field: SchemaField onUpdate: (field: SchemaField) => void onDelete: () => void onMoveUp: () => void onMoveDown: () => void isFirst: boolean isLast: boolean collections: Collection[] }) { const [isExpanded, setIsExpanded] = useState(false) const [optionsInput, setOptionsInput] = useState(field.options?.join('\n') || '') const handleOptionsChange = (value: string) => { setOptionsInput(value) const options = value.split('\n').filter(Boolean) onUpdate({ ...field, options: options.length > 0 ? options : undefined }) } return (
{/* Field header */}
{/* Drag handle placeholder */}
{/* Move buttons */}
{/* Type badge */} {FIELD_TYPES.find((t) => t.value === field.type)?.icon || '?'} {/* Field name and label */}
{field.name} {field.label && field.label !== field.name && ( ({field.label}) )}
{/* Required badge */} {field.required && ( Required )} {/* Type selector */} {/* Expand/collapse */} {/* Delete button */}
{/* Field properties (expanded) */} {isExpanded && (
onUpdate({ ...field, label: e.target.value })} placeholder={field.name} /> onUpdate({ ...field, description: e.target.value })} placeholder="Help text for this field" />
{/* Markdown option - only visible for textarea widget */} {field.widget === 'textarea' && (
(WYSIWYG editor)
)} onUpdate({ ...field, group: e.target.value })} placeholder="e.g., Basic Info, Contact" /> {field.type === 'select' && (