diff --git a/node.go b/node.go index e3a61d5..6b79769 100644 --- a/node.go +++ b/node.go @@ -54,6 +54,7 @@ type Node struct { DeletedSeq sql.NullInt64 `db:"deleted_seq"` Content string ContentEncrypted string `db:"content_encrypted" json:"-"` + Markdown bool } func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint64, moreRowsExist bool, err error) { // {{{ @@ -74,7 +75,7 @@ func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint6 public.node WHERE user_id = $1 AND - ( + NOT history AND ( created_seq > $4 OR updated_seq > $4 OR deleted_seq > $4 @@ -131,13 +132,14 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, updated_seq, deleted_seq, content, - content_encrypted + content_encrypted, + markdown FROM public.node WHERE user_id = $1 AND client != $5::uuid AND - ( + NOT history AND ( created_seq > $4 OR updated_seq > $4 OR deleted_seq > $4 @@ -190,7 +192,7 @@ func NodesCount(userID int, synced uint64, clientUUID string) (count int, err er WHERE user_id = $1 AND client != $3 AND - ( + NOT history AND ( created_seq > $2 OR updated_seq > $2 OR deleted_seq > $2 diff --git a/sql/00005.sql b/sql/00005.sql deleted file mode 100644 index b272085..0000000 --- a/sql/00005.sql +++ /dev/null @@ -1,129 +0,0 @@ --- Some cleanup of old columns not used anymore. -DROP INDEX public.node_history_client_idx; -ALTER TABLE public.node_history DROP COLUMN client_sequence; - -ALTER TABLE public.node DROP COLUMN markdown; -DROP INDEX public.node_history_idx; -ALTER TABLE public.node DROP COLUMN history; -ALTER TABLE public.node DROP COLUMN client_sequence; - - - -CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb) - LANGUAGE plpgsql -AS $procedure$ - -DECLARE - node_data jsonb; - node_updated timestamptz; - db_updated timestamptz; - db_uuid uuid; - db_client uuid; - db_history_uuid uuid; - node_uuid uuid; - node_parent_uuid uuid; - node_history_uuid uuid; - -BEGIN - FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) - LOOP - node_uuid = (node_data->>'UUID')::uuid; - node_history_uuid = (node_data->>'HistoryUUID')::uuid; - node_updated = (node_data->>'Updated')::timestamptz; - - - - -- Frontend is using an all-zero UUID to define the root node. - -- Database is using NULL. - IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' OR node_data->>'ParentUUID' = '' THEN - node_parent_uuid = NULL; - ELSE - node_parent_uuid = (node_data->>'ParentUUID')::uuid; - END IF; - - - - -- Every jode has a new history UUID to keep the history entry uniquely identifiable - -- across clients. A history entry could potentially be sent again, but should be - -- safe to ignore as every change to a node should have a new history UUID. - -- - -- The current node is also stored as history. - INSERT INTO node_history( - user_id, "uuid", "history_uuid", parents, created, updated, - "name", "content", "content_encrypted", - client - ) - VALUES( - p_user_id, -- combined key - node_uuid, -- combined key - node_history_uuid, -- combined key - (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", - COALESCE((node_data->>'Created')::timestamptz, NOW()), - COALESCE((node_data->>'Updated')::timestamptz, NOW()), - (node_data->>'Name')::varchar, - (node_data->>'Content')::text, - '', /* content_encrypted */ - p_client_uuid - ) - ON CONFLICT ("user_id", "uuid", "history_uuid") - DO NOTHING; - - - - -- Retrieve the current modified timestamp for this node from the database. - SELECT - uuid, updated, client - INTO - db_uuid, db_updated, db_client - FROM public."node" - WHERE - user_id = p_user_id AND - uuid::uuid = node_uuid::uuid; - - - - -- Is the node not in database? It needs to be created. - IF db_uuid IS NULL THEN - RAISE NOTICE '01 New node %', node_uuid; - - INSERT INTO public."node" ( - user_id, "uuid", parent_uuid, created, updated, - "name", "content", "content_encrypted", - client - ) - VALUES( - p_user_id, - node_uuid, - node_parent_uuid, - COALESCE((node_data->>'Created')::timestamptz, NOW()), - COALESCE((node_data->>'Updated')::timestamptz, NOW()), - (node_data->>'Name')::varchar, - (node_data->>'Content')::text, - '', /* content_encrypted */ - p_client_uuid - ); - - CONTINUE; - - END IF; - - - - -- Update the public node as well if it was older than incoming node. - IF node_updated > db_updated THEN - UPDATE public."node" - SET - updated = (node_data->>'Updated')::timestamptz, - updated_seq = nextval('node_updates'), - name = (node_data->>'Name')::varchar, - content = (node_data->>'Content')::text, - client = p_client_uuid - WHERE - user_id = p_user_id AND - uuid::uuid = node_uuid::uuid; - END IF; - - END LOOP; -END -$procedure$ -; diff --git a/static/js/app.mjs b/static/js/app.mjs index 112827e..38aebd4 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -74,11 +74,6 @@ export class App { keyHandler(event) {//{{{ let handled = true - if (event.key == 'F2') { - this.nodeUI.renameNode() - return - } - // All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees. // Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving. // Thus, the exception is acceptable to consequent use of alt+shift. @@ -87,15 +82,25 @@ export class App { switch (event.key.toUpperCase()) { case 'T': - if (document.activeElement.id === 'tree-nodes') + if (document.activeElement.id === 'tree-nodes') { this.nodeUI.takeFocus() - else + } else { this.sidebar.focus() + } break case 'F': _mbus.dispatch('op-search') break + /* + case 'C': + this.showPage('node') + break + + case 'E': + this.showPage('keys') + break + */ case 'M': globalThis._mbus.dispatch('MARKDOWN_TOGGLE') @@ -105,9 +110,25 @@ export class App { this.createNode() break + /* + case 'P': + this.showPage('node-properties') + break + + */ case 'S': this.nodeUI.saveNode() break + /* + + case 'U': + this.showPage('upload') + break + + case 'F': + this.showPage('search') + break + */ default: handled = false @@ -140,12 +161,11 @@ export class App { return const nn = Node.create(name, this.currentNode.UUID) - await nn.save() + nn.save() + + nodeStore.sendQueue.add(nn) + nodeStore.add([nn]) - // Treenode is forcefully rerendered and children refetched to both show the new node - // and to get it resorted. - const treenode = this.sidebar.getTreeNode(this.currentNode.UUID) - treenode.render(true, true) }//}}} async goToNode(nodeUUID, dontPush, dontExpand) {//{{{ if (nodeUUID === null || nodeUUID === undefined) diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index 963a72b..2005816 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -63,7 +63,18 @@ export class N2PageNodeUI extends CustomHTMLElement { _mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown())) _mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data)) - this.elName.addEventListener('click', async () => this.renameNode()) + this.elName.addEventListener('click', () => { + const name = prompt('Change title', this.node.data.Name) + if (name === null) + return + + try { + this.node.setName(name) + } catch (err) { + console.error(err) + alert(err) + } + }) this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event)) this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown())) @@ -82,7 +93,7 @@ export class N2PageNodeUI extends CustomHTMLElement { this.node.setContent(this.elNodeContent.value) }) this.elIconHistory.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'history' })) - this.elIconSave.addEventListener('click', () => this.saveNode()) + this.elIconSave.addEventListener('click', ()=>this.saveNode()) this.showMarkdown(true) }// }}} @@ -100,37 +111,35 @@ export class N2PageNodeUI extends CustomHTMLElement { } else this.elNodeContent.focus({ preventScroll: true }) }// }}} - async renameNode() {// {{{ - const name = prompt('Change title', this.node.data.Name) - if (name === null) - return - - try { - // Document isn't only renamed, but also saved at once. - // Not really correct, but good enough to not have to implement - // a separate way to only rename the document. Since history is - // preserved it shouldn't be that horrible. - this.node.setName(name) - await this.node.save() - - // Re-render the parent treenode forcefully to sort it again. - const parentUUID = this.node.ParentUUID - if (!parentUUID) - return - const parentTreeNode = _app.sidebar.getTreeNode(parentUUID) - parentTreeNode?.render(true, true) - } catch (err) { - console.error(err) - alert(err) - } - }// }}} async saveNode() {// {{{ if (!this.node.isModified()) return - // node.save takes care of both "nodes" and "nodes_history" stores, also adds it to send queue. - // Sets "Updated" value to current date and time and generates a new history UUID. + /* The node history is a local store for node history. + * This could be provisioned from the server or cleared if + * deemed unnecessary. + * + * The send queue is what will be sent back to the server + * to have a recorded history of the notes. + * + * A setting to be implemented in the future could be to + * not save the history locally at all. */ + + // The node is still in its old state and will present + // the unmodified content to the node store. + const history = nodeStore.nodesHistory.add(this.node) + + // Prepares the node object for saving. + // Sets Updated value to current date and time. await this.node.save() + + // Updated node is added to the send queue to be stored on server. + const sendQueue = nodeStore.sendQueue.add(this.node) + + // Updated node is saved to the primary node store. + const nodeStoreAdding = nodeStore.add([this.node]) + + await Promise.all([history, sendQueue, nodeStoreAdding]) }// }}} contentChanged(event) {//{{{ @@ -297,20 +306,15 @@ export class Node { return 0 }//}}} static create(name, parentUUID) {// {{{ - const node = new Node({ + return new Node({ UUID: uuidv7(), Created: (new Date()).toISOString(), Content: '', Name: name, ParentUUID: parentUUID, Markdown: false, + History: false, }) - - // Newly created node (not constructed from existing data) is considered modified - // since node.save returns early if it isn't modified. - node._modified = true - - return node }// }}} constructor(nodeData, level) {//{{{ @@ -427,28 +431,6 @@ export class Node { // the ancestry path could be interesting. const ancestors = await nodeStore.getNodeAncestry(this) this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() - - /* The node history is a local store for node history. - * This could be provisioned from the server or cleared if - * deemed unnecessary. - * - * The send queue is what will be sent back to the server - * to have a recorded history of the notes. - * - * A setting to be implemented in the future could be to - * not save the history locally at all. */ - - // Current node is added to history. It will be duplicated with the "nodes" store - // for simplicity, to hopefully avoid bugs. - const history = nodeStore.nodesHistory.add(this) - - // Updated node is added to the send queue to be stored on server. - const sendQueue = nodeStore.sendQueue.add(this) - - // Updated node is saved to the primary node store. - const nodeStoreAdding = nodeStore.add([this]) - - return Promise.all([history, sendQueue, nodeStoreAdding]) }//}}} } diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs index 8d5bcbd..23c78c0 100644 --- a/static/js/sidebar.mjs +++ b/static/js/sidebar.mjs @@ -212,9 +212,6 @@ export class N2Sidebar extends CustomHTMLElement { isSelected(node) {//{{{ return this.selectedNode?.UUID === node.UUID }//}}} - getTreeNode(uuid) {// {{{ - return this.treeNodeComponents[uuid] - }// }}} async keyHandler(event) {//{{{ let handled = true @@ -517,8 +514,8 @@ export class N2TreeNode extends CustomHTMLElement { if (this.rendered && force_update !== true) return this - if (this.sidebar.getNodeExpanded(this.node.UUID) || force_refetch_children) - await this.fetchChildren(force_refetch_children) + if (this.sidebar.getNodeExpanded(this.node.UUID)) + await this.fetchChildren() // Update the name and selected status. this.elName.querySelector('span').innerText = this.node.get('Name') diff --git a/static/js/sync.mjs b/static/js/sync.mjs index b6328aa..35485eb 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -89,7 +89,7 @@ export class Sync { nodeStore.setAppState('latest_sync_node', currMax) } catch (e) { - console.error('sync node tree', e) + console.log('sync node tree', e) } finally { syncEnd = Date.now() const duration = (syncEnd - syncStart) / 1000 @@ -235,6 +235,7 @@ export class N2SyncProgress extends CustomHTMLElement { this.elDownloadTransferred.innerText = this.state.nodesDowloaded this.elDownloadTotal.innerText = this.state.nodesToDownload + console.log('setting elUploadTransferred', this.state.nodesUploaded) this.elUploadTransferred.innerText = this.state.nodesUploaded this.elUploadTotal.innerText = this.state.nodesToUpload }//}}}