feat: enhance schema type handling with new formats and UI components

This commit is contained in:
2026-05-29 23:08:17 +12:00
parent d4969172c2
commit 20808b6962
3 changed files with 122 additions and 4 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ _EDITOR_CONTEXT = {
"patterns": [p.value for p in ReconPatterns], "patterns": [p.value for p in ReconPatterns],
"statuses": [s.value for s in ReconConfigStatus], "statuses": [s.value for s in ReconConfigStatus],
"frequencies": ["Ad Hoc", "Intra Day", "Daily", "Weekly", "Monthly", "Quarterly"], "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"],
} }
+28
View File
@@ -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; padding: 5px 8px;
width: 100%; 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 */ /* Field mapping */
.mapping-header { .mapping-header {
+93 -3
View File
@@ -290,6 +290,40 @@
// ── Schema type options ───────────────────────────────────────────────────── // ── Schema type options ─────────────────────────────────────────────────────
const SCHEMA_TYPES = {{ schema_types | tojson }}; const SCHEMA_TYPES = {{ schema_types | tojson }};
const IS_NEW = {{ 'true' if is_new else 'false' }}; 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' }}; const CONFIG_REF = {{ (reference | tojson) if not is_new else 'null' }};
// ── Build a syscfg card from a data object ────────────────────────────────── // ── Build a syscfg card from a data object ──────────────────────────────────
@@ -412,20 +446,76 @@ function addSchemaRow(card, colName, colType, isIndex) {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'schema-row'; row.className = 'schema-row';
const parsed = parseSchemaType(colType);
const colInput = document.createElement('input'); const colInput = document.createElement('input');
colInput.type = 'text'; colInput.type = 'text';
colInput.placeholder = 'column_name'; colInput.placeholder = 'column_name';
colInput.value = colName || ''; colInput.value = colName || '';
colInput.addEventListener('input', () => updateSchemaCount(card)); colInput.addEventListener('input', () => updateSchemaCount(card));
// ── Type cell ────────────────────────────────────────────────
const typeCell = document.createElement('div');
typeCell.className = 'schema-type-cell';
const typeSelect = document.createElement('select'); const typeSelect = document.createElement('select');
typeSelect.className = 'schema-type-select';
SCHEMA_TYPES.forEach(t => { SCHEMA_TYPES.forEach(t => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = t; opt.textContent = t; opt.value = t; opt.textContent = t;
if (t === (colType || 'str')) opt.selected = true; if (t === parsed.base) opt.selected = true;
typeSelect.appendChild(opt); 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'); const idxCheck = document.createElement('input');
idxCheck.type = 'checkbox'; idxCheck.type = 'checkbox';
idxCheck.className = 'idx-check'; idxCheck.className = 'idx-check';
@@ -438,7 +528,7 @@ function addSchemaRow(card, colName, colType, isIndex) {
removeBtn.textContent = '×'; removeBtn.textContent = '×';
removeBtn.addEventListener('click', () => { row.remove(); updateSchemaCount(card); }); removeBtn.addEventListener('click', () => { row.remove(); updateSchemaCount(card); });
row.append(colInput, typeSelect, idxCheck, removeBtn); row.append(colInput, typeCell, idxCheck, removeBtn);
container.appendChild(row); container.appendChild(row);
} }
@@ -474,7 +564,7 @@ function readSyscfgCard(card) {
const index_fields = []; const index_fields = [];
card.querySelectorAll('.schema-rows .schema-row').forEach(row => { card.querySelectorAll('.schema-rows .schema-row').forEach(row => {
const name = row.querySelector('input[type="text"]').value.trim(); const name = row.querySelector('input[type="text"]').value.trim();
const type = row.querySelector('select').value; const type = composeSchemaType(row);
if (!name) return; if (!name) return;
schema[name] = type; schema[name] = type;
if (row.querySelector('.idx-check')?.checked) index_fields.push(name); if (row.querySelector('.idx-check')?.checked) index_fields.push(name);