diff --git a/README.md b/README.md index 30ee93c8a..a9daff393 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ The Wiki has [detailed instructions](https://github.com/netcreateorg/netcreate-2 This will get NetCreate installed and ready to run. It may take some time for the `npm ci` command to finish installing, so be patient! +The first time you run Net.Create, you also need to create a `net-create.config` file. This will tell Net.Create which dataset to open and which ports to use. We have a script that can do this for you. Run: +* `./nc.js --dataset=demo` -- where `demo` will be the name of the dataset you want to use. + ## QUICK OPERATION GUIDE Once you have NetCreate installed, make sure you are in the `netcreate-2018/build` directory. From there, you can execute **scripts** through the `npm` tool that is installed as part of NodeJS. diff --git a/build/app/assets/templates/_default.template b/build/app/assets/templates/_default.template index c08268b15..39c8d3d94 100644 --- a/build/app/assets/templates/_default.template +++ b/build/app/assets/templates/_default.template @@ -113,6 +113,7 @@ "_cmt4": "//updated is included in the edge tables if in admin mode, and in the graph tooltip if indicated here. Probably those should be more consistent." }, "delete": { + "_cmt5": "// Only admins see the delete button. This hides delete button even for admins.", "type": "hidden", "hidden": false } diff --git a/build/app/assets/templates/_default.template.toml b/build/app/assets/templates/_default.template.toml new file mode 100644 index 000000000..fb49f790e --- /dev/null +++ b/build/app/assets/templates/_default.template.toml @@ -0,0 +1,175 @@ +name = "Untitled" +description = "Default empty template" +requireLogin = false +hideDeleteNodeButton = false +duplicateWarning = "You’re entering a duplicate node. Do you want to View the Existing node, or Continue creating?" +nodeIsLockedMessage = "This node is currently being edited by someone else, please try again later." +edgeIsLockedMessage = "This edge is currently being edited by someone else, please try again later." +nodeDefaultTransparency = 1.0 +edgeDefaultTransparency = 0.3 +searchColor = "#FFFF00" +sourceColor = "#FFa500" + +[citation] +text = "No citation set" +hidden = false + +[nodeDefs.id] +type = "number" +displayLabel = "Id" +exportLabel = "ID" +help = "System-generated unique id number" + +[nodeDefs.label] +type = "string" +displayLabel = "Label" +exportLabel = "Label" +help = "Display name of the node" +includeInGraphTooltip = true + +[nodeDefs.type] +type = "select" +displayLabel = "Type" +exportLabel = "Type" +help = "Multiple people are a 'Group'" +includeInGraphTooltip = true +hidden = false + +[[nodeDefs.type.options]] +label = "" +color = "#eeeeee" + +[[nodeDefs.type.options]] +label = "Person" +color = "#00fbff" + +[[nodeDefs.type.options]] +label = "Group" +color = "#3399ff" + +[[nodeDefs.type.options]] +label = "Place" +color = "#00ff00" + +[[nodeDefs.type.options]] +label = "Thing" +color = "#009999" + +[[nodeDefs.type.options]] +label = "Event" +color = "#ff0000" + +[nodeDefs.notes] +type = "string" +displayLabel = "Significance" +exportLabel = "Notes" +help = "Add some details" +includeInGraphTooltip = true +hidden = false + +[nodeDefs.info] +type = "number" +displayLabel = "Number" +exportLabel = "Info" +help = "Some number comparison" +includeInGraphTooltip = true +hidden = false + +[nodeDefs.degrees] +type = "number" +displayLabel = "Degrees" +exportLabel = "Degrees" +help = "Number of edges" +includeInGraphTooltip = true +hidden = false + +[nodeDefs.created] +type = "string" +displayLabel = "Created" +exportLabel = "Created" +help = "Date and time node was created" +includeInGraphTooltip = true +hidden = false + +[nodeDefs.updated] +type = "string" +displayLabel = "Last Updated" +exportLabel = "Last Updated" +help = "Date and time node was last modified" +includeInGraphTooltip = true +hidden = false + +[edgeDefs.id] +type = "number" +displayLabel = "Id" +exportLabel = "ID" +help = "System-generated unique id number" + +[edgeDefs.source] +type = "node" +displayLabel = "Source" +exportLabel = "Source" +help = "" +hidden = false + +[edgeDefs.target] +type = "node" +displayLabel = "Target" +exportLabel = "Target" +help = "" +hidden = false + +[edgeDefs.type] +type = "select" +displayLabel = "Type" +exportLabel = "Type" +help = "" +hidden = false + +[[edgeDefs.type.options]] +label = "" +color = "#c2c2c2" + +[[edgeDefs.type.options]] +label = "you've got, a \"friend\"" +color = "#0dbd00" + +[[edgeDefs.type.options]] +label = "has unfriendly interaction with" +color = "#ff0000" + +[[edgeDefs.type.options]] +label = "is a member of" +color = "#9ebeff" + +[[edgeDefs.type.options]] +label = "visits" +color = "#ffae00" + +[edgeDefs.notes] +type = "string" +displayLabel = "Significatnce" +exportLabel = "Significatnce" +help = "" +hidden = false + +[edgeDefs.info] +type = "number" +displayLabel = "Date" +exportLabel = "Date" +help = "'YYYY-MM-DD' format" +hidden = false + +[edgeDefs.citation] +type = "string" +displayLabel = "Citation" +exportLabel = "Citation" +help = "Source Book.Chapter (Part 2 06.03)" +hidden = false + +[edgeDefs.category] +type = "string" +displayLabel = "Category" +exportLabel = "Category" +help = "Category?" +hidden = false diff --git a/build/app/settings.js b/build/app/settings.js index 49e56d607..3576ef4f2 100644 --- a/build/app/settings.js +++ b/build/app/settings.js @@ -63,6 +63,12 @@ let RELOAD_TIMER = null; /*/ MOD.CurrentTime = () => { return DATE.toDateString(); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ +/*/ MOD.IsAdmin = () => { + return (MOD.EJSProp('client').ip === '127.0.0.1') || + location.href.includes('admin=true'); + } /// SERVER-PROVIDED PROPERTIES //////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/app/system/datastore.js b/build/app/system/datastore.js index 3cd506996..397267f1a 100644 --- a/build/app/system/datastore.js +++ b/build/app/system/datastore.js @@ -156,12 +156,26 @@ DSTOR.PromiseJSONFile = function(jsonFile) { /*/ DSTOR.PromiseD3Data = function() { // UDATA.Call() returns a promise - return UDATA.Call("SRV_DBGET", {}); + return UDATA.Call("SRV_DBGET", {}); // server.js }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ API: Write Template file to Server +/*/ +DSTOR.SaveTemplateFile = template => { + // UDATA.Call() returns a promise + return UDATA.Call("SRV_TEMPLATESAVE", {template}); // server.js +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ API: Get Template File Path. + Called by template-logic when downloading template file. +/*/ +DSTOR.GetTemplateTOMLFileName = () => { + return UDATA.Call("SRV_GET_TEMPLATETOML_FILENAME"); +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ API: (WIP) write database from d3data-formatted object /*/ -DSTOR.OverwriteDataPromise = function(d3data) { +DSTOR.OverwriteDataPromise = function (d3data) { return new Promise((resolve, reject) => { UDATA.Call("SRV_DBSET", d3data).then(res => { if (res.OK) { diff --git a/build/app/unisys/server-database.js b/build/app/unisys/server-database.js index 79ad01dd0..38f979c48 100644 --- a/build/app/unisys/server-database.js +++ b/build/app/unisys/server-database.js @@ -11,6 +11,7 @@ const DBG = false; const Loki = require("lokijs"); const PATH = require("path"); const FS = require("fs-extra"); +const TOML = require("@iarna/toml"); /// CONSTANTS ///////////////////////////////////////////////////////////////// /// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = @@ -20,6 +21,7 @@ const PROMPTS = require("../system/util/prompts"); const PR = PROMPTS.Pad("ServerDB"); const RUNTIMEPATH = './runtime/'; const TEMPLATEPATH = './app/assets/templates/'; +const TEMPLATE_EXT = '.template.toml' const DB_CLONEMASTER = "blank.loki"; const NC_CONFIG = require("../assets/netcreate-config"); @@ -126,22 +128,8 @@ DB.InitializeDatabase = function (options = {}) { console.log(PR,`DATABASE LOADED! m_max_nodeID '${m_max_nodeID}', m_max_edgeID '${m_max_edgeID}'`); m_db.saveDatabase(); - // LOAD TEMPLATE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - let templatePath = RUNTIMEPATH + NC_CONFIG.dataset + ".template"; - FS.ensureDirSync(PATH.dirname(templatePath)); - // Does the template exist? - if (!FS.existsSync(templatePath)) { - console.log(PR, `NO EXISTING TEMPLATE ${templatePath}, so cloning default template...`); - FS.copySync(TEMPLATEPATH+'_default.template', templatePath); - } - console.log(PR, `LOADING TEMPLATE ${templatePath}`); - // Now load it - TEMPLATE = FS.readJsonSync(templatePath); + m_LoadTemplate(); - // Call complete callback - if (typeof m_options.onLoadComplete === 'function') { - m_options.onLoadComplete(); - } } // end f_DatabaseInitialize // UTILITY FUNCTION @@ -152,6 +140,190 @@ DB.InitializeDatabase = function (options = {}) { } }; // InitializeDatabase() /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// utility function for loading template +/*/ Converts a version 1.3 JSON template to a version 1.4 TOML template +/*/ +// eslint-disable-next-line complexity +function m_MigrateJSONtoTOML(JSONtemplate) { + console.log(PR, 'Converting JSON to TOML...'); + const jt = JSONtemplate; + const TOMLtemplate = { + name: jt.name, + description: jt.description, + requireLogin: jt.requireLogin || false, + // REVIEW: These really ought to load the default values from the schema. + hideDeleteNodeButton: (jt.nodePrompts && jt.nodePrompts.delete && jt.nodePrompts.delete.hidden) || false, + duplicateWarning: (jt.nodePrompts && jt.nodePrompts.label && jt.nodePrompts.label.duplicateWarning) || 'Warning: Duplicate', + nodeIsLockedMessage: (jt.nodePrompts && jt.nodePrompts.label && jt.nodePrompts.label.sourceNodeIsLockedMessage) || 'Node is locked', + edgeIsLockedMessage: (jt.edgePrompts && jt.edgePrompts.edgeIsLockedMessage) || "Edge is locked", + nodeDefaultTransparency: (jt.nodePrompts && jt.nodePrompts.defaultTransparency) || 1.0, + edgeDefaultTransparency: (jt.edgePrompts && jt.edgePrompts.defaultTransparency) || 0.3, + searchColor: jt.searchColor || '#008800', + sourceColor: jt.sourceColor || '#FFa500', + citation: { + text: (jt.citationPrompts && jt.citationPrompts.citation) || jt.name, + hidden: (jt.citationPrompts && jt.citationPrompts.hidden) || true + } + } + // convert nodePrompts + const nodeDefs = {}; + // 1. Add fields + Object.keys(JSONtemplate.nodePrompts).forEach(k => { + const field = JSONtemplate.nodePrompts[k]; + nodeDefs[k] = { + type: field.type || 'string', // default to 'string' + displayLabel: field.label, + exportLabel: field.label, + help: field.help, + includeInGraphTooltip: field.includeInGraphTooltip || true, // default to show tool tip + hidden: field.hidden || false // default to not hidden + } + if (k === 'type') { + // special handling for type options + const options = field.options.map(o => { + return { + label: o.label, + color: o.color + } + }); + // make sure field type is set to "select" -- older templates do not set type + nodeDefs[k].type = 'select'; + console.log(PR,'...migrating nodeDefs field', k,'with options, forcing type to "select"') + nodeDefs[k].options = options; + } + }) + // 2. Add id -- clobbers any existing id + nodeDefs.id = { + type: 'number', + displayLabel: 'id', + exportLabel: 'ID', + help: 'System-generated unique id number' + }; + // 3. remove deprecated fields + Reflect.deleteProperty(nodeDefs, 'delete'); // `delete` -- mapped to hideDeleteNodeButton + Reflect.deleteProperty(nodeDefs, 'defaultTransparency'); // `nodeDefaultTransparency` -- moved to root + + // 4. Add other built-ins + nodeDefs.updated = { + displayLabel: 'Last Updated', + exportLabel: 'Last Updated', + help: 'Date and time of last update', + includeInGraphTooltip: true // default to show tool tip + } + nodeDefs.created = { + displayLabel: 'Created', + exportLabel: 'Created', + help: 'Date and time node was created', + includeInGraphTooltip: true // default to show tool tip + } + + // convert edgePrompts + const edgeDefs = {}; + // 1. Add fields + Object.keys(JSONtemplate.edgePrompts).forEach(k => { + const field = JSONtemplate.edgePrompts[k]; + edgeDefs[k] = { + type: field.type || 'string', // default to 'string' + displayLabel: field.label, + exportLabel: field.label, + help: field.help, + hidden: field.hidden || false // default to not hidden + // If necessary, user can edit template to hide it again. + // We want it visible by default, because of migrations + // the original field may not be defined. + // e.g. orig template uses "Relationship" not "type" + } + if (k === 'type') { + // special handling for type options + const options = field.options.map(o => { + return { + label: o.label, + color: o.color + } + }) + // make sure field type is set to "select" -- older templates do not set type + edgeDefs[k].type = 'select'; + console.log(PR,'...migrating edgeDefs field', k,'with options, forcing type to "select"') + edgeDefs[k].options = options; + } + }) + // 2. Add id + edgeDefs.id = { // will clobber any existing id + type: 'number', + displayLabel: 'id', + exportLabel: 'ID', + help: 'System-generated unique id number' + }; + // 3. remove deprecated fields + Reflect.deleteProperty(edgeDefs, 'edgeIsLockedMessage'); // `edgeIsLockedMessage` -- moved to root + Reflect.deleteProperty(edgeDefs, 'defaultTransparency'); // `edgeDefaultTransparency` -- moved to root + + TOMLtemplate.nodeDefs = nodeDefs; + TOMLtemplate.edgeDefs = edgeDefs; + if (DBG) console.log(PR, 'Imported TOML TEMPLATE', TOMLtemplate) + + return TOMLtemplate; +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Loads an original circa version 1.3 JSON template + and converts it to a TOML template +/*/ +function m_LoadJSONTemplate(templatePath) { + // 1. Load JSON + console.log(PR, `LOADING JSON TEMPLATE ${templatePath}`); + const JSONTEMPLATE = FS.readJsonSync(templatePath); + // 2. Convert to TOML + TEMPLATE = m_MigrateJSONtoTOML(JSONTEMPLATE); + // 3. Save it (and load) + DB.WriteTemplateTOML({ data: { template: TEMPLATE } }) + .then(() => { + console.log(PR, '...converted JSON template saved!'); + }); +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Loads a *.template.toml file from the server. +/*/ +function m_LoadTOMLTemplate(templateFilePath) { + const templateFile = FS.readFile(templateFilePath, 'utf8', (err, data) => { + if (err) throw err; + // Read TOML + const json = TOML.parse(data); + TEMPLATE = json; + console.log(PR, 'Template loaded', templateFilePath); + }); +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Load Template + 1. Tries to load a TOML template + 2. If it can't be found, tries to load the JSON template and convert it + 3. If that fails, clone the default TOML template and load it + Called by + * DB.InitializeDatabase + * DB.WriteTemplateTOML +/*/ +function m_LoadTemplate() { + const TOMLtemplateFilePath = m_GetTemplateTOMLFilePath(); + FS.ensureDirSync(PATH.dirname(TOMLtemplateFilePath)); + // Does the TOML template exist? + if (FS.existsSync(TOMLtemplateFilePath)) { + // 1. If TOML exists, load it + m_LoadTOMLTemplate(TOMLtemplateFilePath); + } else { + // 2/ Try falling back to JSON template + const JSONTemplatePath = RUNTIMEPATH + NC_CONFIG.dataset + ".template"; + // Does the JSON template exist? + if (FS.existsSync(JSONTemplatePath)) { + m_LoadJSONTemplate(JSONTemplatePath); + } else { + // 3. Else, no existing template, clone _default.template.toml + console.log(PR, `NO EXISTING TEMPLATE ${TOMLtemplateFilePath}, so cloning default template...`); + FS.copySync(TEMPLATEPATH + '_default' + TEMPLATE_EXT, TOMLtemplateFilePath); + // then load it + m_LoadTOMLTemplate(TOMLtemplateFilePath); + } + } +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ API: load database note: InitializeDatabase() was already called on system initialization to populate the NODES and EDGES structures. @@ -166,7 +338,7 @@ DB.PKT_GetDatabase = function(pkt) { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ API: reset database from scratch /*/ -DB.PKT_SetDatabase = function(pkt) { +DB.PKT_SetDatabase = function (pkt) { if (DBG) console.log(PR, `PKT_SetDatabase`); let { nodes = [], edges = [] } = pkt.Data(); if (!nodes.length) console.log(PR, "WARNING: empty nodes array"); @@ -497,7 +669,8 @@ DB.WriteDbJSON = function (filePath) { ); }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ called by brunch to generate an up-to-date Template file to path. +/*/ DEPRECATED. Replaced by WriteTemplateTOML + called by brunch to generate an up-to-date Template file to path. creates the path if it doesn't exist /*/ DB.WriteTemplateJSON = function (filePath) { @@ -512,6 +685,48 @@ DB.WriteTemplateJSON = function (filePath) { } }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ called by Template Editor and DB.WriteTemplateTOML +/*/ +function m_GetTemplateTOMLFileName() { + return NC_CONFIG.dataset + TEMPLATE_EXT; +} +function m_GetTemplateTOMLFilePath() { + return RUNTIMEPATH + m_GetTemplateTOMLFileName(); +} +DB.GetTemplateTOMLFileName = () => { + return { filename: m_GetTemplateTOMLFileName() }; +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ called by Template Editor to save TOML template changes to disk. + parm {object} pkt.data.template + Loads the template after saving! +/*/ +DB.WriteTemplateTOML = (pkt) => { + const templateFilePath = m_GetTemplateTOMLFilePath(); + FS.ensureDirSync(PATH.dirname(templateFilePath)); + // Does the template exist? If so, rename the old version with curren timestamp. + if (FS.existsSync(templateFilePath)) { + const timestamp = new Date().toISOString() + .replace(/:/g, '.'); + const backupFilePath = RUNTIMEPATH + NC_CONFIG.dataset + '_' + timestamp + TEMPLATE_EXT; + FS.copySync(templateFilePath, backupFilePath); + console.log(PR, 'Backed up template to', backupFilePath); + } + const toml = TOML.stringify(pkt.data.template); + return FS.outputFile(templateFilePath, toml) + .then(data => { + console.log(PR, 'Saved template to', templateFilePath) + // reload template + m_LoadTemplate(); + return { OK: true, info: templateFilePath } + }) + .catch(err => { + console.log(PR, 'Failed trying to save', templateFilePath, err); + return { OK: false, info: 'Failed trying to save', templateFilePath } + }); +} + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// utility function for cleaning nodes with numeric id property function m_CleanObjID(prompt, obj) { diff --git a/build/app/unisys/server.js b/build/app/unisys/server.js index 4c4258440..034ac1c75 100644 --- a/build/app/unisys/server.js +++ b/build/app/unisys/server.js @@ -28,7 +28,7 @@ var UNISYS = {}; /*/ Initialize() is called by brunch-server.js to define the default UNISYS network values, so it can embed them in the index.ejs file for webapps override = { port } -/*/ UNISYS.InitializeNetwork = ( override ) => { +/*/ UNISYS.InitializeNetwork = override => { UDB.InitializeDatabase(override); return UNET.InitializeNetwork(override); }; @@ -58,11 +58,20 @@ var UNISYS = {}; return UDB.PKT_GetDatabase(pkt); }); - UNET.HandleMessage('SRV_DBSET',function(pkt) { + UNET.HandleMessage('SRV_DBSET', function (pkt) { if (DBG) console.log(PR,sprint_message(pkt)); return UDB.PKT_SetDatabase(pkt); }); + UNET.HandleMessage('SRV_TEMPLATESAVE', pkt => { // server-database + if (DBG) console.log(PR, sprint_message(pkt)); + return UDB.WriteTemplateTOML(pkt); + }); + + UNET.HandleMessage('SRV_GET_TEMPLATETOML_FILENAME', () => { + return UDB.GetTemplateTOMLFileName(); + }) + // receives a packet from a client UNET.HandleMessage('SRV_DBUPDATE',function(pkt) { if (DBG) console.log(PR,sprint_message(pkt)); diff --git a/build/app/view/netcreate/components/EdgeEditor.jsx b/build/app/view/netcreate/components/EdgeEditor.jsx index ebce9c2cc..c4e96ecbd 100644 --- a/build/app/view/netcreate/components/EdgeEditor.jsx +++ b/build/app/view/netcreate/components/EdgeEditor.jsx @@ -61,6 +61,10 @@ to be displayed. This is only checked when the user clicks "Edit". + disableEdit Template is being edited, disable "Edit Edge" button + + isBeingEdited The form fields are active and text can be changed. + ## TECHNICAL DESCRIPTION @@ -208,13 +212,15 @@ var UDATA = null; class EdgeEditor extends UNISYS.Component { constructor (props) { super(props); + const TEMPLATE = this.AppState('TEMPLATE'); this.state = { - edgePrompts: this.AppState('TEMPLATE').edgePrompts, - citationPrompts: this.AppState('TEMPLATE').citationPrompts, + edgeDefs: TEMPLATE.edgeDefs, + citation: TEMPLATE.citation, + edgeIsLockedMessage: TEMPLATE.edgeIsLockedMessage, formData: { // Holds the state of the form fields sourceId: '', targetId: '', - relationship: '', + type: '', info: '', notes: '', citation: '', @@ -238,7 +244,8 @@ class EdgeEditor extends UNISYS.Component { }, isLocked: true, // User has not logged in, don't allow edge edit dbIsLocked: false, // Server Database is locked because someone else is editing - isEditable: false, // Form is in an edtiable state + disableEdit: false, // Template is being edited, disable "Edit Edge" button + isBeingEdited: false, // Form is in an editable state isExpanded: false, // Show EdgeEditor Component in Summary view vs Expanded view sourceIsEditable:false, // Source ndoe field is only editable when source is not parent hasValidSource: false, // Used by SwapSourceAndTarget and the Change Source button @@ -251,6 +258,8 @@ class EdgeEditor extends UNISYS.Component { /// Initialize UNISYS DATA LINK for REACT UDATA = UNISYS.NewDataLink(this); + this.setTemplate = this.setTemplate.bind(this); + this.updateEditState = this.updateEditState.bind(this); this.handleSelection = this.handleSelection.bind(this); this.handleEdgeSelection = this.handleEdgeSelection.bind(this); this.handleEdgeEdit = this.handleEdgeEdit.bind(this); @@ -291,17 +300,14 @@ class EdgeEditor extends UNISYS.Component { UDATA.HandleMessage('EDGE_EDIT',(data) => { this.handleEdgeEdit(data); }); - - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - UDATA.HandleMessage('EDGE_CLOSE',(data) => { - if (this.state.isExpanded) - this.setState({ isExpanded: false }); - }); - - + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + UDATA.HandleMessage('EDGE_CLOSE',(data) => { + if (this.state.isExpanded) this.setState({ isExpanded: false }); + }); + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Template handler this.OnAppStateChange('TEMPLATE', this.setTemplate); + this.OnAppStateChange('OPENEDITORS', this.updateEditState); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Prevent editing if server is disconnected. @@ -323,7 +329,7 @@ class EdgeEditor extends UNISYS.Component { formData: { sourceId: '', targetId: '', - relationship: '', + type: '', info: '', notes: '', citation: '', @@ -345,7 +351,7 @@ class EdgeEditor extends UNISYS.Component { notes: '', id: '' }, - isEditable: false, + isBeingEdited: false, isExpanded: false, // Summary view vs Expanded view dbIsLocked: false, sourceIsEditable: false, // Source ndoe field is only editable when source is not parent @@ -357,8 +363,17 @@ class EdgeEditor extends UNISYS.Component { }); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setTemplate (data) { - this.setState({ edgePrompts: data.edgePrompts }); + setTemplate(data) { + this.setState({ edgeDefs: data.edgeDefs }); + } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Disable Edge Edit if a Template is being edited +/*/ + updateEditState() { + let disableEdit = false; + const openEditors = UDATA.AppState("OPENEDITORS").editors; + if (openEditors.includes('template')) disableEdit = true; + this.setState({ disableEdit }); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ populate formdata from D3DATA @@ -400,19 +415,17 @@ class EdgeEditor extends UNISYS.Component { source: parseInt(sourceNodes[0].id), // REVIEW: d3data 'source' is id, rename this to 'sourceId'? // though after d3 processes, source does become an object. target: undefined, - attributes: { - Relationship: '', - Info: '', - Citations: '', - Category: '', - Notes: '' - } + type: '', + notes: '', + info: '', + citation: '', + category: '' } // Expand this EdgeEditor and set it to Edit mode. this.setState({ isExpanded: true, targetIsEditable: true, - isEditable: true + isBeingEdited: true }, () => { // AUTOCOMPLETE mode needs to be set AFTER the edit state has already been set // otherwise, the component may not have even been defined in the collapsed view. @@ -451,11 +464,11 @@ class EdgeEditor extends UNISYS.Component { id: parseInt(edge.id) || '', sourceId: edge.source, targetId: edge.target, - relationship: edge.attributes["Relationship"] || '', // Make sure there's valid data - info: edge.attributes["Info"] || '', - citation: edge.attributes["Citations"] || '', - category: edge.attributes["Category"] || '', - notes: edge.attributes["Notes"] || '', + type: edge.type || '', // Make sure there's valid data + info: edge.info || '', + citation: edge.citation || '', + category: edge.category || '', + notes: edge.notes || '', isNewEdge: false }, sourceNode: sourceNode, @@ -482,7 +495,7 @@ class EdgeEditor extends UNISYS.Component { // If we're not currently being edited, then if edges have been updated, update self if (data.edges !== undefined) { let updatedEdge = data.edges.find((edge) => { return edge.id === this.state.formData.id; }); - if (!this.state.isEditable && updatedEdge !== undefined) { + if (!this.state.isBeingEdited && updatedEdge !== undefined) { if (DBG) console.log('EdgeEditor: Updating edges with', updatedEdge); this.loadSourceAndTarget(); return; @@ -492,7 +505,7 @@ class EdgeEditor extends UNISYS.Component { // We're being edited, and the updated node is either our source or target // Technically we probably ought to also check to make sure we're the current // activeAutoCompleteId, but we wouldn't be editable if we weren't. - if (this.state.isEditable && data.nodes && data.nodes.length > 0) { + if (this.state.isBeingEdited && data.nodes && data.nodes.length > 0) { // A node was selected, so load it let node = data.nodes[0]; @@ -598,14 +611,16 @@ class EdgeEditor extends UNISYS.Component { this.setState({ isExpanded: false }); // If we were editing, then revert and exit - if (this.state.isEditable) { + if (this.state.isBeingEdited) { const D3DATA = this.AppState('D3DATA'); - this.setState({ isEditable: false, targetIsEditable: false }); + this.setState({ isBeingEdited: false, targetIsEditable: false }); // Return focus of autocomplete to Search field. this.AppCall('AUTOCOMPLETE_SELECT', { id: 'search' }); // Tell parent node to exit out of edge edit mode this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); + // Deregister as an open editor + UDATA.LocalCall("DEREGISTER_OPENEDITOR", { type: 'edge' }); // Cancel edit existing or cancel edit new? let originalEdge = D3DATA.edges.filter(edge => parseInt(edge.id) === this.props.edgeID)[0]; @@ -647,6 +662,7 @@ class EdgeEditor extends UNISYS.Component { /*/ onDeleteButtonClick () { this.clearForm(); this.AppCall('AUTOCOMPLETE_SELECT', { id: 'search' }); + if (this.state.isBeingEdited) UDATA.LocalCall("DEREGISTER_OPENEDITOR", { type: 'edge' }); // Deregister as an open editor (in case the editor was open) this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); // inform NodeSelector this.AppCall('DB_UPDATE',{edgeID:this.props.edgeID}); } @@ -678,7 +694,7 @@ class EdgeEditor extends UNISYS.Component { } // onCiteButtonClick - onCloseCiteClick (event) { + onCloseCiteClick (event) { event.preventDefault(); this.setState({ hideModal: true }); @@ -698,7 +714,7 @@ class EdgeEditor extends UNISYS.Component { /*/ /*/ requestEdit() { let edgeID = this.state.formData.id; - if (edgeID && edgeID!=='' && !isNaN(edgeID) && (typeof edgeID ==="number") && !this.state.isEditable) { + if (edgeID && edgeID!=='' && !isNaN(edgeID) && (typeof edgeID ==="number") && !this.state.isBeingEdited) { this.NetCall('SRV_DBLOCKEDGE', { edgeID: edgeID }) .then((data) => { if (data.NOP) { @@ -712,11 +728,13 @@ class EdgeEditor extends UNISYS.Component { if (DBG) console.log(`SERVER SAYS: lock success! you can edit Edge ${data.edgeID}`); if (DBG) console.log(`SERVER SAYS: unlock the edge after successful DBUPDATE`); this.setState({ - isEditable: true, + isBeingEdited: true, isExpanded: true, dbIsLocked: false }); this.AppCall('EDGEEDIT_LOCK', { edgeID: this.props.edgeID }); // inform NodeSelector + // Register as an open editor + UDATA.LocalCall("REGISTER_OPENEDITOR", { type: 'edge' }); } }); } @@ -776,7 +794,7 @@ class EdgeEditor extends UNISYS.Component { /*/ /*/ onRelationshipChange (event) { let formData = this.state.formData; - formData.relationship = event.target.value; + formData.type = event.target.value; this.setState({formData: formData}); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -820,13 +838,11 @@ class EdgeEditor extends UNISYS.Component { source: this.state.sourceNode.id, // REVIEW: d3data 'source' is id, rename this to 'sourceId'? // though after d3 processes, source does become an object. target: this.state.targetNode.id, // REVIEW: d3data 'target' is id, rename this to 'targetId'? - attributes: { - Relationship: formData.relationship, - Info: formData.info, - Citations: formData.citation, - Category: formData.category, - Notes: formData.notes - } + type: formData.type, + info: formData.info, + citation: formData.citation, + category: formData.category, + notes: formData.notes } if (DBG) console.group('EdgeEntry.onSubmit submitting',edge) @@ -854,10 +870,12 @@ class EdgeEditor extends UNISYS.Component { } } + // Deregister as an open editor + UDATA.LocalCall("DEREGISTER_OPENEDITOR", { type: 'edge' }); this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); // inform NodeSelector // pass currentAutoComplete back to nodeselector this.AppCall('AUTOCOMPLETE_SELECT',{id:'search'}); - this.setState({ isEditable: false, sourceIsEditable: false, targetIsEditable: false }); + this.setState({ isBeingEdited: false, sourceIsEditable: false, targetIsEditable: false }); this.AppCall('DB_UPDATE', { edge }) .then(() => { this.NetCall('SRV_DBUNLOCKEDGE', { edgeID: edge.id }) @@ -886,16 +904,16 @@ class EdgeEditor extends UNISYS.Component { /*/ /*/ render () { const { edgeID, parentNodeLabel, parentNodeIsLocked } = this.props; - const { formData, sourceNode, targetNode, edgePrompts} = this.state; - let {citationPrompts} = this.state; - if (edgePrompts.category === undefined) { // for backwards compatability - edgePrompts.category = {}; - edgePrompts.category.label = ""; - edgePrompts.category.hidden = true; + const { formData, sourceNode, targetNode, edgeDefs, edgeIsLockedMessage, disableEdit} = this.state; + let {citation} = this.state; + if (edgeDefs.category === undefined) { // for backwards compatability + edgeDefs.category = {}; + edgeDefs.category.label = ""; + edgeDefs.category.hidden = true; } - if (citationPrompts === undefined) { // if citationPrompts were left out, simply make them hidden - citationPrompts = {}; - citationPrompts.hidden = true; + if (citation === undefined) { // if citation were left out, simply make them hidden + citation = {}; + citation.hidden = true; } const me = this node; // special override to allow editing an edge that has the same parent node for both source and target @@ -915,7 +933,7 @@ class EdgeEditor extends UNISYS.Component { style={{ backgroundColor: "#a9d3ff", borderColor: 'transparent', width: '100%', marginBottom: '3px', textAlign: "left", overflow: "hidden" }} onClick={this.onEdgeClick} >{parentNodeLabel === sourceNode.label ? me : sourceNode.label} -    +    {parentNodeLabel === targetNode.label ? me : targetNode.label} @@ -934,21 +952,21 @@ class EdgeEditor extends UNISYS.Component { -