Source: mixins/table.js

/**
 * @desc A mixin object containing methods for table initalization and action invocations
 * @module TableMixin
 */
import { exportFile } from 'quasar';
import { TableExportMixin } from '../mixins/table-export.js';
import { TableCustomMixin } from './table-custom.js';
export const TableMixin = {
    props: {
        options: null,
    },
    mixins: [TableExportMixin, TableCustomMixin],
    data() {
        return {
            title: null,
            selection: "none",
            selectedRows: [],
            pagination: { rowsPerPage: 0 },
            rows: [],
            columns: [],
            visibleColumns: [],
            from: null,
            to: null,
            filter: {},
            filter2: {},
            showFilter: false,
            frugal: false,
            tableAPI : null,
            inEdit: false,
            editMode: null,
            editingRow: null,
            editingRowIndex: null,
            dbFunction: null,
            lookups: {},
            filterSet: false,
            key: "id",
            rowActions: null,
            tableActions: null,
            columns: [],
            visibleColumns: [],
            colAtts: {},
            alignment: { "double precision": "right", "integer": "right", "boolean": "center", "number":"right" },
            compare: { "double precision": "interval", "integer": "interval", "date" : "interval", "number":"interval" },
            format: {
                "date": val => this.formatDate(val),
                "timestamp with time zone": val => this.formatDate(val),
                "numeric": val => val ? val.toFixed(2) : null,
            },
            changedRows: {},    
            colWidths: {
                "integer": '60px', "character varying": '150px', "text": '200px', "json": '200px', "double precision": '100px', "boolean": '50px', "number": '100px'
            },
            details: null,
            showDetails: false,
            current: null,
            parent: null,
            parentKey: null,
            parentName: null,
            standardEditing: null,
            backButton: false,
            selection: "none",
            loaded: false,
            params: null,
            contextValuesLocal: [],
            data: [],
            exportPreprocess: null,
            noInlineEditing: false,
            grid: false,
            lookupsLoaded: false,
        }
    },
    methods: {
        async reload() {
            if (this.contextValuesLocal.length > 0) {           
                this.params = {};
                for (let cv of this.contextValuesLocal) {
                    this.params[cv.name] = cv.value;
                    this.$store.contextValues[cv.name] = cv.value;
                }
            }
        
            if (this.dbFunction) {
                this.data = await this.get("Table/GetTable", {
                    dbFunction: this.dbFunction,
                    frugal: this.frugal.toString(),
                    json: this.json.toString(),
                    pars: JSON.stringify(this.params) ?? "{}",
                    preprocess: this.preprocess ?? null
                });
            } else if (this.restAPI) {
                let api = this.restAPI;
                for (let key in this.params) {
                    api = api + "/" + this.params[key];
                }
                this.data = await this.get(api);
            } else if (this.tableAPI ) {
                this.frugal = true;
                if (this.params) {
                    this.data = await this.get("Table/" + this.tableAPI, { pars: JSON.stringify(this.params) });
                } else {
                    this.data = await this.get("Table/" + this.tableAPI);
                }
            } 

            if (!this.data) {
                this.rows = [];
                this.columns = [];
                return;
            }
            // set up the tableAPI 
            let attributes = [];
            if (this.frugal) { 
                attributes = this.data.attributes;
                this.key = '0';
            } else {
                if (this.data.length > 0) {
                    attributes = Object.keys(this.data[0]).map((name, index) => { return { name: name }; });
                    // determine attribute types
                    this.data.forEach(obj => {
                        let i = 0;
                        for (let key in obj) {
                            if (obj[key]) {
                                const currentType = typeof obj[key];
                                if (!attributes[i].type) {
                                    attributes[i].type = currentType;
                                } else if (attributes[i].type !== currentType) {
                                    attributes[i].type = 'string';
                                }
                            }
                            i++;
                        }
                    });
                }
                this.key = attributes[0].name;
            }

            if (this.parent) {
                let index = attributes.findIndex(att => att.name == this.parent.key);
                this.data.data = this.data.data.filter(row => row[index] == this.parent.value);
            }

            // set up the columns
            this.columns = attributes.map((attribute, index) => {
                let format = this.format[attribute.type] ?? function (val) { return val };
                return {
                    name: attribute.name,
                    label: attribute.name,
                    field: this.frugal ? row => row[index] : attribute.name,
                    sortable: true,
                    format: val => format(val),
                    align: this.alignment[attribute.type] ?? 'left',
                    index: this.frugal ? index : attribute.name,
                    type: attribute.type,
                    compare: this.compare[attribute.type] ?? 'string',
                    //width: this.calcWidth(attribute.type),
                }
            });

            // chemistry for lookup fields (id in popup, id_val in table)
            if (this.tableAPI ) {
                this.visibleColumns = [];
                this.swapIdAndValColumns(this.columns);
                for (let col of this.columns) {
                    if (this.parent && (col.name == this.parent.key || col.name == this.parent.key + '_val')) continue;
                    if (col.name.endsWith('_id')) {
                        col.label = col.label.slice(0, -3);
                    } else if (col.name.endsWith('_id_val')) {
                        col.label = col.label.slice(0, -7);
                    }
                }
            } 

            for (let col of this.columns) {
                // snake case to readable label
                col.label = col.label.replaceAll(/_/g, ' ');
                col.label = col.label.charAt(0).toUpperCase() + col.label.slice(1);
                if (this.colAtts[col.name]) {
                    this.copyObject(this.colAtts[col.name], col, true);
                }

                if (col.disabled) {
                    let valCol = this.columns.find(c => c.name == col.name + '_val');
                    if (valCol) valCol.disabled = true;
                }

                if (col.name.endsWith("_id")) {
                    col.lookup = { name: col.name.slice(0, -3), default: true };
                }
                if (col.name.endsWith("_id_val")) {
                    col.lookup = { name: col.name.slice(0, -7), default: true };
                }

                if (col.name.endsWith('_id') && !col.visible
                    || col.invisible
                    || this.parent != null && (col.name == this.parent.key || col.name == this.parent.key + '_val')) continue;
                
                if (col.name == 'id' || col.name == 'time_created' || col.name == 'time_modified' || col.name == 'user_modified') col.disabled = true;

                if (col.name != 'id') this.visibleColumns.push(col.name);

            }

            this.clearFilter();
            this.rows = this.frugal ? this.data.data : this.data;

        },

        /**
         * Initializes the tableAPI  component.
         */
        async init() {
            // set default values (needed because component is reused)
            this.$store.formChanged = false;    
            this.title = null;
            this.frugal = false;
            this.json = false;
            this.tableAPI  = null;
            this.restAPI = null;
            this.dbFunction = null;
            this.rowActions = null;
            this.tableActions = null;
            this.lookups = {};
            this.params = null;
            this.filter = {};
            this.filter2 = {};
            this.rows = [];
            this.selectedRows = [];
            this.changedRows = {};
            this.colAtts = {};
            this.details = null;
            this.current = null;
            this.parentKey = null;
            this.parentName = null;
            this.backButton = false;
            this.selection = "none";
            this.columns = [];
            this.visibleColumns = [];
            this.loaded = false;
            this.contextValuesLocal = [];
            this.contextValues = [] ;
            this.data = [];
            this.exportPreprocess = null;
            this.noInlineEditing = false;
            this.lookupsLoaded = false;

            this.grid = this.$q.screen.width <= 800;

            if (this.options) {
                this.copyObject(this.options, this, true);
            } else {
                this.copyObject(this.$store.props[this.$route.name], this, true);
            }
            
            // copy context values so that action is not changed
            if (this.contextValues) {
                this.copyArray(this.contextValues, this.contextValuesLocal);
            }
    
            if (this.title) this.title = this.$t(this.title);
            this.standardEditing = this.tableAPI && !this.dbFunction;

            if (this.contextValuesLocal) {
                for (let cv of this.contextValuesLocal) {
                    cv.options = await this.get("Table/GetParamLookup/" + cv.lookup);
                    if (cv.options && cv.options.length > 0) {
                        // get first key in first row
                        cv.optionValue = Object.keys(cv.options[0])[0];
                        cv.optionLabel = Object.keys(cv.options[0])[1];
                        cv.value = cv.options[0][cv.optionValue];
                    }
                };
            }

            // get data from the server
            await this.reload();
            this.loaded = true; 
        },

        /**
         * Swaps the id and value columns for lookup fields.
         * @param {*} columns 
         */
        swapIdAndValColumns(columns) {
            let swapped = [];
            for (let i = 0; i < columns.length; i++) {
                if (columns[i].name.endsWith('_id')) {
                    let name = columns[i].name.slice(0, -3);
                    if (!swapped.includes(name)) {
                        for (let j = 0; j < columns.length; j++) {
                            if (columns[j].name == name + '_id_val') {
                                swapped.push(name);
                                let temp = columns[i];
                                columns[i] = columns[j];
                                columns[j] = temp;
                                break;
                            }
                        }
                    }
                }
            }
        },

        /**
         * Wraps a value in a CSV cell.
         * @param {*} val - The value to be wrapped.
         * @param {*} formatFn - The function to format the value.
         * @param {*} row - The row containing the value.
         * @returns {string} The wrapped value.
        */
        wrapCsvValue (val, formatFn, row) {
            let formatted = formatFn !== void 0
                ? formatFn(val, row)
                : val
            formatted = formatted === void 0 || formatted === null ? '' : String(formatted)
            formatted = formatted.split('"').join('""')
            return `"${formatted}"`
        },
        
        /**
         * Creates content for export based on the provided export rows and columns.
         *
         * @param {Array} exportRows - The rows to be exported.
         * @param {Array} columns - The columns to be exported.
         * @returns {string} The content for export.
         */
        createContentForExport(exportRows, columns) {
            const content = [columns.map(col => this.wrapCsvValue(col.label.replace(/<[^>]*>/g, '')))].concat(
                exportRows.map(row => columns.map(col => this.wrapCsvValue(
                    typeof col.field === 'function'
                        ? col.field(row)
                        : row[col.field === void 0 ? col.name : col.field],
                    col.format,
                    row
                )).join(','))
            ).join('\r\n');
            return content;
        },	

        /**
         * Exports the tableAPI  to a CSV file.
         */
        exportTable() {
            let exportRows;

            if (this.selectedRows.length > 0) {
                exportRows = this.selectedRows;
            } else {
                exportRows = this.$refs.table.filteredSortedRows;
            }
            if (!exportRows || exportRows.length === 0) return;

            let content;

            if (this.exportPreprocess) {
                content = this[this.exportPreprocess].call(this, exportRows, this.columns)
            } else {
                content = this.createContentForExport(exportRows, this.columns);
            }

            const bom = '\uFEFF';
            const status = exportFile(
                this.$route.name + '.csv',
                bom + content,
                'text/csv'
            )

            if (status !== true) {
                $q.notify({
                    message: this.$t('Browser denied file download...'),
                    color: 'negative',
                    icon: 'warning'
                })
            }
                
        },

        /**
         * Initializes the properties for custom component in popup.
         * @param {string} action - The action to be performed.
         * @param {Object} rowToPass - The row object to be passed to the popup.
         * @param {Array} rows - The array of rows in the table.
         */
        initPopupProps(action, rowToPass) {
            this.$store.popup.props = this.deepClone(action);
            this.replaceVariables(this.$store.popup.props, rowToPass);
            this.$store.popup.props.rows = this.rows;
            this.$store.popup.props.selectedRows = this.selectedRows;
            this.$store.popup.props.parent = this;
            this.$store.popup.props.row = rowToPass;
            this.$store.popup.props.columns = this.columns;
            this.$store.popup.props.editingRow = rowToPass;
            this.$store.popup.props.editingRowIndex = this.editingRowIndex;
        },

        /**
         * Replaces moustache variables in an object with corresponding values from row.
         * 
         * @param {Object} obj - The object containing variables to be replaced.
         * @param {Object} rowToPass - The object containing values to replace the variables.
         */
        replaceVariables(obj, rowToPass) {
            // console.log ("replace", this.contextValuesLocal)
            for (let keyO in obj) {
                if (typeof obj[keyO] == "string" && obj[keyO].includes("{{")) {
                    for (let keyR in rowToPass) {
                        obj[keyO] = obj[keyO].replaceAll("{{" + keyR + "}}", rowToPass[keyR]);
                    }
                    for (let keyG in this.$store.globalValues) {
                        obj[keyO] = obj[keyO].replaceAll("{{store.globalValues." + keyG + "}}", this.$store.globalValues[keyG]);
                    }
                    for (let cv of this.contextValuesLocal) {
                        obj[keyO] = obj[keyO].replaceAll("{{store.contextValues." + cv.name + "}}", this.$store.contextValues[cv.name]);
                    }
                } else if (typeof obj[keyO] == "object") {
                    this.replaceVariables(obj[keyO], rowToPass);
                }
                if (keyO == "store.globalValues") {
                    this.copyObject(obj[keyO], this.$store.globalValues, true);    
                }
            }
        },
        
        /**
         * Prepares and activates a route based on the provided action, row, and rows.
         *
         * @param {Object} action - The action object containing route information.
         * @param {Object} row - The row object.
         * @param {Array} rows - The array of rows.
         * @returns {void}
         */
        prepareRoute(action, rowToPass, rows) {
            // activate a route
            let routerRoute = this.$store.routes.find((item) => item.path == action.route);
            let route = {};
            if (!routerRoute) {
                route = { name: action.route, component_name: action.componentName, path: action.route };
                route.props = this.deepClone(action);
            } else {
                route = this.deepClone(routerRoute);
            }

            this.replaceVariables(route.props, rowToPass);

            route.props.row = rowToPass;
            route.props.rows = rows;
            route.props.backButton = true;
            this.activateRoute(route);
        },


        /**
         * Deletes a row from the array in store, if necessary.
         * @param {Object} row - The row to be deleted.
         * @returns {void}
         */
         deleteInStore(row, action) {
            if (action.deleteInStore) {
                let index = this.$store[action.deleteInStore].findIndex(r => r.id == row.id);
                this.$store[action.deleteInStore].splice(index, 1);
            }
        },
        /**
         * Runs a row action.
         * @param {Object} action - Action to be run.
         * @param {Object} row - The row to be acted upon.
         */
        async runRowAction(action, row) { 
         
            this.editingRowIndex = this.rows.indexOf(row);

            if (action.confirmationMessage) {
                if (!(await this.confirmDialog(action.confirmationMessage))) {
                    return;
                }
            }

            let rowToPass = {};
            if (this.frugal) {
                rowToPass = this.rowToObject(row);
            } else {
                rowToPass = row;
            }

            if (action.route) {
                this.prepareRoute(action, rowToPass, this.rows);
            } else if (action.customFunction) {
                this[action.customFunction](rowToPass);
            } else if (action.restAPI) {
                let a = this.deepClone(action);
                this.replaceVariables(a, rowToPass);
                let url = a.restAPI;

                let ret = this.api(this.axios.API[a.method ?? "get"], url, a.params);
                if (ret != null) {

                    if (a.method == "delete" && !a.noRowDelete) {
                        this.rows.splice(this.editingRowIndex, 1);
                        this.deleteInStore(row, action);
                    }
                }
            } else if (action.delete) {
                this.tableAPI = action.tableAPI;
                this.deleteRow(row, action.confirmationMessage);
                this.deleteInStore(row, action);
            } else if (action.clone) {
                await this.post("Table/Clone/" + action.clone + "/" + rowToPass[action.key]);
                await this.showMessage(this.$t("Record cloned. Table will be refreshed."));
                await this.reload();
            } else {
                // activate a component
                this.initPopupProps(action, rowToPass); 
                this.$store.popup.component = action.component;
                this.$store.popup.show = true;
            }
        },

        /**
         * Runs a tableAPI  action (custom function).
         * @param {Object} action - Action to be run.
         */
        async runTableAction(action) {

            let rows;

            if (this.selection == "multiple") {
                rows = this.selectedRows;
                if (action.mustSelectRows && rows.length == 0) {
                    this.showMessage(this.$t("Please select rows!"));
                    return;
                }
            } else {
                rows = this.rows;
            }

            if (action.confirmationMessage) {
                if (!await this.confirmDialog(action.confirmationMessage)) {
                    return;
                }
            }

            if (action.customFunction) {
                this[action.customFunction](rows);
            } else if (action.restAPI) {
                let keys = rows.map(row => row[action.keyForKeys ?? this.key]);
                let a = this.deepClone(action);
                this.replaceVariables(a, {});
                let ret = await this.api(this.axios.API[action.method ?? "get"], action.restAPI, { keys: keys, ...a.params });
                if (ret && ret.taskId) {
                    this.$store.progress.props = { taskId: ret.taskId, min: 0, max: ret.count, title: a.title, message: a.message, parent: this, action: action };
                    this.$store.progress.show = true;
                }
            } else if (action.chart) {
                this.$store.chart.props = this.deepClone(action.chart);
                //this.copyObject(action.chart, this.$store.chart.props);
                let rowsToChart = rows;
                let actionRetrievesData = action.chart.dbFunction || action.chart.restAPI;
                if (action.chart.dbFunction) {
                    console.log("chart.dbf", action.chart.dbFunction);
                    rowsToChart = await this.get("Table/GetTable", {
                        dbFunction: action.chart.dbFunction,
                        frugal: action.chart.frugal ? action.chart.frugal.toString() : "false",
                        json: action.chart.json ? action.chart.json.toString() : "false",
                        pars: JSON.stringify(action.chart.params) ?? "{}"
                    });
                } else if (action.chart.restAPI) {
                    rowsToChart = await this.get(action.chart.restAPI);
                } else {
                    rowsToChart = rows;
                }
                if (rows) {
                    if (action.chart.preprocess) {
                        this.$store.chart.props.data = this[action.chart.preprocess].call(this, rowsToChart);
                    } else if (this.frugal && !actionRetrievesData || actionRetrievesData && action.chart.frugal) {
                        this.$store.chart.props.data = rowsToChart.map(row => this.rowToObject(row));
                    } else {
                        this.$store.chart.props.data = rowsToChart;
                    }
                    this.$store.chart.show = true;
                }
            } else if (action.route) {
                this.prepareRoute(action, null, action.passRows ? rows : null);
            } else if (action.component) {
                this.initPopupProps(action, null);
                this.$store.popup.component = action.component;
                this.$store.popup.show = true;
            }

            if (action.reload) {
                await this.reload();
            }
        },

        /**
         * Calculates the width of a column based on its type.
         * @param {string} type - The type of the column.
         * @returns {string} The width of the column.
         */
        calcWidth(type) {
            return this.colWidths[type] ?? '100px';
        },

        /**
         * Shows the row info in a popup.
         * @param {Object} row - The row to show info for.
         */
        showRowInfo(row) {
            let rowToShow = row;
            if (this.frugal) {
                rowToShow = this.rowToObject(row);
            }
            this.$store.popup.props = {
                columns: this.columns,
                rowToShow: rowToShow,
            };
            this.$store.popup.component = 'row-info';
            this.$store.popup.show = true;
        },

        
        

    }
}