Skip to content

Commit

Permalink
[Perf] Introduce server side pagination for selectors (#8274)
Browse files Browse the repository at this point in the history
* feat: create routing for tabs

* feat: improve lanaguage service

* feat: create cached server data server

* feat: caching task for 24 hours

* feat: add new observable to `selectorService`

* feat: change modidier private to public

* feat: improve create task dialog

* feat: create task table features

* feat: replace table with router tabset

* fix: deepscan

* feat: add pagination type to task cache

* feat: improve selector with pagination params

* feat: add new input to generic ng selector

* feat: update ISelector interface

* feat: introduce server side pagination for task selectors

* feat: infinite scroll on client selector

* feat: removed 'tags' from relations in client selector request and updated method call to getClientWithPagination

* refactor: code in various components and services to improve pagination and caching functionality

* feat: add reset page functionality to client, project, and team selectors; updated task table component to use selected project and team IDs; refactored time tracker service to use toParams function

* fix: remove "prefix" property from search term object in task table component.

* feat: updated TimeTrackerService to use TaskStatisticsCacheService

* fix:  'tags' from relations and modified join and where clauses in TimeTrackerService

* feat: add search component with debounce and loading indicator, updated task table component to display loading spinner, and refactored cached server data source to handle loading state

* fix: removed spinner from search component, updated search component to use distinctUntilChange and filter, and removed loading state handling from task table component

* feat: update SelectComponent to use Subject<string> for typeahead instead of any"

* fix: add search functionality to TaskSelectorService and TaskSelectorComponent, updated TimeTrackerService to include search term in API request

* fix: add search functionality to client selector, project selector, and team selector components and services

* feat: add typeToSearchText input property to SelectComponent and updated template to use it

* refactor: moved nbSpinner to separate div, added absolute positioning and dimensions to .smart-table class

* fix: relations and join from TimeTrackerService query parameters, update employeeId condition

* refactored: tasks component, replaced ng-select with gauzy-select, updated imports and modules, and made minor template changes.

* fix: Task Table component HTML and SCSS: wrapped spinner in ng-container, changed smart-table styles to use absolute positioning and added padding.

* fix: remove resetPage calls from client-selector, project-selector, and team-selector components.

* refactor: selector components to extend AbstractSelectorComponent, update search logic, and add NG_VALUE_ACCESSOR provider

* feat: use selectors as `formControl` for reactive forms

* refactor: selectors and make it reusable for reactive form and template binding form

* refactor: selector store and components, update time tracker component and dialog

* fix: max-width property

* fix: cspell

* Update project-cache.service.ts

* Update client-cache.service.ts

* Update teams-cache.service.ts

---------

Co-authored-by: Ruslan Konviser <[email protected]>
  • Loading branch information
adkif and evereq authored Sep 27, 2024
1 parent 417bb09 commit ccd993a
Show file tree
Hide file tree
Showing 49 changed files with 970 additions and 561 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,8 @@
"nbbutton",
"xaxis",
"wdth",
"concate"
"concate",
"typeahead"
],
"useGitignore": true,
"ignorePaths": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Injectable } from '@angular/core';
import { AbstractCacheService } from './abstract-cache.service';
import { IOrganizationContact } from '@gauzy/contracts';
import { IOrganizationContact, IPagination } from '@gauzy/contracts';
import { StorageService } from './storage.service';
import { Store } from '../services';

