Action
Send Multi-Items to Dynalist
UPDATES
almost 3 years ago
The position where the items will be inserted is now selectable. (Top / End)
almost 3 years ago
The position where the items will be inserted is now selectable. (Top / End)
almost 3 years ago
Enabled to select the position where the item will be inserted. (Top / End)
almost 3 years ago
Enabled to select the position where the item will be inserted. (Prepend / Append)
about 3 years ago
Changed to keep the draft in the Inbox instead of moving it to the archive if you cancel when selecting a destination or if there is any error communicating with Dynalist.
Send action from Drafts app to Dynalist for multiple items with nested structures.
Initial setting
Access token registration
When you run this action for the first time, you will be asked for the access token of Dynalist, so enter it.
Access tokens can be obtained from Dynalist’s Developer Page.
Destination item settings
You can register destinations other than inbox in advance.
The first time this action is launched, <iCloud Drive>/Drafts/DynalistMultiNestBulkSend/destinations.txt
will be created.
Add the links of the items you want to send to.
example:
When the action is executed, it asks for the destination.
Format
Limitations
The Dynalist API used by this action has two restrictions:
Rate limit policy
You can access this API at a steady rate of 60 times per minute, with a burst of 20 requests at once. See the rate limit section for more information on how it works.
The rate limit above is for number of requests. There’s another rate limit for the number of changes you can send. You can send changes at a steady rate of 240 changes per minute, with a burst of 500 changes.
https://apidocs.dynalist.io/#rate-limit-policy-5
This action sends items of the same nesting level together, so you won’t hit the first limitation unless you use it abnormally to send an outline structure that exceeds 20 nesting levels at once.
However, even if the outline structure is one nested level, sending more than 500 items at a time can lead to a second limit.
https://sorashima.hatenablog.com/entry/MultiNestLvlMultiNodeSendFromDraftsToDynalist
Steps
-
script
/* DynalistMultiNestBulkSend */ /* 2021-12-13 */ const ROOTPATH = "/" const DIRNAME = "DynalistMultiNestBulkSend" const FILENAME = "destinations.txt" // Store Secret Token of Dynalist in Credential. const cre = Credential.create("Dynalist", "Dynalist") cre.addPasswordField("token", "Secret Token") cre.authorize() // // If the directory "DynalistMultiNestBulkSend" in iCloud drive does not exist, create it. // const mkDir = function () { const fmCloud = FileManager.createCloud() let success = true if (!fmCloud.listContents(ROOTPATH).includes(DIRNAME + "/")) { if ( !fmCloud.createDirectory(DIRNAME, ROOTPATH) ) { alert(`Can't create directory "${DIRNAME}".`) success = false } } return success } // // read destination urls from "DynalistMultiNestBulkSend/destinations.txt" stored in iCloud drive // const readDestTxt = function () { const fmCloud = FileManager.createCloud() const res = {success: true} let content = fmCloud.readString(ROOTPATH + DIRNAME + "/" + FILENAME) if (content === undefined) { // if the file does not exist or could not be read, try to create the file. content = "destinations\n// Write the destination Dynalist item links one by one below.\n" if ( !fmCloud.writeString(ROOTPATH + DIRNAME + "/" + FILENAME, content) ) { alert(`Can't create file "${FILENAME}".`) res.success = false } } res.content = content return res } // // Read items from Dynalist based on URL read from destinations.txt. // const readDestContentFromDL = function (t) { const http = HTTP.create() const destA = [] t.split("\n").forEach(l => { const idA = l.match(/https:\/\/dynalist\.io\/d\/([^#]+)(#z=(.+)|)$/) if (idA !== null) { const file_id = idA[1] const node_id = idA[3] !== undefined ? idA[3] : "root" const response = http.request({ url: "https://dynalist.io/api/v1/doc/read", encoding: "json", method: "POST", data: { token: cre.getValue("token"), file_id: file_id } }) if (response.success) { // If the HTTP request completes successfully. const dynalistRes = JSON.parse(response.responseText) if (dynalistRes._code == 'Ok') { // If the _code in the response is Ok (successful request) const destO = {title: dynalistRes.title} const nodeO = dynalistRes.nodes.find(n => n.id == node_id) if (nodeO !== undefined) { destO.content = nodeO.content destO.file_id = file_id destO.node_id = node_id destA.push(destO) } } } } }) return destA } // // Get the location of inbox // const getLocOfInbox = function () { const res = {success: false} const http = HTTP.create() const response = http.request({ url: "https://dynalist.io/api/v1/pref/get", encoding: "json", method: "POST", data: { token: cre.getValue("token"), key: "inbox_location" } }) if (response.success) { // If the HTTP request completes successfully. const dynalistRes = JSON.parse(response.responseText) if (dynalistRes._code == 'Ok') { // If the _code in the response is Ok (successful request) const valueA = dynalistRes.value.split("/") res.file_id = valueA[0] res.node_id = valueA[1] !== undefined ? valueA[1] : "root" res.success = true } else { alert(dynalistRes._code + "\n" + dynalistRes._msg) } } else { alert(response.error) } return res } // // Read the draft line by line, parse it, and save the items in an array. // const parseDraft = function () { let isTitle = true let maxLvl = 0 let preLvl = 0 let cNodeNum = 0 const parentStack = [] const nodeStack = [] let noteStack = [] let aNode = {param: {content: ""}} draft.lines.forEach(l => { const bulletA = l.match(/^([-ー]+)([0-30-3]|[roygbpれおいぐぶぱレオイグブパ]|[?!?!])?([0-30-3]|[roygbpれおいぐぶぱレオイグブパ]|[?!?!])?([0-30-3]|[roygbpれおいぐぶぱレオイグブパ]|[?!?!])?[ ]/i) if (bulletA !== null) { // // title of a item // // Stack the previous item on the array if (cNodeNum != 0) { // Execute if not the first loop. // Delete the last blank line in the note. if (noteStack[noteStack.length - 1] == "") noteStack.pop() aNode.param.note = noteStack.join("\n") noteStack = [] nodeStack.push(aNode) aNode = {param: {content: ""}} } isTitle = true // Determine nesting level aNode.nestLvl = bulletA[1].length // Determine maximum level if (aNode.nestLvl > maxLvl) maxLvl = aNode.nestLvl switch (true) { case aNode.nestLvl == preLvl: // If the nesting level does not change, do nothing to the parent stack. break case aNode.nestLvl == preLvl + 1: // If the nesting level is one deeper, stack the previous nesting level on the parent stack. parentStack.push(cNodeNum) break case aNode.nestLvl < preLvl: // Break the delta parent stack when the nesting level is shallow. for (let i = 0; i < preLvl - aNode.nestLvl; i++) { parentStack.pop() } break case aNode.nestLvl > preLvl + 1: // Add empty diff node if nesting level goes up by 2 or more. for (let i = preLvl + 1; i < aNode.nestLvl; i++) { const iNode = {param: {content: ""}} iNode.nestLvl = i parentStack.push(cNodeNum) iNode.parentNum = parentStack[parentStack.length - 1] iNode.num = ++cNodeNum nodeStack.push(iNode) } parentStack.push(cNodeNum) break } aNode.parentNum = parentStack[parentStack.length - 1] aNode.num = ++cNodeNum preLvl = aNode.nestLvl aNode.param.content = l.match(/^([-ー]+)([0-30-3]|[roygbpれおいぐぶぱレオイグブパ]|[?!?!])?([0-30-3]|[roygbpれおいぐぶぱレオイグブパ]|[?!?!])?([0-30-3]|[roygbpれおいぐぶぱレオイグブパ]|[?!?!])?[ ](.*)$/i)[5] Array.from(bulletA[0]).forEach(s => { switch (true) { case /[00]/.test(s): break case /[11]/.test(s): aNode.param.heading = 1 break case /[22]/.test(s): aNode.param.heading = 2 break case /[33]/.test(s): aNode.param.heading = 3 break case /[rれレ]/i.test(s): aNode.param.color = 1 break case /[oおオ]/i.test(s): aNode.param.color = 2 break case /[yいイ]/i.test(s): aNode.param.color = 3 break case /[gぐグ]/i.test(s): aNode.param.color = 4 break case /[bぶブ]/i.test(s): aNode.param.color = 5 break case /[pぱパ]/i.test(s): aNode.param.color = 6 break case /[??]/.test(s): aNode.param.checkbox = true aNode.param.checked = false break case /[!!]/.test(s): aNode.param.checkbox = true aNode.param.checked = true break } }) } else { // // [note | other than the first line of the title] of a item // const bullet2A = l.match(/^[、,](.*)$/) if (bullet2A !== null && isTitle) { // // If the lines starting with a comma follow the title line, treat it as a continuation of the title, otherwise treat it as part of a note. // aNode.param.content += `\n${bullet2A[1]}` } else { // // note // isTitle = false noteStack.push(l) } } }) // Stack last item on the array //Delete the last blank line in the note. if (noteStack[noteStack.length - 1] == "") noteStack.pop() aNode.param.note = noteStack.join("\n") nodeStack.push(aNode) return {nodeStack: nodeStack, maxLvl: maxLvl} } // // Select a destination. // const selDest = function (locOfInbox, destA) { const res = {selected: true} const p = Prompt.create() p.title = "Select Destination" p.addButton("Inbox") destA.forEach((dest, idx) => { p.addButton(dest.content, idx) }) p.addSelect("position","Position",["Top","End"],["Top"],false) if ( !p.show() ) { res.selected = false } else { res.index = p.fieldValues["position"] == "Top" ? 0 : -1 if (p.buttonPressed == "Inbox") { res.file_id = locOfInbox.file_id res.node_id = locOfInbox.node_id } else { p.buttonPressed res.file_id = destA[p.buttonPressed].file_id res.node_id = destA[p.buttonPressed].node_id } } return res } // // Insert items of the same nesting level together into Dynalist from the highest nesting level to lowest. // const sendToDynalistInBulk = function (parsDrft, dest) { let sendingStatOK = true const http = HTTP.create() const req = { url: "https://dynalist.io/api/v1/doc/edit", encoding: "json", method: "POST", data: { token: cre.getValue("token"), file_id: dest.file_id } } for (i = 1; i <= parsDrft.maxLvl; i++) { if (sendingStatOK) { const changesA = [] const sentNode = [] parsDrft.nodeStack.forEach((node, idx) => { if (node.nestLvl == i) { const changeO = { action: "insert", index: dest.index } // Parameter settings for each item (content, note, color, heading, checkboxe and it status) Object.assign(changeO, node.param) // For the outermost nested node, parent_id should be the node_id of the selected destination. if (node.parentNum == 0) { changeO.parent_id = dest.node_id } else { // Otherwise, parent_id should be the node_id of the parent node of each item. changeO.parent_id = parsDrft.nodeStack.find(n => n.num == node.parentNum).node_id } changesA.push(changeO) sentNode.push(idx) } }) req.data.changes = changesA.reverse() console.log(JSON.stringify(req)) const response = http.request(req) if (response.success) { // If the HTTP request completes successfully. const dynalistRes = JSON.parse(response.responseText) console.log(JSON.stringify(dynalistRes)) if (dynalistRes._code == 'Ok') { // If the _code in the response is Ok (successful request) // If sending is successful, store the item's node_id for descendant nodes. dynalistRes.new_node_ids.reverse().forEach((n, idx) => { parsDrft.nodeStack[sentNode[idx]].node_id = n }) } else { alert(dynalistRes._code + "\n" + dynalistRes._msg) sendingStatOK = false } } else { alert(response.error) sendingStatOK = false } } } } // // supervisor // let success = false if (mkDir()) { const destTxt = readDestTxt() if (destTxt.success) { const destA = readDestContentFromDL(destTxt.content) const locOfInbox = getLocOfInbox() if (locOfInbox.success) { const pd = parseDraft() const dest = selDest(locOfInbox, destA) success = true if (dest.selected) { sendToDynalistInBulk(pd, dest) } else { context.cancel() } } } } if (!success) context.fail()
Options
-
After Success Archive , Tags: dynalist Notification Info Log Level Info