├── CompletedTaskReport.omnifocusjs ├── Resources │ ├── en.lproj │ │ ├── manifest.strings │ │ ├── sendToDayOne.strings │ │ ├── sendToDrafts.strings │ │ ├── copyToClipboard.strings │ │ └── preferences.strings │ ├── copyToClipboard.js │ ├── sendToDrafts.js │ ├── sendToDayOne.js │ ├── preferences.js │ └── completedReportLib.js └── manifest.json ├── .github └── workflows │ └── main.yml ├── CHANGELOG.md └── README.md /CompletedTaskReport.omnifocusjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.KaitlinSalzke.completedTaskReport" = "Completed Task Report"; -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/en.lproj/sendToDayOne.strings: -------------------------------------------------------------------------------- 1 | "label" = "To Day One"; 2 | "shortLabel" = "To Day One"; 3 | "mediumLabel" = "To Day One"; 4 | "longLabel" = "To Day One"; -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/en.lproj/sendToDrafts.strings: -------------------------------------------------------------------------------- 1 | "label" = "To Drafts"; 2 | "shortLabel" = "To Drafts"; 3 | "mediumLabel" = "To Drafts"; 4 | "longLabel" = "To Drafts"; -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/en.lproj/copyToClipboard.strings: -------------------------------------------------------------------------------- 1 | "label" = "Copy To Clipboard"; 2 | "shortLabel" = "Copy To Clipboard"; 3 | "mediumLabel" = "Copy To Clipboard"; 4 | "longLabel" = "Copy To Clipboard"; -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/en.lproj/preferences.strings: -------------------------------------------------------------------------------- 1 | "label" = "Preferences: Completed Task Report"; 2 | "shortLabel" = "Preferences: Completed Task Report"; 3 | "mediumLabel" = "Preferences: Completed Task Report"; 4 | "longLabel" = "Preferences: Completed Task Report"; -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/copyToClipboard.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn */ 2 | (() => { 3 | const action = new PlugIn.Action(function (selection, sender) { 4 | // FUNCTIONS FROM LIBRARY 5 | const lib = this.completedReportLib 6 | 7 | const urlTemplate = 'CLIPBOARD' 8 | 9 | lib.promptAndRunReport(urlTemplate) 10 | }) 11 | 12 | action.validate = function (selection, sender) { 13 | return true 14 | } 15 | 16 | return action 17 | })() 18 | -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/sendToDrafts.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn */ 2 | (() => { 3 | const action = new PlugIn.Action(function (selection, sender) { 4 | // FUNCTIONS FROM LIBRARY 5 | const lib = this.completedReportLib 6 | 7 | const urlTemplate = 'drafts5://create?text={{LIST}}' 8 | 9 | lib.promptAndRunReport(urlTemplate) 10 | }) 11 | 12 | action.validate = function (selection, sender) { 13 | return true 14 | } 15 | 16 | return action 17 | })() 18 | -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/sendToDayOne.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const lib = this.completedReportLib 5 | const journal = await lib.getDayOneJournalName() 6 | 7 | const urlTemplate = 8 | 'dayone://post?entry={{LIST}}&journal=' + 9 | encodeURIComponent(journal) 10 | 11 | lib.promptAndRunReport(urlTemplate) 12 | }) 13 | 14 | action.validate = function (selection, sender) { 15 | return true 16 | } 17 | 18 | return action 19 | })() 20 | -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultLocale": "en", 3 | "identifier": "com.KaitlinSalzke.completedTaskReport", 4 | "author": "Kaitlin Salzke", 5 | "description": "Plugin to generate a daily completed task report from OmniFocus", 6 | "version": "2.4.1", 7 | "actions": [ 8 | { 9 | "identifier": "sendToDayOne", 10 | "image": "bookmark" 11 | }, 12 | { 13 | "identifier": "sendToDrafts", 14 | "image": "square.on.square" 15 | }, 16 | { 17 | "identifier": "copyToClipboard", 18 | "image": "arrow.right.doc.on.clipboard" 19 | }, 20 | { 21 | "identifier": "preferences", 22 | "image": "doc.badge.gearshape.fill" 23 | } 24 | ], 25 | "libraries": [ 26 | { 27 | "identifier": "completedReportLib" 28 | } 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: CompletedTaskReport.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 | -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/preferences.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Form flattenedTags flattenedProjects */ 2 | (() => { 3 | const action = new PlugIn.Action(async function (selection, sender) { 4 | const preferences = this.completedReportLib.loadSyncedPrefs() 5 | 6 | // get current preferences or set defaults if they don't yet exist 7 | const dayOneJournalName = preferences.readString('dayOneJournalName') 8 | const showTopLevelOnly = this.completedReportLib.getShowTopLevelOnly() 9 | const includeFolderHeadings = this.completedReportLib.getIncludeFolderHeadings() 10 | const includeProjectHeadings = this.completedReportLib.getIncludeProjectHeadings() 11 | const bulletPoint = this.completedReportLib.getBulletPoint() 12 | const tagsToExclude = this.completedReportLib.getExcludedTags() 13 | const projectsToExclude = this.completedReportLib.getExcludedProjects() 14 | 15 | // create and show form 16 | const prefForm = new Form() 17 | prefForm.addField(new Form.Field.String('dayOneJournalName', 'Day One Journal Name', dayOneJournalName, null)) 18 | prefForm.addField(new Form.Field.Checkbox('showTopLevelOnly', 'Show Top Level Only', showTopLevelOnly)) 19 | prefForm.addField(new Form.Field.Checkbox('includeFolderHeadings', 'Include Folder Headings', includeFolderHeadings)) 20 | prefForm.addField(new Form.Field.Checkbox('includeProjectHeadings', 'Include Project Headings', includeProjectHeadings)) 21 | prefForm.addField(new Form.Field.String('bulletPoint', 'Bullet Point', bulletPoint, null)) 22 | prefForm.addField(new Form.Field.MultipleOptions('tagsToExclude', 'Tags To Exclude', flattenedTags, flattenedTags.map(t => t.name), tagsToExclude)) 23 | prefForm.addField(new Form.Field.MultipleOptions('projectsToExclude', 'Projects To Exclude', flattenedProjects, flattenedProjects.map(p => p.name), projectsToExclude)) 24 | await prefForm.show('Preferences: Completed Task Report', 'OK') 25 | 26 | // save preferences 27 | preferences.write('dayOneJournalName', prefForm.values.dayOneJournalName) 28 | preferences.write('showTopLevelOnly', prefForm.values.showTopLevelOnly) 29 | preferences.write('includeFolderHeadings', prefForm.values.includeFolderHeadings) 30 | preferences.write('includeProjectHeadings', prefForm.values.includeProjectHeadings) 31 | preferences.write('bulletPoint', prefForm.values.bulletPoint) 32 | preferences.write('tagsToExclude', prefForm.values.tagsToExclude.map(t => t.id.primaryKey)) 33 | preferences.write('projectsToExclude', prefForm.values.projectsToExclude.map(p => p.id.primaryKey)) 34 | }) 35 | 36 | action.validate = function (selection, sender) { 37 | return true 38 | } 39 | 40 | return action 41 | })() 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.4.1](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/compare/v2.4.0...v2.4.1) (2022-02-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * :bug: fix handling of deleted projects/tags ([ee0c3b9](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/ee0c3b9b7f85c18fd6ec9cd73daa9c8bcabb0312)) 7 | 8 | 9 | 10 | # [2.4.0](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/compare/v2.3.0...v2.4.0) (2022-02-19) 11 | 12 | 13 | ### Features 14 | 15 | * :lipstick: rename 'Preferences' to 'Preferences: Completed Task Report' ([d925cc2](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/d925cc2092189f37a84ca35b968c14a694b97df7)) 16 | * :sparkles: update validation so that actions are always available ([e917293](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/e9172934d1bfd47f0ab08832a9a579957e216563)) 17 | 18 | 19 | 20 | # [2.3.0](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/compare/v2.2.0...v2.3.0) (2021-11-17) 21 | 22 | 23 | ### Features 24 | 25 | * restructure for easier installation ([ec07f7d](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/ec07f7d13df4e3e2749330482dc595d022fbefbb)) 26 | 27 | 28 | 29 | # [2.2.0](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/compare/v2.1.0...v2.2.0) (2021-10-31) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * await result from getDayOneJournalName() ([c281739](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/c281739565ac972a4f4a353d0119311d30950a85)) 35 | * use correct function for bullet ([84db6ea](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/84db6ea902502276f66fa0fa22222d79d1309ab7)) 36 | 37 | 38 | ### Features 39 | 40 | * add getBulletPoint() ([dcdb9d5](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/dcdb9d5532d876c6306f852f59cd881fd6880a8a)) 41 | * add getDayOneJournalName() ([105a62b](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/105a62b09a17a8f8464126b66e7460bc9b712833)) 42 | * add getExcludedProjects() ([5fbf72f](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/5fbf72feb9d5265136b5d7a85f03d7dc0c1d0533)) 43 | * add getExcludedTags() ([cf4d024](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/cf4d0245e1ea18af3fa2f9ae81e34750b0b8d7ae)) 44 | * add getIncludeFolderHeadings() ([e8229fe](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/e8229fedfeec9a74f8a077d5ede1a111247a8958)) 45 | * add getIncludeProjectHeadings() ([f533610](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/f533610b5a1ecc669bc0aa0ec348649938b45813)) 46 | * add getShowTopLevelOnly() ([f49992d](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/f49992d28e228b44a5819224ac2704b732e22621)) 47 | 48 | 49 | 50 | # [2.1.0](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/compare/246140ef4379c77b9ce684d235d9f41086308fbc...v2.1.0) (2021-10-24) 51 | 52 | 53 | ### Features 54 | 55 | * add SF symbols ([246140e](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/commit/246140ef4379c77b9ce684d235d9f41086308fbc)) 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is an Omni Automation plug-in bundle for OmniFocus that sends a list of completed tasks for a given period (in Markdown format) to a Day One journal, to Drafts, or to the clipboard. It also includes functions that allow the user to use their own custom URL schemes. Further details are provided below. 4 | 5 | You may find the 'Mark Completed Yesterday' script included in my [Miscellaneous OmniFocus Plug-ins](https://github.com/ksalzke/miscellaneous-omnifocus-plugins) to be useful in conjunction with this plug-in. 6 | 7 | _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 a random amateur on the internet!)_ 8 | 9 | ## Known issues 10 | 11 | ### Running a report for a day other than today 12 | 13 | Although this plug-in will allow a report to be run for any period of time, the Day One URL scheme does not allow for a day to be specified (nor does Drafts). Therefore, the report will be added ton the current date at the current time. This can then be edited in Day One if desired. 14 | 15 | Also bear in mind that if a report is run for a date in the past, tasks will only be included in the report if they are still in the database (e.g. if tasks from that date have been archived they will not appear in the report). 16 | 17 | ### Other 18 | 19 | Refer to ['issues'](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/issues) for other known issues and planned changes/enhancements. 20 | 21 | # Installation & Set-Up 22 | 23 | **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.** 24 | 25 | **Important note: for this plug-in bundle to work correctly, my [Function Library for OmniFocus](https://github.com/ksalzke/function-library-for-omnifocus) is currently also required and needs to be added to the your OmniFocus plug-in folder separately.** 26 | 27 | 1. Download the [latest release](https://github.com/ksalzke/completed-task-report-omnifocus-plugin/releases/latest). 28 | 2. Unzip the downloaded file. 29 | 3. Move the `.omnifocusjs` file to your OmniFocus plug-in library folder (or open it to install). 30 | 4. Configure your preferences using the `Preferences` action. (Note that to run this action, no tasks can be selected.) 31 | 32 | For instructions on adding additional actions to run custom reports or different URL schemes, refer to `Adding New Actions` in the `Actions` section below. 33 | 34 | # The Report 35 | 36 | By default, a report similar to the below is produced in Markdown and sent to the desired destination. 37 | 38 | Sample Report 39 | 40 | # Actions 41 | 42 | This plug-in bundle contains three actions, `To Day One`, `To Drafts`, and `Copy To Clipboard`. In all three cases, the action: 43 | 44 | 1. Asks the user to select a day for the report. The options are: 45 | 46 | * Today (default) 47 | 48 | * Yesterday 49 | 50 | * Other Date - If this is selected, the user will also be prompted for a specific date. Note that [OmniFocus date parsing](https://support.omnigroup.com/documentation/omnifocus/mac/3.4/en/the-inspector/#dates) can be used. (A time is shown but may be ignored.) 51 | 52 | * Custom Period - If this is selected, the user will also be prompted to enter a start and end time. Note that [OmniFocus date parsing](https://support.omnigroup.com/documentation/omnifocus/mac/3.4/en/the-inspector/#dates) can be used. (In this case, the specific times will be used.) 53 | 54 | 2. Identifies tasks that have been completed on the entered date (in the first three cases) or between the specified times (if a Custom Period is entered), provided they do not have any of the tags from the `tagsToExclude` list set up in the configuration. If `showTopLevelOnly` is set to true in the configuration file, this stops at the top-level task that has been completed; e.g if a task or project "Buy groceries" has subtasks "Buy potatoes" and "Buy milk", only "Buy groceries" will be included in the final list. If this is set to false, the report will include all completed tasks that do not have children, or whose children are all hidden. 55 | 56 | 3. Generates a report including those tasks in Markdown, with: 57 | 58 | * A `h1` heading such as `Tasks Completed on Mon Dec 23 2019` (for the first three options, or if the custom period begins and ends on the same date) or `Tasks Completed from Mon Dec 23 2019 to Wed Dec 25 2019`. 59 | 60 | * (If `includeFolderHeadings` is set to true) a bold heading for each top-level folder (where the folder has completed tasks) 61 | 62 | * (If `includeProjectHeadings` is set to true) an italic heading for each project (where the project has completed tasks). 63 | 64 | * A list of completed tasks, grouped under the applicable folder and project headings if shown (Note that the root project task will not be included in the 'task' listing if a project heading is already shown.) If there are multiple tasks (or projects, where project headings are shown) with the same name in a row (for example as the result of a repeating task) the task will only be listed once with (for example) `(x5)` appended. 65 | 66 | It also contains a Preferences action. 67 | 68 | ## Preferences 69 | 70 | This action allows the user to set the preferences for the plug-in. These sync between devices using the Synced Preferences plug-in linked above. Currently, the available preferences are: 71 | 72 | * **Day One Journal Name** The name of the Day One journal for the report to be sent to. 73 | 74 | * **Show Top Level Only** If this option is checked, the report will stop at the top-level task that has been completed; e.g if a task or project "Buy groceries" has subtasks "Buy potatoes" and "Buy milk", only "Buy groceries" will be included in the final list. If this is unchecked, the report will include all completed tasks that do not have children, or whose children are all hidden. By default, only the top level is shown. 75 | 76 | * **Include Folder Headings** If this option is checked, the report will include folder headings. By default, folder headings are included. 77 | 78 | * **Include Project Headings** If this option is checked, the report will include project headings. By default, project headings are not included. 79 | 80 | * **Bullet Point** This is a string to be used as the bullet point before each task in the report. 81 | 82 | * **Tags To Exclude** Tasks that have these tags will be excluded from the report. 83 | 84 | * **Projects To Exclude** Tasks from these projects will be excluded from the report. 85 | 86 | ## To Day One 87 | 88 | This action generates the report as described above and sends it to the Day One journal specified as `dayOneJournalName` in the configuration (using Day One's URL scheme). The report is shown in Day One. 89 | 90 | ## To Drafts 91 | 92 | This action generates the report as described above and creates a new draft in Drafts (using the Drafts URL scheme). 93 | 94 | ## Copy To Clipboard 95 | 96 | This action generates the report as described above, copies it to the clipboard, and shows an alert when this has been completed. 97 | 98 | ## Adding New Actions 99 | 100 | Given a particular URL scheme, a new action can be added by: 101 | 1. Creating an additional `sendToSomewhere.js` file in the `Resources` folder of this bundle with code similar to the sample code below; for a basic case, all that needs to be changed is the `urlTemplate = URL` line (to reflect the URL scheme of the app you are using). The placeholder `{{LIST}}` can be used where the list needs to go. 102 | 2. Creating an additional `sendToSomewhere.strings` file in the `Resources/en.lproj` folder of this bundle. Its content should be similar to the existing `.strings` files. 103 | 3. Add `{ "identifier": "sendToDrafts" }` to the actions array in `manifest.json`. 104 | 105 | If you do create a new action, consider creating a pull request so that it can be included here for others to use. 106 | 107 | **Sample code:** 108 | ``` 109 | (() => { 110 | const action = new PlugIn.Action(function (selection, sender) { 111 | const lib = this.completedReportLib 112 | 113 | const urlTemplate = 'someapp://post?entry={{LIST}}' 114 | 115 | lib.promptAndRunReport(urlTemplate) 116 | }) 117 | 118 | action.validate = function (selection, sender) { 119 | // only valid if nothing is selected - so does not show in share menu 120 | return selection.tasks.length === 0 && selection.projects.length === 0 121 | } 122 | 123 | return action 124 | })() 125 | ``` 126 | 127 | # Functions 128 | 129 | This plug-in bundle also contains several functions in the `completedReportLib` which may be useful in adapting it to fit your own needs or in creating other plug-ins. 130 | 131 | ## loadSyncedPrefs () 132 | 133 | This function loads the synced preferences for the 'Completed Task Report' plug-in. 134 | 135 | ## getExcludedTags () 136 | 137 | This function returns an array of excluded tags, as set in the preferences. If no preference has been set, it returns an empty array. 138 | 139 | ## getExcludedProjects () 140 | 141 | This function returns an array of excluded projects, as set in the preferences. If no preference has been set, it returns an empty array. 142 | 143 | ## getDayOneJournalName () 144 | 145 | This function returns a string containing the Day One journal name, if it has been set in the preferences. If it has not been set, a form is shown prompting the user for the name of the journal and the entered string is saved in preferences. 146 | 147 | ## getShowTopLevelOnly () 148 | 149 | This function returns a boolean value reflecting whether the 'Show Top Level Only' preference has been set. If no preference has been set, this defaults to true. 150 | 151 | ## getIncludeFolderHeadings () 152 | 153 | This function returns a boolean value reflecting whether the 'Include Folder Headings' preference has been set. If no preference has been set, this defaults to true. 154 | 155 | ## getIncludeProjectHeadings () 156 | 157 | This function returns a boolean value reflecting whether the 'Include Project Headings' preference has been set. If no preference has been set, this defaults to true. 158 | 159 | ## getBulletPoint () 160 | 161 | This function returns a string containing the bullet point that has been set in preferences. If no prefernce has been set, this defaults to `' * '`. 162 | 163 | ## getTasksCompletedBetweenDates (startDate, endDate) 164 | 165 | This function takes a start time and end time (in the Date format) as input and returns an array of the tasks that have been completed between the two times. 166 | 167 | ## makeDateHeading (startDate, endDate) 168 | 169 | This function produces a markdown heading using the provided start and end dates. 170 | 171 | ## getMarkdownReport (heading, tasksCompleted) 172 | 173 | This function takes a heading and an array of tasks as input, and returns a string which is a report listing the tasks (in Markdown format). 174 | 175 | ## runReportForPeriod (startDate, endDate, templateUrl) 176 | 177 | This function takes a start time and end time as input (in the Date format) as well as a string which uses a URL scheme. The placeholder `{{LIST}}` can be used in the URL to indicate where the Markdown list of tasks which have been completed between the two times should be included. 178 | 179 | The function will generate the Markdown list using `getMarkdownReport` and then call the specified URL. 180 | 181 | Special case: If the templateURL is set to 'CLIPBOARD' the report will instead be copied to the clipboard. 182 | 183 | ## promptAndRunReport (templateUrl) 184 | 185 | This function will complete steps 1-3 directly under `Actions` above (most noteably prompting the user for input), then call `runReportForPeriod` to create the report. 186 | -------------------------------------------------------------------------------- /CompletedTaskReport.omnifocusjs/Resources/completedReportLib.js: -------------------------------------------------------------------------------- 1 | /* global PlugIn Version inbox Alert ApplyResult library Project Form Calendar Pasteboard Tag Project */ 2 | (() => { 3 | const completedReportLib = new PlugIn.Library(new Version('1.0')) 4 | 5 | completedReportLib.functionLibrary = () => { 6 | const functionLibrary = PlugIn.find('com.KaitlinSalzke.functionLibrary') 7 | if (functionLibrary !== null) { 8 | return functionLibrary.library('functionLibrary') 9 | } else { 10 | const alert = new Alert( 11 | 'Function Library Required', 12 | 'For this plug-in bundle (Completed Task Report) to work correctly, my Function Library for OmniFocus (https://github.com/ksalzke/function-library-for-omnifocus) is currently also required and needs to be added to the your OmniFocus plug-in folder separately. Either you do not currently have this library file installed, or it is not installed correctly.' 13 | ) 14 | alert.show() 15 | } 16 | } 17 | 18 | completedReportLib.loadSyncedPrefs = () => { 19 | const syncedPrefsPlugin = PlugIn.find('com.KaitlinSalzke.SyncedPrefLibrary') 20 | 21 | if (syncedPrefsPlugin !== null) { 22 | const SyncedPref = syncedPrefsPlugin.library('syncedPrefLibrary').SyncedPref 23 | return new SyncedPref('com.KaitlinSalzke.CompletedTaskReport') 24 | } else { 25 | const alert = new Alert( 26 | 'Synced Preferences Library Required', 27 | 'For the Completed Task Report 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.' 28 | ) 29 | alert.show() 30 | } 31 | } 32 | 33 | completedReportLib.getExcludedTags = () => { 34 | const preferences = completedReportLib.loadSyncedPrefs() 35 | return (preferences.read('tagsToExclude') !== null) ? preferences.read('tagsToExclude').map(id => Tag.byIdentifier(id)).filter(tag => tag !== null) : [] 36 | } 37 | 38 | completedReportLib.getExcludedProjects = () => { 39 | const preferences = completedReportLib.loadSyncedPrefs() 40 | return (preferences.read('projectsToExclude') !== null) ? preferences.read('projectsToExclude').map(id => Project.byIdentifier(id)).map(proj => proj !== null) : [] 41 | } 42 | 43 | completedReportLib.getDayOneJournalName = async () => { 44 | const preferences = completedReportLib.loadSyncedPrefs() 45 | const dayOneJournalName = preferences.readString('dayOneJournalName') 46 | 47 | // return name if already set 48 | if (dayOneJournalName !== null) return dayOneJournalName 49 | 50 | // if not set, prompt user for string, save preference and return name 51 | const form = new Form() 52 | form.addField(new Form.Field.String('dayOneJournalName', 'Day One Journal Name', '', null)) 53 | await form.show('Send To Day One', 'OK') 54 | preferences.write('dayOneJournalName', form.values.dayOneJournalName) 55 | return form.values.dayOneJournalName 56 | } 57 | 58 | completedReportLib.getShowTopLevelOnly = () => { 59 | const preferences = completedReportLib.loadSyncedPrefs() 60 | return (preferences.read('showTopLevelOnly') !== null) ? preferences.readBoolean('showTopLevelOnly') : true 61 | } 62 | 63 | completedReportLib.getIncludeFolderHeadings = () => { 64 | const preferences = completedReportLib.loadSyncedPrefs() 65 | return (preferences.read('includeFolderHeadings') !== null) ? preferences.read('includeFolderHeadings') : true 66 | } 67 | 68 | completedReportLib.getIncludeProjectHeadings = () => { 69 | const preferences = completedReportLib.loadSyncedPrefs() 70 | return (preferences.read('includeProjectHeadings') !== null) ? preferences.read('includeProjectHeadings') : false 71 | } 72 | 73 | completedReportLib.getBulletPoint = () => { 74 | const preferences = completedReportLib.loadSyncedPrefs() 75 | return (preferences.readString('bulletPoint') !== null) ? preferences.readString('bulletPoint') : ' * ' 76 | } 77 | 78 | completedReportLib.getTasksCompletedBetweenDates = (startDate, endDate) => { 79 | // function to check if a tag is included in 'excluded tags' 80 | const isHidden = (element) => { 81 | return completedReportLib.getExcludedTags().includes(element) 82 | } 83 | 84 | const showTopLevelOnly = completedReportLib.getShowTopLevelOnly() 85 | 86 | // create an array to store completed tasks 87 | const tasksCompleted = [] 88 | 89 | // function to check if a task was completed today 90 | const completedToday = (item) => { 91 | if ( 92 | item.completed && 93 | item.completionDate > startDate && 94 | item.completionDate < endDate && 95 | !item.tags.some(isHidden) 96 | ) { 97 | return true 98 | } else return false 99 | } 100 | 101 | // get completed tasks from inbox 102 | inbox.apply((item) => { 103 | if (completedToday(item)) { 104 | // if has children, only add if all children excluded due to hidden tags 105 | if (item.hasChildren && !showTopLevelOnly) { 106 | if ( 107 | item.children.every((child) => { 108 | return child.tags.some(isHidden) 109 | }) 110 | ) { 111 | tasksCompleted.push(item) 112 | } 113 | } else { 114 | tasksCompleted.push(item) 115 | } 116 | // skip children if showTopLevelOnly is set to true in prefs 117 | if (showTopLevelOnly) { 118 | return ApplyResult.SkipChildren 119 | } 120 | } 121 | }) 122 | 123 | // get other tasks 124 | library.apply(function (item) { 125 | if (item instanceof Project && !completedReportLib.getExcludedProjects().includes(item) && item.task.hasChildren) { 126 | item.task.apply((tsk) => { 127 | if (completedToday(tsk)) { 128 | // if has children, only add if all children excluded due to hidden tags 129 | if (tsk.hasChildren && !showTopLevelOnly) { 130 | if ( 131 | tsk.children.every((child) => { 132 | return child.tags.some(isHidden) 133 | }) 134 | ) { 135 | tasksCompleted.push(tsk) 136 | } 137 | } else { // add if has no children 138 | tasksCompleted.push(tsk) 139 | } 140 | // skip children if showTopLevelOnly is set to true in prefs 141 | if (showTopLevelOnly) { 142 | return ApplyResult.SkipChildren 143 | } 144 | } 145 | }) 146 | } 147 | }) 148 | return tasksCompleted 149 | } 150 | 151 | completedReportLib.makeDateHeading = (startDate, endDate) => { 152 | let headingDates 153 | if (startDate.toDateString() === endDate.toDateString()) { 154 | headingDates = 'on ' + startDate.toDateString() 155 | } else { 156 | headingDates = 157 | 'from ' + startDate.toDateString() + ' to ' + endDate.toDateString() 158 | } 159 | return '# Tasks Completed ' + headingDates + '\n' 160 | } 161 | 162 | completedReportLib.getMarkdownReport = (heading, tasksCompleted) => { 163 | let markdown = heading 164 | let currentFolder = 'No Folder' 165 | let currentProject = 'No Project' 166 | let lastTaskName = '' 167 | let taskNameCounter = 1 168 | let projectNameCounter = 1 169 | tasksCompleted.forEach(function (completedTask) { 170 | // if last instance of same task, show as multiple 171 | if (completedTask.name !== lastTaskName && taskNameCounter > 1) { 172 | markdown = markdown.replace(/\n$/g, ' (x' + taskNameCounter + ')\n') 173 | taskNameCounter = 1 174 | } 175 | const containingFolder = completedReportLib 176 | .functionLibrary() 177 | .getContainingFolder(completedTask).name 178 | if (currentFolder !== containingFolder) { 179 | if (completedReportLib.getIncludeFolderHeadings()) { 180 | markdown = markdown.concat('\n**', containingFolder.trim(), '**\n') 181 | } 182 | currentFolder = containingFolder 183 | } 184 | // get current project name - if null (in inbox) use "No Project" 185 | let taskProject 186 | if (completedTask.containingProject == null) { 187 | taskProject = 'No Project' 188 | } else { 189 | taskProject = completedTask.containingProject.name 190 | } 191 | 192 | // check if project has changed 193 | if (currentProject !== taskProject) { 194 | if (completedReportLib.getIncludeProjectHeadings()) { 195 | if (projectNameCounter > 1) { 196 | markdown = markdown.replace( 197 | /\n$/g, 198 | ' (x' + projectNameCounter + ')\n' 199 | ) 200 | } 201 | markdown = markdown.concat('\n_', taskProject.trim(), '_\n') 202 | } 203 | currentProject = taskProject 204 | projectNameCounter = 1 205 | } else if (completedTask.name === currentProject) { 206 | projectNameCounter++ 207 | } 208 | // include task, unless it's a project and project headings are shown 209 | if ( 210 | !(completedTask.project !== null && completedReportLib.getIncludeProjectHeadings()) 211 | ) { 212 | if (completedTask.name !== lastTaskName) { 213 | markdown = markdown.concat(completedReportLib.getBulletPoint(), completedTask.name, '\n') 214 | } else { 215 | taskNameCounter++ 216 | } 217 | lastTaskName = completedTask.name 218 | } 219 | }) 220 | return markdown 221 | } 222 | 223 | completedReportLib.runReportForPeriod = (startDate, endDate, templateUrl) => { 224 | console.log('running report for period') 225 | const tasksCompleted = completedReportLib.getTasksCompletedBetweenDates( 226 | startDate, 227 | endDate 228 | ) 229 | console.log('tasks completed: ' + tasksCompleted) 230 | const heading = completedReportLib.makeDateHeading(startDate, endDate) 231 | console.log(heading) 232 | 233 | const markdown = completedReportLib.getMarkdownReport(heading, tasksCompleted) 234 | console.log(markdown) 235 | 236 | if (templateUrl === 'CLIPBOARD') { 237 | Pasteboard.general.string = markdown 238 | new Alert('Done!', 'Completed task report has been copied to the clipboard.').show() 239 | } else { 240 | const fullUrl = templateUrl.replace('{{LIST}}', encodeURIComponent(markdown)) 241 | 242 | URL.fromString(fullUrl).call(() => {}) 243 | } 244 | } 245 | 246 | completedReportLib.promptAndRunReport = (templateUrl) => { 247 | const now = new Date() 248 | const today = Calendar.current.startOfDay(now) 249 | const yesterday = completedReportLib 250 | .functionLibrary() 251 | .adjustDateByDays(today, -1) 252 | 253 | // basic selection form - select today, tomorrow, or other 254 | const selectDayForm = new Form() 255 | const selectDayPopupMenu = new Form.Field.Option( 256 | 'selectedDay', 257 | 'Day', 258 | ['Today', 'Yesterday', 'Other Day', 'Custom Period'], 259 | null, 260 | 'Today' 261 | ) 262 | selectDayForm.addField(selectDayPopupMenu) 263 | const selectDayFormPrompt = 'Which day?' 264 | const selectDayFormPromise = selectDayForm.show(selectDayFormPrompt, 'Continue') 265 | 266 | // form for when 'other' is selected - to enter alternative date 267 | const selectOtherDateForm = new Form() 268 | const selectOtherDateDateField = new Form.Field.Date('dateInput', 'Date', today) 269 | selectOtherDateForm.addField(selectOtherDateDateField) 270 | const selectOtherDateFormPrompt = 'Select date:' 271 | 272 | const selectCustomPeriodForm = new Form() 273 | const startTimeField = new Form.Field.Date('startTime', 'Start', today) 274 | const endTimeField = new Form.Field.Date('endTime', 'End', today) 275 | selectCustomPeriodForm.addField(startTimeField) 276 | selectCustomPeriodForm.addField(endTimeField) 277 | const selectCustomPeriodPrompt = 'Select start and end times: ' 278 | 279 | // show forms 280 | selectDayFormPromise.then(function (formObject) { 281 | const optionSelected = formObject.values.selectedDay 282 | let startDate 283 | let endDate 284 | let selectOtherDateFormPromise 285 | let day 286 | let selectCustomPeriodFormPromise 287 | switch (optionSelected) { 288 | case 'Today': 289 | startDate = Calendar.current.startOfDay(today) 290 | endDate = new Date(today.setHours(23, 59, 59, 999)) 291 | completedReportLib.runReportForPeriod( 292 | startDate, 293 | endDate, 294 | templateUrl 295 | ) 296 | break 297 | case 'Yesterday': 298 | startDate = Calendar.current.startOfDay(yesterday) 299 | endDate = new Date(yesterday.setHours(23, 59, 59, 999)) 300 | completedReportLib.runReportForPeriod( 301 | startDate, 302 | endDate, 303 | templateUrl 304 | ) 305 | break 306 | case 'Other Day': 307 | selectOtherDateFormPromise = selectOtherDateForm.show( 308 | selectOtherDateFormPrompt, 309 | 'Continue' 310 | ) 311 | selectOtherDateFormPromise.then(function (formObject) { 312 | day = formObject.values.dateInput 313 | startDate = Calendar.current.startOfDay(day) 314 | endDate = new Date(day.setHours(23, 59, 59, 999)) 315 | completedReportLib.runReportForPeriod( 316 | startDate, 317 | endDate, 318 | templateUrl 319 | ) 320 | }) 321 | selectOtherDateFormPromise.catch(function (err) { 322 | console.log('form cancelled', err.message) 323 | }) 324 | break 325 | case 'Custom Period': 326 | selectCustomPeriodFormPromise = selectCustomPeriodForm.show( 327 | selectCustomPeriodPrompt, 328 | 'Continue' 329 | ) 330 | selectCustomPeriodFormPromise.then(function (formObject) { 331 | startDate = formObject.values.startTime 332 | endDate = formObject.values.endTime 333 | completedReportLib.runReportForPeriod( 334 | startDate, 335 | endDate, 336 | templateUrl 337 | ) 338 | }) 339 | selectCustomPeriodFormPromise.catch(function (err) { 340 | console.log('form cancelled', err.message) 341 | }) 342 | break 343 | default: 344 | } 345 | }) 346 | 347 | selectDayFormPromise.catch(function (err) { 348 | console.log('form cancelled', err.message) 349 | }) 350 | } 351 | 352 | return completedReportLib 353 | })() 354 | --------------------------------------------------------------------------------