diff --git a/sql/00006.sql b/sql/00006.sql
new file mode 100644
index 0000000..56f2acb
--- /dev/null
+++ b/sql/00006.sql
@@ -0,0 +1,119 @@
+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'),
+ parent_uuid = (node_data->>'ParentUUID')::uuid,
+ 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/css/notes2.css b/static/css/notes2.css
index 04c9f68..079e3c1 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -418,6 +418,8 @@ n2-nodeui {
font-size: 1.75em;
margin-top: 8px;
margin-bottom: 0px;
+ white-space: nowrap;
+ width: min-content;
}
.el-functions {
diff --git a/static/images/icon_drag.svg b/static/images/icon_drag.svg
new file mode 100644
index 0000000..02d628e
--- /dev/null
+++ b/static/images/icon_drag.svg
@@ -0,0 +1,71 @@
+
+
+
+
diff --git a/static/images/icon_drag_ok.svg b/static/images/icon_drag_ok.svg
new file mode 100644
index 0000000..94ba949
--- /dev/null
+++ b/static/images/icon_drag_ok.svg
@@ -0,0 +1,75 @@
+
+
+
+
diff --git a/static/images/icon_drag_source.svg b/static/images/icon_drag_source.svg
new file mode 100644
index 0000000..6378ed9
--- /dev/null
+++ b/static/images/icon_drag_source.svg
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/static/js/app.mjs b/static/js/app.mjs
index baefe87..8fc43df 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -12,6 +12,7 @@ export class App {
this.crumbs = new N2Crumbs()
this.crumbsElement = document.getElementById('crumbs')
this.nodeUI = document.getElementById('note')
+ this.dragIcon = new N2DragIcon()
this.sidebar.render().then(sidebar => {
document.getElementById('tree').append(sidebar)
@@ -68,6 +69,7 @@ export class App {
})
document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
+ document.body.append(this.dragIcon)
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
@@ -78,7 +80,6 @@ export class App {
// There a slight delay to initiate sync seems reasonable.
setTimeout(() => window._sync.run(), 1000)
}// }}}
-
keyHandler(event) {//{{{
let handled = true
@@ -151,6 +152,10 @@ export class App {
async saveNode() {//{{{
}//}}}
+ async moveNode(node, targetNodeUUID) {// {{{
+ node.moveToParent(targetNodeUUID)
+ await node.save()
+ }// }}}
async createNode(createUnderUUID) {//{{{
const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
const p = createUnderUUID ? 'Name for sibling document' : 'Name for sub-document'
@@ -239,7 +244,6 @@ class N2Crumbs extends CustomHTMLElement {
return this
}// }}}
}
-customElements.define('n2-crumbs', N2Crumbs)
class N2Crumb extends CustomHTMLElement {
static {// {{{
@@ -270,7 +274,6 @@ class N2Crumb extends CustomHTMLElement {
this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
}// }}}
}
-customElements.define('n2-crumb', N2Crumb)
function tmpl(html) {// {{{
const el = document.createElement('template')
@@ -344,4 +347,52 @@ class OpSearch extends Op {
}// }}}
}
+class N2DragIcon extends CustomHTMLElement {
+ static {// {{{
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+
+ `
+ }// }}}
+ constructor() {// {{{
+ super(true)
+
+ document.addEventListener('dragover', e => {
+ this.style.left = `${e.clientX + 8}px`
+ this.style.top = `${e.clientY}px`
+ })
+
+ this.dragTarget = null
+ }// }}}
+ start() {// {{{
+ this.style.display = 'block'
+ }// }}}
+ end() {// {{{
+ this.style.display = 'none'
+ }// }}}
+ icon(name) {// {{{
+ if (name != '')
+ name = '_' + name
+ this.elIcon.setAttribute('src', `/images/${_VERSION}/icon_drag${name}.svg`)
+ }// }}}
+ setTarget(t) {// {{{
+ this.dragTarget = t
+ }// }}}
+ getTarget() {// {{{
+ return this.dragTarget
+ }// }}}
+}
+
+customElements.define('n2-crumbs', N2Crumbs)
+customElements.define('n2-crumb', N2Crumb)
+customElements.define('n2-dragicon', N2DragIcon)
+
// vim: foldmethod=marker
diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs
index f31a4b6..324f004 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -247,6 +247,7 @@ export class NodeStore {
nodeStore = t.objectStore('nodes')
t.oncomplete = (_event) => {
+ console.log('complete')
resolve()
}
@@ -358,6 +359,7 @@ class SimpleNodeStore {
// Node to be moved is first stored in the new queue.
const req = store.put(node.data)
req.onsuccess = () => {
+ console.log('here')
resolve()
}
req.onerror = (event) => {
diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs
index 834e22f..f65afb9 100644
--- a/static/js/page_node.mjs
+++ b/static/js/page_node.mjs
@@ -422,6 +422,11 @@ export class Node {
getParent() {//{{{
return this._parent
}//}}}
+ moveToParent(newParentUUID) {// {{{
+ this.ParentUUID = newParentUUID
+ this.data.ParentUUID = newParentUUID
+ this._modified = true
+ }// }}}
isLastSibling() {//{{{
return this._sibling_after === null
}//}}}
@@ -463,9 +468,10 @@ export class Node {
// When stored into database and ancestry was changed,
// 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.
@@ -481,12 +487,17 @@ export class Node {
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])
+ console.log('waiting')
+ await Promise.all([history, sendQueue, nodeStoreAdding])
+ console.log('waiting done')
+
+ return
}//}}}
}
diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs
index 7d73d6a..561de8d 100644
--- a/static/js/sidebar.mjs
+++ b/static/js/sidebar.mjs
@@ -449,15 +449,20 @@ export class N2Sidebar extends CustomHTMLElement {
treenode?.scrollIntoView({ block: 'nearest' })
}// }}}
}
-customElements.define('n2-sidebar', N2Sidebar)
export class N2TreeNode extends CustomHTMLElement {
+ static DRAG_ICON = new Image()
+ static DRAG_ICON_OK = new Image()
+
static {// {{{
+ N2TreeNode.DRAG_ICON.src = `/images/${_VERSION}/leaf.svg`
+ N2TreeNode.DRAG_ICON_OK.src = `/images/${_VERSION}/expanded.svg`
+
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `