Skip to content

Commit

Permalink
Fix/#7492 task switching (#8195)
Browse files Browse the repository at this point in the history
* feat: create selector interface

* feat: create generate store and query abstract

* feat: create reusable components

* feat: create client selector feature

* feat: create note selector feature

* feat: project selector feature

* feat: create task selector feature

* feat: create team selector feature

* feat: create time tracker form

* refactored: Time Tracker component to use GauzyTimeTrackerForm, removed client, project, team, and task selection fields, and added TimeTrackerModule to imports in TimeTrackerModule.

* updated various files in desktop-ui-lib, including selector stores, components, and modules, with changes to imports, exports, and method implementations.

* fix: null safety in selector queries, and add canAddTag and addTagText properties to gauzy-select components"

* feat: add isLoading input to SelectComponent and updated ClientSelector, ProjectSelector, TaskSelector, and TeamSelector components to use it.

* update: styles and templates for ng-select component in desktop-ui-lib and desktop-timer apps.

* feat: integrate selectors to time tracker

* refactor SelectorStore: update data concatenation and remove redundant line

* feat: add time tracker query, store, and implement dialog for task updates

* remove: NbButtonModule, NbIconModule, and PipeModule imports from TimeTrackerFormModule.

* update project selector, task selector, and team selector components; modify time tracker form and query; add ignition method to time tracker store; update time tracker component

* update: time tracker component to calculate and send idle time on restart

* fix: TimeTracker component to send startedAt as ISO string, and modify TimerTrackerChangeDialog component to add large class and expanded$ observable

* feat: add isLoading$, disabled$, and hasError$ bindings to client-selector, project-selector, task-selector, and team-selector components

* refactor: desktop timer app update imports, rename variables, and modify component templates and logic

* refactor: adjust logic in time tracker form and dialog components.

* chore: add new command to make mac build fast in package.json

* fix: intervalDAO and IntervalService remove idle queries

* refactor: PowerManagerDetectInactivity and DesktopOsInactivityHandler classes, updating event handling, inactivity detection, and idle time removal logic.

* fix: remove idle event listener conditional statements, added checks, and modified IPC renderer events

* feat: prevent duplicated API Requests method for start/stop API calls

* feat: add `isOnBattery` property to `BasePowerManagerDecorator` and `DesktopPowerManager`, and updated `DesktopOsInactivityHandler` to use it.

* feat: add new IPC handlers

* refactor: TimeTrackerComponent and TimeTrackerService: removed ITimerStatus, added ITimeLog, modified method signatures and implementations

* feat: add `hasPermission` property to  selectors and update components to use new property, and removed old permission BehaviorSubjects.

* feat: add UntilDestroy decorator to several components, modified time-tracker-form.component.ts, and updated time-tracker.query.ts and time-tracker.component.ts

* fix: use complete value

* feat:  handle UPDATE_SELECTOR IPC event

* feat: update desktop-ipc, desktop-timer, and time-tracker component with changes to timer handling and dependencies.

* fix:  deepscan
  • Loading branch information
