|
|
@@ -55,6 +55,51 @@ function _tabulatorPersistenceKey(key) { |
|
|
return 'ct.tabulator.' + key; |
|
|
return 'ct.tabulator.' + key; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Builds a stable Tabulator field name for a dynamic attribute column, derived from the |
|
|
|
|
|
// attribute's name rather than its position. This keeps persisted column order, width, |
|
|
|
|
|
// visibility, and filters tied to the same attribute across page loads, even when the |
|
|
|
|
|
// set/order of attributes discovered from the result rows changes. |
|
|
|
|
|
function _attributeFieldKey(name, usedFields) { |
|
|
|
|
|
let slug = String(name).toLowerCase().trim().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); |
|
|
|
|
|
if (slug === '') { |
|
|
|
|
|
slug = 'field'; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const base = 'attr__' + slug; |
|
|
|
|
|
let key = base; |
|
|
|
|
|
let suffix = 2; |
|
|
|
|
|
while (usedFields.has(key)) { |
|
|
|
|
|
key = base + '_' + suffix; |
|
|
|
|
|
suffix++; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
usedFields.add(key); |
|
|
|
|
|
return key; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Discovers the unique set of dynamic attributes across a list of rows, assigning each |
|
|
|
|
|
// a stable `field` name (see _attributeFieldKey) for use as a Tabulator column field. |
|
|
|
|
|
function _discoverAttributeColumns(rows, typeAttributesField, normalizeFn) { |
|
|
|
|
|
const attributes = []; |
|
|
|
|
|
const usedFields = new Set(); |
|
|
|
|
|
|
|
|
|
|
|
rows.forEach((row) => { |
|
|
|
|
|
normalizeFn(row[typeAttributesField] || []).forEach((attr) => { |
|
|
|
|
|
if (!attributes.some((existing) => existing.name === attr.name)) { |
|
|
|
|
|
attributes.push({ ...attr, field: _attributeFieldKey(attr.name, usedFields) }); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
Object.keys(row.attribute_values || {}).forEach((name) => { |
|
|
|
|
|
if (!attributes.some((existing) => existing.name === name)) { |
|
|
|
|
|
attributes.push({ name: name, type: 'text', order: attributes.length + 1, field: _attributeFieldKey(name, usedFields) }); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return attributes; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function _tabulatorBaseOptions(persistenceKey) { |
|
|
function _tabulatorBaseOptions(persistenceKey) { |
|
|
return { |
|
|
return { |
|
|
persistence: { |
|
|
persistence: { |
|
|
@@ -528,23 +573,7 @@ window.campaignTable = function () { |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
jobAttributeColumnsForRows(rows) { |
|
|
jobAttributeColumnsForRows(rows) { |
|
|
const attributes = []; |
|
|
|
|
|
|
|
|
|
|
|
rows.forEach((row) => { |
|
|
|
|
|
this.normalizeAttributes(row.job_type_attributes || []).forEach((attr) => { |
|
|
|
|
|
if (!attributes.some((existing) => existing.name === attr.name)) { |
|
|
|
|
|
attributes.push(attr); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
Object.keys(row.attribute_values || {}).forEach((name) => { |
|
|
|
|
|
if (!attributes.some((existing) => existing.name === name)) { |
|
|
|
|
|
attributes.push({ name: name, type: 'text', order: attributes.length + 1 }); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return attributes; |
|
|
|
|
|
|
|
|
return _discoverAttributeColumns(rows, 'job_type_attributes', this.normalizeAttributes); |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
formatJobRows(rows, attributes) { |
|
|
formatJobRows(rows, attributes) { |
|
|
@@ -560,8 +589,8 @@ window.campaignTable = function () { |
|
|
edit_url: '/jobs/' + encodeURIComponent(row.id) + '/edit', |
|
|
edit_url: '/jobs/' + encodeURIComponent(row.id) + '/edit', |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
attributes.forEach((attr, index) => { |
|
|
|
|
|
tableRow['job_attr_' + index] = this.formatAttributeValue(attributeValues[attr.name] ?? ''); |
|
|
|
|
|
|
|
|
attributes.forEach((attr) => { |
|
|
|
|
|
tableRow[attr.field] = this.formatAttributeValue(attributeValues[attr.name] ?? ''); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
return tableRow; |
|
|
return tableRow; |
|
|
@@ -586,10 +615,10 @@ window.campaignTable = function () { |
|
|
{ title: 'Job Type', field: 'job_type_name', minWidth: 160, headerFilter: 'input' }, |
|
|
{ title: 'Job Type', field: 'job_type_name', minWidth: 160, headerFilter: 'input' }, |
|
|
]; |
|
|
]; |
|
|
|
|
|
|
|
|
attributes.forEach((attr, index) => { |
|
|
|
|
|
|
|
|
attributes.forEach((attr) => { |
|
|
columns.push({ |
|
|
columns.push({ |
|
|
title: attr.name, |
|
|
title: attr.name, |
|
|
field: 'job_attr_' + index, |
|
|
|
|
|
|
|
|
field: attr.field, |
|
|
minWidth: 150, |
|
|
minWidth: 150, |
|
|
headerFilter: 'input', |
|
|
headerFilter: 'input', |
|
|
formatter: function (cell) { |
|
|
formatter: function (cell) { |
|
|
@@ -1440,23 +1469,7 @@ window.jobTable = function () { |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
attributeColumnsForRows(rows) { |
|
|
attributeColumnsForRows(rows) { |
|
|
const attributes = []; |
|
|
|
|
|
|
|
|
|
|
|
rows.forEach((row) => { |
|
|
|
|
|
this.normalizeAttributes(row.job_type_attributes || []).forEach((attr) => { |
|
|
|
|
|
if (!attributes.some((existing) => existing.name === attr.name)) { |
|
|
|
|
|
attributes.push(attr); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
Object.keys(row.attribute_values || {}).forEach((name) => { |
|
|
|
|
|
if (!attributes.some((existing) => existing.name === name)) { |
|
|
|
|
|
attributes.push({ name: name, type: 'text', order: attributes.length + 1 }); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return attributes; |
|
|
|
|
|
|
|
|
return _discoverAttributeColumns(rows, 'job_type_attributes', this.normalizeAttributes); |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
normalizeAttributes(attributes) { |
|
|
normalizeAttributes(attributes) { |
|
|
@@ -1480,8 +1493,8 @@ window.jobTable = function () { |
|
|
updated_at: row.updated_at || '', |
|
|
updated_at: row.updated_at || '', |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
attributes.forEach((attr, index) => { |
|
|
|
|
|
tableRow['attr_' + index] = this.formatAttributeValue(attributeValues[attr.name] ?? ''); |
|
|
|
|
|
|
|
|
attributes.forEach((attr) => { |
|
|
|
|
|
tableRow[attr.field] = this.formatAttributeValue(attributeValues[attr.name] ?? ''); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
return tableRow; |
|
|
return tableRow; |
|
|
@@ -1520,10 +1533,10 @@ window.jobTable = function () { |
|
|
{ title: 'Job Type', field: 'job_type_name', minWidth: 160, headerFilter: 'input' }, |
|
|
{ title: 'Job Type', field: 'job_type_name', minWidth: 160, headerFilter: 'input' }, |
|
|
]; |
|
|
]; |
|
|
|
|
|
|
|
|
attributes.forEach((attr, index) => { |
|
|
|
|
|
|
|
|
attributes.forEach((attr) => { |
|
|
columns.push({ |
|
|
columns.push({ |
|
|
title: attr.name, |
|
|
title: attr.name, |
|
|
field: 'attr_' + index, |
|
|
|
|
|
|
|
|
field: attr.field, |
|
|
minWidth: 150, |
|
|
minWidth: 150, |
|
|
headerFilter: 'input', |
|
|
headerFilter: 'input', |
|
|
formatter: function (cell) { |
|
|
formatter: function (cell) { |
|
|
|