OO-PG0004
Plug-In: Outline to Project
This plug-in creates a new OmniFocus project using the contents of the outline document.
The plug-in works by converting the outline data into a JSON record and then sending the created record to OmniFocus to be processed as input by a specialized function that creates a project with a hierarchical task structure matching the source outline. Row topic columns and their corresponding notes are transfered. Any user-added columns are ignored.
FEATURE: Hold down the Option key when selecting the plug-in to activate spoken prompts.
Return to: OmniOutliner Plug-In Collection
Outline to Project
/*{"type": "action","targets": ["omnioutliner"],"author": "Otto Automator","identifier": "com.omni-automation.oo.outline-to-project","version": "1.1","description": "Create a new OmniFocus project containing the contents of the outline.","label": "Outline to OmniFocus Project","shortLabel": "Outline to Project","paletteLabel": "Outline to Project","image":"archivebox.fill"}*/(() => {var shouldSpeakconst action = new PlugIn.Action(async function(selection, sender){try {function createUtterance(textToSpeak){langCode = Speech.Voice.currentLanguageCodevoiceObj = Speech.Voice.withLanguage(langCode)utterance = new Speech.Utterance(textToSpeak)utterance.voice = voiceObjutterance.rate = Speech.Utterance.defaultSpeechRatereturn utterance}shouldSpeak = (app.optionKeyDown) ? true:falsevar synthesizer = new Speech.Synthesizer()try {document.name} catch(err){throw {name:"Missing Resource", message:"No outline document is open."}}if (columns.length > 3){alertTitle = "Incompatible Columns"alertMessage = "This document contains custom columns, which are not transferable to OmniFocus. Choose “Continue” to transfer only row topics and their notes."alert = new Alert(alertTitle, alertMessage)alert.addOption("Continue")alert.addOption("Stop")buttonIndex = await alert.show()if(buttonIndex === 1){throw {name: "-128",message: "User cancelled."}}}documentName = document.name.replace(/\.[^/.]+$/, "")console.log("DOCUMENT NAME:", documentName)// GET DATA AS JSON RECORD ARRAYitemData = rootItem.descendants.map(item => {itemObj = new Object()itemObj.level = item.levelitemObj.topic = item.topicitemObj.note = item.notereturn itemObj})console.log("ITEM DATA:", JSON.stringify(itemData))// CONSTRUCT AND DISPLAY FORMtextInputField = new Form.Field.String("projectTitle","Project Title",documentName)menuItems = ["Parallel", "Sequential", "Single Actions"]menuIndexes = [0,1,2]menuElement = new Form.Field.Option("projectTypeIndex","Project Type",menuIndexes,menuItems,0)completedByChildrenCheckbox = new Form.Field.Checkbox("completedByActions","Complete with last action",null)inputForm = new Form()inputForm.addField(textInputField)inputForm.addField(menuElement)inputForm.addField(completedByChildrenCheckbox)inputForm.validate = function(formObject){projectTitle = formObject.values['projectTitle']return ((!projectTitle)?false:true)}formPrompt = "Title and settings for new project:"buttonTitle = "Continue"if(shouldSpeak){utterance = createUtterance(formPrompt)synthesizer.speakUtterance(utterance)}formObject = await inputForm.show(formPrompt, buttonTitle)projectTitle = formObject.values['projectTitle']projectTypeIndex = formObject.values['projectTypeIndex']completedByActions = formObject.values['completedByActions']// THE OPTIONAL ARGUMENT: string, number, date, array, or objectvar targetFunctionArgument = {"items": itemData,"title": projectTitle,"projectTypeIndex": projectTypeIndex,"completedByActions": completedByActions}var targetAppName = "omnifocus"// THE FUNCTION TO BE EXECUTED ON THE TARGET APPfunction targetAppFunction(targetFunctionArgument){try {dataItems = targetFunctionArgument["items"]projectTitle = targetFunctionArgument["title"]projectTypeIndex = targetFunctionArgument["projectTypeIndex"]completedByActions = targetFunctionArgument["completedByActions"]var project = new Project(projectTitle)project.status = Project.Status.Activeif (projectTypeIndex === 1){project.sequential = true} else if (projectTypeIndex === 2){project.containsSingletonActions = true}project.completedByChildren = completedByActions// link-back to projectprojectID = project.id.primaryKeyvar linkURL = "omnifocus:///task/" + projectIDconsole.log("PROJECT LINK:", linkURL)var previousItem = nullvar previousLevel = nullvar insertionLocation = nulldataItems.forEach((dataItem, index, dataArray) => {itemLevel = dataItem.levelitemTopic = dataItem.topicitemNote = dataItem.noteif(index === 0){ // first top-level iteminsertionLocation = project.ending} else if (itemLevel === 1){ // top level iteminsertionLocation = project.ending} else if (itemLevel > 1){ // child or sibling of previous itemif(itemLevel === previousLevel){ // siblinginsertionLocation = previousItem.parent.ending} else if (itemLevel > previousLevel){ // childinsertionLocation = previousItem.ending} else if (itemLevel < previousLevel){ // calculate previous parentvar parentItem = project.taskfor(var i = 0; i < (itemLevel -1); i++){parentItem = parentItem.tasks.pop()}insertionLocation = parentItem.ending}}task = new Task(itemTopic, insertionLocation)task.note = itemNotepreviousItem = taskpreviousLevel = itemLevel})document.windows[0].focus = [project]// return link to project to calling scriptreturn linkURL}catch(err){console.error(err.name, err.message)throw {name: err.name,message: err.message}}}// CREATE SCRIPT URL WITH FUNCTION AND PASSED DATAscriptURL = URL.tellFunction(targetAppName,targetAppFunction,targetFunctionArgument)// CALL THE SCRIPT URL, PROCESS RESULTS OR ERRORscriptURL.call(reply => {// PROCESS RESULTS OF SCRIPTif (typeof reply === "object"){console.log(reply["result"])} else {if(reply){console.log("CALL RESULT:", reply)// open returned link to show new projectURL.fromString(reply).open()}}if(shouldSpeak){utterance = createUtterance("Done!")synthesizer.speakUtterance(utterance)}}, error => {// PROCESS SCRIPT ERRORconsole.error("CALL ERROR:", error.errorMessage)throw error})}catch(err){if(!err.causedByUserCancelling && err.name !== "-128"){if(shouldSpeak){utterance = createUtterance(err.message)synthesizer.speakUtterance(utterance)}new Alert(err.name, err.message).show()}}});action.validate = function(selection, sender){return true};return action;})();