Source: widget/help/CatalogueBackend.js

(function() {
    /**
     * @typedef {'SEN'|'SEN2'|'UACP'|'GlobalHelp'|'API'|'QuickTour'|'UR'} Help4.widget.help.CatalogueTypes
     */

    /**
     * @typedef {'SEN'|'UACP'} Help4.widget.help.DataTypes
     */

    /**
     * @typedef {'HELP'|'TOUR'} Help4.widget.help.ContextTypes
     */

    /**
     * @typedef {'pub'|'head'} Help4.widget.help.CatalogueKeys
     */

    /**
     * @memberof Help4.widget.help
     * @typedef {?string}
     * @property {null} invalid - no published information available
     * @property {'published'} published - this item is completely published
     * @property {'updated'} updated - this item has been updated after publishing
     * @property {'new'} new - this item is new and not published at all
     */
    Help4.widget.help.PUBLISHED_STATUS = {
        invalid: null,
        published: 'published',
        updated: 'updated',
        new: 'new'
    }

    /**
     * @typedef {Object} Help4.widget.help.CatalogueProject
     * @property {string} alias - alternative project name; internally added
     * @property {Array} conditions - possible conditions
     * @property {string} [cloneSrc] - reduced clone source of project; internally added
     * @property {Help4.widget.help.ContextTypes} contextType - context type
     * @property {boolean} hidden - whether project is hidden for end-users
     * @property {string} id - project ID
     * @property {string} language - project language
     * @property {string} loio - project LOIO (LOgical Info Object)
     * @property {string} modificationTime - time of last modification
     * @property {string} product - context information
     * @property {Help4.widget.help.PUBLISHED_STATUS} published - project published information
     * @property {string} screen - screen ID
     * @property {string} system - context information
     * @property {string} title - title of the project
     * @property {string} version - context information
     * @property {Help4.widget.help.CatalogueTypes} _catalogueType - SEN, SEN2, UACP, ...; internally added
     * @property {Help4.widget.help.DataTypes} _dataType - SEN, UACP, ...; internally added
     * @property {string|null} [_ext] - ext information for RO projects: <id: string> - this project is extended by the project with id
     * @property {boolean} [_ignore] - ext information for RW projects: project is removed by EXT, e.g. due to low-priority language
     * @property {string|null} [_ro] - ext information for RW projects: <id: string> - this project extends the project with id
     * @property {boolean} [_whatsnew] - whether project is a whatsnew one
     */

    /**
     * @typedef {Object} Help4.widget.help.CatalogueSelection
     * @property {Help4.widget.help.CatalogueTypes} catalogueType - backend ID
     * @property {string} id - project ID
     * @property {Help4.widget.help.CatalogueTypes} [catalogueType2] - backend ID; EXT
     * @property {string} [id2] - project ID; EXT
     */

    /**
     * @typedef {Object} Help4.widget.help.Catalogue
     * @property {Help4.widget.help.CatalogueProject[]} projects - list of projects
     * @property {Object} selected - selected projects per screen; an array of {@link Help4.widget.help.CatalogueSelection} per screen
     */

    /**
     * @typedef {Object} Help4.widget.help.Catalogues
     * @property {Help4.widget.help.Catalogue} pub - public catalogue projects for end-users
     * @property {Help4.widget.help.Catalogue} head - non-public catalogue projects for authors
     */

    const ATTRIBUTES_MAPPING = {
        appUrl: 'screen',
        clone_src: null,
        editable: null,
        environment: null,
        locale: 'language',
        maxversion: null,
        modification_time: 'modificationTime',
        lastModifiedDate: 'modificationTime',
        otherMetadata: null,
        profile: 'featureProfileUACP',
        shortDescription: null,
        state: null,
        wt: null,
        wt_location: null,
        wt_owner: null
    }

    const DEFAULTS = {
        hidden: false,
        published: Help4.widget.help.PUBLISHED_STATUS.published
    }

    /**
     * Backend connector for help widget.
     */
    Help4.widget.help.CatalogueBackend = class {
        /**
         * @memberof Help4.widget.help.CatalogueBackend
         * @type {'!whatsnew'}
         */
        static WHATSNEW_SCREEN_ID = '!whatsnew';

        /**
         * @param {Help4.typedef.SystemConfiguration} config - the system configuration
         * @returns {Promise<Help4.widget.help.Catalogues>}
         */
        static async load(config) {
            // map of all downloaded data sorted by module
            const map = await _load(config);

            /** @type {Help4.widget.help.Catalogues} */ const screenInfo = _assemble(map, config);
            return _handleEXT(screenInfo, config);
        }
    }

    /**
     * @memberof Help4.widget.help.CatalogueBackend
     * @private
     * @param {Help4.typedef.SystemConfiguration} config - the system configuration
     * @returns {Promise<Object>}
     */
    async function _load(config) {
        function* loadCatalogues() {
            for (const [key, catalogue] of Object.entries(Help4.widget.help.catalogues)) {
                if (!key.startsWith('_') && catalogue.load) {
                    const promise = catalogue.load(config);
                    yield {promise, key};
                }
            }
        }

        // wait for simultaneous loading of all integrated catalogues from all backends
        /** {@link Help4.widget.help.catalogues.SEN.CatalogueProjects} */
        /** {@link Help4.widget.help.catalogues.UACP.CatalogueProjects} */
        const {map} = await Help4.awaitPromises(loadCatalogues);

        /**
         * transforms data into format of {@link Help4.widget.help.CatalogueProject}
         * @param {Help4.widget.help.catalogues.SEN.CatalogueProject|Help4.widget.help.catalogues.UACP.CatalogueProject} project
         * @returns {Help4.widget.help.CatalogueProject}
         */
        const normalize = project => {
            const {WHATSNEW_SCREEN_ID} = Help4.widget.help.CatalogueBackend;

            /** @type {Help4.widget.help.CatalogueProject} */
            const normalized = {};

            // map attributes
            for (const [key, value] of Object.entries(project)) {
                let destKey = ATTRIBUTES_MAPPING[key];
                if (destKey === null) continue;  // remove attribute
                destKey ||= key;

                if (destKey === 'screen' && value.indexOf(WHATSNEW_SCREEN_ID) >= 0) {
                    normalized._whatsnew = true;
                }

                normalized[destKey] = value;
            }

            for (const [key, value] of Object.entries(DEFAULTS)) {
                if (!normalized.hasOwnProperty(key)) normalized[key] = value;
            }

            return normalized;
        }

        // normalize to internal data model
        const result = {};
        // catalogueKey: UACP, SEN, SEN2, ...
        // catalogueProjects: {pub: [project1, ...], head: [project1, ...]}
        for (const [
                /** @type {string} */ catalogueId,
                /** @type {?Help4.widget.help.catalogues.SEN.CatalogueProjects|?Help4.widget.help.catalogues.UACP.CatalogueProjects} */ catalogueResult
            ] of Object.entries(map))
        {
            const {
                /** @type {Help4.widget.help.catalogues.SEN.CatalogueProject[]|Help4.widget.help.catalogues.UACP.CatalogueProject[]|null} */ pub,
                /** @type {Help4.widget.help.catalogues.SEN.CatalogueProject[]|Help4.widget.help.catalogues.UACP.CatalogueProject[]|null} */ head
            } = catalogueResult || {};

            if (pub) {
                /** @type {Help4.widget.help.CatalogueProject[]} */
                const normPub = pub.map(project => normalize(project));
                result[catalogueId] = {pub: normPub};
            }

            if (head) {
                /** @type {Help4.widget.help.CatalogueProject[]} */
                const normHead = head.map(project => normalize(project));
                result[catalogueId] ||= {};  // might be defined by pub already
                result[catalogueId].head = normHead;
            }
        }
        return result;
    }

    /**
     * @memberof Help4.widget.help.CatalogueBackend
     * @private
     * @param {Object} map - map of all downloaded data sorted by module
     * @param {Help4.typedef.SystemConfiguration} config - the system configuration
     * @returns {Help4.widget.help.Catalogues}
     */
    function _assemble(map, config) {
        /**
         * @param {string} catalogueId
         * @param {Help4.widget.help.Catalogue} catalogue
         * @param {Help4.widget.help.CatalogueProject[]} projects
         */
        const process = (catalogueId, {projects: cProjects, selected: cSelected}, projects) => {
            cProjects.push(...projects);

            const {catalogues} = Help4.widget.help;
            const selected = catalogues[catalogueId].selectHelp?.(projects, config) || _selectHelp(projects, config, catalogueId);

            Object.entries(selected)
            .forEach(([screenId, id]) => {
                cSelected[screenId] ||= [];
                cSelected[screenId].push({catalogueType: catalogueId, id});
            });
        };

        const isEXT = _isEXT(config);
        /** @type {Help4.widget.help.Catalogues} */
        const catalogues = {
            pub: {projects: [], selected: {}},
            head: {projects: [], selected: {}}
        };

        // assemble screen project lists and select help projects per screen id
        for (const [catalogueId, catalogueResult] of Object.entries(map)) {
            const {
                /** @type {Help4.widget.help.CatalogueProject[]|null} */ pub,
                /** @type {Help4.widget.help.CatalogueProject[]|null} */ head
            } = catalogueResult || {};  // can be null!

            pub && process(catalogueId, catalogues.pub, pub);

            // in EXT mode: always use published data for RO sources!
            if (isEXT && (catalogueId === 'SEN' || catalogueId === 'UACP')) {
                pub && process(catalogueId, catalogues.head, Help4.cloneArray(pub));
            } else if (head) {
                process(catalogueId, catalogues.head, head);
            }
        }

        return catalogues;
    }

    /**
     * @memberof Help4.widget.help.CatalogueBackend
     * @private
     * @param {Help4.widget.help.CatalogueProject[]} projects - the projects to select from
     * @param {Help4.typedef.SystemConfiguration} config - the system configuration
     * @param {string} catalogueId - backend id
     * @returns {Object}
     */
    function _selectHelp(projects, config, catalogueId) {
        // creates language list, first language is the most preferred one
        // all others are fallback languages
        const {help: {catalogues}, companionCore} = Help4.widget;
        /** @type {string} */ const dataType = catalogues[catalogueId].getDataType();
        /** @type {string[]} */ const langList = companionCore[dataType].getLanguages(config);

        const reduce = ({selected, languages}, {screen, language, id}) => {
            // index of language; the lower the index the higher the language priority
            const index = langList.indexOf(language);

            if (!selected[screen]) {
                // no project selected so far
                selected[screen] = id;
                languages[screen] = index;
            } else if (languages[screen] > index) {
                // project with higher language priority has been found
                selected[screen] = id;
                languages[screen] = index;
            }

            return {selected, languages};
        }

        const {selected} = projects
        .filter(({contextType}) => contextType === 'HELP')
        .reduce(reduce, {selected: {}, languages: {}});

        return selected;
    }

    /**
     * @memberof Help4.widget.help.CatalogueBackend
     * @private
     * @param {Help4.widget.help.Catalogues} catalogues - the screen catalogue
     * @param {Help4.typedef.SystemConfiguration} config - the system configuration
     * @returns {Help4.widget.help.Catalogues}
     */
    function _handleEXT(catalogues, config) {
        if (!_isEXT(config)) return catalogues;

        const {SEN2} = Help4.widget.help.catalogues;
        const blockListEXT = {UACP: 1, SEN: 1, SEN2: 1};

        for (const {selected, projects} of [catalogues.pub, catalogues.head]) {
            // get EXT information for tours
            /** @type {Object|null} */ const ext = SEN2.getTourList(projects, config);
            for (const [srcId, destId] of Object.entries(ext || {})) {
                // destId === <string>: this RW tour extends the one described by srcId
                // destId === null: this RW tour must not be displayed
                const srcTour = projects.find(({id}) => id === srcId);
                if (destId) {
                    const destTour = projects.find(({id}) => id === destId);
                    srcTour._ext = destId;  // tour1 is extended by tour2
                    destTour._ro = srcId;  // tour2 extends tour1
                } else {
                    srcTour._ignore = true;  // tour1 is ignored due to EXT
                }
            }

            // get help project selection
            for (const [screenId, selection] of Object.entries(selected)) {
                if (selection.length > 1) {
                    // check for EXT mode, where a help from RO and one from RW exist
                    /** @type {?Help4.widget.help.CatalogueSelection} */
                    const ext = SEN2.getHelpSelection(screenId, selection, projects, config);
                    if (ext) {
                        // remove all RO (UACP, SEN) and RW (SEN2) projects but keep all others; e.g. GlobalHelp or UR Harmonization content
                        selected[screenId] = selection.filter(({catalogueType: ct}) => !blockListEXT[ct]);
                        // re-add the selected EXT one that exist in RO and RW
                        selected[screenId].push(ext);
                    }
                }
            }
        }

        return catalogues;
    }

    /**
     * @memberof Help4.widget.help.CatalogueBackend
     * @private
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {boolean}
     */
    function _isEXT({help: {serviceLayer}}) {
        return serviceLayer === Help4.SERVICE_LAYER.ext;
    }
})();