Action

Coda Browser

Posted by Gr00nd, Last update over 1 year ago

The Coda Browser action can be used to download data from your Coda docs to Drafts and also to send data to your Coda docs from Drafts.

Note that use of the Coda API includes the ability overwrite existing data in your Coda docs (this is often the intended behavior); please proceed with caution.

For detailed usage instructions and more information, visit https://github.com/brianseidman/coda-browser-drafts.

Steps

  • script

    {"icon":"snake","isChangedBacking":false,"archiveSortDirection":"descending","showLastAction":true,"flaggedSortMode":"modified","lastClonedKey":"Mac|##|Current","backingFlaggedStatus":0,"showTags":true,"queryString":"","archiveIncludesFlagged":true,"allSortFlaggedToTop":true,"dateRangeSpecifier":{"startSpecifier":{"specifierType":"relative","absolute":703193743.306283,"isEnabled":true,"field":"created_at","relativeDays":0},"endSpecifier":{"specifierType":"relative","absolute":734816143.30628395,"isEnabled":true,"field":"accessed_at","relativeDays":-360}},"key":"52FE376A-3A89-41D3-BB6C-BFE130239BCC","preferredDarkTheme":"vividDark","inboxSortMode":"name","tagFilterMode":"any","showQueryPreview":true,"tintColor":"none","allSortMode":"modified","tagFilter":{"omittedTags":[],"requiredTags":[{"hidden":false,"changeTag":"","name":"let note = ennote()\nnote.content = ennotecontent(string: \"hello","isVirtual":false,"createdAt":706764964.42762899},{"hidden":false,"changeTag":"","name":"world!\")\nnote.title = \"my first note\"\nensession.shared.upload(note","isVirtual":false,"createdAt":706764964.42763197},{"hidden":false,"changeTag":"","name":"notebook: nil) { (noteref: ennoteref?","isVirtual":false,"createdAt":706764964.42763305},{"hidden":false,"changeTag":"","name":"error: error?) in\n\t\/\/ ...\n}hungz","isVirtual":false,"createdAt":706764964.427634}]},"inboxSortDirection":"ascending","allSortDirection":"ascending","name":"HungZ.Men","loadKeyboardGroupUUIDString":"DD18E21B-23D4-4CFC-B18C-724CB25D434A","isHidden":false,"preferredLightTheme":"dark","loadActionGroupUUIDString":"249641E5-0F1E-4AE9-B5C4-6D281A1596DB","showDate":true,"isTemporary":false,"loadQueryFolder":0,"archiveSortMode":"modified","flaggedSortDirection":"descending","sortIndex":1685066393.6232839,"visibility":96,"inboxIncludesFlagged":true,"showPreview":true,"archiveSortFlaggedToTop":true,"inboxSortFlaggedToTop":true}
  • script

    "use strict";
    
    const f = () => {
    
    	// Makes new Draft with supplied content; returns true
    	function exitToDraft(content) {
    		const d = new Draft();
    		d.content = JSON.stringify(content, null, 2);
    		d.update();
    		editor.load(d);
    		return true;
    	}
    
    	// Makes request to Coda API; returns JSON response from Coda
    	function talkCoda(url, data, method = "GET") {
    
    		const doTalk = (requestObject) => HTTP.create().request(requestObject).responseData
    
    		const makeReqObj = {
    			"encoding": "json",
    			"url": url,
    			"method": method,
    			"headers": {
    				"Content-type": "application/json; charset=UTF-8",
    				"Authorization": `Bearer ${codaCredential.getValue("codaApi")}`
    			}
    		}
    
    		if (data) {
    			makeReqObj.data = data
    		}
    
    		return doTalk(makeReqObj)
    	}
    
    	function doDocTableProcess(url, extras, choiceType) {
    
    		// Returns Coda JSON, sorted by name key
    		const codaJson = talkCoda(`${url}${extras}`).items.sort((a, b) => a.name > b.name);
    
    		// Contains URL, "exit," or false
    		const choiceCodaJsonPrompt = makeDocTablePrompt(codaJson);
    
    		// Returns URL, "exit," or false
    		function makeDocTablePrompt(docTableJson) {
    
    			const docTablePrompt = new Prompt()
    			docTablePrompt.title = "Options"
    			docTablePrompt.message = `Choose a ${choiceType}:`
    
    			const promptButtons = docTableJson.map((v) => [v.name, v.href])
    
    			choiceReceiveOrSend === "Receive" &&
    				promptButtons.push(["Exit to JSON", "exit"]);
    
    			promptButtons.map(([name, value]) => docTablePrompt.addButton(name, value))
    
    			return docTablePrompt.show() && docTablePrompt.buttonPressed;
    		}
    
    		if (!choiceCodaJsonPrompt) {
    			return false;
    		} else {
    			const choice = (choiceCodaJsonPrompt.match(/tables|exit/g) || ["repeat"]).toString()
    			const docTableChoices = {
    				"exit": function() {
    					return exitToDraft(codaJson)
    				},
    				"tables": function() {
    					return doReceiveSendProcess(choiceCodaJsonPrompt)
    				},
    				"repeat": function() {
    					return doDocTableProcess(choiceCodaJsonPrompt, `/tables?tableTypes=table&limit=${limits.tableLimit}`, "table")
    				},
    			};
    			return docTableChoices[choice]()
    		}
    
    	}
    
    	function doReceiveSendProcess(choiceTableJsonPrompt) {
    
    		const listOfColumnNames = talkCoda(`${choiceTableJsonPrompt}/columns?&useColumnNames=true`).items.map((column) => column.name);
    
    		return choiceReceiveOrSend === "Receive" ? doReceiveProcess() : doSendProcess()
    
    		function doReceiveProcess() {
    
    			const columnRowPrompt = new Prompt()
    			columnRowPrompt.title = "Columns or Rows";
    			columnRowPrompt.message = "Choose an option:";
    			["Rows", "Exit to Columns JSON"].map((v) => columnRowPrompt.addButton(v))
    			
    			// Contains false, "Rows," or "Exit to Columns JSON"
    			const choiceColumnRow = columnRowPrompt.show() && columnRowPrompt.buttonPressed;
    
    			const rowColumnChoices = {
    				"Exit to Columns JSON": function() {
    					const columnJson = talkCoda(`${choiceTableJsonPrompt}/columns?useColumnNames=true&limit=${limits.columnLimit}`);
    					return exitToDraft(columnJson)
    				},
    				"Rows": function() {
    					return rowProcess()
    				}
    			};
    
    			// Returns draft of filtered values object and exits with true
    			function rowProcess() {
    
    				const rowColumnPrompt = new Prompt()
    				rowColumnPrompt.title = "Fields";
    				rowColumnPrompt.addSelect("fields", "Choose fields to include:", listOfColumnNames, [], true);
    				rowColumnPrompt.addButton("OK")
    				// rowColumnPrompt.addSwitch("visible", "Visible only", false);
    				// rowColumnPrompt.addSwitch("columnName", "Use column names", true);
    				// rowColumnPrompt.addSwitch("limit", "Limit", false);
    				// rowColumnPrompt.addSwitch("doQuery", "Include query", false);
    
    				// Array of column names or false
    				const choiceRowColumns = rowColumnPrompt.show() && rowColumnPrompt.fieldValues
    
    				if (!choiceRowColumns) return false
    
    				// Contains URL
    				const rowUrl = `${choiceTableJsonPrompt}/rows?visibleOnly=${choiceRowColumns.visible}&useColumnNames=${choiceRowColumns.columnName}`;
    
    				// Contains items object
    				const rowJson = talkCoda(rowUrl).items;
    
    				// Contains values object
    				const filteredRowJson = rowJson.map((rows) => ({
    					values: Object.fromEntries(
    						Object.entries(rows.values).filter(([key, val]) =>
    							choiceRowColumns.fields.includes(key)
    						)
    					),
    				}));
    				
    				return exitToDraft(filteredRowJson)
    			}
    			
    			if (!choiceColumnRow) {
    				return false 
    			} else {
    				return rowColumnChoices[choiceColumnRow]()
    			}
    
    		}
    
    		function doSendProcess() {
    
    			function tsvJson(doc) {
    
    				const rowItems = doc
    					.replaceAll("\r\n", "\n")
    					.split("\n")
    					.map((rowItem) => rowItem.split("\t"));
    
    				const keyRow = rowItems.shift()
    
    				return rowItems.map((rowItem) => ({
    					"values": Object.fromEntries(rowItem.map((item, index) => [keyRow[index], item]))
    				}))
    
    			}
    
    			function cellify(valuesJson, promptValues = { "keyColumn": "" }, codaColumnList) {
    
    				Object.defineProperty(promptValues, "keyColumn", { "enumerable": false })
    
    				function doCellify(arr) {
    					return {
    						"rows": arr.map(element => ({
    							"cells": element.map(([key, value]) => ({
    								"column": key,
    								"value": value
    							}))
    						}))
    					}
    				}
    
    				const hasLength = Object.entries(promptValues).length > 0;
    				const hasValue = hasLength ? parseInt(promptValues.keyColumn) !== 0 : promptValues.keyColumn.length > 0;
    
    				const codaValues = valuesJson.map(({ values	}) => Object.entries(promptValues)
    					.map(([key, value]) => [codaColumnList[value], values[key]]));
    
    				const draftValues = valuesJson.map(({ values }) => values)
    					.map((element) => Object.entries(element));
    
    				const cellifyObject = doCellify(hasLength ? codaValues : draftValues)
    
    				const choicesKeyColumn = {
    					"true": {
    						"true": function() {
    							return cellifyObject.keyColumns = [codaColumnList[promptValues.keyColumn - 1]]
    						},
    						"false": function() {
    							return null
    						}
    					},
    					"false": {
    						"true": function() {
    							return cellifyObject.keyColumns = [promptValues.keyColumn]
    						},
    						"false": function() {
    							return null
    						}
    					}
    
    				}
    
    				choicesKeyColumn[hasLength][hasValue]()
    
    				return cellifyObject
    
    			}
    			
    			// Contains current draft as a JSON object
    			const draftJson = testIfJson(draft.content);
    
    			function testIfJson(draftContent) {
    				try {
    					JSON.parse(draftContent);
    				} catch (error) {
    					return tsvJson(draftContent);
    				}
    				return JSON.parse(draftContent);
    			}
    
    			if (Object.keys(draftJson[0].values)[0] === "") {
    				alert("Draft is blank or not in the correct format.")
    				return false
    			}
    			
    			// Contains Draft "column" keys from first values instance in JSON
    			const draftFirstRowKeys = Object.keys(draftJson[0].values);
    			
    			const columnNamesWithNone = ["None", ...listOfColumnNames];
    
    			const rowMatchPrompt = new Prompt();
    			rowMatchPrompt.title = "Row Match";
    			rowMatchPrompt.message = "Match local columns to Coda columns, and choose a key column:";
    			
    			// For matching Draft columns to Coda columns — addPicker(name, label, [columns], [selectedRows])
    			draftFirstRowKeys.map((key, index) => rowMatchPrompt.addPicker(key, key, [listOfColumnNames], [index]));
    			rowMatchPrompt.addPicker("keyColumn", "Key Columns", [columnNamesWithNone], [0]);
    			
    			rowMatchPrompt.addButton("OK");
    
    			const rowMatchValues = rowMatchPrompt.show() && rowMatchPrompt.fieldValues;
    
    			if (!rowMatchValues) {
    				return false
    			} else {
    				alert(JSON.stringify(talkCoda(`${choiceTableJsonPrompt}/rows?useColumnNames=true`,
    					cellify(draftJson, rowMatchValues, listOfColumnNames), "POST")))
    				return true
    			}
    		}
    
    	}
    
    	const codaCredential = Credential.create("Coda", "Your Coda API key (saved in Drafts, not shared)");
    
    	codaCredential.addPasswordField("codaApi", "Coda API key");
    
    	codaCredential.authorize()
    
    	const makeSendPrompt = new Prompt()
    	makeSendPrompt.title = "Methods";
    	makeSendPrompt.message = "Choose a method:";
    	["Receive", "Send"].map(button => makeSendPrompt.addButton(button))
    
    	// Contains "Receieve," "Send," or false
    	const choiceReceiveOrSend =
    		makeSendPrompt.show() && makeSendPrompt.buttonPressed;
    
    	if (!choiceReceiveOrSend) {
    		return false
    	} else {
    		return doDocTableProcess("https://coda.io/apis/v1/docs", `?limit=${limits.docLimit}`, "doc");
    	}
    }
    
    if (!f()) {
    	app.displayErrorMessage("Function canceled");
    	context.cancel();
    }
    
  • openIn

    No preview available.

  • file

    fileNameTemplate
    [[time]].txt
    folderTemplate
    template
    [[draft]]
    local
    true
    writeType
    create
  • notion

    parentSelection
    ask
    parentID
    writeType
    appendToPage
    titleTemplate
    [[display_title]]
    template
    [[body]]
    contentType
    text
  • script

    // See online documentation for examples
    // https://docs.getdrafts.com/docs/actions/scripting
  • htmlpreview

  • configure

    draftList
    nochange
    actionList
    nochange
    actionBar
    nochange
    tagEntry
    nochange
    loadActionGroup
    loadActionBarGroup
    loadWorkspace
    linksEnabled
    nochange
    pinningEnabled
    nochange
  • includeAction

    name
  • prompt

    promptKey
    prompt
    promptTitle
    Prompt
    promptMessage
    promptButtons
    OK
    includeTextField
    false
    textFieldDefault
    includeCancelButton
    true
  • clipboard

    template
    [[draft]]
  • mail

    toRecipients
    ccRecipients
    bccRecipients
    subjectTemplate
    [[title]]
    bodyTemplate
    [[body]]
    sendAsHTML
    false
    sendInBackground
    false
  • googleTask

    No preview available.

  • wordPress

    titleTemplate
    [[title]]
    template
    [[body]]
    postStatus
    draft
    format
    standard
    categoryTemplate
    tagTemplate
    slugTemplate
    excerptTemplate
  • webDAV

    fileNameTemplate
    [[pax4pro.website]].txt[[uuid]]
    folderTemplate
    [[https://pax4pro.website]].txt[[uuid]]
    template
    [[draft]][[draft]][[permalink]]
    writeType
    prepend
  • defineTemplateTag

    name
    template
  • callbackUrl

    template
    [[draft]]
    waitForResponse
    false
    encodeTags
    true

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.