@Injectable({
providedIn: 'root',
})
export class ClientCacheService extends AbstractCacheService<
IOrganizationContact[]
IOrganizationContact[] | IPagination<IOrganizationContact>
> {
constructor(
protected _storageService: StorageService<IOrganizationContact[]>,
protected _storageService: StorageService<IOrganizationContact[] | IPagination<IOrganizationContact>>,
protected _store: Store
) {
super(_storageService, _store);
this.prefix = ClientCacheService.name.toString();
this.duration = 1 * 3600 * 1000; // 1 hour
}
}
36 changes: 19 additions & 17 deletions packages/desktop-ui-lib/src/lib/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
export * from './abstract-cache.service';
export * from './client-cache.service';
export * from './employee-cache.service';
export * from './error-client.service';
export * from './error-handler.service';
export * from './error-server.service';
export * from './image-cache.service';
export * from './language-cache.service';
export * from './native-notification.service';
export * from './notification.service';
export * from './organizations-cache.service';
export * from './project-cache.service';
export * from './server-connection.service';
export * from './status-icon-service/status-icon-cache.service';
export * from './status-icon-service/status-icon.service';
export * from './storage.service';
export * from './store.service';
export * from './tag-cache.service';
export * from './tag.service';
export * from './task-cache.service';
export * from './task-priority-cache.service';
export * from './task-size-cache.service';
export * from './task-statistics-cache.service';
export * from './task-status-cache.service';
export * from './teams-cache.service';
export * from './time-log-cache.service';
export * from './time-slot-cache.service';
export * from './user-organization-cache.service';
export * from './error-client.service';
export * from './error-handler.service';
export * from './error-server.service';
export * from './native-notification.service';
export * from './notification.service';
export * from './toastr-notification.service';
export * from './store.service';
export * from './server-connection.service';
export * from './image-cache.service';
export * from './language-cache.service';
export * from './time-tracker-date.manager';
export * from './time-zone-manager';
export * from './task-status-cache.service';
export * from './teams-cache.service';
export * from './status-icon-service';
export * from './task-priority-cache.service';
export * from './task-size-cache.service';
export * from './tag.service';
export * from './toastr-notification.service';
export * from './user-organization-cache.service';
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Injectable } from '@angular/core';
import { IOrganizationProject, IPagination } from '@gauzy/contracts';
import { Store } from '../services';
import { AbstractCacheService } from './abstract-cache.service';
import { IOrganizationProject } from '@gauzy/contracts';
import { StorageService } from './storage.service';
import { Store } from '../services';

