(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;
}
})();