Skip to content

Commit 0eaed7e

Browse files
authored
Creates a custom grid header menu (#51)
* header menu actions fxnal * user settings reset * update user settings on menu click * display filter/sort regardless of colDef * reset menu - not hooked into grid menu * reset menu fxnal * store potential menu options * cleanup
1 parent c11d581 commit 0eaed7e

File tree

12 files changed

+409
-81
lines changed

12 files changed

+409
-81
lines changed

src/app/modules/inventory-detail/detail/header.component.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MatDialog } from '@angular/material/dialog'
55
import { type MatSelect } from '@angular/material/select'
66
import { AgGridAngular } from 'ag-grid-angular'
77
import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
8+
import { filter, take, tap } from 'rxjs'
89
import type { AccessLevelInstance, Label, Organization } from '@seed/api'
910
import { LabelComponent } from '@seed/components'
1011
import { MaterialImports } from '@seed/materials'
@@ -208,9 +209,13 @@ export class HeaderComponent implements OnInit {
208209
},
209210
})
210211

211-
dialogRef.afterClosed().subscribe((message) => {
212-
if (message === 'refresh') this.refreshDetail.emit()
213-
})
212+
dialogRef.afterClosed()
213+
.pipe(
214+
take(1),
215+
filter(Boolean),
216+
tap(() => { this.refreshDetail.emit() }),
217+
)
218+
.subscribe()
214219
}
215220

