chore: move frontend files
This commit is contained in:
811
frontend/src/modules/parseTaskText.test.ts
Normal file
811
frontend/src/modules/parseTaskText.test.ts
Normal file
@ -0,0 +1,811 @@
|
||||
import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
|
||||
|
||||
import {parseTaskText, PrefixMode} from './parseTaskText'
|
||||
import {getDateFromText, parseDate} from '../helpers/time/parseDate'
|
||||
import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
import {MILLISECONDS_A_DAY} from '@/constants/date'
|
||||
import type {IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
|
||||
describe('Parse Task Text', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
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 +project !2 @user'
|
||||
const result = parseTaskText(text, PrefixMode.Disabled)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
})
|
||||
|
||||
it('should parse text in todoist mode when configured', () => {
|
||||
const result = parseTaskText('Lorem Ipsum today @label #project !2 +user', PrefixMode.Todoist)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum +user')
|
||||
const now = new Date()
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label')
|
||||
expect(result.project).toBe('project')
|
||||
expect(result.priority).toBe(2)
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user')
|
||||
})
|
||||
|
||||
it('should ignore email addresses', () => {
|
||||
const text = 'Lorem Ipsum email@example.com'
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
})
|
||||
|
||||
describe('Date Parsing', () => {
|
||||
it('should not return any date if none was provided', () => {
|
||||
const result = parseTaskText('Lorem Ipsum')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it('should ignore casing', () => {
|
||||
const result = parseTaskText('Lorem Ipsum ToDay')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
})
|
||||
it('should recognize today', () => {
|
||||
const result = parseTaskText('Lorem Ipsum today')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
})
|
||||
describe('should recognize today with a time', () => {
|
||||
const cases = {
|
||||
'at 15:00': '15:0',
|
||||
'@ 15:00': '15:0',
|
||||
'at 15:30': '15:30',
|
||||
'@ 3pm': '15:0',
|
||||
'at 3pm': '15:0',
|
||||
'at 3 pm': '15:0',
|
||||
'at 3am': '3:0',
|
||||
'at 3:12 am': '3:12',
|
||||
'at 3:12 pm': '15:12',
|
||||
} as const
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should recognize today with a time ${c}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum today ${c}`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
expect(`${result?.date?.getHours()}:${result?.date?.getMinutes()}`).toBe(cases[c as keyof typeof cases])
|
||||
expect(result?.date?.getSeconds()).toBe(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
it('should recognize tomorrow', () => {
|
||||
const result = parseTaskText('Lorem Ipsum tomorrow')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
expect(result?.date?.getFullYear()).toBe(tomorrow.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
|
||||
})
|
||||
it('should recognize Tomorrow', () => {
|
||||
const result = parseTaskText('Lorem Ipsum Tomorrow')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
expect(result?.date?.getFullYear()).toBe(tomorrow.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
|
||||
})
|
||||
it('should recognize next monday', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next monday')
|
||||
|
||||
const untilNextMonday = calculateDayInterval('nextMonday')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize next monday on the beginning of the sentence', () => {
|
||||
const result = parseTaskText('next monday Lorem Ipsum')
|
||||
|
||||
const untilNextMonday = calculateDayInterval('nextMonday')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize next monday and ignore casing', () => {
|
||||
const result = parseTaskText('Lorem Ipsum nExt Monday')
|
||||
|
||||
const untilNextMonday = calculateDayInterval('nextMonday')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize this weekend', () => {
|
||||
const result = parseTaskText('Lorem Ipsum this weekend')
|
||||
|
||||
const untilThisWeekend = calculateDayInterval('thisWeekend')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const thisWeekend = new Date()
|
||||
thisWeekend.setDate(thisWeekend.getDate() + untilThisWeekend)
|
||||
expect(result?.date?.getFullYear()).toBe(thisWeekend.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(thisWeekend.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(thisWeekend.getDate())
|
||||
})
|
||||
it('should recognize later this week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum later this week')
|
||||
|
||||
const untilLaterThisWeek = calculateDayInterval('laterThisWeek')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const laterThisWeek = new Date()
|
||||
laterThisWeek.setDate(laterThisWeek.getDate() + untilLaterThisWeek)
|
||||
expect(result?.date?.getFullYear()).toBe(laterThisWeek.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(laterThisWeek.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(laterThisWeek.getDate())
|
||||
})
|
||||
it('should recognize later next week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum later next week')
|
||||
|
||||
const untilLaterNextWeek = calculateDayInterval('laterNextWeek')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const laterNextWeek = new Date()
|
||||
laterNextWeek.setDate(laterNextWeek.getDate() + untilLaterNextWeek)
|
||||
expect(result?.date?.getFullYear()).toBe(laterNextWeek.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(laterNextWeek.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(laterNextWeek.getDate())
|
||||
})
|
||||
it('should recognize next week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next week')
|
||||
|
||||
const untilNextWeek = calculateDayInterval('nextWeek')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextWeek = new Date()
|
||||
nextWeek.setDate(nextWeek.getDate() + untilNextWeek)
|
||||
expect(result?.date?.getFullYear()).toBe(nextWeek.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextWeek.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextWeek.getDate())
|
||||
})
|
||||
it('should recognize next month', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next month')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonth = new Date()
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1)
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonth.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonth.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonth.getDate())
|
||||
})
|
||||
it('should recognize a date', () => {
|
||||
const result = parseTaskText('Lorem Ipsum 06/26/2021')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const date = new Date()
|
||||
date.setFullYear(2021, 5, 26)
|
||||
expect(result?.date?.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(date.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(date.getDate())
|
||||
})
|
||||
it('should recognize end of month', () => {
|
||||
const result = parseTaskText('Lorem Ipsum end of month')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const curDate = new Date()
|
||||
const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
|
||||
expect(result?.date?.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(date.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(date.getDate())
|
||||
})
|
||||
|
||||
|
||||
it('should recognize weekdays with time', () => {
|
||||
const result = parseTaskText('Lorem Ipsum thu at 14:00')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextThursday = new Date()
|
||||
nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7))
|
||||
expect(`${result?.date?.getFullYear()}-${result?.date?.getMonth()}-${result?.date?.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`)
|
||||
expect(`${result?.date?.getHours()}:${result?.date?.getMinutes()}`).toBe('14:0')
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month', () => {
|
||||
const time = new Date(2022, 0, 15)
|
||||
vi.setSystemTime(time)
|
||||
|
||||
const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getDate()).toBe(time.getDate() - 1)
|
||||
expect(result?.date?.getMonth()).toBe(time.getMonth() + 1)
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month when february is the next month', () => {
|
||||
const jan = new Date(2022, 0, 30)
|
||||
vi.setSystemTime(jan)
|
||||
|
||||
const result = parseTaskText(`Lorem Ipsum ${jan.getDate() - 1}th`)
|
||||
|
||||
const expectedDate = new Date(2022, 2, jan.getDate() - 1)
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result?.date?.getMonth()).toBe(expectedDate.getMonth())
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month when the next month has less days than this one', () => {
|
||||
const mar = new Date(2022, 2, 32)
|
||||
vi.setSystemTime(mar)
|
||||
|
||||
const result = parseTaskText(`Lorem Ipsum 31st`)
|
||||
|
||||
const expectedDate = new Date(2022, 4, 31)
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result?.date?.getMonth()).toBe(expectedDate.getMonth())
|
||||
})
|
||||
it('should recognize dates of the month in the future', () => {
|
||||
const nextDay = new Date(+new Date() + MILLISECONDS_A_DAY)
|
||||
const result = parseTaskText(`Lorem Ipsum ${nextDay.getDate()}nd`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getDate()).toBe(nextDay.getDate())
|
||||
})
|
||||
it('should only recognize weekdays with a space before or after them 1', () => {
|
||||
const result = parseTaskText('Lorem Ipsum renewed')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum renewed')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it('should only recognize weekdays with a space before or after them 2', () => {
|
||||
const result = parseTaskText('Lorem Ipsum github')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum github')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
describe('Should not recognize weekdays in words', () => {
|
||||
const cases = [
|
||||
'renewed',
|
||||
'github',
|
||||
'fix monitor stand',
|
||||
'order wedding cake',
|
||||
'investigate thumping noise',
|
||||
'iron frilly napkins',
|
||||
'take photo of saturn',
|
||||
'fix sunglasses',
|
||||
'monitor blood pressure',
|
||||
'Monitor blood pressure',
|
||||
'buy almonds',
|
||||
]
|
||||
|
||||
cases.forEach(c => {
|
||||
it(`should not recognize text with ${c} at the beginning as weekday`, () => {
|
||||
const result = parseTaskText(`${c} dolor sit amet`)
|
||||
|
||||
expect(result.text).toBe(`${c} dolor sit amet`)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it(`should not recognize text with ${c} at the end as weekday`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c}`)
|
||||
|
||||
expect(result.text).toBe(`Lorem Ipsum ${c}`)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it(`should not recognize text with ${c} as weekday`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c} dolor`)
|
||||
|
||||
expect(result.text).toBe(`Lorem Ipsum ${c} dolor`)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
it('should not recognize date number with no spacing around them', () => {
|
||||
const result = parseTaskText('Lorem Ispum v1.1.1')
|
||||
|
||||
expect(result.text).toBe('Lorem Ispum v1.1.1')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it('should not recognize dates in urls', () => {
|
||||
const text = 'https://some-url.org/blog/2019/1/233526-some-more-text'
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
|
||||
describe('Parse weekdays', () => {
|
||||
|
||||
const days = {
|
||||
'monday': 1,
|
||||
'Monday': 1,
|
||||
'mon': 1,
|
||||
'Mon': 1,
|
||||
'tuesday': 2,
|
||||
'Tuesday': 2,
|
||||
'tue': 2,
|
||||
'Tue': 2,
|
||||
'wednesday': 3,
|
||||
'Wednesday': 3,
|
||||
'wed': 3,
|
||||
'Wed': 3,
|
||||
'thursday': 4,
|
||||
'Thursday': 4,
|
||||
'thu': 4,
|
||||
'Thu': 4,
|
||||
'friday': 5,
|
||||
'Friday': 5,
|
||||
'fri': 5,
|
||||
'Fri': 5,
|
||||
'saturday': 6,
|
||||
'Saturday': 6,
|
||||
'sat': 6,
|
||||
'Sat': 6,
|
||||
'sunday': 7,
|
||||
'Sunday': 7,
|
||||
'sun': 7,
|
||||
'Sun': 7,
|
||||
} as Record<string, number>
|
||||
|
||||
const prefix = [
|
||||
'next ',
|
||||
'',
|
||||
]
|
||||
|
||||
prefix.forEach(p => {
|
||||
for (const d in days) {
|
||||
it(`should recognize ${p}${d}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${p}${d}`)
|
||||
|
||||
const next = new Date()
|
||||
const distance = (days[d] + 7 - next.getDay()) % 7
|
||||
next.setDate(next.getDate() + distance)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(next.getDate())
|
||||
})
|
||||
it(`should recognize ${p}${d} at the beginning of the text`, () => {
|
||||
const result = parseTaskText(`${p}${d} Lorem Ipsum`)
|
||||
|
||||
const next = new Date()
|
||||
const distance = (days[d] + 7 - next.getDay()) % 7
|
||||
next.setDate(next.getDate() + distance)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(next.getDate())
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// This tests only standalone days are recognized and not things like "github", "monitor" or "renewed".
|
||||
// We're not using real words here to generate tests for all days on the fly.
|
||||
for (const d in days) {
|
||||
it(`should not recognize ${d} with a space before it but none after it`, () => {
|
||||
const text = `Lorem Ipsum ${d}ipsum`
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it(`should not recognize ${d} with a space after it but none before it`, () => {
|
||||
const text = `Lorem ipsum${d} dolor`
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it(`should not recognize ${d} with no space before or after it`, () => {
|
||||
const text = `Lorem Ipsum lorem${d}ipsum`
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('Parse date from text', () => {
|
||||
const now = new Date()
|
||||
now.setFullYear(2021, 5, 24)
|
||||
|
||||
const cases = {
|
||||
'06/08/2021': '2021-6-8',
|
||||
'6/7/21': '2021-6-7',
|
||||
'27/07/2021,': null,
|
||||
'2021/07/06': '2021-7-6',
|
||||
'2021-07-06': '2021-7-6',
|
||||
'27 jan': '2022-1-27',
|
||||
'27/1': '2022-1-27',
|
||||
'27/01': '2022-1-27',
|
||||
'16/12': '2021-12-16',
|
||||
'01/27': '2022-1-27',
|
||||
'1/27': '2022-1-27',
|
||||
'jan 27': '2022-1-27',
|
||||
'Jan 27': '2022-1-27',
|
||||
'feb 21': '2022-2-21',
|
||||
'Feb 21': '2022-2-21',
|
||||
'mar 21': '2022-3-21',
|
||||
'Mar 21': '2022-3-21',
|
||||
'apr 21': '2022-4-21',
|
||||
'Apr 21': '2022-4-21',
|
||||
'may 21': '2022-5-21',
|
||||
'May 21': '2022-5-21',
|
||||
'jun 21': '2022-6-21',
|
||||
'Jun 21': '2022-6-21',
|
||||
'jul 21': '2021-7-21',
|
||||
'Jul 21': '2021-7-21',
|
||||
'aug 21': '2021-8-21',
|
||||
'Aug 21': '2021-8-21',
|
||||
'sep 21': '2021-9-21',
|
||||
'Sep 21': '2021-9-21',
|
||||
'oct 21': '2021-10-21',
|
||||
'Oct 21': '2021-10-21',
|
||||
'nov 21': '2021-11-21',
|
||||
'Nov 21': '2021-11-21',
|
||||
'dec 21': '2021-12-21',
|
||||
'Dec 21': '2021-12-21',
|
||||
} as Record<string, string | null>
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}' with the date at the end`, () => {
|
||||
const {date, foundText} = getDateFromText(`Lorem Ipsum ${c}`, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
|
||||
expect(foundText.trim()).toBe(c)
|
||||
})
|
||||
it(`should parse '${c}' as '${cases[c]}' with the date at the beginning`, () => {
|
||||
const {date, foundText} = getDateFromText(`${c} Lorem Ipsum`, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
|
||||
expect(foundText.trim()).toBe(c)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('Parse date from text in', () => {
|
||||
const now = new Date()
|
||||
now.setFullYear(2021, 5, 24)
|
||||
now.setHours(12)
|
||||
now.setMinutes(0)
|
||||
now.setSeconds(0)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(now)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
const cases = {
|
||||
'Lorem Ipsum in 1 hour': '2021-6-24 13:0',
|
||||
'in 2 hours': '2021-6-24 14:0',
|
||||
'in 1 day': '2021-6-25 12:0',
|
||||
'in 2 days': '2021-6-26 12:0',
|
||||
'in 1 week': '2021-7-1 12:0',
|
||||
'in 2 weeks': '2021-7-8 12:0',
|
||||
'in 4 weeks': '2021-7-22 12:0',
|
||||
'in 1 month': '2021-7-24 12:0',
|
||||
'in 3 months': '2021-9-24 12:0',
|
||||
'Something in 5 days at 10:00': '2021-6-29 10:0',
|
||||
'Something 17th at 10:00': '2021-7-17 10:0',
|
||||
'Something sep 17 at 10:00': '2021-9-17 10:0',
|
||||
'Something sep 17th at 10:00': '2021-9-17 10:0',
|
||||
'Something at 10:00 in 5 days': '2021-6-29 10:0',
|
||||
'Something at 10:00 17th': '2021-7-17 10:0',
|
||||
'Something at 10:00 sep 17th': '2021-9-17 10:0',
|
||||
} as Record<string, string>
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}'`, () => {
|
||||
const {date} = parseDate(c, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe(cases[c])
|
||||
})
|
||||
}
|
||||
|
||||
it('should replace the text in title case', () => {
|
||||
const {date, newText} = parseDate('Some task Mar 8th', now)
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe('2021-3-8 12:0')
|
||||
expect(newText).toBe('Some task')
|
||||
})
|
||||
|
||||
it('should replace the text in lowercase', () => {
|
||||
const {date, newText} = parseDate('Some task mar 8th', now)
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe('2021-3-8 12:0')
|
||||
expect(newText).toBe('Some task')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Labels', () => {
|
||||
it('should parse labels', () => {
|
||||
const result = parseTaskText('Lorem Ipsum *label1 *label2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(2)
|
||||
expect(result.labels[0]).toBe('label1')
|
||||
expect(result.labels[1]).toBe('label2')
|
||||
})
|
||||
it('should parse labels from the start', () => {
|
||||
const result = parseTaskText('*label1 Lorem Ipsum *label2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(2)
|
||||
expect(result.labels[0]).toBe('label1')
|
||||
expect(result.labels[1]).toBe('label2')
|
||||
})
|
||||
it('should resolve duplicate labels', () => {
|
||||
const result = parseTaskText('Lorem Ipsum *label1 *label1 *label2')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(2)
|
||||
expect(result.labels[0]).toBe('label1')
|
||||
expect(result.labels[1]).toBe('label2')
|
||||
})
|
||||
it('should correctly parse labels with spaces in them', () => {
|
||||
const result = parseTaskText(`Lorem *'label with space' Ipsum`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label with space')
|
||||
})
|
||||
it('should correctly parse labels with spaces in them and "', () => {
|
||||
const result = parseTaskText('Lorem *"label with space" Ipsum')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label with space')
|
||||
})
|
||||
it('should not parse labels called date expressions as dates', () => {
|
||||
const result = parseTaskText('Lorem Ipsum *today')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('today')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Project', () => {
|
||||
it('should parse a project', () => {
|
||||
const result = parseTaskText('Lorem Ipsum +project')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.project).toBe('project')
|
||||
})
|
||||
it('should parse a project with a space in it', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +'project with long name'`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.project).toBe('project with long name')
|
||||
})
|
||||
it('should parse a project with a space in it and "', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +"project with long name"`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.project).toBe('project with long name')
|
||||
})
|
||||
it('should parse only the first project', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +project1 +project2 +project3`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum +project2 +project3')
|
||||
expect(result.project).toBe('project1')
|
||||
})
|
||||
it('should parse a project that\'s called like a date as project', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +today`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.project).toBe('today')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Priority', () => {
|
||||
for (const p in PRIORITIES) {
|
||||
it(`should parse priority ${p}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum !${PRIORITIES[p]}`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.priority).toBe(PRIORITIES[p])
|
||||
})
|
||||
}
|
||||
it(`should not parse an invalid priority`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum !9999`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum !9999')
|
||||
expect(result.priority).toBe(null)
|
||||
})
|
||||
it(`should not parse an invalid priority but use the first valid one it finds`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum !9999 !1`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum !9999')
|
||||
expect(result.priority).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Assignee', () => {
|
||||
it('should parse an assignee', () => {
|
||||
const text = 'Lorem Ipsum @user'
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user')
|
||||
})
|
||||
it('should parse multiple assignees', () => {
|
||||
const text = 'Lorem Ipsum @user1 @user2 @user3'
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.assignees).toHaveLength(3)
|
||||
expect(result.assignees[0]).toBe('user1')
|
||||
expect(result.assignees[1]).toBe('user2')
|
||||
expect(result.assignees[2]).toBe('user3')
|
||||
})
|
||||
it('should parse avoid duplicate assignees', () => {
|
||||
const text = 'Lorem Ipsum @user1 @user1 @user2'
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.assignees).toHaveLength(2)
|
||||
expect(result.assignees[0]).toBe('user1')
|
||||
expect(result.assignees[1]).toBe('user2')
|
||||
})
|
||||
it('should parse an assignee with a space in it', () => {
|
||||
const text = `Lorem Ipsum @'user with long name'`
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user with long name')
|
||||
})
|
||||
it('should parse an assignee with a space in it and "', () => {
|
||||
const text = `Lorem Ipsum @"user with long name"`
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user with long name')
|
||||
})
|
||||
it('should parse an assignee who is called like a date as assignee', () => {
|
||||
const text = `Lorem Ipsum @today`
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('today')
|
||||
})
|
||||
it('should recognize an email address', () => {
|
||||
const text = 'Lorem Ipsum @email@example.com'
|
||||
const result = parseTaskText(text)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum @email@example.com')
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('email@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
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},
|
||||
'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},
|
||||
'annually': {type: 'years', amount: 1},
|
||||
'biannually': {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},
|
||||
} as Record<string, IRepeatAfter>
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
const wordCases = [
|
||||
'annually',
|
||||
'biannually',
|
||||
'semiannually',
|
||||
'biennially',
|
||||
'daily',
|
||||
'hourly',
|
||||
'monthly',
|
||||
'weekly',
|
||||
'yearly',
|
||||
]
|
||||
|
||||
wordCases.forEach(c => {
|
||||
it(`should ignore recurring periods if they are part of a word ${c}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum word${c}notword`)
|
||||
|
||||
expect(result.text).toBe(`Lorem Ipsum word${c}notword`)
|
||||
expect(result?.repeats).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
298
frontend/src/modules/parseTaskText.ts
Normal file
298
frontend/src/modules/parseTaskText.ts
Normal file
@ -0,0 +1,298 @@
|
||||
import {parseDate} from '../helpers/time/parseDate'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
import {REPEAT_TYPES, type IRepeatAfter, type IRepeatType} from '@/types/IRepeatAfter'
|
||||
|
||||
const VIKUNJA_PREFIXES: Prefixes = {
|
||||
label: '*',
|
||||
project: '+',
|
||||
priority: '!',
|
||||
assignee: '@',
|
||||
}
|
||||
|
||||
const TODOIST_PREFIXES: Prefixes = {
|
||||
label: '@',
|
||||
project: '#',
|
||||
priority: '!',
|
||||
assignee: '+',
|
||||
}
|
||||
|
||||
export enum PrefixMode {
|
||||
Disabled = 'disabled',
|
||||
Default = 'vikunja',
|
||||
Todoist = 'todoist',
|
||||
}
|
||||
|
||||
export const PREFIXES = {
|
||||
[PrefixMode.Disabled]: undefined,
|
||||
[PrefixMode.Default]: VIKUNJA_PREFIXES,
|
||||
[PrefixMode.Todoist]: TODOIST_PREFIXES,
|
||||
}
|
||||
|
||||
interface repeatParsedResult {
|
||||
textWithoutMatched: string,
|
||||
repeats: IRepeatAfter | null,
|
||||
}
|
||||
|
||||
export interface ParsedTaskText {
|
||||
text: string,
|
||||
date: Date | null,
|
||||
labels: string[],
|
||||
project: string | null,
|
||||
priority: number | null,
|
||||
assignees: string[],
|
||||
repeats: IRepeatAfter | null,
|
||||
}
|
||||
|
||||
interface Prefixes {
|
||||
label: string,
|
||||
project: string,
|
||||
priority: string,
|
||||
assignee: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses task text for dates, assignees, labels, projects, priorities and returns an object with all found intents.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default): ParsedTaskText => {
|
||||
const result: ParsedTaskText = {
|
||||
text: text,
|
||||
date: null,
|
||||
labels: [],
|
||||
project: null,
|
||||
priority: null,
|
||||
assignees: [],
|
||||
repeats: null,
|
||||
}
|
||||
|
||||
const prefixes = PREFIXES[prefixesMode]
|
||||
if (prefixes === undefined) {
|
||||
return result
|
||||
}
|
||||
|
||||
result.labels = getLabelsFromPrefix(text, prefixesMode) ?? []
|
||||
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
|
||||
|
||||
result.project = getProjectFromPrefix(result.text, prefixesMode)
|
||||
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
|
||||
|
||||
result.priority = getPriority(result.text, prefixes.priority)
|
||||
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
|
||||
|
||||
result.assignees = getItemsFromPrefix(result.text, prefixes.assignee)
|
||||
|
||||
const {textWithoutMatched, repeats} = getRepeats(result.text)
|
||||
result.text = textWithoutMatched
|
||||
result.repeats = repeats
|
||||
|
||||
const {newText, date} = parseDate(result.text)
|
||||
result.text = newText
|
||||
result.date = date
|
||||
|
||||
return cleanupResult(result, prefixes)
|
||||
}
|
||||
|
||||
const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
||||
const items: string[] = []
|
||||
|
||||
const itemParts = text.split(' ' + prefix)
|
||||
if (text.startsWith(prefix)) {
|
||||
const firstItem = text.split(prefix)[1]
|
||||
itemParts.unshift(firstItem)
|
||||
}
|
||||
|
||||
itemParts.forEach((p, index) => {
|
||||
// First part contains the rest
|
||||
if (index < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (p.startsWith(prefix)) {
|
||||
p = p.substring(1)
|
||||
}
|
||||
|
||||
let itemText
|
||||
if (p.charAt(0) === '\'') {
|
||||
itemText = p.split('\'')[1]
|
||||
} else if (p.charAt(0) === '"') {
|
||||
itemText = p.split('"')[1]
|
||||
} else {
|
||||
// Only until the next space
|
||||
itemText = p.split(' ')[0]
|
||||
}
|
||||
|
||||
if (itemText !== '') {
|
||||
items.push(itemText)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(new Set(items))
|
||||
}
|
||||
|
||||
export const getProjectFromPrefix = (text: string, prefixMode: PrefixMode): string | null => {
|
||||
const projectPrefix = PREFIXES[prefixMode]?.project
|
||||
if(typeof projectPrefix === 'undefined') {
|
||||
return null
|
||||
}
|
||||
const projects: string[] = getItemsFromPrefix(text, projectPrefix)
|
||||
return projects.length > 0 ? projects[0] : null
|
||||
}
|
||||
|
||||
export const getLabelsFromPrefix = (text: string, prefixMode: PrefixMode): string[] | null => {
|
||||
const labelsPrefix = PREFIXES[prefixMode]?.label
|
||||
if(typeof labelsPrefix === 'undefined') {
|
||||
return null
|
||||
}
|
||||
return getItemsFromPrefix(text, labelsPrefix)
|
||||
}
|
||||
|
||||
const getPriority = (text: string, prefix: string): number | null => {
|
||||
const ps = getItemsFromPrefix(text, prefix)
|
||||
if (ps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const p of ps) {
|
||||
for (const pi of Object.values(PRIORITIES)) {
|
||||
if (pi === parseInt(p)) {
|
||||
return parseInt(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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?))|(annually|biannually|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[5] ? results[5].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[5] ? parseInt(results[5]) : 1
|
||||
}
|
||||
let type: IRepeatType = REPEAT_TYPES.Hours
|
||||
|
||||
switch (results[2]) {
|
||||
case 'biennially':
|
||||
type = REPEAT_TYPES.Years
|
||||
amount = 2
|
||||
break
|
||||
case 'biannually':
|
||||
case 'semiannually':
|
||||
type = REPEAT_TYPES.Months
|
||||
amount = 6
|
||||
break
|
||||
case 'yearly':
|
||||
case 'annually':
|
||||
type = REPEAT_TYPES.Years
|
||||
break
|
||||
case 'daily':
|
||||
type = REPEAT_TYPES.Days
|
||||
break
|
||||
case 'hourly':
|
||||
type = REPEAT_TYPES.Hours
|
||||
break
|
||||
case 'monthly':
|
||||
type = REPEAT_TYPES.Months
|
||||
break
|
||||
case 'weekly':
|
||||
type = REPEAT_TYPES.Weeks
|
||||
break
|
||||
default:
|
||||
switch (results[7]) {
|
||||
case 'hour':
|
||||
case 'hours':
|
||||
type = REPEAT_TYPES.Hours
|
||||
break
|
||||
case 'day':
|
||||
case 'days':
|
||||
type = REPEAT_TYPES.Days
|
||||
break
|
||||
case 'week':
|
||||
case 'weeks':
|
||||
type = REPEAT_TYPES.Weeks
|
||||
break
|
||||
case 'month':
|
||||
case 'months':
|
||||
type = REPEAT_TYPES.Months
|
||||
break
|
||||
case 'year':
|
||||
case 'years':
|
||||
type = REPEAT_TYPES.Years
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
textWithoutMatched: text.replace(results[0], ''),
|
||||
repeats: {
|
||||
amount,
|
||||
type,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const cleanupItemText = (text: string, items: string[], prefix: string): string => {
|
||||
items.forEach(l => {
|
||||
if (l === '') {
|
||||
return
|
||||
}
|
||||
text = text
|
||||
.replace(new RegExp(`\\${prefix}'${l}' `, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}'${l}'`, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}"${l}" `, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}"${l}"`, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}${l} `, 'ig'), '')
|
||||
.replace(new RegExp(`\\${prefix}${l}`, 'ig'), '')
|
||||
})
|
||||
return text
|
||||
}
|
||||
|
||||
const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => {
|
||||
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
|
||||
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
|
||||
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
|
||||
// Not removing assignees to avoid removing @text where the user does not exist
|
||||
result.text = result.text.trim()
|
||||
|
||||
return result
|
||||
}
|
82
frontend/src/modules/projectHistory.test.ts
Normal file
82
frontend/src/modules/projectHistory.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {test, expect, vi} from 'vitest'
|
||||
import {getHistory, removeProjectFromHistory, saveProjectToHistory} from './projectHistory'
|
||||
|
||||
test('return an empty history when none was saved', () => {
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('return a saved history', () => {
|
||||
const saved = [{id: 1}, {id: 2}]
|
||||
Storage.prototype.getItem = vi.fn(() => JSON.stringify(saved))
|
||||
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual(saved)
|
||||
})
|
||||
|
||||
test('store project in history', () => {
|
||||
let saved = {}
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
Storage.prototype.setItem = vi.fn((key, projects) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveProjectToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1}]')
|
||||
})
|
||||
|
||||
test('store only the last 5 projects in history', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveProjectToHistory({id: 1})
|
||||
saveProjectToHistory({id: 2})
|
||||
saveProjectToHistory({id: 3})
|
||||
saveProjectToHistory({id: 4})
|
||||
saveProjectToHistory({id: 5})
|
||||
saveProjectToHistory({id: 6})
|
||||
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
|
||||
})
|
||||
|
||||
test('don\'t store the same project twice', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveProjectToHistory({id: 1})
|
||||
saveProjectToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1}]')
|
||||
})
|
||||
|
||||
test('move a project to the beginning when storing it multiple times', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveProjectToHistory({id: 1})
|
||||
saveProjectToHistory({id: 2})
|
||||
saveProjectToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1},{"id":2}]')
|
||||
})
|
||||
|
||||
test('remove project from history', () => {
|
||||
let saved: string | null = '[{"id": 1}]'
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
Storage.prototype.removeItem = vi.fn((key: string) => {
|
||||
saved = null
|
||||
})
|
||||
|
||||
removeProjectFromHistory({id: 1})
|
||||
expect(saved).toBeNull()
|
||||
})
|
51
frontend/src/modules/projectHistory.ts
Normal file
51
frontend/src/modules/projectHistory.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export interface ProjectHistory {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function getHistory(): ProjectHistory[] {
|
||||
const savedHistory = localStorage.getItem('projectHistory')
|
||||
if (savedHistory === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
return JSON.parse(savedHistory)
|
||||
}
|
||||
|
||||
function saveHistory(history: ProjectHistory[]) {
|
||||
if (history.length === 0) {
|
||||
localStorage.removeItem('projectHistory')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('projectHistory', JSON.stringify(history))
|
||||
}
|
||||
|
||||
export function saveProjectToHistory(project: ProjectHistory) {
|
||||
const history: ProjectHistory[] = getHistory()
|
||||
|
||||
// Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning
|
||||
history.forEach((l, i) => {
|
||||
if (l.id === project.id) {
|
||||
history.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
||||
// Add the new project to the beginning of the project
|
||||
history.unshift(project)
|
||||
|
||||
if (history.length > 5) {
|
||||
history.pop()
|
||||
}
|
||||
saveHistory(history)
|
||||
}
|
||||
|
||||
export function removeProjectFromHistory(project: ProjectHistory) {
|
||||
const history: ProjectHistory[] = getHistory()
|
||||
|
||||
history.forEach((l, i) => {
|
||||
if (l.id === project.id) {
|
||||
history.splice(i, 1)
|
||||
}
|
||||
})
|
||||
saveHistory(history)
|
||||
}
|
Reference in New Issue
Block a user