post merge

This commit is contained in:
Johannes Millan 2024-10-12 12:51:26 +02:00
commit 44f572414d
16 changed files with 415 additions and 125 deletions

View file

@ -141,12 +141,14 @@ There is another older the app looks and feels much better now ;) [artic
### Short-Syntax
Can be used when adding a task.
Can be used when adding a task. <strong>(Each of these can be disabled in settings->short syntax)</strong>
- `# <tag-name>`: add a tag to the task
(`"task-description #tag1"`)
- `<number>m` or `<number>h`: set time-estimate for the task
(`"task-description 10m"` or `"task-description 5h"`)
- `@<time>`: add due time to the task
(`"task-description @fri 10pm"`)
- `+ <project-name>`: add the task to an existing project
(`"task-description +Important Project"`)
- `Ctr + 2`: toggle between moving the new task to the bottom and top of the list
@ -315,13 +317,12 @@ Packaging the app is done via [electron-builder](https://github.com/electron-use
### Building for Android
*This feature was added on October 7, 2024. See [Pull Request #57](https://github.com/johannesjo/super-productivity-android/pull/57).*
_This feature was added on October 7, 2024. See [Pull Request #57](https://github.com/johannesjo/super-productivity-android/pull/57)._
To build the Android version of Super Productivity, please refer to the [Android Build Documentation](./android/offline.md), which includes instructions on configuring **Connectivity-Free Mode** and **Online-Only Mode (Compatibility Mode)**.
Ensure you follow the setup steps properly to configure the environment for building the app.
## Run as Docker Container
```bash

View file

@ -5,7 +5,7 @@ export enum MODEL_VERSION {
// needs to be always the same as TASK !!!
TASK_ARCHIVE = 3.5,
PROJECT = 6.14,
GLOBAL_CONFIG = 3.4,
GLOBAL_CONFIG = 3.5,
METRIC = 1.0,
SIMPLE_COUNTER = 2.0,
NOTE = 1.0,

View file

@ -30,6 +30,11 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
**Why do I want it?**
`,
},
shortSyntax: {
isEnableProject: true,
isEnableDue: true,
isEnableTag: true,
},
evaluation: {
isHideEvaluationSheet: false,
},

View file

@ -0,0 +1,31 @@
import { T } from '../../../t.const';
import { ConfigFormSection, ShortSyntaxConfig } from '../global-config.model';
export const SHORT_SYNTAX_FORM_CFG: ConfigFormSection<ShortSyntaxConfig> = {
title: T.GCF.SHORT_SYNTAX.TITLE,
key: 'shortSyntax',
help: T.GCF.SHORT_SYNTAX.HELP,
items: [
{
key: 'isEnableProject',
type: 'checkbox',
templateOptions: {
label: T.GCF.SHORT_SYNTAX.IS_ENABLE_PROJECT,
},
},
{
key: 'isEnableTag',
type: 'checkbox',
templateOptions: {
label: T.GCF.SHORT_SYNTAX.IS_ENABLE_TAG,
},
},
{
key: 'isEnableDue',
type: 'checkbox',
templateOptions: {
label: T.GCF.SHORT_SYNTAX.IS_ENABLE_DUE,
},
},
],
};

View file

@ -18,6 +18,7 @@ import { FOCUS_MODE_FORM_CFG } from './form-cfgs/focus-mode-form.const';
import { IS_FIREFOX } from '../../util/is-firefox';
import { CALENDAR_FORM_CFG } from './form-cfgs/calendar-form.const';
import { REMINDER_FORM_CFG } from './form-cfgs/reminder-form.const';
import { SHORT_SYNTAX_FORM_CFG } from './form-cfgs/short-syntax-form.const';
const filterGlobalConfigForm = (cfg: ConfigFormSection<any>): boolean => {
return (
@ -29,6 +30,7 @@ const filterGlobalConfigForm = (cfg: ConfigFormSection<any>): boolean => {
export const GLOBAL_CONFIG_FORM_CONFIG: ConfigFormConfig = [
LANGUAGE_SELECTION_FORM_FORM,
MISC_SETTINGS_FORM_CFG,
SHORT_SYNTAX_FORM_CFG,
IDLE_FORM_CFG,
KEYBOARD_SETTINGS_FORM_CFG,
TIME_TRACKING_FORM_CFG,

View file

@ -24,6 +24,12 @@ export type MiscConfig = Readonly<{
isDisableAnimations: boolean;
}>;
export type ShortSyntaxConfig = Readonly<{
isEnableProject: boolean;
isEnableDue: boolean;
isEnableTag: boolean;
}>;
export type TimeTrackingConfig = Readonly<{
trackingInterval: number;
defaultEstimate: number;
@ -170,6 +176,7 @@ export type FocusModeConfig = Readonly<{
export type GlobalConfigState = Readonly<{
lang: LanguageConfig;
misc: MiscConfig;
shortSyntax: ShortSyntaxConfig;
evaluation: EvaluationConfig;
idle: IdleConfig;
takeABreak: TakeABreakConfig;

View file

@ -10,6 +10,7 @@ import {
IdleConfig,
MiscConfig,
ScheduleConfig,
ShortSyntaxConfig,
SoundConfig,
SyncConfig,
TakeABreakConfig,
@ -19,6 +20,7 @@ import {
selectEvaluationConfig,
selectIdleConfig,
selectMiscConfig,
selectShortSyntaxConfig,
selectSoundConfig,
selectSyncConfig,
selectTakeABreakConfig,
@ -38,6 +40,10 @@ export class GlobalConfigService {
shareReplay(1),
);
shortSyntax$: Observable<ShortSyntaxConfig> = this._store.pipe(
select(selectShortSyntaxConfig),
);
sound$: Observable<SoundConfig> = this._store.pipe(
select(selectSoundConfig),
shareReplay(1),

View file

@ -11,6 +11,7 @@ import {
PomodoroConfig,
ReminderConfig,
ScheduleConfig,
ShortSyntaxConfig,
SoundConfig,
SyncConfig,
TakeABreakConfig,
@ -29,6 +30,10 @@ export const selectMiscConfig = createSelector(
selectConfigFeatureState,
(cfg): MiscConfig => cfg.misc,
);
export const selectShortSyntaxConfig = createSelector(
selectConfigFeatureState,
(cfg): ShortSyntaxConfig => cfg.shortSyntax,
);
export const selectSoundConfig = createSelector(
selectConfigFeatureState,
(cfg): SoundConfig => cfg.sound,

View file

@ -19,7 +19,6 @@ import { OpenProjectCfg } from '../../issue/providers/open-project/open-project.
import { GiteaCfg } from '../../issue/providers/gitea/gitea.model';
import { RedmineCfg } from '../../issue/providers/redmine/redmine.model';
// TODO rename to selectProjectFeatureState
export const selectProjectFeatureState =
createFeatureSelector<ProjectState>(PROJECT_FEATURE_NAME);
const { selectAll } = projectAdapter.getSelectors();

View file

@ -47,6 +47,9 @@ import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view';
import { Store } from '@ngrx/store';
import { PlannerActions } from '../../planner/store/planner.actions';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { GlobalConfigService } from '../../config/global-config.service';
import { ShortSyntaxConfig } from '../../config/global-config.model';
import { DEFAULT_GLOBAL_CONFIG } from '../../config/default-global-config.const';
@Component({
selector: 'add-task-bar',
@ -126,6 +129,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy {
tags,
projects,
defaultColor: activeWorkContext.theme.primary,
shortSyntaxConfig: this._shortSyntaxConfig,
}),
),
startWith([]),
@ -165,6 +169,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy {
private _saveTmpTodoTimeout?: number;
private _lastAddedTaskId?: string;
private _subs: Subscription = new Subscription();
private _shortSyntaxConfig: ShortSyntaxConfig = DEFAULT_GLOBAL_CONFIG.shortSyntax;
constructor(
private _taskService: TaskService,
@ -175,6 +180,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy {
private _tagService: TagService,
private _cd: ChangeDetectorRef,
private _store: Store,
private _globalConfigService: GlobalConfigService,
) {
this._subs.add(
this.activatedIssueTask$.subscribe((v) => (this.activatedIssueTask = v)),
@ -182,6 +188,11 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy {
this._subs.add(this.shortSyntaxTags$.subscribe((v) => (this.shortSyntaxTags = v)));
this._subs.add(this.inputVal$.subscribe((v) => (this.inputVal = v)));
this._subs.add(this.tagSuggestions$.subscribe((v) => (this.tagSuggestions = v)));
this._subs.add(
this._globalConfigService.shortSyntax$.subscribe(
(shortSyntaxConfig) => (this._shortSyntaxConfig = shortSyntaxConfig),
),
);
}
ngAfterViewInit(): void {

View file

@ -4,6 +4,7 @@ import { DEFAULT_TODAY_TAG_COLOR } from '../../work-context/work-context.const';
import { Tag } from '../../tag/tag.model';
import { Project } from '../../project/project.model';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { ShortSyntaxConfig } from '../../config/global-config.model';
export interface ShortSyntaxTag {
title: string;
@ -18,11 +19,13 @@ export const shortSyntaxToTags = ({
tags,
projects,
defaultColor,
shortSyntaxConfig,
}: {
val: string;
tags: Tag[];
projects: Project[];
defaultColor: string;
shortSyntaxConfig: ShortSyntaxConfig;
}): ShortSyntaxTag[] => {
const r = shortSyntax(
{
@ -30,6 +33,7 @@ export const shortSyntaxToTags = ({
tagIds: [],
projectId: undefined,
},
shortSyntaxConfig,
tags,
projects,
);

View file

@ -4,6 +4,7 @@ import { getWorklogStr } from '../../util/get-work-log-str';
import { Tag } from '../tag/tag.model';
import { DEFAULT_TAG } from '../tag/tag.const';
import { Project } from '../project/project.model';
import { DEFAULT_GLOBAL_CONFIG } from '../config/default-global-config.const';
const TASK: TaskCopy = {
id: 'id',
@ -44,12 +45,13 @@ const ALL_TAGS: Tag[] = [
{ ...DEFAULT_TAG, id: 'A_id', title: 'A' },
{ ...DEFAULT_TAG, id: 'multi_word_id', title: 'Multi Word Tag' },
];
const CONFIG = DEFAULT_GLOBAL_CONFIG.shortSyntax;
const getPlannedDateTimestampFromShortSyntaxReturnValue = (
taskInput: TaskCopy,
now: Date = new Date(),
): number => {
const r = shortSyntax(taskInput, undefined, undefined, now);
const r = shortSyntax(taskInput, CONFIG, undefined, undefined, now);
const parsedDateInMilliseconds = r?.taskChanges?.plannedAt as number;
return parsedDateInMilliseconds;
};
@ -111,15 +113,18 @@ const checkIfCorrectDateAndTime = (
describe('shortSyntax', () => {
it('should ignore for no short syntax', () => {
const r = shortSyntax(TASK);
const r = shortSyntax(TASK, CONFIG);
expect(r).toEqual(undefined);
});
it('should ignore if the changes cause no further changes', () => {
const r = shortSyntax({
...TASK,
title: 'So what shall I do',
});
const r = shortSyntax(
{
...TASK,
title: 'So what shall I do',
},
CONFIG,
);
expect(r).toEqual(undefined);
});
@ -129,7 +134,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title 10m/1h',
};
const r = shortSyntax(t);
const r = shortSyntax(t, CONFIG);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -150,7 +155,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title whatever 1h/120m',
};
const r = shortSyntax(t);
const r = shortSyntax(t, CONFIG);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -165,6 +170,15 @@ describe('shortSyntax', () => {
},
});
});
it('should ignore time short syntax when disabled', () => {
const t = {
...TASK,
title: 'Fun title whatever 1h/120m',
};
const r = shortSyntax(t, { ...CONFIG, isEnableDue: false });
expect(r).toEqual(undefined);
});
});
describe('should recognize short syntax for date', () => {
@ -188,29 +202,22 @@ describe('shortSyntax', () => {
expect(isTimeSetCorrectly).toBeTrue();
});
it('should correctly parse day of the week', () => {
it('should ignore schedule syntax with time only when disabled', () => {
const t = {
...TASK,
title: 'Test @4pm',
};
const r = shortSyntax(t, { ...CONFIG, isEnableDue: false });
expect(r).toEqual(undefined);
});
it('should ignore day of the week when disabled', () => {
const t = {
...TASK,
title: 'Test @Friday',
};
const now = new Date('Fri Feb 09 2024 13:31:29 ');
const parsedDateInMilliseconds = getPlannedDateTimestampFromShortSyntaxReturnValue(
t,
now,
);
const parsedDate = new Date(parsedDateInMilliseconds);
// 5 represents Friday
expect(parsedDate.getDay()).toEqual(5);
const dayIncrement = 7;
// If today happens to be Friday, the parsed date will be the next Friday,
// 7 days from today
const nextFriday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() + dayIncrement,
);
const isDateSetCorrectly = checkSameDay(parsedDate, nextFriday);
expect(isDateSetCorrectly).toBeTrue();
const r = shortSyntax(t, { ...CONFIG, isEnableDue: false });
expect(r).toEqual(undefined);
});
it('should correctly parse day of the week', () => {
@ -244,7 +251,17 @@ describe('shortSyntax', () => {
...TASK,
title: '#134 Fun title',
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual(undefined);
});
it('should not trigger for tasks with starting # (e.g. github issues) when disabled', () => {
const t = {
...TASK,
title: '#134 Fun title',
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
@ -254,7 +271,7 @@ describe('shortSyntax', () => {
...TASK,
title: '#134 Fun title #blu',
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
@ -267,12 +284,22 @@ describe('shortSyntax', () => {
});
});
it('should not trigger for multiple tasks when disabled', () => {
const t = {
...TASK,
title: '#134 Fun title #blu',
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
it('should work with tags', () => {
const t = {
...TASK,
title: 'Fun title #blu #A',
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
@ -285,12 +312,22 @@ describe('shortSyntax', () => {
});
});
it("shouldn't work with tags when disabled", () => {
const t = {
...TASK,
title: 'Fun title #blu #A',
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
it('should not trigger for # without space before', () => {
const t = {
...TASK,
title: 'Fun title#blu',
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual(undefined);
});
@ -300,7 +337,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title#blu #bla',
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
@ -319,7 +356,7 @@ describe('shortSyntax', () => {
title: 'Fun title #blu #hihi',
tagIds: ['blu_id', 'A', 'multi_word_id'],
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
@ -332,13 +369,24 @@ describe('shortSyntax', () => {
});
});
it('should not overwrite existing tags when disabled', () => {
const t = {
...TASK,
title: 'Fun title #blu #hihi',
tagIds: ['blu_id', 'A', 'multi_word_id'],
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
it('should add new tag names', () => {
const t = {
...TASK,
title: 'Fun title #blu #idontexist',
tagIds: [],
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: ['idontexist'],
@ -351,13 +399,24 @@ describe('shortSyntax', () => {
});
});
it('should not add new tag names when disabled', () => {
const t = {
...TASK,
title: 'Fun title #blu #idontexist',
tagIds: [],
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
it('should add new "asd #asd" tag', () => {
const t = {
...TASK,
title: 'asd #asd',
tagIds: [],
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: ['asd'],
@ -369,6 +428,17 @@ describe('shortSyntax', () => {
});
});
it('should not add new "asd #asd" tag when disabled', () => {
const t = {
...TASK,
title: 'asd #asd',
tagIds: [],
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
it('should add tags for sub tasks', () => {
const t = {
...TASK,
@ -376,7 +446,7 @@ describe('shortSyntax', () => {
title: 'Fun title #blu #idontexist',
tagIds: [],
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: ['idontexist'],
@ -385,14 +455,26 @@ describe('shortSyntax', () => {
taskChanges: { tagIds: ['blu_id'], title: 'Fun title' },
});
});
it('should not add tags for sub tasks when disabled', () => {
const t = {
...TASK,
parentId: 'SOMEPARENT',
title: 'Fun title #blu #idontexist',
tagIds: [],
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual(undefined);
});
});
describe('should work with all combined', () => {
it('', () => {
describe('should work with tags and time estimates combined', () => {
it('tag before time estimate', () => {
const t = {
...TASK,
title: 'Fun title #blu 10m/1h',
};
const r = shortSyntax(t, ALL_TAGS);
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -409,23 +491,63 @@ describe('shortSyntax', () => {
});
});
// TODO make this work maybe
// it('', () => {
// const t = {
// ...TASK,
// title: 'Fun title 10m/1h #blu'
// };
// const r = shortSyntax(t, ALL_TAGS);
// expect(r).toEqual({
// title: 'Fun title',
// // timeSpent: 7200000,
// timeSpentOnDay: {
// [getWorklogStr()]: 600000
// },
// timeEstimate: 3600000,
// tagIds: ['blu_id']
// });
// });
it('time estimate before tag', () => {
const t = {
...TASK,
title: 'Fun title 10m/1h #blu',
};
const r = shortSyntax(t, CONFIG, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
projectId: undefined,
taskChanges: {
title: 'Fun title',
timeSpentOnDay: {
[getWorklogStr()]: 600000,
},
timeEstimate: 3600000,
tagIds: ['blu_id'],
},
});
});
it('time estimate disabled', () => {
const t = {
...TASK,
title: 'Fun title 10m/1h #blu',
};
const r = shortSyntax(t, { ...CONFIG, isEnableDue: false }, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
projectId: undefined,
taskChanges: {
title: 'Fun title 10m/1h',
tagIds: ['blu_id'],
},
});
});
it('tags disabled', () => {
const t = {
...TASK,
title: 'Fun title 10m/1h #blu',
};
const r = shortSyntax(t, { ...CONFIG, isEnableTag: false }, ALL_TAGS);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
projectId: undefined,
taskChanges: {
title: 'Fun title #blu',
timeSpentOnDay: {
[getWorklogStr()]: 600000,
},
timeEstimate: 3600000,
},
});
});
});
describe('projects', () => {
@ -448,7 +570,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title +ProjectEasyShort',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -459,12 +581,21 @@ describe('shortSyntax', () => {
});
});
it("shouldn't work when disabled", () => {
const t = {
...TASK,
title: 'Fun title +ProjectEasyShort',
};
const r = shortSyntax(t, { ...CONFIG, isEnableProject: false }, [], projects);
expect(r).toEqual(undefined);
});
it('should not parse without missing whitespace before', () => {
const t = {
...TASK,
title: 'Fun title+ProjectEasyShort',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual(undefined);
});
@ -473,7 +604,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title +ProjectEasyShort 10m/1h',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -489,12 +620,49 @@ describe('shortSyntax', () => {
});
});
it('should work together with time estimates when disabled', () => {
const t = {
...TASK,
title: 'Fun title +ProjectEasyShort 10m/1h',
};
const r = shortSyntax(t, { ...CONFIG, isEnableProject: false }, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
projectId: undefined,
taskChanges: {
title: 'Fun title +ProjectEasyShort',
// timeSpent: 7200000,
timeSpentOnDay: {
[getWorklogStr()]: 600000,
},
timeEstimate: 3600000,
},
});
});
it('should work together with disabled time estimates', () => {
const t = {
...TASK,
title: 'Fun title +ProjectEasyShort 10m/1h',
};
const r = shortSyntax(t, { ...CONFIG, isEnableDue: false }, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
projectId: 'ProjectEasyShortID',
taskChanges: {
title: 'Fun title 10m/1h',
},
});
});
it('should work with only the beginning of a project title if it is at least 3 chars long', () => {
const t = {
...TASK,
title: 'Fun title +Project',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -510,7 +678,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title +Some Project Title',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -526,7 +694,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title +Some Pro',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -542,7 +710,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Other fun title +SomePro',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -558,14 +726,11 @@ describe('shortSyntax', () => {
...TASK,
title: 'Other fun title +Non existing project',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual(undefined);
});
});
// TODO
// describe('due:', () => {});
describe('combined', () => {
it('should work when time comes first', () => {
const projects = [
@ -578,7 +743,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Fun title 10m/1h +ProjectEasyShort',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: [],
remindAt: null,
@ -605,7 +770,7 @@ describe('shortSyntax', () => {
...TASK,
title: 'Some task +ProjectEasyShort 30m #tag',
};
const r = shortSyntax(t, [], projects);
const r = shortSyntax(t, CONFIG, [], projects);
expect(r).toEqual({
newTagTitles: ['tag'],
remindAt: null,
@ -640,7 +805,7 @@ describe('shortSyntax', () => {
expect(parsedDate.getDay()).toEqual(5);
const isTimeSetCorrectly = checkIfDateHasCorrectTime(parsedDate, 16, 0);
expect(isTimeSetCorrectly).toBeTrue();
const parsedTaskInfo = shortSyntax(t, [], projects);
const parsedTaskInfo = shortSyntax(t, CONFIG, [], projects);
expect(parsedTaskInfo?.projectId).toEqual(projects[0].id);
// The time spent value is stored to the property equal to today
// in format YYYY-MM-DD of the object `timeSpentOnDay`
@ -665,10 +830,11 @@ describe('shortSyntax', () => {
0,
);
expect(isPlannedDateAndTimeCorrect).toBeTrue();
const parsedTaskInfo = shortSyntax(t, []);
const parsedTaskInfo = shortSyntax(t, CONFIG, []);
expect(parsedTaskInfo?.newTagTitles.includes('html')).toBeTrue();
expect(parsedTaskInfo?.newTagTitles.includes('css')).toBeTrue();
});
it('should parse scheduled date using local time zone when unspecified', () => {
const t = {
...TASK,
@ -677,5 +843,18 @@ describe('shortSyntax', () => {
const plannedTimestamp = getPlannedDateTimestampFromShortSyntaxReturnValue(t);
expect(checkIfCorrectDateAndTime(plannedTimestamp, 'saturday', 13, 37)).toBeTrue();
});
it('should work when all are disabled', () => {
const t = {
...TASK,
title: 'Test @fri 4pm #html #css +ProjectEasyShort',
};
const r = shortSyntax(t, {
isEnableDue: false,
isEnableProject: false,
isEnableTag: false,
});
expect(r).toEqual(undefined);
});
});
});

View file

@ -4,8 +4,22 @@ import { getWorklogStr } from '../../util/get-work-log-str';
import { stringToMs } from '../../ui/duration/string-to-ms.pipe';
import { Tag } from '../tag/tag.model';
import { Project } from '../project/project.model';
import { ShortSyntaxConfig } from '../config/global-config.model';
const SHORT_SYNTAX_TIME_REG_EX = / t?(([0-9]+(m|h|d)+)? *\/ *)?([0-9]+(m|h|d)+) *$/;
type ProjectChanges = {
title?: string;
projectId?: string;
};
type TagChanges = {
taskChanges?: Partial<TaskCopy>;
newTagTitlesToCreate?: string[];
};
type DueChanges = {
title?: string;
plannedAt?: number;
};
const SHORT_SYNTAX_TIME_REG_EX = / t?(([0-9]+(m|h|d)+)? *\/ *)?([0-9]+(m|h|d)+)/;
// NOTE: should come after the time reg ex is executed so we don't have to deal with those strings too
const CH_PRO = '+';
@ -25,6 +39,7 @@ const SHORT_SYNTAX_DUE_REG_EX = new RegExp(`\\${CH_DUE}[^${ALL_SPECIAL}]+`, 'gi'
export const shortSyntax = (
task: Task | Partial<Task>,
config: ShortSyntaxConfig,
allTags?: Tag[],
allProjects?: Project[],
now = new Date(),
@ -44,40 +59,51 @@ export const shortSyntax = (
}
// TODO clean up this mess
let taskChanges: Partial<TaskCopy>;
let taskChanges: Partial<TaskCopy> = {};
let changesForProject: ProjectChanges = {};
let changesForTag: TagChanges = {};
// NOTE: we do this twice... :-O ...it's weird, but required to make whitespaces work as separator and not as one
taskChanges = parseTimeSpentChanges(task);
const changesForScheduledDate = parseScheduledDate(task, now);
taskChanges = {
...taskChanges,
...changesForScheduledDate,
};
const changesForProject = parseProjectChanges(
{ ...task, title: taskChanges.title || task.title },
allProjects?.filter((p) => !p.isArchived && !p.isHiddenFromMenu),
);
if (changesForProject.projectId) {
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,
title: changesForProject.title,
...parseScheduledDate(task, now),
};
}
const changesForTag = parseTagChanges(
{ ...task, title: taskChanges.title || task.title },
allTags,
);
taskChanges = {
...taskChanges,
...changesForTag.taskChanges,
};
taskChanges = {
...taskChanges,
// NOTE: because we pass the new taskChanges here we need to assignments...
...parseTimeSpentChanges(taskChanges),
// title: taskChanges.title?.trim(),
};
if (config.isEnableProject) {
changesForProject = parseProjectChanges(
{ ...task, title: taskChanges.title || task.title },
allProjects?.filter((p) => !p.isArchived && !p.isHiddenFromMenu),
);
if (changesForProject.projectId) {
taskChanges = {
...taskChanges,
title: changesForProject.title,
};
}
}
if (config.isEnableTag) {
changesForTag = parseTagChanges(
{ ...task, title: taskChanges.title || task.title },
allTags,
);
taskChanges = {
...taskChanges,
...(changesForTag.taskChanges || {}),
};
}
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) {
@ -93,7 +119,7 @@ export const shortSyntax = (
return {
taskChanges,
newTagTitles: changesForTag.newTagTitlesToCreate,
newTagTitles: changesForTag.newTagTitlesToCreate || [],
remindAt: null,
projectId: changesForProject.projectId,
// remindAt: changesForDue.remindAt
@ -103,18 +129,14 @@ export const shortSyntax = (
const parseProjectChanges = (
task: Partial<TaskCopy>,
allProjects?: Project[],
): {
title?: string;
projectId?: string;
} => {
// don't allow for issue tasks
if (task.issueId) {
return {};
}
if (!Array.isArray(allProjects) || !allProjects || allProjects.length === 0) {
return {};
}
if (!task.title) {
): ProjectChanges => {
if (
task.issueId || // don't allow for issue tasks
!task.title ||
!Array.isArray(allProjects) ||
!allProjects ||
allProjects.length === 0
) {
return {};
}
@ -167,10 +189,7 @@ const parseProjectChanges = (
return {};
};
const parseTagChanges = (
task: Partial<TaskCopy>,
allTags?: Tag[],
): { taskChanges: Partial<TaskCopy>; newTagTitlesToCreate: string[] } => {
const parseTagChanges = (task: Partial<TaskCopy>, allTags?: Tag[]): TagChanges => {
const taskChanges: Partial<TaskCopy> = {};
const newTagTitlesToCreate: string[] = [];
@ -240,7 +259,7 @@ const parseTagChanges = (
};
};
const parseScheduledDate = (task: Partial<TaskCopy>, now: Date): Partial<Task> => {
const parseScheduledDate = (task: Partial<TaskCopy>, now: Date): DueChanges => {
if (!task.title) {
return {};
}

View file

@ -32,6 +32,7 @@ import { T } from '../../../t.const';
import { MatDialog } from '@angular/material/dialog';
import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confirm.component';
import { LayoutService } from '../../../core-ui/layout/layout.service';
import { DEFAULT_GLOBAL_CONFIG } from '../../config/default-global-config.const';
@Injectable()
export class ShortSyntaxEffects {
@ -79,7 +80,13 @@ export class ShortSyntaxEffects {
),
),
mergeMap(([{ task, originalAction }, tags, projects, defaultProjectId]) => {
const r = shortSyntax(task, tags, projects);
const r = shortSyntax(
task,
this._globalConfigService?.cfg?.shortSyntax ||
DEFAULT_GLOBAL_CONFIG.shortSyntax,
tags,
projects,
);
if (environment.production) {
console.log('shortSyntax', r);
}

View file

@ -677,7 +677,7 @@ const T = {
TASK_PLANNED_FOR: 'F.PLANNER.S.TASK_PLANNED_FOR',
},
TASK_DRAWER: 'F.PLANNER.TASK_DRAWER',
EDIT_REPEATED_TASK: 'F.PLANNER.EDIT_REPEATED_TASK'
EDIT_REPEATED_TASK: 'F.PLANNER.EDIT_REPEATED_TASK',
},
POMODORO: {
BACK_TO_WORK: 'F.POMODORO.BACK_TO_WORK',
@ -1482,6 +1482,13 @@ const T = {
TASK_NOTES_TPL: 'GCF.MISC.TASK_NOTES_TPL',
TITLE: 'GCF.MISC.TITLE',
},
SHORT_SYNTAX: {
TITLE: 'GCF.SHORT_SYNTAX.TITLE',
HELP: 'GCF.SHORT_SYNTAX.HELP',
IS_ENABLE_PROJECT: 'GCF.SHORT_SYNTAX.IS_ENABLE_PROJECT',
IS_ENABLE_TAG: 'GCF.SHORT_SYNTAX.IS_ENABLE_TAG',
IS_ENABLE_DUE: 'GCF.SHORT_SYNTAX.IS_ENABLE_DUE',
},
POMODORO: {
BREAK_DURATION: 'GCF.POMODORO.BREAK_DURATION',
CYCLES_BEFORE_LONGER_BREAK: 'GCF.POMODORO.CYCLES_BEFORE_LONGER_BREAK',

View file

@ -1458,6 +1458,13 @@
"TASK_NOTES_TPL": "Task description template",
"TITLE": "Misc Settings"
},
"SHORT_SYNTAX": {
"TITLE": "Short Syntax",
"HELP": "<p>Here you can control short syntax options when creating a task</p>",
"IS_ENABLE_PROJECT": "Enable project short syntax (+<Project name>)",
"IS_ENABLE_TAG": "Enable tag short syntax (#<Tag>)",
"IS_ENABLE_DUE": "Enable due short syntax (@<Due time>)"
},
"POMODORO": {
"BREAK_DURATION": "Duration of short breaks",
"CYCLES_BEFORE_LONGER_BREAK": "Start longer break after X work sessions",