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