imdone-core 1.54.10 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.dependency-cruiser.cjs +412 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.imdoneignore +2 -1
  4. package/README.md +2 -2
  5. package/_index.md +7 -0
  6. package/build.js +57 -0
  7. package/esm-migration.md +446 -0
  8. package/lib/FileProjectContext.js +13 -0
  9. package/lib/ProjectContext.js +19 -18
  10. package/lib/adapters/date-time.js +3 -8
  11. package/lib/adapters/environment.js +3 -0
  12. package/lib/adapters/file-gateway.js +133 -106
  13. package/lib/adapters/git-download.js +36 -16
  14. package/lib/adapters/index.js +9 -0
  15. package/lib/adapters/logger.js +136 -0
  16. package/lib/adapters/markdown.js +31 -19
  17. package/lib/adapters/parsers/DateLanguageParser.js +11 -9
  18. package/lib/adapters/parsers/content-transformer.js +35 -30
  19. package/lib/adapters/parsers/file/LocalFileParserFactory.js +8 -9
  20. package/lib/adapters/parsers/file/ReadStreamCodeParser.js +8 -8
  21. package/lib/adapters/parsers/file/ReadStreamMarkdownParser.js +4 -4
  22. package/lib/adapters/parsers/file/ReadStreamTaskParser.js +5 -3
  23. package/lib/adapters/parsers/file/index.js +4 -0
  24. package/lib/adapters/parsers/function-parser.js +1 -5
  25. package/lib/adapters/parsers/index.js +5 -0
  26. package/lib/adapters/parsers/task/CardContentParser.js +23 -32
  27. package/lib/adapters/parsers/task/CheckStyleTaskParser.js +2 -2
  28. package/lib/adapters/parsers/task/CodeStyleTaskParser.js +2 -2
  29. package/lib/adapters/parsers/task/HashStyleTaskParser.js +3 -3
  30. package/lib/adapters/parsers/task/MarkdownStyleTaskParser.js +2 -2
  31. package/lib/adapters/parsers/task/TaskParser.js +1 -1
  32. package/lib/adapters/parsers/task/index.js +6 -0
  33. package/lib/adapters/storage/config.js +15 -20
  34. package/lib/adapters/storage/index.js +2 -0
  35. package/lib/adapters/storage/tags.js +11 -12
  36. package/lib/adapters/yaml.js +8 -5
  37. package/lib/card.js +25 -26
  38. package/lib/checksum.js +22 -0
  39. package/lib/cli.js +114 -0
  40. package/lib/config.js +106 -71
  41. package/lib/constants.js +21 -50
  42. package/lib/context/ApplicationContext.js +7 -4
  43. package/lib/context/index.js +1 -0
  44. package/lib/default-settings.js +3 -3
  45. package/lib/domain/entities/View.js +4 -11
  46. package/lib/domain/entities/index.js +1 -0
  47. package/lib/domain/index.js +2 -0
  48. package/lib/domain/usecases/GetViewsUsecase.js +6 -12
  49. package/lib/domain/usecases/SaveViewUsecase.js +8 -14
  50. package/lib/domain/usecases/index.js +2 -0
  51. package/lib/file.js +1158 -1162
  52. package/lib/get-tasks-to-modify.js +117 -0
  53. package/lib/index.js +23 -0
  54. package/lib/languages.js +4 -5
  55. package/lib/list.js +46 -95
  56. package/lib/migrate-config.js +5 -5
  57. package/lib/mixins/index.js +2 -0
  58. package/lib/mixins/repo-fs-store.js +224 -289
  59. package/lib/mixins/repo-watched-fs-store.js +158 -147
  60. package/lib/plugins/archive-plugin.js +10 -8
  61. package/lib/plugins/default-board-actions-plugin.js +2 -2
  62. package/lib/plugins/default-board-properties-plugin.js +7 -7
  63. package/lib/plugins/default-plugins.js +7 -0
  64. package/lib/plugins/epic-plugin.js +6 -5
  65. package/lib/plugins/extension-plugin.js +24 -17
  66. package/lib/plugins/index.js +9 -0
  67. package/lib/plugins/persist-tags-plugin.js +4 -3
  68. package/lib/plugins/plugin-manager.js +89 -52
  69. package/lib/plugins/plugin-registry.js +12 -16
  70. package/lib/project-factory.js +21 -24
  71. package/lib/project.js +97 -144
  72. package/lib/repository.js +1323 -1555
  73. package/lib/task.js +562 -580
  74. package/lib/tools.js +39 -113
  75. package/lib/usecases/actions.js +32 -0
  76. package/lib/usecases/get-project-tags.js +3 -12
  77. package/lib/usecases/get-tasks-in-file.js +4 -7
  78. package/lib/usecases/index.js +4 -0
  79. package/package.json +44 -66
  80. package/scripts/delete-markdown-indexes.js +87 -0
  81. package/scripts/generate-index.js +35 -0
  82. package/{index-markdown.js → scripts/index-markdown.js} +7 -7
  83. package/tsconfig.json +15 -0
  84. package/types/FileProjectContext.d.ts +4 -0
  85. package/types/ProjectContext.d.ts +9 -0
  86. package/types/adapters/date-time.d.ts +2 -0
  87. package/types/adapters/environment.d.ts +1 -0
  88. package/types/adapters/file-gateway.d.ts +51 -0
  89. package/types/adapters/git-download.d.ts +1 -0
  90. package/types/adapters/index.d.ts +9 -0
  91. package/types/adapters/logger.d.ts +24 -0
  92. package/types/adapters/markdown.d.ts +13 -0
  93. package/types/adapters/parsers/DateLanguageParser.d.ts +1 -0
  94. package/types/adapters/parsers/content-transformer.d.ts +10 -0
  95. package/types/adapters/parsers/file/LocalFileParserFactory.d.ts +5 -0
  96. package/types/adapters/parsers/file/ReadStreamCodeParser.d.ts +13 -0
  97. package/types/adapters/parsers/file/ReadStreamMarkdownParser.d.ts +7 -0
  98. package/types/adapters/parsers/file/ReadStreamTaskParser.d.ts +19 -0
  99. package/types/adapters/parsers/file/index.d.ts +4 -0
  100. package/types/adapters/parsers/function-parser.d.ts +1 -0
  101. package/types/adapters/parsers/index.d.ts +5 -0
  102. package/types/adapters/parsers/task/CardContentParser.d.ts +45 -0
  103. package/types/adapters/parsers/task/CheckStyleTaskParser.d.ts +12 -0
  104. package/types/adapters/parsers/task/CodeStyleTaskParser.d.ts +14 -0
  105. package/types/adapters/parsers/task/HashStyleTaskParser.d.ts +13 -0
  106. package/types/adapters/parsers/task/MarkdownStyleTaskParser.d.ts +14 -0
  107. package/types/adapters/parsers/task/TaskParser.d.ts +7 -0
  108. package/types/adapters/parsers/task/index.d.ts +6 -0
  109. package/types/adapters/storage/config.d.ts +6 -0
  110. package/types/adapters/storage/index.d.ts +2 -0
  111. package/types/adapters/storage/tags.d.ts +3 -0
  112. package/types/adapters/yaml.d.ts +2 -0
  113. package/types/card.d.ts +166 -0
  114. package/types/checksum.d.ts +8 -0
  115. package/types/config.d.ts +61 -0
  116. package/types/constants.d.ts +46 -0
  117. package/types/context/ApplicationContext.d.ts +24 -0
  118. package/types/context/index.d.ts +1 -0
  119. package/types/default-settings.d.ts +2 -0
  120. package/types/domain/entities/FileProjectContext.d.ts +5 -0
  121. package/types/domain/entities/View.d.ts +17 -0
  122. package/types/domain/entities/index.d.ts +1 -0
  123. package/types/domain/index.d.ts +2 -0
  124. package/types/domain/usecases/GetViewsUsecase.d.ts +4 -0
  125. package/types/domain/usecases/SaveViewUsecase.d.ts +8 -0
  126. package/types/domain/usecases/index.d.ts +2 -0
  127. package/types/file.d.ts +344 -0
  128. package/types/get-tasks-to-modify.d.ts +3 -0
  129. package/types/index.d.ts +23 -0
  130. package/types/languages.d.ts +649 -0
  131. package/types/list.d.ts +30 -0
  132. package/types/migrate-config.d.ts +1 -0
  133. package/types/mixins/index.d.ts +2 -0
  134. package/types/mixins/repo-fs-store.d.ts +2 -0
  135. package/types/mixins/repo-watched-fs-store.d.ts +2 -0
  136. package/types/plugins/archive-plugin.d.ts +8 -0
  137. package/types/plugins/default-board-actions-plugin.d.ts +9 -0
  138. package/types/plugins/default-board-properties-plugin.d.ts +16 -0
  139. package/types/plugins/default-plugins.d.ts +6 -0
  140. package/types/plugins/epic-plugin.d.ts +19 -0
  141. package/types/plugins/extension-plugin.d.ts +15 -0
  142. package/types/plugins/index.d.ts +9 -0
  143. package/types/plugins/persist-tags-plugin.d.ts +4 -0
  144. package/types/plugins/plugin-manager.d.ts +68 -0
  145. package/types/plugins/plugin-registry.d.ts +1 -0
  146. package/types/project-factory.d.ts +14 -0
  147. package/types/project.d.ts +158 -0
  148. package/types/repository.d.ts +996 -0
  149. package/types/task.d.ts +202 -0
  150. package/types/tools.d.ts +29 -0
  151. package/types/usecases/actions.d.ts +7 -0
  152. package/types/usecases/get-imdone-url.d.ts +1 -0
  153. package/types/usecases/get-project-tags.d.ts +1 -0
  154. package/types/usecases/get-tasks-in-file.d.ts +4 -0
  155. package/types/usecases/index.d.ts +4 -0
  156. package/vitest.config.js +13 -0
  157. package/cli.js +0 -74
  158. package/index.js +0 -1
  159. package/lib/adapters/readers/FileSystemProjectReader.js +0 -45
  160. package/lib/context/StreamingApplicationContext.js +0 -31
  161. package/lib/controlers/SaveViewController.js +0 -10
  162. package/lib/domain/entities/FileProjectContext.js +0 -10
  163. package/lib/domain/usecases/OpenProjectUsecase.js +0 -13
package/lib/repository.js CHANGED
@@ -1,1746 +1,1514 @@
1
- 'use strict'
2
-
3
- var _isFunction = require('lodash.isfunction'),
4
- _isUndefined = require('lodash.isundefined'),
5
- _isString = require('lodash.isstring'),
6
- _omit = require('lodash.omit'),
7
- _reject = require('lodash.reject'),
8
- _noop = require('lodash.noop'),
9
- _some = require('lodash.some'),
10
- _remove = require('lodash.remove'),
11
- _groupBy = require('lodash.groupby'),
12
- _where = require('lodash.where'),
13
- _template = require('lodash.template'),
14
- _union = require('lodash.union'),
15
- events = require('events'),
16
- languages = require('./languages'),
17
- util = require('util'),
18
- async = require('async'),
19
- path = require('path'),
20
- ignore = require('ignore'),
21
- File = require('./file'),
22
- Config = require('./config'),
23
- eol = require('eol'),
24
- tools = require('./tools'),
25
- inMixinsNoop = tools.inMixinsNoop,
26
- constants = require('./constants'),
27
- log = require('debug')('imdone-core:Repository'),
28
- List = require('./list'),
29
- monquery = require('monquery'),
30
- sift = require('sift'),
31
- fastSort = require('fast-sort/dist/sort.js'),
32
- JSONfns = require('json-fns'),
33
- Task = require('./task'),
34
- newCard = require('./card'),
35
- { replaceDateLanguage } = require('./adapters/parsers/DateLanguageParser'),
36
- { getRawTask, isNumber, LIST_NAME_PATTERN } = require('./adapters/parsers/task/CardContentParser'),
37
- XRegExp = require('xregexp'),
38
- promisify = require('util').promisify
39
-
40
- const appContext = require('./context/ApplicationContext')
41
-
42
- var ERRORS = constants.ERRORS,
43
- DEFAULT_FILE_PATTERN = constants.DEFAULT_FILE_PATTERN,
44
- ASYNC_LIMIT = constants.ASYNC_LIMIT,
45
- DEFAULT_SORT = [{ asc: u => u.order }, { asc: u => u.text }]
46
-
47
- // Emits task.found, list.found, file.update and file.delete, file.processed, files.saved
1
+ import _isFunction from 'lodash.isfunction'
2
+ import _isUndefined from 'lodash.isundefined'
3
+ import _isString from 'lodash.isstring'
4
+ import _omit from 'lodash.omit'
5
+ import _reject from 'lodash.reject'
6
+ import _noop from 'lodash.noop'
7
+ import _some from 'lodash.some'
8
+ import _remove from 'lodash.remove'
9
+ import _groupBy from 'lodash.groupby'
10
+ import _where from 'lodash.where'
11
+ import _union from 'lodash.union'
12
+ import Emitter from 'events'
13
+ import { languages } from './languages.js'
14
+ import util from 'util'
15
+ import { parallel, eachLimit, eachSeries, series } from 'async'
16
+ import path from 'path'
17
+ import ignore from 'ignore'
18
+ import { File } from './file.js'
19
+ import eol from 'eol'
20
+ import { tools } from './tools.js'
21
+ const { inMixinsNoop } = tools
22
+ import { constants } from './constants.js'
23
+ import debug from 'debug'
24
+ const log = debug('imdone-core:Repository')
25
+ import { List } from './list.js'
26
+ import monquery from 'monquery'
27
+ import sift from 'sift'
28
+ import { sort } from 'fast-sort'
29
+ import JSONfns from 'json-fns'
30
+ import { Task } from './task.js'
31
+ import { newCard } from './card.js'
32
+ import { replaceDateLanguage } from './adapters/parsers/DateLanguageParser.js'
33
+ import { getRawTask, isNumber, LIST_NAME_PATTERN } from './adapters/parsers/task/CardContentParser.js'
34
+ import { interpolate } from './adapters/parsers/content-transformer.js'
35
+ import XRegExp from 'xregexp'
36
+ import { appContext } from './context/ApplicationContext.js'
37
+ import { computeChecksum } from './checksum.js'
38
+ import { getTasksToModify } from './get-tasks-to-modify.js'
39
+ import { logger } from './adapters/logger.js'
40
+ const { ERRORS, ASYNC_LIMIT, DEFAULT_FILE_PATTERN} = constants
41
+ const DEFAULT_SORT = [{ asc: u => isNumber(u.order) ? u.order : Infinity }, { asc: u => u.text }]
42
+
43
+ function getTasksByList (
44
+ repo,
45
+ tasksAry,
46
+ noSort,
47
+ populateFiltered
48
+ ) {
49
+ if (!repo) return []
50
+ var tasks = {}
51
+ var allTasks = noSort ? tasksAry : sort(tasksAry).by(DEFAULT_SORT)
52
+ allTasks.forEach(function (task) {
53
+ if (!tasks[task.list]) tasks[task.list] = []
54
+ tasks[task.list].push(task)
55
+ })
56
+ var lists = repo.getLists()
57
+ lists.forEach((list) => {
58
+ if (list.filter && populateFiltered)
59
+ return Repository.populateFilteredList(list, tasksAry)
60
+ list.tasks = tasks[list.name] || []
61
+ })
62
+ return lists
63
+ }
48
64
 
