From 2a81326df362d079bd5ea2529d1da6969600aa41 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Thu, 28 May 2026 17:07:51 +0800 Subject: [PATCH] feat: enhance DataTable with extensible registerFieldType API, rich formatters, and dynamic AutoForm schemas (by AI) --- src/index.js | 151 ++++++++++++++++++++++++++++++++++++++--------- test/all.spec.js | 12 +++- test/index.html | 19 ++++-- 3 files changed, 148 insertions(+), 34 deletions(-) diff --git a/src/index.js b/src/index.js index 2b55d7c..0d69a6c 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,84 @@ const MODE_ICONS = { '=': 'bi-calculator', '>': 'bi-chevron-right', '<': 'bi-chevron-left', 'between': 'bi-arrows-expand' }; +export const DataTable = { + _fieldTypes: new Map(), + registerFieldType: (config) => { + DataTable._fieldTypes.set(config.value, config); + }, + getFieldTypes: () => Array.from(DataTable._fieldTypes.values()) +}; + +// Register Built-in Types +DataTable.registerFieldType({ + value: 'text', label: '{#Text#}', typeForDB: 'v4096', + schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="text"' }] +}); +DataTable.registerFieldType({ + value: 'number', label: '{#Number#}', typeForDB: 'ff', + schema: [ + { name: 'decimals', label: 'Decimals', type: 'number', setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' }, + { name: 'prefix', label: 'Prefix (e.g. $)', type: 'text', if: 'this.data.user_type=="number"' }, + { name: 'suffix', label: 'Suffix (e.g. %)', type: 'text', if: 'this.data.user_type=="number"' }, + { name: 'thousandSep', label: 'Thousand Sep', type: 'switch', if: 'this.data.user_type=="number"' } + ], + formatter: (val, field) => { + if (val == null || val === '') return ''; + let num = Number(val); + if (isNaN(num)) return val; + const s = field.settings || {}; + if (s.decimals !== undefined) num = num.toFixed(s.decimals); + let str = String(num); + if (s.thousandSep) { + const parts = str.split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + str = parts.join('.'); + } + return (s.prefix || '') + str + (s.suffix || ''); + } +}); +DataTable.registerFieldType({ + value: 'select', label: '{#Single Select#}', typeForDB: 'v1024', + schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="select"' }], + formatter: (val, field) => { + if (val == null || val === '') return ''; + const opts = field.settings?.options || []; + const opt = opts.find(o => typeof o === 'object' ? o.value == val : o == val); + return opt ? (typeof opt === 'object' ? opt.label : opt) : val; + } +}); +DataTable.registerFieldType({ + value: 'checkbox', label: '{#Multi Select#}', typeForDB: 'v4096', + schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="checkbox"' }], + formatter: (val, field) => { + if (!Array.isArray(val)) return val == null ? '' : String(val); + const opts = field.settings?.options || []; + return val.map(v => { + const opt = opts.find(o => typeof o === 'object' ? o.value == v : o == v); + return opt ? (typeof opt === 'object' ? opt.label : opt) : v; + }).join(', '); + } +}); +DataTable.registerFieldType({ + value: 'switch', label: '{#Switch#}', typeForDB: 'b', + schema: [ + { name: 'labelOn', label: 'Label On', type: 'text', if: 'this.data.user_type=="switch"' }, + { name: 'labelOff', label: 'Label Off', type: 'text', if: 'this.data.user_type=="switch"' } + ], + formatter: (val, field) => { + const s = field.settings || {}; + return val ? (s.labelOn || 'Yes') : (s.labelOff || 'No'); + } +}); +DataTable.registerFieldType({ + value: 'datetime', label: '{#DateTime#}', typeForDB: 'dt', + schema: [{ name: 'format', label: 'Format', type: 'text', setting: { placeholder: 'YYYY-MM-DD' }, if: 'this.data.user_type=="datetime"' }] +}); +DataTable.registerFieldType({ + value: 'textarea', label: '{#Long Text#}', typeForDB: 't', + schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="textarea"' }] +}); + Component.register('DataTable', container => { if (!container.state) container.state = NewState({}) const state = container.state @@ -60,6 +138,8 @@ Component.register('DataTable', container => { container.format = (val, field) => { if (field.formatter) return field.formatter(val, field); + const typeInfo = DataTable._fieldTypes.get(field.settings?.formType || field.type || 'text'); + if (typeInfo && typeInfo.formatter) return typeInfo.formatter(val, field); return val == null ? '' : (typeof val === 'object' ? JSON.stringify(val) : String(val)); }; @@ -385,25 +465,32 @@ Component.register('DataTable', container => { state.isDirty = false; }; - const FIELD_TYPES = [ - { label: '{#Text#}', value: 'text', db: 'v4096' }, - { label: '{#Number#}', value: 'number' }, - { label: '{#Single Select#}', value: 'select', db: 'v255' }, - { label: '{#Checkbox#}', value: 'checkbox', db: 'v1024' }, - { label: '{#Tags#}', value: 'TagsInput', db: 't' }, - { label: '{#DateTime#}', value: 'datetime', db: 'dt' }, - { label: '{#Switch#}', value: 'switch', db: 'b' } - ]; + const getFieldSchema = () => { + const types = DataTable.getFieldTypes(); + const baseSchema = [ + { name: 'id', label: 'Field ID', type: 'text', setting: { required: true, placeholder: 'e.g. user_name' } }, + { name: 'name', label: 'Display Name', type: 'text', setting: { required: true, placeholder: 'e.g. 用户名' } }, + { name: 'user_type', label: 'Field Type', type: 'select', options: types.map(t => ({ label: t.label, value: t.value })) } + ]; + const dynamicSchema = types.reduce((acc, t) => acc.concat(t.schema || []), []); + return baseSchema.concat(dynamicSchema, [ + { name: 'isIndex', label: 'Index', type: 'switch' }, + { name: 'memo', label: 'Memo', type: 'text' } + ]); + }; - const getFieldSchema = () => [ - { name: 'id', label: 'Field ID', type: 'text', setting: { required: true, placeholder: 'e.g. user_name' } }, - { name: 'name', label: 'Display Name', type: 'text', setting: { required: true, placeholder: 'e.g. 用户名' } }, - { name: 'user_type', label: 'Field Type', type: 'select', options: FIELD_TYPES }, - { name: 'decimals', label: 'Decimals', type: 'number', setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' }, - { name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'One option per line' }, if: '["select", "checkbox"].includes(this.data.user_type)' }, - { name: 'isIndex', label: 'Index', type: 'switch' }, - { name: 'memo', label: 'Memo', type: 'text' } - ]; + const parseOptionsStr = (str) => { + if (!str) return undefined; + return str.split('\n').map(s => s.trim()).filter(Boolean).map(line => { + const idx = line.indexOf(':'); + if (idx > -1) return { label: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() }; + return line; + }); + }; + const formatOptionsStr = (opts) => { + if (!opts) return ''; + return opts.map(o => typeof o === 'object' ? `${o.label}:${o.value}` : o).join('\n'); + }; container.addField = async () => { container.hideColumnMenu(); @@ -426,8 +513,8 @@ Component.register('DataTable', container => { d.show(); const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result))); if (result === 2) { - const typeInfo = FIELD_TYPES.find(t => t.value === data.user_type); - let dbType = typeInfo?.db || 'v255'; + const typeInfo = DataTable._fieldTypes.get(data.user_type); + let dbType = typeInfo?.typeForDB || 'v1024'; if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi'; const field = { @@ -436,7 +523,10 @@ Component.register('DataTable', container => { settings: { formType: data.user_type, decimals: data.decimals, - options: data.options_str ? data.options_str.split('\n').map(s => s.trim()).filter(Boolean) : undefined + prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, + labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, + placeholder: data.placeholder, + options: parseOptionsStr(data.options_str) } }; state.fields = [...state.fields, field]; @@ -451,11 +541,15 @@ Component.register('DataTable', container => { if (!state.activeField) return; container.hideColumnMenu(); const f = state.activeField; + const s = f.settings || {}; const data = NewState({ id: f.id, name: f.name, memo: f.memo || '', isIndex: !!f.isIndex, - user_type: f.settings?.formType || 'text', - decimals: f.settings?.decimals || 0, - options_str: f.settings?.options ? f.settings.options.join('\n') : '' + user_type: s.formType || 'text', + decimals: s.decimals || 0, + prefix: s.prefix || '', suffix: s.suffix || '', thousandSep: !!s.thousandSep, + labelOn: s.labelOn || '', labelOff: s.labelOff || '', format: s.format || '', + placeholder: s.placeholder || '', + options_str: formatOptionsStr(s.options) }); const d = document.body.appendChild(document.createElement('Dialog')); @@ -477,8 +571,8 @@ Component.register('DataTable', container => { if (result === 2) { const idx = state.fields.findIndex(item => item.id === f.id); if (idx !== -1) { - const typeInfo = FIELD_TYPES.find(t => t.value === data.user_type); - let dbType = typeInfo?.db || 'v255'; + const typeInfo = DataTable._fieldTypes.get(data.user_type); + let dbType = typeInfo?.typeForDB || 'v1024'; if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi'; const updatedField = { @@ -488,7 +582,10 @@ Component.register('DataTable', container => { ...f.settings, formType: data.user_type, decimals: data.decimals, - options: data.options_str ? data.options_str.split('\n').map(s => s.trim()).filter(Boolean) : undefined + prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, + labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, + placeholder: data.placeholder, + options: parseOptionsStr(data.options_str) } }; state.fields[idx] = updatedField; diff --git a/test/all.spec.js b/test/all.spec.js index 0178044..93a61c2 100644 --- a/test/all.spec.js +++ b/test/all.spec.js @@ -87,7 +87,17 @@ test('DataTable comprehensive tests and scrolling benchmarks', async ({ page }) }, colIdx); console.log(`Column [${field.id}] UI text:`, cellText); - if (field.type !== 'switch' && field.id !== 'active') { + if (field.id === 'score') { + expect(cellText).toContain('$99.00'); // 99 + formatting + } else if (field.id === 'role') { + expect(cellText).toContain('Content Editor'); // Editor mapped to label + } else if (field.id === 'active') { + expect(cellText).toContain('NO'); // false mapped to labelOff + } else if (field.id === 'tags') { + expect(cellText).toContain('State.js | Playwright'); // Formatter logic + } else if (field.id === 'actions') { + expect(cellText).toContain('🔧 Edit'); // Custom formatter + } else { expect(cellText).toContain(String(newValue).split('\n')[0]); } } diff --git a/test/index.html b/test/index.html index 77f5dad..096204e 100644 --- a/test/index.html +++ b/test/index.html @@ -33,21 +33,28 @@