From 41952df764f5e75be26b65b29d83e9941e84c2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 19 Dec 2024 22:28:13 +0100 Subject: [PATCH] Storing Node objects in NodeStore for single instance objects --- static/css/notes2.css | 7 ++++ static/js/node.mjs | 65 ++++++++++++++++++++--------- static/js/node_store.mjs | 85 +++++++++++++++++++++++++++++++++---- static/js/notes2.mjs | 90 +++++++++++++++++++++++----------------- static/less/notes2.less | 8 ++++ 5 files changed, 187 insertions(+), 68 deletions(-) diff --git a/static/css/notes2.css b/static/css/notes2.css index bed7439..30f207a 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -83,6 +83,13 @@ html { color: #333; border-radius: 5px; } +.crumbs.node-modified { + background-color: #fe5f55; + color: #efede8; +} +.crumbs.node-modified .crumb:after { + color: #efede8; +} .crumbs .crumb { margin-right: 8px; cursor: pointer; diff --git a/static/js/node.mjs b/static/js/node.mjs index 32572af..02676c3 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -28,18 +28,20 @@ export class NodeUI extends Component { return const node = this.node.value - document.title = node.Name + document.title = node.get('Name') + + const nodeModified = this.nodeModified.value ? 'node-modified' : '' return html` -
-
+
this.saveNode()}> +
_notes2.current.goToNode(ROOT_NODE)}>Start
Minnie
Fluffy
Chili
-
${node.Name}
+
${node.get('Name')}
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
` @@ -57,10 +59,6 @@ export class NodeUI extends Component { html`
this.goToNode(node.ID)}>${node.Name}
` ).reverse()) - let modified = '' - if (this.props.app.nodeModified.value) - modified = 'modified' - // Page to display let page = '' @@ -145,15 +143,27 @@ export class NodeUI extends Component { }//}}} async componentDidMount() {//{{{ _notes2.current.goToNode(this.props.startNode.UUID, true) + _notes2.current.tree.expandToTrunk(this.props.startNode) }//}}} setNode(node) {//{{{ this.nodeModified.value = false this.node.value = node }//}}} + async saveNode() {//{{{ + if (!this.nodeModified.value) + return + + await nodeStore.copyToNodesHistory(this.node.value) + + // Prepares the node object for saving. + // Sets Updated value to current date and time. + const node = this.node.value + node.save() + await nodeStore.add([node]) + this.nodeModified.value = false + }//}}} keyHandler(evt) {//{{{ - return - let handled = true // All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees. @@ -163,6 +173,7 @@ export class NodeUI extends Component { return switch (evt.key.toUpperCase()) { + /* case 'C': this.showPage('node') break @@ -183,12 +194,14 @@ export class NodeUI extends Component { this.showPage('node-properties') break + */ case 'S': - if (this.page.value == 'node') + if (this.page.value === 'node') this.saveNode() - else if (this.page.value == 'node-properties') + else if (this.page.value === 'node-properties') this.nodeProperties.current.save() break + /* case 'U': this.showPage('upload') @@ -197,6 +210,7 @@ export class NodeUI extends Component { case 'F': this.showPage('search') break + */ default: handled = false @@ -262,8 +276,7 @@ class NodeContent extends Component { }//}}} contentChanged(evt) {//{{{ _notes2.current.nodeUI.current.nodeModified.value = true - const content = evt.target.value - this.props.node.setContent(content) + this.props.node.setContent(evt.target.value) this.resize() }//}}} resize() {//{{{ @@ -274,6 +287,11 @@ class NodeContent extends Component { } export class Node { + static sort(a, b) {//{{{ + if (a.data.Name < b.data.Name) return -1 + if (a.data.Name > b.data.Name) return 0 + return 0 + }//}}} constructor(nodeData, level) {//{{{ this.Level = level this.data = nodeData @@ -283,6 +301,10 @@ export class Node { this._children_fetched = false this.Children = [] + + this._content = this.data.Content + this._modified = false + /* this.RenderMarkdown = signal(nodeData.RenderMarkdown) this.Markdown = false @@ -298,6 +320,7 @@ export class Node { // Used to expand the crumbs upon site loading. */ }//}}} + get(prop) {//{{{ return this.data[prop] }//}}} @@ -314,15 +337,17 @@ export class Node { this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1) this._children_fetched = true return this.Children - }//}}} + content() {//{{{ /* TODO - implement crypto if (this.CryptoKeyID != 0 && !this._decrypted) this.#decrypt() */ - return this.data.Content + this.modified = true + return this._content }//}}} + setContent(new_content) {//{{{ this._content = new_content /* TODO - implement crypto @@ -334,10 +359,10 @@ export class Node { this._decrypted = true */ }//}}} - static sort(a, b) {//{{{ - if (a.data.Name < b.data.Name) return -1 - if (a.data.Name > b.data.Name) return 0 - return 0 + save() {//{{{ + this.data.Content = this._content + this.data.Updated = new Date().toISOString() + this._modified = false }//}}} } diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index b90a613..7f06573 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -9,10 +9,11 @@ export class NodeStore { } this.db = null + this.nodes = {} }//}}} async initializeDB() {//{{{ return new Promise((resolve, reject) => { - const req = indexedDB.open('notes', 5) + const req = indexedDB.open('notes', 7) // Schema upgrades for IndexedDB. // These can start from different points depending on updates to Notes2 since a device was online. @@ -20,6 +21,7 @@ export class NodeStore { let nodes let appState let sendQueue + let nodesHistory const db = event.target.result const trx = event.target.transaction @@ -30,11 +32,11 @@ export class NodeStore { switch (i) { case 1: nodes = db.createObjectStore('nodes', { keyPath: 'UUID' }) - nodes.createIndex('nameIndex', 'Name', { unique: false }) + nodes.createIndex('byName', 'Name', { unique: false }) break case 2: - trx.objectStore('nodes').createIndex('parentIndex', 'ParentUUID', { unique: false }) + trx.objectStore('nodes').createIndex('byParent', 'ParentUUID', { unique: false }) break case 3: @@ -42,13 +44,21 @@ export class NodeStore { break case 4: - trx.objectStore('nodes').createIndex('modifiedIndex', 'modified', { unique: false }) + trx.objectStore('nodes').createIndex('byModified', 'modified', { unique: false }) break case 5: sendQueue = db.createObjectStore('send_queue', { keyPath: ['UUID', 'Updated'] }) sendQueue.createIndex('updated', 'Updated', { unique: false }) break + + case 6: + nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] }) + break + + case 7: + trx.objectStore('nodes_history').createIndex('byUUID', 'UUID', { unique: false }) + break } } } @@ -83,6 +93,7 @@ export class NodeStore { UUID: ROOT_NODE, Name: 'Notes2', Content: 'Hello, World!', + Updated: new Date().toISOString(), }) putRequest.onsuccess = (event) => { resolve(event.target.result) @@ -102,6 +113,13 @@ export class NodeStore { return this.setAppState('client_uuid', clientUUID) }//}}} + node(uuid, dataIfUndefined, newLevel) {//{{{ + let n = this.node[uuid] + if (n === undefined && dataIfUndefined !== undefined) + n = this.node[uuid] = new Node(dataIfUndefined, newLevel) + return n + }//}}} + async getAppState(key) {//{{{ return new Promise((resolve, reject) => { const trx = this.db.transaction('app_state', 'readonly') @@ -180,6 +198,25 @@ export class NodeStore { } }) }//}}} + async copyToNodesHistory(nodeToCopy) {//{{{ + return new Promise((resolve, reject) => { + const t = this.db.transaction('nodes_history', 'readwrite') + const nodesHistory = t.objectStore('nodes_history') + t.oncomplete = () => { + resolve() + } + t.onerror = (event) => { + console.log('transaction error', event.target.error) + reject(event.target.error) + } + + const historyReq = nodesHistory.put(nodeToCopy.data) + historyReq.onerror = (event) => { + console.log(`Error copying ${nodeToCopy.UUID}`, event.target.error) + reject(event.target.error) + } + }) + }//}}} async storeNode(node) {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') @@ -245,12 +282,13 @@ export class NodeStore { return new Promise((resolve, reject) => { const trx = this.db.transaction('nodes', 'readonly') const nodeStore = trx.objectStore('nodes') - const index = nodeStore.index('parentIndex') + const index = nodeStore.index('byParent') const req = index.getAll(parent) req.onsuccess = (event) => { const nodes = [] for (const i in event.target.result) { - const node = new Node(event.target.result[i], newLevel) + const nodeData = event.target.result[i] + const node = this.node(nodeData.UUID, nodeData, newLevel) nodes.push(node) } @@ -259,6 +297,7 @@ export class NodeStore { req.onerror = (event) => reject(event.target.error) }) }//}}} + async add(records) {//{{{ return new Promise((resolve, reject) => { try { @@ -299,8 +338,6 @@ export class NodeStore { }//}}} async get(uuid) {//{{{ return new Promise((resolve, reject) => { - // Node is always returned from IndexedDB if existing there. - // Otherwise an attempt to get it from backend is executed. const trx = this.db.transaction('nodes', 'readonly') const nodeStore = trx.objectStore('nodes') const getRequest = nodeStore.get(uuid) @@ -310,11 +347,41 @@ export class NodeStore { reject("No such node") return } - const node = new Node(event.target.result, -1) + const node = this.node(uuid, event.target.result, -1) resolve(node) } }) }//}}} + async getNodeAncestry(node, accumulated) {//{{{ + console.log('blaha') + return new Promise((resolve, reject) => { + const nodeParentIndex = this.db + .transaction('nodes', 'readonly') + .objectStore('nodes') + + if (node.ParentUUID === '') { + resolve(accumulated) + return + } + + const getRequest = nodeParentIndex.get(node.ParentUUID) + getRequest.onsuccess = (event) => { + // Node not found in IndexedDB. + // Not expected to happen. + const parentNodeData = event.target.result + if (parentNodeData === undefined) { + reject("No such node") + return + } + + const parentNode = this.node(parentNodeData.UUID, parentNodeData, -1) + this.getNodeAncestry(parentNode, accumulated.concat(parentNode)) + .then(accumulated => resolve(accumulated)) + } + }) + + }//}}} + async nodeCount() {//{{{ return new Promise((resolve, reject) => { const t = this.db.transaction('nodes', 'readwrite') diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs index 665568f..af67544 100644 --- a/static/js/notes2.mjs +++ b/static/js/notes2.mjs @@ -8,7 +8,6 @@ const html = htm.bind(h) export class Notes2 extends Component { constructor() {//{{{ super() - this.tree = createRef() this.nodeUI = createRef() this.state = { startNode: null, @@ -25,7 +24,7 @@ export class Notes2 extends Component { return return html` - <${Tree} ref=${this.tree} app=${this} startNode=${startNode} /> + <${Tree} app=${this} startNode=${startNode} />
<${NodeUI} app=${this} ref=${this.nodeUI} startNode=${startNode} /> @@ -56,10 +55,9 @@ export class Notes2 extends Component { // New node is fetched in order to retrieve content and files. // Such data is unnecessary to transfer for tree/navigational purposes. - nodeStore.get(nodeUUID).then(node => { - this.nodeUI.current.setNode(node) - //this.showPage('node') - }) + const node = nodeStore.node(nodeUUID) + this.nodeUI.current.setNode(node) + this.tree.setSelected(node) }//}}} logout() {//{{{ localStorage.removeItem('session.UUID') @@ -73,7 +71,9 @@ class Tree extends Component { this.treeNodes = {} this.treeNodeComponents = {} this.treeTrunk = [] - this.selectedTreeNode = null + this.selectedNode = null + this.expandedNodes = {} // keyed on UUID + this.props.app.tree = this this.populateFirstLevel() @@ -81,7 +81,7 @@ class Tree extends Component { render({ app }) {//{{{ const renderedTreeTrunk = this.treeTrunk.map(node => { this.treeNodeComponents[node.UUID] = createRef() - return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.startNode?.UUID} />` + return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />` }) return html`
@@ -98,7 +98,6 @@ class Tree extends Component { this.treeNodes = {} this.treeNodeComponents = {} this.treeTrunk = [] - this.selectedTreeNode = null // A tree of nodes is built. This requires the list of nodes // returned from the server to be sorted in such a way that @@ -125,14 +124,21 @@ class Tree extends Component { .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) }//}}} setSelected(node) {//{{{ - return // TODO - if (this.selectedTreeNode) - this.selectedTreeNode.selected.value = false + // The previously selected node, if any, needs to be rerendered + // to not retain its 'selected' class. + const prevUUID = this.selectedNode?.UUID + this.selectedNode = node + if (prevUUID) + this.treeNodeComponents[prevUUID]?.current.forceUpdate() - this.selectedTreeNode = this.treeNodeComponents[node.ID].current - this.selectedTreeNode.selected.value = true - this.selectedTreeNode.expanded.value = true - this.expandToTrunk(node.ID) + // And now the newly selected node is rerendered. + this.treeNodeComponents[node.UUID]?.current.forceUpdate() + + // Expanding selected nodes... I don't know... + this.setNodeExpanded(node.UUID, true) + }//}}} + isSelected(node) {//{{{ + return this.selectedNode?.UUID === node.UUID }//}}} crumbsUpdateNodes(node) {//{{{ console.log('crumbs', this.props.app.startNode.Crumbs) @@ -153,32 +159,41 @@ class Tree extends Component { if (node !== undefined) this.setSelected(node) }//}}} - expandToTrunk(nodeUUID) {//{{{ - let node = this.treeNodes[nodeUUID] - if (node === undefined) - return - - node = this.treeNodes[node.ParentUUID] - while (node !== undefined) { - this.treeNodeComponents[node.UUID].current.expanded.value = true - node = this.treeNodes[node.ParentUUID] + async expandToTrunk(node) {//{{{ + // Get all ancestors from a certain node up to the highest grandparent. + const ancestry = await nodeStore.getNodeAncestry(node, []) + for (const i in ancestry) { + await nodeStore.node(ancestry[i].UUID).fetchChildren() + this.setNodeExpanded(ancestry[i].UUID, true) } + + // Start the chain of by expanding the top node. + this.setNodeExpanded(ancestry[ancestry.length-1].UUID, true) + }//}}} + getNodeExpanded(UUID) {//{{{ + if (this.expandedNodes[UUID] === undefined) + this.expandedNodes[UUID] = signal(false) + return this.expandedNodes[UUID].value + }//}}} + setNodeExpanded(UUID, value) {//{{{ + // Creating a default value if it doesn't exist already. + this.getNodeExpanded(UUID) + this.expandedNodes[UUID].value = value }//}}} } class TreeNode extends Component { constructor(props) {//{{{ super(props) - this.selected = signal(props.selected) - this.expanded = signal(this.props.node._expanded) - this.children_populated = signal(false) - if (this.props.node.Level === 0) + if (this.props.node.Level === 0 || this.props.tree.getNodeExpanded(this.props.node.UUID)) this.fetchChildren() }//}}} render({ tree, node, parent }) {//{{{ // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. - if (!this.children_populated.value && parent?.expanded.value) + const selected = tree.isSelected(node) ? 'selected' : '' + + if (!this.children_populated.value && tree.getNodeExpanded(parent?.props.node.UUID)) this.fetchChildren() const children = node.Children.map(node => { @@ -190,25 +205,22 @@ class TreeNode extends Component { if (node.Children.length === 0) expandImg = html`` else { - if (this.expanded.value) + if (tree.getNodeExpanded(node.UUID)) expandImg = html`` else expandImg = html`` } - const selected = (this.selected.value ? 'selected' : '') - return html`
-
{ this.expanded.value ^= true }}>${expandImg}
+
{ tree.setNodeExpanded(node.UUID, !tree.getNodeExpanded(node.UUID)) }}>${expandImg}
window._notes2.current.goToNode(node.UUID)}>${node.get('Name')}
-
${children}
+
${children}
` }//}}} - fetchChildren() {//{{{ - this.props.node.fetchChildren().then(() => { - this.children_populated.value = true - }) + async fetchChildren() {//{{{ + await this.props.node.fetchChildren() + this.children_populated.value = true }//}}} } diff --git a/static/less/notes2.less b/static/less/notes2.less index 52472ef..f3abdc5 100644 --- a/static/less/notes2.less +++ b/static/less/notes2.less @@ -119,6 +119,14 @@ html { color: #333; border-radius: 5px; + &.node-modified { + background-color: @color1; + color: @color2; + .crumb:after { + color: @color2; + } + } + .crumb { margin-right: 8px; cursor: pointer;