mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-31 11:40:47 +00:00
perf: lazy load ical.js and chrono-node to reduce initial bundle size
Lazy load ical.js (~76 KB) in CalDAV client service using the existing loadIcalModule() pattern and lazy load chrono-node (~150 KB) in short-syntax.ts by making date parsing async. Reduces initial bundle from 5.54 MB to 5.42 MB (120 KB savings, well under 5.50 MB budget).
This commit is contained in:
parent
cdd29f7789
commit
de08bc83bf
11 changed files with 510 additions and 473 deletions
|
|
@ -4,8 +4,7 @@ import { CaldavCfg } from './caldav.model';
|
|||
import DavClient, { namespaces as NS } from '@nextcloud/cdav-library';
|
||||
// @ts-ignore
|
||||
import Calendar from 'cdav-library/models/calendar';
|
||||
// @ts-ignore
|
||||
import ICAL from 'ical.js';
|
||||
import { loadIcalModule } from '../../../schedule/ical/ical-lazy-loader';
|
||||
|
||||
import { from, Observable, throwError } from 'rxjs';
|
||||
import { CaldavIssue, CaldavIssueStatus } from './caldav-issue.model';
|
||||
|
|
@ -122,7 +121,8 @@ export class CaldavClientService {
|
|||
return await calendar.calendarQuery([query]);
|
||||
}
|
||||
|
||||
private static _mapTask(task: CalDavTaskData): CaldavIssue {
|
||||
private static async _mapTask(task: CalDavTaskData): Promise<CaldavIssue> {
|
||||
const ICAL = await loadIcalModule();
|
||||
const jCal = ICAL.parse(task.data);
|
||||
const comp = new ICAL.Component(jCal);
|
||||
const todo = comp.getFirstSubcomponent('vtodo');
|
||||
|
|
@ -140,8 +140,8 @@ export class CaldavClientService {
|
|||
completed: !!todo.getFirstPropertyValue('completed'),
|
||||
item_url: task.url,
|
||||
summary: (todo.getFirstPropertyValue('summary') as string) || '',
|
||||
start: (todo.getFirstPropertyValue('dtstart') as ICAL.Time)?.toJSDate().getTime(),
|
||||
due: (todo.getFirstPropertyValue('due') as ICAL.Time)?.toJSDate().getTime(),
|
||||
start: (todo.getFirstPropertyValue('dtstart') as any)?.toJSDate().getTime(),
|
||||
due: (todo.getFirstPropertyValue('due') as any)?.toJSDate().getTime(),
|
||||
note: (todo.getFirstPropertyValue('description') as string) || undefined,
|
||||
status: (todo.getFirstPropertyValue('status') as CaldavIssueStatus) || undefined,
|
||||
priority: +(todo.getFirstPropertyValue('priority') as string) || undefined,
|
||||
|
|
@ -347,12 +347,13 @@ export class CaldavClientService {
|
|||
const tasks = await CaldavClientService._getAllTodos(cal, filterOpen).catch((err) =>
|
||||
this._handleNetErr(err),
|
||||
);
|
||||
return tasks
|
||||
.map((t) => CaldavClientService._mapTask(t))
|
||||
.filter(
|
||||
(t: CaldavIssue) =>
|
||||
!filterCategory || !cfg.categoryFilter || t.labels.includes(cfg.categoryFilter),
|
||||
);
|
||||
const mappedTasks = await Promise.all(
|
||||
tasks.map((t) => CaldavClientService._mapTask(t)),
|
||||
);
|
||||
return mappedTasks.filter(
|
||||
(t: CaldavIssue) =>
|
||||
!filterCategory || !cfg.categoryFilter || t.labels.includes(cfg.categoryFilter),
|
||||
);
|
||||
}
|
||||
|
||||
private async _getTask(cfg: CaldavCfg, uid: string): Promise<CaldavIssue> {
|
||||
|
|
@ -369,7 +370,7 @@ export class CaldavClientService {
|
|||
throw new Error('ISSUE NOT FOUND: ' + uid);
|
||||
}
|
||||
|
||||
return CaldavClientService._mapTask(task[0]);
|
||||
return await CaldavClientService._mapTask(task[0]);
|
||||
}
|
||||
|
||||
private async _updateTask(
|
||||
|
|
@ -406,6 +407,7 @@ export class CaldavClientService {
|
|||
}
|
||||
|
||||
const task = tasks[0];
|
||||
const ICAL = await loadIcalModule();
|
||||
const jCal = ICAL.parse(task.data);
|
||||
const comp = new ICAL.Component(jCal);
|
||||
const todo = comp.getFirstSubcomponent('vtodo');
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Eager import ensures ical.js is in the webpack test bundle,
|
||||
// so the dynamic import() inside loadIcalModule() can resolve it.
|
||||
import 'ical.js';
|
||||
import { getRelevantEventsForCalendarIntegrationFromIcal } from './get-relevant-events-from-ical';
|
||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Eager import ensures ical.js is in the webpack test bundle,
|
||||
// so the dynamic import() inside loadIcalModule() can resolve it.
|
||||
import 'ical.js';
|
||||
import { getRelevantEventsForCalendarIntegrationFromIcal } from './get-relevant-events-from-ical';
|
||||
|
||||
describe('getRelevantEventsForCalendarIntegrationFromIcal', () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Eager import ensures ical.js is in the webpack test bundle,
|
||||
// so the dynamic import() inside loadIcalModule() can resolve it.
|
||||
import 'ical.js';
|
||||
import { loadIcalModule } from './ical-lazy-loader';
|
||||
|
||||
describe('ical-lazy-loader', () => {
|
||||
|
|
|
|||
|
|
@ -93,18 +93,18 @@ describe('AddTaskBarParserService', () => {
|
|||
mockStateService.updateAttachments.calls.reset();
|
||||
});
|
||||
|
||||
it('should handle empty text', () => {
|
||||
service.parseAndUpdateText('', null, [], [], null as any);
|
||||
it('should handle empty text', async () => {
|
||||
await service.parseAndUpdateText('', null, [], [], null as any);
|
||||
expect(mockStateService.updateCleanText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle null config', () => {
|
||||
service.parseAndUpdateText('test task', null, [], [], null as any);
|
||||
it('should handle null config', async () => {
|
||||
await service.parseAndUpdateText('test task', null, [], [], null as any);
|
||||
expect(mockStateService.updateCleanText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Date Parsing', () => {
|
||||
it('should handle default date when no date syntax present and no current state', () => {
|
||||
it('should handle default date when no date syntax present and no current state', async () => {
|
||||
const defaultDate = '2024-01-15';
|
||||
const defaultTime = '09:00';
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -141,7 +141,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(time).toBe(defaultTime);
|
||||
});
|
||||
|
||||
it('should preserve current date/time when no syntax present', () => {
|
||||
it('should preserve current date/time when no syntax present', async () => {
|
||||
const currentDate = '2024-02-20';
|
||||
const currentTime = '14:30';
|
||||
|
||||
|
|
@ -161,7 +161,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task without date syntax',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -175,7 +175,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(time).toBe(currentTime);
|
||||
});
|
||||
|
||||
it('should preserve current date but use default time when no current time', () => {
|
||||
it('should preserve current date but use default time when no current time', async () => {
|
||||
const currentDate = '2024-02-20';
|
||||
const defaultTime = '09:00';
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task text',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -211,8 +211,8 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(time).toBe(defaultTime);
|
||||
});
|
||||
|
||||
it('should handle no date or default date', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should handle no date or default date', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -226,10 +226,10 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(time).toBeNull();
|
||||
});
|
||||
|
||||
it('should test date parsing integration with shortSyntax', () => {
|
||||
it('should test date parsing integration with shortSyntax', async () => {
|
||||
// Since shortSyntax is complex and depends on external implementation,
|
||||
// we'll test the parser's handling of the parsed results
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task with date syntax that may or may not be parsed',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -241,11 +241,11 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateCleanText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle default date and time when no syntax is found', () => {
|
||||
it('should handle default date and time when no syntax is found', async () => {
|
||||
const defaultDate = '2024-01-15';
|
||||
const defaultTime = '09:00';
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Plain text task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -263,8 +263,8 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('Parsing Integration', () => {
|
||||
it('should call updateEstimate when parsing text', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should call updateEstimate when parsing text', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task with potential time estimate',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -275,8 +275,8 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateEstimate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle null time estimates', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should handle null time estimates', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -287,8 +287,8 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateEstimate).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('should call updateSpent when parsing text', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should call updateSpent when parsing text', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task with potential time spent',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -299,8 +299,8 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateSpent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle null time spent', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should handle null time spent', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -313,8 +313,8 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('Basic Parsing Tests', () => {
|
||||
it('should update tags when parsing text', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should update tags when parsing text', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task with tags',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -326,10 +326,10 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateNewTagTitles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle auto-detected projects', () => {
|
||||
it('should handle auto-detected projects', async () => {
|
||||
mockStateService.isAutoDetected.and.returnValue(false);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -342,8 +342,8 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateDate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle text with clean text update', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should handle text with clean text update', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task text',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -354,22 +354,22 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateCleanText).toHaveBeenCalledWith('Task text');
|
||||
});
|
||||
|
||||
it('should handle edge cases gracefully', () => {
|
||||
it('should handle edge cases gracefully', async () => {
|
||||
const longText = 'Task ' + 'a'.repeat(1000);
|
||||
|
||||
expect(() => {
|
||||
await expectAsync(
|
||||
service.parseAndUpdateText(
|
||||
longText,
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
mockTags,
|
||||
mockDefaultProject,
|
||||
);
|
||||
}).not.toThrow();
|
||||
),
|
||||
).not.toBeRejected();
|
||||
});
|
||||
|
||||
it('should handle empty arrays for projects and tags', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should handle empty arrays for projects and tags', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task with no matching items',
|
||||
mockConfig,
|
||||
[],
|
||||
|
|
@ -381,8 +381,8 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateNewTagTitles).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle special characters in task text', () => {
|
||||
service.parseAndUpdateText(
|
||||
it('should handle special characters in task text', async () => {
|
||||
await service.parseAndUpdateText(
|
||||
'Task with special chars !@#$%^&*()',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -396,42 +396,42 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('resetPreviousResult', () => {
|
||||
it('should reset previous result without error', () => {
|
||||
it('should reset previous result without error', async () => {
|
||||
expect(() => service.resetPreviousResult()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeShortSyntaxFromInput', () => {
|
||||
it('should return same input for empty string', () => {
|
||||
it('should return same input for empty string', async () => {
|
||||
expect(service.removeShortSyntaxFromInput('', 'tags')).toBe('');
|
||||
});
|
||||
|
||||
describe('tags removal', () => {
|
||||
it('should remove specific tag', () => {
|
||||
it('should remove specific tag', async () => {
|
||||
const input = 'Task #important #urgent';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags', 'important');
|
||||
expect(result).toBe('Task #urgent');
|
||||
});
|
||||
|
||||
it('should remove all tags when no specific tag provided', () => {
|
||||
it('should remove all tags when no specific tag provided', async () => {
|
||||
const input = 'Task #important #urgent #todo';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags');
|
||||
expect(result).toBe('Task');
|
||||
});
|
||||
|
||||
it('should handle tag removal case insensitively', () => {
|
||||
it('should handle tag removal case insensitively', async () => {
|
||||
const input = 'Task #Important #URGENT';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags', 'important');
|
||||
expect(result).toBe('Task #URGENT');
|
||||
});
|
||||
|
||||
it('should handle tags at the beginning of input', () => {
|
||||
it('should handle tags at the beginning of input', async () => {
|
||||
const input = '#urgent Task content';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags', 'urgent');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
|
||||
it('should handle tags at the end of input', () => {
|
||||
it('should handle tags at the end of input', async () => {
|
||||
const input = 'Task content #urgent';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags', 'urgent');
|
||||
expect(result).toBe('Task content');
|
||||
|
|
@ -439,25 +439,25 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('date removal', () => {
|
||||
it('should remove date syntax', () => {
|
||||
it('should remove date syntax', async () => {
|
||||
const input = 'Task @today @16:30 @2024-01-15';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'date');
|
||||
expect(result).toBe('Task');
|
||||
});
|
||||
|
||||
it('should handle complex date formats', () => {
|
||||
it('should handle complex date formats', async () => {
|
||||
const input = 'Meeting @tomorrow @next-week @2024-12-25';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'date');
|
||||
expect(result).toBe('Meeting');
|
||||
});
|
||||
|
||||
it('should handle date at beginning', () => {
|
||||
it('should handle date at beginning', async () => {
|
||||
const input = '@today Task content';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'date');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
|
||||
it('should handle date at end', () => {
|
||||
it('should handle date at end', async () => {
|
||||
const input = 'Task content @today';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'date');
|
||||
expect(result).toBe('Task content');
|
||||
|
|
@ -465,7 +465,7 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('estimate removal', () => {
|
||||
it('should remove various time estimate formats', () => {
|
||||
it('should remove various time estimate formats', async () => {
|
||||
const testCases = [
|
||||
{ input: 'Task t30m', expected: 'Task' },
|
||||
{ input: 'Task 1h', expected: 'Task' },
|
||||
|
|
@ -484,63 +484,63 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should handle multiple time estimates', () => {
|
||||
it('should handle multiple time estimates', async () => {
|
||||
const input = 'Task t30m another t1h final';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'estimate');
|
||||
expect(result).toBe('Task another final');
|
||||
});
|
||||
|
||||
it('should handle time estimate at beginning', () => {
|
||||
it('should handle time estimate at beginning', async () => {
|
||||
const input = 't30m Task content';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'estimate');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
|
||||
it('should handle time estimate at end', () => {
|
||||
it('should handle time estimate at end', async () => {
|
||||
const input = 'Task content t30m';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'estimate');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
|
||||
it('should handle decimal hours', () => {
|
||||
it('should handle decimal hours', async () => {
|
||||
const input = 'Task t2.5h content';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'estimate');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
|
||||
it('should handle days format', () => {
|
||||
it('should handle days format', async () => {
|
||||
const input = 'Task 3d content';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'estimate');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
});
|
||||
|
||||
it('should clean up extra whitespace', () => {
|
||||
it('should clean up extra whitespace', async () => {
|
||||
const input = 'Task #tag @today t30m end';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags');
|
||||
expect(result).toBe('Task @today t30m end');
|
||||
});
|
||||
|
||||
it('should trim final result', () => {
|
||||
it('should trim final result', async () => {
|
||||
const input = ' #tag Task #another ';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags');
|
||||
expect(result).toBe('Task');
|
||||
});
|
||||
|
||||
it('should handle mixed removal scenarios', () => {
|
||||
it('should handle mixed removal scenarios', async () => {
|
||||
// Remove all tags and clean up spacing
|
||||
const input = 'Task #urgent #important content #todo';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags');
|
||||
expect(result).toBe('Task content');
|
||||
});
|
||||
|
||||
it('should handle empty input after removal', () => {
|
||||
it('should handle empty input after removal', async () => {
|
||||
const input = '#tag1 #tag2';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle whitespace-only input after removal', () => {
|
||||
it('should handle whitespace-only input after removal', async () => {
|
||||
const input = ' #tag1 #tag2 ';
|
||||
const result = service.removeShortSyntaxFromInput(input, 'tags');
|
||||
expect(result).toBe('');
|
||||
|
|
@ -578,7 +578,7 @@ describe('AddTaskBarParserService', () => {
|
|||
];
|
||||
});
|
||||
|
||||
it('should handle date strings consistently across timezones', () => {
|
||||
it('should handle date strings consistently across timezones', async () => {
|
||||
const dateStr = '2025-01-15';
|
||||
const timeStr = '14:30';
|
||||
|
||||
|
|
@ -598,7 +598,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task with date',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -612,7 +612,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateDate).toHaveBeenCalledWith(dateStr, timeStr);
|
||||
});
|
||||
|
||||
it('should handle dates near DST transitions', () => {
|
||||
it('should handle dates near DST transitions', async () => {
|
||||
// Test spring forward (March DST transition in many timezones)
|
||||
const springDateStr = '2024-03-10';
|
||||
const springTimeStr = '02:30';
|
||||
|
|
@ -632,7 +632,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'DST test task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -648,7 +648,7 @@ describe('AddTaskBarParserService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should handle dates at year boundaries', () => {
|
||||
it('should handle dates at year boundaries', async () => {
|
||||
// Test New Year's Eve
|
||||
const newYearDateStr = '2024-12-31';
|
||||
const newYearTimeStr = '23:59';
|
||||
|
|
@ -668,7 +668,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'New Year task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -684,7 +684,7 @@ describe('AddTaskBarParserService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should maintain date consistency when parsing date-only strings', () => {
|
||||
it('should maintain date consistency when parsing date-only strings', async () => {
|
||||
const dateStr = '2025-01-01';
|
||||
|
||||
const mockState = {
|
||||
|
|
@ -702,7 +702,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -716,7 +716,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateDate).toHaveBeenCalledWith(dateStr, null);
|
||||
});
|
||||
|
||||
it('should handle midnight time correctly', () => {
|
||||
it('should handle midnight time correctly', async () => {
|
||||
const dateStr = '2025-01-15';
|
||||
const midnightTime = '00:00';
|
||||
|
||||
|
|
@ -735,7 +735,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Midnight task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -758,13 +758,13 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('_arraysEqual', () => {
|
||||
it('should return true for equal arrays', () => {
|
||||
it('should return true for equal arrays', async () => {
|
||||
expect(serviceAny._arraysEqual(['a', 'b'], ['a', 'b'])).toBe(true);
|
||||
expect(serviceAny._arraysEqual([], [])).toBe(true);
|
||||
expect(serviceAny._arraysEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different arrays', () => {
|
||||
it('should return false for different arrays', async () => {
|
||||
expect(serviceAny._arraysEqual(['a', 'b'], ['a', 'c'])).toBe(false);
|
||||
expect(serviceAny._arraysEqual(['a'], ['a', 'b'])).toBe(false);
|
||||
expect(serviceAny._arraysEqual([1, 2], [2, 1])).toBe(false);
|
||||
|
|
@ -772,7 +772,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(serviceAny._arraysEqual([], [1])).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null and undefined values', () => {
|
||||
it('should handle null and undefined values', async () => {
|
||||
expect(serviceAny._arraysEqual([null], [null])).toBe(true);
|
||||
expect(serviceAny._arraysEqual([undefined], [undefined])).toBe(true);
|
||||
expect(serviceAny._arraysEqual([null], [undefined])).toBe(false);
|
||||
|
|
@ -780,14 +780,14 @@ describe('AddTaskBarParserService', () => {
|
|||
});
|
||||
|
||||
describe('_datesEqual', () => {
|
||||
it('should return true for equal date strings', () => {
|
||||
it('should return true for equal date strings', async () => {
|
||||
const dateStr1 = '2024-01-15';
|
||||
const dateStr2 = '2024-01-15';
|
||||
expect(serviceAny._datesEqual(dateStr1, dateStr2)).toBe(true);
|
||||
expect(serviceAny._datesEqual(null, null)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different date strings', () => {
|
||||
it('should return false for different date strings', async () => {
|
||||
const dateStr1 = '2024-01-15';
|
||||
const dateStr2 = '2024-01-16';
|
||||
expect(serviceAny._datesEqual(dateStr1, dateStr2)).toBe(false);
|
||||
|
|
@ -795,13 +795,13 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(serviceAny._datesEqual(null, dateStr1)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle same date strings', () => {
|
||||
it('should handle same date strings', async () => {
|
||||
const dateStr1 = '2024-01-15';
|
||||
const dateStr2 = '2024-01-15';
|
||||
expect(serviceAny._datesEqual(dateStr1, dateStr2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle different date strings', () => {
|
||||
it('should handle different date strings', async () => {
|
||||
const dateStr1 = '2024-01-15';
|
||||
const dateStr2 = '2024-01-16';
|
||||
expect(serviceAny._datesEqual(dateStr1, dateStr2)).toBe(false);
|
||||
|
|
@ -835,7 +835,7 @@ describe('AddTaskBarParserService', () => {
|
|||
mockStateService.updateAttachments.calls.reset();
|
||||
});
|
||||
|
||||
it('should extract single HTTPS URL and update state', () => {
|
||||
it('should extract single HTTPS URL and update state', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -851,7 +851,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task https://example.com',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -868,7 +868,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateCleanText).toHaveBeenCalledWith('Task');
|
||||
});
|
||||
|
||||
it('should extract file:// URL with FILE type', () => {
|
||||
it('should extract file:// URL with FILE type', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -884,7 +884,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Document file:///home/user/doc.pdf',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -901,7 +901,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateCleanText).toHaveBeenCalledWith('Document');
|
||||
});
|
||||
|
||||
it('should detect image URLs as IMG type', () => {
|
||||
it('should detect image URLs as IMG type', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -917,7 +917,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Screenshot https://example.com/image.png',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -932,7 +932,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(attachments[0].icon).toBe('image');
|
||||
});
|
||||
|
||||
it('should extract multiple URLs', () => {
|
||||
it('should extract multiple URLs', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -948,7 +948,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Links https://example.com www.test.org',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -964,7 +964,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateCleanText).toHaveBeenCalledWith('Links');
|
||||
});
|
||||
|
||||
it('should work with combined short syntax (URL + date + tag + estimate)', () => {
|
||||
it('should work with combined short syntax (URL + date + tag + estimate)', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -983,7 +983,7 @@ describe('AddTaskBarParserService', () => {
|
|||
const mockTag = { id: 'urgent-id', title: 'urgent' } as Tag;
|
||||
const tagsWithUrgent = [mockTag];
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task https://github.com/pr/123 @tomorrow #urgent 30m',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -1006,7 +1006,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateTagIdsFromTxt).toHaveBeenCalledWith(['urgent-id']);
|
||||
});
|
||||
|
||||
it('should not extract URLs from empty text', () => {
|
||||
it('should not extract URLs from empty text', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -1022,7 +1022,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -1033,7 +1033,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateAttachments).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update attachments when URL changes', () => {
|
||||
it('should update attachments when URL changes', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -1050,7 +1050,7 @@ describe('AddTaskBarParserService', () => {
|
|||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
// First parse
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task https://example.com',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -1065,7 +1065,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(firstAttachments[0].path).toBe('https://example.com');
|
||||
|
||||
// Change URL
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task https://different.com',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -1080,7 +1080,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(secondAttachments[0].path).toBe('https://different.com');
|
||||
});
|
||||
|
||||
it('should clear attachments when URL removed from text', () => {
|
||||
it('should clear attachments when URL removed from text', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -1097,7 +1097,7 @@ describe('AddTaskBarParserService', () => {
|
|||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
// First parse with URL
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task https://example.com',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -1108,7 +1108,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(mockStateService.updateAttachments).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Remove URL from text
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
@ -1121,7 +1121,7 @@ describe('AddTaskBarParserService', () => {
|
|||
expect(attachments.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle www URLs correctly', () => {
|
||||
it('should handle www URLs correctly', async () => {
|
||||
const mockState = {
|
||||
projectId: 'default-project',
|
||||
tagIds: [],
|
||||
|
|
@ -1137,7 +1137,7 @@ describe('AddTaskBarParserService', () => {
|
|||
};
|
||||
mockStateService.state.and.returnValue(mockState);
|
||||
|
||||
service.parseAndUpdateText(
|
||||
await service.parseAndUpdateText(
|
||||
'Task www.example.com',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export class AddTaskBarParserService {
|
|||
return a === b;
|
||||
}
|
||||
|
||||
parseAndUpdateText(
|
||||
async parseAndUpdateText(
|
||||
text: string,
|
||||
config: ShortSyntaxConfig | null,
|
||||
allProjects: Project[],
|
||||
|
|
@ -43,7 +43,7 @@ export class AddTaskBarParserService {
|
|||
defaultProject: Project,
|
||||
defaultDate?: string,
|
||||
defaultTime?: string,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
if (!text || !config) {
|
||||
this._previousParseResult = null;
|
||||
return;
|
||||
|
|
@ -51,7 +51,7 @@ export class AddTaskBarParserService {
|
|||
|
||||
// Get current tags from state to preserve pre-selected tags
|
||||
const currentState = this._stateService.state();
|
||||
const parseResult = shortSyntax(
|
||||
const parseResult = await shortSyntax(
|
||||
{ title: text, tagIds: currentState.tagIdsFromTxt },
|
||||
config,
|
||||
allTags,
|
||||
|
|
|
|||
|
|
@ -40,11 +40,12 @@ import {
|
|||
first,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
timeout,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, from, Observable } from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confirm.component';
|
||||
import {
|
||||
|
|
@ -228,14 +229,16 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
|
|||
this._workContextService.activeWorkContext$,
|
||||
this._globalConfigService.shortSyntax$,
|
||||
),
|
||||
map(([val, tags, projects, activeWorkContext, shortSyntaxConfig]) =>
|
||||
shortSyntaxToTags({
|
||||
val,
|
||||
tags,
|
||||
projects,
|
||||
defaultColor: activeWorkContext?.theme?.primary || DEFAULT_PROJECT_COLOR,
|
||||
shortSyntaxConfig,
|
||||
}),
|
||||
switchMap(([val, tags, projects, activeWorkContext, shortSyntaxConfig]) =>
|
||||
from(
|
||||
shortSyntaxToTags({
|
||||
val,
|
||||
tags,
|
||||
projects,
|
||||
defaultColor: activeWorkContext?.theme?.primary || DEFAULT_PROJECT_COLOR,
|
||||
shortSyntaxConfig,
|
||||
}),
|
||||
),
|
||||
),
|
||||
startWith([]),
|
||||
);
|
||||
|
|
@ -354,21 +357,26 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
|
|||
this.defaultProject$,
|
||||
this.defaultDateAndTime$,
|
||||
])
|
||||
.pipe(takeUntilDestroyed(this._destroyRef))
|
||||
.subscribe(
|
||||
([title, config, allTags, allProjects, defaultProject, defaultDateInfo]) => {
|
||||
const { date, time } = defaultDateInfo;
|
||||
this._parserService.parseAndUpdateText(
|
||||
title || '',
|
||||
config,
|
||||
allProjects,
|
||||
allTags,
|
||||
defaultProject!,
|
||||
date,
|
||||
time,
|
||||
);
|
||||
},
|
||||
);
|
||||
.pipe(
|
||||
switchMap(
|
||||
([title, config, allTags, allProjects, defaultProject, defaultDateInfo]) => {
|
||||
const { date, time } = defaultDateInfo;
|
||||
return from(
|
||||
this._parserService.parseAndUpdateText(
|
||||
title || '',
|
||||
config,
|
||||
allProjects,
|
||||
allTags,
|
||||
defaultProject!,
|
||||
date,
|
||||
time,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
takeUntilDestroyed(this._destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private _setupSuggestions(): void {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface ShortSyntaxTag {
|
|||
projectId?: string;
|
||||
}
|
||||
|
||||
export const shortSyntaxToTags = ({
|
||||
export const shortSyntaxToTags = async ({
|
||||
val,
|
||||
tags,
|
||||
projects,
|
||||
|
|
@ -31,8 +31,8 @@ export const shortSyntaxToTags = ({
|
|||
projects: Project[];
|
||||
defaultColor: string;
|
||||
shortSyntaxConfig: ShortSyntaxConfig;
|
||||
}): ShortSyntaxTag[] => {
|
||||
const r = shortSyntax(
|
||||
}): Promise<ShortSyntaxTag[]> => {
|
||||
const r = await shortSyntax(
|
||||
{
|
||||
title: val,
|
||||
tagIds: [],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
import { casual } from 'chrono-node';
|
||||
import { Task, TaskCopy } from './task.model';
|
||||
import { getDbDateStr } from '../../util/get-db-date-str';
|
||||
import { stringToMs } from '../../ui/duration/string-to-ms.pipe';
|
||||
|
|
@ -37,44 +36,53 @@ const CH_TAG = '#';
|
|||
const CH_DUE = '@';
|
||||
const ALL_SPECIAL = `(\\${CH_PRO}|\\${CH_TAG}|\\${CH_DUE})`;
|
||||
|
||||
const customDateParser = casual.clone();
|
||||
customDateParser.refiners.push({
|
||||
refine: (context, results) => {
|
||||
results.forEach((result) => {
|
||||
const { refDate, text, start } = result;
|
||||
const regex = / [5-9][0-9]$/;
|
||||
const yearIndex = text.search(regex);
|
||||
// The year pattern in Chrono's source code is (?:[1-9][0-9]{0,3}\\s{0,2}(?:BE|AD|BC|BCE|CE)|[1-2][0-9]{3}|[5-9][0-9]|2[0-5]).
|
||||
// This means any two-digit numeric value from 50 to 99 will be considered a year.
|
||||
// Link: https://github.com/wanasit/chrono/blob/54e7ff12f9185e735ee860c25922b2ab2367d40b/src/locales/en/constants.ts#L234C30-L234C108
|
||||
// When someone creates a task like "Test @25/4 90m", Chrono will return the year as 1990, which is an undesirable behaviour in most cases.
|
||||
if (yearIndex !== -1) {
|
||||
result.text = text.slice(0, yearIndex);
|
||||
const current = new Date();
|
||||
let year = current.getFullYear();
|
||||
// If the parsed month is smaller than the current month,
|
||||
// it means the time is for next year. For example, parsed month is March
|
||||
// and it is currently April
|
||||
const impliedDate = start.get('day');
|
||||
const impliedMonth = start.get('month');
|
||||
// Due to the future-forward nature of the date parser, there are two scenarios that the implied year is next year:
|
||||
// - Implied month is smaller than current month i.e. 20/3 vs 2/4
|
||||
// - Same month but the implied date is before the current date i.e. 14/4 vs 20/4
|
||||
if (
|
||||
(impliedMonth && impliedMonth < refDate.getMonth() + 1) ||
|
||||
(impliedMonth === refDate.getMonth() + 1 &&
|
||||
impliedDate &&
|
||||
impliedDate < refDate.getDate())
|
||||
) {
|
||||
// || (impliedMonth === refDate.getMonth() + 1 && impliedDay && impliedDay < refDate.getDay())
|
||||
year += 1;
|
||||
}
|
||||
result.start.assign('year', year);
|
||||
}
|
||||
let customDateParserPromise: Promise<any> | null = null;
|
||||
let customDateParserCache: any = null;
|
||||
|
||||
const loadCustomDateParser = (): Promise<any> => {
|
||||
if (customDateParserCache) {
|
||||
return Promise.resolve(customDateParserCache);
|
||||
}
|
||||
if (!customDateParserPromise) {
|
||||
customDateParserPromise = import('chrono-node').then(({ casual }) => {
|
||||
const parser = casual.clone();
|
||||
parser.refiners.push({
|
||||
refine: (context: unknown, results: any[]) => {
|
||||
results.forEach((result) => {
|
||||
const { refDate, text, start } = result;
|
||||
const regex = / [5-9][0-9]$/;
|
||||
const yearIndex = text.search(regex);
|
||||
// The year pattern in Chrono's source code is (?:[1-9][0-9]{0,3}\\s{0,2}(?:BE|AD|BC|BCE|CE)|[1-2][0-9]{3}|[5-9][0-9]|2[0-5]).
|
||||
// This means any two-digit numeric value from 50 to 99 will be considered a year.
|
||||
// Link: https://github.com/wanasit/chrono/blob/54e7ff12f9185e735ee860c25922b2ab2367d40b/src/locales/en/constants.ts#L234C30-L234C108
|
||||
// When someone creates a task like "Test @25/4 90m", Chrono will return
|
||||
// the year as 1990, which is an undesirable behaviour in most cases.
|
||||
if (yearIndex !== -1) {
|
||||
result.text = text.slice(0, yearIndex);
|
||||
const current = new Date();
|
||||
let year = current.getFullYear();
|
||||
const impliedDate = start.get('day');
|
||||
const impliedMonth = start.get('month');
|
||||
if (
|
||||
(impliedMonth && impliedMonth < refDate.getMonth() + 1) ||
|
||||
(impliedMonth === refDate.getMonth() + 1 &&
|
||||
impliedDate &&
|
||||
impliedDate < refDate.getDate())
|
||||
) {
|
||||
year += 1;
|
||||
}
|
||||
result.start.assign('year', year);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
},
|
||||
});
|
||||
customDateParserCache = parser;
|
||||
return parser;
|
||||
});
|
||||
return results;
|
||||
},
|
||||
});
|
||||
}
|
||||
return customDateParserPromise;
|
||||
};
|
||||
|
||||
// The following project name extraction pattern attempts to improve on the
|
||||
// previous version by not immediately terminating upon encountering a short
|
||||
|
|
@ -97,14 +105,14 @@ const SHORT_SYNTAX_URL_REG_EX = new RegExp(
|
|||
'gi',
|
||||
);
|
||||
|
||||
export const shortSyntax = (
|
||||
export const shortSyntax = async (
|
||||
task: Task | Partial<Task>,
|
||||
config: ShortSyntaxConfig,
|
||||
allTags?: Tag[],
|
||||
allProjects?: Project[],
|
||||
now = new Date(),
|
||||
mode: 'combine' | 'replace' = 'combine',
|
||||
):
|
||||
): Promise<
|
||||
| {
|
||||
taskChanges: Partial<Task>;
|
||||
newTagTitles: string[];
|
||||
|
|
@ -112,7 +120,8 @@ export const shortSyntax = (
|
|||
projectId: string | undefined;
|
||||
attachments: TaskAttachment[];
|
||||
}
|
||||
| undefined => {
|
||||
| undefined
|
||||
> => {
|
||||
if (!task.title) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -130,7 +139,10 @@ export const shortSyntax = (
|
|||
taskChanges = parseTimeSpentChanges(task);
|
||||
taskChanges = {
|
||||
...taskChanges,
|
||||
...parseScheduledDate({ ...task, title: taskChanges.title || task.title }, now),
|
||||
...(await parseScheduledDate(
|
||||
{ ...task, title: taskChanges.title || task.title },
|
||||
now,
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -365,14 +377,18 @@ const parseTagChanges = (
|
|||
};
|
||||
};
|
||||
|
||||
const parseScheduledDate = (task: Partial<TaskCopy>, now: Date): DueChanges => {
|
||||
const parseScheduledDate = async (
|
||||
task: Partial<TaskCopy>,
|
||||
now: Date,
|
||||
): Promise<DueChanges> => {
|
||||
if (!task.title) {
|
||||
return {};
|
||||
}
|
||||
const rr = task.title.match(SHORT_SYNTAX_DUE_REG_EX);
|
||||
|
||||
if (rr && rr[0]) {
|
||||
const parsedDateArr = customDateParser.parse(rr[0], now, {
|
||||
const dateParser = await loadCustomDateParser();
|
||||
const parsedDateArr = dateParser.parse(rr[0], now, {
|
||||
forwardDate: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { GlobalConfigService } from '../../config/global-config.service';
|
||||
import { unique } from '../../../util/unique';
|
||||
import { TaskService } from '../task.service';
|
||||
import { EMPTY, of } from 'rxjs';
|
||||
import { from, of } from 'rxjs';
|
||||
import { ProjectService } from '../../project/project.service';
|
||||
import { TagService } from '../../tag/tag.service';
|
||||
import { shortSyntax } from '../short-syntax';
|
||||
|
|
@ -103,120 +103,126 @@ export class ShortSyntaxEffects {
|
|||
),
|
||||
mergeMap(([{ task, originalAction }, tags, projects, defaultProjectId]) => {
|
||||
const isReplaceTagIds = originalAction.type === TaskSharedActions.updateTask.type;
|
||||
const r = shortSyntax(
|
||||
task,
|
||||
this._globalConfigService?.cfg()?.shortSyntax ||
|
||||
DEFAULT_GLOBAL_CONFIG.shortSyntax,
|
||||
tags,
|
||||
projects,
|
||||
undefined,
|
||||
isReplaceTagIds ? 'replace' : 'combine',
|
||||
);
|
||||
if (environment.production) {
|
||||
TaskLog.log('shortSyntax', r);
|
||||
}
|
||||
const isAddDefaultProjectIfNecessary: boolean =
|
||||
!!defaultProjectId &&
|
||||
!task.projectId &&
|
||||
!task.parentId &&
|
||||
task.projectId !== defaultProjectId &&
|
||||
originalAction.type === TaskSharedActions.addTask.type;
|
||||
|
||||
if (!r) {
|
||||
if (isAddDefaultProjectIfNecessary) {
|
||||
return [
|
||||
TaskSharedActions.moveToOtherProject({
|
||||
task: { ...task, subTasks: [] },
|
||||
targetProjectId: defaultProjectId as string,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const actions: Action[] = [];
|
||||
const { taskChanges, attachments } = r;
|
||||
|
||||
// Build scheduling info from parsed short syntax
|
||||
let schedulingInfo:
|
||||
| {
|
||||
day?: string;
|
||||
isAddToTop?: boolean;
|
||||
dueWithTime?: number;
|
||||
remindAt?: number | null;
|
||||
isMoveToBacklog?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (taskChanges.dueWithTime && !taskChanges.remindAt) {
|
||||
const { dueWithTime } = taskChanges;
|
||||
if (taskChanges.hasPlannedTime === false) {
|
||||
// Plan for day only (no specific time)
|
||||
const plannedDay = new Date(dueWithTime);
|
||||
schedulingInfo = {
|
||||
day: getDbDateStr(plannedDay),
|
||||
isAddToTop: false,
|
||||
};
|
||||
} else {
|
||||
// Schedule with specific time
|
||||
schedulingInfo = {
|
||||
dueWithTime,
|
||||
remindAt: remindOptionToMilliseconds(
|
||||
dueWithTime,
|
||||
this._globalConfigService.cfg()?.reminder.defaultTaskRemindOption ??
|
||||
DEFAULT_GLOBAL_CONFIG.reminder.defaultTaskRemindOption!,
|
||||
),
|
||||
isMoveToBacklog: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Determine target project
|
||||
let targetProjectId: string | undefined;
|
||||
if (r.projectId && r.projectId !== task.projectId && !task.parentId) {
|
||||
if (task.repeatCfgId) {
|
||||
this._snackService.open({
|
||||
ico: 'warning',
|
||||
msg: T.F.TASK.S.CANNOT_ASSIGN_PROJECT_FOR_REPEATABLE_TASK,
|
||||
});
|
||||
} else {
|
||||
targetProjectId = r.projectId;
|
||||
}
|
||||
} else if (isAddDefaultProjectIfNecessary) {
|
||||
targetProjectId = defaultProjectId as string;
|
||||
}
|
||||
|
||||
// Build task changes including tagIds update
|
||||
const tagIds: string[] = [...(r.taskChanges.tagIds || task.tagIds)];
|
||||
const isEqualTags = JSON.stringify(tagIds) === JSON.stringify(task.tagIds);
|
||||
const finalTaskChanges = { ...taskChanges };
|
||||
if (tagIds && tagIds.length && !isEqualTags) {
|
||||
finalTaskChanges.tagIds = unique(tagIds);
|
||||
}
|
||||
|
||||
// Add parsed URL attachments to task (merge with existing)
|
||||
if (attachments.length > 0) {
|
||||
finalTaskChanges.attachments = [...(task.attachments || []), ...attachments];
|
||||
}
|
||||
|
||||
// Use compound action for atomic state update
|
||||
actions.push(
|
||||
TaskSharedActions.applyShortSyntax({
|
||||
return from(
|
||||
shortSyntax(
|
||||
task,
|
||||
taskChanges: finalTaskChanges,
|
||||
targetProjectId,
|
||||
schedulingInfo,
|
||||
this._globalConfigService?.cfg()?.shortSyntax ||
|
||||
DEFAULT_GLOBAL_CONFIG.shortSyntax,
|
||||
tags,
|
||||
projects,
|
||||
undefined,
|
||||
isReplaceTagIds ? 'replace' : 'combine',
|
||||
).then((r) => {
|
||||
if (environment.production) {
|
||||
TaskLog.log('shortSyntax', r);
|
||||
}
|
||||
const isAddDefaultProjectIfNecessary: boolean =
|
||||
!!defaultProjectId &&
|
||||
!task.projectId &&
|
||||
!task.parentId &&
|
||||
task.projectId !== defaultProjectId &&
|
||||
originalAction.type === TaskSharedActions.addTask.type;
|
||||
|
||||
if (!r) {
|
||||
if (isAddDefaultProjectIfNecessary) {
|
||||
return [
|
||||
TaskSharedActions.moveToOtherProject({
|
||||
task: { ...task, subTasks: [] },
|
||||
targetProjectId: defaultProjectId as string,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [] as Action[];
|
||||
}
|
||||
|
||||
const actions: Action[] = [];
|
||||
const { taskChanges, attachments } = r;
|
||||
|
||||
// Build scheduling info from parsed short syntax
|
||||
let schedulingInfo:
|
||||
| {
|
||||
day?: string;
|
||||
isAddToTop?: boolean;
|
||||
dueWithTime?: number;
|
||||
remindAt?: number | null;
|
||||
isMoveToBacklog?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (taskChanges.dueWithTime && !taskChanges.remindAt) {
|
||||
const { dueWithTime } = taskChanges;
|
||||
if (taskChanges.hasPlannedTime === false) {
|
||||
// Plan for day only (no specific time)
|
||||
const plannedDay = new Date(dueWithTime);
|
||||
schedulingInfo = {
|
||||
day: getDbDateStr(plannedDay),
|
||||
isAddToTop: false,
|
||||
};
|
||||
} else {
|
||||
// Schedule with specific time
|
||||
schedulingInfo = {
|
||||
dueWithTime,
|
||||
remindAt: remindOptionToMilliseconds(
|
||||
dueWithTime,
|
||||
this._globalConfigService.cfg()?.reminder.defaultTaskRemindOption ??
|
||||
DEFAULT_GLOBAL_CONFIG.reminder.defaultTaskRemindOption!,
|
||||
),
|
||||
isMoveToBacklog: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Determine target project
|
||||
let targetProjectId: string | undefined;
|
||||
if (r.projectId && r.projectId !== task.projectId && !task.parentId) {
|
||||
if (task.repeatCfgId) {
|
||||
this._snackService.open({
|
||||
ico: 'warning',
|
||||
msg: T.F.TASK.S.CANNOT_ASSIGN_PROJECT_FOR_REPEATABLE_TASK,
|
||||
});
|
||||
} else {
|
||||
targetProjectId = r.projectId;
|
||||
}
|
||||
} else if (isAddDefaultProjectIfNecessary) {
|
||||
targetProjectId = defaultProjectId as string;
|
||||
}
|
||||
|
||||
// Build task changes including tagIds update
|
||||
const tagIds: string[] = [...(r.taskChanges.tagIds || task.tagIds)];
|
||||
const isEqualTags = JSON.stringify(tagIds) === JSON.stringify(task.tagIds);
|
||||
const finalTaskChanges = { ...taskChanges };
|
||||
if (tagIds && tagIds.length && !isEqualTags) {
|
||||
finalTaskChanges.tagIds = unique(tagIds);
|
||||
}
|
||||
|
||||
// Add parsed URL attachments to task (merge with existing)
|
||||
if (attachments.length > 0) {
|
||||
finalTaskChanges.attachments = [
|
||||
...(task.attachments || []),
|
||||
...attachments,
|
||||
];
|
||||
}
|
||||
|
||||
// Use compound action for atomic state update
|
||||
actions.push(
|
||||
TaskSharedActions.applyShortSyntax({
|
||||
task,
|
||||
taskChanges: finalTaskChanges,
|
||||
targetProjectId,
|
||||
schedulingInfo,
|
||||
}),
|
||||
);
|
||||
|
||||
// New tag creation requires user confirmation, so remains separate
|
||||
if (r.newTagTitles.length) {
|
||||
actions.push(
|
||||
addNewTagsFromShortSyntax({ taskId: task.id, newTitles: r.newTagTitles }),
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}),
|
||||
);
|
||||
|
||||
// New tag creation requires user confirmation, so remains separate
|
||||
if (r.newTagTitles.length) {
|
||||
actions.push(
|
||||
addNewTagsFromShortSyntax({ taskId: task.id, newTitles: r.newTagTitles }),
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
).pipe(mergeMap((actions) => actions));
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue