JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: components.dt/js/utils/TableColumns.js

define([
    'components/js/utils/Currency',
    'components.dt/js/api/ComponentProviderRegistry',
    'components.dt/js/api/constants/Style',
    'components.dt/js/utils/ActionUtils',
    'components.dt/js/utils/TableColumn',
    'entity/js/api/Property',
    'entity/js/api/PropertyType',
    'pagedesigner.dt/js/api/grid/DeleteSupport',
    'pages.dt/js/ViewFactory',
    'pages.dt/js/api/Navigation',
    'pages.dt/js/api/View',
    'pages.dt/js/api/ViewType',
    'translations/js/api/I18n',
    'translations/js/api/Translatable'
], function (
        Currency,
        ComponentProviderRegistry,
        Style,
        ActionUtils,
        TableColumn,
        Property,
        PropertyType,
        deleteSupport,
        ViewFactory,
        navigation,
        View,
        ViewType,
        I18n,
        Translatable
        ) {

    'use strict';

    /*
     * =========================================================================
     * Column
     * =========================================================================
     */
    /**
     * @constructor
     * @private
     *
     * @param {Property} property
     * @param {View} view
     */
    var Column = function (property, view) {
        this._property = property;
        this._view = view;
    };

    Column.prototype.getProperty = function () {
        return this._property;
    };

    Column.prototype.getView = function () {
        return this._view;
    };

    Column.prototype.getDisplayName = function () {
        var custom = this.getView().getProperties().getTranslatableValue(TableColumn.TITLE);
        return custom || this.getProperty().getName();
    };

    /**
     * Get expression for getting translated display name of the column.
     *
     * @returns {string} JS expression that evaluates to translated column name.
     */
    Column.prototype.getDisplayNameTranslatable = function () {
        var custom = this.getView().getProperties().getValue(TableColumn.TITLE);
        return custom || this.getProperty().getNameAsTranslatable();
    };

    Column.prototype.getComplexName = function () {
        var orig = this.getProperty().getName();
        var custom = this.getView().getProperties().getValue(TableColumn.TITLE);
        if (custom && custom.toString() !== orig.toString()) {
            return orig + ' (' + custom + ')';
        } else {
            return orig;
        }
    };

    Column.prototype.getType = function () {
        return this.getProperty().getType();
    };

    Column.prototype.getPropertyId = function () {
        return this.getProperty().getId();
    };

    Column.prototype.getColumnId = function () {
        return this.getPropertyId();
    };

    Column.prototype.getRenderingProps = function () {
        var props = this.getView().getProperties();
        var allKeys = props.getKeys();
        var nonRenderingKeys = ['__silentProperties', View.IS_WRAPPABLE_KEY,
            View.IS_DRAGGABLE_KEY, View.IS_DROPPABLE_KEY,
            View.IS_HORIZONTALLY_RESIZABLE_KEY, View.IS_BEING_DELETED,
            View.IS_VERTICALLY_RESIZABLE_KEY, View.IS_RERENDER_PARENT,
            TableColumn.TITLE, TableColumn.FIELD_ID, TableColumn.WIDTH_TYPE,
            TableColumn.WIDTH_VALUE_PERCENT, TableColumn.WIDTH_VALUE_PX,
            TableColumn.LONG_VALUES_TYPE, ActionUtils.PROPS_METHODS];
        var renderingKeys = allKeys.filter(function (key) {
            return nonRenderingKeys.indexOf(key) === -1;
        });
        var res = {};
        renderingKeys.forEach(function (key) {
            res[key] = props.getValue(key);
        });
        return res;
    };

    /**
     * Create a new column for specified property.
     *
     * @param {Property} property
     * @param {View} view
     *
     * @returns {Column}
     */
    Column.create = function (property, view) {
        AbcsLib.checkParameterCount(arguments, 2);
        AbcsLib.checkParametersType(arguments, [Property, View]);
        return new Column(property, view);
    };

    /*
     * =========================================================================
     * InvalidProperty
     * =========================================================================
     */
    /**
     *
     * @param {String} id
     * @private
     * @constructor
     */
    var InvalidProperty = function (id) {
        this._id = id;
    };

    InvalidProperty.prototype.getId = function () {
        return this._id;
    };

    InvalidProperty.prototype.isReference = function () {
        return false;
    };

    InvalidProperty.prototype.getType = function () {
        return '';
    };

    /*
     * =========================================================================
     * InvalidColumn
     * =========================================================================
     */
    var InvalidColumnTranslatable = function (id) {
        this.id = id;
    };
    AbcsLib.extend(InvalidColumnTranslatable, Translatable);

    InvalidColumnTranslatable.prototype.toTranslatableString = function () {
        return 'AbcsLib.i18n(\'components.columnPropertyNotFound\', {id: \'' + this.id + '\'})';
    };

    /**
     * @constructor
     * @private
     *
     * @param {InvalidProperty} invalidProperty
     */
    var InvalidColumn = function (invalidProperty) {
        this._invalidProperty = invalidProperty;
        this.invalid = true;
    };

    InvalidColumn.prototype.getDisplayName = function () {
        return this._invalidProperty.getId();
    };
    InvalidColumn.prototype.getComplexName =
            InvalidColumn.prototype.getDisplayName;

    InvalidColumn.prototype.getType = function () {
        return 'invalid';
    };

    InvalidColumn.prototype.getProperty = function () {
        return this._invalidProperty;
    };

    InvalidColumn.prototype.getPropertyId = function () {
        return this._invalidProperty.getId();
    };

    InvalidColumn.prototype.getColumnId = function () {
        return this.getPropertyId();
    };

    InvalidColumn.prototype.getRenderingProps = function () {
        return {
            flags: {
                invalid: true
            }
        };
    };

    InvalidColumn.prototype.getDisplayNameTranslatable = function () {
        return new InvalidColumnTranslatable(this._invalidProperty.getId());
    };

    InvalidColumn.prototype.getView = function () {
        return null;
    };

    /*
     * =========================================================================
     * NewColumn
     * =========================================================================
     */
    /**
     * @constructor
     * @private
     *
     * @param {Property} property
     */
    var NewColumn = function (property) {
        this._property = property;
        this._view = null;
    };
    AbcsLib.extend(NewColumn, Column);

     NewColumn.prototype.getDisplayName = function () {
        return this.getProperty().getName();
    };

    /**
     * Get expression for getting translated display name of the column.
     *
     * @returns {string} JS expression that evaluates to translated column name.
     */
    NewColumn.prototype.getDisplayNameTranslatable = function () {
        return this.getProperty().getNameAsTranslatable();
    };

    NewColumn.prototype.getComplexName = function () {
        return this.getProperty().getName();
    };

    NewColumn.prototype.getRenderingProps = function () {
        return {};
    };

    /*
     * =========================================================================
     * TableColumns
     * =========================================================================
     */
    /**
     * @constructor
     * @private
     *
     * @param {Boolean} newTable - Mark that the table is being configured.
     * @param {View} view - Table view.
     * @param {Entity} entity

     */
    var TableColumns = function (newTable, view, entity) {
        this._newTable = newTable;
        this._view = view;
        this._columns = ko.observableArray();
        this._viewListener = undefined;
        this._listeningOffLevel = 0;
        this._entity = entity;
        this._suppressChangeNotifications = 0;

        // Read-only, exposed, observable array of columns
        this.columns = ko.pureComputed(function () {
            return this._columns();
        }.bind(this));

        // Read-only, exposed, observable array of assigned entity properties
        this.selectedProperties = ko.pureComputed(function () {
            return this._getSelectedProperties();
        }.bind(this));

        if (!newTable || entity) {
            this._reload();
        }
    };

    /**
     * Basically reverse operation to the {@link TableColumns.formatColumns} method.
     *
     * <p>
     * It takes a JSON file containing columns in JET table format and parse them to get a list
     * of properties.
     * </p>
     *
     * @param {JSON} columnsJSON
     * @param {Entity} entity
     * @param {function} [invalidCreator] Function that creates placeholders for
     *                                    invalid properties. It will be passed
     *                                    id of each invalid property.
     *
     * @returns {Array[]} array of {@link Property properties}
     */
    TableColumns.parseColumnsJSON = function (columnsJSON, entity, invalidCreator) {
        columnsJSON = columnsJSON && columnsJSON.replace(/\'/g, '\"') || '[]';

        var visibleColumns = [];
        JSON.parse(columnsJSON).forEach(function(column) {
            var id = column.field;
            if (id) {
                var property = entity.getProperty(id);
                if (property) {
                    visibleColumns.push(property);
                } else if (AbcsLib.isDefined(invalidCreator)) {
                    visibleColumns.push(invalidCreator(id));
                }
            }
        });
        return visibleColumns;
    };

    /**
     * Formats the given properties to a JSON compliant with the JET table.
     *
     * @param {Property[]} properties
     * @returns {String}
     */
    TableColumns.formatColumns = function (properties) {
        var visibleProperties = [];
        properties.forEach(function(property) {
            var desc = {
                field: property.getId()
            };
            visibleProperties.push(desc);
        });

        // Note: This is awkward. The array should be stored as is. If needed,
        // it should be escaped in template using escaping mark objectJS, or at
        // least should be escaped using StringUtils.escapeObjectJS() and
        // returned. But keeping this for backward compatibility with already
        // created table views. Fortunately, field ids cannot contain special
        // characters.

        var result = JSON.stringify(visibleProperties);

        // Replace all double quotes for single once, because JET is shitty and can't handle both
        result = result.replace(/\"/g, '\'');
        return result;
    };

    /**
     * Similar to TableColumns.parseColumnsJSON but returns list of invalid (non-existent) fields.
     * @param {String} columnsJSON
     * @param {Entity} entity
     * @returns {Array} array of invalid field ids.
     */
    TableColumns.getInvalidColumns = function (columnsJSON, entity) {
        columnsJSON = columnsJSON && columnsJSON.replace(/\'/g, '\"') || '[]';

        var invalidColumns = [];
        JSON.parse(columnsJSON).forEach(function(column) {
            var id = column.field;
            if (id) {
                if (!entity.getProperty(id)) {
                    invalidColumns.push(id);
                }
            }
        });
        return invalidColumns;
    };

    TableColumns.prototype._getEntity = function () {
        if (this._entity) {
            return this._entity;
        }
        var view = this._view;
        var archetype = view.getEnclosingArchetype();
        var entity = archetype.getEntity();
        return entity;
    };

    /**
     * Reload column definitions for the view.
     *
     * @returns {undefined}
     */
    TableColumns.prototype._reload = function () {
        var entity = this._getEntity();
        var columnViews = this._getColumnChildren();
        this._loadColumnsFromChildViews(entity, columnViews);
    };

    /**
     * Create column view for a property.
     *
     * @param {Property} prop
     * @param {Page} [pageOpt] - Page, optional. If not specified, active page
     *                           will be used.
     *
     * @returns {View} View of type table-column.
     */
    TableColumns.prototype._createViewForProperty = function (prop, pageOpt) {
        var page = pageOpt || navigation.getActivePage();
        var vf = page.getViewFactory();
        var viewDefinition = {
            id: this.getTableView().getId() + '-col-' + prop.getId() + '-0',
            type: ViewType.TABLE_COLUMN,
            displayName: 'Column'
        };
        var v = vf.createView(viewDefinition);
        var p = v.getProperties();
        var i18n = I18n.key(I18n.Type.Page, page.getId(), this.getTopTableContainerView().getId(), 'columns', v.getId());
        p.addTranslationSupport(i18n);
        p.setValue(TableColumn.FIELD_ID, prop.getId());
        p.setTranslatableValue(TableColumn.TITLE, prop.getName());

        // default column alignment
        if (!p.getValue(Style.TEXT_ALIGNMENT)) {
            var defaultAlign = Style.getDefaultAlignment(prop.getType());
            p.setValue(Style.TEXT_ALIGNMENT, defaultAlign);
        }
        this._setDefaultFormattingProperties(prop.getType(), p);

        var propertyCreator;
        switch (prop.getType()) {
            case PropertyType.URL:
                propertyCreator = ComponentProviderRegistry.findCreator(ViewType.INPUT_FIELD_URL);
                break;
            case PropertyType.EMAIL:
                propertyCreator = ComponentProviderRegistry.findCreator(ViewType.INPUT_FIELD_EMAIL);
                break;
            case PropertyType.PHONE:
                propertyCreator = ComponentProviderRegistry.findCreator(ViewType.INPUT_FIELD_PHONE);
                break;
            default: // nothing
        }

        if (propertyCreator && AbcsLib.isFunction(propertyCreator.createDefaultAction)) {
            var archetype = this.getTopTableContainerView().getEnclosingArchetype();
            propertyCreator.createDefaultAction(page, archetype, v, false);
        }

        return v;
    };

    TableColumns.prototype._setDefaultFormattingProperties = function (propertyType, properties) {
        if (propertyType === PropertyType.NUMBER) {
            properties.setValue('useGrouping', true);
        } else if (propertyType === PropertyType.CURRENCY) {
            properties.setValue('currencyCode', Currency.DEFAULT_CURRENCY_CODE);
            properties.setValue('fractionDigits', 2);
        } else if (propertyType === PropertyType.PERCENTAGE) {
            properties.setValue('maximumFractionDigits', 3);
        }
    };

    /**
     * Load column definitions from child views.
     *
     * @param {Entity} entity
     * @param {Array} columnViews - Array of child views.
     */
    TableColumns.prototype._loadColumnsFromChildViews = function (entity, columnViews) {
        var cols = columnViews.map(function (childView) {
            var p = childView.getProperties();
            var fieldId = p.getValue(TableColumn.FIELD_ID);
            var property = fieldId && entity.getProperty(fieldId);
            if (property) {
                return new Column(property, childView);
            } else {
                var invalidProperty = new InvalidProperty(fieldId || '?');
                return new InvalidColumn(invalidProperty);
            }
        });
        this._columns(cols);
    };

    TableColumns.prototype._getColumnChildren = function () {
        var tableView = this.getTableView();
        return tableView.getChildren().filter(function (childView) {
            return childView.getType() === ViewType.TABLE_COLUMN;
        });
    };

    /**
     * Create invalid property for incorrect or missing property id.
     *
     * @param {string} id - Property id.
     *
     * @returns {InvalidProperty}
     */
    TableColumns.prototype._createInvalidProperty = function (id) {
        return new InvalidProperty(id);
    };

    /**
     * Change order of one of columns.
     *
     * @param {number} fromIndex - Original column index.
     * @param {number} toIndex - New column index.
     */
    TableColumns.prototype.reorderColumn = function (fromIndex, toIndex) {
        var cols = this.columns().slice();
        var len = cols.length;
        if (fromIndex < len && toIndex < len && fromIndex >= 0 && toIndex >= 0) {
            var movedCol = cols.splice(fromIndex, 1)[0];
            cols.splice(toIndex, 0, movedCol);
            var tableView = this.getTableView();
            var movedChild = tableView.getChildren()[fromIndex];
            if (movedChild) {
                tableView.moveChild(movedChild, toIndex);
            }
            // Fire change event in order to update the view correctly.
            // (Without this, no data are displayed in the table.)
            this._view.fireEvent(ViewFactory.EVENT_VIEW_CHANGED, this._view);
            this._columns([]);
            this._columns(cols);
        }
        return Promise.resolve();
    };

    /**
     * Add a new column.
     *
     * @param {object} propertyOrId - Id of property.
     * @param {number} [index] - Index of new column.
     * @param {page} [page] - Page to which the table belongs. Optional, if not
     *                        specified, active page will be used.
     */
    TableColumns.prototype.addColumn = function (propertyOrId, index, page) {
        var self = this;
        var property = TableColumns._getProperty(propertyOrId, self._getEntity());
        var newView = self._createViewForProperty(property, page);
        var tableView = self.getTableView();
        var column = Column.create(property, newView);
        if (AbcsLib.hasValue(index)) {
            this._columns.splice(index, 0, column);
        } else {
            this._columns.push(column);
        }
        tableView.addChild(newView, index);
        this.notifyChanged();
        return Promise.resolve();
    };

    /*
     * Get property from an argument which may be a property directly or a
     * property id.
     */
    TableColumns._getProperty = function (propertyOrId, entity) {
        if (propertyOrId instanceof Property) {
            return propertyOrId;
        } else if (AbcsLib.isString(propertyOrId)) {
           return entity.getProperty(propertyOrId);
        } else {
            throw new Error('Expected property or property id.');
        }
    };

    /**
     * Remove one of columns.
     *
     * @param {number} index - Column index.
     */
    TableColumns.prototype.removeColumn = function (index) {
        var cols = this.columns();
        if (cols.length > index && index >= 0) {
            var removed = cols.splice(index, 1);
            if (removed.length) {
                return this._removeView(removed[0].getView()).then(function () {
                    return this.notifyChanged();
                }.bind(this));
            }
        }
        return Promise.resolve();
    };

    /**
     * Dispose this TableColumns instance. Remove all listeners and release
     * all other allocated resources.
     */
    TableColumns.prototype.dispose = function () {
        var view = this._view;
        if (this._bindingListener) {
            var binding = view.getBinding();
            binding.removeListener(this._bindingListener);
        }
    };

    /**
     * Compare list of properties with list of column views that migh represent
     * the same list of visible table columns.
     *
     * @param {Array} properties - Array of properties.
     * @param {Array} views - Array of vies of type table-column.
     *
     * @returns True if properties and views represent the same list of
     * visible table columns, false otherwise.
     */
    TableColumns.prototype._comparePropsAndViews = function (properties, views) {
        if (properties.length !== views.length) {
            return false;
        }
        for (var i = 0; i < views.length; i++) {
            var v = views[i];
            var p = properties[i];
            var viewFieldId = v.getProperties().getValue(TableColumn.FIELD_ID);
            var propertyId = p.getId();
            if (!viewFieldId || !propertyId || viewFieldId !== propertyId) {
                return false;
            }
        }
        return true;
    };

    TableColumns.prototype._getSelectedProperties = function () {
        return this._columns().map(function (column) {
            return column.getProperty();
        }).filter(function (prop) {
            return !TableColumns.isInvalidProperty(prop);
        }).filter(function (prop, index, array) {
            return array.indexOf(prop) === index; // unique only
        });
    };

    /**
     * Perform some operation with listeners disabled.
     *
     * @param {function} fn Function to run without listening.
     */
    TableColumns.prototype.withoutListeners = function (fn) {
        this._listeningOffLevel += 1;
        try {
            return fn.call(this);
        } finally {
            this._listeningOffLevel -= 1;
        }
    };

    TableColumns.prototype.getTableView = function () {
        return this._view.findChildOfType(ViewType.TABLE);
    };

    TableColumns.prototype.getTopTableContainerView = function () {
        return this._view;
    };

    TableColumns.prototype.getColumnView = function (columnIndex) {
        return TableColumns._getColumnView(this.columns(), columnIndex);
    };

    TableColumns._getColumnView = function (columns, columnIndex) {
        var result = null;
        if (columnIndex < columns.length && columnIndex >= 0) {
            result = columns[columnIndex].getView();
        }
        return result;
    };

    /**
     * Remove a table column view.
     *
     * @param {View} view
     */
    TableColumns.prototype._removeView = function (view) {
        return deleteSupport.deleteView(view).then(function () {
            var entity = this._getEntity();
            var children = this._getColumnChildren();
            this._loadColumnsFromChildViews(entity, children);
        }.bind(this));
    };

    /**
     * Called from table wizard if entity is changed.
     *
     * @param {Entity} entity
     */
    TableColumns.prototype.reset = function (entity) {
        this._entity = entity;
        var columns = this._columns().slice();
        columns.forEach(function (col) {
            this._removeView(col.getView());
        }.bind(this));
        this._columns([]);
    };

    TableColumns.prototype.notifyChanged = function () {
        if (this._suppressChangeNotifications === 0) {
            this._view.fireEvent(ViewFactory.EVENT_VIEW_CHANGED, this._view);
        }
    };

    TableColumns.prototype._withoutChangeNotifications = function (fn, context) {
        this._suppressChangeNotifications += 1;
        try {
            fn.apply(context);
        } finally {
            this._suppressChangeNotifications -= 1;
        }
    };

    /**
     * Create an intance that can schedule a set of changes and then approve
     * them or cancel them. This should be used in dialogs with OK/Cancel
     * buttons.
     *
     * @returns {VetoableTableColumns}
     */
    TableColumns.prototype.createTentativeInstance = function () {
        return new TableColumns._Tentative(this);
    };

    /**
     * @param {TableColumns} parent - Parent TableColumns object.
     *
     * @constructor
     */
    TableColumns._Tentative = function (parent) {
        this.parent = parent;
        this._columns = ko.observableArray(parent.columns().slice());
        this.columns = ko.pureComputed(function () {
            return this._columns();
        }.bind(this));
    };

    TableColumns._Tentative.prototype.approve = function () {
        var self = this;
        var original = this.parent.columns();
        var edited = this.columns();

        // Current index of a column
        var curIdx = function (col) {
            var columnViewArray = self.parent.columns().map(function (column) {
                return column.getView();
            });
            return columnViewArray.indexOf(col.getView());
        };

        var deleted = original.filter(function (col) {
            return edited.indexOf(col) === -1; // in orig, not in edited
        });
        var reordered = edited.filter(function (col) {
            return original.indexOf(col) !== -1; // in edited and in orig
        });
        var added = edited.filter(function (col) {
            return original.indexOf(col) === -1; // in edited, not in orig
        });

        var res = Promise.resolve();
        self.parent._withoutChangeNotifications(function () {
            deleted.forEach(function (del) {
                res = res.then(function () {
                    return self.parent.removeColumn(curIdx(del));
                });
            });
            reordered.forEach(function (reord, index) {
                res = res.then(function () {
                    var currIndex = curIdx(reord);
                    if (index !== currIndex) {
                        return self.parent.reorderColumn(currIndex, index);
                    }
                });
            });
            added.forEach(function (add) {
                res = res.then(function () {
                    var index = edited.indexOf(add);
                    return self.parent.addColumn(add.getProperty(), index);
                });
            });
        });
        return res.then(function () {
            if (deleted.length || added.length) { // not needed for reorder changes
                self.parent.notifyChanged();
            }
        });
    };

    TableColumns._Tentative.prototype.addColumn = function (propertyOrId, index) {
        var entity = this.parent._getEntity();
        var property = TableColumns._getProperty(propertyOrId, entity);
        var newCol = new NewColumn(property);
        this._columns.splice(index, 0, newCol);
    };

    TableColumns._Tentative.prototype.reorderColumn = function (fromIndex, toIndex) {
        var cols = this._columns();
        var deleted = cols.splice(fromIndex, 1);
        cols.splice(toIndex, 0, deleted[0]);
        this._columns([]);
        this._columns(cols);
    };

    TableColumns._Tentative.prototype.removeColumn = function (index) {
        this._columns.splice(index, 1);
    };

    /**
     * Create table columns instance for a table view.
     *
     * @param {View} view - Table view (of type top-table-container).
     *
     * @returns {TableColumns}
     */
    TableColumns.forTable = function (view) {
        AbcsLib.checkParameterCount(arguments, 1, 0);
        var type = view.getType();
        if (type !== ViewType.TOP_TABLE_CONTAINER) {
            throw new Error('Expected view of type top-table-container, not ' + type);
        }
        return new TableColumns(false, view);
    };

    TableColumns.forNewTable = function (view, entity) {
        AbcsLib.checkParameterCount(arguments, 2, 0);
        if (view.getType() !== ViewType.TOP_TABLE_CONTAINER) {
            throw new Error('Expected view of type top-table-container.');
        }
        return new TableColumns(true, view, entity);
    };

    /**
     * Check if the passed top-table-container view is associated with any
     * columns.
     *
     * @param {View} view - View of type top-table-container.
     * @returns {Boolean}
     */
    TableColumns.hasColumns = function (view) {
        var tableView = view.findChildOfType(ViewType.TABLE);
        return !!(tableView && tableView.getChildren().length > 0);
    };

    /**
     * Check whether passed object represents placeholder for some invalid
     * property.
     *
     * @param {Object} obj
     * @returns {boolean}
     */
    TableColumns.isInvalidProperty = function (obj) {
        return obj instanceof InvalidProperty;
    };

    TableColumns.Column = Column;

    return TableColumns;
});