diff --git a/sql/00008.sql b/sql/00008.sql new file mode 100644 index 0000000..2701ba5 --- /dev/null +++ b/sql/00008.sql @@ -0,0 +1,123 @@ +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; + + -- Safeguard against being your own parent. + IF node_uuid = node_parent_uuid THEN + RAISE EXCEPTION 'Node UUID is same as node parent UUID.' USING ERRCODE = 'XPRNT'; + 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_parent_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/js/page_node.mjs b/static/js/page_node.mjs index f6aa681..339f903 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -424,6 +424,9 @@ export class Node { return this._parent }//}}} moveToParent(newParentUUID) {// {{{ + if (this.UUID === newParentUUID) + throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.") + this.ParentUUID = newParentUUID this.data.ParentUUID = newParentUUID this._modified = true diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs index 63c4ddc..becf44d 100644 --- a/static/js/sidebar.mjs +++ b/static/js/sidebar.mjs @@ -598,15 +598,25 @@ export class N2TreeNode extends CustomHTMLElement { e.preventDefault() }// }}} async dragDrop(e) {// {{{ - e.stopPropagation() - const sourceNode = _app.dragIcon.getSource() - await _app.moveNode(sourceNode.node, this.node.UUID) + try { + e.stopPropagation() + const sourceNode = _app.dragIcon.getSource() - _app.sidebar.setNodeExpanded(this, true) - await this.render(true, true) - await sourceNode.render(true, true) + // Abort if user drops the node back on itself. + if (sourceNode.node.UUID === this.node.UUID) + return - this.dragLeave(e) + await _app.moveNode(sourceNode.node, this.node.UUID) + + _app.sidebar.setNodeExpanded(this, true) + await this.render(true, true) + await sourceNode.render(true, true) + } catch (e) { + console.error(e) + alert(e) + } finally { + this.dragLeave(e) + } }// }}} dragEnter(e) {// {{{ const targetNode = e.target.closest('n2-treenode')