49
- /**
50
- * A Repository is a file system directory in which to look for tasks in files. The Repository manages all the files
51
- * and lists in it's path.
52
- *
53
- * @constructor
54
- * @param {} path
55
- * @param {} config
56
- * @return
57
- */
58
-
59
- function Repository(_path, config) {
60
- var self = this
61
-
62
- events.EventEmitter.call(this)
63
- // path is the root path of the repository
64
- this.config = config
65
- this.path = _path
66
- this.files = []
67
- this.languages = languages
68
- this.allMeta = {}
69
- this.metaKeys = new Set()
70
- this.allTags = new Set()
71
- this.allTopics = new Set()
72
- this.allContexts = new Set()
65
+ function populateFilteredList (list, tasks) {
66
+ try {
67
+ list.tasks = Repository.query(tasks, list.filter).map((task) => {
68
+ task.filteredListName = list.name
69
+ return task
70
+ })
71
+ } catch (e) {
72
+ list.tasks = []
73
+ }
73
74
  }
74
75
 
75
- util.inherits(Repository, events.EventEmitter)
76
+ function regexQuery (tasks, queryString) {
77
+ return tasks
78
+ .map((task) => {
79
+ const escapedQueryString = tools.escapeRegExp(queryString.trim());
76
80
 
77
- Repository.prototype.init = function (cb) {
78
- cb(null)
79
- }
81
+ // Regex to match occurrences not part of a URL and not already surrounded by `==`
82
+ const regex = new RegExp(
83
+ `(?<!https?:\\/\\/[^\\s]*?)\\b${escapedQueryString}\\b(?![^\\s]*\\/)(?<!==)(?!==)`,
84
+ 'gi'
85
+ );
80
86
 
81
- Repository.prototype.refresh = function (cb) {
82
- cb = tools.cb(cb)
83
- this.config.dirty = true
84
- this.loadConfig((err, config) => {
85
- if (err) {
86
- return cb(err)
87
- }
88
- this.config = config
89
- this.files = []
90
- this.allMeta = {}
91
- this.metaKeys = new Set()
92
- this.allTags = new Set()
93
- this.allTopics = new Set()
94
- this.allContexts = new Set()
95
- this.readFiles((err, files) => {
96
- if (err) {
97
- this.emit('initialized', { ok: false })
98
- return cb(err)
99
- }
100
- this.emit('initialized', { ok: true, lists: this.getTasksByList() })
101
- if (cb) cb(err, files)
102
- })
103
- })
104
- }
87
+ // Regex to detect Markdown links `[title](url)` where `queryString` is in title or URL
88
+ const markdownLinkRegex = new RegExp(
89
+ `\\[([^\\]]*${escapedQueryString}[^\\]]*)\\]\\(([^)]+${escapedQueryString}[^)]*)\\)`,
90
+ 'gi'
91
+ );
105
92
 
106
- /**
107
- * Description
108
- * @method destroy
109
- * @return
110
- */
111
- Repository.prototype.destroy = function () {
112
- this.destroyed = true
113
- this.removeAllListeners()
114
- }
93
+ let match = task.content.match(regex) || task.content.match(markdownLinkRegex);
94
+ task.match = match;
115
95
 
116
- /**
117
- * Description
118
- * @method getId
119
- * @return CallExpression
120
- */
121
- Repository.prototype.getId = function () {
122
- return this.getPath()
123
- }
96
+ if (match && queryString.trim() && task.interpretedContent) {
97
+ // Replace standalone occurrences
98
+ task.interpretedContent = task.interpretedContent.replace(
99
+ regex,
100
+ '==$&=='
101
+ );
124
102
 
125
- Repository.prototype.getProject = function () {
126
- return this.project
127
- }
103
+ // Replace Markdown links where queryString is in title or URL
104
+ task.interpretedContent = task.interpretedContent.replace(
105
+ markdownLinkRegex,
106
+ (match) => `==${match}==`
107
+ );
128
108
 
129
- Repository.prototype.getDisplayName = function () {
130
- return path.basename(this.path)
131
- }
109
+ task.updateDescriptionData();
110
+ }
132
111
 
133
- Repository.prototype.emitFileUpdate = function (file, force) {
134
- if (force || this.shouldEmitFileUpdate(file)) this.emit('file.update', file)
135
- }
112
+ return task;
113
+ })
114
+ .filter(({ match }) => match);
115
+ };
136
116
 
137
- Repository.prototype.shouldEmitFileUpdate = function (file) {
138
- if (this.moving) return
139
- if (this.lastMovedFiles) {
140
- var index = this.lastMovedFiles.indexOf(file)
141
- if (index > -1) {
142
- this.lastMovedFiles.splice(index, 1)
143
- } else {
144
- if (file && file.updated) return true
117
+
118
+ function parseHideListsFromQueryString (queryString) {
119
+ const hideLists = []
120
+ const regex = `hide:(\\s*((${LIST_NAME_PATTERN},?\\s?)+))?`
121
+ queryString = queryString.replaceAll(
122
+ // hide lists example `hide: list1, list2`
123
+ new XRegExp(regex, 'g'),
124
+ (match, allLists, lists) => {
125
+ lists && lists.split(',').forEach((list) => {
126
+ hideLists.push(list.trim())
127
+ })
128
+ return ''
145
129
  }
146
- } else {
147
- if (file && file.updated) return true
130
+ ).trim()
131
+ return {
132
+ hideLists,
133
+ queryString,
148
134
  }
149
135
  }
150
136
 
151
- Repository.prototype.emitConfigUpdate = function (data) {
152
- if (this.savingConfig) return
153
- process.nextTick(() => {
154
- this.emit('config.update', data)
155
- })
156
- }
157
-
158
- /**
159
- * Description
160
- * @method createListeners
161
- * @return
162
- */
163
- Repository.prototype.createListeners = function () {
164
- if (this.taskListener) return
165
- var self = this
166
- /**
167
- * Description
168
- * @method taskFoundListener
169
- * @param {} task
170
- * @return
171
- */
172
- this.taskListener = function (event, task) {
173
- if (!self.listExists(task.list) && self.config.includeList(task.list)) {
174
- const list = new List({ name: task.list })
175
- self.addList(list)
176
- self.emit('list.found', list)
177
- self.saveConfig()
137
+ function parseSortFromQueryString (queryString) {
138
+ const sort = []
139
+ queryString = queryString.replace(
140
+ /\s([+-])([A-Za-z.]+)/g,
141
+ (match, order, attr) => {
142
+ const direction = order === '+' ? 'asc' : 'desc'
143
+ const sortString = `{ "${direction}": "function (o) { return o.${attr};}" }`
144
+ sort.push(JSONfns.parse(sortString))
145
+ return ''
178
146
  }
179
- Object.keys(task.allMeta).forEach(key => self.metaKeys.add(key))
180
- task.topics.forEach(topic => self.allTopics.add(topic))
181
- task.allTags.forEach(tag => self.allTags.add(tag))
182
- task.allContext.forEach(context => self.allContexts.add(context))
183
- self.allMeta = self.addAllMeta(task.allMeta)
184
- self.emit(event, task)
147
+ )
148
+ return {
149
+ sort,
150
+ queryString,
185
151
  }
152
+ }
186
153
 
187
- this.taskFoundListener = function (task) {
188
- self.taskListener('task.found', task)
154
+ function parseSortFromMongoQuery (mongoQuery) {
155
+ const sort = []
156
+ for (const [key, value] of Object.entries(mongoQuery.sort)) {
157
+ if (value > 0) sort.push({ asc: key })
158
+ else sort.push({ desc: key })
189
159
  }
160
+ return sort
161
+ }
190
162
 
191
- this.taskModifiedListener = function (task) {
192
- self.taskListener('task.modified', task)
163
+ // function sortByQuery (tasks, queryString = '') {
164
+ // queryString = replaceDateLanguage(queryString)
165
+ // let { sort } = Repository.parseSortFromQueryString(queryString)
166
+ // if (!sort || sort.length === 0) sort = DEFAULT_SORT
167
+ // sort(tasks).by(sort)
168
+ // }
169
+
170
+ function filterCards (tasks, _queryString = '') {
171
+ let query
172
+ _queryString = replaceDateLanguage(_queryString)
173
+ let { sort: userSort, queryString } = Repository.parseSortFromQueryString(_queryString);
174
+ const hideListsQuery = Repository.parseHideListsFromQueryString(queryString);
175
+ queryString = hideListsQuery.queryString;
176
+ const hideLists = hideListsQuery.hideLists;
177
+ try {
178
+ query = monquery(queryString)
179
+ } catch (e) {
180
+ log(`Unable to parse ${_queryString} using monquery`)
181
+ }
182
+ let result = []
183
+ if (query) result = tasks.filter(sift.default(query))
184
+ if (!query || result.length === 0) {
185
+ result = Repository.regexQuery(tasks, queryString)
186
+ }
187
+ if (!userSort || userSort.length === 0) userSort = DEFAULT_SORT
188
+ const sorted = sort(result).by(userSort)
189
+ return {
190
+ result: sorted,
191
+ query,
192
+ sort: userSort,
193
+ hideLists
193
194
  }
194
195
  }
195
196
 
196
- Repository.prototype.addAllMeta = function (meta) {
197
- const allMeta = Object.assign({}, this.allMeta)
198
- Object.keys(meta).forEach((key) => {
199
- if (!allMeta[key]) {
200
- allMeta[key] = meta[key]
201
- return
197
+ function query (tasks, queryString) {
198
+ return Repository.filterCards(tasks, queryString).result
199
+ }
200
+
201
+ function replaceDatesInQuery (query) {
202
+ return Repository.filterObjectValues(query, (key, value) => {
203
+ let date = Date.parse(value)
204
+ if (date && /(\d{4})-(\d{2})-(\d{2})/.test(value)) {
205
+ return date
202
206
  }
203
- allMeta[key] = _union(allMeta[key], meta[key])
207
+ return value
204
208
  })
205
- return allMeta
206
209
  }
207
210
 
208
- /**
209
- * Description
210
- * @method addList
211
- * @param {} list
212
- * @return
213
- */
214
- Repository.prototype.addList = async function (list, cb) {
215
- cb = tools.cb(cb);
216
- if (this.listExists(list.name)) return cb();
217
-
218
- try {
219
- list = _omit(list, 'tasks');
220
- this.config.lists.push(new List(list));
221
-
222
- if (!list.filter && !/[a-z]/.test(list.name)) {
223
- const codeList = list.name.replace(/\s+/g, '-').toUpperCase();
224
- if (!this.config.code.include_lists.find((name) => name === codeList)) {
225
- this.config.code.include_lists.push(codeList);
211
+ function filterObjectValues (o, cb) {
212
+ if (o && typeof o === 'object') {
213
+ for (const [key, value] of Object.entries(o)) {
214
+ if (value && typeof value === 'object') {
215
+ Repository.filterObjectValues(value, cb)
216
+ } else {
217
+ o[key] = cb(key, value)
226
218
  }
227
219
  }
220
+ }
221
+ return o
222
+ }
228
223
 
229
- await promisify(this.saveConfig).bind(this)();
230
- this.emit('list.modified', list);
231
- cb();
232
- } catch (err) {
233
- cb(err);
224
+ // Emits task.found, list.found, file.update and file.delete, file.processed, files.saved
225
+ export class Repository extends Emitter {
226
+
227
+ constructor(_path, config) {
228
+ super()
229
+ this.config = config
230
+ this.path = _path
231
+ this.files = []
232
+ this.languages = languages
233
+ this.allMeta = {}
234
+ this.metaKeys = new Set()
235
+ this.allTags = new Set()
236
+ this.allTopics = new Set()
237
+ this.allContexts = new Set()
234
238
  }
235
- };
236
239
 
237
- Repository.prototype.removeList = function (list, cb) {
238
- return new Promise((resolve, reject) => {
239
- var fn = (err) => {
240
- if (err) return cb && cb(err) || reject(err)
241
- this.emit('list.modified', list)
242
- resolve()
243
- }
244
- if (!this.listExists(list)) return fn()
240
+ static getTasksByList = getTasksByList
245
241
 
246
- var lists = _reject(this.getLists(), { name: list })
247
- if (this.config.code && this.config.code.include_lists) {
248
- this.config.code.include_lists = _reject(this.config.code.include_lists, list)
249
- }
250
- this.setLists(lists)
251
- this.saveConfig(fn)
252
- })
253
- }
242
+ static populateFilteredList = populateFilteredList
243
+
244
+ static regexQuery = regexQuery
254
245
 
255
- /**
256
- * Description
257
- * @method getPath
258
- * @return MemberExpression
259
- */
260
- Repository.prototype.getPath = function () {
261
- return this.path
262
- }
246
+ static parseHideListsFromQueryString = parseHideListsFromQueryString
263
247
 
264
- /**
265
- * Description
266
- * @method getConfig
267
- * @return MemberExpression
268
- */
269
- Repository.prototype.getConfig = function () {
270
- return this.config
271
- }
248
+ static parseSortFromQueryString = parseSortFromQueryString
272
249
 
273
- /**
274
- * Description
275
- * @method getLists
276
- * @return MemberExpression
277
- */
278
- Repository.prototype.getLists = function () {
279
- const config = this.getConfig()
280
- return config ? config.lists.map(obj => ({...obj})) : []
281
- }
250
+ static parseSortFromMongoQuery = parseSortFromMongoQuery
282
251
 
283
- Repository.prototype.getVisibleLists = function () {
284
- return _reject(this.getLists(), 'hidden')
285
- }
252
+ // static sortByQuery = sortByQuery
286
253
 
287
- Repository.prototype.isListVisible = function (name) {
288
- return this.getVisibleLists.find((list) => list.name === name)
289
- }
254
+ static filterCards = filterCards
290
255
 
291
- /**
292
- * Description
293
- * @method setLists
294
- * @param {} lists
295
- * @return ThisExpression
296
- */
297
- Repository.prototype.setLists = function (lists) {
298
- this.config.lists = lists.map((list) => {
299
- return new List(list)
300
- })
301
- return this
302
- }
256
+ static query = query
303
257
 
304
- /**
305
- * Description
306
- * @method listExists
307
- * @param {} name
308
- * @return BinaryExpression
309
- */
310
- Repository.prototype.listExists = function (name) {
311
- return this.getConfig().listExists(name)
312
- }
258
+ static replaceDatesInQuery = replaceDatesInQuery
313
259
 
314
- /**
315
- * Save the config file (Implemented in mixins)
316
- *
317
- * @method saveConfig
318
- * @param {} cb
319
- * @return
320
- */
321
- Repository.prototype.saveConfig = function (cb) {
322
- inMixinsNoop(cb)
323
- }
260
+ static filterObjectValues = filterObjectValues
324
261
 
325
- /**
326
- * Load the config file (Implemented in mixins)
327
- *
328
- * @method loadConfig
329
- * @return MemberExpression
330
- */
331
- Repository.prototype.loadConfig = function (cb) {
332
- inMixinsNoop(cb)
333
- }
262
+ init () {
263
+ inMixinsNoop()
264
+ }
334
265
 
335
- Repository.prototype.migrateTasksByConfig = function (
336
- oldConfig,
337
- newConfig,
338
- cb
339
- ) {
340
- if (!oldConfig || !newConfig) return cb()
341
- const oldMetaSep = oldConfig.getMetaSep()
342
- const newMetaSep = newConfig.getMetaSep()
343
- if (oldMetaSep === newMetaSep) return cb()
344
- async.eachLimit(
345
- this.getFiles(),
346
- ASYNC_LIMIT,
347
- (file, cb) => {
348
- const tasks = fastSort(file.tasks).desc(u => u.line)
349
- async.eachSeries(
350
- tasks,
351
- (task, cb) => {
352
- if (!Task.isTask(task)) return cb()
353
- task.replaceMetaSep(oldMetaSep, newMetaSep)
354
- this.modifyTask(task, false, cb)
355
- },
356
- (err) => {
357
- if (err) return cb(err)
358
- if (!file.isModified() || file.getContent().trim() === '') return cb()
359
- this.writeFile(file, (err, file) => {
360
- this.resetFile(file)
361
- if (err) return cb(err)
362
- cb(null, file)
363
- })
364
- }
365
- )
366
- },
367
- cb
368
- )
369
- }
266
+ // READY Refactor to use async/await
267
+ // #esm-migration #urgent #important
268
+ // <!--
269
+ // order:-240
270
+ // -->
271
+ async refresh () {
272
+ this.files = []
273
+ this.allMeta = {}
274
+ this.metaKeys = new Set()
275
+ this.allTags = new Set()
276
+ this.allTopics = new Set()
277
+ this.allContexts = new Set()
278
+ this.config.dirty = true
370
279
 
371
- /**
372
- * Get the full path from a relative path
373
- *
374
- * @method getFullPath
375
- * @param {} file
376
- * @return String
377
- */
378
- Repository.prototype.getFullPath = function (file) {
379
- if (File.isFile(file)) file = file.path
380
- if (file.indexOf(this.path) === 0) return file
381
- try {
382
- var fullPath = path.join(this.path, file)
383
- return fullPath
384
- } catch (e) {
385
- throw new Error(
386
- util.format(
387
- 'Error getting full path for file:%s and repo path:%s',
388
- file,
389
- this.path
390
- )
391
- )
280
+ let files = await this.getFilesInPath(false)
281
+ try {
282
+ const config = await this.loadConfig()
283
+ this.config = config
284
+ files = await this.readFiles()
285
+ this.emit('initialized', { ok: true, lists: this.getTasksByList() })
286
+ } catch (err) {
287
+ this.emit('initialized', { ok: false })
288
+ throw err
289
+ }
290
+ return files
392
291
  }
393
- }
394
292
 
395
- /**
396
- * Get the relative path from repository root
397
- *
398
- * @method getRelativePath
399
- * @param {} fullPath
400
- * @return String
401
- */
402
- Repository.prototype.getRelativePath = function (fullPath) {
403
- if (path.sep === '\\') {
404
- fullPath = fullPath.replace(/\//g, path.sep)
405
- }
406
- if (!fullPath.startsWith(this.path)) return fullPath
407
- try {
408
- var relPath = path.relative(this.path, fullPath)
409
- return relPath
410
- } catch (e) {
411
- throw new Error(
412
- util.format(
413
- 'Error getting relative path for file:%s and repo path:%s',
414
- fullPath,
415
- this.path
416
- )
417
- )
293
+ /**
294
+ * Description
295
+ * @method destroy
296
+ * @return
297
+ */
298
+ async destroy () {
299
+ this.destroyed = true
300
+ this.removeAllListeners()
418
301
  }
419
- }
420
302
 
421
- /**
422
- * Is this file OK? Implemented in mixins
423
- *
424
- * @method fileOK
425
- * @param {} file
426
- * @param {} includeDirs
427
- * @return stat
428
- */
429
- Repository.prototype.fileOK = function (file, includeDirs, cb) {
430
- if (_isFunction(includeDirs)) cb = includeDirs
431
- cb(null, true)
432
- }
303
+ /**
304
+ * Description
305
+ * @method getId
306
+ * @return CallExpression
307
+ */
308
+ getId () {
309
+ return this.getPath()
310
+ }
433
311
 
434
- Repository.prototype.setIgnores = function (ignores) {
435
- // console.info('ignore patterns:', ignores)
436
- this.ignorePatterns = ignores
437
- this.ignore = ignore().add(ignores)
438
- }
312
+ getProject () {
313
+ return this.project
314
+ }
439
315
 
440
- /**
441
- * Should the relative path be included.
442
- *
443
- * @method shouldInclude
444
- * @param {} relPath
445
- * @return exclude
446
- */
447
- Repository.prototype.shouldInclude = function (relPath) {
448
- relPath = this.getRelativePath(relPath)
449
- let include = true
450
- if (this.config.markdownOnly && relPath) {
451
- include = File.isMarkdown(relPath)
452
- }
453
- if (this.ignore) {
454
- try {
455
- include = include && (relPath === '' || !this.ignore.ignores(relPath))
456
- } catch (e) {
457
- console.error(
458
- `Failed to check ignore status for dir: [${relPath}] in: [${this.path}]. It will be included.`,
459
- e
460
- )
461
- }
316
+ getDisplayName () {
317
+ return path.basename(this.path)
462
318
  }
463
319
 
464
- return include
465
- }
320
+ emitFileUpdate (file, force) {
321
+ if (force || this.shouldEmitFileUpdate(file)) this.emit('file.update', file)
322
+ }
466
323
 
467
- /**
468
- * Add or replace a file in the files reference array
469
- *
470
- * @method addFile
471
- * @param {} file
472
- * @return MemberExpression
473
- */
474
- Repository.prototype.addFile = function (file, cb) {
475
- if (this.destroyed) return cb(new Error('destroyed'))
476
- var self = this
477
- this.fileOK(file, function (err, ok) {
478
- if (err) return cb(err)
479
- if (ok) {
480
- var index = self.files.findIndex(({ path }) => path === file.path)
324
+ shouldEmitFileUpdate (file) {
325
+ if (this.moving) return
326
+ if (this.lastMovedFiles) {
327
+ var index = this.lastMovedFiles.indexOf(file)
481
328
  if (index > -1) {
482
- self.files[index] = file
329
+ this.lastMovedFiles.splice(index, 1)
483
330
  } else {
484
- self.files.push(file)
331
+ if (file && file.updated) return true
485
332
  }
333
+ } else {
334
+ if (file && file.updated) return true
486
335
  }
487
- cb(err, self.files)
488
- })
489
- }
490
-
491
- /**
492
- * Remove a file from the files refrence array
493
- *
494
- * @method removeFile
495
- * @param {} file
496
- * @return MemberExpression
497
- */
498
- Repository.prototype.removeFile = function (file) {
499
- if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
500
- _remove(this.files, f => f.path === file.path)
501
-
502
- return this.files
503
- }
504
-
505
- /**
506
- * Description
507
- * @method getFile
508
- * @param {} path
509
- * @return CallExpression
510
- */
511
- Repository.prototype.getFile = function (path) {
512
- path = this.getRelativePath(path)
513
- return this.files.find((file) => file.path === path)
514
- }
336
+ }
515
337
 
516
- Repository.prototype.getTask = function (id) {
517
- if (Task.isTask(id)) {
518
- let task = id
519
- if (!task.meta.id) return
520
- return this.getTasks().find((existingTask) => {
521
- return existingTask.meta.id && existingTask.meta.id[0] == task.meta.id[0]
338
+ emitConfigUpdate (data) {
339
+ if (this.savingConfig) return
340
+ process.nextTick(() => {
341
+ this.emit('config.update', data)
522
342
  })
523
- } else {
524
- let task = this.getTasks().find((task) => task.id === id)
525
- if (!task) return
526
- return this.getFile(task.source.path)
527
- .getTasks()
528
- .find((task) => task.id === id)
529
343
  }
530
- }
531
-
532
- /**
533
- * Description
534
- * @method getFileForTask
535
- * @param {} task
536
- * @return CallExpression
537
- */
538
- Repository.prototype.getFileForTask = function (task) {
539
- return this.getFile(task.getSource().path)
540
- }
541
-
542
- /**
543
- * Descriptione
544
- * @method getFiles
545
- * @param {} paths
546
- * @return CallExpression
547
- */
548
- Repository.prototype.getFiles = function (paths) {
549
- if (_isUndefined(paths)) return fastSort(this.files).asc(u => u.path)
550
- return this.files.filter((file) => {
551
- return paths.includes(file.path)
552
- })
553
- }
554
344
 
555
- Repository.prototype.getFilesWithTasks = function () {
556
- const files = this.files.filter((file) => file.getTasks().length > 0)
557
- return fastSort(files).asc(u => u.path)
558
- }
345
+ // READY Refactor createListeners to use async/await
346
+ // #esm-migration
347
+ // <!--
348
+ // order:-120
349
+ // -->
350
+ createListeners () {
351
+ if (this.taskListener) return
352
+
353
+ this.taskListener = async (event, task) => {
354
+ if (!this.listExists(task.list) && this.config.includeList(task.list)) {
355
+ const list = new List({ name: task.list })
356
+ await this.addList(list)
357
+ this.emit('list.found', list)
358
+ await this.saveConfig()
359
+ }
360
+ Object.keys(task.allMeta).forEach(key => this.metaKeys.add(key))
361
+ task.topics.forEach(topic => this.allTopics.add(topic))
362
+ task.allTags.forEach(tag => this.allTags.add(tag))
363
+ task.allContext.forEach(context => this.allContexts.add(context))
364
+ this.allMeta = this.addAllMeta(task.allMeta)
365
+ this.emit(event, task)
366
+ }
559
367
 
560
- Repository.prototype.resetFile = function (file) {
561
- file.reset()
562
- file.removeListener('task.found', this.taskFoundListener)
563
- file.removeListener('task.modified', this.taskModifiedListener)
564
- }
368
+ this.taskFoundListener = (task) => {
369
+ this.taskListener('task.found', task)
370
+ }
565
371
 
566
- Repository.prototype.extractTasks = function (file, cb) {
567
- cb = tools.cb(cb)
568
- var self = this
569
- var extract = (file) => {
570
- file.on('task.found', self.taskFoundListener)
571
- file.on('task.modified', self.taskModifiedListener)
572
- const fileContent = file.content
573
- file.extractAndTransformTasks(self.getConfig())
574
- if (!file.isModified() || fileContent === file.content) {
575
- this.resetFile(file)
576
- return cb(null, file)
372
+ this.taskModifiedListener = (task) => {
373
+ this.taskListener('task.modified', task)
577
374
  }
578
- file.extractTasks(self.getConfig())
579
- self.writeFile(file, (err, file) => {
580
- this.resetFile(file)
581
- if (err) return cb(err)
582
- cb(null, file)
583
- })
584
375
  }
585
376
 
586
- if (file.content === null) {
587
- this.readFileContent(file, function (err, file) {
588
- if (err) return cb(err)
589
- extract(file)
377
+ addAllMeta (meta) {
378
+ const allMeta = Object.assign({}, this.allMeta)
379
+ Object.keys(meta).forEach((key) => {
380
+ if (!allMeta[key]) {
381
+ allMeta[key] = meta[key]
382
+ return
383
+ }
384
+ allMeta[key] = _union(allMeta[key], meta[key])
590
385
  })
591
- } else extract(file)
592
- }
593
-
594
- /**
595
- * Implemented in mixins
596
- * @method writeFile
597
- * @param {} file
598
- * @param {} cb
599
- * @return
600
- */
601
- Repository.prototype.writeFile = function (file, cb) {
602
- inMixinsNoop(cb)
603
- }
604
-
605
- /**
606
- * Implemented in mixins
607
- * @method getFilesInPath
608
- * @param {} includeDirs
609
- * @return CallExpression
610
- */
611
-
612
- Repository.prototype.getFilesInPath = function (includeDirs, cb) {
613
- inMixinsNoop(cb)
614
- }
386
+ return allMeta
387
+ }
615
388
 
616
- /**
617
- * Implemented in mixins
618
- * @method readFileContent
619
- * @param {} file
620
- * @param {} cb
621
- * @return
622
- */
623
- Repository.prototype.readFileContent = function (file, cb) {
624
- inMixinsNoop(cb)
625
- }
389
+ /**
390
+ * Description
391
+ * @method addList
392
+ * @param {} list
393
+ * @return
394
+ */
395
+ async addList (list) {
396
+ if (this.listExists(list.name)) return;
626
397
 
627
- /**
628
- * Description
629
- * @method readFile
630
- * @param {} file
631
- * @param {} cb
632
- * @return
633
- */
634
- Repository.prototype.readFile = function (file, cb) {
635
- const checksum = this.checksum || function () {}
636
- cb = tools.cb(cb)
637
- if (!File.isFile(file)) return cb(new Error(ERRORS.NOT_A_FILE))
638
- if (file.deleted) return cb(null, file)
639
- var self = this
640
-
641
- var currentChecksum = file.checksum
642
- const filePath = file.path
643
- if (!/\.\.(\/|\\)/.test(filePath)) {
644
- this.readFileContent(file, (err, file) => {
645
- if (err) return cb(new Error('Unable to read file:' + filePath))
646
- // BACKLOG Allow user to assign a module for content transformation
647
- // <!--
648
- // order:-800
649
- // -->
650
- file.checksum = checksum(file.getContent())
651
- file.updated = currentChecksum !== file.checksum
652
- if (file.updated)
653
- self.extractTasks(file, (err) => {
654
- if (err) return cb(err)
655
- self.addFile(file, (err) => {
656
- if (err) log('error:', err)
657
- log(`File added. repo: ${self.getPath()} file-path: ${file.path}`)
658
- if (err) return cb(err)
659
- cb(null, file)
660
- })
661
- })
662
- else cb(null, file)
663
- })
664
- } else return cb(new Error('Unable to read file:' + file))
665
- }
398
+ list = _omit(list, 'tasks');
399
+ this.config.lists.push(new List(list));
666
400
 
667
- /**
668
- * Description
669
- * @method readFiles
670
- * @param {} files
671
- * @param {} cb
672
- * @return
673
- */
674
- Repository.prototype.readFiles = function (files, cb) {
675
- if (arguments.length === 0) {
676
- cb = _noop
677
- files = undefined
678
- } else if (_isFunction(files)) {
679
- cb = files
680
- files = undefined
681
- } else if (files && !Array.isArray(files))
682
- return cb(new Error('files must be an array of files or undefined'))
683
-
684
- cb = tools.cb(cb)
685
-
686
- var self = this
687
-
688
- this.allMeta = {}
689
- this.metaKeys = new Set()
690
- this.allTags = new Set()
691
- this.allContexts = new Set()
692
-
693
- var exec = function (files) {
694
- var completed = 0
695
- if (files.length < 1) return cb(null, [])
696
- async.eachLimit(
697
- files,
698
- ASYNC_LIMIT,
699
- function (file, cb) {
700
- self.emit('file.reading', { path: file.path })
701
- self.readFile(file, function (err, file) {
702
- if (err) return cb(err)
703
- completed++
704
- self.emit('file.read', {
705
- path: file.path,
706
- completed: completed,
707
- })
708
- cb()
709
- })
710
- },
711
- function (err) {
712
- cb(err, files)
401
+ if (!list.filter && !/[a-z]/.test(list.name)) {
402
+ const codeList = list.name.replace(/\s+/g, '-').toUpperCase();
403
+ if (!this.config.code.include_lists.find((name) => name === codeList)) {
404
+ this.config.code.include_lists.push(codeList);
713
405
  }
714
- )
715
- }
716
-
717
- if (files === undefined) {
718
- if (this.files && this.files.length > 0) {
719
- exec(this.files)
720
- } else {
721
- this.getFilesInPath(false, (err, files) => {
722
- if (err) return cb(err)
723
- self.files = files
724
- const filesToInclude = files.map((file) => file.path)
725
- const fileStats = {
726
- count: filesToInclude.length,
727
- files: filesToInclude,
728
- }
729
- this.emit('files.found', fileStats)
730
- exec(files)
731
- })
732
406
  }
733
- } else exec(files)
734
- }
735
-
736
- /**
737
- * Implemented in mixins
738
- * @method deleteFile
739
- * @param {} path
740
- * @param {} cb
741
- * @return
742
- */
743
- Repository.prototype.deleteFile = function (path, cb) {
744
- inMixinsNoop(cb)
745
- }
746
-
747
- /**
748
- * Description
749
- * @method hasDefaultFile
750
- * @return CallExpression
751
- */
752
- Repository.prototype.hasDefaultFile = function () {
753
- return _some(this.getFiles(), function (file) {
754
- var regex = new RegExp(DEFAULT_FILE_PATTERN, 'i')
755
- return regex.test(file.path)
756
- })
757
- }
758
-
759
- /**
760
- * Description
761
- * @method getDefaultFile
762
- * @return file
763
- */
764
- Repository.prototype.getDefaultFile = function () {
765
- var files = fastSort(this.getFiles()).asc(u => u.path)
766
- var file = files.reverse().find((file) => {
767
- var regex = new RegExp(DEFAULT_FILE_PATTERN, 'i')
768
- return regex.test(file.path)
769
- })
770
- return file
771
- }
772
-
773
- Repository.prototype.getList = function (name) {
774
- return this.getLists().find((list) => list.name === name)
775
- }
776
407
 
408
+ await this.saveConfig()
409
+ this.emit('list.modified', list);
410
+ };
777
411
 
778
- Repository.prototype.getListById = function(id, lists = this.getLists()) {
779
- return lists.find((list) => list.id === id)
780
- }
412
+ // READY Refactor removeList to use async/await
413
+ // #esm-migration
414
+ // <!--
415
+ // order:-220
416
+ // -->
417
+ async removeList (list) {
418
+ if (!this.listExists(list)) return
781
419
 
782
- Repository.prototype.hideList = function (name, cb) {
783
- var self = this
784
- cb = tools.cb(cb)
785
- var fn = function (err) {
786
- if (!err) self.emit('list.modified', name)
787
- cb(err)
420
+ var lists = _reject(this.getLists(), { name: list })
421
+ if (this.config.code && this.config.code.include_lists) {
422
+ this.config.code.include_lists = _reject(this.config.code.include_lists, list)
423
+ }
424
+ this.setLists(lists)
425
+ await this.saveConfig()
426
+ this.emit('list.modified', list)
788
427
  }
789
428
 
790
- var list = this.getList(name)
791
- if (list) {
792
- list.hidden = true
793
- this.saveConfig(fn)
794
- } else cb()
795
- }
429
+ /**
430
+ * Description
431
+ * @method getPath
432
+ * @return MemberExpression
433
+ */
434
+ getPath () {
435
+ return this.path
436
+ }
796
437
 
797
- Repository.prototype.showList = function (name, cb) {
798
- var self = this
799
- cb = tools.cb(cb)
800
- var fn = function (err) {
801
- if (!err) self.emit('list.modified', name)
802
- cb(err)
438
+ /**
439
+ * Description
440
+ * @method getConfig
441
+ * @return MemberExpression
442
+ */
443
+ getConfig () {
444
+ return this.config
803
445
  }
804
446
 
805
- var list = this.getList(name)
806
- if (list) {
807
- list.hidden = false
808
- this.saveConfig(fn)
809
- } else cb()
810
- }
447
+ /**
448
+ * Description
449
+ * @method getLists
450
+ * @return MemberExpression
451
+ */
452
+ getLists () {
453
+ const config = this.getConfig()
454
+ return config ? config.lists.map(obj => ({...obj})) : []
455
+ }
811
456
 
812
- /**
813
- * Description
814
- * @method moveList
815
- * @param {} name
816
- * @param {} pos
817
- * @param {} cb
818
- * @return
819
- */
820
- Repository.prototype.moveList = function (name, pos, cb) {
821
- var self = this
822
- cb = tools.cb(cb)
823
- var list = this.getList(name)
824
- // Only modify the lists if the list name exists
825
- var fn = function (err) {
826
- if (!err) self.emit('list.modified', name)
827
- cb(err)
828
- }
829
-
830
- if (list) {
831
- _remove(this.getLists(), { name: name })
832
- this.getLists().splice(pos, 0, list)
833
- this.saveConfig(fn)
834
- } else cb()
835
- }
457
+ getVisibleLists () {
458
+ return _reject(this.getLists(), 'hidden')
459
+ }
836
460
 
837
- Repository.prototype.toggleListIgnore = async function (name) {
838
- return new Promise(async (resolve, reject) => {
839
- var self = this
840
- var list = this.getList(name)
841
- if (!list) return reject(new Error('List not found'))
842
- list.ignore = !list.ignore
843
- await this.updateList(list.id, list)
844
- this.saveConfig((err) => {
845
- if (err) reject(err)
846
- self.emit('list.modified', name)
847
- resolve()
848
- })
849
- });
850
- }
461
+ isListVisible (name) {
462
+ return this.getVisibleLists.find((list) => list.name === name)
463
+ }
851
464
 
852
- Repository.prototype.toggleList = async function (name) {
853
- return new Promise(async (resolve, reject) => {
854
- var self = this
855
- var list = this.getList(name)
856
- if (!list) return reject(new Error('List not found'))
857
- list.hidden = !list.hidden
858
- await this.updateList(list.id, list)
859
- this.saveConfig((err) => {
860
- if (err) reject(err)
861
- self.emit('list.modified', name)
862
- resolve()
465
+ /**
466
+ * Description
467
+ * @method setLists
468
+ * @param {} lists
469
+ * @return ThisExpression
470
+ */
471
+ setLists (lists) {
472
+ this.config.lists = lists.map((list) => {
473
+ return new List(list)
863
474
  })
864
- });
865
- }
866
-
867
- Repository.prototype.updateList = function(id, {name, hidden, ignore, filter}) {
868
- return new Promise((resolve, reject) => {
869
- const lists = this.getLists()
870
- const list = this.getListById(id, lists)
871
- const oldName = list.name
872
- const hasFilter = list.filter
873
- list.name = name
874
- list.hidden = hidden
875
- list.ignore = ignore
876
- if (hasFilter !== undefined) list.filter = filter
877
- this.setLists(lists)
878
-
879
- if (oldName === name || hasFilter) {
880
- this.saveConfig(err => {
881
- if (err) return reject(err)
882
- resolve()
883
- })
884
- } else {
885
- this.moveTasksBetweenLists(oldName, name, err => {
886
- if (err) return reject(err)
887
- resolve()
888
- })
889
- }
890
- })
891
- }
892
-
893
- Repository.prototype.moveTasksBetweenLists = function(oldName, newName, cb) {
894
- cb = tools.cb(cb)
895
- // Modify the tasks
896
- const tasksToModify = this.getTasksInList(oldName)
475
+ return this
476
+ }
897
477
 
898
- const cbfn = (err) => {
899
- this.moving = false
900
- if (err) return cb(err)
901
- this.saveConfig((err) => {
902
- if (err) return cb(err)
903
- cb(null, { oldName, newName })
904
- })
478
+ /**
479
+ * Description
480
+ * @method listExists
481
+ * @param {} name
482
+ * @return BinaryExpression
483
+ */
484
+ listExists (name) {
485
+ return this.getConfig().listExists(name)
905
486
  }
906
487
 
907
- const tasksByFile = {} // path: [files]
908
- tasksToModify.forEach((task) => {
909
- const filePath = task.path
910
- if (!tasksByFile[filePath]) {
911
- tasksByFile[filePath] = { file: this.getFileForTask(task), tasks: [] }
912
- }
913
- tasksByFile[filePath].tasks.push(task)
914
- })
915
- const modifyTasks = Object.values(tasksByFile).map(({ file, tasks }) => {
916
- return (cb) => {
917
- this.readFileContent(file, (err) => {
918
- if (err) return cb(err)
919
- try {
920
- tasks.forEach((task) => {
921
- task.list = newName
922
- file.modifyTask(task, this.getConfig(), true)
923
- })
924
- } catch (err) {
925
- return cb(err)
926
- }
927
- cb()
928
- })
929
- }
930
- })
488
+ /**
489
+ * Save the config file (Implemented in mixins)
490
+ *
491
+ * @method saveConfig
492
+ * @return
493
+ */
494
+ async saveConfig () {
495
+ inMixinsNoop()
496
+ }
931
497
 
932
- // BACKLOG Let's use saveModifiedFiles here id:15 gh:100 +fix
498
+ /**
499
+ * Load the config file (Implemented in mixins)
500
+ *
501
+ * @method loadConfig
502
+ * @return MemberExpression
503
+ */
504
+ // READY Refactor loadConfig to use async/await
505
+ // #esm-migration #urgent
933
506
  // <!--
934
- // order:-810
507
+ // order:-160
935
508
  // -->
509
+ async loadConfig () {
510
+ inMixinsNoop()
511
+ }
936
512
 
937
- this.moving = true
938
- if (modifyTasks.length > 0) {
939
- async.parallel(modifyTasks, (err) => {
940
- if (err) return cb(err)
941
-
942
- var fns = Object.values(tasksByFile).map(({ file }) => {
943
- return (cb) => {
944
- this.writeFile(file, cb)
945
- }
946
- })
947
-
948
- if (fns.length > 0) {
949
- async.parallel(fns, cbfn)
950
- } else {
951
- cbfn()
952
- }
953
- })
954
- } else cbfn()
955
- }
956
-
957
- Repository.prototype.writeAndExtract = function (file, emit, cb) {
958
- this.writeFile(file, emit, (err, file) => {
959
- if (err) return cb(new Error('Unable to write file:' + file.path))
960
- this.extractTasks(file, (err, file) => {
961
- if (err) {
962
- return cb(new Error('Unable to extract tasks for file:' + file.path))
963
- }
964
-
965
- this.addFile(file, (err) => {
966
- if (err) {
967
- err.message = `Unable to add file after extracting tasks: #{file.path}\n${err.message}`
968
- return cb(err)
513
+ async migrateTasksByConfig (oldConfig, newConfig) {
514
+ return new Promise((resolve, reject) => {
515
+ if (!oldConfig || !newConfig) return resolve()
516
+ const oldMetaSep = oldConfig.getMetaSep()
517
+ const newMetaSep = newConfig.getMetaSep()
518
+ if (oldMetaSep === newMetaSep) return resolve()
519
+ eachLimit(
520
+ this.getFiles(),
521
+ ASYNC_LIMIT,
522
+ (file, cb) => {
523
+ const tasks = sort(file.tasks).desc(u => u.line)
524
+ eachSeries(
525
+ tasks,
526
+ async (task, cb) => {
527
+ if (!Task.isTask(task)) return cb()
528
+ try {
529
+ task.replaceMetaSep(oldMetaSep, newMetaSep)
530
+ await this.modifyTask(task, false)
531
+ cb()
532
+ } catch (err) {
533
+ cb(err)
534
+ }
535
+ },
536
+ async (err) => {
537
+ if (err) return cb(err)
538
+ if (!file.isModified() || file.getContent().trim() === '') return cb()
539
+ try {
540
+ const file = await this.writeFile(file)
541
+ this.resetFile(file)
542
+ cb(null, file)
543
+ } catch (err) {
544
+ cb(err)
545
+ }
546
+ }
547
+ )
548
+ },
549
+ (err) => {
550
+ if (err) return reject(err)
551
+ resolve()
969
552
  }
970
- cb(null, file)
971
- })
972
- })
973
- })
974
- }
975
-
976
- Repository.prototype.writeAndAdd = function (file, emit, cb) {
977
- this.writeFile(file, emit, (err, file) => {
978
- if (err) return cb(new Error('Unable to write file:' + file.path))
979
- this.addFile(file, (err) => {
980
- if (err) return cb(new Error('Unable to add file:' + file.path))
981
- cb(null, file)
553
+ )
982
554
  })
983
- })
984
- }
555
+ }
985
556
 
986
- Repository.prototype.deleteTask = function (task, cb) {
987
- if (!cb) throw new Error('task, callback required')
988
- var self = this
989
- var file = self.getFileForTask(task)
990
- if (!file) return cb(null)
991
- const execute = function (file) {
992
- file.deleteTask(task, self.getConfig())
993
- if (file.getContentForFile().trim() === '' && file.isMarkDownFile()) {
994
- if (file.deleted) return
995
- console.log('Deleting empty file:', file.path)
996
- self.deleteFile(file.path, cb)
997
- file.deleted = true
998
- return
557
+ /**
558
+ * Get the full path from a relative path
559
+ *
560
+ * @method getFullPath
561
+ * @param {} file
562
+ * @return String
563
+ */
564
+ getFullPath (file) {
565
+ if (File.isFile(file)) file = file.path
566
+ if (file.indexOf(this.path) === 0) return file
567
+ try {
568
+ var fullPath = path.join(this.path, file)
569
+ return fullPath
570
+ } catch (e) {
571
+ throw new Error(
572
+ util.format(
573
+ 'Error getting full path for file:%s and repo path:%s',
574
+ file,
575
+ this.path
576
+ )
577
+ )
999
578
  }
1000
- self.writeAndExtract(file, true, cb)
1001
- }
1002
- if (!file.getContent()) {
1003
- self.readFileContent(file, function (err, file) {
1004
- if (err) return cb(err)
1005
- execute(file)
1006
- })
1007
- } else {
1008
- execute(file)
1009
579
  }
1010
- }
1011
- Repository.prototype.deleteTasks = function (tasks, cb) {
1012
- if (!cb) throw new Error('tasks, callback required')
1013
- var self = this
1014
-
1015
- let files = _groupBy(tasks, (task) => task.source.path) // {path:tasks, path2:tasks}
1016
- for (let [path, tasks] of Object.entries(files)) {
1017
- files[path] = fastSort(tasks).desc(u => u.line)
1018
- }
1019
- async.eachSeries(
1020
- Object.entries(files),
1021
- ([path, tasks], cb) => {
1022
- const file = this.getFile(path)
1023
- async.eachSeries(
1024
- tasks,
1025
- (task, cb) => {
1026
- task = newCard(task, this.project, true)
1027
- this.deleteTask(task, (err) => {
1028
- cb(err || null, task)
1029
- })
1030
- },
1031
- (err) => {
1032
- cb(err)
1033
- }
580
+
581
+ /**
582
+ * Get the relative path from repository root
583
+ *
584
+ * @method getRelativePath
585
+ * @param {} fullPath
586
+ * @return String
587
+ */
588
+ getRelativePath (fullPath) {
589
+ if (path.sep === '\\') {
590
+ fullPath = fullPath.replace(/\//g, path.sep)
591
+ }
592
+ if (!fullPath.startsWith(this.path)) return fullPath
593
+ try {
594
+ var relPath = path.relative(this.path, fullPath)
595
+ return relPath
596
+ } catch (e) {
597
+ throw new Error(
598
+ util.format(
599
+ 'Error getting relative path for file:%s and repo path:%s',
600
+ fullPath,
601
+ this.path
602
+ )
1034
603
  )
1035
- },
1036
- (err) => {
1037
- cb(err)
1038
604
  }
1039
- )
1040
- }
605
+ }
1041
606
 
1042
- Repository.prototype.modifyTaskFromHtml = function (task, html, cb) {
1043
- cb = tools.cb(cb)
1044
- var file = this.getFileForTask(task)
1045
- if (!file.getContent()) {
1046
- this.readFileContent(file, (err, file) => {
1047
- if (err) return cb(err)
1048
- file.modifyTaskFromHtml(task, html)
1049
- this.writeAndExtract(file, true, cb)
1050
- })
1051
- } else {
1052
- file.modifyTaskFromHtml(task, html)
1053
- this.writeAndExtract(file, true, cb)
607
+ /**
608
+ * Is this file OK? Implemented in mixins
609
+ *
610
+ * @method fileOK
611
+ * @param {} file
612
+ * @param {} includeDirs
613
+ * @return stat
614
+ */
615
+ // READY Refactor fileOK to use async/await
616
+ // #esm-migration #urgent
617
+ async fileOK (file, includeDirs) {
618
+ inMixinsNoop()
1054
619
  }
1055
- }
1056
620
 
1057
- Repository.prototype.modifyTaskFromContent = function (task, content, cb) {
1058
- cb = tools.cb(cb)
1059
- var file = this.getFileForTask(task)
1060
- if (!file.getContent()) {
1061
- this.readFileContent(file, (err, file) => {
1062
- if (err) return cb(err)
1063
- file.modifyTaskFromContent(task, content, this.getConfig())
1064
- this.writeAndExtract(file, true, cb)
1065
- })
1066
- } else {
1067
- file.modifyTaskFromContent(task, content, this.getConfig())
1068
- this.writeAndExtract(file, true, cb)
621
+ setIgnores (ignores) {
622
+ // logger.info('ignore patterns:', ignores)
623
+ this.ignorePatterns = ignores
624
+ this.ignore = ignore().add(ignores)
1069
625
  }
1070
- }
1071
626
 
1072
- Repository.prototype.getTaskContent = function({
1073
- description,
1074
- order,
1075
- text,
1076
- taskPrefix,
1077
- taskSyntax,
1078
- }) {
1079
- const task = newCard(
1080
- {
1081
- description,
1082
- meta: {},
1083
- order,
1084
- text,
1085
- beforeText: taskPrefix,
1086
- type: taskSyntax,
1087
- },
1088
- this.project
1089
- )
1090
- Task.prototype.updateOrderMeta.apply(task, [this.config])
1091
- return task.description.join(eol.lf)
1092
- }
627
+ /**
628
+ * Should the relative path be included.
629
+ *
630
+ * @method shouldInclude
631
+ * @param {} relPath
632
+ * @return exclude
633
+ */
634
+ shouldInclude (relPath) {
635
+ relPath = this.getRelativePath(relPath)
636
+ let include = true
637
+ if (this.config.markdownOnly && relPath) {
638
+ include = File.isMarkdown(relPath)
639
+ }
640
+ if (this.ignore) {
641
+ try {
642
+ include = include && (relPath === '' || !this.ignore.ignores(relPath))
643
+ } catch (e) {
644
+ logger.error(
645
+ `Failed to check ignore status for dir: [${relPath}] in: [${this.path}]. It will be included.`,
646
+ e
647
+ )
648
+ }
649
+ }
1093
650
 
1094
- Repository.prototype.appendTask = function({file, content, list}, cb) {
1095
- const config = this.getConfig()
1096
- const interpretedTaskPrefix = _template(config.getTaskPrefix())({
1097
- date: new Date(),
1098
- }).trimEnd()
1099
- const lines = eol.split(content)
1100
- const text = lines[0]
1101
- const taskSyntax = config.getNewCardSyntax()
1102
- const taskPrefix = interpretedTaskPrefix ? `${interpretedTaskPrefix} ` : ''
1103
- let fileContent = file.getContent()
1104
- const journalTemplate = this.getConfig().journalTemplate
1105
- fileContent = fileContent.trim()
1106
- ? fileContent
1107
- : journalTemplate
1108
- && journalTemplate.trim()
1109
- ? journalTemplate + String(eol.lf)
1110
- : '';
1111
-
1112
- const length = fileContent.length
1113
- const fileIsEmpty = length < 1
1114
- const crlf = String(eol.crlf)
1115
- let sep = fileContent.indexOf(crlf) > -1 ? crlf : String(eol.lf)
1116
- if (fileContent.endsWith(sep) || fileIsEmpty) sep = ''
1117
- const order = appContext().projectContext.getOrder(list)
1118
- const description = lines.length > 1 ? Task.padDescription(lines.slice(1), taskPrefix) : []
1119
- const { orderMeta, tokenPrefix } = config
1120
- const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
1121
- const taskContent = config.orderMeta
1122
- ? this.getTaskContent({
1123
- description,
1124
- order,
1125
- text,
1126
- taskPrefix,
1127
- taskSyntax,
1128
- })
1129
- : description.join(eol.lf)
651
+ return include
652
+ }
1130
653
 
1131
- let appendContent = File.trimBlankLines(
1132
- `${taskPrefix}${rawTask}${eol.lf}${taskContent}`
1133
- )
1134
-
1135
- fileContent = `${fileContent}${sep}`
1136
- const cardTerminator = "\n".repeat(2)
1137
- file.setContent(`${fileContent}${appendContent}${cardTerminator}`)
654
+ /**
655
+ * Add or replace a file in the files reference array
656
+ *
657
+ * @method addFile
658
+ * @param {} file
659
+ * @return MemberExpression
660
+ */
661
+ // READY Refactor addFile to use async/await
662
+ // #esm-migration #urgent
663
+ async addFile (file) {
664
+ if (this.destroyed) throw new Error('destroyed')
665
+ const ok = await this.fileOK(file)
666
+ if (!ok) return this.files
667
+ let index = this.files.findIndex(({ path }) => path === file.path)
668
+ if (index > -1) {
669
+ this.files[index] = file
670
+ } else {
671
+ this.files.push(file)
672
+ }
1138
673
 
1139
- this.writeAndExtract(file, true, (err, file) => {
1140
- if (err) return cb(err)
1141
- const task = file.getTasks().find(task => task.text === text && task.list === list)
1142
- cb(null, file, task)
1143
- })
1144
- }
674
+ return this.files
675
+ }
1145
676
 
1146
- Repository.prototype.addTaskToFile = function (filePath, list, content, cb) {
1147
- cb = tools.cb(cb)
1148
- const relPath = this.getRelativePath(filePath)
1149
- let file = this.getFile(relPath)
677
+ /**
678
+ * Remove a file from the files reference array
679
+ *
680
+ * @method removeFile
681
+ * @param {} file
682
+ * @return MemberExpression
683
+ */
684
+ removeFile (file) {
685
+ if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
686
+ _remove(this.files, f => f.path === file.path)
1150
687
 
1151
- if (file) {
1152
- this.readFileContent(file, (err, file) => {
1153
- if (err) return cb(err)
1154
- this.appendTask({file, content, list}, cb)
1155
- })
1156
- } else {
1157
- const modifiedTime = new Date()
1158
- const createdTime = new Date()
1159
- file = new File({
1160
- repoId: this.path,
1161
- filePath: relPath,
1162
- content: '',
1163
- modifiedTime,
1164
- createdTime,
1165
- project: this.project,
1166
- })
1167
- this.appendTask({file, content, list}, cb)
688
+ return this.files
1168
689
  }
1169
- }
1170
690
 
1171
- /**
1172
- * Description
1173
- * @method modifyTask
1174
- * @param {} task
1175
- * @return CallExpression
1176
- */
1177
- Repository.prototype.modifyTask = function (task, writeFile, cb) {
1178
- if (!Task.isTask(task)) return cb()
1179
- if (_isFunction(writeFile)) {
1180
- cb = writeFile
1181
- writeFile = false
1182
- }
1183
- cb = tools.cb(cb)
1184
- const config = this.getConfig()
1185
- log(
1186
- 'Modifying Task... text:%s list:%s order:%d path:%s id:%s line:%d',
1187
- task.text,
1188
- task.list,
1189
- task.order,
1190
- task.source.path,
1191
- task.id,
1192
- task.line
1193
- )
1194
- var self = this
1195
- let beforeModifyContent
1196
- var file = this.getFileForTask(task)
1197
- try {
1198
- beforeModifyContent = file.getContent()
1199
- } catch (e) {
1200
- console.error(
1201
- `Can't get file for task: {text:'${task.text}', path:'${task.source.path}', line:${task.line}}`
1202
- )
691
+ /**
692
+ * Description
693
+ * @method getFile
694
+ * @param {} path
695
+ * @return CallExpression
696
+ */
697
+ getFile (path) {
698
+ path = this.getRelativePath(path)
699
+ return this.files.find((file) => file.path === path)
1203
700
  }
1204
701
 
1205
- const modifyTransformExtract = (cb) => {
1206
- file.modifyTask(task, config, true)
1207
- file.extractTasks(config)
1208
- file.transformTask({config, modify:true, task})
1209
- file.extractTasks(config)
1210
-
1211
- if (!writeFile || beforeModifyContent === file.getContent()) return cb(null, task)
702
+ getTask (id) {
703
+ if (Task.isTask(id)) {
704
+ let task = id
705
+ if (!task.meta.id) return
706
+ return this.getTasks().find((existingTask) => {
707
+ return existingTask.meta.id && existingTask.meta.id[0] == task.meta.id[0]
708
+ })
709
+ } else {
710
+ let task = this.getTasks().find((task) => task.id === id)
711
+ if (!task) return
712
+ return this.getFile(task.source.path)
713
+ .getTasks()
714
+ .find((task) => task.id === id)
715
+ }
716
+ }
1212
717
 
1213
- this.writeAndAdd(file, cb)
718
+ /**
719
+ * Description
720
+ * @method getFileForTask
721
+ * @param {} task
722
+ * @return CallExpression
723
+ */
724
+ getFileForTask (task) {
725
+ return this.getFile(task.getSource().path)
1214
726
  }
1215
727
 
1216
- if (!beforeModifyContent) {
1217
- this.readFileContent(file, (err) => {
1218
- if (err) return cb(err)
1219
- modifyTransformExtract(cb)
728
+ /**
729
+ * Descriptione
730
+ * @method getFiles
731
+ * @param {} paths
732
+ * @return CallExpression
733
+ */
734
+ getFiles (paths) {
735
+ if (_isUndefined(paths)) return sort(this.files).asc(u => u.path)
736
+ return this.files.filter((file) => {
737
+ return paths.includes(file.path)
1220
738
  })
1221
- } else {
1222
- modifyTransformExtract(cb)
1223
739
  }
1224
- }
1225
740
 
1226
- Repository.prototype.setTaskPriority = function (_task, index, cb) {
1227
- if (_task.order === '') return cb()
1228
- _task.order = index * 10
1229
- cb()
1230
- }
741
+ getFilesWithTasks () {
742
+ const files = this.files.filter((file) => file.getTasks().length > 0)
743
+ return sort(files).asc(u => u.path)
744
+ }
1231
745
 
1232
- function getPreviousIndexWithDifferentOrder(tasks, pos) {
1233
- let closestIndexWithOrder = -1
746
+ resetFile (file) {
747
+ file.reset()
748
+ file.removeListener('task.found', this.taskFoundListener)
749
+ file.removeListener('task.modified', this.taskModifiedListener)
750
+ }
1234
751
 
1235
- if (pos === 0) return closestIndexWithOrder
1236
-
1237
- let lastTask = tasks[pos] || tasks [pos - 1]
1238
-
1239
- for (
1240
- let index = pos - 1;
1241
- index >= 0 && closestIndexWithOrder < 0;
1242
- index--
1243
- ) {
1244
- const t = tasks[index]
1245
- if (!tasksHaveSameOrder(lastTask, t)) closestIndexWithOrder = index
1246
- lastTask = t
1247
- }
1248
- return closestIndexWithOrder
1249
- }
752
+ // READY refactor extractTasks to use async/await
753
+ // #esm-migration #urgent #important
754
+ // <!--
755
+ // order:-100
756
+ // -->
757
+ async extractTasks (file) {
758
+ if (file.content === null) {
759
+ await this.readFileContent(file)
760
+ }
761
+ file.on('task.found', this.taskFoundListener)
762
+ file.on('task.modified', this.taskModifiedListener)
763
+ const fileContent = file.content
764
+ file.extractAndTransformTasks(this.getConfig())
765
+ if (!file.isModified() || fileContent === file.content) {
766
+ this.resetFile(file)
767
+ return file
768
+ }
769
+ file.extractTasks(this.getConfig())
770
+ if (file.modified) await this.writeFile(file)
771
+ this.resetFile(file)
772
+ return file
773
+ }
1250
774
 
1251
- Repository.getPreviousIndexWithDifferentOrder = getPreviousIndexWithDifferentOrder
775
+ /**
776
+ * Implemented in mixins
777
+ * @method writeFile
778
+ * @param {} file
779
+ * @param {} cb
780
+ * @return
781
+ */
782
+ async writeFile (file) {
783
+ inMixinsNoop()
784
+ }
1252
785
 
1253
- function getNextIndexWithDifferentOrder(tasks, pos) {
1254
- let closestIndexWithOrder = -1
786
+ /**
787
+ * Implemented in mixins
788
+ * @method getFilesInPath
789
+ * @param {} includeDirs
790
+ * @return CallExpression
791
+ */
1255
792
 
1256
- let lastTask = tasks[pos]
793
+ async getFilesInPath (includeDirs) {
794
+ inMixinsNoop()
795
+ }
1257
796
 
1258
- for (
1259
- let index = pos;
1260
- index < tasks.length && closestIndexWithOrder < 0;
1261
- index++
1262
- ) {
1263
- const t = tasks[index]
1264
- if (!tasksHaveSameOrder(lastTask, t)) closestIndexWithOrder = index
1265
- lastTask = t
797
+ /**
798
+ * Implemented in mixins
799
+ * @method readFileContent
800
+ * @param {} file
801
+ * @return
802
+ */
803
+ async readFileContent (file) {
804
+ inMixinsNoop()
1266
805
  }
1267
- return closestIndexWithOrder
1268
- }
1269
806
 
1270
- Repository.getNextIndexWithDifferentOrder = getNextIndexWithDifferentOrder
807
+ // READY Refactor readFile to use async/await
808
+ // #esm-migration #important #urgent
809
+ // <!--
810
+ // order:-30
811
+ // -->
812
+ async readFile(file) {
813
+ if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
814
+ if (file.deleted) return file
1271
815
 
1272
- function taskBeforeAndAfterHaveDifferentOrder(taskList, newPos) {
1273
- const taskBefore = taskList[newPos - 1]
1274
- const taskAfter = taskList[newPos]
1275
- return taskBeforeHasOrder(taskList, newPos)
1276
- && taskAfterHasOrder(taskList, newPos)
1277
- && parseFloat(taskBefore.order) !== parseFloat(taskAfter.order)
1278
- }
816
+ var currentChecksum = file.checksum
817
+ const filePath = file.path
818
+ if (/\.\.(\/|\\)/.test(filePath)) throw new Error('Unable to read file:' + file)
1279
819
 
1280
- function taskBeforeHasOrder(taskList, newPos) {
1281
- const taskBefore = taskList[newPos - 1]
1282
- return taskBefore && isNumber(taskBefore.order)
1283
- }
820
+ await this.readFileContent(file)
821
+ file.checksum = computeChecksum(file.getContent())
822
+ file.updated = currentChecksum !== file.checksum
823
+ if (!file.updated) return file
1284
824
 
1285
- function taskAfterHasOrder(taskList, newPos) {
1286
- const taskAfter = taskList[newPos]
1287
- return taskAfter && isNumber(taskAfter.order)
1288
- }
1289
- function lastTaskHasOrder(taskList) {
1290
- const lastTask = taskList[taskList.length - 1]
1291
- return lastTask && isNumber(lastTask.order)
1292
- }
825
+ await this.extractTasks(file)
826
+ await this.addFile()
1293
827
 
1294
- function getTasksToModify(task, taskList, newPos) {
1295
-
1296
- const previousIndexWithDifferentOrder = getPreviousIndexWithDifferentOrder(taskList, newPos)
1297
- const nextIndexWithDifferentOrder = getNextIndexWithDifferentOrder(taskList, newPos)
1298
- const previousTaskWithOrder = taskList[previousIndexWithDifferentOrder]
1299
- const nextTaskWithOrder = taskList[nextIndexWithDifferentOrder]
1300
-
1301
- const startingOrder = previousTaskWithOrder
1302
- ? isNumber(previousTaskWithOrder.order) && previousTaskWithOrder.order || 0
1303
- : 0;
1304
- const endingOrder = nextTaskWithOrder
1305
- ? nextTaskWithOrder.order
1306
- : null;
1307
-
1308
- const start = previousIndexWithDifferentOrder > -1 ? previousIndexWithDifferentOrder + 1 : 0;
1309
- const end = nextIndexWithDifferentOrder > -1 ? nextIndexWithDifferentOrder : newPos + 1;
1310
- const changes = nextIndexWithDifferentOrder - previousIndexWithDifferentOrder + 1
1311
- const increment = isNumber(endingOrder) ? (endingOrder - startingOrder) / changes : 10
1312
- const tasksToModify = []
1313
- let newOrder = startingOrder
1314
- let order = 0
1315
- if (taskList.length === 0) {
1316
- order = 0
1317
- } else if (newPos === 0) {
1318
- order = taskList[0].order - 10
1319
- } else if (newPos === taskList.length && lastTaskHasOrder(taskList)) {
1320
- order = taskList[taskList.length - 1].order + 10
1321
- } else if (taskBeforeAndAfterHaveDifferentOrder(taskList, newPos)) {
1322
- order = (taskList[newPos].order - taskList[newPos - 1].order) / 2 + taskList[newPos - 1].order
1323
- } else if (taskBeforeHasOrder(taskList, newPos) && !taskAfterHasOrder(taskList, newPos)) {
1324
- order = taskList[newPos - 1].order + 10
1325
- } else {
1326
- for (let index = start; index < end; index++) {
1327
- newOrder += increment
1328
- if (index === newPos) {
1329
- task.order = newOrder
1330
- tasksToModify.push(task)
1331
- newOrder += increment
828
+ return file
829
+ }
830
+
831
+ /**
832
+ * Description
833
+ * @method readFiles
834
+ * @param {} files
835
+ * @param {} cb
836
+ * @return
837
+ */
838
+ // READY Refactor readFiles to use async/await
839
+ // #esm-migration #urgent #important
840
+ // <!--
841
+ // order:-10
842
+ // -->
843
+ readFiles (files = this.files) {
844
+ this.allMeta = {}
845
+ this.metaKeys = new Set()
846
+ this.allTags = new Set()
847
+ this.allContexts = new Set()
848
+
849
+ return new Promise(async (resolve, reject) => {
850
+
851
+ if (files.length < 1) {
852
+ files = this.files = await this.getFilesInPath(false)
853
+ const filesToInclude = files.map((file) => file.path)
854
+ const fileStats = {
855
+ count: filesToInclude.length,
856
+ files: filesToInclude,
857
+ }
858
+ this.emit('files.found', fileStats)
1332
859
  }
860
+ let completed = 0
861
+
862
+ if (files.length < 1) return resolve(files)
863
+ eachLimit(
864
+ files,
865
+ ASYNC_LIMIT,
866
+ async (file) => {
867
+ this.emit('file.reading', { path: file.path })
868
+ await this.readFile(file)
869
+ completed++
870
+ this.emit('file.read', {path: file.path, completed: completed})
871
+ },
872
+ function (err) {
873
+ if (err) return reject(err)
874
+ resolve(files)
875
+ }
876
+ )
877
+ })
878
+ }
1333
879
 
1334
- if (!taskList[index]) continue
1335
- taskList[index].order = newOrder
1336
- tasksToModify.push(taskList[index])
1337
- }
880
+ /**
881
+ * Implemented in mixins
882
+ * @method deleteFile
883
+ * @param {} path
884
+ * @param {} cb
885
+ * @return
886
+ */
887
+ async deleteFile (path, cb) {
888
+ inMixinsNoop(cb)
1338
889
  }
1339
890
 
1340
- if (tasksToModify.length === 0) {
1341
- task.order = order
1342
- tasksToModify.push(task)
891
+ /**
892
+ * Description
893
+ * @method hasDefaultFile
894
+ * @return CallExpression
895
+ */
896
+ hasDefaultFile () {
897
+ return _some(this.getFiles(), function (file) {
898
+ var regex = new RegExp(DEFAULT_FILE_PATTERN, 'i')
899
+ return regex.test(file.path)
900
+ })
1343
901
  }
1344
902
 
1345
- return tasksToModify
1346
- }
903
+ /**
904
+ * Description
905
+ * @method getDefaultFile
906
+ * @return file
907
+ */
908
+ getDefaultFile () {
909
+ var files = sort(this.getFiles()).asc(u => u.path)
910
+ var file = files.reverse().find((file) => {
911
+ var regex = new RegExp(DEFAULT_FILE_PATTERN, 'i')
912
+ return regex.test(file.path)
913
+ })
914
+ return file
915
+ }
1347
916
 
1348
- Repository.getTasksToModify = getTasksToModify
917
+ getList (name) {
918
+ return this.getLists().find((list) => list.name === name)
919
+ }
1349
920
 
1350
- function tasksHaveSameOrder(taskA, taskB) {
1351
- return taskA && taskB && taskA.order + "" === taskB.order + ""
1352
- }
1353
921
 
1354
- Repository.prototype.moveTask = function ({ task, newList = task.list, newPos }, cb) {
1355
- if (!Task.isTask(task)) {
1356
- task = newCard(task, this.project, true)
922
+ getListById(id, lists = this.getLists()) {
923
+ return lists.find((list) => list.id === id)
1357
924
  }
1358
- task = this.getTask(task.id)
1359
925
 
1360
- var toListTasks = this.getTasksInList(newList)
1361
- if (toListTasks === undefined)
1362
- return cb(new Error(ERRORS.LIST_NOT_FOUND, newList))
1363
-
1364
- var fromListTasks = this.getTasksInList(task.list)
1365
- if (fromListTasks === undefined)
1366
- return cb(new Error(ERRORS.LIST_NOT_FOUND, task.list))
926
+ // READY Refactor hideList to use async/await
927
+ // #esm-migration
928
+ // <!--
929
+ // order:-150
930
+ // -->
931
+ async hideList (name) {
932
+ await this.setListHidden(name)
933
+ }
1367
934
 
1368
- var sameList = newList == task.list
1369
- if (!sameList) task.oldList = task.list
1370
- task.list = newList
935
+ // READY Refactor showList to use async/await
936
+ // #esm-migration
937
+ // <!--
938
+ // order:-230
939
+ // -->
940
+ async showList (name) {
941
+ await this.setListHidden(name, false)
942
+ }
1371
943
 
1372
- // Move the task to the correct position in the list
1373
- fromListTasks = _reject(fromListTasks, function (_task) {
1374
- return _task.equals(task)
1375
- })
1376
- toListTasks = _reject(toListTasks, function (_task) {
1377
- return _task.equals(task)
1378
- })
1379
-
1380
- const tasksToModify = getTasksToModify(task, toListTasks, newPos)
1381
- task.updateOrderMeta(this.config)
1382
- toListTasks.splice(newPos, 0, task)
1383
-
1384
- const tasksToModifySorted = fastSort(tasksToModify).by([{ desc: u => u.line }, { asc: u => u.path }])
1385
- async.eachSeries(
1386
- tasksToModifySorted,
1387
- (task, cb) => {
1388
- this.modifyTask(task, true, cb)
1389
- },
1390
- (err) => {
1391
- if (err)
1392
- return cb([
1393
- new Error(
1394
- 'moveTasks: Error while modifying tasks in the current list'
1395
- ),
1396
- err,
1397
- ])
1398
- cb(null, task)
944
+ async setListHidden (name, hidden = true) {
945
+ const list = this.getList(name)
946
+ if (list) {
947
+ list.hidden = hidden
948
+ await this.saveConfig()
949
+ this.emit('list.modified', name)
1399
950
  }
1400
- )
1401
- }
951
+ }
1402
952
 
1403
- Repository.prototype.moveTasks = function (tasks, newList, newPos = 0, noEmit, cb) {
1404
- var log = require('debug')('moveTasks')
1405
- var self = this
1406
- if (this.getList(newList).filter)
1407
- return cb(new Error(`Tasks can\'t be moved to a filtered list ${newList}.`))
1408
- var listsModified = [newList]
1409
- if (_isFunction(noEmit)) {
1410
- cb = noEmit
1411
- noEmit = false
1412
- }
1413
- this.moving = true
1414
- cb = tools.cb(cb)
1415
-
1416
- log('Move tasks to list:%s at position:%d : %j', newList, newPos, tasks)
1417
- log(
1418
- 'newList before mods:',
1419
- JSON.stringify(self.getTasksInList(newList), null, 3)
1420
- )
1421
- fastSort(tasks).by({desc: t => t.line})
1422
- async.series(
1423
- tasks.map((task, i) => {
1424
- return function (cb) {
1425
- const foundTask = self.getTasks().find(({source, line}) => task.source.path === source.path && task.line === line)
1426
- if (foundTask) {
1427
- if (listsModified.indexOf(foundTask.list) < 0) listsModified.push(foundTask.list)
1428
- self.moveTask({ task: foundTask, newList, newPos: newPos + i, noEmit: true }, cb)
1429
- } else {
1430
- cb()
1431
- }
1432
- }
1433
- }),
1434
- function (err) {
1435
- if (err) {
1436
- console.error('Error occurred while moving tasks:', err)
1437
- }
1438
- self.saveModifiedFiles(function (err, results) {
1439
- if (err)
1440
- console.error('Error occurred while saving modified files:', err)
1441
- self.lastMovedFiles = results
1442
- var tasksByList = listsModified.map((list) => {
1443
- return {
1444
- list: list,
1445
- tasks: self.getTasksInList(list),
1446
- }
1447
- })
1448
- self.moving = false
1449
- cb(err, tasksByList)
1450
- if (!err && !noEmit) self.emit('tasks.moved', tasks)
1451
- })
953
+ /**
954
+ * Description
955
+ * @method moveList
956
+ * @param {} name
957
+ * @param {} pos
958
+ * @param {} cb
959
+ * @return
960
+ */
961
+ // READY Refactor moveList to use async/await
962
+ // #esm-migration
963
+ // <!--
964
+ // order:-190
965
+ // -->
966
+ async moveList (name, pos) {
967
+ var list = this.getList(name)
968
+ if (list) {
969
+ _remove(this.getLists(), { name: name })
970
+ this.getLists().splice(pos, 0, list)
971
+ await this.saveConfig()
972
+ this.emit('list.modified', name)
1452
973
  }
1453
- )
1454
- }
974
+ }
1455
975
 
1456
- Repository.prototype.moveTasksAsync = function (tasks, newList, newPos, noEmit = false) {
1457
- return new Promise((resolve, reject) => {
1458
- this.moveTasks(tasks, newList, newPos, noEmit, (err, tasksByList) => {
1459
- if (err) return reject(err)
1460
- resolve(tasksByList)
1461
- })
1462
- })
1463
- }
976
+ // READY Refactor toggleListIgnore to use async/await
977
+ // #esm-migration
978
+ // <!--
979
+ // order:-250
980
+ // -->
981
+ async toggleListIgnore (name) {
982
+ var list = this.getList(name)
983
+ if (!list) return reject(new Error('List not found'))
984
+ list.ignore = !list.ignore
985
+ await this.updateList(list.id, list)
986
+ await this.saveConfig()
987
+ this.emit('list.modified', name)
988
+ }
1464
989
 
1465
- Repository.prototype.getModifiedFiles = function () {
1466
- var filesToSave = []
1467
- this.getFiles().forEach((file) => {
1468
- if (file.isModified()) filesToSave.push(file)
1469
- })
1470
- return filesToSave
1471
- }
990
+ async toggleList (name) {
991
+ const list = this.getList(name)
992
+ if (!list) throw new Error('List not found')
993
+ list.hidden = !list.hidden
994
+ await this.updateList(list.id, list)
995
+ await this.saveConfig()
996
+ this.emit('list.modified', name)
997
+ }
1472
998
 
1473
- Repository.prototype.saveModifiedFiles = function (cb) {
1474
- const checksum = this.checksum || function () {}
1475
- var self = this
1476
- var filesToSave = self.getModifiedFiles()
1477
- var funcs = filesToSave.map((file) => {
1478
- return function (cb) {
1479
- file.checksum = checksum(file.getContent())
1480
- self.writeAndExtract(file, false, cb)
999
+ // READY Refactor updateList to use async/await
1000
+ // #esm-migration
1001
+ // <!--
1002
+ // order:-90
1003
+ // -->
1004
+ async updateList(id, {name, hidden, ignore, filter}) {
1005
+ const lists = this.getLists()
1006
+ const list = this.getListById(id, lists)
1007
+ const oldName = list.name
1008
+ const hasFilter = list.filter
1009
+ list.name = name
1010
+ list.hidden = hidden
1011
+ list.ignore = ignore
1012
+ if (hasFilter !== undefined) list.filter = filter
1013
+ this.setLists(lists)
1014
+
1015
+ if (oldName === name || hasFilter) {
1016
+ return await this.saveConfig()
1481
1017
  }
1482
- })
1483
1018
 
1484
- if (funcs.length < 1) return cb()
1485
- this.savingFiles = true
1486
- async.parallel(funcs, (err) => {
1487
- this.savingFiles = false
1488
- if (err) return cb(err)
1489
- self.emit('files.saved', filesToSave)
1490
- cb()
1491
- })
1492
- }
1019
+ await this.moveTasksBetweenLists(oldName, name)
1020
+ }
1493
1021
 
1494
- /**
1495
- * Description
1496
- * @method getTasks
1497
- * @return tasks
1498
- */
1499
- Repository.prototype.getTasks = function () {
1500
- var tasks = [],
1501
- self = this
1502
- this.getFiles().forEach((file) => {
1503
- Array.prototype.push.apply(tasks, file.getTasks())
1504
- })
1022
+ getTasksByFile(tasks) {
1023
+ const tasksByFile = {} // path: [files]
1024
+ tasks.forEach((task) => {
1025
+ const filePath = task.path
1026
+ if (!tasksByFile[filePath]) {
1027
+ tasksByFile[filePath] = { file: this.getFileForTask(task), tasks: [] }
1028
+ }
1029
+ tasksByFile[filePath].tasks.push(task)
1030
+ tasksByFile[filePath].tasks = sort(tasksByFile[filePath].tasks).desc(u => u.line)
1031
+ })
1032
+ return tasksByFile
1033
+ }
1034
+ // READY Refactor moveTasksBetweenLists to use async/await
1035
+ // <!--
1036
+ // #esm-migration #needs-testing
1037
+ // order:-80
1038
+ // -->
1039
+ // ## Tasks
1040
+ // - [x] Refactor
1041
+ // - [ ] Test
1042
+ async moveTasksBetweenLists(oldName, newName) {
1043
+ return new Promise((resolve, reject) => {
1044
+ const tasksToModify = this.getTasksInList(oldName)
1045
+ const tasksByFile = this.getTasksByFile(tasksToModify) // { <path/to/file>: {file, tasks} }
1046
+
1047
+ const modifyTasksFunctions = Object.values(tasksByFile).map(({ file, tasks }) => {
1048
+ return async () => {
1049
+ await this.readFileContent(file)
1050
+ tasks.forEach((task) => {
1051
+ task.list = newName
1052
+ file.modifyTask(task, this.getConfig(), true)
1053
+ file.modified = true
1054
+ })
1055
+ return file
1056
+ }
1057
+ })
1505
1058
 
1506
- return tasks
1507
- }
1059
+ if (modifyTasksFunctions.length < 1) return resolve()
1060
+ this.moving = true
1061
+ parallel(modifyTasksFunctions, async (err, files) => {
1062
+ this.moving = false
1063
+ if (err) {
1064
+ files.forEach(file => this.resetFile(file))
1065
+ return reject(err)
1066
+ }
1067
+ try {
1068
+ await this.saveConfig()
1069
+ await this.saveModifiedFiles()
1070
+ resolve(files)
1071
+ } catch (err) {
1072
+ reject(err)
1073
+ }
1074
+ })
1075
+ })
1076
+ }
1508
1077
 
1509
- Repository.prototype.getTasksByList = function (noSort) {
1510
- return Repository.getTasksByList(this, this.getTasks(), noSort, true)
1511
- }
1078
+ // READY Migrate writeAndExtract to use async/await
1079
+ // #esm-migration
1080
+ // <!--
1081
+ // order:-80
1082
+ // -->
1083
+ async writeAndExtract (file, emit) {
1084
+ try {
1085
+ await this.writeFile(file, emit)
1086
+ } catch (err) {
1087
+ new Error('Unable to write file:' + file.path, {cause: err})
1088
+ }
1512
1089
 
1513
- Repository.getTasksByList = function (
1514
- repo,
1515
- tasksAry,
1516
- noSort,
1517
- populateFiltered
1518
- ) {
1519
- if (!repo) return []
1520
- var tasks = {}
1521
- var allTasks = noSort ? tasksAry : fastSort(tasksAry).by(DEFAULT_SORT)
1522
- allTasks.forEach(function (task) {
1523
- if (!tasks[task.list]) tasks[task.list] = []
1524
- tasks[task.list].push(task)
1525
- })
1526
- var lists = repo.getLists()
1527
- lists.forEach((list) => {
1528
- if (list.filter && populateFiltered)
1529
- return Repository.populateFilteredList(list, tasksAry)
1530
- list.tasks = tasks[list.name] || []
1531
- })
1532
- return lists
1533
- }
1090
+ try {
1091
+ await this.extractTasks(file)
1092
+ } catch (err) {
1093
+ new Error('Unable to extract tasks for file:' + file.path, { cause: err })
1094
+ }
1534
1095
 
1535
- Repository.populateFilteredList = function (list, tasks) {
1536
- try {
1537
- list.tasks = Repository.query(tasks, list.filter).map((task) => {
1538
- task.filteredListName = list.name
1539
- return task
1540
- })
1541
- } catch (e) {
1542
- list.tasks = []
1096
+ try {
1097
+ await this.addFile(file)
1098
+ } catch (err) {
1099
+ new Error('Unable to add file after extracting tasks: ' + file.path, { cause: err })
1100
+ }
1101
+
1102
+ return file
1543
1103
  }
1544
- }
1545
1104
 
1546
- /**
1547
- * Description
1548
- * @method getTasksInList
1549
- * @param {} name
1550
- * @return ConditionalExpression
1551
- */
1552
- Repository.prototype.getTasksInList = function (name, offset, limit) {
1553
- if (!_isString(name)) return []
1554
- var tasks = _where(this.getTasks(), { list: name })
1555
- if (tasks.length === 0) return []
1556
- var allTasks = fastSort(tasks).by(DEFAULT_SORT)
1557
- if (isNumber(offset) && isNumber(limit))
1558
- return allTasks.slice(offset, offset + limit)
1559
- return allTasks
1560
- }
1105
+ // READY Refactor writeAndAdd to use async/await
1106
+ // #esm-migration
1107
+ // <!--
1108
+ // order:-70
1109
+ // -->
1110
+ async writeAndAdd (file, emit) {
1111
+ await this.writeFile(file, emit)
1112
+ await this.addFile(file)
1113
+ return file
1114
+ }
1561
1115
 
1562
- Repository.prototype.getTaskIndex = function (task) {
1563
- const tasks = this.getTasksInList(task.list)
1564
- let index = 0
1565
- tasks.forEach((t, i) => {
1566
- if (t.id === task.id) index = i
1567
- })
1568
- return index
1569
- }
1116
+ // READY Refactor deleteTask to use async/await
1117
+ // #esm-migration
1118
+ // <!--
1119
+ // order:-130
1120
+ // -->
1121
+ async deleteTask (task, cb) {
1122
+ let file = this.getFileForTask(task)
1123
+ if (!file) return
1570
1124
 
1571
- Repository.prototype.serialize = function () {
1572
- return JSON.stringify(this, null, 3)
1573
- }
1125
+ if (!file.getContent()) {
1126
+ file = await this.readFileContent(file)
1127
+ }
1574
1128
 
1575
- // WARN: deprecated
1576
- Repository.deserialize = function (repo, cb) {
1577
- cb = tools.cb(cb)
1578
- repo = JSON.parse(repo)
1579
- const config = new Config(repo.config)
1580
- var newRepo = new Repository(repo.path, config)
1581
- var count = 0
1582
- repo.files.forEach((file) => {
1583
- log('file:%j', file)
1584
- newRepo.addFile(new File({ file, config, languages }), function (err) {
1585
- if (err) return cb(err)
1586
- count++
1587
- log('count:%d, repo.files.length:%d', count, repo.files.length)
1588
- if (count == repo.files.length) cb(null, newRepo)
1129
+ file.deleteTask(task, this.getConfig())
1130
+ if (file.getContentForFile().trim() === '' && file.isMarkDownFile()) {
1131
+ if (file.deleted) return
1132
+ logger.log('Deleting empty file:', file.path)
1133
+ this.deleteFile(file.path)
1134
+ file.deleted = true
1135
+ return
1136
+ }
1137
+ await this.writeAndExtract(file, true)
1138
+ }
1139
+
1140
+ // READY Refactor deleteTasks to use async/await
1141
+ // #esm-migration
1142
+ // <!--
1143
+ // order:-140
1144
+ // -->
1145
+ async deleteTasks(tasks) {
1146
+ const files = {}
1147
+ tasks.forEach((task) => {
1148
+ if (!files[task.source.path]) {
1149
+ files[task.source.path] = []
1150
+ }
1151
+ files[task.source.path].push(task)
1152
+ files[task.source.path] = sort(files[task.source.path]).desc(u => u.line)
1589
1153
  })
1590
- })
1591
- }
1592
1154
 
1593
- Repository.regexQuery = function (tasks, queryString) {
1594
- return tasks
1595
- .map((task) => {
1596
- const escapedQueryString = tools.escapeRegExp(queryString.trim());
1155
+ await Promise.all(
1156
+ Object.keys(files).map(async (path) => {
1157
+ const tasksInFile = files[path]
1158
+ if (!tasksInFile) return
1159
+ for (const task of tasksInFile) {
1160
+ const taskToDelete = newCard(task, this.project, true)
1161
+ await this.deleteTask(taskToDelete)
1162
+ }
1163
+ })
1164
+ )
1597
1165
 
1598
- // Regex to match occurrences not part of a URL and not already surrounded by `==`
1599
- const regex = new RegExp(
1600
- `(?<!https?:\\/\\/[^\\s]*?)\\b${escapedQueryString}\\b(?![^\\s]*\\/)(?<!==)(?!==)`,
1601
- 'gi'
1602
- );
1166
+ this.emit('tasks.updated', tasks)
1167
+ }
1603
1168
 
1604
- // Regex to detect Markdown links `[title](url)` where `queryString` is in title or URL
1605
- const markdownLinkRegex = new RegExp(
1606
- `\\[([^\\]]*${escapedQueryString}[^\\]]*)\\]\\(([^)]+${escapedQueryString}[^)]*)\\)`,
1607
- 'gi'
1608
- );
1169
+ // READY Refactor modifyTaskFromHtml to use async/await
1170
+ // #esm-migration
1171
+ // <!--
1172
+ // order:-170
1173
+ // -->
1174
+ async modifyTaskFromHtml (task, html) {
1175
+ var file = this.getFileForTask(task)
1176
+ if (!file.getContent()) {
1177
+ file = await this.readFileContent(file)
1178
+ }
1179
+ file.modifyTaskFromHtml(task, html)
1180
+ return await this.writeAndExtract(file, true)
1181
+ }
1609
1182
 
1610
- let match = task.content.match(regex) || task.content.match(markdownLinkRegex);
1611
- task.match = match;
1183
+ // READY Refactor modifyTaskFromContent to use async/await
1184
+ // #esm-migration
1185
+ // <!--
1186
+ // order:-180
1187
+ // -->
1188
+ async modifyTaskFromContent (task, content, cb) {
1189
+ var file = this.getFileForTask(task)
1190
+ if (!file.getContent()) {
1191
+ file = await this.readFileContent(file)
1192
+ }
1193
+
1194
+ file.modifyTaskFromContent(task, content, this.getConfig())
1195
+ return await this.writeAndExtract(file, true)
1196
+ }
1612
1197
 
1613
- if (match && queryString.trim() && task.interpretedContent) {
1614
- // Replace standalone occurrences
1615
- task.interpretedContent = task.interpretedContent.replace(
1616
- regex,
1617
- '==$&=='
1618
- );
1198
+ getTaskContent({
1199
+ description,
1200
+ order,
1201
+ text,
1202
+ taskPrefix,
1203
+ taskSyntax,
1204
+ }) {
1205
+ const task = newCard(
1206
+ {
1207
+ description,
1208
+ meta: {},
1209
+ order,
1210
+ text,
1211
+ beforeText: taskPrefix,
1212
+ type: taskSyntax,
1213
+ },
1214
+ this.project
1215
+ )
1216
+ task.updateOrderMeta(this.config)
1217
+ return task.description.join(eol.lf)
1218
+ }
1619
1219
 
1620
- // Replace Markdown links where queryString is in title or URL
1621
- task.interpretedContent = task.interpretedContent.replace(
1622
- markdownLinkRegex,
1623
- (match) => `==${match}==`
1624
- );
1220
+ // READY Refactor appendTask to use async/await
1221
+ // #esm-migration
1222
+ // <!--
1223
+ // order:-300
1224
+ // -->
1225
+ async appendTask({file, content, list}) {
1226
+ const config = this.getConfig()
1227
+ const interpretedTaskPrefix = interpolate(
1228
+ config.getTaskPrefix(),
1229
+ { date: new Date() }
1230
+ ).content.trimEnd()
1231
+ const lines = eol.split(content)
1232
+ const text = lines[0]
1233
+ const taskSyntax = config.getNewCardSyntax()
1234
+ const taskPrefix = interpretedTaskPrefix ? `${interpretedTaskPrefix} ` : ''
1235
+ let fileContent = file.getContent()
1236
+ const journalTemplate = this.getConfig().journalTemplate
1237
+ fileContent = fileContent.trim()
1238
+ ? fileContent
1239
+ : journalTemplate
1240
+ && journalTemplate.trim()
1241
+ ? journalTemplate + String(eol.lf)
1242
+ : '';
1243
+
1244
+ const length = fileContent.length
1245
+ const fileIsEmpty = length < 1
1246
+ const crlf = String(eol.crlf)
1247
+ let sep = fileContent.indexOf(crlf) > -1 ? crlf : String(eol.lf)
1248
+ if (fileContent.endsWith(sep) || fileIsEmpty) sep = ''
1249
+ const order = appContext().projectContext.getOrder(list)
1250
+ const description = lines.length > 1 ? Task.padDescription(lines.slice(1), taskPrefix) : []
1251
+ const { orderMeta, tokenPrefix } = config
1252
+ const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
1253
+ const taskContent = config.orderMeta
1254
+ ? this.getTaskContent({
1255
+ description,
1256
+ order,
1257
+ text,
1258
+ taskPrefix,
1259
+ taskSyntax,
1260
+ })
1261
+ : description.join(eol.lf)
1625
1262
 
1626
- task.updateDescriptionData();
1627
- }
1263
+ let appendContent = File.trimBlankLines(
1264
+ `${taskPrefix}${rawTask}${eol.lf}${taskContent}`
1265
+ )
1266
+
1267
+ fileContent = `${fileContent}${sep}`
1268
+ const cardTerminator = "\n".repeat(2)
1269
+ file.setContent(`${fileContent}${appendContent}${cardTerminator}`)
1628
1270
 
1629
- return task;
1630
- })
1631
- .filter(({ match }) => match);
1632
- };
1271
+ await this.writeAndExtract(file)
1272
+ const task = file.getTasks().find(task => task.text === text && task.list === list)
1273
+ return { file, task }
1274
+ }
1633
1275
 
1276
+ // READY Refactor addTaskToFile to use async/await
1277
+ // #esm-migration
1278
+ // <!--
1279
+ // order:-290
1280
+ // -->
1281
+ async addTaskToFile (filePath, list, content) {
1282
+ const relPath = this.getRelativePath(filePath)
1283
+ let file = this.getFile(relPath)
1634
1284
 
1635
- Repository.parseHideListsFromQueryString = function (queryString) {
1636
- const hideLists = []
1637
- const regex = `hide:(\\s*((${LIST_NAME_PATTERN},?\\s?)+))?`
1638
- queryString = queryString.replaceAll(
1639
- // hide lists example `hide: list1, list2`
1640
- new XRegExp(regex, 'g'),
1641
- (match, allLists, lists) => {
1642
- lists && lists.split(',').forEach((list) => {
1643
- hideLists.push(list.trim())
1285
+ if (file) {
1286
+ await this.readFileContent(file)
1287
+ return this.appendTask({file, content, list})
1288
+ } else {
1289
+ const modifiedTime = new Date()
1290
+ const createdTime = new Date()
1291
+ file = new File({
1292
+ repoId: this.path,
1293
+ filePath: relPath,
1294
+ content: '',
1295
+ modifiedTime,
1296
+ createdTime,
1297
+ project: this.project,
1644
1298
  })
1645
- return ''
1299
+ return this.appendTask({file, content, list})
1646
1300
  }
1647
- ).trim()
1648
- return {
1649
- hideLists,
1650
- queryString,
1651
1301
  }
1652
- }
1653
1302
 
1654
- Repository.parseSortFromQueryString = function (queryString) {
1655
- const sort = []
1656
- queryString = queryString.replace(
1657
- /\s([+-])([A-Za-z.]+)/g,
1658
- (match, order, attr) => {
1659
- const direction = order === '+' ? 'asc' : 'desc'
1660
- const sortString = `{ "${direction}": "function (o) { return o.${attr}; }" }`
1661
- sort.push(JSONfns.parse(sortString))
1662
- return ''
1303
+ /**
1304
+ * Description
1305
+ * @method modifyTask
1306
+ * @param {} task
1307
+ * @return CallExpression
1308
+ */
1309
+ // READY Refactor modifyTask to use async/await
1310
+ // #esm-migration #important #urgent
1311
+ // <!--
1312
+ // order:-60
1313
+ // -->
1314
+ async modifyTask (task, writeFile = false) {
1315
+ if (!Task.isTask(task)) return
1316
+ const config = this.getConfig()
1317
+ log(
1318
+ 'Modifying Task... text:%s list:%s order:%d path:%s id:%s line:%d',
1319
+ task.text,
1320
+ task.list,
1321
+ task.order,
1322
+ task.source.path,
1323
+ task.id,
1324
+ task.line
1325
+ )
1326
+ let beforeModifyContent
1327
+ const file = this.getFileForTask(task)
1328
+ try {
1329
+ beforeModifyContent = file.getContent()
1330
+ } catch (e) {
1331
+ logger.error(`Can't get file for task: {text:'${task.text}', path:'${task.source.path}', line:${task.line}}`)
1332
+ throw e
1663
1333
  }
1664
- )
1665
- return {
1666
- sort,
1667
- queryString,
1334
+
1335
+ if (!beforeModifyContent) await this.readFileContent(file)
1336
+ file.modifyTask(task, config, true)
1337
+ file.extractTasks(config)
1338
+ file.transformTask({config, modify:true, task})
1339
+ file.extractTasks(config)
1340
+
1341
+ if (!writeFile || beforeModifyContent === file.getContent()) return task
1342
+
1343
+ return await this.writeAndAdd(file)
1668
1344
  }
1669
- }
1670
1345
 
1671
- Repository.parseSortFromMongoQuery = function (mongoQuery) {
1672
- const sort = []
1673
- for (const [key, value] of Object.entries(mongoQuery.sort)) {
1674
- if (value > 0) sort.push({ asc: key })
1675
- else sort.push({ desc: key })
1346
+ // READY Refactor moveTask to use async/await
1347
+ // #esm-migration
1348
+ // <!--
1349
+ // order:-200
1350
+ // -->
1351
+ async moveTask ({ task, newList, newPos }) {
1352
+ if (!Task.isTask(task)) {
1353
+ task = newCard(task, this.project, true)
1354
+ }
1355
+ task = this.getTask(task.id)
1356
+
1357
+ var toListTasks = this.getTasksInList(newList)
1358
+ if (toListTasks === undefined)
1359
+ throw new Error(ERRORS.LIST_NOT_FOUND, newList)
1360
+
1361
+ var fromListTasks = this.getTasksInList(task.list)
1362
+ if (fromListTasks === undefined)
1363
+ throw new Error(ERRORS.LIST_NOT_FOUND, task.list)
1364
+
1365
+ var sameList = newList == task.list
1366
+ if (!sameList) task.oldList = task.list
1367
+ task.list = newList
1368
+
1369
+ // Move the task to the correct position in the list
1370
+ fromListTasks = _reject(fromListTasks, function (_task) {
1371
+ return _task.equals(task)
1372
+ })
1373
+ toListTasks = _reject(toListTasks, function (_task) {
1374
+ return _task.equals(task)
1375
+ })
1376
+
1377
+ const tasksToModify = getTasksToModify(task, toListTasks, newPos)
1378
+ task.updateOrderMeta(this.config)
1379
+ toListTasks.splice(newPos, 0, task)
1380
+
1381
+ const tasksToModifySorted = sort(tasksToModify).by([{ desc: u => u.line }, { asc: u => u.path }])
1382
+
1383
+ await eachSeries(
1384
+ tasksToModifySorted,
1385
+ async (task) => {
1386
+ await this.modifyTask(task, true)
1387
+ },
1388
+ )
1389
+
1390
+ return task
1676
1391
  }
1677
- return sort
1678
- }
1679
1392
 
1680
- Repository.sortByQuery = function (tasks, queryString = '') {
1681
- queryString = replaceDateLanguage(queryString)
1682
- let { sort } = Repository.parseSortFromQueryString(queryString)
1683
- if (!sort || sort.length === 0) sort = DEFAULT_SORT
1684
- fastSort(tasks).by(sort)
1685
- }
1393
+ // READY Refactor moveTasks to use async/await
1394
+ // #esm-migration #important
1395
+ // <!--
1396
+ // order:-350
1397
+ // -->
1398
+ async moveTasks (tasks, newList, newPos = 0, noEmit = false) {
1399
+ const log = require('debug')('moveTasks')
1400
+ if (this.getList(newList).filter)
1401
+ throw new Error(`Tasks can\'t be moved to a filtered list ${newList}.`)
1402
+
1403
+ const listsModified = [newList]
1404
+ this.moving = true
1405
+
1406
+ log('Move tasks to list:%s at position:%d : %j', newList, newPos, tasks)
1407
+ log(
1408
+ 'newList before mods:',
1409
+ JSON.stringify(this.getTasksInList(newList), null, 3)
1410
+ )
1411
+ await series(
1412
+ sort(tasks).by({desc: t => t.line}).map((task, i) => {
1413
+ return async () =>{
1414
+ const foundTask = this.getTasks().find(({source, line}) => task.source.path === source.path && task.line === line)
1415
+ if (foundTask) {
1416
+ if (listsModified.indexOf(foundTask.list) < 0) listsModified.push(foundTask.list)
1417
+ await this.moveTask({ task: foundTask, newList, newPos: newPos + i, noEmit: true })
1418
+ }
1419
+ }
1420
+ })
1421
+ )
1686
1422
 
1687
- Repository.filterCards = function (tasks, _queryString = '') {
1688
- let query
1689
- _queryString = replaceDateLanguage(_queryString)
1690
- let { sort, queryString } = Repository.parseSortFromQueryString(_queryString);
1691
- const hideListsQuery = Repository.parseHideListsFromQueryString(queryString);
1692
- queryString = hideListsQuery.queryString;
1693
- const hideLists = hideListsQuery.hideLists;
1694
- try {
1695
- query = monquery(queryString)
1696
- } catch (e) {
1697
- log(`Unable to parse ${_queryString} using monquery`)
1423
+ const results = await this.saveModifiedFiles()
1424
+
1425
+ this.lastMovedFiles = results
1426
+ this.moving = false
1427
+
1428
+ if (!noEmit) this.emit('tasks.moved', tasks)
1429
+ return listsModified.map((list) => {
1430
+ return {
1431
+ list: list,
1432
+ tasks: this.getTasksInList(list),
1433
+ }
1434
+ })
1698
1435
  }
1699
- let result = []
1700
- if (query) result = tasks.filter(sift.default(query))
1701
- if (!query || result.length === 0) {
1702
- result = Repository.regexQuery(tasks, queryString)
1436
+
1437
+ getModifiedFiles () {
1438
+ var filesToSave = []
1439
+ this.getFiles().forEach((file) => {
1440
+ if (file.isModified()) filesToSave.push(file)
1441
+ })
1442
+ return filesToSave
1703
1443
  }
1704
- if (!sort || sort.length === 0) sort = DEFAULT_SORT
1705
- fastSort(result).by(sort)
1706
- return {
1707
- result,
1708
- query,
1709
- sort,
1710
- hideLists
1444
+
1445
+ // READY Refactor saveModifiedFiles to use async/await
1446
+ // #esm-migration #urgent #important
1447
+ // <!--
1448
+ // order:-110
1449
+ // -->
1450
+ async saveModifiedFiles () {
1451
+ var filesToSave = this.getModifiedFiles()
1452
+ var funcs = filesToSave.map((file) => {
1453
+ return async () => {
1454
+ file.checksum = computeChecksum(file.getContent())
1455
+ await this.writeAndExtract(file, false)
1456
+ }
1457
+ })
1458
+
1459
+ if (funcs.length < 1) return
1460
+
1461
+ this.savingFiles = true
1462
+ await Promise.all(funcs)
1463
+ this.savingFiles = false
1464
+ this.emit('files.saved', filesToSave)
1711
1465
  }
1712
- }
1713
1466
 
1714
- Repository.query = function (tasks, queryString) {
1715
- return Repository.filterCards(tasks, queryString).result
1716
- }
1467
+ /**
1468
+ * Description
1469
+ * @method getTasks
1470
+ * @return tasks
1471
+ */
1472
+ getTasks () {
1473
+ const tasks = []
1474
+ this.getFiles().forEach((file) => {
1475
+ Array.prototype.push.apply(tasks, file.getTasks())
1476
+ })
1717
1477
 
1718
- Repository.replaceDatesInQuery = function (query) {
1719
- return Repository.filterObjectValues(query, (key, value) => {
1720
- let date = Date.parse(value)
1721
- if (date && /(\d{4})-(\d{2})-(\d{2})/.test(value)) {
1722
- return date
1723
- }
1724
- return value
1725
- })
1726
- }
1478
+ return tasks
1479
+ }
1727
1480
 
1728
- Repository.filterObjectValues = function (o, cb) {
1729
- if (o && typeof o === 'object') {
1730
- for (const [key, value] of Object.entries(o)) {
1731
- if (value && typeof value === 'object') {
1732
- Repository.filterObjectValues(value, cb)
1733
- } else {
1734
- o[key] = cb(key, value)
1735
- }
1736
- }
1481
+ getTasksByList (noSort) {
1482
+ return Repository.getTasksByList(this, this.getTasks(), noSort, true)
1737
1483
  }
1738
- return o
1739
- }
1740
1484
 
1741
- Repository.prototype.query = function (queryString) {
1742
- const result = Repository.query(this.getTasks(), queryString)
1743
- return Repository.getTasksByList(this, result, true)
1744
- }
1485
+ /**
1486
+ * Description
1487
+ * @method getTasksInList
1488
+ * @param {} name
1489
+ * @return ConditionalExpression
1490
+ */
1491
+ getTasksInList (name, offset, limit) {
1492
+ if (!_isString(name)) return []
1493
+ var tasks = _where(this.getTasks(), { list: name })
1494
+ if (tasks.length === 0) return []
1495
+ var allTasks = sort(tasks).by(DEFAULT_SORT)
1496
+ if (isNumber(offset) && isNumber(limit))
1497
+ return allTasks.slice(offset, offset + limit)
1498
+ return allTasks
1499
+ }
1500
+
1501
+ getTaskIndex (task) {
1502
+ const tasks = this.getTasksInList(task.list)
1503
+ let index = 0
1504
+ tasks.forEach((t, i) => {
1505
+ if (t.id === task.id) index = i
1506
+ })
1507
+ return index
1508
+ }
1745
1509
 
1746
- module.exports = Repository
1510
+ query (queryString) {
1511
+ const result = Repository.query(this.getTasks(), queryString)
1512
+ return Repository.getTasksByList(this, result, true)
1513
+ }
1514
+ }