feat: recurring for quick add magic (#1105)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1105
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
This commit is contained in:
konrad 2021-12-07 20:08:39 +00:00
parent c8029ec3c4
commit 8b8e413af0
7 changed files with 213 additions and 13 deletions

View file

@ -65,6 +65,21 @@
<li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li> <li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li>
</ul> </ul>
<p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p> <p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p>
<h3>{{ $t('task.quickAddMagic.repeats') }}</h3>
<p>{{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}</p>
<p>{{ $t('misc.forExample') }}</p>
<ul>
<!-- Not localized because these only work in english -->
<li>Every day</li>
<li>Every 3 days</li>
<li>Every week</li>
<li>Every 2 weeks</li>
<li>Every month</li>
<li>Every 6 months</li>
<li>Every year</li>
<li>Every 2 years</li>
</ul>
</card> </card>
</modal> </modal>
</div> </div>

View file

@ -471,7 +471,8 @@
"close": "Close", "close": "Close",
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu" "hideMenu": "Hide the menu",
"forExample": "For example:"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -720,7 +721,9 @@
"dateWeekday": "any weekday, will use the next date with that date", "dateWeekday": "any weekday, will use the next date with that date",
"dateCurrentYear": "will use the current year", "dateCurrentYear": "will use the current year",
"dateNth": "will use the {day}th of the current month", "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": { "team": {

View file

@ -510,4 +510,54 @@ describe('Parse Task Text', () => {
expect(result.assignees[0]).toBe('user with long name') 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)
})
}
})
}) })

View file

@ -38,6 +38,24 @@ interface Priorites {
DO_NOW: number, 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 { interface ParsedTaskText {
text: string, text: string,
date: Date | null, date: Date | null,
@ -45,6 +63,7 @@ interface ParsedTaskText {
list: string | null, list: string | null,
priority: number | null, priority: number | null,
assignees: string[], assignees: string[],
repeats: Repeats | null,
} }
interface Prefixes { interface Prefixes {
@ -67,6 +86,7 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
list: null, list: null,
priority: null, priority: null,
assignees: [], assignees: [],
repeats: null,
} }
const prefixes = PREFIXES[prefixesMode] const prefixes = PREFIXES[prefixesMode]
@ -83,7 +103,11 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
result.assignees = getItemsFromPrefix(text, prefixes.assignee) 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.text = newText
result.date = date result.date = date
@ -132,6 +156,113 @@ const getPriority = (text: string, prefix: string): number | null => {
return 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 => { const cleanupItemText = (text: string, items: string[], prefix: string): string => {
items.forEach(l => { items.forEach(l => {
text = text text = text

View file

@ -69,7 +69,7 @@ export default class TaskService extends AbstractService {
// Make the repeating amount to seconds // Make the repeating amount to seconds
let repeatAfterSeconds = 0 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) { switch (model.repeatAfter.type) {
case 'hours': case 'hours':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 repeatAfterSeconds = model.repeatAfter.amount * 60 * 60

View file

@ -292,6 +292,7 @@ export default {
bucketId: bucketId || 0, bucketId: bucketId || 0,
position, position,
}) })
task.repeatAfter = parsedTask.repeats
const taskService = new TaskService() const taskService = new TaskService()
const createdTask = await taskService.create(task) const createdTask = await taskService.create(task)