diff --git a/lib/AppContext.js b/lib/AppContext.js index 2ec6a630..05e37390 100644 --- a/lib/AppContext.js +++ b/lib/AppContext.js @@ -332,75 +332,93 @@ export default class AppContext { if (locales === null) { locales = this.getLocaleData(template); } - + const defaultLocale = { langcode: 'default', nativeName: 'Default', }; - + // Combine default and found locales locales = { ...locales, default: defaultLocale, ...findLocalesForLangcodes(template.locales), }; - + // Helper function to process permissible values into a translation map const getEnumResource = (enumObject) => { - return consolidate(enumObject, (acc, [enumKey, enumObj]) => { - const { permissible_values } = enumObj || {}; + return consolidate(enumObject, (acc, [, enumObj]) => { + const { permissible_values } = enumObj || {}; if (permissible_values) { - Object.entries(permissible_values).forEach(([enumKey, enumData]) => { + Object.entries(permissible_values).forEach(([, enumData]) => { const { title, text } = enumData; const key = text; // Enum keys are the 'text' const value = title || text; // Use title if available, fallback to text - acc[key] = value; // Forward translation (text -> title) - acc[value] = key; // Reverse translation (title -> text) + acc[key] = value; // Forward translation (text -> title) + acc[value] = key; // Reverse translation (title -> text) }); } return acc; }); }; - + // Build translation maps for each language const translationsByLanguage = {}; - + Object.entries(template.translations).forEach(([langcode, translation]) => { const currentLang = langcode.split('-')[0]; // Use primary language code - + // Compute the enum resource for this language const enumResource = getEnumResource(translation.schema.enums); - + // Store the translations for this language translationsByLanguage[currentLang] = { ...enumResource, }; - - console.info("Computed translation resources for:", currentLang, enumResource); + + console.info( + 'Computed translation resources for:', + currentLang, + enumResource + ); }); - + // Generate translation maps between all possible language combinations const languageCodes = Object.keys(translationsByLanguage); - + // Loop over each source-destination language pair languageCodes.forEach((sourceLang) => { languageCodes.forEach((targetLang) => { if (sourceLang === targetLang) return; // Skip identical language pairs - + const sourceTranslation = translationsByLanguage[sourceLang]; - const targetTranslation = translationsByLanguage[targetLang]; - + // const targetTranslation = translationsByLanguage[targetLang]; + // Add resources for the current source-target language pair const translationNamespace = `${sourceLang}_to_${targetLang}`; - i18n.addResources(sourceLang, translationNamespace, sourceTranslation); - + i18n.addResources( + sourceLang, + translationNamespace, + removeNumericKeys(sourceTranslation) + ); + // Add reverse mapping for this pair (target -> source) const reverseTranslationMap = invert(sourceTranslation); const reverseNamespace = `${targetLang}_to_${sourceLang}`; - i18n.addResources(targetLang, reverseNamespace, reverseTranslationMap); - - console.info(`Added translation resources from ${sourceLang} to ${targetLang}:`, translationNamespace); - console.info(`Added reverse translation resources from ${targetLang} to ${sourceLang}:`, reverseNamespace); + i18n.addResources( + targetLang, + reverseNamespace, + removeNumericKeys(reverseTranslationMap) + ); + + console.info( + `Added translation resources from ${sourceLang} to ${targetLang}:`, + translationNamespace + ); + console.info( + `Added reverse translation resources from ${targetLang} to ${sourceLang}:`, + reverseNamespace + ); }); }); } @@ -557,7 +575,6 @@ export default class AppContext { */ - // this.clearContext(); return this.initializeTemplate(this.appConfig.template_path, { forced_schema, }).then(async (context) => { diff --git a/lib/DataHarmonizer.js b/lib/DataHarmonizer.js index 79e5a6a4..c4acd163 100644 --- a/lib/DataHarmonizer.js +++ b/lib/DataHarmonizer.js @@ -552,8 +552,7 @@ class DataHarmonizer { let dataObjects = jsonData.Container[container_class]; list_data = this.loadDataObjects(dataObjects); this.hot.loadData(list_data); - } - + } } else { // assume tabular data if not a JSON datatype list_data = this.loadSpreadsheetData(contentBuffer.binary); @@ -1705,6 +1704,16 @@ class DataHarmonizer { fillColumn(colname, value) { const fieldYCoordinates = this.getFieldYCoordinates(); + const fields = this.getFields(); + + console.log( + colname, + value, + fieldYCoordinates, + this.getFieldNameMap(fields), + this.getFieldTitleMap(fields) + ); + // ENSURE colname hasn't been tampered with (the autocomplete allows // other text) if (colname in fieldYCoordinates) { diff --git a/lib/Toolbar.js b/lib/Toolbar.js index c7d7ebf4..43b98db1 100644 --- a/lib/Toolbar.js +++ b/lib/Toolbar.js @@ -70,19 +70,20 @@ class Toolbar { } translationSelect.on('input', async () => { - const previousLocale = i18next.language; const language_update = translationSelect.val() !== 'en' ? translationSelect.val() : 'default'; i18next.changeLanguage(language_update); const targetLocale = findBestLocaleMatch(this.context.template.locales, [ - i18next.language + i18next.language, ]); - - const supportsLocale = - targetLocale !== null; - const translationNamespace = previousLocale != targetLocale ? `${previousLocale}_to_${targetLocale}` : 'translation'; + + const supportsLocale = targetLocale !== null; + const translationNamespace = + previousLocale != targetLocale + ? `${previousLocale}_to_${targetLocale}` + : 'translation'; const findTranslatableFields = (data_harmonizer_fields) => { // what counts as translatable? @@ -105,27 +106,45 @@ class Toolbar { return is_translatable; }; - const translateData = (isTranslatable, dataHarmonizerData, namespace='translation') => { + const translateData = ( + isTranslatable, + dataHarmonizerData, + namespace = 'translation' + ) => { // Helper function to translate a single value based on the current language and namespace const translateValue = (value) => { - console.log("executing translateValue for", value, i18next.options.resources); - console.log(value, previousLocale, targetLocale, i18next.t(value, { lng: i18next.language, ns: 'reverse' }), i18next.t(value, { lng: i18next.language, ns: 'translation' })); + console.log( + 'executing translateValue for', + value, + i18next.options.resources + ); + console.log( + value, + previousLocale, + targetLocale, + i18next.t(value, { lng: i18next.language, ns: 'reverse' }), + i18next.t(value, { lng: i18next.language, ns: 'translation' }) + ); // const namespace = i18next.language === 'default' ? 'reverse' : 'translation'; return i18next.t(value, { lng: previousLocale, ns: namespace }); }; - + // Helper function to translate multiple values if they are delimited const translateMultivalued = (multivaluedString) => { const values = multivaluedString.split(MULTIVALUED_DELIMITER); const translatedValues = values.map((value) => translateValue(value)); return translatedValues.join(MULTIVALUED_DELIMITER); }; - + // Main function logic to iterate over the data return dataHarmonizerData.map((row, row_index) => { return row.map((value, index) => { - if (value != null && value != "" && isTranslatable[index]) { - console.log(`translating ${value} at ${index} (${this.context.getCurrentDataHarmonizer().hot.getDataAtCell(row_index, index)})`) + if (value != null && value != '' && isTranslatable[index]) { + console.log( + `translating ${value} at ${index} (${this.context + .getCurrentDataHarmonizer() + .hot.getDataAtCell(row_index, index)})` + ); } // Only translate if the value exists and it's marked as translatable if (value && isTranslatable[index]) { @@ -136,13 +155,13 @@ class Toolbar { return translateValue(value); // Translate single value } } - + // If the value is not translatable or doesn't exist, return it as-is return value; }); }); }; - + if (supportsLocale) { try { // first cache data @@ -152,7 +171,11 @@ class Toolbar { const data = this.context.dhs[dh].hot.getData(); const fields = this.context.dhs[dh].fields; const translatableFields = findTranslatableFields(fields); - _dh_data_cache[dh] = translateData(translatableFields, data, translationNamespace); + _dh_data_cache[dh] = translateData( + translatableFields, + data, + translationNamespace + ); } // reload the context with the new locale await this.context @@ -174,7 +197,6 @@ class Toolbar { this.setupFillModal(context.dhs[dh]); } }); - } catch (error) { if (error instanceof LocaleNotSupportedError) { console.warn(error); @@ -362,9 +384,9 @@ class Toolbar { i18next.changeLanguage( in_language === 'en' ? 'default' : in_language ); - translationSelect.val( - i18next.language === 'en' ? 'default' : in_language - ).trigger("input"); + translationSelect + .val(i18next.language === 'en' ? 'default' : in_language) + .trigger('input'); $(document).localize(); } @@ -384,7 +406,7 @@ class Toolbar { 'Error: JSON data file does not have Container dictionary.' ); } - + // NOTE: the data is possibly *sparse*. where does it make sense to fill in its missing alues? // It doesn't appear to matter @@ -515,23 +537,30 @@ class Toolbar { }; if (ext === 'json') { - const fields = this.context.getCurrentDataHarmonizer().getFields(); - const slotNamesForTitles = this.context.getCurrentDataHarmonizer().getFieldTitleMap(fields); + const slotNamesForTitles = this.context + .getCurrentDataHarmonizer() + .getFieldTitleMap(fields); + + const filterEmptyKeys = (obj, keyCallback = (id) => id) => + Object.keys(obj).reduce((acc, itemKey) => { + return itemKey in obj && !(itemKey in acc) && obj[itemKey] != '' + ? { + ...acc, + [keyCallback(itemKey)]: obj[itemKey], + } + : acc; + }, {}); - const filterEmptyKeys = (obj, keyCallback = id => id) => Object.keys(obj).reduce((acc, itemKey) => { - return itemKey in obj && !(itemKey in acc) && (obj[itemKey] != "") ? { - ...acc, - [keyCallback(itemKey)]: obj[itemKey], - } : acc; - }, {}); - - const filterEmptyKeysForObjectsInList = lst => lst.map(el => filterEmptyKeys(el, em => slotNamesForTitles[em])); + const filterEmptyKeysForObjectsInList = (lst) => + lst.map((el) => filterEmptyKeys(el, (em) => slotNamesForTitles[em])); for (let concept in JSONFormat.Container) { - JSONFormat.Container[concept] = filterEmptyKeysForObjectsInList(JSONFormat.Container[concept]); - }; - + JSONFormat.Container[concept] = filterEmptyKeysForObjectsInList( + JSONFormat.Container[concept] + ); + } + await this.context.runBehindLoadingScreen(exportJsonFile, [ JSONFormat, baseName, @@ -862,14 +891,14 @@ class Toolbar { )) { translationSelect.append(new Option(nativeName, langcode)); } - console.log("currentTranslationVal", currentTranslationVal); + console.log('currentTranslationVal', currentTranslationVal); if (currentTranslationVal) { translationSelect.val(currentTranslationVal); - translationSelect.trigger("input"); + translationSelect.trigger('input'); } else { // translationSelect.val('en'); } - + const helpSop = $('#help_sop'); const template_classes = this.context.template.schema.classes[template_name]; @@ -886,55 +915,90 @@ class Toolbar { } setupFillModal(dh) { - const fillColumnInput = $('#fill-column-input').selectize({ + const fillColumnInput = $('#fill-column-input').empty(); + + // Initialize the selectize input field for column selection + fillColumnInput.selectize({ valueField: 'title', labelField: 'title', searchField: ['title'], openOnFocus: true, }); - // clear options before providing them to update when active data harmonizer changes - fillColumnInput[0].selectize.clearOptions(); - fillColumnInput[0].selectize.addOption(dh.getFields()); - - const fillValueInput = $('#fill-value-input'); + // Set up the modal opening event to clear and refresh the options dynamically $('#fill-modal').on('shown.bs.modal', () => { - fillColumnInput[0].selectize.open(); + // Ensure the selectize input is fully cleared before loading new options + const selectizeInstance = fillColumnInput[0].selectize; + selectizeInstance.clear(); // Clear the current selection + selectizeInstance.clearOptions(); // Clear all options + selectizeInstance.clearCache(); // Clear the cache for proper updating + + // Add new options from the data harmonizer + selectizeInstance.addOption(dh.getFields()); + + // Reset the value input field + const fillValueInput = $('#fill-value-input'); + fillValueInput.val(''); // Clear any previously entered values + + // Open the selectize dropdown + selectizeInstance.open(); }); + + // Set up the click event for the fill button $('#fill-button').on('click', () => { - dh.fillColumn(fillColumnInput.val(), fillValueInput.val()); - fillColumnInput[0].selectize.clear(); - fillValueInput.val(''); + const column = fillColumnInput.val(); + const value = $('#fill-value-input').val(); + + // Trigger the fillColumn function with the selected column and value + dh.fillColumn(column, value); + + // Hide the modal after filling the column $('#fill-modal').modal('hide'); }); } setupJumpToModal(dh) { - console.log('running jump to modal', dh.class_assignment); const columnCoordinates = dh.getColumnCoordinates(); - let jumpToInput = $('#jump-to-input') - .selectize({ - openOnFocus: true, - }) + // Initialize and reset the jump-to input field + let jumpToInput = $('#jump-to-input').empty(); + jumpToInput.selectize({ + openOnFocus: true, + }); - // clear options before providing them to update when active data harmonizer changes - jumpToInput[0].selectize.clearOptions(); - jumpToInput[0].selectize.addOption(Object.keys(columnCoordinates).map(col => ({ - text: col, - value: col, - }))); + // Set up the modal opening event to clear and refresh the options dynamically + $('#jump-to-modal').on('shown.bs.modal', () => { + const selectizeInstance = jumpToInput[0].selectize; + + // Ensure the selectize input is fully cleared before loading new options + selectizeInstance.clear(); // Clear the current selection + selectizeInstance.clearOptions(); // Clear all options + selectizeInstance.clearCache(); // Clear the cache for proper updating + + // Add new options using the columnCoordinates + selectizeInstance.addOption( + Object.keys(columnCoordinates).map((col) => ({ + text: col, + value: col, + })) + ); + + // Open the selectize dropdown + selectizeInstance.open(); + }); + + // Set up the change event for handling the jump-to functionality + $('#jump-to-input').on('change', (e) => { + if (!e.target.value) return; // If no value is selected, do nothing - $('#jump-to-modal').on('change', (e) => { - if (!e.target.value) return; const columnX = columnCoordinates[e.target.value]; + + // Scroll to the selected column position dh.scrollTo(0, columnX); + + // Hide the modal after the selection is made $('#jump-to-modal').modal('hide'); }); - - $('#jump-to-modal').on('shown.bs.modal', () => { - jumpToInput[0].selectize.open(); - }); } setupSectionMenu(dh) { diff --git a/lib/utils/1m.js b/lib/utils/1m.js index d0eea9ea..0f51d464 100644 --- a/lib/utils/1m.js +++ b/lib/utils/1m.js @@ -615,8 +615,8 @@ Details can be fetched by listener via appContext[className].unique_keys[keyName // NOTE: Handsontable doesn't have a arbitrary listeners like the DOM. Hooks are fired through methods on individual tables. // We emulate a listener by creating a new event manager instead instead. // This is also necessary to ensure the listeners have a lifecycle where they can be removed off the DOM properly. -// Since the action handlers are parameterized by appContext/data harmonizers, their uniqueness of their closures prevents referencing them by their function symbol. -// The event tracker stores the listener in a map so we can guarantee their deletion. +// Since the action handlers are parameterized by appContext/data harmonizers, the uniqueness of their closures prevents referencing them by their function symbol. +// The event tracker stores the listeners in a map so we can guarantee their deletion. class EventTracker { constructor() { this.listeners = new Map(); @@ -631,16 +631,16 @@ class EventTracker { if (!this.listeners.has(eventName)) { this.listeners.set(eventName, new Set()); } - + // Create a bound handler if context is provided const boundHandler = context ? handler.bind(context) : handler; - + // Store reference to original and bound handler for removal this.boundHandlers.set(handler, { bound: boundHandler, - context: context + context: context, }); - + this.listeners.get(eventName).add(boundHandler); document.addEventListener(eventName, boundHandler); } @@ -703,7 +703,7 @@ export class OneToManyEventTracker extends EventTracker { dh.hot.addHook('afterSelection', (row, column, row2, column2) => { // Determine if the selection spans multiple rows const multiRowSelected = row !== row2; - + if (multiRowSelected) { /* 1.a: Multi-row select exception If a user has marked out a multi row selection in parent table, this should trigger a table key = empty Read event. @@ -714,7 +714,7 @@ export class OneToManyEventTracker extends EventTracker { console.log( `Multi-row selection detected from row ${row} to ${row2}. Emitting empty Read event.` ); - + // Emit an empty "Read" event to indicate no specific key selection this.dispatchHandsontableUpdate({ action: ACTION.READ, @@ -732,10 +732,10 @@ export class OneToManyEventTracker extends EventTracker { If a user clicks on any part of a tab / table row, if that table has one or more primary keys, they yield primary key events that all subordinate tables/classes can listen to. This is the “Read” event. */ - + // Single-row selection handling const [min, max] = [column, column2]; // Determine the column range of the selection - + // Identify unique keys that may be affected by this selection const maybe_unique_keys = uniqueKeysInIndexRange( appContext, @@ -743,7 +743,7 @@ export class OneToManyEventTracker extends EventTracker { min, max ); - + console.info( `there are n=${maybe_unique_keys.length} keys in range: ${maybe_unique_keys}` ); @@ -759,34 +759,38 @@ export class OneToManyEventTracker extends EventTracker { row, column, key_values: - appContext[dh.class_assignment].unique_keys[key_name].key_values, // Send the key values to filter by + appContext[dh.class_assignment].unique_keys[key_name] + .key_values, // Send the key values to filter by }); } } } }); - }; - + } + bindParentBroadcastsChange() { const nodes = Object.keys(this.appContext); nodes.forEach((nodeName) => { const node = this.appContext[nodeName]; - for (const [uniqueKeyName, uniqueKey] of Object.entries(node.unique_keys)) { + for (const [, uniqueKey] of Object.entries(node.unique_keys)) { if (uniqueKey.foreign_key) { const { foreign_key_class, foreign_key_slot } = uniqueKey; const dh = this.dhs[foreign_key_class]; dh.hot.addHook('afterChange', (changes, source) => { if (source === 'loadData') return; - + if (changes) { const rowChanges = changesToRows(changes); - const columnIndex = dh.getColumnIndexByFieldName(foreign_key_slot); - + const columnIndex = + dh.getColumnIndexByFieldName(foreign_key_slot); + for (const [row, change] of Object.entries(rowChanges)) { const updateActionType = Object.values(change.newValues) .map(isEmptyUnitVal) - .includes(true) ? ACTION.DELETE : ACTION.UPDATE; + .includes(true) + ? ACTION.DELETE + : ACTION.UPDATE; const detail = { action: updateActionType, @@ -803,7 +807,10 @@ export class OneToManyEventTracker extends EventTracker { this.dispatchHandsontableUpdate(detail); - if (findActiveDataHarmonizer(this.appContext) === detail.emitted_by) { + if ( + findActiveDataHarmonizer(this.appContext) === + detail.emitted_by + ) { detail.action = ACTION.SELECT; this.dispatchHandsontableUpdate(detail); } @@ -815,7 +822,7 @@ export class OneToManyEventTracker extends EventTracker { }); } - dispatchChildHooks (unique_key, action, details) { + dispatchChildHooks(unique_key, action, details) { if ('child_classes' in unique_key) { for (let child_class of unique_key.child_classes) { this.dispatchHandsontableUpdate({ @@ -825,8 +832,8 @@ export class OneToManyEventTracker extends EventTracker { }); } } - }; - + } + routeAction(appContext, dhs, action, event) { const { emitted_by, target, key_name } = event.detail; const unique_key = appContext[emitted_by]?.unique_keys[key_name]; @@ -860,7 +867,7 @@ export class OneToManyEventTracker extends EventTracker { // FILTER, UPDATE, DELETE handleAction(appContext, dhs, dhs[target], action, event.detail); } - }; + } bindChildListensAndHandlesChange() { const actionHandler = (event) => { @@ -890,7 +897,7 @@ export class OneToManyEventTracker extends EventTracker { this.bindParentBroadcastsChange(); this.bindChildListensAndHandlesChange(); - + return this; } } @@ -928,4 +935,4 @@ const makeColumnsReadOnly = (appContext, dh) => { export const setup1M = ({ appContext }, dhs) => { const eventTracker = new OneToManyEventTracker(appContext, dhs); return eventTracker.setup1M(); -}; \ No newline at end of file +}; diff --git a/lib/utils/files.js b/lib/utils/files.js index 1d4b7134..1acaa928 100644 --- a/lib/utils/files.js +++ b/lib/utils/files.js @@ -306,7 +306,7 @@ export function importJsonFile(jsonData) { // TODO: this required fields problem is only when loading DH files, not DH templates // Loader is doubled up? // Check if the object has all required fields - const requiredFields = ['schema', 'version', 'in_language', 'Container']; + const requiredFields = []; // ['schema', 'version', 'in_language', 'Container']; const requiredFieldTest = (field) => typeof data[field] !== 'undefined'; const requiredFieldsFilterArray = requiredFields.map(requiredFieldTest); if (!requiredFieldsFilterArray.every((id) => id)) {