diff --git a/client/stillbox/src/app/app.component.html b/client/stillbox/src/app/app.component.html index cef7467..0f6f61c 100644 --- a/client/stillbox/src/app/app.component.html +++ b/client/stillbox/src/app/app.component.html @@ -8,7 +8,7 @@
@if (auth.loggedIn) { - } diff --git a/client/stillbox/src/app/app.routes.ts b/client/stillbox/src/app/app.routes.ts index e5f6f80..8cd8375 100644 --- a/client/stillbox/src/app/app.routes.ts +++ b/client/stillbox/src/app/app.routes.ts @@ -1,30 +1,68 @@ import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { LoginComponent } from './login/login.component'; -import { TalkgroupsComponent } from './talkgroups/talkgroups.component'; -import { TalkgroupRecordComponent } from './talkgroups/talkgroup-record/talkgroup-record.component'; -import { CallsComponent } from './calls/calls.component'; -import { IncidentsComponent } from './incidents/incidents.component'; -import { AlertsComponent } from './alerts/alerts.component'; -import { ImportComponent } from './talkgroups/import/import.component'; -import { ExportComponent } from './talkgroups/export/export.component'; import { AuthGuard } from './auth.guard'; export const routes: Routes = [ - { path: 'login', component: LoginComponent }, + { + path: 'login', + loadComponent: () => + import('./login/login.component').then((m) => m.LoginComponent), + }, { path: '', canActivateChild: [AuthGuard], children: [ - { path: '', component: HomeComponent, pathMatch: 'full' }, - { path: 'talkgroups', component: TalkgroupsComponent }, - { path: 'talkgroups/import', component: ImportComponent }, - { path: 'talkgroups/export', component: ExportComponent }, - { path: 'talkgroups/:sys/:tg', component: TalkgroupRecordComponent }, - { path: 'calls', component: CallsComponent }, - { path: 'incidents', component: IncidentsComponent }, - { path: 'alerts', component: AlertsComponent }, + { + path: '', + loadComponent: () => + import('./home/home.component').then((m) => m.HomeComponent), + pathMatch: 'full', + }, + { + path: 'talkgroups', + loadComponent: () => + import('./talkgroups/talkgroups.component').then( + (m) => m.TalkgroupsComponent, + ), + }, + { + path: 'talkgroups/import', + loadComponent: () => + import('./talkgroups/import/import.component').then( + (m) => m.ImportComponent, + ), + }, + { + path: 'talkgroups/export', + loadComponent: () => + import('./talkgroups/export/export.component').then( + (m) => m.ExportComponent, + ), + }, + { + path: 'talkgroups/:sys/:tg', + loadComponent: () => + import( + './talkgroups/talkgroup-record/talkgroup-record.component' + ).then((m) => m.TalkgroupRecordComponent), + }, + { + path: 'calls', + loadComponent: () => + import('./calls/calls.component').then((m) => m.CallsComponent), + }, + { + path: 'incidents', + loadComponent: () => + import('./incidents/incidents.component').then( + (m) => m.IncidentsComponent, + ), + }, + { + path: 'alerts', + loadComponent: () => + import('./alerts/alerts.component').then((m) => m.AlertsComponent), + }, ], }, ]; diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html index 95c8684..9676174 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html @@ -63,7 +63,7 @@
Tags - + @for (tag of tg.tags; track tag) { {{ tag }} @@ -74,10 +74,25 @@ } + + @for (tag of filteredTags(); track tag) { + {{ tag }} + } +
@@ -88,6 +103,6 @@ Rules:
- +
diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index c5de619..4b36a92 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -1,14 +1,33 @@ -import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { + Component, + computed, + inject, + model, + ChangeDetectionStrategy, + Signal, + ViewChild, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { debounceTime } from 'rxjs/operators'; import { Talkgroup, TalkgroupUpdate, IconMap, iconMapping, } from '../../talkgroup'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { TalkgroupService } from '../talkgroups.service'; import { AlertRuleBuilderComponent } from './alert-rule-builder/alert-rule-builder.component'; +import { + MatAutocomplete, + MatAutocompleteModule, + MatAutocompleteSelectedEvent, + MatAutocompleteActivatedEvent, +} from '@angular/material/autocomplete'; import { CommonModule } from '@angular/common'; import { catchError, of } from 'rxjs'; +import { shareReplay } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { ReactiveFormsModule, FormGroup, @@ -35,22 +54,49 @@ import { MatIconModule } from '@angular/material/icon'; MatCheckboxModule, MatChipsModule, MatIconModule, + MatAutocompleteModule, ], templateUrl: './talkgroup-record.component.html', styleUrl: './talkgroup-record.component.scss', + // changeDetection: ChangeDetectionStrategy.OnPush, }) export class TalkgroupRecordComponent { tg!: Talkgroup; iconMapping: IconMap = iconMapping; tgService: TalkgroupService = inject(TalkgroupService); - tagsControl = new FormControl([]); - - form!: FormGroup; + allTags = []; + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + readonly filteredTags = computed(() => { + const currentTag = this.tagInputSig()?.toLowerCase() ?? ''; + return currentTag + ? this.allTags.filter((tag) => tag.toLowerCase().includes(currentTag)) + : this.allTags.slice(); + }); + readonly _allTags: Observable; + form = new FormGroup({ + name: new FormControl(''), + alpha_tag: new FormControl(''), + tg_group: new FormControl(''), + frequency: new FormControl(0), + alert: new FormControl(false), + weight: new FormControl(0.0), + icon: new FormControl(''), + tagInput: new FormControl(''), + tagsControl: new FormControl([]), + }); + tagInputSig = toSignal( + this.form.get('tagInput')!.valueChanges.pipe(debounceTime(300)) ?? of(null), + {}, + ); + @ViewChild('auto') autocomp!: MatAutocomplete; + active: string | null = null; constructor( private route: ActivatedRoute, private router: Router, - ) {} + ) { + this._allTags = this.tgService.allTags().pipe(shareReplay()); + } removeTag(tag: string) { const idx = this.tg.tags.indexOf(tag); @@ -63,17 +109,52 @@ export class TalkgroupRecordComponent { return [...this.tg.tags]; } - addTag(event: MatChipInputEvent) { - const value = (event.value || '').trim(); - - if (value) { - this.tg.tags = [...this.tg.tags, value]; + addTagEv(event: MatChipInputEvent) { + if (this.active != null) { + // this is a hack + this.addTag(this.active); + this.active = null; + event.chipInput!.clear(); + return; } + const value = (event.value || '').trim(); + this.addTag(value); + event.chipInput!.clear(); } + activated(event: MatAutocompleteActivatedEvent) { + console.log('activated'); + this.active = event.option?.value; + } + + addTag(tag: string) { + const idx = this.tg.tags.indexOf(tag); + if (idx > -1) { + return; + } + + if (tag) { + this.tg.tags = [...this.tg.tags, tag]; + } + } + + selected(event: any) { + let ev = event as MatAutocompleteSelectedEvent; + this.addTag(ev.option.viewValue); + ev.option.deselect(); + this.form.controls['tagInput'].reset(); + } + + loadTags() { + this._allTags.subscribe((event) => { + this.allTags = event; + }); + } + ngOnInit() { + this.loadTags(); const sysId = this.route.snapshot.paramMap.get('sys'); const tgId = this.route.snapshot.paramMap.get('tg'); @@ -81,16 +162,15 @@ export class TalkgroupRecordComponent { .getTalkgroup(Number(sysId), Number(tgId)) .subscribe((data: Talkgroup) => { this.tg = data; - this.form = new FormGroup({ - name: new FormControl(this.tg.name), - alpha_tag: new FormControl(this.tg.alpha_tag), - tg_group: new FormControl(this.tg.tg_group), - frequency: new FormControl(this.tg.frequency), - alert: new FormControl(this.tg.alert), - weight: new FormControl(this.tg.weight), - icon: new FormControl(this.tg?.metadata?.icon ?? ''), - }); - this.tagsControl.setValue(this.tg?.tags ?? []); + this.form.controls['name'].setValue(this.tg.name); + this.form.controls['alpha_tag'].setValue(this.tg.alpha_tag); + this.form.controls['tg_group'].setValue(this.tg.tg_group); + this.form.controls['frequency'].setValue(this.tg.frequency); + this.form.controls['alert'].setValue(this.tg.alert); + this.form.controls['weight'].setValue(this.tg.weight); + this.form.controls['icon'].setValue(this.tg?.metadata?.icon ?? ''); + this.form.controls['tagInput'].setValue(''); + this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []); }); } @@ -118,11 +198,11 @@ export class TalkgroupRecordComponent { if (this.form.controls['weight'].dirty) { tgu.weight = Number(this.form.controls['weight'].value); } - if (this.tagsControl.dirty) { - tgu.tags = this.tagsControl.value; + if (this.form.controls['tagsControl'].dirty) { + tgu.tags = this.form.controls['tagsControl'].value; } if (this.form.controls['icon'].dirty) { - let iv: string = this.form.controls['icon'].value; + let iv: string = this.form.controls['icon'].value ?? ''; if (tgu.metadata == null) { tgu.metadata = {}; } diff --git a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html index 35bc970..c544166 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html @@ -1,5 +1,23 @@
+ + + +
+ + + + + + diff --git a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts index a59e17b..6f6a68c 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts @@ -2,8 +2,10 @@ import { Component, inject, Pipe, + Input, PipeTransform, output, + ViewChild, input, } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; @@ -14,13 +16,17 @@ import { RouterModule, RouterLink } from '@angular/router'; import { CommonModule } from '@angular/common'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; -import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, + MatPaginatorModule, + PageEvent, +} from '@angular/material/paginator'; import { PrefsService } from '../../prefs/prefs.service'; import { MatIconModule } from '@angular/material/icon'; -import { - MatChipSelectionChange, - MatChipsModule, -} from '@angular/material/chips'; +import { MatChipsModule } from '@angular/material/chips'; +import { SelectionModel } from '@angular/cdk/collections'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { Observable, Subscription } from 'rxjs'; @Pipe({ standalone: true, @@ -60,6 +66,7 @@ export class SanitizeHtmlPipe implements PipeTransform { IconifyPipe, MatTableModule, MatPaginatorModule, + MatCheckboxModule, MatChipsModule, ], templateUrl: './talkgroup-table.component.html', @@ -81,6 +88,7 @@ export class TalkgroupTableComponent { perPage: number = 25; count = 0; columns = [ + 'select', 'icon', 'sysID', 'sysName', @@ -92,14 +100,30 @@ export class TalkgroupTableComponent { 'learned', 'edit', ]; + selection = new SelectionModel(true, []); + private resetPageSub!: Subscription; + @Input() resetPage!: Observable; + @ViewChild('paginator') paginator!: MatPaginator; + suppress = false; constructor(private route: ActivatedRoute) {} setPage(p: PageEvent) { - this.switchPage.emit(p); + // don't needlessly request page 0 if we were asked merely to reset the state of the control + this.selection.clear(); + if (!this.suppress) { + this.switchPage.emit(p); + } } ngOnInit() { + this.resetPageSub = this.resetPage.subscribe(() => { + this.suppress = true; + this.paginator.firstPage(); + this.selection.clear(); + this.suppress = false; + }); + this.perPage = this.prefsService.last.tgsPerPage; this.talkgroups$.subscribe((event) => { if (event != null) { @@ -109,10 +133,26 @@ export class TalkgroupTableComponent { }); } + ngOnDestroy() { + this.resetPageSub.unsubscribe(); + } + searchChip(event: MouseEvent) { // not a fan of how this looks, but it works... this.changeFilter.emit( (event.target as Element).childNodes[0].textContent ?? '', ); } + + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.dataSource.data.length; + return numSelected === numRows; + } + + masterToggle() { + this.isAllSelected() + ? this.selection.clear() + : this.dataSource.data.forEach((row) => this.selection.select(row)); + } } diff --git a/client/stillbox/src/app/talkgroups/talkgroups.component.html b/client/stillbox/src/app/talkgroups/talkgroups.component.html index ac3654a..adbffc2 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroups.component.html @@ -1,16 +1,33 @@
Filter - + + +
- - + +
diff --git a/client/stillbox/src/app/talkgroups/talkgroups.component.scss b/client/stillbox/src/app/talkgroups/talkgroups.component.scss index be14df6..fb24c88 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.component.scss +++ b/client/stillbox/src/app/talkgroups/talkgroups.component.scss @@ -3,6 +3,10 @@ talkgroup-table { height: 100%; } +.filterBox { + width: 300px; +} + talkgroups { display: flex; flex-direction: column; @@ -10,6 +14,11 @@ talkgroups { height: 90%; } +.clearBtn { + border: 0; + background-color: transparent; +} + .tgTools { display: flex; } diff --git a/client/stillbox/src/app/talkgroups/talkgroups.component.ts b/client/stillbox/src/app/talkgroups/talkgroups.component.ts index aa8891a..f29bfc8 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.component.ts @@ -8,10 +8,12 @@ import { TalkgroupTableComponent } from './talkgroup-table/talkgroup-table.compo import { PageEvent } from '@angular/material/paginator'; import { MatToolbarModule } from '@angular/material/toolbar'; import { PrefsService } from '../prefs/prefs.service'; +import { Subject } from 'rxjs'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatInputModule, MatInput } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms'; +import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'talkgroups', @@ -27,6 +29,7 @@ import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms'; ReactiveFormsModule, FormsModule, MatProgressSpinnerModule, + MatIconModule, ], templateUrl: './talkgroups.component.html', styleUrl: './talkgroups.component.scss', @@ -43,6 +46,12 @@ export class TalkgroupsComponent { curPage = { pageIndex: 0, pageSize: this.perPage }; constructor(private route: ActivatedRoute) {} + pageReset: Subject = new Subject(); + + resetPage() { + this.pageReset.next(); + } + switchPage(p: PageEvent) { this.curPage = p; this.route.paramMap @@ -84,6 +93,8 @@ export class TalkgroupsComponent { this.filter.setValue(f); } filterChange() { + this.curPage.pageIndex = 0; + this.resetPage(); this.switchPage(this.curPage); } } diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index b38a887..c8b9c85 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -40,6 +40,10 @@ export class TalkgroupService { }); } + allTags(): Observable { + return this.http.get('/api/talkgroup/tags'); + } + exportTGs( type: string, sysID: number, diff --git a/client/stillbox/src/styles.scss b/client/stillbox/src/styles.scss index 926843a..98e15aa 100644 --- a/client/stillbox/src/styles.scss +++ b/client/stillbox/src/styles.scss @@ -167,7 +167,7 @@ button.ybtn { text-shadow: 2px 2px 2px rgb(156, 156, 156); } -button { +button.sbButton { border-radius: 3px; padding: 10px; border: none; @@ -199,3 +199,7 @@ body { .navItems { width: 100px; } + +input { + caret-color: var(--color-dark-fg) !important; +}