From 20808b6962c5cce6a713af0844bd79bd3a579340 Mon Sep 17 00:00:00 2001 From: Semprini Date: Fri, 29 May 2026 23:08:17 +1200 Subject: [PATCH] feat: enhance schema type handling with new formats and UI components --- app/views/config_views.py | 2 +- data/static/css/styles.css | 28 +++++++++ data/templates/config_editor.html | 96 ++++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/app/views/config_views.py b/app/views/config_views.py index 5f31891..26f6cdb 100644 --- a/app/views/config_views.py +++ b/app/views/config_views.py @@ -18,7 +18,7 @@ _EDITOR_CONTEXT = { "patterns": [p.value for p in ReconPatterns], "statuses": [s.value for s in ReconConfigStatus], "frequencies": ["Ad Hoc", "Intra Day", "Daily", "Weekly", "Monthly", "Quarterly"], - "schema_types": ["str", "int", "float", "date('%Y-%m-%d')", "datetime('%Y-%m-%d %H:%M:%S')", "bool"], + "schema_types": ["str", "int", "float", "date", "datetime", "decimal", "bool"], } diff --git a/data/static/css/styles.css b/data/static/css/styles.css index 84f99c1..4f53e6c 100644 --- a/data/static/css/styles.css +++ b/data/static/css/styles.css @@ -959,6 +959,34 @@ a.job-bar:hover { filter: brightness(1.1); box-shadow: 0 0 0 1px rgba(255,255,25 padding: 5px 8px; width: 100%; } +.schema-type-cell { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.schema-type-cell .schema-type-select { + width: auto; + min-width: 80px; + flex-shrink: 0; +} +.schema-fmt-wrapper { + flex: 1; + min-width: 0; + display: flex; +} +.schema-fmt-input { + width: 100%; + min-width: 80px; +} +.schema-dec-wrapper { + display: flex; + gap: 4px; +} +.schema-dec-prec, +.schema-dec-scale { + width: 54px; +} /* Field mapping */ .mapping-header { diff --git a/data/templates/config_editor.html b/data/templates/config_editor.html index 350889e..88a3540 100644 --- a/data/templates/config_editor.html +++ b/data/templates/config_editor.html @@ -290,6 +290,40 @@ // ── Schema type options ───────────────────────────────────────────────────── const SCHEMA_TYPES = {{ schema_types | tojson }}; const IS_NEW = {{ 'true' if is_new else 'false' }}; + +const DATE_FORMATS = [ + '%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%d-%m-%Y', + '%Y%m%d', '%d %b %Y', '%d %B %Y', '%b %d, %Y', +]; +const DATETIME_FORMATS = [ + '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', + '%d/%m/%Y %H:%M:%S', '%d/%m/%Y %H:%M', + '%Y-%m-%d %H:%M', '%Y%m%d%H%M%S', '%d %b %Y %H:%M:%S', +]; + +function parseSchemaType(typeStr) { + if (!typeStr) return { base: 'str' }; + let m; + if ((m = typeStr.match(/^date\('([^']+)'\)$/))) return { base: 'date', format: m[1] }; + if ((m = typeStr.match(/^datetime\('([^']+)'\)$/))) return { base: 'datetime', format: m[1] }; + if ((m = typeStr.match(/^decimal\((\d+),\s*(\d+)\)$/))) return { base: 'decimal', precision: +m[1], scale: +m[2] }; + return { base: typeStr }; +} + +function composeSchemaType(row) { + const base = row.querySelector('.schema-type-select').value; + if (base === 'date' || base === 'datetime') { + const fmt = row.querySelector('.schema-fmt-input')?.value.trim() + || (base === 'date' ? '%Y-%m-%d' : '%Y-%m-%d %H:%M:%S'); + return `${base}('${fmt}')`; + } + if (base === 'decimal') { + const prec = parseInt(row.querySelector('.schema-dec-prec')?.value) || 18; + const scale = parseInt(row.querySelector('.schema-dec-scale')?.value) || 2; + return `decimal(${prec}, ${scale})`; + } + return base; +} const CONFIG_REF = {{ (reference | tojson) if not is_new else 'null' }}; // ── Build a syscfg card from a data object ────────────────────────────────── @@ -412,20 +446,76 @@ function addSchemaRow(card, colName, colType, isIndex) { const row = document.createElement('div'); row.className = 'schema-row'; + const parsed = parseSchemaType(colType); + const colInput = document.createElement('input'); colInput.type = 'text'; colInput.placeholder = 'column_name'; colInput.value = colName || ''; colInput.addEventListener('input', () => updateSchemaCount(card)); + // ── Type cell ──────────────────────────────────────────────── + const typeCell = document.createElement('div'); + typeCell.className = 'schema-type-cell'; + const typeSelect = document.createElement('select'); + typeSelect.className = 'schema-type-select'; SCHEMA_TYPES.forEach(t => { const opt = document.createElement('option'); opt.value = t; opt.textContent = t; - if (t === (colType || 'str')) opt.selected = true; + if (t === parsed.base) opt.selected = true; typeSelect.appendChild(opt); }); + // Date / datetime format combobox + const fmtId = `fmt-${Math.random().toString(36).slice(2)}`; + const fmtWrapper = document.createElement('span'); + fmtWrapper.className = 'schema-fmt-wrapper'; + const fmtInput = document.createElement('input'); + fmtInput.type = 'text'; + fmtInput.className = 'schema-fmt-input'; + fmtInput.setAttribute('list', fmtId); + if (parsed.format) fmtInput.value = parsed.format; + const fmtList = document.createElement('datalist'); + fmtList.id = fmtId; + fmtWrapper.append(fmtInput, fmtList); + + // Decimal precision + scale inputs + const decWrapper = document.createElement('span'); + decWrapper.className = 'schema-dec-wrapper'; + const precInput = document.createElement('input'); + precInput.type = 'number'; + precInput.className = 'schema-dec-prec'; + precInput.placeholder = 'prec'; + precInput.min = 1; precInput.max = 38; + if (parsed.precision != null) precInput.value = parsed.precision; + const scaleInput = document.createElement('input'); + scaleInput.type = 'number'; + scaleInput.className = 'schema-dec-scale'; + scaleInput.placeholder = 'scale'; + scaleInput.min = 0; scaleInput.max = 38; + if (parsed.scale != null) scaleInput.value = parsed.scale; + decWrapper.append(precInput, scaleInput); + + function syncExtras() { + const base = typeSelect.value; + const isDate = base === 'date', isDt = base === 'datetime'; + fmtWrapper.hidden = !(isDate || isDt); + decWrapper.hidden = base !== 'decimal'; + if (isDate || isDt) { + fmtList.innerHTML = ''; + (isDate ? DATE_FORMATS : DATETIME_FORMATS).forEach(f => { + const o = document.createElement('option'); o.value = f; fmtList.appendChild(o); + }); + if (!fmtInput.value) fmtInput.value = isDate ? '%Y-%m-%d' : '%Y-%m-%d %H:%M:%S'; + fmtInput.placeholder = isDate ? '%Y-%m-%d' : '%Y-%m-%d %H:%M:%S'; + } + } + typeSelect.addEventListener('change', syncExtras); + syncExtras(); + + typeCell.append(typeSelect, fmtWrapper, decWrapper); + const idxCheck = document.createElement('input'); idxCheck.type = 'checkbox'; idxCheck.className = 'idx-check'; @@ -438,7 +528,7 @@ function addSchemaRow(card, colName, colType, isIndex) { removeBtn.textContent = '×'; removeBtn.addEventListener('click', () => { row.remove(); updateSchemaCount(card); }); - row.append(colInput, typeSelect, idxCheck, removeBtn); + row.append(colInput, typeCell, idxCheck, removeBtn); container.appendChild(row); } @@ -474,7 +564,7 @@ function readSyscfgCard(card) { const index_fields = []; card.querySelectorAll('.schema-rows .schema-row').forEach(row => { const name = row.querySelector('input[type="text"]').value.trim(); - const type = row.querySelector('select').value; + const type = composeSchemaType(row); if (!name) return; schema[name] = type; if (row.querySelector('.idx-check')?.checked) index_fields.push(name);