adkif authored Sep 14, 2024
1 parent f142775 commit 0827e45
Show file tree
Hide file tree
Showing 75 changed files with 2,706 additions and 608 deletions.
5 changes: 3 additions & 2 deletions apps/desktop-timer/src/assets/styles/_overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ body {

.ng-dropdown-panel {
background-color: var(--gauzy-card-5) !important;
border-radius: var(--border-radius) !important;

.scroll-host {
scrollbar-width: thin;
Expand Down Expand Up @@ -262,7 +263,7 @@ body {
}

.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background: var(--background-basic-color-1);
background: var(--background-basic-color-1) !important;
color: var(--gauzy-text-color-1);
transition-duration: 0.15s;
transition-property: border, background-color, color, box-shadow;
Expand All @@ -277,7 +278,7 @@ body {
}

.ng-dropdown-panel .ng-dropdown-panel-items .ng-option-marked {
color: var(--gauzy-text-color-1);
color: var(--gauzy-text-color-1) !important;
background-color: var(--background-basic-color-1) !important;
}

Expand Down
50 changes: 48 additions & 2 deletions packages/desktop-libs/src/lib/desktop-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind
log.info('Update Synced Timer');
try {
if (!arg.id) {
const lastCapture = await timerService.findLastCapture();
const lastCapture = await timerService.findLastOne();

if (lastCapture && lastCapture.id) {
const { id } = lastCapture;
Expand Down Expand Up @@ -1108,6 +1108,43 @@ export function ipcTimer(
ipcMain.handle('MARK_AS_STOPPED_OFFLINE', async () => {
await markLastTimerAsStoppedOffline();
});

ipcMain.handle('CURRENT_TIMER', () => {
return timerService.findLastOne();
});

ipcMain.handle('LAST_SYNCED_INTERVAL', () => {
return intervalService.findLastInterval();
});

ipcMain.handle('UPDATE_SELECTOR', async (_, args) => {
try {
// Destructure args for cleaner access
const { taskId, timerId, projectId, description, organizationTeamId, organizationContactId, startedAt } =
args;

const config = {
taskId,
projectId,
note: description,
organizationTeamId,
organizationContactId
};

// Update the local store configuration
LocalStore.updateConfigProject(config);

// Update last timeslot moment
timerHandler.timeSlotStart = moment(startedAt);

// Update timer with new config
await timerService.update(new Timer({ id: timerId, description, startedAt, ...config }));

console.log('update selector', '✔️ Done');
} catch (error) {
console.error('Failed to update selector:', error);
}
});
}

export function removeMainListener() {
Expand Down Expand Up @@ -1167,7 +1204,16 @@ export function removeAllHandlers() {
}

export function removeTimerHandlers() {
const channels = ['START_TIMER', 'STOP_TIMER', 'LOGOUT_STOP', 'DELETE_TIME_SLOT', 'MARK_AS_STOPPED_OFFLINE'];
const channels = [
'START_TIMER',
'STOP_TIMER',
'LOGOUT_STOP',
'DELETE_TIME_SLOT',
'MARK_AS_STOPPED_OFFLINE',
'CURRENT_TIMER',
'LAST_SYNCED_INTERVAL',
'UPDATE_SELECTOR'
];
channels.forEach((channel: string) => {
ipcMain.removeHandler(channel);
});
Expand Down
29 changes: 14 additions & 15 deletions packages/desktop-libs/src/lib/desktop-timer.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import moment from 'moment';
import { ActivityType, IActivityWatchCollectEventData, ITimeLog, TimeLogSourceEnum } from '@gauzy/contracts';
import { app, screen } from 'electron';
import { metaData } from './desktop-wakatime';
import { LocalStore } from './desktop-store';
import moment from 'moment';
import { DesktopActiveWindow } from './desktop-active-window';
import { DesktopEventCounter } from './desktop-event-counter';
import NotificationDesktop from './desktop-notifier';
import { detectActiveWindow, getScreenshot } from './desktop-screenshot';
import { ActivityType, ITimeLog, TimeLogSourceEnum } from '@gauzy/contracts';
import { DesktopEventCounter } from './desktop-event-counter';
import { DesktopActiveWindow } from './desktop-active-window';
import { DesktopOfflineModeHandler, Timer, TimerService, UserService } from './offline';
import { IOfflineMode } from './interfaces';
import { IActivityWatchCollectEventData } from '@gauzy/contracts';
import { LocalStore } from './desktop-store';
import { metaData } from './desktop-wakatime';
import {
ActivityWatchAfkService,
ActivityWatchChromeService,
ActivityWatchEdgeService,
ActivityWatchEventManager,
ActivityWatchEventTableList,
ActivityWatchService,
ActivityWatchFirefoxService,
ActivityWatchChromeService,
ActivityWatchWindowService,
ActivityWatchAfkService,
ActivityWatchEdgeService
ActivityWatchService,
ActivityWatchWindowService
} from './integrations';
import { IOfflineMode } from './interfaces';
import { DesktopOfflineModeHandler, Timer, TimerService, UserService } from './offline';

import log from 'electron-log';
console.log = log.log;
Expand Down Expand Up @@ -219,7 +218,7 @@ export default class TimerHandler {
await this._timerService.update(
new Timer({
id: this.lastTimer ? this.lastTimer.id : null,
startedAt: new Date(),
startedAt: this.timeSlotStart.utc().toDate(),
synced: !this._offlineMode.enabled,
isStartedOffline: this._offlineMode.enabled
})
Expand Down
34 changes: 34 additions & 0 deletions packages/desktop-ui-lib/src/lib/shared/+state/selector.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Query } from '@datorama/akita';
import { Observable } from 'rxjs';
import { ISelector } from '../interfaces/selector.interface';
import { SelectorStore } from './selector.store';

export abstract class SelectorQuery<T> extends Query<ISelector<T>> {
/**
* Creates an instance of SelectorQuery.
* @param store The store of type {@link SelectorStore<T>}
*/
protected constructor(store: SelectorStore<T>) {
super(store);
}

public get data$(): Observable<T[]> {
return this.select((state) => state.data);
}

public get data(): T[] {
return this.getValue().data;
}

public get selected$(): Observable<T> {
return this.select((state) => state.selected);
}

public get selected(): T {
return this.getValue().selected;
}

public get hasPermission$(): Observable<boolean> {
return this.select((state) => state.hasPermission);
}
}
44 changes: 44 additions & 0 deletions packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { SelectorQuery } from './selector.query';
import { SelectorStore } from './selector.store';

@Injectable({
providedIn: 'root'
})
export abstract class SelectorService<T> {
protected constructor(
public readonly selectorStore: SelectorStore<T>,
public readonly selectorQuery: SelectorQuery<T>
) {}

public abstract load(): Promise<void>;

public getAll$(): Observable<T[]> {
return this.selectorQuery.data$;
}

public getAll(): T[] {
return this.selectorQuery.data;
}

public get selected(): T {
return this.selectorQuery.selected;
}

public set selected(selected: T | string) {
this.selectorStore.updateSelected(selected);
}

public set hasPermission(hasPermission: boolean) {
this.selectorStore.update({ hasPermission: hasPermission });
}

public get hasPermission(): boolean {
return this.selectorStore.getValue().hasPermission;
}

public get hasPermission$(): Observable<boolean> {
return this.selectorQuery.hasPermission$;
}
}
38 changes: 38 additions & 0 deletions packages/desktop-ui-lib/src/lib/shared/+state/selector.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Store } from '@datorama/akita';
import { ISelector } from '../interfaces/selector.interface';

export abstract class SelectorStore<T> extends Store<ISelector<T>> {
protected constructor(private readonly initialState: ISelector<T>) {
super(initialState);
}

public updateData(data: T[]): void {
this.update({ data });
}

public updateSelected(selected: T | string): void {
if (!selected) {
this.update({ selected: null });
return;
}

if (typeof selected === 'string') {
selected = this.getValue().data.find((value: any) => selected === value.id);
}

this.update({ selected });
}

public appendData(selected: T): void {
if (!selected) {
return;
}
const data = this.getValue().data;
this.updateSelected(selected);
this.updateData(data.concat([selected]));
}

public resetToInitialState(): void {
this.update(this.initialState);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<div class="gauzy-select-container">
<label *ngIf="label">{{ label | translate }}</label>
<ng-select
(clear)="onClear()"
[loading]="isLoading"
(ngModelChange)="onModelChange($event)"
[ngModel]="selectedItem"
[addTagText]="addTagText | translate"
[addTag]="canAddTag ? addTag : null"
[class.error-border]="hasError"
[clearable]="clearable"
[disabled]="disabled"
[items]="items"
[nbTooltipDisabled]="!disabled"
[nbTooltip]="tooltipText | translate"
[placeholder]="placeholder | translate"
[bindLabel]="bindLabel"
[bindValue]="bindValue"
nbTooltipStatus="warning"
>
<!-- Option Template -->
<ng-template ng-option-tmp let-item="item">
<ng-container
[ngTemplateOutlet]="bindLabel === 'title' ? titleTemplate : defaultTemplate"
[ngTemplateOutletContext]="{ item: item }"
></ng-container>
</ng-template>

<!-- Label Template -->
<ng-template ng-label-tmp let-item="item">
<div class="selector-template">
<ng-container
[ngTemplateOutlet]="bindLabel === 'title' ? titleTemplate : defaultTemplate"
[ngTemplateOutletContext]="{ item: item }"
></ng-container>
</div>
</ng-template>
</ng-select>
</div>

<!-- Default Template -->
<ng-template #defaultTemplate let-item="item">
<ng-container *ngTemplateOutlet="commonTemplate; context: { item: item, label: item[bindLabel] }"></ng-container>
</ng-template>

<!-- Title Template -->
<ng-template #titleTemplate let-item="item">
<span class="text-container">
<span *ngIf="item?.number" class="gray-bold-text">
#{{ item?.prefix ? (item?.prefix + '-' + item?.number | uppercase) : item?.number }}
</span>
<span class="black-bold-text">{{ item?.title }}</span>
</span>
</ng-template>

<!-- Common Template (for both option and label) -->
<ng-template #commonTemplate let-item="item" let-label="label">
<div class="common-template">
<img *ngIf="item?.imageUrl" [src]="item?.imageUrl" height="20px" width="20px" />
<span class="black-bold-text">{{ label }}</span>
</div>
</ng-template>
Loading

0 comments on commit 0827e45

Please sign in to comment.