├── CustomComplete.omnifocusjs ├── Resources │ ├── en.lproj │ │ ├── manifest.strings │ │ ├── customComplete.strings │ │ └── preferences.strings │ ├── customComplete.js │ ├── preferences.js │ └── customCompleteLib.js └── manifest.json ├── .github └── workflows │ └── main.yml ├── CHANGELOG.md └── README.md /CustomComplete.omnifocusjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.KaitlinSalzke.customComplete" = "Custom Complete"; -------------------------------------------------------------------------------- /CustomComplete.omnifocusjs/Resources/en.lproj/customComplete.strings: -------------------------------------------------------------------------------- 1 | "label" = "Custom Complete"; 2 | "shortLabel" = "Custom Complete"; 3 | "mediumLabel" = "Custom Complete"; 4 | "longLabel" = "Custom Complete"; -------------------------------------------------------------------------------- /CustomComplete.omnifocusjs/Resources/en.lproj/preferences.strings: -------------------------------------------------------------------------------- 1 | "label" = "Preferences: Custom Complete"; 2 | "shortLabel" = "Preferences: Custom Complete"; 3 | "mediumLabel" = "Preferences: Custom Complete"; 4 | "longLabel" = "Preferences: Custom Complete"; -------------------------------------------------------------------------------- /CustomComplete.omnifocusjs/Resources/customComplete.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const task = selection.tasks[0] || selection.projects[0].task 5 | 6 | const lib = await this.customCompleteLib.onComplete(task) 7 | }) 8 | 9 | action.validate = function (selection, sender) { 10 | return selection.tasks.length === 1 || selection.projects.length === 1 11 | } 12 | 13 | return action 14 | })() 15 | -------------------------------------------------------------------------------- /CustomComplete.omnifocusjs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultLocale": "en", 3 | "identifier": "com.KaitlinSalzke.customComplete", 4 | "author": "Kaitlin Salzke", 5 | "description": "A custom complete action for OmniFocus", 6 | "version": "3.2.0", 7 | "actions": [ 8 | { 9 | "identifier": "customComplete", 10 | "image": "checkmark.diamond.fill" 11 | }, 12 | { 13 | "identifier": "preferences", 14 | "image": "gearshape.2" 15 | } 16 | ], 17 | "libraries": [ 18 | { 19 | "identifier": "customCompleteLib" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /CustomComplete.omnifocusjs/Resources/preferences.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Form flattenedTags */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const syncedPrefs = this.customCompleteLib.loadSyncedPrefs() 5 | 6 | // get current preferences or set defaults if they don't yet exist 7 | const tagsToRemove = this.customCompleteLib.tagsToRemove() 8 | const selectNextNode = this.customCompleteLib.selectNextNodePref() 9 | const openInNewWindow = this.customCompleteLib.openInNewWindowPref() 10 | 11 | // create and show form 12 | const form = new Form() 13 | form.addField(new Form.Field.Checkbox('selectNextNode', 'Select next task when completing', selectNextNode)) 14 | form.addField(new Form.Field.Checkbox('openInNewWindow', 'Open in new window', openInNewWindow)) 15 | form.addField(new Form.Field.MultipleOptions('tagsToRemove', 'Tag(s) to remove when a task is completed', flattenedTags, flattenedTags.map(t => t.name), tagsToRemove)) 16 | await form.show('Preferences: Custom Complete', 'OK') 17 | 18 | // save preferences 19 | syncedPrefs.write('selectNextNode', form.values.selectNextNode) 20 | syncedPrefs.write('tagsToRemoveIDs', form.values.tagsToRemove.map(tag => tag.id.primaryKey)) 21 | syncedPrefs.write('openInNewWindowPref', form.values.openInNewWindow) 22 | }) 23 | 24 | action.validate = function (selection, sender) { 25 | return true 26 | } 27 | 28 | return action 29 | })() 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | - name: Conventional Changelog Action 29 | id: changelog 30 | uses: TriPSs/conventional-changelog-action@v3 31 | with: 32 | version-file: CustomComplete.omnifocusjs/manifest.json 33 | release-count: 0 34 | 35 | - name: Release 36 | uses: softprops/action-gh-release@v1 37 | if: ${{ steps.changelog.outputs.skipped == 'false' }} 38 | with: 39 | tag_name: ${{ steps.changelog.outputs.tag }} 40 | body: ${{ steps.changelog.outputs.clean_changelog }} 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.2.0](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v3.1.2...v3.2.0) (2024-05-31) 2 | 3 | 4 | ### Features 5 | 6 | * :sparkles: add option to open project in new window if reviewing when stalled ([b48ff18](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/b48ff180f3cd64729579e2ecfe493dbd362ca31a)) 7 | 8 | 9 | 10 | ## [3.1.2](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v3.1.1...v3.1.2) (2024-01-04) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * :bug: remove tags and unschedule completed task where task is repeating ([bf7af41](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/bf7af41d12e78b2df73a248adbbd9791d01b8898)) 16 | 17 | 18 | 19 | ## [3.1.1](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v3.1.0...v3.1.1) (2023-06-30) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * :bug: remove absolute scheduled notifications for repeating tasks (closes [#10](https://github.com/ksalzke/custom-complete-omnifocus-plugin/issues/10)) ([e2fc97a](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/e2fc97ad36d991209f1a7f59fad0d3b060b06446)) 25 | 26 | 27 | 28 | # [3.1.0](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v3.0.1...v3.1.0) (2022-11-18) 29 | 30 | 31 | ### Features 32 | 33 | * :sparkles: add option to automatically select next node ([dede01d](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/dede01d6eaa10f5e11c4bd8a945b3e5245e89858)) 34 | 35 | 36 | 37 | ## [3.0.1](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v3.0.0...v3.0.1) (2022-03-24) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * :bug: don't show 'stalled' prompt when the last action in a SAL is completed ([3d97f54](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/3d97f54df55ae0cb565e262ecbe178e1145957ae)) 43 | 44 | 45 | 46 | # [3.0.0](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v2.1.0...v3.0.0) (2022-03-08) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * :bug: convert Alert to Form to get around OF bug re 'project stalled' ([b7b2556](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/b7b2556f659e3e7489cec20b1f25cc5a2fb376aa)) 52 | 53 | 54 | ### Features 55 | 56 | * :heavy_minus_sign: remove dependency on function library ([4b29530](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/4b29530bac79fb460cd2dd7148d1d3f63d8a345f)) 57 | * :sparkles: add 'preferences' pane and convert 'tags to remove' to synced pref ([f1f2d9c](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/f1f2d9c99f1544c2756490a2b74124d7ce5df76f)) 58 | * :sparkles: expand 'promptIfStalled' to include a prompt if an action group is stalled (not just project) ([a4382af](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/a4382afdef4d95bdb744e7ced9b8b155dc6bb49c)) 59 | * :sparkles: integrate with 'Tag Tasks Due Today' plug-in ([f88a317](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/f88a317d1c7244ecceaaa79f4cb3e863c489b031)) 60 | * :sparkles: integrate with 'Work On...' plug-in ([7beaca5](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/7beaca5011dd3f72e42440ecb66afac2fd075c74)) 61 | 62 | 63 | ### BREAKING CHANGES 64 | 65 | * 'tags to remove' are now set in preferences pane rather than in config file; Synced Preferences plug-in is required; completion function changed to 'onComplete' 66 | 67 | 68 | 69 | # [2.1.0](https://github.com/ksalzke/custom-complete-omnifocus-plugin/compare/v2.0.1...v2.1.0) (2022-02-28) 70 | 71 | 72 | ### Features 73 | 74 | * :lipstick: add SF symbol ([0bccd84](https://github.com/ksalzke/custom-complete-omnifocus-plugin/commit/0bccd8408a243aadcd1c4559c702627c68b2ccd4)) 75 | 76 | 77 | 78 | ## 2.0.1 (2022-01-29) 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is an Omni Automation plug-in bundle for OmniFocus that marks a task as complete and performs a series of customisable actions. 4 | 5 | _Please note that all scripts on my GitHub account (or shared elsewhere) are works in progress. If you encounter any issues or have any suggestions please let me know--and do please make sure you backup your database before running scripts from the internet!)_ 6 | 7 | ## Known issues 8 | 9 | Refer to ['issues'](https://github.com/ksalzke/custom-complete-omnifocus-plugin/issues) for known issues and planned changes/enhancements. 10 | 11 | # Installation & Set-Up 12 | 13 | ## Synced Preferences Plug-In 14 | 15 | **Important note: for this plug-in bundle to work correctly, my [Synced Preferences for OmniFocus plug-in](https://github.com/ksalzke/synced-preferences-for-omnifocus) is also required and needs to be added to the plug-in folder separately.** 16 | 17 | ## Installation 18 | 19 | 1. Download the [latest release](https://github.com/ksalzke/custom-complete-omnifocus-plugin/releases/latest). 20 | 2. Unzip the downloaded file. 21 | 3. Move the `.omnifocusjs` file to your OmniFocus plug-in library folder (or open it to install). 22 | 4. If desired, add one or more tags to be removed from a task after completion using the 'Preferences' action. 23 | 24 | # Actions 25 | 26 | This plug-in contains the following actions: 27 | 28 | ## Custom Complete 29 | 30 | This action runs the `onComplete` function on one selected task or project, using the other functions in the `customCompleteLib` (detailed below). 31 | 32 | ## Preferences 33 | 34 | This action allows you to: 35 | * select whether the next task in a perspective should automatically be selected when 'custom complete' is run (this will only apply when there is a single task selected) 36 | * select whether the project will be shown in a new window, when the last task in a project or action group is completed and the user chooses to 'review' the project 37 | * configure one or more tags that should be removed from tasks after they have been completed. 38 | 39 | # Functions 40 | 41 | This plug-in contains the following functions within the `customCompleteLib` library: 42 | 43 | ## `loadSyncedPrefs () : SyncedPref` 44 | 45 | Returns the [SyncedPref](https://github.com/ksalzke/synced-preferences-for-omnifocus) object for this plug-in. 46 | 47 | If the user does not have the plug-in installed correctly, they are alerted. 48 | 49 | ## `tagsToRemove () : Array` 50 | 51 | Returns an array of tags to be removed from tasks when they are completed, as configured in the preferences. 52 | 53 | ## `selectNextNodePref () : Boolean` 54 | 55 | Returns 'true' or 'false' depending on whether the 'select next task' option is checked in the preferences. 56 | 57 | ## `openInNewWindowPref () : Boolean` 58 | 59 | Returns 'true' or 'false' depending on whether the 'open in new window' option is checked in the preferences. 60 | 61 | ## `unschedule (task: Task)` 62 | 63 | If the task is a repeating task, removes any 'absolute' notifications (i.e. those that are not set relative to the defer or due date) 64 | 65 | ## `onComplete (task: Task)` 66 | 67 | **Asynchronous.** Marks the given task as complete, and runs each of the functions below (with the task as the only parameter). 68 | 69 | ## `selectNextNode (task: Task)` 70 | 71 | **Asynchronous.** Selects the next 'node' (task) in the tree, if the preference is set, and only one task is selected. 72 | 73 | ## `checkDependendants (task: Task)` 74 | 75 | **Asynchronous.** If my [Dependency OmniFocus Plug-In](https://github.com/ksalzke/dependency-omnifocus-plugin) is installed, this runs the `checkDependantsForTaskAndAncestors` function on the task to check whether any dependent tasks should become available. 76 | 77 | ## `noteFollowUp (task: Task)` 78 | 79 | If my [Delegation OmniFocus Plug-In](https://github.com/ksalzke/delegation-omnifocus-plugin) is installed, runs the `noteFollowUp` action. If the task being completed is a 'follow up' task, a note is added to the original task indicating that the task has been followed up at the current time and date. 80 | 81 | ## `removeUnwantedTags (task: Task)` 82 | 83 | Removes any unwanted tags (specified in `customCompleteConfig.js`) from the task. (This is predominantly intended for repeating tasks where certain tags are intended to be applied to the current instance only.) 84 | 85 | ## `removeDueSoonTag (task: Task)` 86 | 87 | If my ['Tag Tasks Due Today' Plug-In](https://github.com/ksalzke/tag-tasks-due-today-for-omnifocus) is installed, removes the 'Due Today' tag from the task. 88 | 89 | ## `checkWorkOnTask (task: Task)` 90 | 91 | **Asynchronous.** If my ['Work On...' Plug-In](https://github.com/ksalzke/work-on-omnifocus-plug-in) is installed, runs the 'onComplete' action for 'work on...' tasks, prompting the user for whether the base task is completed, should be deferred, etc. 92 | 93 | ## `promptIfStalled (task: Task)` 94 | 95 | **Asynchronous.** If the user is not in the Projects perspective, and an action group or project is stalled as a result of completing the task, prompts the user to confirm whether they would like to review the stalled group/project, or mark it complete. -------------------------------------------------------------------------------- /CustomComplete.omnifocusjs/Resources/customCompleteLib.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Version Perspective Project Alert */ 2 | (() => { 3 | const customCompleteLib = new PlugIn.Library(new Version('1.0')) 4 | 5 | customCompleteLib.loadSyncedPrefs = () => { 6 | const syncedPrefsPlugin = PlugIn.find('com.KaitlinSalzke.SyncedPrefLibrary') 7 | 8 | if (syncedPrefsPlugin !== null) { 9 | const SyncedPref = syncedPrefsPlugin.library('syncedPrefLibrary').SyncedPref 10 | return new SyncedPref('com.KaitlinSalzke.customComplete') 11 | } else { 12 | const alert = new Alert( 13 | 'Synced Preferences Library Required', 14 | 'For the Custom Complete plug-in to work correctly, the \'Synced Preferences for OmniFocus\' plug-in (https://github.com/ksalzke/synced-preferences-for-omnifocus) is also required and needs to be added to the plug-in folder separately. Either you do not currently have this plugin installed, or it is not installed correctly.' 15 | ) 16 | alert.show() 17 | } 18 | } 19 | 20 | customCompleteLib.tagsToRemove = () => { 21 | const preferences = customCompleteLib.loadSyncedPrefs() 22 | const tagsToRemoveIDs = preferences.read('tagsToRemoveIDs') || [] 23 | 24 | return tagsToRemoveIDs.map(id => Tag.byIdentifier(id)).filter(tag => tag !== null) 25 | } 26 | 27 | customCompleteLib.selectNextNodePref = () => { 28 | const preferences = customCompleteLib.loadSyncedPrefs() 29 | const selectNextNodePref = preferences.read('selectNextNode') || false 30 | console.log(selectNextNodePref ? 'true' : 'false') 31 | return selectNextNodePref 32 | } 33 | 34 | customCompleteLib.openInNewWindowPref = () => { 35 | const preferences = customCompleteLib.loadSyncedPrefs() 36 | const openInNewWindowPref = preferences.read('openInNewWindowPref') || false 37 | return openInNewWindowPref 38 | } 39 | 40 | customCompleteLib.onComplete = async (task) => { 41 | const lib = customCompleteLib 42 | await lib.selectNextNode(task) 43 | const completedTask = task.markComplete() 44 | await lib.unschedule(task) 45 | await lib.unschedule(completedTask) 46 | await lib.checkDependants(task) 47 | lib.noteFollowUp(task) 48 | lib.removeUnwantedTags(task) 49 | lib.removeUnwantedTags(completedTask) 50 | lib.removeDueSoonTag(task) 51 | lib.removeDueSoonTag(completedTask) 52 | await lib.checkWorkOnTask(task) 53 | await lib.promptIfStalled(task) 54 | } 55 | 56 | customCompleteLib.selectNextNode = async (task) => { 57 | // don't continue if preference is no 58 | const selectNextNodePref = customCompleteLib.selectNextNodePref() 59 | if (selectNextNodePref === false) return 60 | 61 | const contentTree = document.windows[0].content 62 | 63 | // only continue if single node selected 64 | console.log(contentTree.selectedNodes.length) 65 | if (contentTree.selectedNodes.length !== 1) return 66 | 67 | // get currently selected note 68 | const selectedNode = contentTree.nodeForObject(task) 69 | 70 | // don't continue if node is last task 71 | if (selectedNode.index + 1 === selectedNode.parent.childCount) return 72 | 73 | // select next node 74 | contentTree.select([selectedNode.parent.childAtIndex(selectedNode.index + 1)], false) 75 | } 76 | 77 | customCompleteLib.unschedule = async (task) => { 78 | // don't continue if not repeating task 79 | if (task.repetitionRule === null) return 80 | const absoluteNotifications = task.notifications.filter(notification => notification.kind === Task.Notification.Kind.Absolute) 81 | for (notification of absoluteNotifications) task.removeNotification(notification) 82 | } 83 | 84 | customCompleteLib.checkDependants = async task => { 85 | // update dependencies (if 'dependency' plugin installed) 86 | const dependencyPlugin = PlugIn.find('com.KaitlinSalzke.DependencyForOmniFocus') 87 | if (dependencyPlugin !== null) { 88 | await dependencyPlugin.library('dependencyLibrary').updateDependencies() 89 | } 90 | } 91 | 92 | customCompleteLib.noteFollowUp = task => { 93 | // note details of follow-up if this is a follow up task (if 'delegation' plugin installed) 94 | const delegationPlugin = PlugIn.find('com.KaitlinSalzke.Delegation') 95 | if (delegationPlugin !== null) { 96 | delegationPlugin.library('delegationLib').noteFollowUp(task) 97 | } 98 | } 99 | 100 | customCompleteLib.removeUnwantedTags = task => { 101 | const tagsToRemove = customCompleteLib.tagsToRemove() 102 | task.removeTags(tagsToRemove) 103 | } 104 | 105 | customCompleteLib.removeDueSoonTag = task => { 106 | const plugin = PlugIn.find('com.KaitlinSalzke.TagTasksDueToday') 107 | if (plugin !== null) plugin.library('tagDueTasksLib').onComplete(task) 108 | } 109 | 110 | customCompleteLib.checkWorkOnTask = async task => { 111 | const plugin = PlugIn.find('com.KaitlinSalzke.WorkOn') 112 | if (plugin !== null) await plugin.library('workOnLib').onComplete(task) 113 | } 114 | 115 | customCompleteLib.promptIfStalled = async task => { 116 | 117 | // don't show prompt if already in Projects perspective 118 | if (document.windows[0].perspective === Perspective.BuiltIn.Projects) return 119 | 120 | // don't show prompt if task has no parent 121 | if (task.parent === null) return 122 | 123 | // don't show prompt if parent is SAL 124 | if (task.parent.project !== null && task.parent.project.containsSingletonActions) return 125 | 126 | // don't show prompt if there are remaining tasks 127 | const remainingChildren = task.parent.children.filter(child => child.taskStatus !== Task.Status.Completed && child.taskStatus !== Task.Status.Dropped) 128 | if (remainingChildren.length > 0) return 129 | 130 | // if parent already completed, check its parent 131 | if (task.parent.taskStatus === Task.Status.Completed) { 132 | await customCompleteLib.promptIfStalled(task.parent) 133 | return // don't proceed - no prompt required for this task (it's already completed) 134 | } 135 | 136 | // if parent is stalled, show prompt 137 | const form = new Form() 138 | 139 | form.addField(new Form.Field.Option('action', 'Do you want to review it now?', ['Yes', 'Mark complete', 'No'], null, 'Yes')) 140 | 141 | await form.show(`Stalled: ${task.parent.name}\nThere are no further actions.`, 'OK') 142 | 143 | switch (form.values.action) { 144 | case 'Yes': 145 | if (customCompleteLib.openInNewWindowPref()) await document.newWindow() 146 | const urlStr = 'omnifocus:///task/' + task.parent.id.primaryKey 147 | URL.fromString(urlStr).open() 148 | break 149 | case 'Mark complete': 150 | task.parent.markComplete() 151 | await customCompleteLib.promptIfStalled(task.parent) // after marking complete, check if parent's parent is stalled 152 | break 153 | default: 154 | break 155 | } 156 | } 157 | 158 | return customCompleteLib 159 | })() 160 | --------------------------------------------------------------------------------