Ver Fonte

fix: system fields display and form handling improvements

- Add getColumnValue helper to properly display system fields in table, card, and kanban views
- Filter system fields (starting with _) from create/edit forms (read-only metadata)
- Prevent custom field names starting with _ (reserved for system fields)
- Fix duplicate system fields in view editor by using Set for deduplication
- Show "Selected Field" section even for single system field selection
- Separate system fields from custom fields in view schema loading
Fszontagh há 6 meses atrás
pai
commit
a947d36b94

+ 43 - 9
webui/src/components/PageRenderer/DocumentListRenderer.tsx

@@ -16,6 +16,7 @@ interface ViewField {
   type: string
   required?: boolean
   options?: Array<{ value: string; label: string }> | string[]
+  widget?: string  // Widget type override (e.g., 'textarea' for text fields)
 }
 
 interface View {
@@ -145,7 +146,7 @@ export function DocumentListRenderer({
   const fields = view?.schema?.fields || []
   const collectionName = view?.collection_name || ''
 
-  // Get quick create/edit fields
+  // Get quick create/edit fields (excludes system fields which are read-only)
   const getQuickFields = (mode: 'create' | 'edit'): ViewField[] => {
     const allFields = view?.schema?.fields || []
     const quickFields = mode === 'create'
@@ -155,10 +156,21 @@ export function DocumentListRenderer({
     if (quickFields && quickFields.length > 0) {
       return quickFields
         .map(name => allFields.find(f => f.name === name))
-        .filter((f): f is ViewField => f !== undefined)
+        .filter((f): f is ViewField => f !== undefined && !f.name.startsWith('_'))
     }
 
-    return allFields
+    // Filter out system fields (starting with _) - they are read-only metadata
+    return allFields.filter(f => !f.name.startsWith('_'))
+  }
+
+  // Get value for a column (handles system fields which are stored differently)
+  const getColumnValue = (doc: Document, colName: string): unknown => {
+    // These three fields are mapped due to Document type structure (top-level vs data)
+    if (colName === '_id') return doc.id
+    if (colName === '_created_at') return doc.created_at
+    if (colName === '_updated_at') return doc.updated_at
+    // All other fields (including other system fields like _created_by_name) are in data
+    return doc.data?.[colName]
   }
 
   // Handle inline create form submission
@@ -386,6 +398,28 @@ export function DocumentListRenderer({
       )
     }
 
+    // Check for textarea - either by type or by widget override
+    if (field.type === 'textarea' || field.type === 'text-long' || field.widget === 'textarea') {
+      return (
+        <div key={field.name} className={compact ? "flex-1 min-w-[200px]" : ""}>
+          {!compact && (
+            <label className="mb-1 block text-sm font-medium text-gray-700">
+              {field.label}
+              {field.required && <span className="text-red-500 ml-0.5">*</span>}
+            </label>
+          )}
+          <textarea
+            value={String(value)}
+            onChange={(e) => setFormData(prev => ({ ...prev, [field.name]: e.target.value }))}
+            className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
+            placeholder={compact ? field.label : undefined}
+            rows={compact ? 2 : 4}
+            required={field.required}
+          />
+        </div>
+      )
+    }
+
     // Default: text input
     return (
       <div key={field.name} className={compact ? "flex-1 min-w-[120px]" : ""}>
@@ -576,7 +610,7 @@ export function DocumentListRenderer({
                       <tr key={doc.id} className="hover:bg-gray-50">
                         {visibleColumns.map((colName) => (
                           <td key={colName} className="whitespace-nowrap px-4 py-3 text-sm text-gray-900">
-                            {formatCellValue(doc.data[colName], fields.find(f => f.name === colName)?.type)}
+                            {formatCellValue(getColumnValue(doc, colName), fields.find(f => f.name === colName)?.type)}
                           </td>
                         ))}
                         <td className="whitespace-nowrap px-4 py-3 text-right text-sm">
@@ -680,7 +714,7 @@ export function DocumentListRenderer({
                   >
                     {/* Title */}
                     <h4 className="font-medium text-gray-900 truncate">
-                      {formatCellValue(doc.data[titleField], field?.type)}
+                      {formatCellValue(getColumnValue(doc, titleField), field?.type)}
                     </h4>
 
                     {/* Body fields */}
@@ -688,7 +722,7 @@ export function DocumentListRenderer({
                       <div className="mt-3 space-y-1.5">
                         {bodyFields.map(colName => {
                           const bodyField = fields.find(f => f.name === colName)
-                          const val = doc.data[colName]
+                          const val = getColumnValue(doc, colName)
                           if (val === null || val === undefined) return null
                           return (
                             <div key={colName} className="flex items-center text-sm">
@@ -752,7 +786,7 @@ export function DocumentListRenderer({
     // Group documents by the kanban field
     const groups: Record<string, Document[]> = {}
     documents.forEach((doc) => {
-      const groupValue = String(doc.data[groupField] || 'Unassigned')
+      const groupValue = String(getColumnValue(doc, groupField) || 'Unassigned')
       if (!groups[groupValue]) {
         groups[groupValue] = []
       }
@@ -794,7 +828,7 @@ export function DocumentListRenderer({
                       >
                         {/* Title */}
                         <p className="font-medium text-gray-900">
-                          {formatCellValue(doc.data[titleField], field?.type)}
+                          {formatCellValue(getColumnValue(doc, titleField), field?.type)}
                         </p>
 
                         {/* Body fields */}
@@ -802,7 +836,7 @@ export function DocumentListRenderer({
                           <div className="mt-2 space-y-1">
                             {bodyFields.map(colName => {
                               const bodyField = fields.find(f => f.name === colName)
-                              const val = doc.data[colName]
+                              const val = getColumnValue(doc, colName)
                               if (val === null || val === undefined) return null
                               return (
                                 <div key={colName} className="text-xs text-gray-500">

+ 33 - 12
webui/src/components/PageRenderer/index.tsx

@@ -29,6 +29,7 @@ interface ViewField {
   type: string
   required?: boolean
   options?: Array<{ value: string; label: string }> | string[]
+  widget?: string  // Widget type override (e.g., 'textarea' for text fields)
 }
 
 // Normalize options to {value, label} format
@@ -416,7 +417,8 @@ export function PageRenderer({ page, isEditing = false, onComponentClick, onReor
       )
     }
 
-    if (field.type === 'textarea' || field.type === 'text-long') {
+    // Check for textarea - either by type or by widget override
+    if (field.type === 'textarea' || field.type === 'text-long' || field.widget === 'textarea') {
       return (
         <div key={field.name}>
           <label className="mb-1.5 block text-sm font-medium text-gray-700">
@@ -427,7 +429,7 @@ export function PageRenderer({ page, isEditing = false, onComponentClick, onReor
             value={String(value)}
             onChange={(e) => setCreateFormData(prev => ({ ...prev, [field.name]: e.target.value }))}
             className="w-full rounded-lg border border-gray-300 px-3 py-2"
-            rows={3}
+            rows={4}
             required={field.required}
           />
         </div>
@@ -570,6 +572,25 @@ export function PageRenderer({ page, isEditing = false, onComponentClick, onReor
       )
     }
 
+    // Check for textarea - either by type or by widget override
+    if (field.type === 'textarea' || field.type === 'text-long' || field.widget === 'textarea') {
+      return (
+        <div key={field.name}>
+          <label className="mb-1.5 block text-sm font-medium text-gray-700">
+            {field.label}
+            {field.required && <span className="text-red-500 ml-1">*</span>}
+          </label>
+          <textarea
+            value={String(value)}
+            onChange={(e) => setEditFormData(prev => ({ ...prev, [field.name]: e.target.value }))}
+            className="w-full rounded-lg border border-gray-300 px-3 py-2"
+            rows={4}
+            required={field.required}
+          />
+        </div>
+      )
+    }
+
     // Default: text input
     return (
       <Input
@@ -582,36 +603,36 @@ export function PageRenderer({ page, isEditing = false, onComponentClick, onReor
     )
   }
 
-  // Get fields to show in create modal
+  // Get fields to show in create modal (excludes system fields starting with _)
   const getCreateFields = (view: View): ViewField[] => {
     const allFields = view.schema?.fields || []
     const quickCreateFields = view.settings?.quick_create_fields
 
     if (quickCreateFields && quickCreateFields.length > 0) {
-      // Return only the specified quick create fields in order
+      // Return only the specified quick create fields, excluding system fields
       return quickCreateFields
         .map(name => allFields.find(f => f.name === name))
-        .filter((f): f is ViewField => f !== undefined)
+        .filter((f): f is ViewField => f !== undefined && !f.name.startsWith('_'))
     }
 
-    // Default: return all fields
-    return allFields
+    // Default: return all fields except system fields (starting with _)
+    return allFields.filter(f => !f.name.startsWith('_'))
   }
 
-  // Get fields to show in edit modal
+  // Get fields to show in edit modal (excludes system fields starting with _)
   const getEditFields = (view: View): ViewField[] => {
     const allFields = view.schema?.fields || []
     const quickEditFields = view.settings?.quick_edit_fields
 
     if (quickEditFields && quickEditFields.length > 0) {
-      // Return only the specified quick edit fields in order
+      // Return only the specified quick edit fields, excluding system fields
       return quickEditFields
         .map(name => allFields.find(f => f.name === name))
-        .filter((f): f is ViewField => f !== undefined)
+        .filter((f): f is ViewField => f !== undefined && !f.name.startsWith('_'))
     }
 
-    // Default: return all fields
-    return allFields
+    // Default: return all fields except system fields (starting with _)
+    return allFields.filter(f => !f.name.startsWith('_'))
   }
 
   // Drag handlers

+ 91 - 20
webui/src/pages/Views.tsx

@@ -362,20 +362,23 @@ function ViewModal({
   const [collectionName, setCollectionName] = useState(view?.collection_name || '')
   const [fields, setFields] = useState<SchemaField[]>(() => {
     if (view?.schema?.fields) {
-      return view.schema.fields.map((f, index) => ({
-        _id: crypto.randomUUID(),
-        name: f.name,
-        type: f.type || 'text',
-        label: f.label || '',
-        description: f.description || '',
-        required: f.required || false,
-        display_order: f.display_order ?? index,
-        widget: f.widget || '',
-        group: f.group || '',
-        default_value: f.default_value,
-        options: f.options,
-        reference_collection: f.reference_collection,
-      }))
+      // Filter out system fields (starting with _) - they are managed separately
+      return view.schema.fields
+        .filter(f => !f.name.startsWith('_'))
+        .map((f, index) => ({
+          _id: crypto.randomUUID(),
+          name: f.name,
+          type: f.type || 'text',
+          label: f.label || '',
+          description: f.description || '',
+          required: f.required || false,
+          display_order: f.display_order ?? index,
+          widget: f.widget || '',
+          group: f.group || '',
+          default_value: f.default_value,
+          options: f.options,
+          reference_collection: f.reference_collection,
+        }))
     }
     return []
   })
@@ -407,11 +410,15 @@ function ViewModal({
   // System fields state - built-in document metadata fields
   const [enabledSystemFields, setEnabledSystemFields] = useState<string[]>(() => {
     if (view?.schema?.fields) {
-      // Check which system fields are already in the schema
+      // Check which system fields are already in the schema (use Set to avoid duplicates)
       const systemFieldNames = ['_id', '_created_at', '_updated_at', '_created_by_name', '_updated_by_name']
-      return view.schema.fields
-        .filter(f => systemFieldNames.includes(f.name))
-        .map(f => f.name)
+      const enabledSet = new Set<string>()
+      view.schema.fields.forEach(f => {
+        if (systemFieldNames.includes(f.name)) {
+          enabledSet.add(f.name)
+        }
+      })
+      return Array.from(enabledSet)
     }
     return []
   })
@@ -468,6 +475,16 @@ function ViewModal({
     )
   }
 
+  const moveSystemField = (index: number, direction: 'up' | 'down') => {
+    const newIndex = direction === 'up' ? index - 1 : index + 1
+    if (newIndex < 0 || newIndex >= enabledSystemFields.length) return
+
+    const newOrder = [...enabledSystemFields]
+    const [removed] = newOrder.splice(index, 1)
+    newOrder.splice(newIndex, 0, removed)
+    setEnabledSystemFields(newOrder)
+  }
+
   const startEditingFieldName = (field: SchemaField) => {
     setEditingFieldId(field._id)
     setEditingFieldName(field.name)
@@ -476,6 +493,13 @@ function ViewModal({
   const finishEditingFieldName = () => {
     if (editingFieldId && editingFieldName.trim()) {
       const sanitized = editingFieldName.replace(/[^a-zA-Z0-9_]/g, '')
+      // Prevent field names starting with _ (reserved for system fields)
+      if (sanitized.startsWith('_')) {
+        setError('Field names starting with "_" are reserved for system fields')
+        setEditingFieldId(null)
+        setEditingFieldName('')
+        return
+      }
       if (sanitized && !fields.some((f) => f._id !== editingFieldId && f.name === sanitized)) {
         setFields(
           fields.map((f) => (f._id === editingFieldId ? { ...f, name: sanitized } : f))
@@ -674,6 +698,53 @@ function ViewModal({
                   </label>
                 ))}
               </div>
+
+              {/* Enabled system fields order */}
+              {enabledSystemFields.length >= 1 && (
+                <div className="mt-4">
+                  <p className="mb-2 text-sm font-medium text-gray-700">
+                    {enabledSystemFields.length === 1 ? 'Selected Field' : 'Display Order'}
+                  </p>
+                  <div className="space-y-2">
+                    {enabledSystemFields.map((fieldName, index) => {
+                      const sysField = SYSTEM_FIELDS.find(sf => sf.name === fieldName)
+                      if (!sysField) return null
+                      return (
+                        <div
+                          key={fieldName}
+                          className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
+                        >
+                          <div className="flex flex-col">
+                            <button
+                              type="button"
+                              onClick={() => moveSystemField(index, 'up')}
+                              disabled={index === 0}
+                              className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
+                              title="Move up"
+                            >
+                              <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
+                              </svg>
+                            </button>
+                            <button
+                              type="button"
+                              onClick={() => moveSystemField(index, 'down')}
+                              disabled={index === enabledSystemFields.length - 1}
+                              className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
+                              title="Move down"
+                            >
+                              <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
+                              </svg>
+                            </button>
+                          </div>
+                          <span className="text-sm font-medium text-gray-700">{sysField.label}</span>
+                        </div>
+                      )
+                    })}
+                  </div>
+                </div>
+              )}
             </div>
 
             {/* Schema Designer */}
@@ -840,7 +911,7 @@ function ViewModal({
                       Select which fields to show in the {quickCreateMode === 'inline' ? 'inline form' : 'modal'}. Leave empty to show all fields.
                     </p>
                     <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
-                      {sortedFields.map((field) => (
+                      {sortedFields.filter(f => !f.name.startsWith('_')).map((field) => (
                         <label key={field._id} className="flex items-center gap-2">
                           <input
                             type="checkbox"
@@ -947,7 +1018,7 @@ function ViewModal({
                       Select which fields to show in the {quickEditMode === 'inline' ? 'inline form' : 'modal'}. Leave empty to show all fields.
                     </p>
                     <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
-                      {sortedFields.map((field) => (
+                      {sortedFields.filter(f => !f.name.startsWith('_')).map((field) => (
                         <label key={field._id} className="flex items-center gap-2">
                           <input
                             type="checkbox"