Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.

Commit 672d2a6

Browse files
authored
feat: fix toggle table filters (#141)
1 parent e9c60bf commit 672d2a6

13 files changed

Lines changed: 220 additions & 144 deletions

File tree

apps/gauzy/src/app/pages/employees/employees.component.ts

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
InviteMutationComponent,
4343
PaginationFilterBaseComponent,
4444
PictureNameTagsComponent,
45-
ToggleFilterComponent
45+
SmartToggleFilterComponent
4646
} from '@gauzy/ui-core/shared';
4747
import { EmployeeTimeTrackingStatusComponent, EmployeeWorkStatusComponent } from './table-components';
4848

@@ -182,8 +182,13 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
182182
.componentLayout$(this.viewComponentName)
183183
.pipe(
184184
distinctUntilChange(),
185-
tap((componentLayout) => (this.dataLayoutStyle = componentLayout)),
186-
tap(() => this.refreshPagination()),
185+
tap((componentLayout: ComponentLayoutStyleEnum) => {
186+
this.dataLayoutStyle = componentLayout;
187+
this._loadSmartTableSettings();
188+
this._additionalColumns();
189+
this._registerDataTableColumns();
190+
this.refreshPagination();
191+
}),
187192
filter((componentLayout) => componentLayout === ComponentLayoutStyleEnum.CARDS_GRID),
188193
tap(() => (this.employees = [])),
189194
tap(() => this.employees$.next(true)),
@@ -685,7 +690,10 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
685690
},
686691
filter: {
687692
type: 'custom',
688-
component: InputFilterComponent
693+
component: InputFilterComponent,
694+
config: {
695+
initialValueInput: this.filters?.where?.user?.name ?? null
696+
}
689697
},
690698
filterFunction: this._getFilterFunction('user.name')
691699
},
@@ -700,7 +708,10 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
700708
isFilterable: true,
701709
filter: {
702710
type: 'custom',
703-
component: InputFilterComponent
711+
component: InputFilterComponent,
712+
config: {
713+
initialValueInput: this.filters?.where?.user?.email ?? null
714+
}
704715
},
705716
filterFunction: this._getFilterFunction('user.email')
706717
},
@@ -762,9 +773,28 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
762773
width: '5%',
763774
filter: {
764775
type: 'custom',
765-
component: ToggleFilterComponent
776+
component: SmartToggleFilterComponent,
777+
config: {
778+
initialChoice:
779+
this.filters?.where?.isTrackingEnabled === true
780+
? 'accept'
781+
: this.filters?.where?.isTrackingEnabled === false
782+
? 'deny'
783+
: null,
784+
acceptValue: { field: 'isTrackingEnabled', value: true },
785+
denyValue: { field: 'isTrackingEnabled', value: false }
786+
}
787+
},
788+
filterFunction: (data: { field: string; value: boolean } | null) => {
789+
if (!data) {
790+
// Reset filter
791+
this.setFilterWithServiceFalse({ field: 'isTrackingEnabled', search: null });
792+
return false;
793+
}
794+
795+
this.setFilterWithServiceFalse({ field: data.field, search: data.value });
796+
return false;
766797
},
767-
filterFunction: this._getFilterFunction('isTrackingEnabled'),
768798
renderComponent: EmployeeTimeTrackingStatusComponent,
769799
componentInitFunction: (instance: EmployeeTimeTrackingStatusComponent, cell: Cell) => {
770800
instance.rowData = cell.getRow().getData();
@@ -781,7 +811,10 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
781811
isSortable: false,
782812
filter: {
783813
type: 'custom',
784-
component: EmploymentTypeFilterComponent
814+
component: EmploymentTypeFilterComponent,
815+
config: {
816+
initialValueInput: this.filters?.where?.organizationEmploymentTypes ?? null
817+
}
785818
},
786819
filterFunction: (employmentTypes: IOrganizationEmploymentType[]) => {
787820
const typeIds = employmentTypes.map((t) => t.id);
@@ -806,11 +839,27 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
806839
isSortable: false,
807840
filter: {
808841
type: 'custom',
809-
component: ToggleFilterComponent
842+
component: SmartToggleFilterComponent,
843+
config: {
844+
initialChoice: this.filters?.where?.isActive
845+
? 'accept'
846+
: this.filters?.where?.isArchived
847+
? 'deny'
848+
: null,
849+
acceptValue: { isActive: true, isArchived: false },
850+
denyValue: { isActive: false, isArchived: true }
851+
}
810852
},
811-
filterFunction: (isActive: boolean) => {
812-
this.setFilter({ field: 'isActive', search: isActive });
813-
return isActive;
853+
filterFunction: (value: { isActive?: boolean; isArchived?: boolean } | null) => {
854+
if (!value) {
855+
// Reset filter
856+
this.setFilter({ field: 'isActive', search: null });
857+
this.setFilter({ field: 'isArchived', search: null });
858+
return false;
859+
}
860+
if ('isActive' in value) this.setFilter({ field: 'isActive', search: value.isActive });
861+
if ('isArchived' in value) this.setFilter({ field: 'isArchived', search: value.isArchived });
862+
return false;
814863
},
815864
renderComponent: EmployeeWorkStatusComponent,
816865
componentInitFunction: (instance: EmployeeWorkStatusComponent, cell: Cell) => {
@@ -897,11 +946,27 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements
897946
hide: allowScreenshotCapture === false,
898947
filter: {
899948
type: 'custom',
900-
component: ToggleFilterComponent
949+
component: SmartToggleFilterComponent,
950+
config: {
951+
initialChoice:
952+
this.filters?.where?.allowScreenshotCapture === true
953+
? 'accept'
954+
: this.filters?.where?.allowScreenshotCapture === false
955+
? 'deny'
956+
: null,
957+
acceptValue: { field: 'allowScreenshotCapture', value: true },
958+
denyValue: { field: 'allowScreenshotCapture', value: false }
959+
}
901960
},
902-
filterFunction: (isEnable: boolean) => {
903-
this.setFilter({ field: 'allowScreenshotCapture', search: isEnable });
904-
return isEnable;
961+
filterFunction: (data: { field: string; value: boolean } | null) => {
962+
if (!data) {
963+
// Reset filter
964+
this.setFilterWithServiceFalse({ field: 'allowScreenshotCapture', search: null });
965+
return false;
966+
}
967+
968+
this.setFilterWithServiceFalse({ field: data.field, search: data.value });
969+
return false;
905970
},
906971
renderComponent: AllowScreenshotCaptureComponent, // The component to render the column
907972
componentInitFunction: (instance: AllowScreenshotCaptureComponent, cell: Cell) => {

apps/gauzy/src/app/pages/users/users.component.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
TagsOnlyComponent,
4242
UserMutationComponent,
4343
UserRoleFilterComponent,
44-
UserToggleFilterComponent
44+
SmartToggleFilterComponent
4545
} from '@gauzy/ui-core/shared';
4646
import { EmployeeWorkStatusComponent } from '../employees/table-components';
4747
import { HttpClient } from '@angular/common/http';
@@ -613,30 +613,25 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI
613613
isSortable: false,
614614
filter: {
615615
type: 'custom',
616-
component: UserToggleFilterComponent,
616+
component: SmartToggleFilterComponent,
617617
config: {
618-
initialValueToggle: {
619-
isActive: this.filters?.where?.isActive ?? null,
620-
isArchived: this.filters?.where?.isArchived ?? null
621-
}
618+
initialChoice: this.filters?.where?.isActive
619+
? 'accept'
620+
: this.filters?.where?.isArchived
621+
? 'deny'
622+
: null,
623+
acceptValue: { isActive: true, isArchived: false },
624+
denyValue: { isActive: false, isArchived: true }
622625
}
623626
},
624-
filterFunction: (value: { isActive?: boolean; isArchived?: boolean }) => {
627+
filterFunction: (value: { isActive?: boolean; isArchived?: boolean } | null) => {
625628
if (!value) {
626-
// reset
627629
this.setFilter({ field: 'isActive', search: null });
628630
this.setFilter({ field: 'isArchived', search: null });
629631
return false;
630632
}
631-
632-
if ('isActive' in value) {
633-
this.setFilter({ field: 'isActive', search: value.isActive });
634-
}
635-
636-
if ('isArchived' in value) {
637-
this.setFilter({ field: 'isArchived', search: value.isArchived });
638-
}
639-
633+
if ('isActive' in value) this.setFilter({ field: 'isActive', search: value.isActive });
634+
if ('isArchived' in value) this.setFilter({ field: 'isArchived', search: value.isArchived });
640635
return false;
641636
},
642637
renderComponent: EmployeeWorkStatusComponent,

packages/contracts/src/lib/smart-table-filters.model.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ export interface CustomFilterConfig {
44
initialValueInput?: string;
55
initialValueStatus?: ITaskStatus;
66
initialValueIds?: string[];
7-
initialValueToggle?: StatusFilterValue;
87
}
98

109
export interface StatusFilterValue {
1110
isActive?: boolean;
1211
isArchived?: boolean;
1312
}
13+
14+
export interface ToggleFilterConfig {
15+
initialChoice?: 'accept' | 'deny' | null;
16+
acceptValue?: boolean;
17+
denyValue?: boolean;
18+
}

packages/core/src/lib/employee/employee.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ export class EmployeeService extends TenantAwareCrudService<Employee> {
566566
if (isNotEmpty(user.email)) {
567567
const keywords: string[] = user.email.split(' ');
568568
keywords.forEach((keyword: string, index: number) => {
569-
web.orWhere(p(`LOWER("user"."email") like LOWER(:email_${index})`), {
569+
web.andWhere(p(`LOWER("user"."email") LIKE LOWER(:email_${index})`), {
570570
[`email_${index}`]: `%${keyword}%`
571571
});
572572
});

packages/ui-core/shared/src/lib/smart-data-layout/pagination/pagination-filter-base.component.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,42 @@ export class PaginationFilterBaseComponent extends TranslationBaseComponent impl
117117
}
118118
}
119119

120+
/**
121+
* Set filter for data based on the provided filter object.
122+
* @param filter - The filter object containing information about the field and search criteria.
123+
* @param doEmit - A boolean flag indicating whether to emit a notification after setting the filter. Default is true.
124+
*/
125+
protected setFilterWithServiceFalse(filter: any, doEmit = true) {
126+
// Split the field path into an array of field names
127+
const fields = filter?.field?.split?.('.');
128+
const search = filter?.search;
129+
130+
if (!fields || fields.length === 0) return;
131+
132+
if (search != null) {
133+
// Create an object with nested keys representing the field path and set the search value
134+
const keys = fields.reduceRight((value: string, key: string) => ({ [key]: value }), search);
135+
136+
// Update the 'where' property in the 'filters' object with the new keys
137+
this.filters = {
138+
where: {
139+
...this.filters.where,
140+
...keys,
141+
...mergeDeep(this.filters.where, keys)
142+
}
143+
};
144+
} else {
145+
// If the search criteria is empty or not a boolean, remove the field from the 'where' property
146+
const [field] = fields.reverse();
147+
cleanKeys(this.filters.where, field);
148+
}
149+
150+
// Emit a notification if doEmit is true
151+
if (doEmit) {
152+
this.subject$.next(true);
153+
}
154+
}
155+
120156
/**
121157
*
122158
* @param selectedPage

packages/ui-core/shared/src/lib/table-filters/employment-type-filter.component.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core';
22
import { DefaultFilter } from 'angular2-smart-table';
3-
import { IOrganizationEmploymentType } from '@gauzy/contracts';
3+
import { CustomFilterConfig, IOrganizationEmploymentType } from '@gauzy/contracts';
44
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
55
import { OrganizationEmploymentTypesService, Store } from '@gauzy/ui-core/core';
66

@@ -10,6 +10,7 @@ import { OrganizationEmploymentTypesService, Store } from '@gauzy/ui-core/core';
1010
<ng-select
1111
id="employmentType"
1212
[items]="employmentTypes"
13+
[(ngModel)]="selectedEmploymentTypes"
1314
bindLabel="name"
1415
[searchable]="false"
1516
placeholder="{{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.EMPLOYMENT_TYPE' | translate }}"
@@ -35,6 +36,7 @@ import { OrganizationEmploymentTypesService, Store } from '@gauzy/ui-core/core';
3536
@UntilDestroy({ checkProperties: true })
3637
export class EmploymentTypeFilterComponent extends DefaultFilter implements OnInit, OnChanges {
3738
employmentTypes: IOrganizationEmploymentType[] = [];
39+
selectedEmploymentTypes: IOrganizationEmploymentType[] = [];
3840

3941
constructor(
4042
private readonly organizationEmploymentTypesService: OrganizationEmploymentTypesService,
@@ -44,6 +46,7 @@ export class EmploymentTypeFilterComponent extends DefaultFilter implements OnIn
4446
}
4547

4648
ngOnInit(): void {
49+
const config = this.column?.filter?.config as CustomFilterConfig;
4750
const { id: organizationId, tenantId } = this.store.selectedOrganization;
4851

4952
// Fetch employment types from the service and subscribe to the observable for asynchronous updates
@@ -56,6 +59,12 @@ export class EmploymentTypeFilterComponent extends DefaultFilter implements OnIn
5659
.subscribe((types) => {
5760
// Store the fetched employment types in a class-level property
5861
this.employmentTypes = types.items;
62+
63+
if (config?.initialValueInput?.length) {
64+
this.selectedEmploymentTypes = this.employmentTypes.filter((t) =>
65+
config.initialValueInput.includes(t.id)
66+
);
67+
}
5968
});
6069
}
6170

packages/ui-core/shared/src/lib/table-filters/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export * from './product-category-filter.component';
1414
export * from './product-type-filter.component';
1515
export * from './employment-type-filter.component';
1616
export * from './user-role-filter.component';
17-
export * from './user-toggle-filter/user-toggle-filter.component';
17+
export * from './smart-toggle-filter/smart-toggle-filter.component';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<div class="container">
2+
<div class="btn-grp">
3+
<button
4+
nbButton
5+
class="check"
6+
[ngClass]="choice === 'accept' ? 'on' : 'off'"
7+
type="button"
8+
(click)="choice = 'accept'; onChange()"
9+
>
10+
<fa-icon [icon]="faCheck"></fa-icon>
11+
</button>
12+
<button nbButton class="na" [ngClass]="!choice ? 'on' : 'off'" (click)="choice = null; onChange()">
13+
<fa-icon [icon]="faBan"></fa-icon>
14+
</button>
15+
<button
16+
nbButton
17+
class="deny"
18+
[ngClass]="choice === 'deny' ? 'on' : 'off'"
19+
(click)="choice = 'deny'; onChange()"
20+
>
21+
<fa-icon [icon]="faTimes"></fa-icon>
22+
</button>
23+
</div>
24+
</div>

packages/ui-core/shared/src/lib/table-filters/user-toggle-filter/user-toggle-filter.component.scss renamed to packages/ui-core/shared/src/lib/table-filters/smart-toggle-filter/smart-toggle-filter.component.scss

File renamed without changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core';
2+
import { DefaultFilter } from 'angular2-smart-table';
3+
import { faCheck, faBan, faTimes } from '@fortawesome/free-solid-svg-icons';
4+
import { ToggleFilterConfig } from '@gauzy/contracts';
5+
6+
@Component({
7+
selector: 'ga-smart-toggle-filter',
8+
templateUrl: './smart-toggle-filter.component.html',
9+
styleUrls: ['./smart-toggle-filter.component.scss'],
10+
standalone: false
11+
})
12+
export class SmartToggleFilterComponent extends DefaultFilter implements OnInit, OnChanges {
13+
faCheck = faCheck;
14+
faBan = faBan;
15+
faTimes = faTimes;
16+
choice: 'accept' | 'deny' | null = null;
17+
private config: ToggleFilterConfig;
18+
19+
ngOnInit(): void {
20+
this.config = this.column?.filter?.config as ToggleFilterConfig;
21+
22+
if (this.config?.initialChoice !== undefined) {
23+
this.choice = this.config.initialChoice;
24+
}
25+
}
26+
27+
onChange(): void {
28+
let value = null;
29+
30+
switch (this.choice) {
31+
case 'accept':
32+
value = this.config?.acceptValue ?? true;
33+
break;
34+
case 'deny':
35+
value = this.config?.denyValue ?? false;
36+
break;
37+
default:
38+
value = null;
39+
}
40+
41+
this.column.filterFunction(value, this.column.id);
42+
}
43+
44+
ngOnChanges(changes: SimpleChanges): void {
45+
// Required for angular2-smart-table, even if unused
46+
}
47+
}

0 commit comments

Comments
 (0)