(function() {
/**
* @typedef {Object} Help4.MigrateFioriSpaces.ProjectIdList
* @property {string[]} ro - the RO project IDs
* @property {string[]} rw - the RW project IDs
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.EntityStreamInfo
* @property {string} name - stream name
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.EntityStreamList
* @property {string} uid - entity UID
* @property {Help4.MigrateFioriSpaces.EntityStreamInfo[]} Streams - streams of entity
* @property {Object} [entity_txt] - content of entity.txt
* @property {Object} [lesson_js] - content of lesson.js
* @property {XMLDocument} [project_dpr] - content of project.dpr
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.EntityMetaInfo
* @property {Help4.MigrateFioriSpaces.EntityStreamInfo[]} Streams
* @property {number|string} wt - whether WT is available
* @property {string} wt_location - location of WT, if acquired
* @property {string} wt_owner - owner of WT, if acquired
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.EntityInfo
* @property {Help4.MigrateFioriSpaces.EntityMetaInfo} Meta - entity Meta information
* @property {string} uid - entity UID
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.WhoAmI
* @property {string} name - assembled user name: "<ID> (<Firstname> <Lastname>)
* @property {string} uid - user UID
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.DownloadInfo
* @property {string} url - the server URL as configured for SAP Companion
* @property {string[]} ids - the project IDs
* @property {Object} contextTypes - the project contextTypes
* @property {string} xcsrf - the XCSRF token
* @property {string} serverBaseUrl - the server base URL
* @property {string} waBase - the server's relative workarea URL
* @property {string} waName - the server's workarea ID
* @property {Help4.MigrateFioriSpaces.EntityStreamList[]} [entities] - entity information
* @property {Help4.MigrateFioriSpaces.WhoAmI} [whoAmI] - my identification data on the server
* @property {string[]} [blocked] - all UIDs that are blocked; WTs cannot be acquired
* @property {string[]} [toBeAcquired] - all UIDs where WTs must be acquired to perform operation
* @property {string[]} [acquired] - all UIDs where WTs have been acquired
* @property {string[]} [notAcquired] - all UIDs that we were unable to acquire WTs
* @property {string[]} [returned] - all UIDs where WTs were returned
* @property {string[]} [notReturned] - all UIDs that we were unable to return the WT
* @property {string[]} [modified] - all UIDs where we modified data
* @property {string[]} [saved] - all UIDs that were successfully saved
* @property {string[]} [failed] - all UIDs that were save failed
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.MigrationInfo
* @property {Object} migrationMap - the migration map; data-help-id vs data-help-id2
* @property {Help4.MigrateFioriSpaces.DownloadInfo} [ro] - the download information for RO
* @property {Help4.MigrateFioriSpaces.DownloadInfo} [rw] - the download information for RW
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.PrintParams
* @property {boolean} [isText = true] - whether data is text
* @property {number} [level = 1] - the level of indentation
* @property {boolean} [warn = false] - whether this is a warning
* @property {boolean} [error = false] - whether this is an error
* @property {boolean} [abort = false] - whether an error should be thrown
*/
/**
* @typedef {Object} Help4.MigrateFioriSpaces.PrintEntityUrlParams
* @property {string} text - caption
* @property {number} [level = 2] - the level of indentation
* @property {boolean} [warn = false] - whether this is a warning
* @property {boolean} [error = false] - whether this is an error
* @property {boolean} [abort = false] - whether an error should be thrown
*/
/**
* Migrates all object recordings of all SEN projects from data-help-id to data-help-id2<br>
* <b>Attention:</b> Does not work with UACP!
*/
Help4.MigrateFioriSpaces = class {
/**
* @constructor
*/
constructor() {
/**
* @private
* @type {Help4.MigrateFioriSpaces.MigrationInfo}
*/
this._info = {};
}
static NO_CACHE_HEADERS = {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Pragma: 'no-cache'
}
/**
* Prints console information
* @throws {Error}
* @param {*} data
* @param {Help4.MigrateFioriSpaces.PrintParams} params
*/
static print(data, {
isText = true,
level = 1,
warn = false,
error = false,
abort = false
} = {}) {
if (isText) {
let style = '';
if (error) {
style = 'color:red';
} else if (warn) {
style = 'color:orange';
}
switch (level) {
case 0:
console.log(`%c${data}`, 'border-top:1px solid #000');
break;
case 1:
console.groupEnd();
console.group(data);
break;
case 2:
console.log(`%c${data}`, style);
break;
}
} else {
if (error) {
console.error(data);
} else if (warn) {
console.warn(data);
} else {
console.log(data);
}
}
if (abort) {
throw new Error('Execution aborted due to error(s).');
}
}
/**
* Shows links to projects in Manager UI.
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string[]} ids
* @param {Help4.MigrateFioriSpaces.PrintEntityUrlParams} params
*/
static printEntityUrls(info, ids, {
text,
level = 2,
warn = false,
error = false,
abort = false
}) {
const url = `${info.serverBaseUrl}#wa/overview/wa!${info.waName}:project!`;
const urls = ids
.map(uid => `- ${url}${uid}`)
.join('\n');
this.print(`${text}\n${urls}`, {level, warn, error, abort});
}
/**
* Execute migration.
* @return {Promise<void>}
*/
async execute() {
const {MigrateFioriSpaces: MFS, SERVICE_LAYER} = Help4;
console.clear();
MFS.print('Fiori Spaces Migration', {level: 0});
MFS.print('Init');
const controller = Help4.getController();
const config = controller?.getConfiguration();
const model = controller?.getService('model');
if (!config || !model) MFS.print('System not ready for migration!', {error: true, level: 2, abort: true});
const {serviceLayer, serviceUrl, serviceUrl2, features, core} = config;
const {enableFioriSpacesMigration = false} = features || {};
const {editor = false, screenId = null} = core || {};
if (!enableFioriSpacesMigration) MFS.print('"enableFioriSpacesMigration" flag is not set!', {error: true, level: 2, abort: true});
if (!editor) MFS.print('Feature requires "editor" permissions!', {error: true, level: 2, abort: true});
if (!Help4.Feature.FioriSpacesMigrationDebug) {
const shell = Help4.getShell();
if (!(shell instanceof Help4.shell.Fiori)) MFS.print('Fiori shell is required!', {error: true, level: 2, abort: true});
if (screenId !== 'Shell-home') MFS.print('Migration only works on "Shell-home"!', {error: true, level: 2, abort: true});
}
const isUACP = serviceLayer === SERVICE_LAYER.uacp;
const isEXT = serviceLayer === SERVICE_LAYER.ext;
if (isUACP) MFS.print('Migration does not work for UACP!', {error: true, level: 2, abort: true});
const {_info} = this;
if (!(_info.migrationMap = _getHelpIds.call(this))) {
MFS.print('Nothing to migrate - OPERATION COMPLETED SUCCESSFULLY!', {level: 2});
return;
}
const {ids, contextTypes} = _getProjectIds.call(this, model, config);
_createInfo.call(this, ids, contextTypes, {isEXT, serviceUrl, serviceUrl2, model});
const {ro, rw} = _info;
MFS.print('project info:', {level: 2});
MFS.print(_info, {isText: false});
ro && MFS.printEntityUrls(ro, ro.ids, {text: 'RO projects:'});
rw && MFS.printEntityUrls(rw, rw.ids, {text: 'RW projects:'});
if (!ro && !rw) {
MFS.print('No projects - OPERATION COMPLETED SUCCESSFULLY!', {level: 2});
return;
}
await MFS.WT.check(_info);
const needsWT = ro?.toBeAcquired.length || rw?.toBeAcquired.length;
if (needsWT) await MFS.WT.acquire(_info);
await MFS.Entity.download(_info);
MFS.Entity.replace(_info);
const modified = !!(ro?.modified?.length) || !!(rw?.modified?.length);
if (modified) {
MFS.Entity.createLessonJS(_info);
await MFS.Entity.save(_info);
} else {
MFS.print('NOTHING MODIFIED!');
}
const returnWT = !!(ro?.acquired?.length) || !!(rw?.acquired?.length);
if (returnWT) await MFS.WT.return(_info);
MFS.print('OPERATION COMPLETED SUCCESSFULLY!');
modified && MFS.print('PLEASE RELOAD YOUR BROWSER WINDOW!', {level: 2, error: true});
}
}
/**
* @memberof Help4.MigrateFioriSpaces#
* @return {{}|null}
* @private
*/
function _getHelpIds() {
const {methods} = Help4.selector;
const nodeList = document.querySelectorAll('[data-help-id][data-help-id2]');
const map = {};
nodeList.forEach(node => {
map[node.getAttribute('data-help-id')] = methods.$E(node.getAttribute('data-help-id2'));
});
return Object.keys(map).length ? map : null;
}
/**
* Collects a list of all SEN projects currently available on this screen.
* @private
* @memberof Help4.MigrateFioriSpaces#
* @param {Help4.model.ModelBase} model
* @param {Object} config
* @return {{ids: Help4.MigrateFioriSpaces.ProjectIdList, contextTypes: {ro: {}, rw: {}}}}
*/
function _getProjectIds(model, config) {
/**
* @param {Help4.model.ModelBase} model
* @return {{ids: string[], contextTypes: {}}}
*/
const extract = (model) => {
const toId = project => project._dataId;
const contextTypes = {};
/** @type {Set<string>} */
const ids = new Set();
[
{published: true, whatsNew: true},
{published: true, whatsNew: false},
{published: false, whatsNew: true},
{published: false, whatsNew: false}
]
.forEach(config => {
const help = model?.getScreenHelp(config);
if (help) {
const id = toId(help);
contextTypes[id] = help.contextType;
ids.add(id);
}
const tours = model
?.getScreenTours(config)
?.forEach(tour => {
const id = toId(tour);
contextTypes[id] = tour.contextType;
ids.add(id);
});
});
return {ids: [...new Set(ids)], contextTypes};
}
let roIds = [];
let rwIds = [];
let roCT = {};
let rwCT = {};
let r;
if (config.serviceLayer === Help4.SERVICE_LAYER.ext) {
if (config.roModel === Help4.SERVICE_LAYER.uacp) { // UACP migration not possible
Help4.MigrateFioriSpaces.print('RO model is UACP - will be ignored!', {warn: true, level: 2});
} else {
r = extract(model._ro);
roIds = r.ids;
roCT = r.contextTypes;
}
r = extract(model._rw);
rwIds = r.ids;
rwCT = r.contextTypes;
} else {
r = extract(model);
rwIds = r.ids;
rwCT = r.contextTypes;
}
return {
ids: {ro: roIds, rw: rwIds},
contextTypes: {ro: roCT, rw: rwCT}
};
}
/**
* Prepares the download information
* @private
* @memberof Help4.MigrateFioriSpaces#
* @param {Help4.MigrateFioriSpaces.ProjectIdList} ids
* @param {{ro: {}, rw: {}}} contextTypes
* @param {{isEXT: boolean, serviceUrl: string, serviceUrl2: string, model: Help4.model.ModelBase}} config
*/
function _createInfo({ro, rw}, {ro: roCT, rw: rwCT}, {isEXT, serviceUrl, serviceUrl2, model}) {
const {_info} = this;
if (isEXT) {
const {_ro, _rw} = model;
// ignore RO as we will not get any WTs for it
// if (ro.length) _info.ro = {url: serviceUrl, ids: ro, contextTypes: roCT, xcsrf: _ro.getXcsrfId()};
if (rw.length) _info.rw = {url: serviceUrl2, ids: rw, contextTypes: rwCT, xcsrf: _rw.getXcsrfId()};
} else {
if (rw.length) _info.rw = {url: serviceUrl, ids: rw, contextTypes: rwCT, xcsrf: model.getXcsrfId()};
}
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
*/
const setupServerConfig = info => {
const {url} = info;
info.serverBaseUrl = url.replace(/\/wa\/.*$/, '');
info.waBase = url.replace(/^.*?(\/wa\/)/, '$1');
info.waName = info.waBase.split('/')[2]; // scheme is "/wa/<name>"
}
_info.ro && setupServerConfig(_info.ro);
_info.rw && setupServerConfig(_info.rw);
}
})();
(function() {
/**
* Write Token operation class
*/
Help4.MigrateFioriSpaces.WT = class {
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
* @return {Promise<void>}
*/
static async check(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @return {Promise<Help4.MigrateFioriSpaces.EntityInfo[]>}
*/
const queryServer = info => {
return new Help4.Promise((resolve, reject) => {
const {serverBaseUrl, waBase, ids, xcsrf} = info;
const request = ids.map(id => ({
url: `${waBase}/project/${id}`,
method: 'GET',
body: '{}'
}));
request.unshift({url: '/self', method: 'GET', body: '{}'});
const success = ({response}) => {
info.whoAmI = response.shift();
resolve(response);
}
Help4.ajax.Ajax({
headers: MFS.NO_CACHE_HEADERS,
method: 'POST',
url: `${serverBaseUrl}/multiple_request`,
data: {request},
saml: true,
xcsrf,
success,
error: () => reject(new Error(`Unable to communicate with server: ${serverBaseUrl}`))
});
});
}
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string} name
* @return {Promise<void>}
*/
const loadAndCheck = async (info, name) => {
const response = await queryServer(info);
const me = info.whoAmI.name;
MFS.print(`${name} - Who am I: ${me}`, {level: 2});
info.entities = [];
/** @type {string[]} */
const blocked = info.blocked = [];
/** @type {string[]} */
const toBeAcquired = info.toBeAcquired = [];
response.forEach(entity => {
const {wt, wt_location, wt_owner, Streams} = entity.Meta;
const {uid} = entity;
info.entities.push({Streams, uid});
if (wt == 1) {
toBeAcquired.push(uid);
} else if (wt_location !== '' || wt_owner !== me) {
blocked.push(uid);
}
});
if (blocked.length) {
// blocked WT exist, abort further operation
MFS.printEntityUrls(info, blocked, {text: `${name} - Blocked WTs:`, error: true, abort: true});
}
if (toBeAcquired.length) {
MFS.printEntityUrls(info, toBeAcquired, {text: `${name} - WTs to be acquired:`});
} else {
MFS.print(`${name} - All WTs already acquired`, {level: 2});
}
}
// do NOT implement parallel XHR query execution as the SEN server has issues doing this
// and might fail with SAML and log us in as anonymous
const {ro, rw} = info;
MFS.print('Checking projects on server');
if (ro) await loadAndCheck(ro, 'RO');
if (rw) await loadAndCheck(rw, 'RW');
}
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
* @return {Promise<void>}
*/
static async acquire(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string} name
* @return {Promise<void>}
*/
const acquireWTs = (info, name) => {
return new Help4.Promise((resolve, reject) => {
const {toBeAcquired, serverBaseUrl, waBase, whoAmI: {name: me}, xcsrf} = info;
if (!toBeAcquired.length) return resolve();
const body = JSON.stringify({self_get_wt: 1});
const request = toBeAcquired.map(uid => ({
url: `${waBase}/project/${uid}`,
method: 'POST',
body
}));
const success = ({response}) => {
request.forEach((item, index) => {
const {wt, wt_k, wt_location, wt_owner} = response[index].response;
item.success = wt == 0 && !!wt_k && wt_location === '' && wt_owner === me;
item.uid = item.url.replace(/^.*\//, '');
});
/** @type {string[]} */
const acquired = info.acquired = [];
/** @type {string[]} */
const notAcquired = info.notAcquired = [];
request.forEach(item => item.success ? acquired.push(item.uid) : notAcquired.push(item.uid));
MFS.printEntityUrls(info, acquired, {text: `${name} - Acquired WTs:`});
if (notAcquired.length) {
MFS.printEntityUrls(info, notAcquired, {text: `${name} - Not acquired WTs:`, error: true, abort: true});
}
resolve();
}
Help4.ajax.Ajax({
headers: MFS.NO_CACHE_HEADERS,
method: 'POST',
url: `${serverBaseUrl}/multiple_request`,
data: {request},
saml: true,
xcsrf,
success,
error: () => reject(new Error(`Unable to communicate with server: ${serverBaseUrl}`))
});
});
};
const {ro, rw} = info;
MFS.print('Acquiring missing WTs');
if (ro) await acquireWTs(ro, 'RO');
if (rw) await acquireWTs(rw, 'RW');
}
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
* @return {Promise<void>}
*/
static async return(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string} name
* @return {Promise<void>}
*/
const returnWTs = (info, name) => {
return new Help4.Promise((resolve, reject) => {
const {acquired, serverBaseUrl, waBase, whoAmI: {name: me}, xcsrf} = info;
if (!acquired?.length) return resolve();
const body = JSON.stringify({self_return_wt: 1});
const request = acquired.map(uid => ({
url: `${waBase}/project/${uid}`,
method: 'POST',
body
}));
const success = ({response}) => {
request.forEach((item, index) => {
const {wt} = response[index].response;
item.success = wt == 1;
item.uid = item.url.replace(/^.*\//, '');
});
/** @type {string[]} */
const returned = info.returned = [];
/** @type {string[]} */
const notReturned = info.notReturned = [];
request.forEach(item => item.success ? returned.push(item.uid) : notReturned.push(item.uid));
MFS.printEntityUrls(info, returned, {text: `${name} - Returned WTs:`});
if (notReturned.length) {
MFS.printEntityUrls(info, notReturned, {text: `${name} - Not returned WTs:`, error: true, abort: true});
}
resolve();
}
Help4.ajax.Ajax({
headers: MFS.NO_CACHE_HEADERS,
method: 'POST',
url: `${serverBaseUrl}/multiple_request`,
data: {request},
saml: true,
xcsrf,
success,
error: () => reject(new Error(`Unable to communicate with server: ${serverBaseUrl}`))
});
});
}
const {ro, rw} = info;
MFS.print('Returning acquired WTs');
if (ro) await returnWTs(ro, 'RO');
if (rw) await returnWTs(rw, 'RW');
}
}
})();
(function() {
/**
* Entity data operation class
*/
Help4.MigrateFioriSpaces.Entity = class {
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
* @return {Promise<void>}
*/
static async download(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string} name
* @return {Promise<void>}
*/
const download = (info, name) => {
const ACCEPTED_STREAMS = ['entity.txt', 'project.dpr'];
const DATA_TYPE = {'project.dpr': 'xml'};
return new Help4.Promise((resolve, reject) => {
const {serverBaseUrl, waBase, xcsrf} = info;
const request = [];
info.entities.forEach(entity => {
ACCEPTED_STREAMS.forEach((name) => {
const url = `${waBase}/project/${entity.uid}/${name}`;
const dataType = DATA_TYPE[name];
dataType ? request.push({url, dataType}) : request.push({url});
});
});
const success = (response) => {
const result = {};
request.forEach((item, index) => {
const uid = item.url.replace(/^.*project\/(.*)\/.*$/, '$1');
const filename = item.url.replace(/^.*\/(.*)$/, '$1');
const stream = filename.replace(/\./, '_');
let data = response[index];
result[filename] ||= [];
result[filename].push(uid);
const idx = Help4.indexOf(info.entities, 'uid', uid);
if (idx >= 0) info.entities[idx][stream] = data;
});
for (let [filename, list] of Object.entries(result)) {
MFS.printEntityUrls(info, list, {text: `${name} - Downloaded "${filename}" for:`});
}
resolve();
}
Help4.ajax.EnableNow({
headers: MFS.NO_CACHE_HEADERS,
method: 'POST',
url: `${serverBaseUrl}/multifile`,
dataType: 'multipart',
data: {request},
saml: true,
xcsrf,
success,
error: () => reject(new Error(`Unable to communicate with server: ${serverBaseUrl}`))
});
});
}
const {ro, rw} = info;
MFS.print('Downloading entities');
if (ro) await download(ro, 'RO');
if (rw) await download(rw, 'RW');
}
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
*/
static replace(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {Object} migrationMap
* @param {string} name
*/
const replace = (info, migrationMap, name) => {
const {Selector, methods: {DataAttrSelector}} = Help4.selector;
const ACCEPTED_TEMPLATES = {help_tile: true, guide_click: true};
const ACCEPTED_RULE = 'DataAttrSelector';
const MACRO_ID = 'Macro';
const INIT_ID = 'Init';
const TEMPLATE_ID = 'template_name';
const HOTSPOT_ID = 'hotspot';
/** @type {string[]} */
const modified = [];
/** @type {Help4.MigrateFioriSpaces.EntityStreamList} */
for (const entity of info.entities) {
const projectDpr = entity.project_dpr;
const macros = projectDpr?.getElementsByTagName(MACRO_ID) || [];
for (const macro of macros) {
const init = macro.getElementsByTagName(INIT_ID)[0];
const templateName = init?.getAttribute(TEMPLATE_ID);
if (!ACCEPTED_TEMPLATES[templateName]) continue;
let attrib = init.getAttribute(HOTSPOT_ID);
/** @type {{rule: string, value: string}|null} */
const hotspot = attrib ? Selector.base64ToUtf8(attrib) : null;
if (hotspot?.rule !== ACCEPTED_RULE) continue;
const recordedValue = hotspot.value.match(/data-help-id=(['"])(.*?)\1/)?.[2]; // get data-help-id from recording
if (!recordedValue) continue;
// the data-help-id from the element is different than the one from the recording
// as the recorded value is run through Help4.selector.method.$E
const element = DataAttrSelector.getElement(hotspot.value);
const elementValue = element?.getAttribute('data-help-id'); // get the ID from DOM
if (!elementValue || !migrationMap[elementValue]) continue; // check whether migration is required
const newValue = hotspot.value.replace(recordedValue, migrationMap[elementValue]); // the migrationMap[elementValue] is already escaped; see _getHelpIds
if (hotspot.value !== newValue) {
hotspot.value = newValue;
attrib = Selector.utf8ToBase64(hotspot);
init.setAttribute(HOTSPOT_ID, attrib);
modified.push(entity.uid);
}
}
}
info.modified = modified;
MFS.printEntityUrls(info, modified, {text: `${name} - Modified "project.dpr" for:`});
}
MFS.print('Replacing IDs');
const {ro, rw} = info;
ro && replace(ro, info.migrationMap, 'RO');
rw && replace(rw, info.migrationMap, 'RW');
}
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
*/
static createLessonJS(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string} name
*/
const create = (info, name) => {
const {XmlHelper} = Help4.model.wpb;
const {modified, entities, contextTypes} = info;
for (const uid of modified) {
const index = Help4.indexOf(entities, 'uid', uid);
const entity = entities[index];
if (!entity) continue;
XmlHelper.set(entity.project_dpr);
XmlHelper.setMode(contextTypes[entity.uid].toLowerCase());
entity.lesson_js = XmlHelper.toLessonJs(entity.entity_txt);
}
MFS.printEntityUrls(info, modified, {text: `${name} - Created:`});
}
MFS.print('Creating "lesson.js"');
const {ro, rw} = info;
ro && create(ro, 'RO');
rw && create(rw, 'RW');
}
/**
* @param {Help4.MigrateFioriSpaces.MigrationInfo} info
*/
static async save(info) {
const {MigrateFioriSpaces: MFS} = Help4;
/**
* @param {Object} data
* @param {string} delimiter
* @return {string}
*/
const createMultipartContent = (data, delimiter) => {
const c = [];
for (const [key, value] of Object.entries(data)) {
c.push(...createMultipartContentPart(key, value, delimiter));
}
c.push(...createMultipartContentPart('end', null, delimiter));
return c.join('\r\n');
}
/**
* @param {string} type
* @param {string|null} data
* @param {string} del
* @return {string[]}
*/
const createMultipartContentPart = (type, data, del) => {
const d = {
meta: ['Content-Disposition: form-data; name="meta"'],
projectDpr: ['Content-Disposition: form-data; name="file"; filename="project.dpr"', 'Content-Type: text/html'],
lessonJs: ['Content-Disposition: form-data; name="file"; filename="lesson.js"', 'Content-Type: text/html']
};
let r = ['--part-' + del];
if (d[type]) {
if (!data) return [];
r.push(...d[type], '', data);
} else if (type === 'end') {
r[0] += '--';
} else {
return [];
}
return r;
}
/**
* @param {string} serverUrl
* @param {string} xcsrf
* @param {string} uid
* @param {string} projectDpr
* @param {string} lessonJs
* @return {Promise<boolean>}
*/
const save = (serverUrl, xcsrf, uid, projectDpr, lessonJs) => {
const delimiter = Help4.createId();
const meta = Help4.JSON.stringify({
files_complete: true,
send_mail_to_watchers: false
});
const content = createMultipartContent({meta, projectDpr, lessonJs}, delimiter);
return new Help4.Promise(resolve => {
Help4.ajax.Ajax({
url: serverUrl + uid,
method: 'POST',
headers: {'Content-Type': 'multipart/form-data; boundary=part-' + delimiter},
dataType: 'text',
data: content,
saml: true,
xcsrf,
success: () => resolve(true),
error: () => resolve(false)
});
});
}
/**
* @param {Help4.MigrateFioriSpaces.DownloadInfo} info
* @param {string} name
* @return {Promise<void>}
*/
const prepare = async (info, name) => {
const {XmlHelper} = Help4.model.wpb;
const {modified, entities, contextTypes, serverBaseUrl, waBase, xcsrf} = info;
const serverUrl = `${serverBaseUrl}${waBase}/project/`;
const saved = [], failed = [];
for (const uid of modified) {
const index = Help4.indexOf(entities, 'uid', uid);
const entity = entities[index];
if (!entity) continue;
XmlHelper.set(entity.project_dpr);
XmlHelper.setMode(contextTypes[entity.uid].toLowerCase());
const xml = window.ActiveXObject
? entity.project_dpr.xml
: (new XMLSerializer()).serializeToString(entity.project_dpr);
const json = Help4.JSON.stringify(entity.lesson_js);
const success = await save(serverUrl, xcsrf, uid, xml, json);
success ? saved.push(uid) : failed.push(uid);
}
info.saved = saved;
info.failed = failed;
saved.length && MFS.printEntityUrls(info, saved, {text: `${name} - Saved:`});
failed.length && MFS.printEntityUrls(info, failed, {text: `${name} - Failed:`, error: true, abort: true});
}
MFS.print('Saving entities');
const {ro, rw} = info;
if (ro) await prepare(ro, 'RO');
if (rw) await prepare(rw, 'RW');
}
}
})();