mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-08-02 12:32:14 +00:00
feat(addTaskBar): add time spent short syntax for new tasks (#5269)
This commit is contained in:
parent
2762c60319
commit
deae7b1065
7 changed files with 120 additions and 40 deletions
|
|
@ -16,6 +16,7 @@ describe('AddTaskBarParserService', () => {
|
|||
'updateProjectId',
|
||||
'updateTagIds',
|
||||
'updateNewTagTitles',
|
||||
'updateSpent',
|
||||
'updateEstimate',
|
||||
'updateDate',
|
||||
'isAutoDetected',
|
||||
|
|
@ -88,6 +89,7 @@ describe('AddTaskBarParserService', () => {
|
|||
// Reset all spy calls before each test
|
||||
mockStateService.updateCleanText.calls.reset();
|
||||
mockStateService.updateDate.calls.reset();
|
||||
mockStateService.updateSpent.calls.reset();
|
||||
mockStateService.updateEstimate.calls.reset();
|
||||
mockStateService.updateTagIds.calls.reset();
|
||||
mockStateService.updateNewTagTitles.calls.reset();
|
||||
|
|
@ -117,6 +119,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: null,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -150,6 +153,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: currentDate,
|
||||
time: currentTime,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -180,6 +184,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: currentDate,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -276,6 +281,30 @@ describe('AddTaskBarParserService', () => {
|
|||
|
||||
expect(mockStateService.updateEstimate).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('should call updateSpent when parsing text', () => {
|
||||
service.parseAndUpdateText(
|
||||
'Task with potential time spent',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
mockTags,
|
||||
mockDefaultProject,
|
||||
);
|
||||
|
||||
expect(mockStateService.updateSpent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle null time spent', () => {
|
||||
service.parseAndUpdateText(
|
||||
'Simple task',
|
||||
mockConfig,
|
||||
mockProjects,
|
||||
mockTags,
|
||||
mockDefaultProject,
|
||||
);
|
||||
|
||||
expect(mockStateService.updateSpent).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic Parsing Tests', () => {
|
||||
|
|
@ -439,6 +468,9 @@ describe('AddTaskBarParserService', () => {
|
|||
{ input: 'Task t1.5h', expected: 'Task' },
|
||||
{ input: 'Task 45d', expected: 'Task' },
|
||||
{ input: 'Task t30m other text', expected: 'Task other text' },
|
||||
{ input: 'Task 1h/', expected: 'Task' },
|
||||
{ input: 'Task 1h/ between', expected: 'Task between' },
|
||||
{ input: '1h/ Task', expected: 'Task' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
|
|
@ -552,6 +584,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: dateStr,
|
||||
time: timeStr,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -582,6 +615,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: null,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -614,6 +648,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: null,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -644,6 +679,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: null,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
@ -673,6 +709,7 @@ describe('AddTaskBarParserService', () => {
|
|||
newTagTitles: [],
|
||||
date: null,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
cleanText: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ import { inject, Injectable } from '@angular/core';
|
|||
import { Project } from '../../project/project.model';
|
||||
import { Tag } from '../../tag/tag.model';
|
||||
import { AddTaskBarStateService } from './add-task-bar-state.service';
|
||||
import { shortSyntax } from '../short-syntax';
|
||||
import { SHORT_SYNTAX_TIME_REG_EX, shortSyntax } from '../short-syntax';
|
||||
import { ShortSyntaxConfig } from '../../config/global-config.model';
|
||||
import { getDbDateStr } from '../../../util/get-db-date-str';
|
||||
import { TimeSpentOnDay } from '../task.model';
|
||||
|
||||
interface PreviousParseResult {
|
||||
cleanText: string | null;
|
||||
projectId: string | null;
|
||||
tagIds: string[];
|
||||
newTagTitles: string[];
|
||||
timeSpentOnDay: TimeSpentOnDay | null;
|
||||
timeEstimate: number | null;
|
||||
dueDate: string | null;
|
||||
dueTime: string | null;
|
||||
|
|
@ -68,6 +70,7 @@ export class AddTaskBarParserService {
|
|||
: null,
|
||||
tagIds: currentState.tagIds, // Preserve pre-selected tags
|
||||
newTagTitles: [],
|
||||
timeSpentOnDay: null,
|
||||
timeEstimate: null,
|
||||
// Preserve current date/time if user has selected them, otherwise use defaults
|
||||
dueDate: currentState.date || (defaultDate ? defaultDate : null),
|
||||
|
|
@ -104,6 +107,7 @@ export class AddTaskBarParserService {
|
|||
projectId: parseResult.projectId || null,
|
||||
tagIds: tagIds,
|
||||
newTagTitles: newTagTitles,
|
||||
timeSpentOnDay: parseResult.taskChanges.timeSpentOnDay || null,
|
||||
timeEstimate: parseResult.taskChanges.timeEstimate || null,
|
||||
dueDate: dueDate,
|
||||
dueTime: dueTime,
|
||||
|
|
@ -151,6 +155,25 @@ export class AddTaskBarParserService {
|
|||
this._stateService.updateNewTagTitles(currentResult.newTagTitles);
|
||||
}
|
||||
|
||||
const prevTimeSpentOnDay = this._previousParseResult?.timeSpentOnDay || null;
|
||||
const currTimeSpentOnDay = currentResult.timeSpentOnDay;
|
||||
|
||||
if (
|
||||
!this._previousParseResult ||
|
||||
// Check for field existence change
|
||||
(prevTimeSpentOnDay === null) !== (currTimeSpentOnDay === null) ||
|
||||
// Check for any discrepancy between all recorded time spent
|
||||
(prevTimeSpentOnDay !== null &&
|
||||
currTimeSpentOnDay !== null &&
|
||||
(Object.keys(prevTimeSpentOnDay).length !==
|
||||
Object.keys(currTimeSpentOnDay).length ||
|
||||
Object.keys(prevTimeSpentOnDay).some(
|
||||
(k) => prevTimeSpentOnDay[k] !== currTimeSpentOnDay[k],
|
||||
)))
|
||||
) {
|
||||
this._stateService.updateSpent(currentResult.timeSpentOnDay);
|
||||
}
|
||||
|
||||
if (
|
||||
!this._previousParseResult ||
|
||||
this._previousParseResult.timeEstimate !== currentResult.timeEstimate
|
||||
|
|
@ -203,9 +226,8 @@ export class AddTaskBarParserService {
|
|||
|
||||
case 'estimate':
|
||||
// Remove estimate syntax (e.g., t30m, 1h, 30m/1h, t1.5h)
|
||||
// This matches the SHORT_SYNTAX_TIME_REG_EX from short-syntax.ts
|
||||
cleanedInput = cleanedInput.replace(
|
||||
/(?:\s|^)t?((\d+(?:\.\d+)?[mhd])(?:\s*\/\s*(\d+(?:\.\d+)?[mhd]))?)/gi,
|
||||
new RegExp(SHORT_SYNTAX_TIME_REG_EX.source, 'gi'),
|
||||
' ',
|
||||
);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Tag } from '../../tag/tag.model';
|
|||
import { AddTaskBarState, INITIAL_ADD_TASK_BAR_STATE } from './add-task-bar.const';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { SS } from '../../../core/persistence/storage-keys.const';
|
||||
import { TimeSpentOnDay } from '../task.model';
|
||||
|
||||
@Injectable()
|
||||
export class AddTaskBarStateService {
|
||||
|
|
@ -39,6 +40,10 @@ export class AddTaskBarStateService {
|
|||
this._taskInputState.update((state) => ({ ...state, time }));
|
||||
}
|
||||
|
||||
updateSpent(spent: TimeSpentOnDay | null): void {
|
||||
this._taskInputState.update((state) => ({ ...state, spent }));
|
||||
}
|
||||
|
||||
updateEstimate(estimate: number | null): void {
|
||||
this._taskInputState.update((state) => ({ ...state, estimate }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -433,6 +433,10 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
|
|||
timeEstimate: state.estimate || 0,
|
||||
};
|
||||
|
||||
if (state.spent) {
|
||||
taskData.timeSpentOnDay = state.spent;
|
||||
}
|
||||
|
||||
if (state.date) {
|
||||
// Parse date components to create date in local timezone
|
||||
// This avoids timezone issues when parsing date strings like "2024-01-15"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { INBOX_PROJECT } from '../../project/project.const';
|
||||
import { TimeSpentOnDay } from '../task.model';
|
||||
|
||||
export interface AddTaskBarState {
|
||||
projectId: string;
|
||||
tagIds: string[];
|
||||
date: string | null;
|
||||
time: string | null;
|
||||
spent: TimeSpentOnDay | null;
|
||||
estimate: number | null;
|
||||
newTagTitles: string[];
|
||||
cleanText: string | null;
|
||||
|
|
@ -26,6 +28,7 @@ export const INITIAL_ADD_TASK_BAR_STATE: AddTaskBarState = {
|
|||
tagIds: [],
|
||||
date: null,
|
||||
time: null,
|
||||
spent: null,
|
||||
estimate: null,
|
||||
newTagTitles: [],
|
||||
cleanText: null,
|
||||
|
|
|
|||
|
|
@ -242,6 +242,25 @@ describe('shortSyntax', () => {
|
|||
const r = shortSyntax(t, { ...CONFIG, isEnableDue: false });
|
||||
expect(r).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('with time spent only', () => {
|
||||
const t = {
|
||||
...TASK,
|
||||
title: 'Task description 30m/',
|
||||
};
|
||||
const r = shortSyntax(t, CONFIG);
|
||||
expect(r).toEqual({
|
||||
newTagTitles: [],
|
||||
remindAt: null,
|
||||
projectId: undefined,
|
||||
taskChanges: {
|
||||
title: 'Task description',
|
||||
timeSpentOnDay: {
|
||||
[getDbDateStr()]: 1800000,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should recognize short syntax for date', () => {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ type DueChanges = {
|
|||
dueWithTime?: number;
|
||||
};
|
||||
|
||||
const SHORT_SYNTAX_TIME_REG_EX =
|
||||
/(?:\s|^)t?((\d+(?:\.\d+)?[mhd])(?:\s*\/\s*(\d+(?:\.\d+)?[mhd]))?(?=\s|$))/i;
|
||||
// NOTE: should come after the time reg ex is executed so we don't have to deal with those strings too
|
||||
const CH_TSP = '/';
|
||||
export const SHORT_SYNTAX_TIME_REG_EX = new RegExp(
|
||||
String.raw`(?:\s|^)t?(\d+(?:\.\d+)?[mhd])(?:\s*` +
|
||||
`\\${CH_TSP}` +
|
||||
String.raw`(:?\s*(\d+(?:\.\d+)?[mhd]))?)?(?=\s|$)`,
|
||||
'i',
|
||||
);
|
||||
|
||||
const CH_PRO = '+';
|
||||
const CH_TAG = '#';
|
||||
|
|
@ -107,11 +111,10 @@ export const shortSyntax = (
|
|||
let changesForTag: TagChanges = {};
|
||||
|
||||
if (config.isEnableDue) {
|
||||
// NOTE: we do this twice... :-O ...it's weird, but required to make whitespaces work as separator and not as one
|
||||
taskChanges = parseTimeSpentChanges(task);
|
||||
taskChanges = {
|
||||
...taskChanges,
|
||||
...parseScheduledDate(task, now),
|
||||
...parseScheduledDate({ ...task, title: taskChanges.title || task.title }, now),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -139,15 +142,6 @@ export const shortSyntax = (
|
|||
};
|
||||
}
|
||||
|
||||
if (config.isEnableDue) {
|
||||
taskChanges = {
|
||||
...taskChanges,
|
||||
// NOTE: because we pass the new taskChanges here we need to assignments...
|
||||
...parseTimeSpentChanges(taskChanges),
|
||||
// title: taskChanges.title?.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// const changesForDue = parseDueChanges({...task, title: taskChanges.title || task.title});
|
||||
// if (changesForDue.remindAt) {
|
||||
// taskChanges = {
|
||||
|
|
@ -389,28 +383,24 @@ const parseTimeSpentChanges = (task: Partial<TaskCopy>): Partial<Task> => {
|
|||
}
|
||||
|
||||
const matches = SHORT_SYNTAX_TIME_REG_EX.exec(task.title);
|
||||
if (matches && matches.length >= 3) {
|
||||
const full = matches[0];
|
||||
const timeSpent = matches[2]; // First part (before slash)
|
||||
const timeEstimate = matches[3]; // Second part (after slash)
|
||||
// If no slash, use the single value as timeEstimate only
|
||||
const hasSlashFormat = matches[3] !== undefined;
|
||||
return {
|
||||
...(hasSlashFormat && timeSpent
|
||||
? {
|
||||
timeSpentOnDay: {
|
||||
...(task.timeSpentOnDay || {}),
|
||||
[getDbDateStr()]: stringToMs(timeSpent),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(timeEstimate
|
||||
? { timeEstimate: stringToMs(timeEstimate) }
|
||||
: timeSpent
|
||||
? { timeEstimate: stringToMs(timeSpent) }
|
||||
: {}),
|
||||
title: task.title.replace(full, '').trim(),
|
||||
};
|
||||
if (!matches) {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
|
||||
const [matchSpan, preSplit, postSplit] = matches;
|
||||
const timeSpent = matchSpan.includes(CH_TSP) ? preSplit : null;
|
||||
const timeEstimate = timeSpent === null ? preSplit : postSplit;
|
||||
|
||||
return {
|
||||
...(typeof timeSpent === 'string' && {
|
||||
timeSpentOnDay: {
|
||||
...task.timeSpentOnDay,
|
||||
[getDbDateStr()]: stringToMs(timeSpent),
|
||||
},
|
||||
}),
|
||||
...(typeof timeEstimate === 'string' && {
|
||||
timeEstimate: stringToMs(timeEstimate),
|
||||
}),
|
||||
title: task.title.replace(matchSpan, '').trim(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue