@gmetrixr/rjson
Version:
(R)ecursive Json
879 lines • 70.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectUtils = exports.ProjectFactory = void 0;
const ramda_1 = require("ramda");
const variables_1 = require("../definitions/variables");
const VariableTypes_1 = require("../definitions/variables/VariableTypes");
const RecordFactory_1 = require("../R/RecordFactory");
const RecordNode_1 = require("../R/RecordNode");
const RecordTypes_1 = require("../R/RecordTypes");
const SceneFactory_1 = require("./SceneFactory");
const gdash_1 = require("@gmetrixr/gdash");
const ElementFactory_1 = require("./ElementFactory");
const definitions_1 = require("../definitions");
const Project_1 = require("../recordTypes/Project");
const Element_1 = require("../recordTypes/Element");
const Item_1 = require("../recordTypes/Item");
const Options_1 = require("../recordTypes/Options");
const Shopping_1 = require("../recordTypes/Shopping");
const Menu_1 = require("../recordTypes/Menu");
const ElementDefinition_1 = require("../definitions/elements/ElementDefinition");
const rules_1 = require("../definitions/rules");
const special_1 = require("../definitions/special");
const SpecialTypes_1 = require("../definitions/special/SpecialTypes");
const { deepClone, difference, union, intersection } = gdash_1.jsUtils;
const variable = RecordTypes_1.RT.variable;
/**
* @example
* projectFactory1 = r.project(projectJson): ProjectFactory
* project1 = r.project(projectJson).json(): RecordNode<project> (project1 === projectJson)
*
* Add Scene:
* t.project(projectJson).addScene(scene1Json).json(): RecordNode<scene>
*/
class ProjectFactory extends RecordFactory_1.RecordFactory {
constructor(json) {
super(json);
}
/**
* addBlankRecord calls addRecord internally. So just overriding addRecord is enough.
*
* Override adding of scene -
* Also add scene_id to the menu in case auto_add_new_scene_to_menu is true
* Override adding of lead_gen_field -
* All lead gen fields need a corresponding variable in which its value is saved
* The corresponding variable is saved in the field var_id (of type string) in lead_gen_field
* These variable definitions (like name/type) shouldn't be modifiable by the user (read only = true)
*/
addRecord(record, position) {
super.addRecord(record, position);
//Custom Record Types' Code
switch (record.type) {
case RecordTypes_1.RT.scene: {
this.addMenuAndTourModeRecord(record.id);
break;
}
case RecordTypes_1.RT.lead_gen_field: {
const variableRecord = this.addVariableOfType(VariableTypes_1.VariableType.string);
variableRecord.props.var_category = VariableTypes_1.VarCategory.autogenerated;
variableRecord.props.var_track = true;
variableRecord.props.var_default = "";
//Keep the name of the variable same as the lead gen field
//Going via changeRecordName to take care of duplicate names
this.changeRecordName(RecordTypes_1.RT.variable, variableRecord.id, varNameFromOriginName(record.name));
//Link lead gen field to the variable id
record.props.var_id = variableRecord.id;
break;
}
}
return record;
}
/**
* Adding custom logic for:
* lead_gen_field: Renaming lead_gen_field should rename the linked variable name
*/
changeRecordName(type, id, newName) {
var _a;
const oldRecordName = (_a = this.getRecord(type, id)) === null || _a === void 0 ? void 0 : _a.name;
const record = super.changeRecordName(type, id, newName);
const newRecordName = record === null || record === void 0 ? void 0 : record.name; //Don't use newName as the new name, it might have undergone transformation
if (oldRecordName === undefined || newRecordName === undefined)
return undefined;
//Custom Record Types' Code
switch (type) {
case RecordTypes_1.RT.variable: {
const records = ProjectUtils.getAllTemplatedRecords(this._json);
ProjectUtils.updateStringTemplates(records, oldRecordName, newRecordName);
break;
}
case RecordTypes_1.RT.lead_gen_field: {
const linkedVarId = record.props.var_id;
this.changeRecordName(RecordTypes_1.RT.variable, linkedVarId, varNameFromOriginName(newRecordName));
break;
}
}
return record;
}
addElementOfTypeToScene({ sceneId, elementType, position, groupElementId }) {
const defaultName = ElementFactory_1.ElementUtils.getElementDefinition(elementType).elementDefaultName;
const newElement = (0, RecordNode_1.createRecord)(RecordTypes_1.RT.element, undefined, defaultName);
newElement.props = ElementFactory_1.ElementUtils.getElementTypeDefaults(elementType);
newElement.props.element_type = elementType;
const newRecord = this.addSceneDeepRecord({ sceneId, record: newElement, position, groupElementId });
return newRecord;
}
/**
* Updated version of addSceneSubRecord
* Takes in groupElementId to add record to nth level as well.
*/
addSceneDeepRecord({ sceneId, record, position, groupElementId }) {
const sceneJson = this.getRecord(RecordTypes_1.RT.scene, sceneId);
if (sceneJson !== undefined) {
const addedRecord = (new SceneFactory_1.SceneFactory(sceneJson)).addDeepRecord({ record, position, groupElementId });
if (addedRecord !== undefined) {
this.addLinkedVariables([addedRecord]);
return addedRecord;
}
}
return;
}
/**
* Ideally elements should be renamed via SceneFactory. But because want media_upload element rename to impact
* its linked variable, we do this via ProjectFactory (as only ProjectFactory has access to variables).
*/
changeSceneSubRecordName(sceneId, type, id, newName) {
const sceneJson = this.getRecord(RecordTypes_1.RT.scene, sceneId);
if (sceneJson !== undefined) {
const record = (new SceneFactory_1.SceneFactory(sceneJson)).changeDeepRecordName(type, id, newName);
const newRecordName = record === null || record === void 0 ? void 0 : record.name;
if (newRecordName !== undefined && (record === null || record === void 0 ? void 0 : record.type) === RecordTypes_1.RT.element) {
const currentRecord = record;
switch (currentRecord.props.element_type) {
case ElementDefinition_1.ElementType.media_upload: {
const linkedVarId = currentRecord.props.media_upload_var_id;
this.changeRecordName(RecordTypes_1.RT.variable, linkedVarId, varNameFromOriginName(newRecordName));
break;
}
case ElementDefinition_1.ElementType.embed_scorm: {
const linkedScoreVarId = currentRecord.props.embed_scorm_score_var_id;
this.changeRecordName(RecordTypes_1.RT.variable, linkedScoreVarId, varNameFromOriginName(newRecordName));
const linkedSuspendDataVarId = currentRecord.props.embed_scorm_suspend_data_var_id;
this.changeRecordName(RecordTypes_1.RT.variable, linkedSuspendDataVarId, varNameFromOriginName(newRecordName));
const linkedProgressVarId = currentRecord.props.embed_scorm_progress_var_id;
this.changeRecordName(RecordTypes_1.RT.variable, linkedProgressVarId, varNameFromOriginName(newRecordName));
break;
}
}
}
return record;
}
}
/**
* Ideally elements should get deleted via SceneFactory. But because we want deletion of media_upload element to delete the linked
* variable, we do this via ProjectFactory (as only ProjectFactory has access to variables).
*/
deleteSceneDeepRecord(sceneId, type, id) {
const sceneJson = this.getRecord(RecordTypes_1.RT.scene, sceneId);
if (sceneJson !== undefined) {
const record = (new SceneFactory_1.SceneFactory(sceneJson)).deleteDeepRecord(type, id);
if (record !== undefined) {
const recordsWithLinkedVariables = this.getAllRecordsForLinkedVariables([record]);
this.deleteLinkedVariables(recordsWithLinkedVariables);
return record;
}
}
return;
}
/**
* Update from previous function.
* This has been written in Project Factory so that we can add linked variables to elements like SCORM and Media Upload
* since only Project Factory has access to variables.
*/
duplicateSceneDeepRecord(sceneId, type, id) {
const sceneJson = this.getRecord(RecordTypes_1.RT.scene, sceneId);
if (sceneJson !== undefined) {
const sceneF = (new SceneFactory_1.SceneFactory(sceneJson));
const duplicatedRecord = sceneF.duplicateDeepRecord(type, id);
if (duplicatedRecord !== undefined) {
const recordsWithLinkedVariables = this.getAllRecordsForLinkedVariables([duplicatedRecord]);
this.addLinkedVariables(recordsWithLinkedVariables);
return duplicatedRecord;
}
}
return;
}
/**
* Override from record factory
* Since only Project Factory has access to variables, get the deep records in the override and add the linked variables for SCORM and Media Upload.
*/
duplicateDeepRecord(type, id) {
const duplicatedRecord = super.duplicateDeepRecord(type, id);
if (duplicatedRecord !== undefined) {
switch (type) {
case RecordTypes_1.RT.scene: {
const sceneF = new SceneFactory_1.SceneFactory(duplicatedRecord);
const elements = sceneF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.element, e => definitions_1.en.elementsWithLinkedVariables.includes(e.props.element_type));
this.addLinkedVariables(elements);
this.addMenuAndTourModeRecord(duplicatedRecord.id);
break;
}
}
return duplicatedRecord;
}
return;
}
/**
* Adding custom logic for:
* variable: Deleting a variable should also delete its rules
* scene: Should delete from menu and tour_mode lists also
* lead_gen_field: Should delete the linked autogenerated variable
*/
deleteRecord(type, id) {
switch (type) {
case RecordTypes_1.RT.variable: {
for (const scene of this.getRecords(RecordTypes_1.RT.scene)) {
(new SceneFactory_1.SceneFactory(scene)).deleteRulesForCoId(id);
}
break;
}
case RecordTypes_1.RT.scene: {
const scene = this.getRecord(RecordTypes_1.RT.scene, id);
if (scene !== undefined) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
const childrenWithLinkedVariables = sceneF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.element, e => definitions_1.en.elementsWithLinkedVariables.includes(e.props.element_type));
this.deleteLinkedVariables(childrenWithLinkedVariables);
const linkedMenuId = sceneF.get(RecordTypes_1.rtp.scene.linked_menu_id);
if (typeof linkedMenuId === "number") {
this.deleteRecord(RecordTypes_1.RT.menu, linkedMenuId);
}
const linkedTourModeId = sceneF.get(RecordTypes_1.rtp.scene.linked_tour_mode_id);
if (typeof linkedTourModeId === "number") {
this.deleteRecord(RecordTypes_1.RT.tour_mode, linkedTourModeId);
}
}
break;
}
case RecordTypes_1.RT.lead_gen_field: {
const leadGenField = this.getRecord(RecordTypes_1.RT.lead_gen_field, id);
const varId = leadGenField === null || leadGenField === void 0 ? void 0 : leadGenField.props.var_id;
if (varId !== undefined) {
const variable = this.getRecord(RecordTypes_1.RT.variable, varId);
if (variable !== undefined) {
this.deleteRecord(RecordTypes_1.RT.variable, varId);
}
}
break;
}
}
return super.deleteRecord(type, id);
}
//VARIABLE SPECIFIC FUNCTIONS
/**
* Override general variable defaults with the defaults specified for the given variableType
*/
addVariableOfType(variableType, id) {
const defaults = variables_1.variableTypeToDefn[variableType];
const record = (0, RecordNode_1.createRecord)(variable, id, defaults.varDefaultName);
record.props.var_default = defaults.varDefaultValue;
record.props.var_type = variableType;
record.props.var_category = VariableTypes_1.VarCategory.user_defined;
this.addRecord(record);
return record;
}
/**
* Variables linked to specific functions, that need specific fixed ids (and names)
* Predefined variables cannot be renamed or delted by the user, and are not shown on Variables interface
*/
addPredefinedVariable(predefinedVariableName) {
const pdvarDefaults = VariableTypes_1.predefinedVariableDefaults[predefinedVariableName];
let record = this.getRecord(RecordTypes_1.RT.variable, pdvarDefaults.id);
if (record === undefined) {
record = this.addVariableOfType(pdvarDefaults.type, pdvarDefaults.id);
record.props.var_category = VariableTypes_1.VarCategory.predefined;
record.props.var_track = true;
record.name = predefinedVariableName;
}
return record;
}
addGlobalVariable(globalVar) {
const record = deepClone(globalVar);
record.props.var_category = VariableTypes_1.VarCategory.global;
record.props.var_track = true;
return this.addRecord(globalVar);
}
/**
* This needs to be done everytime before the project is loaded in the editor.
* Because the definitions of the global vars might get updated anytime.
*/
updateGlobalVariableProperties(gvsMap) {
for (const variable of this.getRecords(RecordTypes_1.RT.variable)) {
if (variable.props.var_category === VariableTypes_1.VarCategory.global) {
const variableFromOrg = gvsMap[variable.id];
if (variableFromOrg !== undefined) {
variable.name = variableFromOrg.name;
variable.props.var_default = variableFromOrg.props.var_default;
variable.props.var_type = variableFromOrg.props.var_type;
}
else {
console.log(`Variable ${variable.id} was marked global but not found in the passed global vars map. Marking it non-global`);
variable.props.var_category = VariableTypes_1.VarCategory.user_defined;
}
}
}
}
/**
* Function to be used to add linked variables for element types like SCORM and Media Upload.
* Just need to update this function for any other element type to be added in future
* Takes the elements as an array and adds variables to project factory
*/
addLinkedVariables(records) {
for (const record of records) {
if ((record === null || record === void 0 ? void 0 : record.type) === RecordTypes_1.RT.element) {
switch (record.props.element_type) {
case ElementDefinition_1.ElementType.media_upload: {
const variableRecord = this.addVariableOfType(VariableTypes_1.VariableType.string);
variableRecord.props.var_category = VariableTypes_1.VarCategory.autogenerated;
variableRecord.props.var_track = true;
variableRecord.props.var_default = "";
//Keep the name of the variable same as the lead gen field
//Going via changeRecordName to take care of duplicate names
this.changeRecordName(RecordTypes_1.RT.variable, variableRecord.id, varNameFromOriginName(record.name));
//Link lead gen field to the variable id
record.props.media_upload_var_id = variableRecord.id;
break;
}
case ElementDefinition_1.ElementType.embed_scorm: {
const scoreVariableRecord = this.addVariableOfType(VariableTypes_1.VariableType.number);
scoreVariableRecord.props.var_category = VariableTypes_1.VarCategory.autogenerated;
scoreVariableRecord.props.var_track = true;
scoreVariableRecord.props.var_default = 0;
this.changeRecordName(RecordTypes_1.RT.variable, scoreVariableRecord.id, varNameFromOriginName(record.name));
record.props.embed_scorm_score_var_id = scoreVariableRecord.id;
const suspendDataVariableRecord = this.addVariableOfType(VariableTypes_1.VariableType.string);
suspendDataVariableRecord.props.var_category = VariableTypes_1.VarCategory.autogenerated;
suspendDataVariableRecord.props.var_track = true;
suspendDataVariableRecord.props.var_default = 0;
this.changeRecordName(RecordTypes_1.RT.variable, suspendDataVariableRecord.id, varNameFromOriginName(record.name));
record.props.embed_scorm_suspend_data_var_id = suspendDataVariableRecord.id;
const progressVariableRecord = this.addVariableOfType(VariableTypes_1.VariableType.number);
progressVariableRecord.props.var_category = VariableTypes_1.VarCategory.autogenerated;
progressVariableRecord.props.var_track = true;
progressVariableRecord.props.var_default = 0;
this.changeRecordName(RecordTypes_1.RT.variable, progressVariableRecord.id, varNameFromOriginName(record.name));
record.props.embed_scorm_progress_var_id = progressVariableRecord.id;
break;
}
}
}
}
}
/**
* Function to be used to delete linked variables for element types like SCORM and Media Upload.
* Just need to update this function for any other element type to be added in future
* Takes the elements as an array and deletes variables from project factory
*/
deleteLinkedVariables(records) {
for (const record of records) {
if ((record === null || record === void 0 ? void 0 : record.type) === RecordTypes_1.RT.element) {
switch (record.props.element_type) {
case ElementDefinition_1.ElementType.media_upload: {
const linkedVarId = record.props.media_upload_var_id;
this.deleteRecord(RecordTypes_1.RT.variable, linkedVarId);
break;
}
case ElementDefinition_1.ElementType.embed_scorm: {
const linkedScoreVarId = record.props.embed_scorm_score_var_id;
this.deleteRecord(RecordTypes_1.RT.variable, linkedScoreVarId);
const linkedSuspedDataVarId = record.props.embed_scorm_suspend_data_var_id;
this.deleteRecord(RecordTypes_1.RT.variable, linkedSuspedDataVarId);
const linkedProgressVarId = record.props.embed_scorm_progress_var_id;
this.deleteRecord(RecordTypes_1.RT.variable, linkedProgressVarId);
break;
}
}
}
}
}
//SCENE SPECIFIC FUNCTIONS
getInitialSceneId() {
const initialSceneId = this.get(RecordTypes_1.rtp.project.initial_scene_id);
//Ignore the default value 0, it means its not set
if (initialSceneId !== undefined && initialSceneId !== 0) {
// also check if a scene with this id is valid
const scene = this.getRecord(RecordTypes_1.RT.scene, initialSceneId);
if (scene) {
return Number(initialSceneId);
}
else {
return this.getRecordOrder(RecordTypes_1.RT.scene)[0];
}
}
else {
return this.getRecordOrder(RecordTypes_1.RT.scene)[0];
}
}
addMenuAndTourModeRecord(sceneId) {
const scene = this.getRecord(RecordTypes_1.RT.scene, sceneId);
if (scene) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
//Add menu entry. Calling super.addBlankRecord and not ProjectFactory.addBlankRecord because internally it call addRecord,
//would end up in a cyclic call.
const menuRecord = super.addBlankRecord(RecordTypes_1.RT.menu, sceneId + 10001);
menuRecord.props.menu_scene_id = sceneId;
menuRecord.props.menu_show = this.getValueOrDefault(RecordTypes_1.rtp.project.auto_add_new_scene_to_menu);
sceneF.set(RecordTypes_1.rtp.scene.linked_menu_id, menuRecord.id);
// Adding scene details every time to menu prop and making the boolean menu_show true / false based on the value given or default which is true.
if (this.getValueOrDefault(RecordTypes_1.rtp.project.auto_add_new_scene_to_tour_mode) === true) {
//Making id deterministic (although not needed) - for testing
const tourModeRecord = super.addBlankRecord(RecordTypes_1.RT.tour_mode, sceneId + 10002);
tourModeRecord.props.tour_mode_scene_id = sceneId;
sceneF.set(RecordTypes_1.rtp.scene.linked_tour_mode_id, menuRecord.id);
}
}
}
//PROJECT SPECIFIC FUNCTIONS
/**
* Note: A thumbnail is different form logo.
* Logo is displayed above clickToStart. Can be the org logo for example.
* Thumbnail shows is displayed in project listing page. Can be distinguish the project visually.
*
* Temporary until a full-fledged thumbnail generator is created
* Works only on projects that have gone through "injectSourceIntoProject" (i.e. its source.file_urls are populated)
* This just gets the first scene of the project and returns the thubmnail of the first pano it finds in it
*/
getProjectThumbnail() {
var _a, _b, _c, _d;
let thumbnail;
const projectThumbnailSource = this.get(RecordTypes_1.rtp.project.project_thumbnail_source);
if (projectThumbnailSource !== undefined && ((_a = projectThumbnailSource.file_urls) === null || _a === void 0 ? void 0 : _a.o)) {
return projectThumbnailSource.file_urls.o;
}
const initialSceneId = this.getInitialSceneId();
if (initialSceneId === undefined)
return undefined;
const scene = this.getRecord(RecordTypes_1.RT.scene, initialSceneId);
let sceneF;
let elementF;
if (scene !== undefined) {
sceneF = new SceneFactory_1.SceneFactory(scene);
for (const e of sceneF.getAllDeepChildren(RecordTypes_1.RT.element)) {
elementF = new ElementFactory_1.ElementFactory(e);
if (elementF.getElementType() === definitions_1.en.ElementType.pano_image) {
const source = elementF.get(RecordTypes_1.rtp.element.source);
const newThumbnail = (_c = (_b = source === null || source === void 0 ? void 0 : source.file_urls) === null || _b === void 0 ? void 0 : _b.t) !== null && _c !== void 0 ? _c : (_d = source === null || source === void 0 ? void 0 : source.file_urls) === null || _d === void 0 ? void 0 : _d.o;
if (newThumbnail !== undefined) {
thumbnail = newThumbnail;
break;
}
}
}
}
return thumbnail;
}
getFileIdsFromProject() {
const fileIds = [];
for (const scene of this.getRecords(RecordTypes_1.RT.scene)) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
for (const element of sceneF.getAllDeepChildren(RecordTypes_1.RT.element)) {
fileIds.push(...new ElementFactory_1.ElementFactory(element).getFileIdsFromElement());
}
}
const projectLogo = this.get(RecordTypes_1.rtp.project.project_logo_source);
if (projectLogo !== undefined) {
fileIds.push(projectLogo.id);
}
const projectThumbnail = this.get(RecordTypes_1.rtp.project.project_thumbnail_source);
if (projectThumbnail !== undefined) {
fileIds.push(projectThumbnail.id);
}
return [...new Set(fileIds)]; //Unique ids only
}
injectSourceIntoProject(sourceMap) {
for (const scene of this.getRecords(RecordTypes_1.RT.scene)) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
for (const element of sceneF.getAllDeepChildren(RecordTypes_1.RT.element)) {
new ElementFactory_1.ElementFactory(element).injectSourceIntoElement(sourceMap);
}
}
const projectLogo = this.get(RecordTypes_1.rtp.project.project_logo_source);
if (projectLogo !== undefined) {
const newValue = sourceMap[projectLogo.id];
if (newValue !== undefined) {
this.set(RecordTypes_1.rtp.project.project_logo_source, newValue);
}
}
const projectThumbnail = this.get(RecordTypes_1.rtp.project.project_thumbnail_source);
if (projectThumbnail !== undefined) {
const newValue = sourceMap[projectThumbnail.id];
if (newValue !== undefined) {
this.set(RecordTypes_1.rtp.project.project_thumbnail_source, newValue);
}
}
}
getMetadata() {
// 1. Project meta fields
const metaArray = [
this.get(Project_1.ProjectProperty.description),
this.get(Project_1.ProjectProperty.project_start_description),
this.get(Project_1.ProjectProperty.project_end_description)
];
// 2. Scene DOESN'T have meta fields
for (const scene of this.getRecords(RecordTypes_1.RT.scene)) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
for (const element of sceneF.getAllDeepChildren(RecordTypes_1.RT.element)) {
// 3. Element meta fields
const elementF = new ElementFactory_1.ElementFactory(element);
metaArray.push(elementF.get(Element_1.ElementProperty.text));
metaArray.push(elementF.get(Element_1.ElementProperty.ssml));
metaArray.push(elementF.get(Element_1.ElementProperty.placeholder_text));
metaArray.push(elementF.get(Element_1.ElementProperty.embed_string));
metaArray.push(elementF.get(Element_1.ElementProperty.description));
metaArray.push(elementF.get(Element_1.ElementProperty.short_description));
// 4. Items meta fields
for (const itemRecord of elementF.getRecords(RecordTypes_1.RT.item)) {
const itemF = new RecordFactory_1.RecordFactory(itemRecord);
metaArray.push(itemF.get(Item_1.ItemProperty.item_description));
metaArray.push(itemF.get(Item_1.ItemProperty.item_instruction));
metaArray.push(itemF.get(Item_1.ItemProperty.item_text));
metaArray.push(itemF.get(Item_1.ItemProperty.phrase));
// 5. Options meta fields
for (const optionRecord of itemF.getRecords(RecordTypes_1.RT.option)) {
const optionF = new RecordFactory_1.RecordFactory(optionRecord);
metaArray.push(optionF.get(Options_1.OptionProperty.option_text));
}
}
}
}
for (const menuItem of this.getRecords(RecordTypes_1.RT.menu)) {
const menuItemR = new RecordFactory_1.RecordFactory(menuItem);
if (menuItemR.get(Menu_1.MenuProperty.menu_show) === true) {
metaArray.push(menuItemR.get(Menu_1.MenuProperty.menu_display_name));
}
}
// 6. Shopping meta fields
for (const shoppingRecord of this.getRecords(RecordTypes_1.RT.shopping)) {
const shoppingF = new RecordFactory_1.RecordFactory(shoppingRecord);
metaArray.push(shoppingF.get(Shopping_1.ShoppingProperty.store_name));
}
return metaArray
.filter(metaProp => metaProp !== undefined && metaProp !== "" && metaProp !== null)
.map(metaProp => metaProp
.replace(/\n/g, " ") // replace newline with space
.replace(/_/g, " ") // replace "_" with space
.replace(/{[^}]*}*/gm, "") // removes {{abc}}
.replace(/<[^>]*>?/gm, "") // removes html
.trim());
}
/**
* Scene copy logic:
* In scene rules, vars can be referenced via ids or via variable names (in templates used in elements)
* Copy all variables referenced.
* @param ids - list of ids for child records
*/
copyToClipboardObject(ids) {
var _a;
const baseClipboardObject = super.copyToClipboardObject(ids);
/**
* Parse through the extracted records to check if any variable needs to be added to the clipboard object
* 1. find all variables used
* 1.1 directly in rules (when events and then actions)
* 1.2 indirectly as templates in when_events, then_actions and text element
*/
// * create a map of all possible variable names to look through in rules
const varDefROM = (_a = this.getROM(RecordTypes_1.RT.variable)) !== null && _a !== void 0 ? _a : (0, RecordNode_1.emptyROM)();
const idNameMap = {};
for (const vDef of Object.values(varDefROM.map)) {
//If predefined var, get name from definitions. Predefined vars definitions don't need to be present in the project json
if (variables_1.predefinedVariableIdToName[vDef.id] !== undefined) {
idNameMap[vDef.id] = variables_1.predefinedVariableIdToName[vDef.id]; //Predefined variable type is also the name
}
{
idNameMap[vDef.id] = vDef.name;
}
}
// * using a Set here to store var ids since Set handles uniqueness in entries
const varsSet = new Set();
/**
* variable can be used in the following places:
* 1. rules on variables - straightforward, find all when events and then actions on variables
* 2. when event / then action properties templating - find all when events and then actions with properties.length > 0 and find variables that are templated
* 3. text element templating - look through all text elements and find variables used in templating
*/
baseClipboardObject.nodes
.filter(rn => rn.type === RecordTypes_1.RT.scene)
.forEach((current) => {
const recordF = new SceneFactory_1.SceneFactory(current);
// * get all when events that use variables or have properties defined
recordF
.getAllDeepChildrenWithFilter(RecordTypes_1.RT.when_event, (r) => (0, variables_1.isVariableType)(r.props.co_type) ||
(r.props.properties || []).length > 0)
.forEach(r => {
// * if the cog object is a variable type, blindly add to our list
if ((0, variables_1.isVariableType)(r.props.co_type)) {
varsSet.add(r.props.co_id);
}
/**
* * now check if the properties arr contains any variables.
* ! NOTE: that this is not an else condition since there can be 2 variables used in a single when event:
* ! 1. as cog object
* ! 2. as variable name template
*
* varsSet will take care of unique entries
*/
const properties = (r.props.properties || []);
// * properties = ["{{score}}", "{{number}}+{{string}}"] score, number and string are all variable names
// * the loop has to be on the variable names since the names can be used in a formula too and not just plain templating
for (const [key, value] of Object.entries(idNameMap)) {
// check if any of the names is included in the properties array entries
for (const p of properties) {
if (p.toString().includes(value)) {
varsSet.add(Number(key));
}
}
}
});
// * get all then actions that use variables or have properties defined
recordF
.getAllDeepChildrenWithFilter(RecordTypes_1.RT.then_action, (r) => (0, variables_1.isVariableType)(r.props.co_type) ||
(r.props.properties || []).length > 0)
.forEach(r => {
// * if the cog object is a variable type, blindly add to our list
if ((0, variables_1.isVariableType)(r.props.co_type)) {
varsSet.add(r.props.co_id);
}
/**
* * now check if the properties arr contains any variables.
* ! NOTE: that this is not an else condition since there can be 2 variables used in a single then action:
* ! 1. as cog object
* ! 2. as variable name template
*
* varsSet will take care of unique entries
*/
const properties = (r.props.properties || []);
// * properties = ["{{score}}", "{{number}}+{{string}}"] score, number and string are all variable names
// * the loop has to be on the variable names since the names can be used in a formula too and not just plain templating
for (const [key, value] of Object.entries(idNameMap)) {
// check if any of the names is included in the properties array entries
for (const p of properties) {
if (p.toString().includes(value)) {
varsSet.add(Number(key));
}
}
}
});
// * find all variables templated in text elements. ONLY text ELEMENTS CAN BE TEMPLATED AS OF NOW
recordF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.element, (r) => r.props.element_type === ElementDefinition_1.ElementType.text)
.forEach((r) => {
var _a, _b;
const text = ((_b = (_a = r.props) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : "");
// * text = ["{{score}}", "{{number}}+{{string}}"] score, number and string are all variable names
for (const [key, value] of Object.entries(idNameMap)) {
if (text.includes(value)) {
varsSet.add(Number(key));
}
}
});
});
const varsUsed = Array.from(varsSet);
const varsDef = this.getAllDeepChildrenWithFilter(RecordTypes_1.RT.variable, (r) => varsUsed.includes(r.id));
baseClipboardObject.nodes.push(...varsDef);
return baseClipboardObject;
}
/**
* Variable paste logic: [FYI: Variables only get copied when a scene(s) is using them]
* For EACH variable which was copied
* - If the variable id and name doesn't exist - paste it (1)
* - If the variable id exists, but name doesn't - ignore it. (2) (To make this work, we need to go to each string template used in rules
* and elements in the new scene, and change the templates to use the new name)
* - If the variable id doesn't exist, but name does (3) - replace the variable id in the new scene being pasted (in all rules)
* with the new variable id
* - If both the variable id and name exist - ignore it (4)
* @param obj
* @param position
*/
pasteFromClipboardObject({ obj, position, groupElementId, sceneId }) {
if (obj.parentType === RecordTypes_1.RT.scene && sceneId === undefined) {
console.error(`Can't paste an element at project level. Please provide a sceneId.`);
return;
}
const projectVars = this.getRecords(RecordTypes_1.RT.variable);
const scenesFromClipboard = obj.nodes.filter(s => s.type === RecordTypes_1.RT.scene);
const variablesFromClipboard = obj.nodes.filter(s => s.type === RecordTypes_1.RT.variable);
const otherRecordsFromClipboard = obj.nodes.filter(s => s.type !== RecordTypes_1.RT.variable && s.type !== RecordTypes_1.RT.scene && s.type !== RecordTypes_1.RT.element);
const elementsFromClipboard = obj.nodes.filter(s => s.type === RecordTypes_1.RT.element);
for (const rn of variablesFromClipboard) {
// This needs to follow the pasting logic above
const variable = this.getRecord(RecordTypes_1.RT.variable, rn.id);
if (variable === undefined) {
// ! variable doesn't exist
// * check if one with the same name exists or not.
const nameMatchedVariable = projectVars.find(v => v.name === rn.name);
if (nameMatchedVariable) {
// * (3) there exists a variable with the same name, we replace ids of rn in any new scenes being pasted with the one already present in the project
// * Only replace in rules since templating uses names and will continue using them
// ! Only replace in the scenes being pasted. If there are no scenes to be pasted, this will be a no-op
for (const scene of scenesFromClipboard) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
// ! using getAllDeepChildrenWithFilter here to filter results and avoid unnecessary loops
// * find the when event that is using the variable that is being pasted, also ensure that we are modifying only when type of vars match
const whenEvents = sceneF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.when_event, we => we.props.co_type === nameMatchedVariable.props.var_type && we.props.co_id === rn.id);
// * find the then action that is using the variable that is being pasted, also ensure that we are modifying only when type of vars match
const thenActions = sceneF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.then_action, ta => ta.props.co_type === nameMatchedVariable.props.var_type && ta.props.co_id === rn.id);
for (const we of whenEvents) {
we.props.co_id = nameMatchedVariable.id;
}
for (const ta of thenActions) {
ta.props.co_id = nameMatchedVariable.id;
}
}
}
else {
// * (1) neither the variable exists nor another variable with same name
this.addRecord(rn);
}
}
else {
// * (4) if copied variable is present and name matches then ignore it.
// * (2) When variable exists but names doesn't the ignore
// * Both above are dependant on the fact the variable exists. If variable exists, then simply skip
// ! this is a no-op
}
}
// * add all scenes
for (const scene of scenesFromClipboard) {
// ! Only scenes are inserted in place when position is passed. Have to live with this assumption for now
// * if position is passed, then keep incrementing to insert in order, else add at the end of the list
const addedScene = this.addRecord(scene, position ? position++ : position);
const sceneF = new SceneFactory_1.SceneFactory(addedScene);
const elements = sceneF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.element, e => definitions_1.en.elementsWithLinkedVariables.includes(e.props.element_type));
this.addLinkedVariables(elements);
}
// * add all other records
for (const other of otherRecordsFromClipboard) {
// keep adding to the end of the list
this.addRecord(other);
}
if (sceneId !== undefined && elementsFromClipboard.length > 0) {
const scene = this.getRecord(RecordTypes_1.RT.scene, sceneId);
const sceneF = new SceneFactory_1.SceneFactory(scene);
if (groupElementId !== undefined) {
const group = sceneF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.element, el => el.id === groupElementId);
if (group !== undefined) {
const groupF = new ElementFactory_1.ElementFactory(group[0]);
const addedRecords = groupF.pasteFromClipboardObject({ obj, position });
const recordsToAddLinkedVars = this.getAllRecordsForLinkedVariables(addedRecords);
this.addLinkedVariables(recordsToAddLinkedVars);
}
}
else {
const addedRecords = sceneF.pasteFromClipboardObject({ obj, position, groupElementId });
const recordsToAddLinkedVars = this.getAllRecordsForLinkedVariables(addedRecords);
this.addLinkedVariables(recordsToAddLinkedVars);
}
}
}
getAllRecordsForLinkedVariables(records) {
const recordsToAddLinkedVars = [];
for (const record of records) {
switch (record === null || record === void 0 ? void 0 : record.props.element_type) {
case definitions_1.en.ElementType.group: {
const recordGroupF = new ElementFactory_1.ElementFactory(record);
const allGroupChildrenWithLinkedVariables = recordGroupF.getAllDeepChildrenWithFilter(RecordTypes_1.RT.element, e => definitions_1.en.elementsWithLinkedVariables.includes(e === null || e === void 0 ? void 0 : e.props.element_type));
recordsToAddLinkedVars.push(...allGroupChildrenWithLinkedVariables);
break;
}
default: {
if (definitions_1.en.elementsWithLinkedVariables.includes(record === null || record === void 0 ? void 0 : record.props.element_type)) {
recordsToAddLinkedVars.push(record);
}
break;
}
}
}
return recordsToAddLinkedVars;
}
/**
* Overriding this function from base class as there is element specific code.
* Check base class implementation for reference on function usage and examples
*/
reParentRecordsWithAddress(destParentAddr, sourceRecordAddr, destPosition) {
const destParentRecord = this.getRecordAtAddress(destParentAddr);
const reParentedRecords = [];
const failedReParentedRecords = [];
if (destParentRecord === null) {
console.error(`[reParentRecordsWithAddress]: Error in re-parenting. destParentAddr: ${destParentAddr}`);
return [reParentedRecords, failedReParentedRecords];
}
const destinationParentRecordF = new RecordFactory_1.RecordFactory(destParentRecord);
for (const s of sourceRecordAddr) {
const sourceRecord = this.getRecordAtAddress(s.recordAddr);
const sourceParentRecord = this.getRecordAtAddress(s.parentAddr);
if (sourceRecord === null || sourceParentRecord === null) {
console.error(`[delete-sourceRecordAddresses]: can't find record/parent for : recordAddr: ${s.recordAddr} parentAddr: ${s.recordAddr}`);
continue;
}
/**
* The order of operations here is very important
* 1. add the record in the new parent
* 2. delete the record from older parent
*
* We do this in this order and not reverse since there can be records that don't qualify to be child of a parent
* for ex: a scene can't be a child of a group
* in this case, we don't re-parent the record at all and addRecord function returns undefined.
* A better UX for this would be to restrict the user to be able to do that at all in the UI
*
*/
/**
* Almost all operations are generic baring one: re-parenting groups into other groups. We don't support this functionality yet.
* So a special check needs to be made when sourceRecord and destParentRecord both have ** type === element **
* we need to check that sourceRecord.props.element_type !== group, only then allow re-parenting
*/
if (sourceRecord.type === RecordTypes_1.RT.element && destParentRecord.type === RecordTypes_1.RT.element) {
const sourceRecordElementType = sourceRecord.props.element_type;
const destParentRecordElementType = sourceRecord.props.element_type;
if (destParentRecordElementType === definitions_1.en.ElementType.group && sourceRecordElementType === definitions_1.en.ElementType.group) {
failedReParentedRecords.push(sourceRecord);
continue;
}
}
// * Add to destination parent
// * addRecord takes care of name clashes and id clashes
const addedRecord = destinationParentRecordF.addRecord(sourceRecord, destPosition);
// * Record was added correctly to the appropriate parent
if (addedRecord !== undefined) {
// * delete the record from resp parents
const sourceParentRecordF = new RecordFactory_1.RecordFactory(sourceParentRecord);
sourceParentRecordF.deleteRecord(sourceRecord.type, sourceRecord.id);
reParentedRecords.push(addedRecord);
}
else {
failedReParentedRecords.push(sourceRecord);
}
}
return [reParentedRecords, failedReParentedRecords];
}
/**
* Get Address for a deep record in a particular scene, also has the capability to attach parent address
* If you want the full address for an element (might be a duplicate => there might be two elements with same ID but in different scenes)
* send scene id (scene in which the element is present) and parent address (project address) => can be found via projectF.getSelfRecordAddress()
*
* If just the scene Id is provided => the address will be starting from the scene instead of the project.
* If neither scene Id or parent address is provided => the full address will be returned but might be from a different scene
*/
getDeepChildRecordAddress({ id, type, sceneId, parentAddr }) {
if (sceneId) {
const scene = this.getRecord(RecordTypes_1.RT.scene, sceneId);
if (scene) {
const sceneF = new SceneFactory_1.SceneFactory(scene);
const elementAddr = sceneF.getDeepRecordAddress({ id, type, parentAddr });
return elementAddr;
}
else {
return undefined;
}
}
else {
const elementAddr = this.getDeepRecordAddress({ id, type, parentAddr });
return elementAddr;
}
}
/**
* This method is called from the UI to resolve inconsistencies in the menu records.
* It is called only if the number of menu entries != number of scene entries
*/
syncMenuWithScenes() {
const sceneIdsSet = new Set(this.getRecords(RecordTypes_1.RT.scene).map(s => s.id));
const sceneIdsFroMenuSet = new Set(this.getRecords(RecordTypes_1.RT.menu).map(m => m.props.menu_scene_id));
//If both above sets are equal, return.
//Set equality test is (union's length = intersection's length)
if (union(sceneIdsSet, sceneIdsFroMenuSet).size ===
intersection(sceneIdsSet, sceneIdsFroMenuSet).size) {
return;
}
//For scenes in MenuList, but not in SceneList, delete them from menu
const toBeRemovedFromMenu = difference(sceneIdsFroMenuSet, sceneIdsSet);
//For scenes in SceneList, but not in MenuList, add them to menu
const toBeAddedToMenu = difference(sceneIdsSet, sceneIdsFroMenuSet);
for (const sid of toBeRemovedFromMenu.values()) {
//Find which menu record is to be removed
const removeMenuRecords = this.getRecords(RecordTypes_