import loglevel from 'loglevel'; import { CURRENCY, ComponentModel, getDBManager, structure, toNumber } from './cykLang' import { Ref, onUnmounted, ref } from 'vue'; import { BasicType, DBColumn, DBColumns, DBRemote, Data, Expression, FunctionData, ObjectData, ObjectDataType, PrimitiveData, Scope, Tag, Variable, Variables, XmlError, data2boolean, variable2json, } from '@cyklang/core'; import { componentModelParameter, NamedComponent } from './cykReact'; import { AlertException } from './cykRun'; import { useI18n } from 'vue-i18n'; import { RowObject } from './cykTableEdit'; const logger = loglevel.getLogger('TableViewComponent.vue'); logger.setLevel('debug'); /** * * @param queryFunction * @param scope * @returns */ async function callQuery( queryFunction: FunctionData, scope: Scope ): Promise { let result: ObjectData; try { const params = new Variables(); const data = await queryFunction.callFunction(params, scope); if (data === undefined) throw 'query return undefined'; if (data.type.isPrimitive()) throw 'query return ' + data.type.name + ' instead of object'; result = data as ObjectData; } catch (err) { logger.error(err); throw err; } return result; } /** * * @param props * @returns */ export function useCykTableView(props: { componentArg: ComponentModel | undefined }) { // logger.debug('cykTableView.begin') const isLoading = ref(true); const template = ref('dialog'); let dbNotifEventSource: EventSource | undefined const visible = componentModelParameter(props.componentArg, "visible", true) const nbLines = ref(0); const runQuery = async () => { try { if (dbNotifEventSource) { dbNotifEventSource.close() dbNotifEventSource = undefined } if ( props.componentArg === undefined || props.componentArg.model === undefined || props.componentArg.objectData === undefined ) { throw Error('TableView componentArg undefined'); } await props.componentArg.interpolateAttributes() const reload_on_table_update = props.componentArg.objectData.tag.attributes['RELOAD-ON-TABLE-UPDATE'] !== undefined; // if (reload_on_table_update) { // logger.debug('reload_on_table_update TRUE !') // } // else { // logger.debug('reload_on_table_update FALSE', props.componentArg.objectData.tag.attributes) // } if ( props.componentArg.objectData.tag.attributes.TEMPLATE === 'expand' ) { template.value = 'expand'; } if ( props.componentArg.model.data === null || props.componentArg.model.data === undefined ) throw ' model.data undefined !!!'; if (props.componentArg.model.data.type.isPrimitive()) throw ' model must be an object'; const varQuery = ( props.componentArg.model.data as ObjectData ).variables.getVariable('query'); const varOptionEntity = ( props.componentArg.model.data as ObjectData ).variables.getVariable('entity'); if ( varQuery !== undefined && varQuery.data !== undefined && varQuery.data !== null ) { if (varQuery.data.type.name !== 'function') throw ( 'query is ' + varQuery.data.type.name + ' instead of function' ); const dbResult = await callQuery( varQuery.data as FunctionData, props.componentArg.objectData.scope ); let vardbResult = ( props.componentArg.model.data as ObjectData ).variables.getVariable('dbresult'); if (vardbResult === undefined) { vardbResult = ( props.componentArg.model.data as ObjectData ).variables.addVariable( 'dbresult', props.componentArg.model.dataType ); } vardbResult.data = dbResult; const optionEntity = varOptionEntity?.getString() || 'resultset'; if (optionEntity === undefined) { throw 'entity option value undefined'; } const objEntity = dbResult.variables.getVariable(optionEntity); if (objEntity === undefined) { throw ( 'entity member with name ' + optionEntity + ' not found in the query result' ); } if ( objEntity.data === undefined || objEntity.data === null || objEntity.data.type.isPrimitive() === true ) { throw ( 'entity option is ' + optionEntity + ' and result member with this name should be an objet but is ' + objEntity.dataType.name ); } nbLines.value = (objEntity.data as ObjectData).variables.length(); if (nbLines.value > 0) { const typeName = (objEntity.data as ObjectData).variables.at(0)?.variable.data?.type?.name if (typeName?.startsWith('t__') && reload_on_table_update) { const tableName = typeName.substring(3) let origin = '' const dbDriver = (await getDBManager()).dbDriver if (dbDriver instanceof DBRemote) { const dbRemote = dbDriver as DBRemote origin = dbRemote.serverURL; } const url = `${origin}/api/dbnotif/${tableName}` logger.debug(`cykTableView.runQuery.url: ${url}`) dbNotifEventSource = new EventSource(url, { withCredentials: true }) dbNotifEventSource.onmessage = () => { logger.debug(`dbnotif table ${tableName} `) reload() } dbNotifEventSource.onerror = (err) => { logger.debug('cykTableView error SSE', err) dbNotifEventSource?.close() } } } // logger.debug( // '------- query function called and dbresult has been set. nbLines = ' + // nbLines.value // ); } } catch (err) { // AlertException(err) AlertException(new XmlError(String(err), props.componentArg?.objectData?.tag || new Tag(''))) } }; runQuery().finally(() => { isLoading.value = false; }); // ().finally(() => { // isLoading.value = false; // }); const reload = () => { if (isLoading.value) return isLoading.value = true; runQuery().finally(() => { isLoading.value = false }); }; let namedComponent: NamedComponent; if (props.componentArg?.objectData?.tag.attributes.NAME !== undefined) { namedComponent = new NamedComponent( props.componentArg?.objectData.tag.attributes.NAME, (event: string) => { if (event === 'reload') { reload(); } else { throw Error( 'TableView ' + props.componentArg?.objectData?.tag.attributes.NAME + ' has no event named ' + event ); } } ); try { props.componentArg.variableReact?.addNamedComponent(namedComponent); } catch (err) { AlertException(err) } } onUnmounted(() => { if ( props.componentArg?.variableReact !== undefined && namedComponent !== undefined ) { props.componentArg.variableReact.removeNamedComponent(namedComponent); } props.componentArg?.objectData?.destroy() }); return { isLoading, nbLines, visible, reload, template }; } /** * * @param message * @param tag */ function showTagError(message: string, tag: Tag) { AlertException(new XmlError(message, tag)) } /** * */ // interface Column { // name: string; // label: string | undefined; // type: string | undefined; // dbColumn: DBColumn; // computed: string | undefined; // } /** * * @param dbResult * @param optEntity * @param optColumns * @param optionsObject * @param optCommands * @returns */ export async function buildQColumns( template: 'dialog' | 'expand', dbResult: ObjectData, optEntity: string, optColumns: ObjectData | ObjectDataType | undefined, optionsObject: ObjectData, optCommands: ObjectData, rowKeyName: Ref, qColumns: any[] ): Promise<{ dbColumns: DBColumns, columns: DBColumn[] }> { const dbManager = await getDBManager() rowKeyName.value = '' const columns: DBColumn[] = [] try { const metaObject = dbResult.variables.getData('meta') as ObjectData; const typeObject = metaObject.variables.getData(optEntity) if (!typeObject || typeObject === null) throw 'meta ' + optEntity + ' not found' if (typeObject.type.name !== 'type') { logger.debug('typeObject: ', typeObject) throw "meta " + optEntity + " type: " + typeObject.type.name } const objType = typeObject as ObjectDataType let dbColumns = objType.dbColumns if (!objType.dbColumns) throw 'objType.dbColumns undefined' if (dbColumns === undefined) throw 'dbColumns undefined' rowKeyName.value = dbColumns.key || '' // expandable column if (template === 'expand' && optCommands.variables.length() > 1) qColumns.push({ name: 'expand', label: ' ', }); // if optColumns is absent display all columns of dbTable if (optColumns !== undefined) { if (optColumns instanceof ObjectDataType) { if (!optColumns.dbColumns) throw 'optColumns.dbColumns undefined' for (let ind = 0; ind < optColumns.dbColumns.columns.length; ind++) { let visible = true const dbColumn = optColumns.dbColumns.columns.at(ind) const functionData = dbColumn?.params?.getFunction('visible') if (functionData) { const data = await functionData.callFunction(new Variables(), structure.scope) if (data === null || data === undefined) { visible = false; } else if (data.type.isPrimitive()) { visible = Boolean((data as PrimitiveData).value) } } if (dbColumn && visible) { columns.push(dbColumn) } } // optColumns.dbColumns.columns.forEach((dbColumn) => { // columns.push(dbColumn); // }); } else { logger.error('optColumns', optColumns) throw 'optColumns should be a ' } } else { // display all columns of dbTable dbColumns.columns.forEach((dbColumn) => { if (!dbColumn.name.endsWith('__meta')) columns.push(dbColumn); }); } // build qColumns for (let ind = 0; ind < columns.length; ind++) { const dbColumn = columns[ind] const qColumn: any = {}; qColumn.name = dbColumn.name; qColumn.label = structure.scope.xlate(dbColumn.label ? dbColumn.label : dbColumn.name); qColumn.type = dbColumn.component; qColumn.sortable = true; // qColumn.sort = (a, b, rowA, rowB) => { // switch (dbColumn?.dataTypeName) { // case 'string': // break; // case 'number': // break; // case 'datetime': // break; // } // }; // if (dbColumn?.parentTable) { // await dbManager.addKVTable(dbColumn.parentTable) // } /** * qColumn.field: function that returns text to display in the table cell * @param record : ObjectData * @returns */ qColumn.field = (rowObject: RowObject) => { return qColumnField(dbColumn, rowObject) } qColumn.fieldVariable = (rowObject: RowObject) => { let result; if (qColumn.name !== '__actions__') { const variable = rowObject.objectData.variables.getVariable(qColumn.name); if (variable === undefined) logger.debug('field ' + qColumn.name + ' not found in the record'); result = variable } return result; }; // qColumn.format = dbColumn.format qColumn.align = dbColumn?.dataTypeName === 'number' ? 'right' : 'left'; qColumn.style = dbColumn.style qColumn.dbColumn = dbColumn; qColumns.push(qColumn); }; // logger.debug('before buildQColumns return', dbColumns.toString()) return { dbColumns, columns }; } catch (err) { logger.error(err) const excep = new XmlError('buildQColumns() : ' + String(err), optionsObject.tag) AlertException(excep) throw excep } } /** * * @param dbColumn * @param variable */ function qColumnField(dbColumn: DBColumn, rowObject: RowObject): any { let result: BasicType if (dbColumn.name === '__actions__') { result = rowObject.objectData.objectDataType.dbColumns?.keyPack(rowObject.objectData.variables.json()) } else { const variable = rowObject.objectData.variables.getVariable(dbColumn.name) result = displayVariable(dbColumn.dataTypeName, dbColumn.format, variable) } return result } /** * * @param dbColumn * @param variable * @returns */ export function displayVariable(typeName: string, format: string | undefined, variable: Variable | undefined): string { // const typeName = dbColumn.dataTypeName // const format = dbColumn.format let result = "" if (!variable || !variable.data || variable.data === null) return result const data = variable.data if (data.type.isPrimitive()) { const value = (data as PrimitiveData).value result = String(value) switch (typeName) { case 'string': if (format === 'date') { if (value && value !== null) { result = structure.scope.formatDate(value as string) } // logger.debug('dbColumn', dbColumn, 'formatDate: ' + result) } else { result = structure.scope.xlate(value as string) } break; case 'number': const numericValue = toNumber(value); if (numericValue !== undefined) { if (format === 'money') { result = new Intl.NumberFormat(undefined, { style: "currency", currency: CURRENCY }).format(numericValue) } else if (format === 'number_fixed;2') { result = new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2}).format(numericValue) } else { result = new Intl.NumberFormat(undefined).format(numericValue) } } else { result = '' } break; case 'datetime': if (value && value !== null) { result = structure.scope.formatDatetime(value as Date) } // logger.debug('dbColumn', dbColumn, 'formatDatetime: ' + result) break; case 'boolean': break; default: logger.debug(`displayVariable unsupported dataType: ${typeName}`) } } else if (data.type.isObject()) { result = JSON.stringify(variable2json(variable)) } else { logger.debug('qColumnField unsupported dataType: ' + data.type.name) } return result } /** * * @param records * @param dbResult * @param optEntity * @param optionsObject */ export function loadRecords( rowObjects: Ref, dbResult: ObjectData, optEntity: string, optionsObject: ObjectData ) { const startTime = Date.now(); try { rowObjects.value.length = 0; const dbRecordset = dbResult.variables.getData(optEntity) as ObjectData; if (dbRecordset === null) { const xmlError = new XmlError('optEntity ' + optEntity + ' not found in query result', optionsObject.tag) AlertException(xmlError) } else { for (let ind = 0; ind < dbRecordset.variables.length(); ind++) { const variable = dbRecordset.variables.at(ind)?.variable if (!variable) continue const data = variable.data; if (data === null || data === undefined) { const excep = new XmlError('recordset with an empty line', optionsObject.tag); AlertException(excep) throw excep } const record = data as ObjectData const rowObject = new RowObject(record) // await completeRecord(record, dbColumns) rowObjects.value.push(rowObject) }; } } catch (err) { AlertException(err) } const endTime = Date.now(); const durationInSeconds = (endTime - startTime) / 1000; // logger.debug(`loadRecords exécutée en ${durationInSeconds.toFixed(3)} secondes`); } /** * * @param record * @param dbTable */ export async function completeRecord(record: ObjectData, dbColumns: DBColumns) { for (let ind = 0; ind < dbColumns.columns.length; ind++) { const dbColumn = dbColumns.columns[ind] let variable = record.variables.getVariable(dbColumn.name) if (variable === undefined) { const dataType = await structure.scope.getDataType(dbColumn.dataTypeName) if (!dataType) throw 'dbColumn.dataTypeName: ' + dbColumn.dataTypeName + ' not found' logger.debug(`completeRecord() missing field : ${dbColumn.name}`) variable = new Variable(dataType, null) record.variables.push(dbColumn.name, variable) } } } /** * */ export interface Command { name: string; icon: string; label: string; function: string | undefined; result: string | undefined } /** * * @param variable * @returns */ function buildCommand(variable: Variable): Command { if (variable.data === null || variable.data === undefined) throw 'optCommand without data' let result: Command; if (variable.data.type.isObject()) { const objData = variable.data as ObjectData const name = objData.tag.attributes.NAME || '' const icon = objData.tag.attributes.ICON || name const label = objData.tag.attributes.LABEL || name const action = objData.tag.attributes.ACTION result = { name: name, icon: icon, label: structure.scope.xlate(label), function: action, result: undefined } } else throw 'buildCommand: unexpected datatype ' + variable.data.type.name return result } /** * * @param optTableCommands * @param optionsObject * @returns */ export function buildTableCommands( optTableCommands: ObjectData | undefined, optionsObject: ObjectData, tableCommands: Command[] ): Command[] { try { if (optTableCommands !== undefined) { optTableCommands.variables.forEach(({ variable }) => { const command = buildCommand(variable) tableCommands.push(command); }); } } catch (err) { if (err instanceof XmlError) AlertException(err) else AlertException(new XmlError(String(err), optionsObject.tag)) } return tableCommands; } /** * * @param optCommands * @param optionsObject * @returns */ export function buildCommands(optCommands: ObjectData, optionsObject: ObjectData, commands: Command[]) { try { if (optCommands === undefined) { commands.push({ name: 'edit', icon: 'edit', label: structure.scope.xlate('edit'), function: undefined, result: undefined, }); } else { optCommands.variables.forEach(({ variable }) => { const command = buildCommand(variable) commands.push(command); }); } return commands; } catch (err) { if (err instanceof XmlError) AlertException(err) else AlertException(new XmlError(String(err), optionsObject.tag)) } } /** * * @param columns * @param rowObjects */ export async function calComputedCols(columns: DBColumn[], rowObjects: Ref) { const startTime = Date.now(); try { let message = ''; const promises: Promise[] = []; for (let indi = 0; indi < columns.length; indi++) { const dbColumn = columns[indi]; if (!dbColumn.computed && !dbColumn.params?.getFunction('compute')) continue; for (let indj = 0; indj < rowObjects.value.length; indj++) { const processRecord = async (): Promise => { try { const record = rowObjects.value[indj].objectData; let data: Data | null | undefined const functionData = dbColumn.params?.getFunction('compute') if (functionData) { const callParams = new Variables() const varRow = new Variable(record.type, record) callParams.push('row', varRow) data = await functionData.callFunction(callParams, structure.scope) } else if (dbColumn.computed) { const express = new Expression(record.scope); data = await express.evaluate(dbColumn.computed); } else { throw 'computed attribute and compute function parameter are undefined' } let variable: Variable | undefined if (data === undefined || data === null) { variable = new Variable( record.scope.structure.undefinedDataType, undefined ); } else { variable = new Variable(data.type, data); } const varExist = record.variables.getVariable(dbColumn.name); if (varExist === undefined) { record.variables.push(dbColumn.name, variable); } else { varExist.data = data; } } catch (err) { if ( ! message ) { message = String(err) logger.error(err) } } } promises.push(processRecord()) } } await Promise.allSettled(promises) if (message) { throw message } } catch (err) { throw err } finally { const endTime = Date.now(); const durationInSeconds = (endTime - startTime) / 1000; // logger.debug(`calComputedCols exécutée en ${durationInSeconds.toFixed(3)} secondes`); } } /** * * @param commandFunction * @param scope * @param rowSelected * @returns */ export async function callCommandFunction( commandFunction: string, scope: Scope, rowSelected: RowObject | undefined ): Promise { if (commandFunction === '__reload__') return true; // const varFunction = scope.lookupVariable(commandFunction); const varFunction = await new Expression(scope).LValue(commandFunction); if (varFunction === undefined) throw 'function variable ' + commandFunction + ' not found'; if (varFunction.data === undefined || varFunction.data === null) throw 'function variable ' + commandFunction + ' with no data'; if (varFunction.data.type.name !== 'function') throw ( 'function variable ' + commandFunction + ' data type ' + varFunction.data.type.name ); // logger.debug('function ' + commandFunction + ' FOUND'); const functData = varFunction.data as FunctionData; const params = new Variables(); if (rowSelected !== undefined) { const parametersDefinition = functData.parametersDefinition; if (parametersDefinition === undefined) throw ( 'function ' + commandFunction + ' has no argument and should take 1 : the row selected' ); // if (parametersDefinition.length() > 1) // throw ( // 'function ' + // commandFunction + // ' has ' + // parametersDefinition.length() + // 'argument and should take only 1 : the row selected' // ); const parameterDefinition = parametersDefinition.array[0]; if (parameterDefinition.dataType.isPrimitive()) throw ( 'function ' + commandFunction + ' parameter type is ' + parameterDefinition.dataType.name + ' and should be object ' ); const varParam = params.addVariable( parameterDefinition.name, parameterDefinition.dataType ); varParam.data = rowSelected.objectData; } const dataReturned = await functData.callFunction(params, scope); return data2boolean(dataReturned); // logger.debug('=== COMMAND FUNCTION ' + commandFunction + ' FINISHED '); }