216221
trackByFn(_index: number, { id }: AccessLevelInstance) {
Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,57 @@
1-
<div class="flex justify-between gap-4 overflow-hidden">
2-
<span class="my-auto">{{ params.displayName }}</span>
3-
<mat-icon class="scale-50 cursor-pointer" (click)="toggleMenu($event)" svgIcon="fa-solid:chevron-down"></mat-icon>
1+
<!-- TRIGGER -->
2+
<div class="flex w-full justify-between gap-4 overflow-hidden" #trigger>
3+
<div class="flex gap-4">
4+
<span class="my-auto">{{ params.displayName }}</span>
5+
@if (sortIcon) {
6+
<mat-icon class="scale-50" [svgIcon]="sortIcon"></mat-icon>
7+
}
8+
</div>
9+
<div class="cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800" (click)="toggleMenu()">
10+
<mat-icon class="scale-50" svgIcon="fa-solid:ellipsis-vertical"></mat-icon>
11+
</div>
412
</div>
5-
<!-- THIS IS IN DEVELOPMENT -->
6-
@if (menuVisible) {
7-
<div class="cell-menu absolute z-[1000] w-48 rounded-md border border-gray-300 bg-white p-2 shadow-lg">
8-
<button (click)="sortAsc()">Sort Asc</button>
13+
14+
<!-- MENU -->
15+
<ng-template #menu>
16+
<div class="w-80 rounded border p-2 text-sm" [class.bg-[#2A3341]]="scheme === 'dark'" [class.bg-white]="scheme === 'light'">
17+
<div class="menu-option" (click)="sortCol('asc')">
18+
<mat-icon class="scale-50" svgIcon="fa-solid:arrow-up"></mat-icon>
19+
<span>Sort Ascending</span>
20+
</div>
21+
22+
<div class="menu-option" (click)="sortCol('desc')">
23+
<mat-icon class="scale-50" svgIcon="fa-solid:arrow-down"></mat-icon>
24+
<span>Sort Descending</span>
25+
</div>
26+
27+
@if (sortIcon) {
28+
<div class="menu-option" (click)="sortCol(null)">
29+
<mat-icon class="scale-50" svgIcon="fa-solid:minus"></mat-icon>
30+
<span>Clear Sort</span>
31+
</div>
32+
}
33+
34+
<mat-divider></mat-divider>
35+
36+
<div class="menu-option" (click)="pinCol('left')">
37+
<mat-icon class="scale-50" [svgIcon]="'fa-solid:thumbtack'"></mat-icon>
38+
<span>Pin Left</span>
39+
</div>
40+
<div class="menu-option" (click)="pinCol('right')">
41+
<mat-icon class="scale-50" svgIcon="fa-solid:thumbtack"></mat-icon>
42+
<span>Pin right</span>
43+
</div>
44+
@if (pinState) {
45+
<div class="menu-option" (click)="pinCol(null)">
46+
<mat-icon class="scale-50" svgIcon="fa-solid:minus"></mat-icon>
47+
<span>Unpin</span>
48+
</div>
49+
}
50+
51+
<mat-divider></mat-divider>
52+
<div class="menu-option" (click)="hideCol()">
53+
<mat-icon class="scale-50" svgIcon="fa-solid:eye-slash"></mat-icon>
54+
<span>Hide Column</span>
55+
</div>
956
</div>
10-
}
57+
</ng-template>

src/app/modules/inventory-list/list/grid/cell-header-menu.component.ts

Lines changed: 133 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,149 @@
1-
import { Component } from '@angular/core'
1+
import type { FlexibleConnectedPositionStrategyOrigin, OverlayRef } from '@angular/cdk/overlay'
2+
import { Overlay } from '@angular/cdk/overlay'
3+
import { TemplatePortal } from '@angular/cdk/portal'
4+
import { CommonModule } from '@angular/common'
5+
import type { AfterViewInit, TemplateRef } from '@angular/core'
6+
import { Component, inject, ViewChild, ViewContainerRef } from '@angular/core'
27
import type { IHeaderAngularComp } from 'ag-grid-angular'
3-
import type { IHeaderParams } from 'ag-grid-community'
8+
import type { Column, GridApi, IHeaderParams } from 'ag-grid-community'
9+
import { take } from 'rxjs'
10+
import type { CurrentUser, OrganizationUserSettings } from '@seed/api'
11+
import { OrganizationService } from '@seed/api'
412
import { MaterialImports } from '@seed/materials'
13+
import { ConfigService } from '@seed/services'
14+
import type { InventoryType } from 'app/modules/inventory/inventory.types'
515

616
@Component({
717
selector: 'seed-inventory-grid-cell-header-menu',
818
templateUrl: './cell-header-menu.component.html',
9-
imports: [MaterialImports],
19+
imports: [CommonModule, MaterialImports],
1020
})
11-
export class CellHeaderMenuComponent implements IHeaderAngularComp {
12-
// THIS COMPONENT IS STILL IN DEVELOPMENT
13-
public params: IHeaderParams
14-
public menuVisible = false
21+
export class CellHeaderMenuComponent implements IHeaderAngularComp, AfterViewInit {
22+
@ViewChild('menu') menuTemplate!: TemplateRef<unknown>
23+
@ViewChild('trigger') trigger!: FlexibleConnectedPositionStrategyOrigin
1524

16-
agInit(params: IHeaderParams): void {
25+
private _configService = inject(ConfigService)
26+
private _organizationService = inject(OrganizationService)
27+
private _overlay = inject(Overlay)
28+
private _vcr = inject(ViewContainerRef)
29+
column: Column<unknown>
30+
gridApi: GridApi
31+
opened = false
32+
orgId: number
33+
orgUserId: number
34+
overlayRef: OverlayRef
35+
params: IHeaderParams
36+
pinState: unknown
37+
scheme: 'dark' | 'light'
38+
sortIcon = ''
39+
type: InventoryType
40+
userSettings: OrganizationUserSettings
41+
42+
agInit(params: IHeaderParams & { currentUser: CurrentUser; type: InventoryType }): void {
1743
this.params = params
44+
this.column = params.column
45+
this.gridApi = params.api
46+
this.type = params.type
47+
const { org_id, org_user_id, settings } = params.currentUser
48+
this.orgId = org_id
49+
this.orgUserId = org_user_id
50+
this.userSettings = settings
51+
}
52+
53+
ngAfterViewInit(): void {
54+
this.getScheme()
55+
this.setOverlay()
56+
this.updateSortState()
57+
this.column.addEventListener('sortChanged', () => {
58+
this.updateSortState()
59+
})
60+
this.gridApi.addEventListener('columnPinned', () => {
61+
this.pinState = this.column.isPinned()
62+
})
63+
}
64+
65+
getScheme() {
66+
this._configService.scheme$.subscribe((scheme) => {
67+
this.scheme = scheme
68+
})
69+
}
70+
71+
updateSortState() {
72+
const state = this.gridApi.getColumnState().find((col) => col.colId === this.params.column.getColId())
73+
const sortDir = state?.sort ?? null
74+
const iconMap = {
75+
asc: 'fa-solid:arrow-up',
76+
desc: 'fa-solid:arrow-down',
77+
}
78+
this.sortIcon = iconMap[sortDir] ?? ''
79+
}
80+
81+
setOverlay() {
82+
const positionStrategy = this._overlay
83+
.position()
84+
.flexibleConnectedTo(this.trigger)
85+
.withPositions([
86+
{
87+
originX: 'end',
88+
originY: 'bottom',
89+
overlayX: 'start',
90+
overlayY: 'top',
91+
},
92+
])
93+
94+
this.overlayRef = this._overlay.create({
95+
positionStrategy,
96+
hasBackdrop: true,
97+
backdropClass: 'transparent-backdrop',
98+
})
99+
100+
this.overlayRef.backdropClick().subscribe(() => {
101+
this.overlayRef.detach()
102+
})
103+
}
104+
105+
toggleMenu(): void {
106+
// event.stopPropagation()
107+
// this.menuVisible = !this.menuVisible
108+
if (this.overlayRef?.hasAttached()) {
109+
this.overlayRef.detach()
110+
} else {
111+
const portal = new TemplatePortal(this.menuTemplate, this._vcr)
112+
this.overlayRef?.attach(portal)
113+
}
114+
}
115+
116+
sortCol(direction: 'asc' | 'desc' | null): void {
117+
this.gridApi.applyColumnState({
118+
state: [{ colId: this.params.column.getColId(), sort: direction }],
119+
defaultState: { sort: null },
120+
})
121+
const dir = direction === 'desc' ? '-' : ''
122+
const colDef = this.column.getColDef()
123+
const sort = `${dir}${colDef.field}`
124+
this.userSettings.sorts?.[this.type].push(sort)
125+
126+
this.detach()
127+
}
128+
129+
pinCol(direction: 'left' | 'right' | null): void {
130+
this.gridApi.setColumnsPinned([this.column], direction)
131+
this.detach()
132+
}
133+
134+
hideCol() {
135+
this.gridApi.setColumnsVisible([this.column], false)
136+
this.detach()
18137
}
19138

20-
toggleMenu(event: MouseEvent): void {
21-
event.stopPropagation()
22-
this.menuVisible = !this.menuVisible
139+
updateOrgUserSettings() {
140+
return this._organizationService.updateOrganizationUser(this.orgUserId, this.orgId, this.userSettings)
141+
.pipe(take(1))
142+
.subscribe()
23143
}
24144

25-
sortAsc(): void {
26-
console.log('sort asc')
27-
this.menuVisible = false
145+
detach() {
146+
this.overlayRef.detach()
28147
}
29148

30149
refresh() {

src/app/modules/inventory-list/list/grid/filter-sort-chips.component.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
22
import type { OnChanges, SimpleChanges } from '@angular/core'
33
import { Component, Input } from '@angular/core'
44
import type { ColDef, GridApi } from 'ag-grid-community'
5-
import type { OrganizationUserSettings } from '@seed/api'
5+
import type { Column, OrganizationUserSettings } from '@seed/api'
66
import { MaterialImports } from '@seed/materials'
77
import type { AgFilter, FilterSortChip, FilterType, InventoryType } from '../../../inventory/inventory.types'
88

@@ -12,15 +12,17 @@ import type { AgFilter, FilterSortChip, FilterType, InventoryType } from '../../
1212
imports: [CommonModule, MaterialImports],
1313
})
1414
export class FilterSortChipsComponent implements OnChanges {
15-
@Input() gridApi: GridApi
15+
@Input() columns: Column[]
1616
@Input() columnDefs: ColDef[]
17+
@Input() gridApi: GridApi
1718
@Input() userSettings: OrganizationUserSettings
1819
@Input() type: InventoryType
1920
filterChips: FilterSortChip[] = []
2021
sortChips: FilterSortChip[] = []
2122

22-
ngOnChanges({ userSettings }: SimpleChanges) {
23-
if (userSettings?.currentValue) {
23+
ngOnChanges(changes: SimpleChanges) {
24+
const { userSettings, columnDefs } = changes
25+
if (userSettings?.currentValue || columnDefs?.currentValue) {
2426
this.getFilterChips()
2527
this.getSortChips()
2628
}
@@ -38,12 +40,20 @@ export class FilterSortChipsComponent implements OnChanges {
3840
this.filterChips = []
3941
for (const columnName of Object.keys(this.filters)) {
4042
const colDef = this.columnDefs.find(({ field }) => field === columnName)
41-
42-
if (!colDef) return
43-
44-
const displayName = this.buildFilterDisplayName(colDef, this.filters[columnName])
45-
const chip = { field: colDef.field, displayName, original: columnName }
46-
this.filterChips.push(chip)
43+
if (colDef) {
44+
this.filterChips.push({
45+
field: colDef.field,
46+
displayName: this.buildFilterDisplayName(colDef, this.filters[columnName]),
47+
original: columnName,
48+
})
49+
} else {
50+
const column = this.columns.find((c) => c.name === columnName)
51+
this.filterChips.push({
52+
field: columnName,
53+
displayName: column?.display_name ?? columnName,
54+
original: columnName,
55+
})
56+
}
4757
}
4858
}
4959

@@ -53,11 +63,21 @@ export class FilterSortChipsComponent implements OnChanges {
5363
const direction = sort.startsWith('-') ? 'desc' : 'asc'
5464
sort = sort.replace(/^-/, '')
5565
const colDef = this.columnDefs.find(({ field }) => field === sort)
56-
57-
if (!colDef) return
58-
59-
const chip = { field: colDef.field, displayName: `${colDef.headerName} ${direction}`, original: sort }
60-
this.sortChips.push(chip)
66+
if (colDef) {
67+
this.sortChips.push({
68+
field: colDef.field,
69+
displayName: `${colDef.headerName} ${direction}`,
70+
original: sort,
71+
})
72+
} else {
73+
const column = this.columns.find((c) => c.name === sort)
74+
const displayName = column?.display_name ?? sort
75+
this.sortChips.push({
76+
field: sort,
77+
displayName: `${displayName} ${direction}`,
78+
original: sort,
79+
})
80+
}
6181
}
6282
}
6383

src/app/modules/inventory-list/list/grid/grid-controls.component.html

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,49 @@
11
<div class="mx-2 flex justify-between">
22
<mat-icon class="scale-75 cursor-pointer" (click)="resetGrid()" svgIcon="fa-solid:arrows-rotate" matTooltip="Reset Grid"></mat-icon>
3+
4+
<!-- POTENTIAL RESET MENU FOR MORE CONTROL -->
5+
6+
<!-- <mat-icon
7+
class="scale-75 cursor-pointer"
8+
[matMenuTriggerFor]="menu"
9+
svgIcon="fa-solid:arrows-rotate"
10+
matTooltip="Reset Grid Menu"
11+
></mat-icon> -->
12+
13+
<!-- <mat-menu #menu="matMenu">
14+
<div class="px-0">
15+
@if (gridApi) {
16+
<div class="menu-option" [ngClass]="{ '': scheme === 'dark' }" (click)="resetGrid()">
17+
<mat-icon class="scale-50" svgIcon="fa-solid:rotate"></mat-icon>
18+
<span class="w-200">Reset Grid</span>
19+
</div>
20+
<mat-divider class="mx-3 my-1"></mat-divider>
21+
22+
<div class="menu-option"
23+
[ngClass]="{'': scheme === 'dark'}" (click)="resetColumns()">
24+
<div class="w-6"></div>
25+
<span>Reset Columns</span>
26+
</div>
27+
<div class="menu-option"
28+
[ngClass]="{'': scheme === 'dark'}" (click)="resetFilters()">
29+
<div class="w-6"></div>
30+
<span>Reset Filters</span>
31+
</div>
32+
<div class="menu-option"
33+
[ngClass]="{'': scheme === 'dark'}" (click)="resetSorts()">
34+
<div class="w-6"></div>
35+
<span>Reset Sorts</span>
36+
</div>
37+
<mat-divider class="mx-3 my-1"></mat-divider>
38+
}
39+
40+
<div class="menu-option" [ngClass]="{ '': scheme === 'dark' }" (click)="resetUserSettings()">
41+
<div class="w-6"></div>
42+
<span>Reset User Settings</span>
43+
</div>
44+
</div>
45+
</mat-menu> -->
46+
347
<div class="my-auto flex justify-end">
448
@if (selectedViewIds.length > 0) {
549
<span class="my-auto mr-10">({{ selectedViewIds.length }} selected)</span>

0 commit comments

Comments
 (0)