UNPKG

@gmetrixr/rjson

Version:
879 lines 70.4 kB
"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_