course/amd/src/local/activitychooser/exporter.js

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Module to generate template data for the activity chooser.
 *
 * @module     core_course/local/activitychooser/exporter
 * @copyright  2025 Ferran Recio <ferran@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

import {getStrings} from 'core/str';

const activityCategories = ['activities', 'resources'];

let allStrings = null;

loadNecessaryStrings();

export default class {
    /**
     * A tab data structure.
     *
     * @typedef {object} TabData
     * @property {String} tabId the tab ID
     * @property {Boolean} active whether the tab is active or not
     * @property {Array} items the filtered modules to be displayed in the tab
     * @property {Boolean} displayed whether the tab is displayed or not
     * @property {String} tabLabel the tab label
     * @property {String|null} tabHelp the help text for the tab (optional)
     */

    /**
     * Generate a tab data object for the activity chooser.
     *
     * @private
     * @param {String} tabId Tab ID.
     * @param {Array} filteredModules Filtered modules to be displayed in the tab.
     * @param {String} tabLabel Tab label.
     * @param {String|null} tabHelp Help text for the tab (optional).
     * @param {Boolean} active Whether the tab is active or not.
     * @return {TabData} Tab data object.
     */
    getTabData(tabId, filteredModules, tabLabel, tabHelp = null, active = false) {
        const result = {
            tabId: tabId,
            active: active,
            items: filteredModules,
            displayed: filteredModules.length > 0,
            tabLabel,
        };
        if (tabHelp) {
            result.tabHelp = tabHelp;
        }
        return result;
    }

    /**
     * Normalise the modules data to be used in the chooser.
     *
     * The modulesData can be a plain array or a Map. This method will convert it to a
     * plain array of objects.
     *
     * @param {Array|Map} modulesData Modules data to be used in the chooser.
     * @return {Array} Normalised modules data.
     */
    normaliseModulesData(modulesData) {
        if (modulesData instanceof Map) {
            modulesData = Array.from(modulesData.values());
        } else if (!Array.isArray(modulesData)) {
            throw new Error('Invalid modules data format. Expected an array or a Map.');
        }
        return modulesData;
    }

    /**
     * Fetch the chooser template data for a specific section.
     *
     * @param {Array|Map} modulesData Modules data to be used in the chooser.
     * @return {Promise<Object>} Promise resolved with the template data.
     */
    async getModChooserTemplateData(modulesData) {
        modulesData = this.normaliseModulesData(modulesData);
        const allStrings = await loadNecessaryStrings();
        const favouriteTab = await this.getFavouriteTabData(modulesData);

        const tabs = [
            {
                ...this.getTabData(
                    'all',
                    modulesData,
                    allStrings.all,
                    null,
                    !favouriteTab.displayed,
                ),
                hasSearchResults: true, // The all tab will also show search results.
            },
            favouriteTab,
            {
                ...this.getTabData(
                    'recommended',
                    modulesData.filter(mod => mod.recommended === true),
                    allStrings.recommended,
                    allStrings.recommended_help
                ),
                separator: true, // Add a separator before the purpose categories.
            },
        ];

        activityCategories.forEach((category, index) => {
            const categoryModules = modulesData.filter(mod => mod.archetype == index);
            if (categoryModules.length === 0) {
                return;
            }
            tabs.push(
                this.getTabData(
                    category,
                    categoryModules,
                    allStrings[category],
                    allStrings[category + '_help']
                )
            );
        });

        return {
            modules: modulesData,
            tabs,
        };
    }

    /**
     * Get the favourite tab data.
     *
     * @param {Array|Map} modulesData Modules data to be used in the chooser.
     * @return {Promise<TabData>} Promise resolved with the template data.
     */
    async getFavouriteTabData(modulesData) {
        modulesData = this.normaliseModulesData(modulesData);
        const allStrings = await loadNecessaryStrings();

        // We need to deconstruct the modules data to ensure it is an array.
        const favouriteModules = modulesData.filter(
            mod => {
                return mod.favourite === true;
            }
        );

        return this.getTabData(
            'favourites',
            favouriteModules,
            allStrings.favourites,
            null,
            favouriteModules.length > 0,
        );
    }

    /**
     * Get the search result template data.
     *
     * @param {Array|Map} resultsModulesData Modules data to be used in the chooser.
     * @return {Object} The template data.
     */
    getSearchResultData(resultsModulesData) {
        resultsModulesData = this.normaliseModulesData(resultsModulesData);
        return {
            'searchresultsnumber': resultsModulesData.length,
            'searchresults': resultsModulesData,
        };
    }

    /**
     * Get the number of items in a tab.
     *
     * @param {TabData} tabData The tab data.
     * @return {Number} The number of items in the tab.
     */
    countTabItems(tabData) {
        return tabData.items?.length ?? 0;
    }

}

/**
 * Load the necessary strings for the activity chooser.
 *
 * @return {Promise<Object>} Promise resolved with the loaded strings.
 */
async function loadNecessaryStrings() {
    if (allStrings !== null) {
        return allStrings;
    }
    allStrings = {};

    const stringToLoad = [
        {key: 'all', component: 'core'},
        {key: 'favourites', component: 'core'},
        {key: 'recommended', component: 'core'},
        {key: 'recommended_help', component: 'core_course'},
        ...activityCategories.map(
            (key) => ({
                key: key,
                component: 'core',
            })
        ),
        ...activityCategories.map(
            (key) => ({
                key: key + '_help',
                component: 'core',
            })
        ),
    ];

    const loadedStrings = await getStrings(stringToLoad);
    stringToLoad.forEach(({key}, index) => {
        allStrings[key] = loadedStrings[index];
    });
    return allStrings;
}