diff --git a/src/components/tasks/partials/quick-add-magic.vue b/src/components/tasks/partials/quick-add-magic.vue
index c28c6a7c..00ed7f92 100644
--- a/src/components/tasks/partials/quick-add-magic.vue
+++ b/src/components/tasks/partials/quick-add-magic.vue
@@ -65,6 +65,21 @@
17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})
{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}
+
+ {{ $t('task.quickAddMagic.repeats') }}
+ {{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}
+ {{ $t('misc.forExample') }}
+
+
+ - Every day
+ - Every 3 days
+ - Every week
+ - Every 2 weeks
+ - Every month
+ - Every 6 months
+ - Every year
+ - Every 2 years
+
diff --git a/src/helpers/time/parseDate.ts b/src/helpers/time/parseDate.ts
index 0994b5d2..34e6ef0f 100644
--- a/src/helpers/time/parseDate.ts
+++ b/src/helpers/time/parseDate.ts
@@ -135,18 +135,18 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
if (result === null) {
// 3. Try parsing the date as "27/01" or "01/27"
- const monthNumericRegex:RegExp = /([0-9][0-9]?\/[0-9][0-9]?)/ig
+ const monthNumericRegex: RegExp = /([0-9][0-9]?\/[0-9][0-9]?)/ig
results = monthNumericRegex.exec(text)
// Put the year before or after the date, depending on what works
result = results === null ? null : `${now.getFullYear()}/${results[0]}`
- if(result === null) {
+ if (result === null) {
return {
foundText,
date: null,
}
}
-
+
foundText = results === null ? '' : results[0]
if (result === null || isNaN(new Date(result).getTime())) {
result = results === null ? null : `${results[0]}/${now.getFullYear()}`
@@ -280,7 +280,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
if (foundText.endsWith(' ')) {
foundText = foundText.substr(0, foundText.length - 1)
}
-
+
return {
foundText: foundText,
date: date,
@@ -301,12 +301,12 @@ const getDayFromText = (text: string) => {
const date = new Date(now)
const day = parseInt(results[0])
date.setDate(day)
-
+
// If the parsed day is the 31st but the next month only has 30 days, setting the day to 31 will "overflow" the
// date to the next month, but the first.
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
// setting it for the first time and set it again if it isn't - that would mean the month overflowed.
- if(day === 31 && date.getDate() !== day) {
+ if (day === 31 && date.getDate() !== day) {
date.setDate(day)
}
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json
index 4a939660..8a26c3a1 100644
--- a/src/i18n/lang/en.json
+++ b/src/i18n/lang/en.json
@@ -471,7 +471,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
- "hideMenu": "Hide the menu"
+ "hideMenu": "Hide the menu",
+ "forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@@ -720,7 +721,9 @@
"dateWeekday": "any weekday, will use the next date with that date",
"dateCurrentYear": "will use the current year",
"dateNth": "will use the {day}th of the current month",
- "dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time."
+ "dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
+ "repeats": "Repeating tasks",
+ "repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {
diff --git a/src/modules/parseTaskText.test.js b/src/modules/parseTaskText.test.js
index 885f792d..e68b1e83 100644
--- a/src/modules/parseTaskText.test.js
+++ b/src/modules/parseTaskText.test.js
@@ -7,14 +7,14 @@ describe('Parse Task Text', () => {
it('should return text with no intents as is', () => {
expect(parseTaskText('Lorem Ipsum').text).toBe('Lorem Ipsum')
})
-
+
it('should not parse text when disabled', () => {
const text = 'Lorem Ipsum today *label +list !2 @user'
const result = parseTaskText(text, 'disabled')
-
+
expect(result.text).toBe(text)
})
-
+
it('should parse text in todoist mode when configured', () => {
const result = parseTaskText('Lorem Ipsum today @label #list !2 +user', 'todoist')
@@ -510,4 +510,54 @@ describe('Parse Task Text', () => {
expect(result.assignees[0]).toBe('user with long name')
})
})
+
+ describe('Recurring Dates', () => {
+ const cases = {
+ 'every 1 hour': {type: 'hours', amount: 1},
+ 'every hour': {type: 'hours', amount: 1},
+ 'every 5 hours': {type: 'hours', amount: 5},
+ 'every 12 hours': {type: 'hours', amount: 12},
+ 'every day': {type: 'days', amount: 1},
+ 'every 1 day': {type: 'days', amount: 1},
+ 'every 2 days': {type: 'days', amount: 2},
+ 'every week': {type: 'weeks', amount: 1},
+ 'every 1 week': {type: 'weeks', amount: 1},
+ 'every 3 weeks': {type: 'weeks', amount: 3},
+ 'every month': {type: 'months', amount: 1},
+ 'every 1 month': {type: 'months', amount: 1},
+ 'every 2 months': {type: 'months', amount: 2},
+ 'every year': {type: 'years', amount: 1},
+ 'every 1 year': {type: 'years', amount: 1},
+ 'every 4 years': {type: 'years', amount: 4},
+ 'anually': {type: 'years', amount: 1},
+ 'bianually': {type: 'months', amount: 6},
+ 'semiannually': {type: 'months', amount: 6},
+ 'biennially': {type: 'years', amount: 2},
+ 'daily': {type: 'days', amount: 1},
+ 'hourly': {type: 'hours', amount: 1},
+ 'monthly': {type: 'months', amount: 1},
+ 'weekly': {type: 'weeks', amount: 1},
+ 'yearly': {type: 'years', amount: 1},
+ 'every one hour': {type: 'hours', amount: 1}, // maybe unnesecary but better to include it for completeness sake
+ 'every two hours': {type: 'hours', amount: 2},
+ 'every three hours': {type: 'hours', amount: 3},
+ 'every four hours': {type: 'hours', amount: 4},
+ 'every five hours': {type: 'hours', amount: 5},
+ 'every six hours': {type: 'hours', amount: 6},
+ 'every seven hours': {type: 'hours', amount: 7},
+ 'every eight hours': {type: 'hours', amount: 8},
+ 'every nine hours': {type: 'hours', amount: 9},
+ 'every ten hours': {type: 'hours', amount: 10},
+ }
+
+ for (const c in cases) {
+ it(`should parse ${c} as recurring date every ${cases[c].amount} ${cases[c].type}`, () => {
+ const result = parseTaskText(`Lorem Ipsum ${c}`)
+
+ expect(result.text).toBe('Lorem Ipsum')
+ expect(result.repeats.type).toBe(cases[c].type)
+ expect(result.repeats.amount).toBe(cases[c].amount)
+ })
+ }
+ })
})
diff --git a/src/modules/parseTaskText.ts b/src/modules/parseTaskText.ts
index d3fbf84b..30434f00 100644
--- a/src/modules/parseTaskText.ts
+++ b/src/modules/parseTaskText.ts
@@ -38,6 +38,24 @@ interface Priorites {
DO_NOW: number,
}
+enum RepeatType {
+ Hours = 'hours',
+ Days = 'days',
+ Weeks = 'weeks',
+ Months = 'months',
+ Years = 'years',
+}
+
+interface Repeats {
+ type: RepeatType,
+ amount: number,
+}
+
+interface repeatParsedResult {
+ textWithoutMatched: string,
+ repeats: Repeats | null,
+}
+
interface ParsedTaskText {
text: string,
date: Date | null,
@@ -45,6 +63,7 @@ interface ParsedTaskText {
list: string | null,
priority: number | null,
assignees: string[],
+ repeats: Repeats | null,
}
interface Prefixes {
@@ -67,6 +86,7 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
list: null,
priority: null,
assignees: [],
+ repeats: null,
}
const prefixes = PREFIXES[prefixesMode]
@@ -83,7 +103,11 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
result.assignees = getItemsFromPrefix(text, prefixes.assignee)
- const {newText, date} = parseDate(text)
+ const {textWithoutMatched, repeats} = getRepeats(text)
+ result.text = textWithoutMatched
+ result.repeats = repeats
+
+ const {newText, date} = parseDate(result.text)
result.text = newText
result.date = date
@@ -132,6 +156,113 @@ const getPriority = (text: string, prefix: string): number | null => {
return null
}
+const getRepeats = (text: string): repeatParsedResult => {
+ const regex = /((every|each) (([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten) )?(hours?|days?|weeks?|months?|years?))|anually|bianually|semiannually|biennially|daily|hourly|monthly|weekly|yearly/ig
+ const results = regex.exec(text)
+ if (results === null) {
+ return {
+ textWithoutMatched: text,
+ repeats: null,
+ }
+ }
+
+ let amount = 1
+ switch (results[3] ? results[3].trim() : undefined) {
+ case 'one':
+ amount = 1
+ break
+ case 'two':
+ amount = 2
+ break
+ case 'three':
+ amount = 3
+ break
+ case 'four':
+ amount = 4
+ break
+ case 'five':
+ amount = 5
+ break
+ case 'six':
+ amount = 6
+ break
+ case 'seven':
+ amount = 7
+ break
+ case 'eight':
+ amount = 8
+ break
+ case 'nine':
+ amount = 9
+ break
+ case 'ten':
+ amount = 10
+ break
+ default:
+ amount = results[3] ? parseInt(results[3]) : 1
+ }
+ let type: RepeatType = RepeatType.Hours
+
+ switch (results[0]) {
+ case 'biennially':
+ type = RepeatType.Years
+ amount = 2
+ break
+ case 'bianually':
+ case 'semiannually':
+ type = RepeatType.Months
+ amount = 6
+ break
+ case 'yearly':
+ case 'anually':
+ type = RepeatType.Years
+ break
+ case 'daily':
+ type = RepeatType.Days
+ break
+ case 'hourly':
+ type = RepeatType.Hours
+ break
+ case 'monthly':
+ type = RepeatType.Months
+ break
+ case 'weekly':
+ type = RepeatType.Weeks
+ break
+ default:
+ switch (results[5]) {
+ case 'hour':
+ case 'hours':
+ type = RepeatType.Hours
+ break
+ case 'day':
+ case 'days':
+ type = RepeatType.Days
+ break
+ case 'week':
+ case 'weeks':
+ type = RepeatType.Weeks
+ break
+ case 'month':
+ case 'months':
+ type = RepeatType.Months
+ break
+ case 'year':
+ case 'years':
+ type = RepeatType.Years
+ break
+ }
+ }
+
+ return {
+ textWithoutMatched: text.replace(results[0], ''),
+ repeats: {
+ amount,
+ type,
+ },
+ }
+}
+
const cleanupItemText = (text: string, items: string[], prefix: string): string => {
items.forEach(l => {
text = text
diff --git a/src/services/task.js b/src/services/task.js
index 361d08dd..c17bc2ef 100644
--- a/src/services/task.js
+++ b/src/services/task.js
@@ -69,7 +69,7 @@ export default class TaskService extends AbstractService {
// Make the repeating amount to seconds
let repeatAfterSeconds = 0
- if (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0) {
+ if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) {
switch (model.repeatAfter.type) {
case 'hours':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60
diff --git a/src/store/modules/tasks.js b/src/store/modules/tasks.js
index 70e5370c..8d29cc89 100644
--- a/src/store/modules/tasks.js
+++ b/src/store/modules/tasks.js
@@ -292,6 +292,7 @@ export default {
bucketId: bucketId || 0,
position,
})
+ task.repeatAfter = parsedTask.repeats
const taskService = new TaskService()
const createdTask = await taskService.create(task)