From 07370be4fadfa09ad6ed6414764af7b713e95108 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Sun, 9 Jul 2023 21:48:00 -0500 Subject: [PATCH] Misc. fixes/improvements Standardize vertex as label + list of properties, for rendering and editing --- forum-network/src/classes/actors/expert.js | 10 --- forum-network/src/classes/dao/forum.js | 32 ++++----- forum-network/src/classes/display/actor.js | 8 ++- forum-network/src/classes/display/box.js | 3 +- forum-network/src/classes/display/form.js | 28 ++++---- forum-network/src/classes/supporting/edge.js | 41 +++++------- .../src/classes/supporting/vertex.js | 67 ++++++++++++++----- forum-network/src/classes/supporting/wdg.js | 4 +- forum-network/src/index.css | 3 + forum-network/src/tests/scripts/input.test.js | 8 ++- 10 files changed, 116 insertions(+), 88 deletions(-) diff --git a/forum-network/src/classes/actors/expert.js b/forum-network/src/classes/actors/expert.js index 112f867..eb297e2 100644 --- a/forum-network/src/classes/actors/expert.js +++ b/forum-network/src/classes/actors/expert.js @@ -2,7 +2,6 @@ import { Action } from '../display/action.js'; import { CryptoUtil } from '../supporting/crypto.js'; import { ReputationHolder } from '../reputation/reputation-holder.js'; import { EdgeTypes } from '../../util/constants.js'; -import { displayNumber } from '../../util/helpers.js'; export class Expert extends ReputationHolder { constructor(dao, name, scene, options) { @@ -30,15 +29,6 @@ export class Expert extends ReputationHolder { return tokenValues.reduce((value, total) => total += value, 0); } - getLabel() { - return `${this.name} - - - -
reputation${displayNumber(this.getReputation())}
` - .replaceAll(/\n\s*/g, ''); - } - async initialize() { this.reputationKey = await CryptoUtil.generateAsymmetricKey(); // this.reputationPublicKey = await CryptoUtil.exportKey(this.reputationKey.publicKey); diff --git a/forum-network/src/classes/dao/forum.js b/forum-network/src/classes/dao/forum.js index f06c7b3..44b9121 100644 --- a/forum-network/src/classes/dao/forum.js +++ b/forum-network/src/classes/dao/forum.js @@ -22,22 +22,15 @@ class Post extends Actor { this.title = postContent.title; } - getLabel() { - return `${this.name} - - - - - - -
initial${displayNumber(this.initialValue)}
value${displayNumber(this.value)}
` - .replaceAll(/\n\s*/g, ''); - } - async setValue(value) { this.value = value; await this.setDisplayValue('value', value); - this.forum.graph.getVertex(this.id).setDisplayLabel(this.getLabel()); + this.forum.graph.getVertex(this.id).setProperty('value', value).displayVertex(); + } + + setInitialValue(value) { + this.initialValue = value; + this.forum.graph.getVertex(this.id).setProperty('initialValue', value).displayVertex(); } } @@ -62,7 +55,7 @@ export class Forum extends ReputationHolder { async addPost(senderId, postContent) { console.log('addPost', { senderId, postContent }); const post = new Post(this, senderId, postContent); - this.graph.addVertex(VertexTypes.POST, post.id, post, post.getLabel()); + this.graph.addVertex(VertexTypes.POST, post.id, post, post.name); for (const { postId: citedPostId, weight } of post.citations) { // Special case: Incinerator if (citedPostId === INCINERATOR_ADDRESS && !this.graph.getVertex(INCINERATOR_ADDRESS)) { @@ -98,14 +91,13 @@ export class Forum extends ReputationHolder { const post = postVertex.data; post.setStatus('Validated'); post.initialValue = initialValue; - postVertex.setDisplayLabel(post.getLabel()); const addAuthorToGraph = (publicKey, weight, authorTokenId) => { // For graph display purposes, we want to use the existing Expert actors from the current scene. const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey); author.setDisplayValue('reputation', () => author.getReputation()); const authorVertex = this.graph.getVertex(publicKey) - ?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, author, author.getLabel(), { + ?? this.graph.addVertex(VertexTypes.AUTHOR, publicKey, author, author.name, { hide: author.options.hide, }); this.graph.addEdge( @@ -161,15 +153,15 @@ export class Forum extends ReputationHolder { } else { this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount); } - await author.computeDisplayValues(); - authorVertex.setDisplayLabel(author.getLabel()); + await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value)); + authorVertex.displayVertex(); } } const senderVertex = this.graph.getVertex(post.senderId); const { data: sender } = senderVertex; - await sender.computeDisplayValues(); - senderVertex.setDisplayLabel(sender.getLabel()); + await sender.computeDisplayValues((label, value) => senderVertex.setProperty(label, value)); + senderVertex.displayVertex(); // Transfer ownership of the minted tokens to the authors for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) { diff --git a/forum-network/src/classes/display/actor.js b/forum-network/src/classes/display/actor.js index 0f1b011..08ef1c6 100644 --- a/forum-network/src/classes/display/actor.js +++ b/forum-network/src/classes/display/actor.js @@ -83,10 +83,16 @@ export class Actor { return this; } - async computeDisplayValues() { + /** + * @param {(label: string, value) => {}} cb + */ + async computeDisplayValues(cb) { for (const [label, fn] of this.valueFunctions.entries()) { const value = fn(); await this.setDisplayValue(label, value); + if (cb) { + cb(label, value); + } } } diff --git a/forum-network/src/classes/display/box.js b/forum-network/src/classes/display/box.js index 1ed666a..082bff5 100644 --- a/forum-network/src/classes/display/box.js +++ b/forum-network/src/classes/display/box.js @@ -4,7 +4,8 @@ import { randomID } from '../../util/helpers.js'; export class Box { constructor(name, parentEl, options = {}) { this.name = name; - this.el = document.createElement('div'); + const { tagName = 'div' } = options; + this.el = document.createElement(tagName); this.el.box = this; const id = options.id ?? randomID(); this.el.id = `${parentEl.id}_box_${id}`; diff --git a/forum-network/src/classes/display/form.js b/forum-network/src/classes/display/form.js index 9ad297b..df7c811 100644 --- a/forum-network/src/classes/display/form.js +++ b/forum-network/src/classes/display/form.js @@ -1,30 +1,29 @@ import { randomID } from '../../util/helpers.js'; import { Box } from './box.js'; -const updateValuesOnEventTypes = ['keyup', 'mouseup']; - export class FormElement extends Box { constructor(name, form, opts) { const parentEl = opts.parentEl ?? form.el; super(name, parentEl, opts); this.form = form; this.id = opts.id ?? name; - this.includeInOutput = opts.includeInOutput ?? true; - const { cb } = opts; + const { cb, cbEventTypes = ['change'], cbOnInit = false } = opts; if (cb) { - updateValuesOnEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => { + cbEventTypes.forEach((eventType) => this.el.addEventListener(eventType, () => { cb(this, { initializing: false }); })); - cb(this, { initializing: true }); + if (cbOnInit) { + cb(this, { initializing: true }); + } } } } export class Button extends FormElement { constructor(name, form, opts) { - super(name, form, opts); + super(name, form, { ...opts, cbEventTypes: ['click'] }); this.button = document.createElement('button'); - this.button.setAttribute('type', 'button'); + this.button.setAttribute('type', opts.type ?? 'button'); this.button.innerHTML = name; this.button.disabled = !!opts.disabled; this.el.appendChild(this.button); @@ -77,7 +76,6 @@ export class SubForm extends FormElement { this.subForm = subForm; if (opts.subFormArray) { opts.subFormArray.subForms.push(this); - this.includeInOutput = false; } } @@ -88,10 +86,12 @@ export class SubForm extends FormElement { export class Form extends Box { constructor(document, opts = {}) { - super(opts.name, opts.parentEl || document.el, opts); + super(opts.name, opts.parentEl || document.el, { ...opts, tagName: 'form' }); this.document = document; this.items = []; this.id = opts.id ?? `form_${randomID()}`; + // Action should be handled by a submit button + this.el.onsubmit = () => false; } button(opts) { @@ -124,11 +124,11 @@ export class Form extends Box { } get value() { - return this.items.reduce((result, { id, value, includeInOutput }) => { - if (includeInOutput && value !== undefined) { - result[id] = value; + return this.items.reduce((obj, { id, value }) => { + if (value !== undefined) { + obj[id] = value; } - return result; + return obj; }, {}); } } diff --git a/forum-network/src/classes/supporting/edge.js b/forum-network/src/classes/supporting/edge.js index ddbf71d..48aa0b7 100644 --- a/forum-network/src/classes/supporting/edge.js +++ b/forum-network/src/classes/supporting/edge.js @@ -55,7 +55,8 @@ export class Edge { this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`); this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`); if (this.graph.editable && !this.installedClickCallback) { - this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} "Edit Edge"`); + this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \ +"Edit Edge ${this.from.id} -> ${this.to.id}"`); this.installedClickCallback = true; } } @@ -72,20 +73,19 @@ export class Edge { id: 'to', name: 'to', defaultValue: to, disabled: true, }); + doc.remark('

Edge Types

', { parentEl: form.el }); const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem; + const addEdgeForm = (edge) => { const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem; - doc.remark('
', { parentEl: subForm.el }); - subForm.textField({ id: 'type', name: 'type', defaultValue: edge.type }); - subForm.textField({ id: 'weight', name: 'weight', defaultValue: edge.weight }); - subForm.button({ - id: 'remove', - name: 'Remove Edge Type', - cb: (_, { initializing }) => { - if (initializing) return; - subFormArray.remove(subForm); - }, - }); + subForm.textField({ id: 'type', name: 'type', defaultValue: edge.type }) + .textField({ id: 'weight', name: 'weight', defaultValue: edge.weight }) + .button({ + id: 'remove', + name: 'Remove Edge Type', + cb: () => subFormArray.remove(subForm), + }); + doc.remark('
', { parentEl: subForm.el }); }; for (const edge of graph.getEdges(null, from, to)) { @@ -95,23 +95,19 @@ export class Edge { form.button({ id: 'add', name: 'Add Edge Type', - cb: (_, { initializing }) => { - if (initializing) return; - addEdgeForm(new Edge(graph, null, graph.getVertex(from), graph.getVertex(to))); - }, + cb: () => addEdgeForm(new Edge(graph, null, graph.getVertex(from), graph.getVertex(to))), }) .button({ id: 'save', name: 'Save', - cb: ({ form: { value } }, { initializing }) => { - if (initializing) return; + cb: ({ form: { value: { edges } } }) => { // Handle additions and updates - for (const { type, weight } of value.edges) { + for (const { type, weight } of edges) { graph.setEdgeWeight(type, from, to, weight); } // Handle removals for (const edge of graph.getEdges(null, from, to)) { - if (!value.edges.find(({ type }) => type === edge.type)) { + if (!edges.find(({ type }) => type === edge.type)) { graph.deleteEdge(edge.type, from, to); } } @@ -121,10 +117,7 @@ export class Edge { .button({ id: 'cancel', name: 'Cancel', - cb: (_, { initializing }) => { - if (initializing) return; - doc.clear(); - }, + cb: () => doc.clear(), }); } } diff --git a/forum-network/src/classes/supporting/vertex.js b/forum-network/src/classes/supporting/vertex.js index 3df6a53..ae1819b 100644 --- a/forum-network/src/classes/supporting/vertex.js +++ b/forum-network/src/classes/supporting/vertex.js @@ -1,15 +1,19 @@ +import { displayNumber } from '../../util/helpers.js'; + export class Vertex { constructor(graph, type, id, data, options = {}) { this.graph = graph; this.type = type; this.id = id; this.data = data; + this.label = options.label ?? this.id; this.options = options; this.edges = { from: [], to: [], }; this.installedClickCallback = false; + this.properties = new Map(); } reset() { @@ -22,18 +26,28 @@ export class Vertex { ); } - setDisplayLabel(label) { - this.label = label; - this.displayVertex(); + setProperty(key, value) { + this.properties.set(key, value); + return this; } displayVertex() { if (this.options.hide) { return; } - this.graph.flowchart?.log(`${this.id}[${this.label}]`); + + let html = `${this.label}`; + html += ''; + for (const [key, value] of this.properties.entries()) { + const displayValue = typeof value === 'number' ? displayNumber(value) : value; + html += ``; + } + html += '
${key}${displayValue}
'; + html = html.replaceAll(/\n\s*/g, ''); + this.graph.flowchart?.log(`${this.id}[${html}]`); + if (this.graph.editable && !this.installedClickCallback) { - this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex"`); + this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`); this.installedClickCallback = true; } } @@ -44,24 +58,50 @@ export class Vertex { if (!vertex) { throw new Error(`Could not find WDG Vertex ${vertexId}`); } - doc.remark('

Edit Vertex

'); const form = doc.form().lastElement; + doc.remark('

Edit Vertex

', { parentEl: form.el }); form .textField({ id: 'id', name: 'id', defaultValue: vertex.id, }) .textField({ id: 'type', name: 'type', defaultValue: vertex.type }) - .textField({ id: 'label', name: 'label', defaultValue: vertex.label }) + .textField({ id: 'label', name: 'label', defaultValue: vertex.label }); + + doc.remark('

Properties

', { parentEl: form.el }); + const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem; + const addPropertyForm = (key, value) => { + const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem; + subForm.textField({ id: 'key', name: 'key', defaultValue: key }) + .textField({ id: 'value', name: 'value', defaultValue: value }) + .button({ + id: 'remove', + name: 'Remove Property', + cb: () => subFormArray.remove(subForm), + }); + doc.remark('
', { parentEl: subForm.el }); + }; + + for (const [key, value] of vertex.properties.entries()) { + addPropertyForm(key, value); + } + + form.button({ + id: 'add', + name: 'Add Property', + cb: () => addPropertyForm('', ''), + }) .button({ id: 'save', name: 'Save', - cb: ({ form: { value } }, { initializing }) => { - if (initializing) return; + type: 'submit', + cb: ({ form: { value: formValue } }) => { let fullRedraw = false; - if (value.id && value.id !== vertex.id) { + if (formValue.id !== vertex.id) { fullRedraw = true; } - Object.assign(vertex, value); + // TODO: preserve data types of properties + formValue.properties = new Map(formValue.properties.map(({ key, value }) => [key, value])); + Object.assign(vertex, formValue); vertex.displayVertex(); if (fullRedraw) { graph.redraw(); @@ -71,10 +111,7 @@ export class Vertex { .button({ id: 'cancel', name: 'Cancel', - cb: (_, { initializing }) => { - if (initializing) return; - doc.clear(); - }, + cb: () => doc.clear(), }); return doc; } diff --git a/forum-network/src/classes/supporting/wdg.js b/forum-network/src/classes/supporting/wdg.js index 45b333e..56312a3 100644 --- a/forum-network/src/classes/supporting/wdg.js +++ b/forum-network/src/classes/supporting/wdg.js @@ -103,9 +103,9 @@ export class WeightedDirectedGraph { if (this.vertices.has(id)) { throw new Error(`Vertex already exists with id: ${id}`); } - const vertex = new Vertex(this, type, id, data, options); + const vertex = new Vertex(this, type, id, data, { ...options, label }); this.vertices.set(id, vertex); - vertex.setDisplayLabel(label ?? id); + vertex.displayVertex(); return vertex; } diff --git a/forum-network/src/index.css b/forum-network/src/index.css index 2db2c1b..6354248 100644 --- a/forum-network/src/index.css +++ b/forum-network/src/index.css @@ -83,3 +83,6 @@ label > div { display: inline-block; min-width: 50px; } +table { + width: 100%; +} diff --git a/forum-network/src/tests/scripts/input.test.js b/forum-network/src/tests/scripts/input.test.js index b3bf2ee..a06d5ab 100644 --- a/forum-network/src/tests/scripts/input.test.js +++ b/forum-network/src/tests/scripts/input.test.js @@ -27,7 +27,13 @@ describe('Document > Form > TextField', () => { dv.set(value); }; - form.textField({ id: 'input1', name: 'Input 1', cb: updateFieldValueDisplay }); + form.textField({ + id: 'input1', + name: 'Input 1', + cb: updateFieldValueDisplay, + cbEventTypes: ['keydown', 'keyup'], + cbOnInit: true, + }); doc.remark('Hmm...!'); }); // it('can exist within a graph', () => {