Source: widget/learning/Backend.js

(function() {
    const BLOCK_LIST = [
        'entity.uid',
        'entity.caption',
        'entity.roles',
        'entity.shortdesc',
        'entity.type',
        'entity.sub_type',
        'entity.doc_type',
        'entity.language',
        'entity.clone_src',
        'entity.la_category'
    ];

    // blocked project subtypes
    const BLOCKED_SUB_TYPES = {help: 1, tour: 1, ca: 1, pa: 1, quiz_project: 1};

    /**
     * @typedef {Object} Help4.widget.learning.ServerAsset
     * @property {string} caption
     * @property {?string} clone_src
     * @property {?string} [clone_uid]
     * @property {?string} doc_type
     * @property {?string} la_category
     * @property {string} language
     * @property {?string} roles
     * @property {?string} shortdesc
     * @property {string} sub_type
     * @property {string} type
     * @property {string} uid
     */

    /**
     * @typedef {Object} Help4.widget.learning.Asset
     * @property {Object} _metadata
     * @property {string} _metadata.entityId
     * @property {string} caption
     * @property {string} description
     * @property {string} entityType
     * @property {string} entitySubType
     * @property {string} contentLanguage
     * @property {boolean} enableFeedback
     */

    /** Backend connector for learning widget. */
    Help4.widget.learning.Backend = class {
        /**
         * returns the list of all currently available assets
         * @param {Help4.typedef.SystemConfiguration} config
         * @returns {Promise<Help4.widget.learning.Asset[]>}
         */
        static async getAssets(config) {
            let data = /** @type {Help4.widget.learning.ServerAsset[]} */ await _load(config);

            const {ALLOWED_TYPES} = Help4.widget.learning;
            const {language: {wpb}, mixedLanguages} = config.core;

            data = data
            .filter(item => item.type === ALLOWED_TYPES.project
                ? !BLOCKED_SUB_TYPES[item.sub_type]
                : !!ALLOWED_TYPES[item.type]);

            if (mixedLanguages) {
                // sort by preferred language; correct order required for identifying duplicates
                data.sort(item => item.language === wpb ? -1 : 1);
            } else {
                // select only one language; prefer system to default
                const r = [[], []];
                data.forEach(item => r[item.language === wpb ? 0 : 1].push(item));
                data = r[0].length ? r[0] : r[1];
            }

            return _convert(_removeDuplicates(data), config);
        }

        /**
         * send user feedback to learning backend
         * @param {Help4.typedef.SystemConfiguration} config
         * @param {Object} params
         * @param {string} params.entityType
         * @param {string} params.entityUid
         * @param {Help4.widget.learning.FeedbackBubbleContentControl.Data} params.value
         * @returns {Promise<?boolean>}
         */
        static sendFeedback(config, {entityType, entityUid, value}) {
            const {
                widget: {companionCore: {SEN}},
                ajax: {Ajax},
                Localization
            } = Help4;

            const {
                learning: {learningAppBackendUrl: serverBaseUrl, learningAppWorkspace: waId},
                core: {playbackTag: tag}
            } = config;

            const serverUrl = SEN.createPubUrl({serverBaseUrl, waId, tag});
            const url = `${serverUrl}${entityType}/${entityUid}/.feedback`;

            const data = {
                msg_title: Localization.getText('header.feedback.mail'),
                msg_body: value.text,
                rating: value.rating,
                send_mail_to_watchers: 0
            };

            return Ajax({
                url,
                saml: true,
                xcsrf: SEN.getXCSRF(serverBaseUrl),
                method: 'POST',
                promise: true,
                data
            })
            .then(result => result?.response?.success)
            .catch(() => false);
        }
    }

    /**
     * @memberof Help4.widget.learning.Model
     * @private
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {Promise<Help4.widget.learning.ServerAsset[]>} data
     */
    function _load(config) {
        const {product, version, system, screenId, language, defaultLanguage} = config.core;
        const hasEnUsFallback = defaultLanguage.wpb.length === 1 && defaultLanguage.wpb[0] === 'en-US';
        const isSimpleRequest = !defaultLanguage.wpb.length || hasEnUsFallback;

        const array = [
            'entity.language', {value: language.wpb, operator: hasEnUsFallback ? '=' : 'like'},  // '=' operator includes en-US fallback by default from manager search API
            'entity.hidden', {value: 0, operator: '='},
            'entity.h4_product', product,
            'entity.h4_product_version', version,
            'entity.h4_system', {value: system || undefined, operator: 'like'},
            'entity.context_id', screenId
        ];
        const query = {search: [], out: BLOCK_LIST};

        let key;
        while (key = array.shift()) {
            const data = array.shift();
            const {value, operator} = typeof data === 'object' && data
                ? data
                : {value: data, operator: 'like'};

            if (value !== undefined) {
                query.search.push({
                    param: key,
                    operation: operator,
                    value: value
                });
            }
        }

        return isSimpleRequest
            ? _loadSimple(config, query)
            : _loadMultiple(config, query);
    }

    /**
     * @memberof Help4.widget.learning.Model
     * @private
     * @param {Help4.typedef.SystemConfiguration} config
     * @param {Object} query
     * @returns {Promise<Help4.widget.learning.ServerAsset[]>}
     */
    function _loadSimple(config, query) {
        return new Help4.Promise(resolve => {
            const {SEN} = Help4.widget.companionCore;
            const {learning: {learningAppBackendUrl, learningAppWorkspace}, core: {playbackTag}} = config;
            const serverUrl = SEN.createPubUrl({serverBaseUrl: learningAppBackendUrl, waId: learningAppWorkspace, tag: playbackTag});

            Help4.ajax.Ajax({
                headers: SEN.NO_CACHE_HEADER,
                url: serverUrl + '?' + encodeURIComponent(Help4.JSON.stringify(query)),
                saml: true,
                success: result => void resolve(result?.response?.resource || []),  // data will be a mix of system + fallback language (en-US)
                error: () => void resolve([])
            });
        });
    }

    /**
     * @memberof Help4.widget.learning.Model
     * @private
     * @param {Help4.typedef.SystemConfiguration} config
     * @param {Object} query
     * @returns {Promise<Help4.widget.learning.ServerAsset[]>}
     */
    function _loadMultiple(config, query) {
        return new Help4.Promise((resolve) => {
            // either multiple fallback languages or non en-US fallback
            const {SEN} = Help4.widget.companionCore;
            const {core: {playbackTag}, learning: {learningAppBackendUrl, learningAppWorkspace}} = config;
            const langList = SEN.getLanguages(config);

            // manager search api doesn't support multiple languages in param
            // workaround: construct url per language and do a multifile request instead of making many search requests (one per language)
            const multifileUrl = SEN.createPubUrl({serverBaseUrl: '', waId: learningAppWorkspace, tag: playbackTag});

            const requests = [];
            for (const language of langList) {
                query.search[0].value = language;  // index 0 is entity.language
                requests.push({url: multifileUrl + '?' + encodeURIComponent(Help4.JSON.stringify(query))});
            }

            Help4.ajax.EnableNow({
                headers: SEN.NO_CACHE_HEADER,
                url: learningAppBackendUrl + '/multifile',
                saml: true,
                xcsrf: SEN.getXCSRF(learningAppBackendUrl),
                method: 'POST',
                dataType: 'multipart',
                data: {request: requests},
                success: result => {
                    resolve((result || []).reduce((accumulator, currentValue) => {
                        currentValue = currentValue?.response?.resource || [];
                        accumulator.push(...currentValue);
                        return accumulator;
                    }, []));
                },
                error: () => void resolve([])
            });
        });
    }

    /**
     * @memberof Help4.widget.learning.Model
     * @private
     * @param {Help4.widget.learning.ServerAsset[]} data
     * @returns {Help4.widget.learning.ServerAsset[]}
     */
    function _removeDuplicates(data) {
        /*
        pre-requisite: data should be in order of language preference
        purpose:       remove duplicates [clone_src]
        scenarios:     items marked with x are duplicates and should be removed
        */
        /*const debugScenarios = [[
            // a) an uid can delete all following clone_src of other languages
            // b) there is not the same uid than my one
            {uid: '1', title: 'A', language: 'de-DE'},
            {uid: '2', title: 'B', language: 'de-DE'},
            {uid: '3', title: 'C', language: 'de-DE', clone_src: 'p!1'}, // same language
            {uid: '4', title: 'D', language: 'de-CH', clone_src: 'p!1'}  // x
        ], [
            // c) a clone_src can delete all following uid of other languages
            {uid: '1', title: 'A', language: 'de-DE', clone_src: 'p!3'},
            {uid: '2', title: 'B', language: 'de-DE'},
            {uid: '3', title: 'C', language: 'de-DE'},                   // same language
            {uid: '3', title: 'D', language: 'de-CH'}                    // x
        ], [
            // d) a clone_src cannot delete other clone_src irrespective of language
            {uid: '1', title: 'A', language: 'de-DE', clone_src: 'p!4'},
            {uid: '2', title: 'B', language: 'de-DE', clone_src: 'p!4'}, // other clone_src
            {uid: '3', title: 'C', language: 'en-US', clone_src: 'p!4'}  // other clone_src
        ]];

        data = debugScenarios[0];
        debugger;*/

        /** @param {Help4.widget.learning.ServerAsset} asset */
        const extractCloneUid = (asset) => {
            const {clone_uid, clone_src} = asset;
            if (!clone_uid && clone_src) {
                asset.clone_uid = clone_src.split('!')[1];
            }
        }

        /**
         * @param {Help4.widget.learning.ServerAsset} asset1
         * @param {Help4.widget.learning.ServerAsset} asset2
         * @returns {boolean}
         */
        const isDuplicate = (asset1, asset2) => {
            // item1 is always in front of item2...
            // ...as projects are ordered by language priority...
            // ...item1 has a higher or the same language priority as item2
            const {uid: u1, clone_uid: c1, language: l1} = asset1;
            const {uid: u2, clone_uid: c2, language: l2} = asset2;
            return u1 === c2 && l1 !== l2
                ? true  // an uid can delete all following clone_src of other languages
                : !!c1 && c1 === u2 && l1 !== l2;  // a clone_src can delete all following uid of other languages
        }

        for (const [index1, asset1] of Help4.arrayEntries(data)) {
            extractCloneUid(asset1);

            for (let index2 = index1 + 1, asset2; asset2 = data[index2]; index2++) {
                extractCloneUid(asset2);
                isDuplicate(asset1, asset2) && data.splice(index2--, 1);
            }
        }

        return data;
    }

    /**
     * @memberof Help4.widget.learning.Backend
     * @private
     * @param {Help4.widget.learning.ServerAsset[]} serverAssets
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {Help4.widget.learning.Asset[]}
     */
    function _convert(serverAssets, {learning: {learningAppFeedback}}) {
        return serverAssets
        .map(({uid, caption, shortdesc, type, sub_type, language}) => {
            return {
                _metadata: {entityId: uid},
                caption,
                description: shortdesc,
                entityType: type,
                entitySubType: sub_type,
                contentLanguage: language,
                enableFeedback: learningAppFeedback
            };
        })
        .sort(({caption: c1}, {caption: c2}) => {
            c1 = c1.toLowerCase();
            c2 = c2.toLowerCase();
            return c1 < c2 ? -1 : (c1 > c2 ? 1 : 0);
        });
    }
})();