Source: widget/companionCore/data/Help.js

(function () {
    /**
     * @typedef {Help4.widget.help.ProjectTile} Help4.widget.help.ConditionProjectTile
     * @property {boolean} _conditionResult
     */

    /**
     * @typedef {Object} Help4.widget.help.TileParams
     * @property {Help4.widget.help.ProjectTile} _tile
     * @property {Object} [_data]
     * @property {Object} _metadata
     * @property {Help4.widget.help.view2.Tile.Descriptor} _metadata.descriptor
     * @property {'tile'} _metadata.type
     * @property {string} _metadata.originalDescription
     * @property {boolean} _metadata.isGlobalHelp
     * @property {string} controlType
     * @property {?string} icon
     * @property {boolean} isGlobalHelp
     * @property {boolean} showAsButton
     * @property {boolean} linkLightbox
     * @property {boolean} hidden
     * @property {string} caption
     * @property {string} description
     * @property {string} title
     * @property {string} type
     * @property {string} css
     * @property {boolean} active
     * @property {boolean} visible
     * @property {string} contentLanguage
     */

    const PRIORITIES = {
        GlobalHelp: 0,
        QuickTour: 1,
        WNTours: 2,  // What's New Tours
        WTA_UACP_SEN: 3,  // WTA UACP/SEN (what's this app)
        WTA_UR: 4,  // WTA UR Harmonization
        UACP_SEN: 5,
        UR: 6,
        API: 7
    }

    /** help functionality */
    Help4.widget.companionCore.data.Help = class {
        /**
         * deliver all tiles that are not permanently invisible
         * @param {Object} params
         * @param {boolean} params.whatsnew
         * @param {string} params.screenId
         * @returns {Promise<Help4.widget.help.ProjectTile[]>}
         */
        static async getAvailableTiles({whatsnew, screenId}) {
            const {widget} = _getWidget(whatsnew);
            const {
                /** @type {Help4.typedef.SystemConfiguration} */ configuration,
                widget: {help: {
                    /** @type {Help4.widget.help.Data|Help4.widget.whatsnew.Data} */
                    data
                }}
            } = widget.getContext();

            await data.waitCataloguesLoaded(screenId);
            await data.waitHelpLoaded();

            const {core: {isEditorView}} = configuration;

            const tiles = await data.getHelpTiles();
            return tiles
            // remove all tiles that will never appear
            .map((tile) => {
                // do not show announcement tiles in PUB mode when they are hidden, but keep them within tiles
                // to be able to show the announcement
                const {type, linkLightbox, linkTo, splash, hidden} = tile;
                if (!isEditorView && type === 'link' && !!linkLightbox && !!linkTo && !!splash && !!hidden) {
                    tile._hiddenAnnouncement = true;
                }
                return tile;
            })
            .filter(tile => _filterTilePermanent(tile, configuration))
            // split into categories by priority
            .reduce((a, tile) => {
                const priority = _tileToPriority(tile);
                a[priority] ||= [];
                a[priority].push(tile);
                return a;
            }, [])
            // sort categories
            .map((category, index) => {
                // sort WN tours alphabetically
                if (index === PRIORITIES.WNTours) {
                    category.sort(({title: t1}, {title: t2}) => {
                        t1 = t1.toLowerCase();
                        t2 = t2.toLowerCase();
                        return t1 < t2 ? -1 : (t1 > t2 ? 1 : 0);
                    });
                }

                return category;
            })
            // convert back into one large array
            .reduce((a, category) => {
                category?.length > 0 && a.push(...category);
                return a;
            }, []);
        }

        /**
         * extend tiles with a condition result
         * @param {Help4.widget.help.ProjectTile[]} tiles
         * @param {Object} params
         * @param {boolean} params.whatsnew
         * @returns {Promise<Help4.widget.help.ConditionProjectTile[]>}
         */
        static async evaluateTileConditions(tiles, {whatsnew}) {
            const {widget} = _getWidget(whatsnew);
            const {
                /** @type {Help4.service.ConditionService} */ conditionService
            } = widget.getContext().service;

            /** @type {Array<Promise<boolean>>} */
            const promises = [];

            for (const tile of tiles) {
                const {conditions, hotspotAnchor, _dataType} = tile;
                const promise = /** @type {Promise<boolean>|boolean} */ conditions?.length && Help4.includes(['UACP', 'SEN'], _dataType)
                    ? conditionService.checkConditions({conditions, hotspotAnchor})
                    : true;
                promises.push(promise);
            }

            const results = await Help4.Promise.all(promises);
            return results.map(
                /**
                 * @param {boolean} success
                 * @param {number} index
                 * @returns {Help4.widget.help.ConditionProjectTile}
                 */
                (success, index) => {
                    /** @type {Help4.widget.help.ConditionProjectTile} */
                    const clone = Help4.cloneObject(tiles[index]);
                    clone._conditionResult = success;
                    return clone;
                }
            );
        }

        /**
         * filter tiles by condition and deliver a tile descriptor result
         * @param {Help4.widget.help.ProjectTile[]} tiles
         * @param {Object} params
         * @param {boolean} params.whatsnew
         * @returns {Promise<Help4.widget.help.ProjectTile[]>}
         */
        static async filterTilesByCondition(tiles, {whatsnew}) {
            return (await this.evaluateTileConditions(tiles, {whatsnew}))  // evaluate conditions
            .filter(({_conditionResult}) => !!_conditionResult)  // remove all that failed
            .map(tile => {  // remove _conditionResult
                delete tile._conditionResult;
                return tile;
            });
        }

        /**
         * UrHarmonization uses IdSelectorUI5 in UI5 apps and UrHarmonizationSelector in non-UI5 apps
         * if the selector is UR return it, otherwise mark it for migration
         * @param {?string} hotspotAnchor
         * @param {boolean} [returnOther = false]
         * @param {boolean} [returnToMigrate = false]
         * @returns {{ur: value:string} | {other: {rule:string, value:string}} | {migrate: {rule:string, value:string}} | null}
         */
        static decodeUrHotspot(hotspotAnchor, returnOther = false, returnToMigrate = false) {
            if (hotspotAnchor) {
                const {Selector} = Help4.selector;
                const {rule, value, iframe} = Selector.base64ToUtf8(hotspotAnchor) || {};
                if (!iframe) return returnOther ? {other: {rule, value}} : null;

                const {rule: fRule, value: fValue} = iframe || {};
                if (fRule === 'UrHarmonizationSelector') return {ur: fValue};
                else if (returnToMigrate) return {migrate: {rule: fRule, value: fValue}};
            }
            return null;
        }

        /**
         * filters out UR catalogue tiles if assigned tiles with the same hotspotId exist
         * @param {Help4.widget.help.ProjectTile[]} tiles
         * @returns {Help4.widget.help.ProjectTile[]}
         */
        static filterUrTiles(tiles) {
            const {
                engine: {ur: {
                    Connection: {APP_TYPE},
                    UrHarmonizationEngine: {QUERIES: {UI5}}, Selector}
                },
                selector,
                widget: {help: {catalogues: {UR: {CATALOGUE_TYPE: UR_CATALOGUE_TYPE}}}}
            } = Help4;
            selector.window = window;  /** set for getElement method in {@link Help4.engine.ur.Selector.isAssignedInUI5} */
            const urEngine = Help4.getController().getEngine('urHarmonization');
            const isUI5 = urEngine._currentApp === APP_TYPE.UI5;

            const assignedUrIds = new Set();
            const assignedUI5Hotspots = new Set();
            let hasWta = false;    // non-UR What's This App? tiles overwrite UR tiles

            return tiles.filter(tile => {
                const {
                    _apiData,
                    _catalogueType,
                    _special,
                    hotspotAnchor
                } = tile;

                /** all tiles are already sorted; see {@link getAvailableTiles} and {@link _tileToPriority} */

                if (_special === 'wta') {
                    // remove UR WTA if non-UR WTA exists; non-UR WTA tile always comes first due to sorting
                    return _catalogueType === UR_CATALOGUE_TYPE && hasWta ? false : hasWta = true;
                } else if (_catalogueType === UR_CATALOGUE_TYPE) {
                    // UR: filter all hotspots that exist in UACP/SEN
                    // special handling for UI5 hotspots in UR
                    const {_type, hotspotId} = _apiData;
                    return _type === UI5
                        ? !Selector.isAssignedInUI5(hotspotId, assignedUI5Hotspots)
                        : !assignedUrIds.has(_apiData.hotspotId);
                }

                if (hotspotAnchor) {
                    // UACP/SEN: collect all assigned and migrated hotspots with UR hotspotId
                    const {
                        ur,
                        other,
                        migrate
                    } = this.decodeUrHotspot(hotspotAnchor, isUI5, true) || {};

                    if (ur) {
                        assignedUrIds.add(ur);
                    } else if (other) {
                        assignedUI5Hotspots.add(other);
                    } else if (migrate) {
                        // check if selector was migrated to UrHarmonizationSelector
                        // if no, add it to the migration map
                        // if yes, keep track of the hotspotId to hide the relevant backend tile
                        const {hotspotId, position} = urEngine.getMigratedHotspot(migrate) || {};
                        if (!hotspotId && !position) urEngine.addSelectorToMigrate(migrate);
                        else if (hotspotId) assignedUrIds.add(hotspotId);
                    }
                }

                return true;
            });
        }

        /**
         * transforms a project tile data into control tile data
         * @param {Help4.widget.help.ProjectTile} tile
         * @param {Object} params
         * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
         * @param {boolean} params.hotspotVisible
         * @returns {Help4.widget.help.TileParams}
         */
        static helpTileToTileParams(tile, {descriptor, hotspotVisible}) {
            const {
                Placeholder,
                Localization,
                widget: {help: {
                    view2: {BUTTON_CSS, LIGHTBOX_CSS, HIDDEN_CSS, GLOBAL_HELP_CSS},
                    catalogues: {GlobalHelp: {CATALOGUE_TYPE: GlobalHelp_CATALOGUE_TYPE}}
                }},
                shell: {UI5}
            } = Help4;

            const {
                title: tileCaption,
                summaryText: tileDescription,
                published,
                type,
                linkTo,
                linkLightbox,
                showAsButton,
                hidden,
                tileIcon: icon,
                language,
                content,
                id,
                _projectId: projectId,
                _catalogueKey,
                _catalogueType: catalogueType,
                _dataType: dataType,
                _hiddenAnnouncement
            } = tile;

            const isGlobalHelp = catalogueType === GlobalHelp_CATALOGUE_TYPE;
            const caption = Placeholder.resolve(tileCaption);
            const description = /** @type {string} */ UI5.hyphenate(Placeholder.resolve(tileDescription));
            let title = '';
            let state = undefined;

            const css = [];
            hidden && css.push(HIDDEN_CSS);

            let visible = true;
            switch (type) {
                case 'help':
                    css.push('help-tile');
                    // visibility based on hotspot visibility
                    visible = hotspotVisible;
                    // GlobalHelp handling
                    isGlobalHelp && css.push(GLOBAL_HELP_CSS, BUTTON_CSS);
                    break;
                case 'link':
                    css.push('link-tile');
                    linkLightbox && css.push(LIGHTBOX_CSS);
                    showAsButton && css.push(BUTTON_CSS);
                    // only show, if content or link exists
                    visible &&= !!content || !!linkTo;
                    break;
                case 'tour':
                    css.push('tour-project');
                    if (published && _catalogueKey === 'head' && dataType === 'SEN') {
                        const tooltip = Localization.getText(`tooltip.tourstatus.${published}`);
                        title = `${caption}: ${tooltip}`;
                        state = published;
                    } else {
                        title = caption;
                    }
                    break;
            }

            if (_hiddenAnnouncement) visible = false;

            /** @type {Help4.widget.help.TileParams} */
            return {
                _tile: tile,
                _metadata: {
                    descriptor,
                    type: 'tile',
                    originalDescription: description,
                    isGlobalHelp,
                    tileId: id
                },
                _data: {state},
                controlType: 'Help4.control2.Tile',
                icon: showAsButton ? null : icon,
                isGlobalHelp,
                showAsButton,
                linkLightbox,
                hidden,
                caption,
                description,
                title,
                type,
                css: css.join(' '),
                active: false,
                visible,
                contentLanguage: language
            }
        }
    }

    /**
     * get widget
     * @memberof Help4.widget.companionCore.data.Tour
     * @private
     * @param {boolean} whatsnew
     * @returns {{widgetName: string, widget: Help4.widget.whatsnew.Widget|Help4.widget.help.Widget}}
     */
    function _getWidget(whatsnew) {
        const widgetName = whatsnew ? 'whatsnew' : 'help';
        return {
            widgetName,
            widget: /** @type {Help4.widget.whatsnew.Widget|Help4.widget.help.Widget} */ Help4.widget.getInstance(widgetName)
        }
    }

    /**
     * filter tiles that will never appear; make sure to maintain order!
     * @memberof Help4.widget.companionCore.data.Help
     * @private
     * @param {Help4.widget.help.ProjectTile} tile
     * @param {Help4.typedef.SystemConfiguration} configuration
     * @returns {boolean}
     */
    function _filterTilePermanent(tile, {core: {isEditorView}}) {
        // do not show references or broken tiles
        // do not show hidden tiles outside of editor view
        const {id, hidden, _reference, _hiddenAnnouncement} = tile;
        return id && !_reference && (isEditorView || !hidden || _hiddenAnnouncement);
    }

    /**
     * prioritizes catalogueType and What's This App?
     * @memberof Help4.widget.companionCore.data.Help
     * @private
     * @param {Help4.widget.help.ProjectTile} tile
     * @returns {number} - smaller is higher priority
     */
    function _tileToPriority({_catalogueType, _special, _projectId}) {
        const {
            WNTours: {ID: WNTours_ID},
            GlobalHelp: {CATALOGUE_TYPE: GlobalHelp_CATALOGUE_TYPE},
            QuickTour: {CATALOGUE_TYPE: QuickTour_CATALOGUE_TYPE},
            API: {CATALOGUE_TYPE: API_CATALOGUE_TYPE},
            UR: {CATALOGUE_TYPE: UR_CATALOGUE_TYPE}
        } = Help4.widget.help.catalogues;

        if (_catalogueType === GlobalHelp_CATALOGUE_TYPE) return PRIORITIES.GlobalHelp;
        if (_catalogueType === QuickTour_CATALOGUE_TYPE) return PRIORITIES.QuickTour;
        if (_projectId === WNTours_ID) return PRIORITIES.WNTours;
        if (_catalogueType === API_CATALOGUE_TYPE) return PRIORITIES.API;
        return _special === 'wta'  // What's this app
            ? _catalogueType === UR_CATALOGUE_TYPE ? PRIORITIES.WTA_UR : PRIORITIES.WTA_UACP_SEN
            : _catalogueType === UR_CATALOGUE_TYPE ? PRIORITIES.UR : PRIORITIES.UACP_SEN;
    }
})();