Compare commits
52 Commits
master
...
feat/calen
Author | SHA1 | Date |
---|---|---|
Johannes Millan | ac39fe72c6 | |
Johannes Millan | 02590f1d12 | |
Johannes Millan | 72509af5d3 | |
Johannes Millan | 4aa4a1dcdc | |
Johannes Millan | f68c716cc1 | |
Johannes Millan | 9f08bd7322 | |
Johannes Millan | 32fe3ec630 | |
Johannes Millan | b19809f8a2 | |
Johannes Millan | 9ab3c9aac9 | |
Johannes Millan | 08383bd13f | |
Johannes Millan | db3b9f4737 | |
Johannes Millan | b6ed036653 | |
Johannes Millan | 26f3d5989e | |
Johannes Millan | a2d576269a | |
Johannes Millan | a8dcb6370d | |
Johannes Millan | 9f884303da | |
Johannes Millan | 442e922004 | |
Johannes Millan | eb1f4506f2 | |
Johannes Millan | c544822757 | |
Johannes Millan | 65c373d2c0 | |
Johannes Millan | 91869ae6cf | |
Johannes Millan | 89dd603f6b | |
Johannes Millan | e127926264 | |
Johannes Millan | bb7589b7e1 | |
Johannes Millan | ea8c0c47bf | |
Johannes Millan | 74d48855ac | |
Johannes Millan | 606f2e93e4 | |
Johannes Millan | 51314d3b6e | |
Johannes Millan | 6330bb92a7 | |
Johannes Millan | 9297b69c74 | |
Johannes Millan | ff1bfab424 | |
Johannes Millan | bfbad5318f | |
Johannes Millan | 29167fd91a | |
Johannes Millan | 0ccc315214 | |
Johannes Millan | 2a2dec283f | |
Johannes Millan | 5ffcbcc711 | |
Johannes Millan | 3b49a5c6d2 | |
Johannes Millan | 932cc1212a | |
Johannes Millan | 0641ec2655 | |
Johannes Millan | 2530fb8526 | |
Johannes Millan | 951ff87d7b | |
Johannes Millan | c841408ad6 | |
Johannes Millan | 7e62de3f93 | |
Johannes Millan | c3a6359fe2 | |
Johannes Millan | 1fdc884145 | |
Johannes Millan | fdb0d2a497 | |
Johannes Millan | 9781e9be78 | |
Johannes Millan | 04013dba44 | |
Johannes Millan | a5ebddf99c | |
Johannes Millan | 1b1b86971a | |
Johannes Millan | 72bceb0d65 | |
Johannes Millan | 79fc1ffc43 |
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<full-calendar #calendar
|
||||
*ngIf="calOptions"
|
||||
[options]="calOptions"></full-calendar>
|
|
@ -0,0 +1,10 @@
|
|||
:host {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
> full-calendar {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
// });
|
||||
// });
|
|
@ -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,
|
||||
// }],
|
|
@ -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;
|
|
@ -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 {
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
// });
|
||||
// });
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
}
|
|
@ -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();
|
||||
// });
|
||||
// });
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -6,5 +6,4 @@ export interface Note {
|
|||
backgroundColor?: string;
|
||||
created: number;
|
||||
modified: number;
|
||||
reminderId?: string | null;
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -62,9 +62,6 @@ $noteFontSize: 12px;
|
|||
.note:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
.hasReminder & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 }>(),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface ReminderCopy {
|
|||
workContextType: WorkContextType;
|
||||
remindAt: number;
|
||||
title: string;
|
||||
// TODO cleanup type
|
||||
type: ReminderType;
|
||||
relatedId: string;
|
||||
recurringConfig?: RecurringConfig;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
})),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -21,6 +21,7 @@ const TASK: TaskCopy = {
|
|||
reminderId: null,
|
||||
created: Date.now(),
|
||||
repeatCfgId: null,
|
||||
plannedAt: null,
|
||||
|
||||
_showSubTasksMode: ShowSubTasksMode.Show,
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<sup-calendar></sup-calendar>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue