|
|
@@ -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"
|