├── Templates.omnifocusjs ├── Resources │ ├── en.lproj │ │ ├── manifest.strings │ │ ├── createFromTemplate.strings │ │ ├── preferences.strings │ │ ├── goToTemplatesFolder.strings │ │ └── hideTemplatesFolder.strings │ ├── hideTemplatesFolder.js │ ├── goToTemplatesFolder.js │ ├── createFromTemplate.js │ ├── preferences.js │ └── templateLibrary.js └── manifest.json ├── .github └── workflows │ └── main.yml ├── CHANGELOG.md └── README.md /Templates.omnifocusjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.KaitlinSalzke.Templates" = "Templates"; -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/en.lproj/createFromTemplate.strings: -------------------------------------------------------------------------------- 1 | "label" = "Create From Template"; 2 | "shortLabel" = "Create From Template"; 3 | "mediumLabel" = "Create From Template"; 4 | "longLabel" = "Create From Template"; -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/en.lproj/preferences.strings: -------------------------------------------------------------------------------- 1 | "label" = "Preferences: Templates"; 2 | "shortLabel" = "Preferences: Templates"; 3 | "mediumLabel" = "Preferences: Templates"; 4 | "longLabel" = "Preferences: Templates"; -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/en.lproj/goToTemplatesFolder.strings: -------------------------------------------------------------------------------- 1 | "label" = "Go To Templates Folder"; 2 | "shortLabel" = "Go To Templates Folder"; 3 | "mediumLabel" = "Go To Templates Folder"; 4 | "longLabel" = "Go To Templates Folder"; -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/en.lproj/hideTemplatesFolder.strings: -------------------------------------------------------------------------------- 1 | "label" = "Hide Templates Folder"; 2 | "shortLabel" = "Hide Templates Folder"; 3 | "mediumLabel" = "Hide Templates Folder"; 4 | "longLabel" = "Hide Templates Folder"; -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/hideTemplatesFolder.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Folder */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const templateLibrary = this.templateLibrary 5 | 6 | const templateFolder = await templateLibrary.getTemplateFolder() 7 | 8 | templateFolder.active = false 9 | }) 10 | 11 | action.validate = function (selection, sender) { 12 | // show when Templates folder is visible 13 | const syncedPrefs = this.templateLibrary.loadSyncedPrefs() 14 | const templateFolderID = syncedPrefs.read('templateFolderID') 15 | return templateFolderID ? Folder.byIdentifier(templateFolderID).active : false 16 | } 17 | 18 | return action 19 | })() 20 | -------------------------------------------------------------------------------- /Templates.omnifocusjs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultLocale": "en", 3 | "identifier": "com.KaitlinSalzke.Templates", 4 | "author": "Kaitlin Salzke", 5 | "description": "An OmniFocus plug-in that enables projects to be created from templates", 6 | "version": "1.18.0", 7 | "actions": [ 8 | { 9 | "identifier": "createFromTemplate", 10 | "image": "doc.badge.plus" 11 | }, 12 | { 13 | "identifier": "goToTemplatesFolder", 14 | "image": "folder.fill" 15 | }, 16 | { 17 | "identifier": "hideTemplatesFolder", 18 | "image": "folder" 19 | }, 20 | { 21 | "identifier": "preferences", 22 | "image": "doc.badge.gearshape" 23 | } 24 | ], 25 | "libraries": [ 26 | { 27 | "identifier": "templateLibrary" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/goToTemplatesFolder.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Device */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const templateLibrary = this.templateLibrary 5 | 6 | const templateFolder = await templateLibrary.getTemplateFolder() 7 | 8 | if (Device.current.mac) await document.newTabOnWindow(document.windows[0]) 9 | 10 | templateFolder.active = true 11 | const urlStr = 'omnifocus:///folder/' + templateFolder.id.primaryKey 12 | URL.fromString(urlStr).call(() => {}) 13 | 14 | // if user is in focus mode, add the templates folder to the focus 15 | // Mac only as focus not yet supported on iOS API 16 | if (Device.current.mac) { 17 | const focus = document.windows[0].focus 18 | if (focus !== null) document.windows[0].focus = [templateFolder] 19 | } 20 | }) 21 | 22 | action.validate = function (selection, sender) { 23 | // always show 24 | return true 25 | } 26 | 27 | return action 28 | })() 29 | -------------------------------------------------------------------------------- /.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: Templates.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 | -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/createFromTemplate.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Form Preferences */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const templateLibrary = this.templateLibrary 5 | const preferences = new Preferences('com.KaitlinSalzke.Templates') 6 | 7 | const templateFolder = await templateLibrary.getTemplateFolder() 8 | let template = (selection.projects.length === 1 && templateFolder.flattenedProjects.includes(selection.projects[0])) ? selection.projects[0] : null 9 | 10 | const templateForm = await generateTemplateForm() 11 | if (template === null) { 12 | await templateForm.show('Choose Template', 'Create') 13 | template = templateForm.values.template 14 | } 15 | 16 | const destination = await templateLibrary.getDestination(template) 17 | const created = await templateLibrary.createFromTemplate(template, destination) 18 | if (templateForm.values.goTo) URL.fromString('omnifocus:///task/' + created.id.primaryKey).call(() => {}) 19 | 20 | async function generateTemplateForm () { 21 | // select template to use and destination - show form 22 | const templateFolder = await templateLibrary.getTemplateFolder() 23 | const templateProjects = templateFolder.flattenedProjects.filter(project => { 24 | let isOnHold = preferences.readBoolean('includeOnHoldProjects') && project.status === Project.Status.OnHold 25 | let isActive = project.status === Project.Status.Active 26 | 27 | return isActive || isOnHold 28 | }) 29 | 30 | const templateForm = new Form() 31 | templateForm.addField( 32 | new Form.Field.Option( 33 | 'template', 34 | 'Template', 35 | templateProjects, 36 | templateProjects.map((project) => project.name), 37 | null 38 | ) 39 | ) 40 | templateForm.addField( 41 | new Form.Field.Checkbox( 42 | 'goTo', 43 | 'Go to created project', 44 | preferences.readBoolean('alwaysGoTo') 45 | ) 46 | ) 47 | return templateForm 48 | } 49 | }) 50 | 51 | action.validate = function (selection, sender) { 52 | return true 53 | } 54 | 55 | return action 56 | })() 57 | -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/preferences.js: -------------------------------------------------------------------------------- 1 | /* global Preferences PlugIn Form flattenedFolders Folder */ 2 | (() => { 3 | // declare preferences instance 4 | const preferences = new Preferences() 5 | 6 | const action = new PlugIn.Action(async function (selection, sender) { 7 | const syncedPrefs = this.templateLibrary.loadSyncedPrefs() 8 | 9 | // get current preferences or set defaults if they don't yet exist 10 | const alwaysGoTo = (preferences.read('alwaysGoTo') !== null) ? preferences.readBoolean('alwaysGoTo') : false 11 | const includeOnHoldProjects = (preferences.read('includeOnHoldProjects') !== null) ? preferences.readBoolean('includeOnHoldProjects') : true 12 | const sortLocationsAlphabetically = (preferences.read('sortLocationsAlphabetically') !== null) ? preferences.readBoolean('sortLocationsAlphabetically') : false 13 | const templateFolderID = syncedPrefs.read('templateFolderID') 14 | const templateFolder = templateFolderID ? Folder.byIdentifier(templateFolderID) : null 15 | 16 | // create and show form 17 | const prefForm = new Form() 18 | prefForm.addField(new Form.Field.Checkbox('alwaysGoTo', 'Auto-select \'Go to created project\' when creating from template', alwaysGoTo)) 19 | prefForm.addField(new Form.Field.Checkbox('includeOnHoldProjects', 'Include On-Hold template projects', includeOnHoldProjects)) 20 | prefForm.addField(new Form.Field.Checkbox('sortLocationsAlphabetically', 'Sort folder/project list alphabetically (instead of in OmniFocus order)', sortLocationsAlphabetically)) 21 | prefForm.addField(new Form.Field.Option('templateFolder', 'Template Folder', flattenedFolders, flattenedFolders.map(folder => folder.name), templateFolder, 'Please select')) 22 | await prefForm.show('Preferences: Templates', 'OK') 23 | 24 | // save preferences 25 | preferences.write('alwaysGoTo', prefForm.values.alwaysGoTo) 26 | preferences.write('includeOnHoldProjects', prefForm.values.includeOnHoldProjects) 27 | preferences.write('sortLocationsAlphabetically', prefForm.values.sortLocationsAlphabetically) 28 | syncedPrefs.write('templateFolderID', prefForm.values.templateFolder.id.primaryKey) 29 | }) 30 | 31 | action.validate = function (selection, sender) { 32 | return true 33 | } 34 | 35 | return action 36 | })() 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.18.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.17.0...v1.18.0) (2022-10-02) 2 | 3 | 4 | ### Features 5 | 6 | * :sparkles: add preference to include/exclude on-hold templates ([b532236](https://github.com/ksalzke/templates-for-omnifocus/commit/b5322364faf108fc8c61bd6e11ca096b1e099612)), closes [#31](https://github.com/ksalzke/templates-for-omnifocus/issues/31) 7 | 8 | 9 | 10 | # [1.17.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.16.2...v1.17.0) (2022-06-11) 11 | 12 | 13 | ### Features 14 | 15 | * :sparkles: enable createFromTemplate destination to be of type Task.ChildInsertionLocation ([bb6a134](https://github.com/ksalzke/templates-for-omnifocus/commit/bb6a13421885d4fbded18d83b6c740458ccae5f0)) 16 | 17 | 18 | 19 | ## [1.16.2](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.16.1...v1.16.2) (2022-03-06) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * :bug: don't include templates folder and subfolders in destination list ([da2d58b](https://github.com/ksalzke/templates-for-omnifocus/commit/da2d58b2e4faff006b2a2f27759051e45468e57c)) 25 | 26 | 27 | 28 | ## [1.16.1](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.16.0...v1.16.1) (2022-02-23) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * :bug: make projects active after creation even if destination is not a Project instance ([a66fc3b](https://github.com/ksalzke/templates-for-omnifocus/commit/a66fc3b82f0dc0464bf66095b203eec94e273e7f)) 34 | 35 | 36 | 37 | # [1.16.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.15.0...v1.16.0) (2022-02-19) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * :bug: don't show active projects inside dropped folders in destination list ([8c406a6](https://github.com/ksalzke/templates-for-omnifocus/commit/8c406a688384c42248e0299994f7f6342c4ca35b)), closes [#27](https://github.com/ksalzke/templates-for-omnifocus/issues/27) 43 | 44 | 45 | ### Features 46 | 47 | * :sparkles: don't show 'on hold' templates in drop down for 'Create From Template' action enhancement ([dbc1c38](https://github.com/ksalzke/templates-for-omnifocus/commit/dbc1c389dbc202b6c184dd0e9271e2ecd00621f2)) 48 | 49 | 50 | 51 | # [1.15.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.14.0...v1.15.0) (2022-02-19) 52 | 53 | 54 | ### Features 55 | 56 | * :lipstick: rename 'Preferences' to 'Preferences: Templates' ([965b7e3](https://github.com/ksalzke/templates-for-omnifocus/commit/965b7e3879c0c21f84a0e2356100763dd886fbe4)) 57 | * :lipstick: update validation and remove 'alwaysEnable' pref - now always available ([35c94fb](https://github.com/ksalzke/templates-for-omnifocus/commit/35c94fb1c3fa22d8e8b957bbe0bd23cc023456a5)) 58 | 59 | 60 | 61 | # [1.14.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.13.0...v1.14.0) (2021-11-14) 62 | 63 | 64 | ### Features 65 | 66 | * 'always enable' preference no longer shown on Mac ([686f55b](https://github.com/ksalzke/templates-for-omnifocus/commit/686f55bd14dd8adc57ee2e856aa04a3a6407b2b8)) 67 | * 'always enable' preference no longer shown on Mac ([5be58cc](https://github.com/ksalzke/templates-for-omnifocus/commit/5be58cc7d1f2a979b49051e528c31e91d3dff6e9)) 68 | * make 'create from template' action always available on macOS ([7142ae8](https://github.com/ksalzke/templates-for-omnifocus/commit/7142ae8e1f1d615549e276195255566b2710f7f3)) 69 | * make 'preferences' action always available on macOS ([66ffea9](https://github.com/ksalzke/templates-for-omnifocus/commit/66ffea9e6206a7b6ca6ca4178dc562d4941ae7c3)) 70 | * restructure repo to improve ease of installation ([b417f97](https://github.com/ksalzke/templates-for-omnifocus/commit/b417f971b553d09d99cdb430d96541f2ea2b5ff6)) 71 | * use new tab and focus on Mac ([e875f77](https://github.com/ksalzke/templates-for-omnifocus/commit/e875f77e128d5dfa1a25ea5dc34be82da4d96719)) 72 | 73 | 74 | 75 | # [1.13.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.12.3...v1.13.0) (2021-10-22) 76 | 77 | 78 | ### Features 79 | 80 | * remove due/defer date from note once used ([4c0a254](https://github.com/ksalzke/templates-for-omnifocus/commit/4c0a2546e4f16969873f394cc7e578c09e88eee0)) 81 | 82 | 83 | 84 | ## [1.12.3](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.12.2...v1.12.3) (2021-10-18) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * add await to go to/hide actions to fix bug where promise was returned ([6765d9e](https://github.com/ksalzke/templates-for-omnifocus/commit/6765d9e6965ed54cb16344dc01fa3e8607dd6e0a)) 90 | 91 | 92 | 93 | ## [1.12.2](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.12.1...v1.12.2) (2021-10-17) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * fix logic re whether prefs are set ([a92571b](https://github.com/ksalzke/templates-for-omnifocus/commit/a92571b2d9b1e3138adb2dd96d09bdf70aeba161)) 99 | 100 | 101 | 102 | ## [1.12.1](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.12.0...v1.12.1) (2021-10-17) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * use find instead of foldersMatching ([64c0461](https://github.com/ksalzke/templates-for-omnifocus/commit/64c04615c9832d53650904de32b4c576710cb85d)) 108 | 109 | 110 | 111 | # [1.12.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.11.0...v1.12.0) (2021-10-17) 112 | 113 | 114 | ### Features 115 | 116 | * add SF symbols for actions ([260c945](https://github.com/ksalzke/templates-for-omnifocus/commit/260c9459166082b90ceb4e02226a30c51c64a115)) 117 | 118 | 119 | 120 | # [1.11.0](https://github.com/ksalzke/templates-for-omnifocus/compare/v1.10.0...v1.11.0) (2021-10-16) 121 | 122 | 123 | ### Features 124 | 125 | * add synced pref to select template folder ([07b7322](https://github.com/ksalzke/templates-for-omnifocus/commit/07b7322634ddc6351af4172ea7e61e25cad86442)) 126 | 127 | 128 | 129 | # [1.10.0](https://github.com/ksalzke/templates-for-omnifocus/compare/0ad62a7bd0f1d94ed711cd485bd0ef98f5e427dc...v1.10.0) (2021-10-03) 130 | 131 | 132 | ### Features 133 | 134 | * create from selected template project ([0ad62a7](https://github.com/ksalzke/templates-for-omnifocus/commit/0ad62a7bd0f1d94ed711cd485bd0ef98f5e427dc)) 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is an Omni Automation plug-in bundle for OmniFocus that generates projects from templates. 4 | 5 | _Credit:_ this script draws on the ideas implemented in Curt Clifton's [Populate Template Placeholders](http://curtclifton.net/poptemp) AppleScript. 6 | Thanks also to Tim Stringer (@timstringer) for numerous bug reports and suggestions for improvement. 7 | 8 | _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 an amateur on the internet!)_ 9 | 10 | ## Known issues 11 | 12 | Refer to ['issues'](https://github.com/ksalzke/templates-for-omnifocus/issues) for known issues and planned changes/enhancements. 13 | 14 | # Installation & Set-Up 15 | 16 | 1. Download the [latest release](https://github.com/ksalzke/templates-for-omnifocus/releases/latest). 17 | 2. Unzip the downloaded file. 18 | 3. Move the `.omnifocusjs` file to your OmniFocus plug-in library folder (or open it to install). 19 | 4. Configure your preferences using the `Preferences` action. (Note that to run this action, no tasks can be selected.) 20 | 21 | **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.** 22 | 23 | ## Template Folder 24 | 25 | The plug-in prompts the user to select an existing folder that contains (or will contain) the projects to use as a basis for the projects that are created. As with Curt's original script, the folder may be set to 'dropped' and the plug-in should continue to work as expected. A 'Hide Templates Folder' action is included which will drop the folder for you, and a 'Go To Templates Folder' action is included to help you un-drop the folder and navigate to it quickly. 26 | 27 | Templates should be included as projects within that folder (but can be inside subfolders). 28 | 29 | ## Templates 30 | 31 | ![Example Template](https://user-images.githubusercontent.com/16893787/142519353-22d002dc-6152-46f3-8d9a-54cd7e2a055b.png) 32 | 33 | As with Curt's original script: 34 | 35 | - Placeholders should be written inside double-angle quotation marks (« and », typed using `Option(⌥)` + `\` and `Option (⌥)` + `Shift` + `\` respectively). They should also be included in the project's notes. **Unlike Curt's script, these should be specified on a separate line for each variable.** 36 | 37 | - Defer and due dates can be set for the project; if the project itself has either of these, then you will be prompted for a new date when the script is run, and all dates will be adjusted in line with this. 38 | 39 | In addition: 40 | 41 | - The folder for the project to be created in can be specified by including the following as its own line within the project note, where 'Folder' is the name of the folder: `$FOLDER=Folder`. 42 | 43 | - A due date for a task can be specified using the format `$DUE=Some Date`. The OmniFocus date parser is used so these can be entered in any form supported by OmniFocus. In addition, placeholders already identified using the placeholder format can be used inside this date e.g. if `«Due Date»` is specified as a placeholder then `$DUE={{Due Date}}` can be used. 44 | 45 | - A defer date for a task can be specified using the format `$DEFER=Some Date`. The OmniFocus date parser is used so these can be entered in any form supported by OmniFocus. In addition, placeholders already identified using the placeholder format can be used inside this date e.g. if `«Due Date»` is specified as a placeholder then `$DEFER={{Due Date}} - 3d` can be used. 46 | 47 | - Placeholders can also be used within tags. (New tags will be created as needed at the root level if the tag does not exist.) 48 | 49 | - You can specify a value to be used for a placeholder by using the format `«Placeholder»:Value`. The user will not be prompted to fill in these fields. 50 | 51 | - Alternatively, you can specify a default value for a placeholder by using the format `«Placeholder:Default»`. The user will be prompted for a value for these fields, but the default value will be autofilled in the form. 52 | - If the destination is a project, placeholders in the pre-existing project's note will be used. As these are updated when the original project is created (assuming it is created from a template) this allows for the same values to be re-used without having to be entered a second time. 53 | 54 | # Actions 55 | 56 | This plug-in contains the following actions: 57 | 58 | ## Create From Template 59 | 60 | This action can be run at any time on macOS. It can optionally only be run when no projects or tasks are selected on iOS (to avoid cluttering the share sheet), by adjusting the Preferences. The below screenshots show an example of running this action: 61 | 62 | ### 1. Select a template _(if applicable)_ 63 | 64 | The user is prompted to select a template (from the projects included in the templates folder). If a template project is already selected, the prompt is not shown and the selected template is used. 65 | ![Prompt to choose template](https://user-images.githubusercontent.com/16893787/142519500-f0c0e1f3-8c89-4825-9dde-413660b19e6b.png) 66 | 67 | Optionally, on-hold templates can be excluded from this list; this setting is located in Preferences. 68 | 69 | ### 2. Select a destination _(if applicable)_ 70 | 71 | The `getDestination` function is used to determine where the new project should be created. (This skips the below dialogue if a folder is specified in the note, as in our example template above.) 72 | ![Prompt to choose destination](https://user-images.githubusercontent.com/16893787/142519696-e67298fb-7800-40ba-bd1f-692d9c49caf6.png) 73 | 74 | ### 3. Include or exclude optional tasks _(if applicable)_ 75 | ![Optional tasks prompt](https://user-images.githubusercontent.com/16893787/142519832-5718ffa5-40d3-4b01-93a9-16ccb160681b.png) 76 | 77 | ### 4. Enter values for placeholders: _(if applicable)_ 78 | ![Placeholder form](https://user-images.githubusercontent.com/16893787/142520003-564ebbec-d598-4a6c-b8b4-be601345ca37.png) 79 | 80 | (Note that the third placeholder is not included in this dialogue because its value was specified in the note using the format `«third placeholder»:Some Fixed Value`, and the second placeholder has been pre-populated with the default value, specified using the format `«second placeholder:Default Value»`. 81 | 82 | ![Completed placeholder form](https://user-images.githubusercontent.com/16893787/142520009-de02cb77-ee9c-4bd1-9d5e-1028ea8f2f74.png) 83 | 84 | ### 5. The new project is created (using the `createFromTemplate` function) 85 | ![Created project](https://user-images.githubusercontent.com/16893787/142520077-67bc62c1-999e-4849-931a-594683ca0317.png) 86 | 87 | If the `Go to created project` checkbox is selected, takes the user to the created project. 88 | 89 | ## Go To Templates Folder 90 | 91 | This action navigates to the Templates folder and, if it is dropped, makes it active so that any templates contained within it are visible. (Note that, on iOS, if a focus is set that renders the templates folder hidden, it will not be unhidden unless you first leave the focused mode.) 92 | 93 | On Mac, a new tab is opened and the focus for that tab is set to only the templates folder. 94 | 95 | ## Hide Templates Folder 96 | 97 | This action sets the status of the Templates folder to dropped so that it is not visible in most views/perspectives. 98 | 99 | ## Preferences 100 | 101 | This action allows the user to set the preferences for the plug-in. Currently, the available preferences are: 102 | 103 | * **Template Folder** This is the folder where template projects are saved. 104 | 105 | * **Always enable action in menu (iOS only):** If selected, the 'Create From Template' action is always available. Otherwise, it is only available when nothing is selected. _Please note that this setting is device-specific and does not sync between devices._ 106 | 107 | * **'Auto-select 'Go to created project' when creating from template** _Please note that this setting is device-specific and does not sync between devices._ 108 | 109 | * **Sort folder/project list alphabetically (instead of in OmniFocus order)** _Please note that this setting is device-specific and does not sync between devices._ 110 | 111 | * **Include On-Hold template projects** Determines whether on-hold template projects are included in the dropdown list when the 'Create From Template' action is run. (Defaults to included.) 112 | 113 | # Functions 114 | 115 | This plug-in contains the following functions within the `templateLibrary` library: 116 | 117 | ## `loadSyncedPrefs () : SyncedPref` 118 | 119 | This function returns the synced preferences instance for the plug-in. 120 | 121 | ## `getTemplateFolder () : Folder` 122 | 123 | This asynchronous function returns the folder that is currently set as the folder where templates are stored. If no preference has been set, the user is prompted to select a folder. 124 | 125 | ## `getDestination (template: Task) : Folder | Project | Folder.ChildInsertionLocation` 126 | 127 | This asynchronous function takes a template as input and returns the parent folder or project to be used for the new project. It first looks for a line in the format `$FOLDER=Folder` in the template project's note, and if this is not found prompts the user to select from a list of active folders. The user may also select the "Include projects" checkbox to include active projects in the listing, or select 'Top Level' to add the project at the root level, outside of any folders. 128 | 129 | ## `createFromTemplate (template: Task, destination: Folder | Project | Task | Folder.ChildInsertionLocation | Task.ChildInsertionLocation) : Project | Task` 130 | 131 | This asynchronous function takes a template and a destination (a folder or project) as input. It: 132 | 133 | 1. Copies the template to the specified destination and makes it active. (If the template is copied to a project it is created as an action group.) 134 | 2. For the newly created project, or for the project that the template is duplicated to: 135 | 1. For each placeholder where a value is specified (in the form `«Placeholder»:Value`): 136 | - Replaces all instances of that placeholder in the project/task names 137 | - If there are any tags that include a placeholder in their name, replaces the tag with a matching tag. (If there is no matching tag found, a new tag will be created at the top level.) 138 | 2. For any task that includes `$OPTIONAL` in its note, prompts the user to ask whether they want to include the task or not, and: 139 | - If they don't want the task included, deletes the task (and any of its subtasks) 140 | - If they do want the task included, removes the `$OPTIONAL` annotation from its note 141 | 3. For each placeholder (in the form `«Placeholder»`), prompts the user for a replacement value, and: 142 | - Replaces all instances of that placeholder in the project/task names 143 | - If there are any tags that include a placeholder in their name, replaces the tag with a matching tag. (If there is no matching tag found, a new tag will be created at the top level.) 144 | - Includes the replacement value in the note of the created project (in the form `«Placeholder»:Value`) 145 | - _Note:_ If a template is copied to an existing project, any placeholders in the project's note will be used first before prompting for further information. 146 | 3. If the template project has a due date, prompts the user for a new due date and adjusts the dates of all tasks within the created project/action group accordingly. If there is no due date but there is a defer date, this is used instead. If any actions have `$DUE` 147 | -------------------------------------------------------------------------------- /Templates.omnifocusjs/Resources/templateLibrary.js: -------------------------------------------------------------------------------- 1 | /* global Alert PlugIn Version Preferences Form flattenedSections Folder Project duplicateTasks duplicateSections Task Tag Calendar deleteObject library flattenedFolders flattenedTags Formatter */ 2 | (() => { 3 | const templateLibrary = new PlugIn.Library(new Version('1.0')) 4 | 5 | templateLibrary.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.Templates') 11 | } else { 12 | const alert = new Alert( 13 | 'Synced Preferences Library Required', 14 | 'For the Templates 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 plug-in installed, or it is not installed correctly.' 15 | ) 16 | alert.show() 17 | } 18 | } 19 | 20 | templateLibrary.getTemplateFolder = async () => { 21 | // get ID from preferences 22 | const syncedPrefs = templateLibrary.loadSyncedPrefs() 23 | const templateFolderID = syncedPrefs.read('templateFolderID') 24 | 25 | // if ID has been set 26 | if (templateFolderID) return Folder.byIdentifier(templateFolderID) 27 | 28 | // if not, prompt 29 | const folderForm = new Form() 30 | folderForm.addField(new Form.Field.Option('templateFolder', 'Template Folder', flattenedFolders, flattenedFolders.map(folder => folder.name), null, 'Please select')) 31 | await folderForm.show('Please select the folder where templates are stored.', 'OK') 32 | 33 | // save preference and return folder 34 | syncedPrefs.write('templateFolderID', folderForm.values.templateFolder.id.primaryKey) 35 | return folderForm.values.templateFolder 36 | } 37 | 38 | templateLibrary.getDestination = async (template) => { 39 | const preferences = new Preferences('com.KaitlinSalzke.Templates') 40 | const templateFolder = await templateLibrary.getTemplateFolder() 41 | 42 | // find folder from string, if there is a destination 43 | const match = template.note.match(/\$FOLDER=(.*?)$/m) 44 | const destFolder = (match === null) ? undefined : flattenedFolders.find(folder => folder.name === match[1]) 45 | if (destFolder !== undefined) { 46 | return destFolder 47 | } else { 48 | // otherwise, show form to user to select 49 | const destinationForm = new Form() 50 | // project checkbox 51 | let projectsBoxChecked = false 52 | destinationForm.addField(new Form.Field.Checkbox('projectsIncluded', 'Include projects', projectsBoxChecked)) 53 | // destination dropdown 54 | const activeSections = flattenedSections.filter(section => (section.effectiveActive === true || (section instanceof Project && section.task.effectiveActive === true)) && section !== templateFolder && !templateFolder.flattenedSections.includes(section)) 55 | const activeFolders = flattenedFolders.filter(folder => folder.effectiveActive === true && folder !== templateFolder && !templateFolder.flattenedFolders.includes(folder)) 56 | 57 | function updateDestinationDropdown (sections) { 58 | const sortedSections = preferences.readBoolean('sortLocationsAlphabetically') ? sections.sort((a, b) => a.name > b.name) : sections 59 | const sectionNames = preferences.readBoolean('sortLocationsAlphabetically') ? sortedSections.map(section => section.name) : sortedSections.map((section) => section instanceof Folder ? `📁 ${section.name}` : `—${section.name}`) 60 | destinationOptions = new Form.Field.Option( 61 | 'destination', 62 | 'Destination', 63 | [library.ending, ...sortedSections], 64 | ['Top Level', ...sectionNames], 65 | null 66 | ) 67 | destinationOptions.allowsNull = true 68 | destinationForm.addField(destinationOptions) 69 | } 70 | 71 | let destinationOptions 72 | await updateDestinationDropdown(projectsBoxChecked ? activeSections : activeFolders) 73 | 74 | destinationForm.validate = (formObject) => { 75 | if (formObject.values.projectsIncluded !== projectsBoxChecked) { 76 | projectsBoxChecked = formObject.values.projectsIncluded 77 | destinationForm.removeField(destinationOptions) 78 | updateDestinationDropdown(formObject.values.projectsIncluded ? activeSections : activeFolders) 79 | } 80 | return true 81 | } 82 | 83 | await destinationForm.show('Choose Destination', 'Continue') 84 | return destinationForm.values.destination 85 | } 86 | } 87 | 88 | // returns a set of placeholders from the note of a given task 89 | templateLibrary.getPlaceholdersFrom = (task, knownPlaceholders) => { 90 | const placeholders = knownPlaceholders 91 | const matches = task.note.matchAll(/«([^:»]*):*(.*?)»:*(.*?)$/gm) 92 | for (const placeholder of matches) { 93 | if (!knownPlaceholders.some(existing => existing.name === placeholder[1])) { 94 | placeholders.push({ 95 | name: placeholder[1], 96 | default: placeholder[2], 97 | value: (placeholder[3] === '') ? null : placeholder[3] 98 | }) 99 | } 100 | } 101 | return placeholders 102 | } 103 | 104 | templateLibrary.createFromTemplate = async (template, destination) => { 105 | // CREATE FROM TEMPLATE 106 | let created, project 107 | if (destination instanceof Folder || destination instanceof Folder.ChildInsertionLocation) { 108 | created = duplicateSections([template], destination)[0] 109 | project = created 110 | } else if (destination instanceof Project) { 111 | created = duplicateTasks([template.task], destination)[0] 112 | project = destination 113 | } else if (destination instanceof Task || destination instanceof Task.ChildInsertionLocation) { 114 | created = duplicateTasks([template.task], destination)[0] 115 | project = created.containingProject 116 | } 117 | 118 | if (created instanceof Project) created.status = Project.Status.Active // make status active if not already active 119 | 120 | // ASK ABOUT OPTIONAL TASKS 121 | const optTasks = created.flattenedTasks.filter(task => task.note.includes('$OPTIONAL')) 122 | if (optTasks.length > 0) askAboutOptionalTasks(optTasks) 123 | 124 | async function askAboutOptionalTasks (tasks) { 125 | const form = new Form() 126 | tasks.forEach(task => form.addField(new Form.Field.Checkbox(task.name, task.name, true))) 127 | await form.show('Do you want to include the following tasks?', 'Continue') 128 | tasks.forEach(task => { if (form.values[task.name] === false) { deleteObject(task) } else task.note = task.note.replace('$OPTIONAL', '') }) 129 | } 130 | 131 | // DEAL WITH PLACEHOLDERS 132 | 133 | async function replace (project, placeholder, replacement) { 134 | const regex = new RegExp(`«${placeholder}:*.*».*$`, 'gm') 135 | // if replacement isn't defined, use empty string 136 | replacement = replacement === undefined ? '' : replacement 137 | 138 | // update project note 139 | project.note = project.note.replace( 140 | regex, 141 | `«${placeholder}»:${replacement}` 142 | ) 143 | // tag information 144 | project.task.apply((tsk) => { 145 | // replace in task names 146 | tsk.name = tsk.name.replaceAll(`«${placeholder}»`, replacement) 147 | // replace in task notes 148 | tsk.note = tsk.note.replaceAll(`{{${placeholder}}}`, replacement) 149 | // replace tags 150 | tsk.tags.forEach(tag => { 151 | if (tag.name.includes(`«${placeholder}»`)) { 152 | // work out what the tag name would be with placeholders replaced 153 | const replacementTagName = tag.name.replaceAll(`«${placeholder}»`, replacement) 154 | // look for a matching tag 155 | const matchingTag = flattenedTags.find(tag => tag.name === replacementTagName) 156 | // create tag if it doesn't already exist; otherwise use matching tag 157 | const replacementTag = matchingTag === undefined ? new Tag(replacementTagName) : matchingTag 158 | // update tags 159 | tsk.removeTag(tag) 160 | tsk.addTag(replacementTag) 161 | } 162 | }) 163 | }) 164 | } 165 | 166 | // get a set of placeholders from created and project notes 167 | const projectPlaceholders = templateLibrary.getPlaceholdersFrom(project, []) 168 | const allPlaceholders = templateLibrary.getPlaceholdersFrom(created, projectPlaceholders) 169 | 170 | // relpace placeholders with known value 171 | const placeholdersWithValue = allPlaceholders.filter(placeholder => placeholder.value !== null) 172 | placeholdersWithValue.forEach(placeholder => replace(project, placeholder.name, placeholder.value)) 173 | 174 | // find placeholders with no value, prompt for values, and then replace 175 | const placeholdersWithoutValue = allPlaceholders.filter(placeholder => placeholder.value === null) 176 | const newPlaceholders = placeholdersWithoutValue.length > 0 ? await askForValues(placeholdersWithoutValue) : [] 177 | await newPlaceholders.forEach(placeholder => { 178 | replace(project, placeholder.name, placeholder.value) 179 | }) 180 | 181 | async function askForValues (placeholders) { 182 | const form = new Form() 183 | placeholders.forEach((placeholder) => { 184 | form.addField( 185 | new Form.Field.String(placeholder.name, placeholder.name, placeholder.default) 186 | ) 187 | }) 188 | try { 189 | await form.show('Enter value for placeholders:', 'Continue') 190 | const result = [] 191 | placeholders.forEach((placeholder) => { 192 | result.push({ 193 | name: placeholder.name, 194 | value: form.values[placeholder.name] 195 | }) 196 | }) 197 | return result 198 | } catch (error) { 199 | // if placeholder form cancelled, remove the item that was just created 200 | deleteObject(created) 201 | console.log(`Form cancelled: ${error}`) 202 | } 203 | } 204 | 205 | // ADJUST DATES 206 | function adjustDates (oldDate, newDate, task) { 207 | if (task instanceof Project) { task = task.task } 208 | const difference = Calendar.current.dateComponentsBetweenDates( 209 | oldDate, 210 | newDate 211 | ) 212 | 213 | task.apply((task) => { 214 | if (task.dueDate !== null) { 215 | task.dueDate = Calendar.current.dateByAddingDateComponents( 216 | task.dueDate, 217 | difference 218 | ) 219 | } 220 | if (task.deferDate !== null) { 221 | task.deferDate = Calendar.current.dateByAddingDateComponents( 222 | task.deferDate, 223 | difference 224 | ) 225 | } 226 | }) 227 | } 228 | 229 | // backward-compatible method - using assigned dates 230 | let oldDate = null 231 | if (created.dueDate !== null || created.deferDate !== null) { 232 | const dueForm = new Form() 233 | if (created.dueDate !== null) { 234 | oldDate = created.dueDate 235 | dueForm.addField( 236 | new Form.Field.Date('newDate', 'Due date:', oldDate, null) 237 | ) 238 | } else if (created.deferDate !== null) { 239 | oldDate = created.deferDate 240 | dueForm.addField( 241 | new Form.Field.Date('newDate', 'Defer date:', oldDate, null) 242 | ) 243 | } 244 | 245 | const dueFormPromise = dueForm.show('Date for new project', 'Continue') 246 | dueFormPromise.then((formObject) => { 247 | adjustDates(oldDate, formObject.values.newDate, created) 248 | }) 249 | } 250 | 251 | // new method - using date variables 252 | const tasksWithDueDates = [created, ...created.flattenedTasks].filter(task => task.note.includes('$DUE=')) 253 | tasksWithDueDates.forEach(task => { 254 | const dueString = task.note.match(/\$DUE=(.*?)$/m)[1] 255 | task.dueDate = Formatter.Date.withStyle(Formatter.Date.Style.Full).dateFromString(dueString) 256 | task.note = task.note.replace(task.note.match(/\$DUE=(.*?)$/m)[0], '') 257 | }) 258 | 259 | const tasksWithDeferDates = [created, ...created.flattenedTasks].filter(task => task.note.includes('$DEFER=')) 260 | tasksWithDeferDates.forEach(task => { 261 | const deferString = task.note.match(/\$DEFER=(.*?)$/m)[1] 262 | task.deferDate = Formatter.Date.withStyle(Formatter.Date.Style.Full).dateFromString(deferString) 263 | task.note = task.note.replace(task.note.match(/\$DEFER=(.*?)$/m)[0], '') 264 | }) 265 | 266 | return created 267 | } 268 | 269 | return templateLibrary 270 | })() 271 | --------------------------------------------------------------------------------