Compare commits
No commits in common. "31eee4ede57c10124cadfcd59afd8048c44fc78f" and "3e8d5b6d9a58f3e197d8c8b9c5d31cf5ccfbf026" have entirely different histories.
31eee4ede5
...
3e8d5b6d9a
6 changed files with 81 additions and 208 deletions
10
node.go
10
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
|
||||
|
|
|
|||
129
sql/00005.sql
129
sql/00005.sql
|
|
@ -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$
|
||||
;
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
@ -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])
|
||||
}//}}}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}//}}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue