Compare commits

...

52 Commits

Author SHA1 Message Date
Johannes Millan ac39fe72c6 feat(calendar): add failsafe for missing event 2021-04-22 21:31:09 +02:00
Johannes Millan 02590f1d12 feat(calendar): show no planned done tasks when they were done 2021-04-22 21:31:09 +02:00
Johannes Millan 72509af5d3 feat(calendar): don't show tasks done on another day 2021-04-22 21:31:09 +02:00
Johannes Millan 4aa4a1dcdc refactor(calendar): fix typing issues 2021-04-22 21:31:09 +02:00
Johannes Millan f68c716cc1 feat(calendar): work around full calendar issue 2021-04-22 21:31:09 +02:00
Johannes Millan 9f08bd7322 build: update fullcalendar 2021-04-22 21:31:09 +02:00
Johannes Millan 32fe3ec630 feat(calendar): also allow for reminder 5 minutes before it starts 2021-04-22 21:30:33 +02:00
Johannes Millan b19809f8a2 test: remove for now 2021-04-22 21:30:33 +02:00
Johannes Millan 9ab3c9aac9 fix: lint 2021-04-22 21:30:33 +02:00
Johannes Millan 08383bd13f fix: lint 2021-04-22 21:30:33 +02:00
Johannes Millan db3b9f4737 feat(calendar): emphasize current task more 2021-04-22 21:30:33 +02:00
Johannes Millan b6ed036653 fix(calendar): done tasks min size 2021-04-22 21:30:33 +02:00
Johannes Millan 26f3d5989e fix(calendar): sub tasks not being displayed 2021-04-22 21:30:33 +02:00
Johannes Millan a2d576269a feat(calendar): make task resize work much better for today3 2021-04-22 21:30:33 +02:00
Johannes Millan a8dcb6370d feat(calendar): make task resize work much better for today2 2021-04-22 21:30:33 +02:00
Johannes Millan 9f884303da feat(calendar): make task resize work much better for today 2021-04-22 21:30:33 +02:00
Johannes Millan 442e922004 fix(calendar): estimates messing up 2021-04-22 21:30:33 +02:00
Johannes Millan eb1f4506f2 feat(calendar): add outline for current task 2021-04-22 21:30:33 +02:00
Johannes Millan c544822757 refactor(calendar): improve structure and cleanup 2021-04-22 21:30:33 +02:00
Johannes Millan 65c373d2c0 fix(calendar): double task issue 2021-04-22 21:30:33 +02:00
Johannes Millan 91869ae6cf feat(calendar): get rid off undesirable sub task case 2021-04-22 21:30:33 +02:00
Johannes Millan 89dd603f6b refactor(calendar): more typing 2021-04-22 21:30:33 +02:00
Johannes Millan e127926264 feat(calendar): remap schedule shortcut to calendar for now 2021-04-22 21:30:33 +02:00
Johannes Millan bb7589b7e1 feat(calendar): don't show unplanned done task for all day 2021-04-22 21:30:33 +02:00
Johannes Millan ea8c0c47bf feat(calendar): make schedule at start work 2021-04-22 21:30:33 +02:00
Johannes Millan 74d48855ac feat(calendar): show alarm clock in overview 2021-04-22 21:30:33 +02:00
Johannes Millan 606f2e93e4 feat(calendar): split up reminder and startAt 2021-04-22 21:30:33 +02:00
Johannes Millan 51314d3b6e feat(calendar): improve styling a bit 2021-04-22 21:30:33 +02:00
Johannes Millan 6330bb92a7 feat(calendar): style done tasks in calendar 2021-04-22 21:30:33 +02:00
Johannes Millan 9297b69c74 feat(calendar): handle done tasks duration 2021-04-22 21:30:33 +02:00
Johannes Millan ff1bfab424 feat(calendar): make sub tasks work 2021-04-22 21:30:33 +02:00
Johannes Millan bfbad5318f feat(calendar): add now indicator line 2021-04-22 21:30:33 +02:00
Johannes Millan 29167fd91a feat(calendar): make dragging to all day work 2021-04-22 21:30:32 +02:00
Johannes Millan 0ccc315214 feat(calendar): add migration for reminders 2021-04-22 21:30:32 +02:00
Johannes Millan 2a2dec283f feat(calendar): show scheduled tasks using plannedAt rather than remindAt 2021-04-22 21:30:32 +02:00
Johannes Millan 5ffcbcc711 feat(calendar): update plannedAt when task reminder is changed 2021-04-22 21:30:32 +02:00
Johannes Millan 3b49a5c6d2 feat(calendar,notes): remove reminder from notes 2021-04-22 21:30:32 +02:00
Johannes Millan 932cc1212a feat(calendar): outline ui for new schedule dialog 2021-04-22 21:30:32 +02:00
Johannes Millan 0641ec2655 feat(calendar): unstyle today for day view 2021-04-22 21:30:32 +02:00
Johannes Millan 2530fb8526 feat(calendar): make resize event work 2021-04-22 21:30:32 +02:00
Johannes Millan 951ff87d7b fix(calendar): dirty fix task move 2021-04-22 21:30:32 +02:00
Johannes Millan c841408ad6 feat(calendar): fix most essential styling issues 2021-04-22 21:30:32 +02:00
Johannes Millan 7e62de3f93 feat(calendar): improve duration 2021-04-22 21:30:32 +02:00
Johannes Millan c3a6359fe2 feat(calendar): display events in proper duration 2021-04-22 21:30:32 +02:00
Johannes Millan 1fdc884145 feat(calendar): prepare interaction 2021-04-22 21:30:32 +02:00
Johannes Millan fdb0d2a497 feat(calendar): make colors work for calendar view 2021-04-22 21:30:32 +02:00
Johannes Millan 9781e9be78 feat(calendar): make all colors available as map 2021-04-22 21:30:32 +02:00
Johannes Millan 04013dba44 feat(calendar): make initial view daygrid
Signed-off-by: Johannes Millan <johannes.millan@gmail.com>
2021-04-22 21:30:32 +02:00
Johannes Millan a5ebddf99c feat(calendar): make agenda view available 2021-04-22 21:30:32 +02:00
Johannes Millan 1b1b86971a feat(calendar): display tasks 2021-04-22 21:30:32 +02:00
Johannes Millan 72bceb0d65 feat(calendar): add most simple calendar 2021-04-22 21:30:32 +02:00
Johannes Millan 79fc1ffc43 feat(calendar): add boilerplate 2021-04-22 21:30:32 +02:00
70 changed files with 938 additions and 727 deletions

View File

@ -109,6 +109,10 @@
"@angular/router": "^11.2.10",
"@angular/service-worker": "^11.2.10",
"@biesbjerg/ngx-translate-extract": "^7.0.4",
"@fullcalendar/angular": "^5.5.0",
"@fullcalendar/daygrid": "^5.5.0",
"@fullcalendar/interaction": "^5.5.0",
"@fullcalendar/timegrid": "^5.5.1",
"@ngrx/data": "^11.1.0",
"@ngrx/effects": "^11.1.0",
"@ngrx/entity": "^11.1.0",

View File

@ -21,7 +21,9 @@ import '@angular/common/locales/global/pt';
import '@angular/common/locales/global/nl';
import '@angular/common/locales/global/nb';
export const ALL_THEMES = [
export const DAY_STARTS_AT: string = '9:00';
export const ALL_THEMES: string[] = [
'blue',
'blue-grey',
'light-blue',

View File

@ -12,6 +12,7 @@ import { TagTaskPageComponent } from './pages/tag-task-page/tag-task-page.compon
import { ActiveWorkContextGuard, ValidProjectIdGuard, ValidTagIdGuard } from './app.guard';
import { TagSettingsPageComponent } from './pages/tag-settings-page/tag-settings-page.component';
import { TODAY_TAG } from './features/tag/tag.const';
import { CalendarPageComponent } from './pages/calendar-page/calendar-page.component';
export const APP_ROUTES: Routes = [
{path: 'config', component: ConfigPageComponent, data: {page: 'config'}},
@ -49,7 +50,12 @@ export const APP_ROUTES: Routes = [
data: {page: 'daily-summary'},
canActivate: [ValidTagIdGuard]
},
{
path: 'tag/:id/calendar',
component: CalendarPageComponent,
data: {page: 'calendar'},
canActivate: [ValidTagIdGuard]
},
{
path: 'project/:id/tasks',
component: ProjectTaskPageComponent,
@ -86,6 +92,12 @@ export const APP_ROUTES: Routes = [
data: {page: 'daily-summary'},
canActivate: [ValidProjectIdGuard]
},
{
path: 'project/:id/calendar',
component: CalendarPageComponent,
data: {page: 'calendar'},
canActivate: [ValidProjectIdGuard]
},
{path: 'project-overview', component: ProjectOverviewPageComponent, data: {page: 'project-overview'}},
{path: 'active/:subPageType', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent},

View File

@ -106,7 +106,7 @@ export class ShortcutService {
this._router.navigate(['/config']);
} else if (checkKeyCombo(ev, keys.goToScheduledView)) {
this._router.navigate(['/schedule']);
this._router.navigate(['/active/calendar']);
// } else if (checkKeyCombo(ev, keys.goToDailyAgenda)) {
// this._router.navigate(['/daily-agenda']);

View File

@ -35,6 +35,15 @@
<mat-icon>access_alarms</mat-icon>
<span class="text">{{T.MH.SCHEDULED|translate}}</span>
</button>
<button class="route-link"
#menuEntry
mat-menu-item
routerLink="active/calendar"
routerLinkActive="isActiveRoute">
<mat-icon>date_range</mat-icon>
<span class="text">{{T.MH.CALENDAR|translate}}</span>
</button>
</section>
<section *ngIf="projectList$|async as projectList"

View File

@ -0,0 +1,18 @@
import { AppDataComplete } from '../../imex/sync/sync.model';
export const crossModelMigrations = (data: AppDataComplete): AppDataComplete => {
return migrateTaskReminders(data);
};
function migrateTaskReminders(data: AppDataComplete): AppDataComplete {
if (data?.task?.ids.length && data?.reminders?.length) {
data.reminders.forEach((reminder) => {
const task = data.task.entities[reminder.relatedId];
if (task && task.reminderId && !task.plannedAt) {
// @ts-ignore
task.plannedAt = reminder.remindAt;
}
});
}
return data;
}

View File

@ -66,6 +66,7 @@ import { concatMap, debounceTime, shareReplay, skipWhile } from 'rxjs/operators'
import { devError } from '../../util/dev-error';
import { isValidAppData } from '../../imex/sync/is-valid-app-data.util';
import { removeFromDb, saveToDb } from './persistence.actions';
import { crossModelMigrations } from './cross-model-migrations';
import { metricReducer } from '../../features/metric/store/metric.reducer';
import { improvementReducer } from '../../features/metric/improvement/store/improvement.reducer';
import { obstructionReducer } from '../../features/metric/obstruction/store/obstruction.reducer';
@ -340,10 +341,10 @@ export class PersistenceService {
throw new Error('Project State is broken');
}
r = {
r = crossModelMigrations({
...(await this._loadAppDataForProjects(pids)),
...(await this._loadAppBaseData()),
};
} as AppDataComplete);
this._inMemoryComplete = r;
} else {
r = this._inMemoryComplete;

View File

@ -0,0 +1,3 @@
<full-calendar #calendar
*ngIf="calOptions"
[options]="calOptions"></full-calendar>

View File

@ -0,0 +1,10 @@
:host {
height: 100%;
width: 100%;
display: block;
overflow: hidden;
> full-calendar {
height: 100%;
}
}

View File

@ -0,0 +1,25 @@
// import { ComponentFixture, TestBed } from '@angular/core/testing';
//
// import { CalendarComponent } from './calendar.component';
//
// describe('CalendarComponent', () => {
// let component: CalendarComponent;
// let fixture: ComponentFixture<CalendarComponent>;
//
// beforeEach(async () => {
// await TestBed.configureTestingModule({
// declarations: [CalendarComponent]
// })
// .compileComponents();
// });
//
// beforeEach(() => {
// fixture = TestBed.createComponent(CalendarComponent);
// component = fixture.componentInstance;
// fixture.detectChanges();
// });
//
// it('should create', () => {
// expect(component).toBeTruthy();
// });
// });

View File

@ -0,0 +1,283 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, ViewChild } from '@angular/core';
import { CalendarOptions, FullCalendarComponent } from '@fullcalendar/angular';
import { BehaviorSubject, combineLatest, EMPTY, Observable, Subscription } from 'rxjs';
import { map, switchMap, withLatestFrom } from 'rxjs/operators';
import { EventChangeArg, EventClickArg, EventDropArg, EventInput } from '@fullcalendar/common';
import { WorkContextService } from '../work-context/work-context.service';
import { TaskService } from '../tasks/task.service';
import { getWorklogStr } from '../../util/get-work-log-str';
import { Task, TaskWithReminderData } from '../tasks/task.model';
import { msToString } from '../../ui/duration/ms-to-string.pipe';
import { DAY_STARTS_AT } from '../../app.constants';
import { isToday } from '../../util/is-today.util';
import { millisecondsDiffToRemindOption } from '../tasks/util/remind-option-to-milliseconds';
import { WorkContextColorMap } from '../work-context/work-context.model';
import { CALENDAR_MIN_TASK_DURATION, STATIC_CALENDAR_OPTS } from './calendar.const';
// const WEIRD_MAGIC_HOUR = 60000 * 60;
// TODO with summer time??
// TODO probably the time zone offset
const WEIRD_MAGIC_HOUR = 60000 * 60 * 2;
interface CalOpsAll extends CalendarOptions {
eventResize: (ev: EventChangeArg) => void;
eventDrop: (ev: EventDropArg) => void;
eventDragStart: (ev: unknown) => void;
eventDragStop: (ev: unknown) => void;
eventResizeStart: (ev: unknown) => void;
eventResizeStop: (ev: unknown) => void;
}
@Component({
// apparently calendar does not work, so we add a prefix
selector: 'sup-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CalendarComponent implements OnDestroy {
@ViewChild('calendar') calendarEl?: FullCalendarComponent;
calOptions?: CalendarOptions;
private _isDragging$ = new BehaviorSubject<boolean>(false);
private _isResizing$ = new BehaviorSubject<boolean>(false);
private _isBlockUpdates$ = combineLatest([
this._isDragging$,
this._isResizing$,
]).pipe(
map(([isDragging, isResizing]) => isDragging || isResizing),
// mapTo(false),
);
private DEFAULT_CAL_OPTS: CalOpsAll = {
...STATIC_CALENDAR_OPTS,
eventDragStart: () => this._isDragging$.next(true),
eventDragStop: () => this._isDragging$.next(false),
eventResizeStart: () => this._isResizing$.next(true),
eventResizeStop: () => this._isResizing$.next(false),
eventResize: this._handleResize.bind(this),
eventDrop: this._handleDrop.bind(this),
eventClick: (calEvent: EventClickArg) => {
console.log(calEvent);
},
// dateClick: (arg: any) => {
// // console.log('I am here!');
// // console.log(arg.date.toUTCString()); // use *UTC* methods on the native Date Object
// // will output something like 'Sat, 01 Sep 2018 00:00:00 GMT'
// },
// eventReceive: (calEvent: any) => {
// console.log(calEvent);
// // this.openDialog(calEvent);
// },
// eventLeave: (calEvent: any) => {
// console.log(calEvent);
// // this.openDialog(calEvent);
// },
};
calOptions$: Observable<CalOpsAll> = this._isBlockUpdates$.pipe(
switchMap((isBlockUpdates) => isBlockUpdates
? EMPTY
: this._taskService.plannedTasks$.pipe(
withLatestFrom(
this._workContextService.allWorkContextColors$,
this._taskService.currentTaskId$,
),
map(this._mapTasksToCalOptions.bind(this)),
)
),
// debounceTime(200),
);
private _subs: Subscription = new Subscription();
constructor(
private _workContextService: WorkContextService,
private _taskService: TaskService,
private _changeDetectorRef: ChangeDetectorRef,
) {
// NOTE: this somehow fixes the duplication issue
this._subs.add(this.calOptions$.subscribe((v) => {
this.calOptions = v;
this._changeDetectorRef.markForCheck();
}));
}
ngOnDestroy() {
this._subs.unsubscribe();
}
// private handleDateClick(arg: any) {
// alert('date click! ' + arg.dateStr);
// }
private _handleResize(calEvent: EventChangeArg) {
// can sometimes happen
if (!calEvent.event) {
console.warn(':( @fullcalendar event not ready');
return;
}
const start = calEvent.event._instance?.range.start as Date;
const task: TaskWithReminderData = calEvent.event.extendedProps as TaskWithReminderData;
this._taskService.reScheduleTask({
taskId: task.id,
plannedAt: start.getTime() - WEIRD_MAGIC_HOUR,
title: task.title,
reminderId: task.reminderId as string,
remindCfg: task.reminderData && millisecondsDiffToRemindOption(task.plannedAt as number, task.reminderData.remindAt),
});
const prevEstimate: number = (task.timeEstimate || 0);
const withDelta: number = prevEstimate + (calEvent as any).endDelta.milliseconds;
const timeEstimate: number = Math.max((task.timeSpent || 0), withDelta);
const withMinDuration: number = Math.max(timeEstimate, CALENDAR_MIN_TASK_DURATION, task.timeSpent);
// console.log({
// timeEstimate: timeEstimate / 60000,
// prevEstimate: prevEstimate / 60000,
// withDelta: withDelta / 60000,
// });
// TODO show toast for cannot be smaller than timeSpentToday
this._taskService.update(task.id, {
timeEstimate: withMinDuration
});
}
private _handleDrop(calEvent: EventDropArg) {
// can sometimes happen
if (!calEvent.event) {
console.warn(':( @fullcalendar event not ready');
return;
}
const task: TaskWithReminderData = calEvent.event.extendedProps as TaskWithReminderData;
const start = calEvent.event._instance?.range.start as Date;
console.log(calEvent);
// TODO understand and fix this
if (calEvent.event.allDay) {
if (isToday(start)) {
this._taskService.unScheduleTask(task.id, task.reminderId as string);
} else {
const dayStartsSplit = DAY_STARTS_AT.split(':');
start.setHours(+dayStartsSplit[0], +dayStartsSplit[1], 0, 0);
const startTime = start.getTime();
this._taskService.reScheduleTask({
taskId: task.id,
reminderId: task.reminderId as string,
plannedAt: startTime,
remindCfg: task.reminderData && millisecondsDiffToRemindOption(task.plannedAt as number, task.reminderData.remindAt),
title: task.title,
});
}
} else {
const startTime = start.getTime() - WEIRD_MAGIC_HOUR;
this._taskService.reScheduleTask({
taskId: task.id,
reminderId: task.reminderId as string,
plannedAt: startTime,
title: task.title,
remindCfg: task.reminderData && millisecondsDiffToRemindOption(task.plannedAt as number, task.reminderData.remindAt),
});
}
}
private _mapTasksToCalOptions([tasks, colorMap, currentTaskId]: [Task[], WorkContextColorMap, string | null]): CalOpsAll {
// TODO make it work for other days than today
const TD_STR = getWorklogStr();
const events: EventInput[] = tasks.map((task: Task): EventInput => {
const classNames: string[] = [];
const timeSpentToday: number = task.timeSpentOnDay[TD_STR] || 0;
let timeToGo: number = ((task.timeEstimate || 0) - (task.timeSpent || 0));
timeToGo = timeToGo + timeSpentToday;
// if (task.title.match(/Something/)) {
// console.log({timeToGo: timeToGo / 60000, t: task.title});
// }
if (task.isDone) {
classNames.push('isDone');
}
if (task.reminderId) {
classNames.push('hasAlarm');
}
if (task.id === currentTaskId) {
classNames.push('isCurrent');
}
const base = {
title: task.title
+ ' '
+ msToString(task.timeSpent)
+ '/'
+ msToString(task.timeEstimate),
// groupId: task.parentId || undefined,
extendedProps: task,
classNames,
backgroundColor: task.projectId
? colorMap[task.projectId]
: colorMap[task.tagIds[0]],
};
if (task.plannedAt) {
return {
...base,
start: new Date(task.plannedAt),
end: new Date((task.isDone && task.doneOn)
? (task.plannedAt as number) + Math.max(timeSpentToday, CALENDAR_MIN_TASK_DURATION)
: (task.plannedAt as number) + Math.max(timeToGo, CALENDAR_MIN_TASK_DURATION)
),
};
} else if (task.isDone && task.doneOn) {
return {
...base,
start: task.doneOn - Math.max(timeSpentToday, CALENDAR_MIN_TASK_DURATION),
end: task.doneOn
};
} else {
return {
...base,
allDay: true,
// start: TD_STR,
duration: 2000000,
start: Date.now(),
end: Date.now() + timeToGo
};
}
});
// console.log(tasks, events);
return {
...this.DEFAULT_CAL_OPTS,
events,
};
}
}
// events: [{
// title: 'Asd',
// start: new Date(),
// allDay: true,
// backgroundColor: 'red',
// end: new Date()
// display: 'string | null;',
// startEditable: 'boolean | null;',
// durationEditable: 'boolean | null;',
// constraints: 'Constraint[];',
// overlap: 'boolean | null;',
// allows: 'AllowFunc[];',
// backgroundColor: 'string;',
// borderColor: 'string;',
// textColor: 'string;',
// classNames: 'string[];',
// editable: true,
// startEditable: true,
// durationEditable: true,
// }],

View File

@ -0,0 +1,20 @@
import { CalendarOptions } from '@fullcalendar/angular';
export const STATIC_CALENDAR_OPTS: CalendarOptions = {
events: [],
headerToolbar: {
start: 'today prev,next',
center: 'title',
end: 'timeGridDay,timeGridWeek,dayGridMonth'
},
initialView: 'timeGridDay',
eventOverlap: true,
// height: 'auto',
editable: true,
droppable: false,
slotDuration: '00:15:00',
nowIndicator: true,
timeZone: 'local', // the default (unnecessary to specify)
};
export const CALENDAR_MIN_TASK_DURATION = 15 * 60 * 1000;

View File

@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CalendarComponent } from './calendar.component';
import { FullCalendarModule } from '@fullcalendar/angular';
import dayGridPlugin from '@fullcalendar/daygrid'; // a plugin
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
// import interactionPlugin from '@fullcalendar/interaction'; // a plugin
FullCalendarModule.registerPlugins([ // register FullCalendar plugins
dayGridPlugin,
timeGridPlugin,
interactionPlugin,
]);
@NgModule({
declarations: [
CalendarComponent,
],
imports: [
CommonModule,
FullCalendarModule // register FullCalendar with you app
],
exports: [
CalendarComponent,
],
})
export class CalendarModule {
}

View File

@ -1,54 +0,0 @@
<mat-dialog-content>
<owl-wrapper (triggerSubmit)="save()"
[(dateTime)]="dateTime"></owl-wrapper>
<div class="form-wrapper">
<div class="additional-fields-wrapper">
<mat-form-field>
<input
[(ngModel)]="title"
[placeholder]="T.F.NOTE.D_ADD_REMINDER.L_TITLE|translate"
matInput
name="title"
required
type="text">
<mat-error>{{T.F.NOTE.D_ADD_REMINDER.E_ENTER_TITLE|translate}}</mat-error>
</mat-form-field>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<div class="wrap-buttons">
<button (click)="close()"
[title]="T.G.CANCEL|translate"
class="btn btn-primary submit-button"
color="primary"
mat-button
type="button">
<mat-icon>close</mat-icon>
</button>
<button (click)="remove()"
*ngIf="isEdit"
[title]="T.F.NOTE.REMOVE_REMINDER|translate"
class="btn btn-primary submit-button"
color="primary"
mat-stroked-button
type="button">
<mat-icon>alarm_off</mat-icon>
</button>
<button (click)="save()"
[title]="(reminder
?T.F.NOTE.UPDATE_REMINDER
:T.F.NOTE.ADD_REMINDER)|translate"
class="btn btn-primary submit-button"
color="primary"
mat-stroked-button
type="submit">
<mat-icon *ngIf="!note.reminderId">alarm_add</mat-icon>
<mat-icon *ngIf="note.reminderId">alarm</mat-icon>
</button>
</div>
</mat-dialog-actions>

View File

@ -1,63 +0,0 @@
:host-context([dir=rtl]) {
direction: rtl;
}
.form-wrapper {
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
}
.additional-fields-wrapper {
margin: 12px;
}
:host {
owl-date-time-container {
box-shadow: none !important;
}
.owl-dt-weekday {
padding-bottom: 0;
}
owl-date-time-inline {
overflow: hidden;
}
.mat-dialog-content {
margin: -24px;
margin-bottom: 0;
padding: 0;
//min-height: 82vh;
max-height: 82vh;
}
.mat-dialog-actions {
min-height: 42px;
padding: 4px;
margin: -24px;
margin-top: 0;
}
::ng-deep {
.owl-top-input input {
padding-top: 0;
padding-bottom: 0;
line-height: 36px;
}
}
}
.wrap-buttons {
display: flex;
max-width: 100%;
justify-content: stretch;
flex: 1;
button {
flex: 1;
}
}

View File

@ -1,25 +0,0 @@
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
//
// import { DialogAddNoteReminderComponent } from './dialog-add-note-reminder.component';
//
// describe('DialogAddNoteReminderComponent', () => {
// let component: DialogAddNoteReminderComponent;
// let fixture: ComponentFixture<DialogAddNoteReminderComponent>;
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// declarations: [DialogAddNoteReminderComponent]
// })
// .compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(DialogAddNoteReminderComponent);
// component = fixture.componentInstance;
// fixture.detectChanges();
// });
//
// it('should create', () => {
// expect(component).toBeTruthy();
// });
// });

View File

@ -1,83 +0,0 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Note } from '../note.model';
import { NoteService } from '../note.service';
import { ReminderCopy } from '../../reminder/reminder.model';
import { ReminderService } from '../../reminder/reminder.service';
import { T } from '../../../t.const';
import { throttle } from 'helpful-decorators';
@Component({
selector: 'dialog-add-note-reminder',
templateUrl: './dialog-add-note-reminder.component.html',
styleUrls: ['./dialog-add-note-reminder.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogAddNoteReminderComponent {
T: typeof T = T;
dateTime?: number;
title: string;
isEdit: boolean;
reminder?: ReminderCopy | null;
note: Note;
constructor(
private _noteService: NoteService,
private _reminderService: ReminderService,
private _matDialogRef: MatDialogRef<DialogAddNoteReminderComponent>,
@Inject(MAT_DIALOG_DATA) public data: { note: Note },
) {
this.note = this.data.note;
this.reminder = this.note.reminderId
? this._reminderService.getById(this.data.note.reminderId as string)
: null;
this.isEdit = !!(this.reminder && this.reminder.id);
if (this.isEdit && this.reminder) {
this.dateTime = this.reminder.remindAt;
this.title = this.reminder.title;
} else {
this.title = this.note.content.substr(0, 40);
}
}
// NOTE: throttle is used as quick way to prevent multiple submits
@throttle(2000, {leading: true, trailing: false})
save() {
const timestamp = this.dateTime;
if (!timestamp || !this.title) {
return;
}
if (this.isEdit && this.reminder) {
this._noteService.updateReminder(
this.note.id,
this.reminder.id,
timestamp,
this.title,
);
this.close();
} else {
this._noteService.addReminder(
this.note.id,
timestamp,
this.title,
);
this.close();
}
}
// NOTE: throttle is used as quick way to prevent multiple submits
@throttle(2000, {leading: true, trailing: false})
remove() {
if (!this.reminder) {
throw new Error('No reminder');
}
this._noteService.removeReminder(this.note.id, this.reminder.id);
this.close();
}
close() {
this._matDialogRef.close();
}
}

View File

@ -10,11 +10,6 @@
name="noteContent"
rows="6"></textarea>
</div>
<datetime-input (modelChange)="reminderDate=$event"
[model]="reminderDate"
[placeholder]="T.F.NOTE.D_ADD.DATETIME_LABEL|translate"
name="reminderData"
style="width: 100%;"></datetime-input>
</div>
<mat-dialog-actions align="end">

View File

@ -9,7 +9,7 @@ textarea {
width: 100%;
max-width: 100%;
@include noteStyle;
margin-bottom: $s*2;
margin-bottom: 0;
padding: $s;
min-height: $s*5;

View File

@ -1,7 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { NoteService } from '../note.service';
import { Reminder } from '../../reminder/reminder.model';
import { SS_NOTE_TMP } from '../../../core/persistence/ls-keys.const';
import { T } from '../../../t.const';
import { WorkContextService } from '../../work-context/work-context.service';
@ -18,7 +17,6 @@ import { map } from 'rxjs/operators';
export class DialogAddNoteComponent {
T: typeof T = T;
noteContent: string;
reminderDate?: number;
isSubmitted: boolean = false;
isInProjectContext$: Observable<boolean> = this._workContextService.activeWorkContextTypeAndId$.pipe(
map(({activeType}) => activeType === WorkContextType.PROJECT)
@ -28,7 +26,6 @@ export class DialogAddNoteComponent {
private _matDialogRef: MatDialogRef<DialogAddNoteComponent>,
private _noteService: NoteService,
private _workContextService: WorkContextService,
@Inject(MAT_DIALOG_DATA) public data: { reminder: Reminder },
) {
this.noteContent = sessionStorage.getItem(SS_NOTE_TMP) || '';
}
@ -40,14 +37,10 @@ export class DialogAddNoteComponent {
}
submit() {
const remindAt = this.reminderDate;
if (!this.isSubmitted
&& (this.noteContent && this.noteContent.trim().length > 0
|| remindAt)) {
&& (this.noteContent && this.noteContent.trim().length > 0)) {
this._noteService.add(
{content: this.noteContent},
remindAt,
true,
);

View File

@ -1,63 +0,0 @@
<h1 mat-dialog-title>
<mat-icon class="dialog-header-icon">alarm</mat-icon>
<span>{{T.F.NOTE.D_VIEW_REMINDER.TITLE|translate}}</span>
</h1>
<mat-dialog-content>
<div *ngIf="isForCurrentContext"
class="note">
{{(note$|async)?.content }}
</div>
<ng-container *ngIf="!isForCurrentContext">
<p>From Project</p>
<h3>{{(targetContext$|async)?.title}}</h3>
<div class="note">
{{reminder.title }}
</div>
</ng-container>
</mat-dialog-content>
<mat-dialog-actions align="end">
<div class="wrap-buttons">
<button (dblclick)="snooze(10)"
[matMenuTriggerFor]="snoozeMenu"
color="primary"
mat-button
type="button">
<mat-icon>snooze</mat-icon>
{{T.F.NOTE.D_VIEW_REMINDER.SNOOZE|translate:{time: '10m'} }}
</button>
<button (click)="dismiss()"
color="primary"
mat-stroked-button
type="button">
<mat-icon>alarm_off</mat-icon>
{{T.G.DISMISS|translate}}
</button>
</div>
</mat-dialog-actions>
<mat-menu #snoozeMenu="matMenu">
<button (click)="snooze(10)"
mat-menu-item>
<mat-icon>snooze</mat-icon>
{{T.G.MINUTES|translate:{m: 10} }}
</button>
<button (click)="snooze(30)"
mat-menu-item>
<mat-icon>snooze</mat-icon>
{{T.G.MINUTES|translate:{m: 30} }}
</button>
<button (click)="snooze(60)"
mat-menu-item>
<mat-icon>snooze</mat-icon>
{{T.G.MINUTES|translate:{m: 60} }}
</button>
<!-- <button mat-menu-item-->
<!-- (click)="editReminder()">-->
<!-- <mat-icon>edit</mat-icon>{{T.G.EDIT|translate}}-->
<!-- </button>-->
</mat-menu>

View File

@ -1,20 +0,0 @@
@import '../../../../variables';
:host {
//text-align: center;
}
.note-wrapper {
margin-bottom: 0;
}
.note {
text-align: left;
position: relative;
z-index: 1;
margin-bottom: $s;
display: block;
}
.mat-dialog-content {
}

View File

@ -1,25 +0,0 @@
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
//
// import { DialogViewNoteReminderComponent } from './dialog-view-note-reminder.component';
//
// describe('DialogViewNoteReminderComponent', () => {
// let component: DialogViewNoteReminderComponent;
// let fixture: ComponentFixture<DialogViewNoteReminderComponent>;
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// declarations: [DialogViewNoteReminderComponent]
// })
// .compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(DialogViewNoteReminderComponent);
// component = fixture.componentInstance;
// fixture.detectChanges();
// });
//
// it('should create', () => {
// expect(component).toBeTruthy();
// });
// });

View File

@ -1,86 +0,0 @@
import { ChangeDetectionStrategy, Component, Inject, OnDestroy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Reminder } from '../../reminder/reminder.model';
import { Note } from '../note.model';
import { NoteService } from '../note.service';
import { Observable, Subscription } from 'rxjs';
import { ReminderService } from '../../reminder/reminder.service';
import { ProjectService } from '../../project/project.service';
import { Project } from '../../project/project.model';
import { T } from '../../../t.const';
import { WorkContextService } from '../../work-context/work-context.service';
import { Tag } from '../../tag/tag.model';
import { WorkContextType } from '../../work-context/work-context.model';
import { TagService } from '../../tag/tag.service';
@Component({
selector: 'dialog-view-note-reminder',
templateUrl: './dialog-view-note-reminder.component.html',
styleUrls: ['./dialog-view-note-reminder.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogViewNoteReminderComponent implements OnDestroy {
T: typeof T = T;
note$: Observable<Note> = this._noteService.getById$(this.data.reminder.relatedId);
reminder: Reminder = this.data.reminder;
isForCurrentContext: boolean = (this.reminder.workContextId === this._workContextService.activeWorkContextId);
targetContext$: Observable<Tag | Project> = (this.data.reminder.workContextType === WorkContextType.PROJECT)
? this._projectService.getByIdOnce$(this.reminder.workContextId)
: this._tagService.getTagById$(this.reminder.workContextId);
private _subs: Subscription = new Subscription();
constructor(
private _matDialogRef: MatDialogRef<DialogViewNoteReminderComponent>,
private _noteService: NoteService,
private _projectService: ProjectService,
private _tagService: TagService,
private _workContextService: WorkContextService,
private _reminderService: ReminderService,
@Inject(MAT_DIALOG_DATA) public data: { reminder: Reminder },
) {
this._matDialogRef.disableClose = true;
this._subs.add(this._reminderService.onReloadModel$.subscribe(() => {
this.close();
}));
if (this.reminder.workContextType !== WorkContextType.PROJECT) {
throw new Error('This should never happen');
}
}
ngOnDestroy(): void {
this._subs.unsubscribe();
}
dismiss() {
const reminder = this.reminder;
if (this.isForCurrentContext) {
this._noteService.update(reminder.relatedId, {
reminderId: null,
});
this._reminderService.removeReminder(reminder.id);
this.close();
} else {
this._noteService.updateFromDifferentWorkContext(reminder.workContextId, reminder.relatedId, {
reminderId: null,
}).then(() => {
this._reminderService.removeReminder(reminder.id);
this.close();
});
}
}
snooze(snoozeInMinutes: number) {
this._reminderService.updateReminder(this.reminder.id, {
remindAt: Date.now() + (snoozeInMinutes * 60 * 1000)
});
this.close();
}
close() {
this._matDialogRef.close();
}
}

View File

@ -6,5 +6,4 @@ export interface Note {
backgroundColor?: string;
created: number;
modified: number;
reminderId?: string | null;
}

View File

@ -8,9 +8,7 @@ import { NoteEffects } from './store/note.effects';
import { NotesComponent } from './notes/notes.component';
import { NoteComponent } from './note/note.component';
import { UiModule } from '../../ui/ui.module';
import { DialogAddNoteReminderComponent } from './dialog-add-note-reminder/dialog-add-note-reminder.component';
import { FormsModule } from '@angular/forms';
import { DialogViewNoteReminderComponent } from './dialog-view-note-reminder/dialog-view-note-reminder.component';
import { DialogAddNoteComponent } from './dialog-add-note/dialog-add-note.component';
@NgModule({
@ -24,8 +22,6 @@ import { DialogAddNoteComponent } from './dialog-add-note/dialog-add-note.compon
declarations: [
NotesComponent,
NoteComponent,
DialogAddNoteReminderComponent,
DialogViewNoteReminderComponent,
DialogAddNoteComponent
],
exports: [NotesComponent],

View File

@ -2,16 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Note } from './note.model';
import { select, Store } from '@ngrx/store';
import {
addNote,
addNoteReminder,
deleteNote,
loadNoteState,
removeNoteReminder,
updateNote,
updateNoteOrder,
updateNoteReminder
} from './store/note.actions';
import { addNote, deleteNote, loadNoteState, updateNote, updateNoteOrder } from './store/note.actions';
import * as shortid from 'shortid';
import { initialNoteState, NoteState, selectAllNotes, selectNoteById } from './store/note.reducer';
import { PersistenceService } from '../../core/persistence/persistence.service';
@ -49,7 +40,7 @@ export class NoteService {
this._store$.dispatch(loadNoteState({state}));
}
public add(note: Partial<Note> = {}, remindAt: number | null = null, isPreventFocus: boolean = false) {
public add(note: Partial<Note> = {}, isPreventFocus: boolean = false) {
const id = shortid();
this._store$.dispatch(addNote({
@ -61,7 +52,6 @@ export class NoteService {
...note,
},
isPreventFocus,
remindAt,
}));
}
@ -95,18 +85,6 @@ export class NoteService {
// REMINDER
// --------
addReminder(noteId: string, remindAt: number, title: string) {
this._store$.dispatch(addNoteReminder({id: noteId, remindAt, title}));
}
updateReminder(noteId: string, reminderId: string, remindAt: number, title: string) {
this._store$.dispatch(updateNoteReminder({id: noteId, reminderId, remindAt, title}));
}
removeReminder(noteId: string, reminderId: string) {
this._store$.dispatch(removeNoteReminder({id: noteId, reminderId}));
}
createFromDrop(ev: DragEvent) {
this._handleInput(createFromDrop(ev) as DropPasteInput, ev);
}

View File

@ -1,5 +1,4 @@
<div *ngIf="note"
[class.hasReminder]="note.reminderId"
[class.isFocused]="isFocus"
[class.isImg]="note.imgUrl"
class="note">
@ -23,40 +22,16 @@
class="handle-drag mat-elevation-z2"
color=""
mat-mini-fab>
<mat-icon *ngIf="!note.reminderId"
class="handle-drag">more_vert
</mat-icon>
<mat-icon *ngIf="note.reminderId"
class="handle-drag">alarm
</mat-icon>
<mat-icon class="handle-drag">more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button (click)="editReminder()"
mat-menu-item>
<ng-container *ngIf="!note.reminderId">
<mat-icon>alarm_add</mat-icon>
{{T.F.NOTE.ADD_REMINDER|translate}}
</ng-container>
<ng-container *ngIf="note.reminderId">
<mat-icon>alarm</mat-icon>
{{T.F.NOTE.EDIT_REMINDER|translate}}
</ng-container>
</button>
<button (click)="editFullscreen()"
mat-menu-item>
<mat-icon>fullscreen</mat-icon>
{{T.F.NOTE.EDIT_FULLSCREEN|translate}}
</button>
<button (click)="removeReminder()"
*ngIf="note.reminderId"
mat-menu-item>
<mat-icon style="color:#e15d63;">alarm_off</mat-icon>
{{T.F.NOTE.REMOVE_REMINDER|translate}}
</button>
<button (click)="toggleLock()"
*ngIf="!note.imgUrl"
mat-menu-item>

View File

@ -62,9 +62,6 @@ $noteFontSize: 12px;
.note:hover & {
opacity: 1;
}
.hasReminder & {
opacity: 1;
}
}
}

View File

@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, Input, ViewChild } from '@angular/c
import { Note } from '../note.model';
import { NoteService } from '../note.service';
import { MatDialog } from '@angular/material/dialog';
import { DialogAddNoteReminderComponent } from '../dialog-add-note-reminder/dialog-add-note-reminder.component';
import { T } from '../../../t.const';
import { DialogFullscreenMarkdownComponent } from '../../../ui/dialog-fullscreen-markdown/dialog-fullscreen-markdown.component';
@ -47,25 +46,6 @@ export class NoteComponent {
this._noteService.remove(this.note.id);
}
editReminder() {
this._matDialog.open(DialogAddNoteReminderComponent, {
restoreFocus: true,
data: {
note: this.note,
}
});
}
removeReminder() {
if (!this.note) {
throw new Error('No note');
}
if (!this.note.reminderId) {
throw new Error('No note reminder');
}
this._noteService.removeReminder(this.note.id, this.note.reminderId);
}
editFullscreen() {
if (!this.note) {
throw new Error('No note');

View File

@ -15,7 +15,7 @@ export const updateNoteOrder = createAction(
export const addNote = createAction(
'[Note] Add Note',
props<{ note: Note; isPreventFocus?: boolean; remindAt: number | null }>(),
props<{ note: Note; isPreventFocus?: boolean }>(),
);
export const upsertNote = createAction(
@ -56,18 +56,3 @@ export const deleteNotes = createAction(
export const clearNotes = createAction(
'[Note] Clear Notes',
);
// Reminder Actions
export const addNoteReminder = createAction(
'[Note] Add reminder',
props<{ id: string; title: string; remindAt: number }>(),
);
export const updateNoteReminder = createAction(
'[Note] Update reminder',
props<{ id: string; title: string; reminderId: string; remindAt: number }>(),
);
export const removeNoteReminder = createAction(
'[Note] Remove reminder',
props<{ id: string; reminderId: string }>(),
);

View File

@ -2,20 +2,9 @@ import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { PersistenceService } from '../../../core/persistence/persistence.service';
import { select, Store } from '@ngrx/store';
import { filter, first, map, switchMap, tap } from 'rxjs/operators';
import {
addNote,
addNoteReminder,
deleteNote,
removeNoteReminder,
updateNote,
updateNoteOrder,
updateNoteReminder
} from './note.actions';
import { first, switchMap, tap } from 'rxjs/operators';
import { addNote, deleteNote, updateNote, updateNoteOrder } from './note.actions';
import { NoteState, selectNoteFeatureState } from './note.reducer';
import { ReminderService } from '../../reminder/reminder.service';
import { T } from '../../../t.const';
import { SnackService } from '../../../core/snack/snack.service';
import { WorkContextService } from '../../work-context/work-context.service';
import { combineLatest, Observable } from 'rxjs';
@ -35,86 +24,11 @@ export class NoteEffects {
tap(([projectId, state]) => this._saveToLs(projectId, state)),
), {dispatch: false});
deleteNote$: any = createEffect(() => this._actions$.pipe(
ofType(
deleteNote,
),
tap((p) => this._reminderService.removeReminderByRelatedIdIfSet(p.id))
), {dispatch: false});
addReminderForNewNote$: any = createEffect(() => this._actions$.pipe(
ofType(
addNote
),
filter(({remindAt}) => !!remindAt && remindAt > 0),
map((p) => addNoteReminder({
id: p.note.id,
title: p.note.content.substr(0, 40),
remindAt: p.remindAt as number,
}))
));
addNoteReminder$: any = createEffect(() => this._actions$.pipe(
ofType(
addNoteReminder
),
map(({id, title, remindAt}) => {
const reminderId = this._reminderService.addReminder('NOTE', id, title, remindAt);
return updateNote({
note: {id, changes: {reminderId}}
});
}),
tap(() => this._snackService.open({
type: 'SUCCESS',
msg: T.F.NOTE.S.ADDED_REMINDER,
ico: 'schedule',
})),
));
updateNoteReminder$: any = createEffect(() => this._actions$.pipe(
ofType(
updateNoteReminder
),
tap(({title, remindAt, reminderId}) => {
this._reminderService.updateReminder(reminderId, {
remindAt,
title,
});
this._snackService.open({
type: 'SUCCESS',
msg: T.F.NOTE.S.UPDATED_REMINDER,
ico: 'schedule',
});
})
), {dispatch: false});
removeNoteReminder$: any = createEffect(() => this._actions$.pipe(
ofType(
removeNoteReminder
),
map(({id, reminderId}) => {
this._reminderService.removeReminder(reminderId);
return updateNote({
note: {
id,
changes: {reminderId: null}
}
});
}),
tap(() => this._snackService.open({
type: 'SUCCESS',
msg: T.F.NOTE.S.DELETED_REMINDER,
ico: 'schedule',
})),
));
constructor(
private _actions$: Actions,
private _store$: Store<any>,
private _persistenceService: PersistenceService,
private _reminderService: ReminderService,
private _workContextService: WorkContextService,
private _snackService: SnackService,
) {
}

View File

@ -6,7 +6,9 @@ import { select, Store } from '@ngrx/store';
import { ProjectActionTypes, UpdateProjectOrder } from './store/project.actions';
import * as shortid from 'shortid';
import {
selectArchivedProjects, selectCaldavCfgByProjectId,
selectAllProjectColors,
selectArchivedProjects,
selectCaldavCfgByProjectId,
selectGithubCfgByProjectId,
selectGitlabCfgByProjectId,
selectJiraCfgByProjectId,
@ -24,12 +26,12 @@ import { map, shareReplay, switchMap, take } from 'rxjs/operators';
import { isValidProjectExport } from './util/is-valid-project-export';
import { SnackService } from '../../core/snack/snack.service';
import { T } from '../../t.const';
import { BreakNr, BreakTime, WorkContextType } from '../work-context/work-context.model';
import { BreakNr, BreakTime, WorkContextColorEntry, WorkContextType } from '../work-context/work-context.model';
import { WorkContextService } from '../work-context/work-context.service';
import { GITHUB_TYPE, GITLAB_TYPE, JIRA_TYPE } from '../issue/issue.const';
import { GitlabCfg } from '../issue/providers/gitlab/gitlab';
import { ExportedProject } from './project-archive.model';
import {CaldavCfg} from '../issue/providers/caldav/caldav.model';
import { CaldavCfg } from '../issue/providers/caldav/caldav.model';
@Injectable({
providedIn: 'root',
@ -59,6 +61,7 @@ export class ProjectService {
: of(false)
)
);
projectColors$: Observable<WorkContextColorEntry[]> = this._store$.pipe(select(selectAllProjectColors));
// DYNAMIC

View File

@ -8,6 +8,7 @@ import { GithubCfg } from '../../issue/providers/github/github.model';
import {
WorkContextAdvancedCfg,
WorkContextAdvancedCfgKey,
WorkContextColorEntry,
WorkContextType
} from '../../work-context/work-context.model';
import {
@ -65,6 +66,14 @@ export const selectUnarchivedProjects = createSelector(selectAllProjects, (proje
export const selectArchivedProjects = createSelector(selectAllProjects, (projects) => projects.filter(p => p.isArchived));
export const selectAllProjectColors = createSelector(
selectAllProjects,
(projects: Project[]): WorkContextColorEntry[] => projects.map(project => ({
id: project.id,
color: project.theme.primary
}))
);
// DYNAMIC SELECTORS
// -----------------
export const selectProjectById = createSelector(

View File

@ -13,6 +13,7 @@ export interface ReminderCopy {
workContextType: WorkContextType;
remindAt: number;
title: string;
// TODO cleanup type
type: ReminderType;
relatedId: string;
recurringConfig?: RecurringConfig;

View File

@ -9,7 +9,6 @@ import { concatMap, delay, filter, first } from 'rxjs/operators';
import { Reminder } from './reminder.model';
import { UiHelperService } from '../ui-helper/ui-helper.service';
import { NotifyService } from '../../core/notify/notify.service';
import { DialogViewNoteReminderComponent } from '../note/dialog-view-note-reminder/dialog-view-note-reminder.component';
import { DialogViewTaskRemindersComponent } from '../tasks/dialog-view-task-reminders/dialog-view-task-reminders.component';
import { DataInitService } from '../../core/data-init/data-init.service';
import { throttle } from 'helpful-decorators';
@ -56,15 +55,7 @@ export class ReminderModule {
this._showNotification(reminders);
const oldest = reminders[0];
if (oldest.type === 'NOTE') {
this._matDialog.open(DialogViewNoteReminderComponent, {
autoFocus: false,
restoreFocus: true,
data: {
reminder: oldest,
}
});
} else if (oldest.type === 'TASK') {
if (oldest.type === 'TASK') {
this._matDialog.open(DialogViewTaskRemindersComponent, {
autoFocus: false,
restoreFocus: true,

View File

@ -27,8 +27,9 @@ const reInitCheckInterval = (reminders: ReminderCopy[]) => {
const remindersToSend = (oldest.type === 'TASK')
? dueReminders.filter(r => r.type === 'TASK')
// NOTE: for notes we just send the oldest due reminder
: [oldest];
// TODO LEGACY CODE FOR NOTE: for notes we just send the oldest due reminder
// : [oldest];
: [];
postMessage(remindersToSend);
console.log('Worker postMessage', remindersToSend);

View File

@ -13,7 +13,7 @@ import {
UpdateTaskTags
} from '../../tasks/store/task.actions';
import { TODAY_TAG } from '../tag.const';
import { WorkContextType } from '../../work-context/work-context.model';
import { WorkContextColorEntry, WorkContextType } from '../../work-context/work-context.model';
import {
moveTaskDownInTodayList,
moveTaskInTodayList,
@ -38,7 +38,13 @@ export const selectAllTagsWithoutMyDay = createSelector(
selectAllTags,
(tags: Tag[]): Tag[] => tags.filter(tag => tag.id !== TODAY_TAG.id)
);
export const selectAllTagColors = createSelector(
selectAllTags,
(tags: Tag[]): WorkContextColorEntry[] => tags.map(tag => ({
id: tag.id,
color: tag.color || tag.theme.primary
}))
);
export const selectTagById = createSelector(
selectTagFeatureState,
(state: TagState, props: { id: string }): Tag => {

View File

@ -1,18 +1,26 @@
import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { selectAllTags, selectAllTagsWithoutMyDay, selectTagById, selectTagsByIds } from './store/tag.reducer';
import {
selectAllTagColors,
selectAllTags,
selectAllTagsWithoutMyDay,
selectTagById,
selectTagsByIds
} from './store/tag.reducer';
import { addTag, deleteTag, deleteTags, updateTag, upsertTag } from './store/tag.actions';
import { Observable } from 'rxjs';
import { Tag, TagState } from './tag.model';
import * as shortid from 'shortid';
import { DEFAULT_TAG } from './tag.const';
import { TypedAction } from '@ngrx/store/src/models';
import { WorkContextColorEntry } from '../work-context/work-context.model';
@Injectable({
providedIn: 'root',
})
export class TagService {
tags$: Observable<Tag[]> = this._store$.pipe(select(selectAllTags));
tagsColors$: Observable<WorkContextColorEntry[]> = this._store$.pipe(select(selectAllTagColors));
tagsNoMyDay$: Observable<Tag[]> = this._store$.pipe(select(selectAllTagsWithoutMyDay));
constructor(

View File

@ -11,7 +11,7 @@ import {
import { selectTaskRepeatCfgFeatureState } from './task-repeat-cfg.reducer';
import { PersistenceService } from '../../../core/persistence/persistence.service';
import { Task, TaskArchive, TaskWithSubTasks } from '../../tasks/task.model';
import { AddTask, MoveToArchive, RemoveTaskReminder, UpdateTask } from '../../tasks/store/task.actions';
import { AddTask, MoveToArchive, UnScheduleTask, UpdateTask } from '../../tasks/store/task.actions';
import { TaskService } from '../../tasks/task.service';
import { TaskRepeatCfgService } from '../task-repeat-cfg.service';
import { TASK_REPEAT_WEEKDAY_MAP, TaskRepeatCfg, TaskRepeatCfgState } from '../task-repeat-cfg.model';
@ -150,7 +150,7 @@ export class TaskRepeatCfgEffects {
),
concatMap((a: AddTaskRepeatCfgToTask) => this._taskService.getByIdOnce$(a.payload.taskId).pipe(take(1))),
filter((task: TaskWithSubTasks) => typeof task.reminderId === 'string'),
map((task: TaskWithSubTasks) => new RemoveTaskReminder({
map((task: TaskWithSubTasks) => new UnScheduleTask({
id: task.id,
reminderId: task.reminderId as string
})),

View File

@ -3,15 +3,29 @@
<owl-wrapper (triggerSubmit)="save()"
[(dateTime)]="dateTime"></owl-wrapper>
<div *ngIf="isShowMoveToBacklog"
style="margin: 16px">
<mat-checkbox [(ngModel)]="isMoveToBacklog"
name="isListSubTasks">
<div class="text-wrap"
style="max-width: 280px">
{{T.F.TASK.D_REMINDER_ADD.MOVE_TO_BACKLOG|translate}}
</div>
</mat-checkbox>
<div class="additional-controls">
<mat-form-field>
<mat-select [(ngModel)]="reminderCfgId"
[placeholder]="(T.F.BOOKMARK.DIALOG_EDIT.SELECT_TYPE|translate)"
name="type"
required="true">
<mat-option *ngFor="let remindOption of remindAvailableOptions; trackBy: trackByIndex"
[innerHtml]="(remindOption.title|translate)"
[value]="remindOption.id">
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="isShowMoveToBacklog"
class="move-to-backlog-wrapper">
<mat-checkbox [(ngModel)]="isMoveToBacklog"
name="isListSubTasks">
<div class="text-wrap"
style="max-width: 280px">
{{T.F.TASK.D_REMINDER_ADD.MOVE_TO_BACKLOG|translate}}
</div>
</mat-checkbox>
</div>
</div>
</div>
</mat-dialog-content>

View File

@ -1,3 +1,5 @@
@import '../../../../variables';
:host {
display: flex;
flex-direction: column;
@ -51,6 +53,21 @@
line-height: 36px;
}
}
.additional-controls {
padding-top: $s;
// same as $owl-divider-color
border-top: 1px solid rgba(0, 0, 0, .12);
@include darkTheme(true) {
// same as $owl-dark-divider-color
border-color: rgba(255, 255, 255, .12);
}
}
.move-to-backlog-wrapper,
mat-form-field {
margin: 0 $s;
}
}
.wrap-buttons {

View File

@ -6,7 +6,8 @@ import { ReminderService } from '../../reminder/reminder.service';
import { T } from '../../../t.const';
import { AddTaskReminderInterface } from './add-task-reminder-interface';
import { throttle } from 'helpful-decorators';
import { Task } from '../task.model';
import { Task, TaskReminderOption, TaskReminderOptionId } from '../task.model';
import { millisecondsDiffToRemindOption } from '../util/remind-option-to-milliseconds';
@Component({
selector: 'dialog-add-task-reminder',
@ -22,9 +23,33 @@ export class DialogAddTaskReminderComponent {
: undefined;
isEdit: boolean = !!(this.reminder && this.reminder.id);
dateTime?: number = this.reminder && this.reminder.remindAt;
dateTime?: number = this.task.plannedAt || undefined;
isShowMoveToBacklog: boolean = (!this.isEdit && !!this.task.projectId && this.task.parentId === null);
isMoveToBacklog: boolean = (this.isShowMoveToBacklog);
// TODO make translatable
remindAvailableOptions: TaskReminderOption[] = [{
id: TaskReminderOptionId.DoNotRemind,
title: 'Dont show reminder',
}, {
id: TaskReminderOptionId.AtStart,
title: 'when it starts',
}, {
id: TaskReminderOptionId.m5,
title: '5 minutes before it starts',
}, {
id: TaskReminderOptionId.m10,
title: '10 minutes before it starts',
}, {
id: TaskReminderOptionId.m15,
title: '15 minutes before it starts',
}, {
id: TaskReminderOptionId.m30,
title: '30 minutes before it starts',
}, {
id: TaskReminderOptionId.h1,
title: '1 hour before it starts',
}];
reminderCfgId: TaskReminderOptionId;
constructor(
private _taskService: TaskService,
@ -32,6 +57,11 @@ export class DialogAddTaskReminderComponent {
private _matDialogRef: MatDialogRef<DialogAddTaskReminderComponent>,
@Inject(MAT_DIALOG_DATA) public data: AddTaskReminderInterface,
) {
if (this.isEdit) {
this.reminderCfgId = millisecondsDiffToRemindOption(this.task.plannedAt as number, this.reminder?.remindAt);
} else {
this.reminderCfgId = TaskReminderOptionId.AtStart;
}
}
// NOTE: throttle is used as quick way to prevent multiple submits
@ -44,17 +74,19 @@ export class DialogAddTaskReminderComponent {
}
if (this.isEdit && this.reminder) {
this._taskService.updateReminder(
this.task.id,
this.reminder.id,
timestamp,
this.task.title,
);
this._taskService.reScheduleTask({
taskId: this.task.id,
reminderId: this.task.reminderId as string,
plannedAt: timestamp,
remindCfg: this.reminderCfgId,
title: this.task.title,
});
this.close();
} else {
this._taskService.addReminder(
this._taskService.scheduleTask(
this.task,
timestamp,
this.reminderCfgId,
this.isMoveToBacklog,
);
this.close();
@ -68,12 +100,16 @@ export class DialogAddTaskReminderComponent {
console.log(this.reminder, this.task);
throw new Error('No reminder or id');
}
this._taskService.removeReminder(this.task.id, this.reminder.id);
this._taskService.unScheduleTask(this.task.id, this.reminder.id);
this.close();
}
close() {
this._matDialogRef.close();
}
trackByIndex(i: number, p: any) {
return i;
}
}

View File

@ -8,6 +8,7 @@ import { devError } from '../../util/dev-error';
@Injectable({providedIn: 'root'})
export class ScheduledTaskService {
// TODO maybe remove
allScheduledTasks$: Observable<TaskWithReminderData[]> = this._reminderService.reminders$.pipe(
map((reminders) => reminders.filter(
reminder => reminder.type === 'TASK'

View File

@ -21,6 +21,7 @@ const TASK: TaskCopy = {
reminderId: null,
created: Date.now(),
repeatCfgId: null,
plannedAt: null,
_showSubTasksMode: ShowSubTasksMode.Show,

View File

@ -18,7 +18,7 @@ export class TaskDbEffects {
TaskActionTypes.AddTask,
TaskActionTypes.RestoreTask,
TaskActionTypes.AddTimeSpent,
TaskActionTypes.RemoveTaskReminder,
TaskActionTypes.UnScheduleTask,
TaskActionTypes.DeleteTask,
TaskActionTypes.DeleteMainTasks,
TaskActionTypes.UndoDeleteTask,
@ -37,6 +37,11 @@ export class TaskDbEffects {
TaskActionTypes.ToggleStart,
TaskActionTypes.RoundTimeSpentForDay,
// REMINDER
TaskActionTypes.ScheduleTask,
TaskActionTypes.ReScheduleTask,
TaskActionTypes.UnScheduleTask,
// SUB ACTIONS
TaskAttachmentActionTypes.AddTaskAttachment,
TaskAttachmentActionTypes.DeleteTaskAttachment,

View File

@ -1,21 +1,22 @@
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import {
AddTaskReminder,
DeleteTask,
RemoveTaskReminder,
ReScheduleTask,
ScheduleTask,
TaskActionTypes,
UnScheduleTask,
UpdateTask,
UpdateTaskReminder,
UpdateTaskTags
} from './task.actions';
import { map, mergeMap, tap } from 'rxjs/operators';
import { filter, map, mergeMap, tap } from 'rxjs/operators';
import { ReminderService } from '../../reminder/reminder.service';
import { truncate } from '../../../util/truncate';
import { T } from '../../../t.const';
import { SnackService } from '../../../core/snack/snack.service';
import { moveTaskToBacklogListAuto } from '../../work-context/store/work-context-meta.actions';
import { TODAY_TAG } from '../../tag/tag.const';
import { EMPTY } from 'rxjs';
@Injectable()
export class TaskReminderEffects {
@ -23,9 +24,9 @@ export class TaskReminderEffects {
@Effect()
addTaskReminder$: any = this._actions$.pipe(
ofType(
TaskActionTypes.AddTaskReminder,
TaskActionTypes.ScheduleTask,
),
tap((a: AddTaskReminder) => this._snackService.open({
tap((a: ScheduleTask) => this._snackService.open({
type: 'SUCCESS',
translateParams: {
title: truncate(a.payload.task.title)
@ -33,11 +34,16 @@ export class TaskReminderEffects {
msg: T.F.TASK.S.REMINDER_ADDED,
ico: 'schedule',
})),
mergeMap((a: AddTaskReminder) => {
mergeMap((a: ScheduleTask) => {
console.log(a);
const {task, remindAt, isMoveToBacklog} = a.payload;
if (isMoveToBacklog && !task.projectId) {
throw new Error('Move to backlog not possible for non project tasks');
}
if (typeof remindAt !== 'number') {
return EMPTY;
}
const reminderId = this._reminderService.addReminder('TASK', task.id, truncate(task.title), remindAt);
const isRemoveFromToday = isMoveToBacklog && task.tagIds.includes(TODAY_TAG.id);
@ -68,16 +74,18 @@ export class TaskReminderEffects {
@Effect({dispatch: false})
updateTaskReminder$: any = this._actions$.pipe(
ofType(
TaskActionTypes.UpdateTaskReminder,
TaskActionTypes.ReScheduleTask,
),
tap((a: UpdateTaskReminder) => {
filter(({payload}: ReScheduleTask) => typeof payload.remindAt === 'number' && !!payload.reminderId),
tap((a: ReScheduleTask) => {
console.log(a);
const {title, remindAt, reminderId} = a.payload;
this._reminderService.updateReminder(reminderId, {
this._reminderService.updateReminder(reminderId as string, {
remindAt,
title,
});
}),
tap((a: UpdateTaskReminder) => this._snackService.open({
tap((a: ReScheduleTask) => this._snackService.open({
type: 'SUCCESS',
translateParams: {
title: truncate(a.payload.title)
@ -90,11 +98,12 @@ export class TaskReminderEffects {
@Effect()
removeTaskReminder$: any = this._actions$.pipe(
ofType(
TaskActionTypes.RemoveTaskReminder,
TaskActionTypes.UnScheduleTask,
),
map((a: RemoveTaskReminder) => {
filter(({payload}: UnScheduleTask) => !!payload.reminderId),
map((a: UnScheduleTask) => {
const {id, reminderId} = a.payload;
this._reminderService.removeReminder(reminderId);
this._reminderService.removeReminder(reminderId as string);
return new UpdateTask({
task: {

View File

@ -27,10 +27,10 @@ export enum TaskActionTypes {
'AddTimeSpent' = '[Task] Add time spent',
'RemoveTimeSpent' = '[Task] Remove time spent',
// Reminders
'AddTaskReminder' = '[Task] Add reminder',
'UpdateTaskReminder' = '[Task] Update reminder',
'RemoveTaskReminder' = '[Task] Remove reminder',
// Reminders & StartAt
'ScheduleTask' = '[Task] Schedule',
'UnScheduleTask' = '[Task] UnSchedule',
'ReScheduleTask' = '[Task] ReSchedule',
// Sub Task Actions
'AddSubTask' = '[Task] Add SubTask',
@ -178,24 +178,24 @@ export class RemoveTimeSpent implements Action {
}
// Reminder Actions
export class AddTaskReminder implements Action {
readonly type: string = TaskActionTypes.AddTaskReminder;
export class ScheduleTask implements Action {
readonly type: string = TaskActionTypes.ScheduleTask;
constructor(public payload: { task: Task; remindAt: number; isMoveToBacklog: boolean }) {
constructor(public payload: { task: Task; plannedAt: number; remindAt?: number; isMoveToBacklog: boolean }) {
}
}
export class UpdateTaskReminder implements Action {
readonly type: string = TaskActionTypes.UpdateTaskReminder;
export class ReScheduleTask implements Action {
readonly type: string = TaskActionTypes.ReScheduleTask;
constructor(public payload: { id: string; title: string; reminderId: string; remindAt: number }) {
constructor(public payload: { id: string; title: string; plannedAt: number; reminderId?: string; remindAt?: number }) {
}
}
export class RemoveTaskReminder implements Action {
readonly type: string = TaskActionTypes.RemoveTaskReminder;
export class UnScheduleTask implements Action {
readonly type: string = TaskActionTypes.UnScheduleTask;
constructor(public payload: { id: string; reminderId: string }) {
constructor(public payload: { id: string; reminderId?: string }) {
}
}
@ -264,9 +264,9 @@ export type TaskActions
| MoveSubTaskDown
| AddTimeSpent
| RemoveTimeSpent
| AddTaskReminder
| UpdateTaskReminder
| RemoveTaskReminder
| ScheduleTask
| ReScheduleTask
| UnScheduleTask
| RestoreTask
| AddSubTask
| ConvertToMainTask

View File

@ -1,6 +1,7 @@
import {
AddSubTask,
AddTask,
ScheduleTask,
AddTimeSpent,
ConvertToMainTask,
DeleteMainTasks,
@ -11,9 +12,11 @@ import {
MoveToArchive,
MoveToOtherProject,
RemoveTagsForAllTasks,
UnScheduleTask,
RemoveTimeSpent,
RestoreTask,
RoundTimeSpentForDay,
ReScheduleTask,
SetCurrentTask,
SetSelectedTask,
TaskActions,
@ -544,6 +547,38 @@ export function taskReducer(
}, state);
}
// REMINDER STUFF
// --------------
case TaskActionTypes.ScheduleTask: {
const {task, remindAt} = (action as ScheduleTask).payload;
return taskAdapter.updateOne({
id: task.id,
changes: {
plannedAt: remindAt,
}
}, state);
}
case TaskActionTypes.ReScheduleTask: {
const {id, plannedAt} = (action as ReScheduleTask).payload;
return taskAdapter.updateOne({
id,
changes: {
plannedAt,
}
}, state);
}
case TaskActionTypes.UnScheduleTask: {
const {id} = (action as UnScheduleTask).payload;
return taskAdapter.updateOne({
id,
changes: {
plannedAt: null,
}
}, state);
}
default: {
return state;
}

View File

@ -3,6 +3,7 @@ import { TASK_FEATURE_NAME } from './task.reducer';
import { Task, TaskState, TaskWithSubTasks } from '../task.model';
import { taskAdapter } from './task.adapter';
import { devError } from '../../../util/dev-error';
import { TODAY_TAG } from '../../tag/tag.const';
// TODO fix null stuff here
@ -119,6 +120,31 @@ export const selectCurrentTaskParentOrCurrent = createSelector(selectTaskFeature
|| s.entities[s.currentTaskId]
);
export const selectPlannedTasks = createSelector(selectTaskFeatureState, (s): Task[] => {
const allTasks: Task[] = [];
const allParent = s.ids
.map(id => s.entities[id] as Task)
.filter(task => !task.parentId && (task.plannedAt || task.tagIds.includes(TODAY_TAG.id)));
allParent.forEach((pt) => {
if (pt.subTaskIds.length) {
pt.subTaskIds.forEach(subId => {
const st = s.entities[subId] as Task;
// const par: Task = s.entities[st.parentId as string] as Task;
allTasks.push({
...st,
plannedAt: st.plannedAt || (!st.isDone
? (s.entities[st.parentId as string] as Task).plannedAt
: null)
});
});
} else {
allTasks.push(pt);
}
});
return allTasks;
});
export const selectAllTasks = createSelector(selectTaskFeatureState, selectAll);
export const selectScheduledTasks = createSelector(selectAllTasks, (tasks) => tasks.filter(task => task.reminderId));

View File

@ -17,6 +17,20 @@ export enum TaskAdditionalInfoTargetPanel {
export type DropListModelSource = 'UNDONE' | 'DONE' | 'BACKLOG';
export enum TaskReminderOptionId {
DoNotRemind = 'DoNotRemind',
AtStart = 'AtStart',
m5 = 'm5',
m10 = 'm10',
m15 = 'm15',
m30 = 'm30',
h1 = 'h1',
}
export interface TaskReminderOption {
id: TaskReminderOptionId;
title: string;
}
export interface TimeSpentOnDayCopy {
[key: string]: number;
}
@ -52,6 +66,8 @@ export interface TaskCopy extends IssueFieldsForTask {
created: number;
isDone: boolean;
doneOn: number | null;
plannedAt: number | null;
// remindCfg: TaskReminderOptionId;
notes: string;
@ -103,6 +119,7 @@ export const DEFAULT_TASK: Task = {
reminderId: null,
created: Date.now(),
repeatCfgId: null,
plannedAt: null,
_showSubTasksMode: ShowSubTasksMode.Show,

View File

@ -10,6 +10,7 @@ import {
Task,
TaskAdditionalInfoTargetPanel,
TaskArchive,
TaskReminderOptionId,
TaskState,
TaskWithSubTasks
} from './task.model';
@ -17,7 +18,6 @@ import { select, Store } from '@ngrx/store';
import {
AddSubTask,
AddTask,
AddTaskReminder,
AddTimeSpent,
ConvertToMainTask,
DeleteMainTasks,
@ -28,17 +28,18 @@ import {
MoveToArchive,
MoveToOtherProject,
RemoveTagsForAllTasks,
RemoveTaskReminder,
RemoveTimeSpent,
ReScheduleTask,
RestoreTask,
RoundTimeSpentForDay,
ScheduleTask,
SetCurrentTask,
SetSelectedTask,
ToggleStart,
ToggleTaskShowSubTasks,
UnScheduleTask,
UnsetCurrentTask,
UpdateTask,
UpdateTaskReminder,
UpdateTaskTags,
UpdateTaskUi
} from './store/task.actions';
@ -53,6 +54,7 @@ import {
selectCurrentTaskParentOrCurrent,
selectIsTaskDataLoaded,
selectMainTasksWithoutTag,
selectPlannedTasks,
selectSelectedTask,
selectSelectedTaskId, selectStartableTasks,
selectTaskAdditionalInfoTargetPanel,
@ -87,6 +89,7 @@ import { unique } from '../../util/unique';
import { SnackService } from '../../core/snack/snack.service';
import { T } from '../../t.const';
import { ImexMetaService } from '../../imex/imex-meta/imex-meta.service';
import { remindOptionToMilliseconds } from './util/remind-option-to-milliseconds';
@Injectable({
providedIn: 'root',
@ -145,6 +148,10 @@ export class TaskService {
select(selectStartableTasks),
);
plannedTasks$: Observable<Task[]> = this._store.pipe(
select(selectPlannedTasks),
);
// META FIELDS
// -----------
currentTaskProgress$: Observable<number> = this.currentTask$.pipe(
@ -488,19 +495,46 @@ export class TaskService {
// REMINDER
// --------
addReminder(task: Task | TaskWithSubTasks, remindAt: number, isMoveToBacklog: boolean = false) {
this._store.dispatch(new AddTaskReminder({task, remindAt, isMoveToBacklog}));
scheduleTask(task: Task | TaskWithSubTasks, plannedAt: number, remindCfg: TaskReminderOptionId, isMoveToBacklog: boolean = false) {
console.log(remindOptionToMilliseconds(plannedAt, remindCfg), plannedAt);
console.log(remindOptionToMilliseconds(plannedAt, remindCfg) as number - plannedAt);
console.log({
plannedAt,
remindCfg,
});
this._store.dispatch(new ScheduleTask({
task,
plannedAt,
remindAt: remindOptionToMilliseconds(plannedAt, remindCfg),
isMoveToBacklog
}));
}
updateReminder(taskId: string, reminderId: string, remindAt: number, title: string) {
this._store.dispatch(new UpdateTaskReminder({id: taskId, reminderId, remindAt, title}));
reScheduleTask({
taskId,
plannedAt,
reminderId,
remindCfg,
title
}: { taskId: string; plannedAt: number; title: string; reminderId?: string; remindCfg: TaskReminderOptionId}) {
this._store.dispatch(new ReScheduleTask({
id: taskId,
plannedAt,
reminderId,
remindAt: remindOptionToMilliseconds(plannedAt, remindCfg),
title
}));
}
removeReminder(taskId: string, reminderId: string) {
if (!reminderId || !taskId) {
throw new Error('No reminder or task id');
unScheduleTask(taskId: string, reminderId?: string) {
console.log('unschedzle', {reminderId});
if (!taskId) {
throw new Error('No task id');
}
this._store.dispatch(new RemoveTaskReminder({id: taskId, reminderId}));
this._store.dispatch(new UnScheduleTask({id: taskId, reminderId}));
}
// HELPER

View File

@ -0,0 +1,48 @@
import { TaskReminderOptionId } from '../task.model';
export const remindOptionToMilliseconds = (plannedAt: number, remindOptId: TaskReminderOptionId): number | undefined => {
switch (remindOptId) {
case TaskReminderOptionId.AtStart : {
return plannedAt;
}
case TaskReminderOptionId.m5 : {
return plannedAt - 5 * 60 * 1000;
}
case TaskReminderOptionId.m10 : {
return plannedAt - 10 * 60 * 1000;
}
case TaskReminderOptionId.m15 : {
return plannedAt - 15 * 60 * 1000;
}
case TaskReminderOptionId.m30 : {
return plannedAt - 30 * 60 * 1000;
}
case TaskReminderOptionId.h1 : {
return plannedAt - 60 * 60 * 1000;
}
}
return undefined;
};
export const millisecondsDiffToRemindOption = (plannedAt: number, remindAt?: number): TaskReminderOptionId => {
if (typeof remindAt !== 'number') {
return TaskReminderOptionId.DoNotRemind;
}
const diff: number = plannedAt as number - remindAt;
if (diff >= 60 * 60 * 1000) {
return TaskReminderOptionId.h1;
} else if (diff >= 30 * 60 * 1000) {
return TaskReminderOptionId.m30;
} else if (diff >= 15 * 60 * 1000) {
return TaskReminderOptionId.m15;
} else if (diff >= 10 * 60 * 1000) {
return TaskReminderOptionId.m10;
} else if (diff >= 5 * 60 * 1000) {
return TaskReminderOptionId.m5;
} else if (diff === 0) {
return TaskReminderOptionId.AtStart;
} else {
return TaskReminderOptionId.DoNotRemind;
}
};

View File

@ -86,3 +86,12 @@ export interface WorkContextState {
activeType: WorkContextType | null;
// additional entities state properties
}
export interface WorkContextColorEntry {
id: string;
color: string;
}
export interface WorkContextColorMap {
[key: string]: string;
}

View File

@ -5,6 +5,7 @@ import {
WorkContext,
WorkContextAdvancedCfg,
WorkContextAdvancedCfgKey,
WorkContextColorMap,
WorkContextState,
WorkContextThemeCfg,
WorkContextType
@ -34,7 +35,7 @@ import { hasTasksToWorkOn, mapEstimateRemainingFromTasks } from './work-context.
import { flattenTasks, selectTaskEntities, selectTasksWithSubTasksByIds } from '../tasks/store/task.selectors';
import { Actions, ofType } from '@ngrx/effects';
import { moveTaskToBacklogList } from './store/work-context-meta.actions';
import { selectProjectById } from '../project/store/project.reducer';
import { selectAllProjectColors, selectProjectById } from '../project/store/project.reducer';
import { WorklogExportSettings } from '../worklog/worklog.model';
import {
AddToProjectBreakTime,
@ -288,6 +289,26 @@ export class WorkContextService {
shareReplay(1),
);
allWorkContextColors$: Observable<WorkContextColorMap> = combineLatest([
// avoid circular dep
this._store$.pipe(select(selectAllProjectColors)),
this._tagService.tagsColors$,
]).pipe(
map(([forProjects, forTags]) => {
const workContextColorMap: WorkContextColorMap = {};
forProjects.forEach((project) => {
workContextColorMap[project.id] = project.color;
});
forTags.forEach((tag) => {
workContextColorMap[tag.id] = tag.color;
});
return workContextColorMap;
}),
shareReplay(1),
);
// allWorkContextColors$: Observable<any> =
constructor(
private _store$: Store<WorkContextState>,
private _actions$: Actions,

View File

@ -39,7 +39,7 @@ export class BacklogComponent {
if (!task.reminderId) {
throw new Error('Task without reminder');
}
this.taskService.removeReminder(task.id, task.reminderId);
this.taskService.unScheduleTask(task.id, task.reminderId);
}
}

View File

@ -0,0 +1 @@
<sup-calendar></sup-calendar>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CalendarPageComponent } from './calendar-page.component';
describe('CalendarPageComponent', () => {
let component: CalendarPageComponent;
let fixture: ComponentFixture<CalendarPageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CalendarPageComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CalendarPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
@Component({
selector: 'calendar-page',
templateUrl: './calendar-page.component.html',
styleUrls: ['./calendar-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CalendarPageComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CalendarPageComponent } from './calendar-page.component';
import { CalendarModule } from '../../features/calendar/calendar.module';
@NgModule({
declarations: [
CalendarPageComponent
],
imports: [
CommonModule,
CalendarModule,
]
})
export class CalendarPageModule {
}

View File

@ -10,10 +10,12 @@ import { WorklogPageModule } from './worklog-page/worklog-page.module';
import { ProjectSettingsPageModule } from './project-settings-page/project-settings-page.module';
import { TagTaskPageModule } from './tag-task-page/tag-task-page.module';
import { TagSettingsPageModule } from './tag-settings-page/tag-settings-page.module';
import { CalendarPageModule } from './calendar-page/calendar-page.module';
@NgModule({
imports: [
CommonModule,
CalendarPageModule,
ConfigPageModule,
ProjectOverviewPageModule,
ProjectTaskPageModule,

View File

@ -54,7 +54,7 @@ export class SchedulePageComponent {
removeReminder(task: TaskWithReminderData) {
if (task.reminderId) {
this._taskService.removeReminder(task.id, task.reminderId);
this._taskService.unScheduleTask(task.id, task.reminderId);
}
}
@ -84,7 +84,7 @@ export class SchedulePageComponent {
this._taskService.moveToToday(task.id, true);
}
if (!!task.reminderId) {
this._taskService.removeReminder(task.id, task.reminderId);
this._taskService.unScheduleTask(task.id, task.reminderId);
}
this._taskService.setCurrentId(task.id);
this._router.navigate(['/active/tasks']);

View File

@ -1069,6 +1069,7 @@ const T = {
},
MH: {
ADD_NEW_TASK: 'MH.ADD_NEW_TASK',
CALENDAR: 'MH.CALENDAR',
CREATE_PROJECT: 'MH.CREATE_PROJECT',
CREATE_TAG: 'MH.CREATE_TAG',
DELETE_TAG: 'MH.DELETE_TAG',

View File

@ -4,6 +4,7 @@
[isNoMonthSquares]="true"
[laterTodaySlots]="laterTodaySlots"
[min]="now"
[dayStartsAt]="DAY_STARTS_AT"
[ngModelOptions]="{standalone:true}"
[ngModel]="date"
[sLaterToday]="T.DATETIME_SCHEDULE.LATER_TODAY|translate"

View File

@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { GlobalConfigService } from 'src/app/features/config/global-config.service';
import { T } from 'src/app/t.const';
import { DAY_STARTS_AT } from '../../app.constants';
@Component({
selector: 'owl-wrapper',
@ -36,6 +37,8 @@ export class OwlWrapperComponent {
'23:30',
];
DAY_STARTS_AT: string = DAY_STARTS_AT;
firstDayOfWeek$: Observable<number> = this._globalConfigService.misc$.pipe(
map(cfg => cfg.firstDayOfWeek),
startWith(0),

View File

@ -1,4 +1,4 @@
export const isToday = (date: number): boolean => {
export const isToday = (date: number | Date): boolean => {
const d = new Date(date);
const isValid = d.getTime() > 0;
if (!isValid) {

View File

@ -404,22 +404,11 @@
}
},
"NOTE": {
"ADD_REMINDER": "Add reminder",
"D_ADD": {
"DATETIME_LABEL": "Datetime for reminder (optional)",
"NOTE_LABEL": "Enter some text to save as note..."
},
"D_ADD_REMINDER": {
"E_ENTER_TITLE": "You need to enter a title",
"L_DATETIME": "Datetime for reminder",
"L_TITLE": "Title for notification"
},
"D_VIEW_REMINDER": {
"SNOOZE": "Snooze",
"TITLE": "Note"
},
"EDIT_FULLSCREEN": "Edit in fullscreen",
"EDIT_REMINDER": "Edit reminder",
"NOTES_CMP": {
"ADD_BTN": "Add new Note",
"DROP_TO_ADD": "Drop here to add new note"
@ -427,14 +416,7 @@
"NOTE_CMP": {
"DISABLE_PARSE": "Disable markdown parsing",
"ENABLE_PARSE": "Enable markdown parse"
},
"REMOVE_REMINDER": "Remove reminder",
"S": {
"ADDED_REMINDER": "Added reminder for note",
"DELETED_REMINDER": "Deleted reminder for note",
"UPDATED_REMINDER": "Updated reminder for note"
},
"UPDATE_REMINDER": "Update reminder"
}
},
"POMODORO": {
"BACK_TO_WORK": "Back to work!",
@ -1069,6 +1051,7 @@
},
"MH": {
"ADD_NEW_TASK": "Add new Task",
"CALENDAR": "Calendar",
"CREATE_PROJECT": "Create Project",
"CREATE_TAG": "Create Tag",
"DELETE_TAG": "Delete Tag",

View File

@ -7,5 +7,6 @@
@import "enlarge-img";
@import "date-time-picker-schedule";
@import "overwrite-material";
@import "overwrite-fullcalendar";
@import "promise-btn";
@import "global-error-alert";

View File

@ -0,0 +1,60 @@
//@media(max-width: 767px) {
// .fc-toolbar.fc-header-toolbar {
// display: flex;
// flex-direction: column;
// }
// .fc-toolbar.fc-header-toolbar .fc-left {
// order: 3;
// }
// .fc-toolbar.fc-header-toolbar .fc-center {
// order: 1;
// }
// .fc-toolbar.fc-header-toolbar .fc-right {
// order: 2;
// }
//}
.fc-timeGridDay-view {
.fc-timegrid-col,
.fc-daygrid-day {
&.fc-day-today {
background: transparent !important;
}
}
}
.fc .fc-timegrid-now-indicator-line {
border-color: $c-accent !important;
box-shadow: $shadow-card-shadow;
}
.fc-timegrid-event {
&.isDone .fc-event-title {
text-decoration: line-through;
}
&.hasAlarm {
.fc-event-title:before {
content: '';
}
}
&.isCurrent {
border-width: 3px;
border-color: $c-accent !important;
box-shadow: $shadow-card-shadow;
}
}
.fc-theme-standard td,
.fc-theme-standard th {
border-color: rgba(0, 0, 0, .5);
@include darkTheme(true) {
border-color: rgba(255, 255, 255, .3);
}
}
.fc-theme-standard .fc-scrollgrid {
border: none !important;
}