@Injectable({
providedIn: 'root',
providedIn: 'root'
})
export class ProjectCacheService extends AbstractCacheService<
IOrganizationProject[]
IOrganizationProject[] | IPagination<IOrganizationProject>
> {
constructor(
protected _storageService: StorageService<IOrganizationProject[]>,
protected _storageService: StorageService<IOrganizationProject[] | IPagination<IOrganizationProject>>,
protected _store: Store
) {
super(_storageService, _store);
this.prefix = ProjectCacheService.name.toString();
this.duration = 1 * 3600 * 1000; // 1 hour
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Injectable } from '@angular/core';
import { AbstractCacheService } from './abstract-cache.service';
import { ITask } from '@gauzy/contracts';
import { IPagination, ITask } from '@gauzy/contracts';
import { StorageService } from './storage.service';
import { Store } from '../services';

@Injectable({
providedIn: 'root',
})
export class TaskCacheService extends AbstractCacheService<ITask[]> {
export class TaskCacheService extends AbstractCacheService<ITask[] | IPagination<ITask>> {
constructor(
protected _storageService: StorageService<ITask[]>,
protected _storageService: StorageService<ITask[] | IPagination<ITask>>,
protected _store: Store
) {
super(_storageService, _store);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { ITasksStatistics } from '@gauzy/contracts';
import { Store } from '.';
import { AbstractCacheService } from './abstract-cache.service';
import { StorageService } from './storage.service';

@Injectable({
providedIn: 'root'
})
export class TaskStatisticsCacheService extends AbstractCacheService<ITasksStatistics[]> {
constructor(protected _storageService: StorageService<ITasksStatistics[]>, protected _store: Store) {
super(_storageService, _store);
this.prefix = TaskStatisticsCacheService.name.toString();
this.duration = 600 * 1000; // 1O minutes
}
}
11 changes: 5 additions & 6 deletions packages/desktop-ui-lib/src/lib/services/teams-cache.service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { Injectable } from '@angular/core';
import { IOrganizationTeam, IPagination } from '@gauzy/contracts';
import { StorageService, Store } from '../services';
import { IOrganizationTeam } from '@gauzy/contracts';
import { AbstractCacheService } from './abstract-cache.service';

@Injectable({
providedIn: 'root',
providedIn: 'root'
})
export class TeamsCacheService extends AbstractCacheService<
IOrganizationTeam[]
> {
export class TeamsCacheService extends AbstractCacheService<IOrganizationTeam[] | IPagination<IOrganizationTeam>> {
constructor(
protected _storageService: StorageService<IOrganizationTeam[]>,
protected _storageService: StorageService<IOrganizationTeam[] | IPagination<IOrganizationTeam>>,
protected _store: Store
) {
super(_storageService, _store);
this.prefix = TeamsCacheService.name.toString();
this.duration = 1 * 3600 * 1000; // 1 hour
}
}
20 changes: 20 additions & 0 deletions packages/desktop-ui-lib/src/lib/shared/+state/selector.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ export abstract class SelectorQuery<T> extends Query<ISelector<T>> {
return this.getValue().selected;
}

public get page(): number {
return this.getValue().page;
}

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

public get limit(): number {
return this.getValue().limit;
}

public get total(): number {
return this.getValue().total;
}

public get hasNext(): boolean {
return this.page * this.limit < this.total;
}

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

Expand All @@ -12,7 +12,7 @@ export abstract class SelectorService<T> {
public readonly selectorQuery: SelectorQuery<T>
) {}

public abstract load(): Promise<void>;
public abstract load(options?: { searchTerm?: string }): Promise<void>;

public getAll$(): Observable<T[]> {
return this.selectorQuery.data$;
Expand Down Expand Up @@ -45,4 +45,18 @@ export abstract class SelectorService<T> {
public get selected$(): Observable<T> {
return this.selectorQuery.selected$;
}

public onScrollToEnd(): void {
if (this.selectorQuery.hasNext) {
this.selectorStore.next();
}
}

public get onScroll$(): Observable<void> {
return this.selectorQuery.page$.pipe(concatMap(() => this.load()));
}

public resetPage() {
this.selectorStore.update({ page: 1, data: [] });
}
}
16 changes: 15 additions & 1 deletion packages/desktop-ui-lib/src/lib/shared/+state/selector.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export abstract class SelectorStore<T> extends Store<ISelector<T>> {
this.update({ data });
}

public updateInfiniteList(list: { data: T[]; total: number }): void {
const { data, total } = list;
const items = this.getValue().data;
this.update({
data: [...new Map([...items, ...data].map((item) => [item['id'], item])).values()],
total
});
}

public updateSelected(selected: T | string): void {
if (!selected) {
this.update({ selected: null });
Expand All @@ -28,11 +37,16 @@ export abstract class SelectorStore<T> extends Store<ISelector<T>> {
return;
}
const data = this.getValue().data;
this.updateData([...new Map([...data, selected].map((item) => [item['id'], item])).values()]);
this.updateSelected(selected);
this.updateData(data.concat([selected]));
}

public resetToInitialState(): void {
this.update(this.initialState);
}

public next(): void {
const current = this.getValue().page;
this.update({ page: current + 1 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Component } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { SelectorService } from '../../+state/selector.service';

@UntilDestroy({ checkProperties: true })
@Component({
template: ''
})
export abstract class AbstractSelectorComponent<T> implements ControlValueAccessor {
public search$ = new Subject<string>();
private onChange: (value: any) => void;
private onTouched: () => void;
protected isDisabled$ = new BehaviorSubject<boolean>(false);

// Flag to control whether to update the store
protected useStore: boolean = true;

// Abstract members to be implemented in derived classes
public abstract data$: Observable<T[]>;
public abstract selected$: Observable<T>;
public abstract isLoading$: Observable<boolean>;
public abstract disabled$: Observable<boolean>;
public abstract hasPermission$: Observable<boolean>;

constructor() {}

// Handle value change
public change(value: string): void {
this.onChange?.(value); // Notify the form control
this.onTouched?.();
this.updateSelected(value); // Update the store only if useStore is true
}

// Implement ControlValueAccessor methods
public writeValue(value: string): void {
this.useStore = false; // Disable store updates when used in a form
if (value) {
this.updateSelected(value);
}
}

public registerOnChange(fn: any): void {
this.onChange = fn;
}

public registerOnTouched(fn: any): void {
this.onTouched = fn;
}

public setDisabledState(isDisabled: boolean): void {
this.isDisabled$.next(isDisabled);
}

// Abstract method to update selected item
protected abstract updateSelected(value: string): void;

// Common search handling logic
protected handleSearch(service: SelectorService<T>) {
this.search$
.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() => service.resetPage()),
switchMap((searchTerm) => service.load({ searchTerm })),
untilDestroyed(this)
)
.subscribe();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
[bindLabel]="bindLabel"
[bindValue]="bindValue"
nbTooltipStatus="warning"
[typeahead]="typeahead"
[typeToSearchText]="typeToSearchText | translate"
(scrollToEnd)="onScrollToEnd()"
>
<!-- Option Template -->
<ng-template ng-option-tmp let-item="item">
Expand Down
Loading

0 comments on commit ccd993a

Please sign in to comment.