feat: update to angular 21

This commit is contained in:
Chrystian Huot 2025-12-21 14:21:02 -05:00
parent c9cc7f0a3c
commit 53a426bea9
72 changed files with 10647 additions and 9722 deletions

View file

@ -16,14 +16,15 @@
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "rdio-scanner", "prefix": "rdio-scanner",
"architect": { "targets": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "../server/webapp", "outputPath": "../server/webapp",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "polyfills": [
"polyfills": "src/polyfills.ts", "src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
@ -36,15 +37,16 @@
], ],
"scripts": [], "scripts": [],
"serviceWorker": true, "serviceWorker": true,
"ngswConfigPath": "ngsw-config.json" "ngswConfigPath": "ngsw-config.json",
"main": "src/main.ts"
}, },
"configurations": { "configurations": {
"production": { "production": {
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "1500kb", "maximumWarning": "2mb",
"maximumError": "1500kb" "maximumError": "2mb"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
@ -61,9 +63,7 @@
"outputHashing": "all" "outputHashing": "all"
}, },
"development": { "development": {
"buildOptimizer": false,
"optimization": false, "optimization": false,
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,
"namedChunks": true "namedChunks": true
@ -75,11 +75,11 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "rdio-scanner:build:production" "buildTarget": "rdio-scanner:build:production"
}, },
"development": { "development": {
"browserTarget": "rdio-scanner:build:development", "proxyConfig": "src/proxy.conf.js",
"proxyConfig": "src/proxy.conf.js" "buildTarget": "rdio-scanner:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@ -87,14 +87,16 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "rdio-scanner:build" "buildTarget": "rdio-scanner:build"
} }
}, },
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"main": "src/test.ts", "main": "src/test.ts",
"polyfills": "src/polyfills.ts", "polyfills": [
"src/polyfills.ts"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
@ -114,5 +116,31 @@
}, },
"cli": { "cli": {
"analytics": false "analytics": false
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
} }
} }

15356
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +1,34 @@
{ {
"author": "Chrystian Huot <chrystian.huot@saubeo.solutions>", "author": "Chrystian Huot <chrystian@huot.qc.ca>",
"dependencies": { "dependencies": {
"@angular/animations": "^15.0.4", "@angular/animations": "^21.0.6",
"@angular/cdk": "^15.0.3", "@angular/cdk": "^21.0.5",
"@angular/common": "^15.0.4", "@angular/common": "^21.0.6",
"@angular/compiler": "^15.0.4", "@angular/compiler": "^21.0.6",
"@angular/core": "^15.0.4", "@angular/core": "^21.0.6",
"@angular/forms": "^15.0.4", "@angular/forms": "^21.0.6",
"@angular/material": "^15.0.3", "@angular/material": "^21.0.5",
"@angular/platform-browser": "^15.0.4", "@angular/platform-browser": "^21.0.6",
"@angular/platform-browser-dynamic": "^15.0.4", "@angular/platform-browser-dynamic": "^21.0.6",
"@angular/router": "^15.0.4", "@angular/router": "^21.0.6",
"@angular/service-worker": "^15.0.4", "@angular/service-worker": "^21.0.6",
"rxjs": "~7.5.0", "material-icons": "^1.13.14",
"tslib": "^2.3.0", "rxjs": "~7.8.2",
"zone.js": "~0.11.4" "tslib": "^2.8.1",
"zone.js": "~0.16.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^15.0.4", "@angular-devkit/build-angular": "^21.0.4",
"@angular/cli": "^15.0.4", "@angular/build": "^21.0.4",
"@angular/compiler-cli": "^15.0.4", "@angular/cli": "^21.0.4",
"@types/node": "^18.11.2", "@angular/compiler-cli": "^21.0.6",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@types/node": "^25.0.3",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/eslint-plugin": "^8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"ajv-keywords": "^5.1.0", "ajv-keywords": "^5.1.0",
"eslint": "^8.25.0", "eslint": "^9.39.2",
"ts-node": "~10.9.1", "ts-node": "~10.9.2",
"typescript": "~4.8.4" "typescript": "~5.9.3"
}, },
"engines": { "engines": {
"node": ">=16.0" "node": ">=16.0"
@ -36,7 +38,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "ng build --base-href ./ --configuration production", "build": "ng build --base-href ./ --configuration production",
"start": "ng serve" "serve": "ng serve"
}, },
"version": "7.0.0" "version": "7.0.0"
} }

View file

@ -23,5 +23,6 @@ import { Component } from '@angular/core';
selector: 'app-root', selector: 'app-root',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
templateUrl: './app.component.html', templateUrl: './app.component.html',
standalone: false
}) })
export class AppComponent { } export class AppComponent { }

View file

@ -1,10 +1,10 @@
<ng-container *ngIf="!authenticated"> @if (!authenticated) {
<rdio-scanner-admin-login></rdio-scanner-admin-login> <rdio-scanner-admin-login></rdio-scanner-admin-login>
</ng-container> }
<ng-container *ngIf="authenticated"> @if (authenticated) {
<rdio-scanner-admin-todos></rdio-scanner-admin-todos> <rdio-scanner-admin-todos></rdio-scanner-admin-todos>
<mat-accordion displayMode="flat"> <mat-accordion displayMode="flat">
<mat-expansion-panel #configPanel (afterCollapse)="configComponent?.closeAll()"> <mat-expansion-panel #configPanel (afterCollapse)="configComponent.closeAll()">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
@ -22,7 +22,7 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-logs #logsComponent></rdio-scanner-admin-logs> <rdio-scanner-admin-logs #logsComponent></rdio-scanner-admin-logs>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel (afterCollapse)="toolsComponent?.closeAll()"> <mat-expansion-panel (afterCollapse)="toolsComponent.closeAll()">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon>build</mat-icon> <mat-icon>build</mat-icon>
@ -40,4 +40,4 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
</ng-container> }

View file

@ -25,6 +25,7 @@ import { AdminEvent, RdioScannerAdminService, Group, Tag } from './admin.service
selector: 'rdio-scanner-admin', selector: 'rdio-scanner-admin',
styleUrls: ['./admin.component.scss'], styleUrls: ['./admin.component.scss'],
templateUrl: './admin.component.html', templateUrl: './admin.component.html',
standalone: false
}) })
export class RdioScannerAdminComponent implements OnDestroy { export class RdioScannerAdminComponent implements OnDestroy {
authenticated = true; authenticated = true;

View file

@ -17,7 +17,7 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { HttpClientModule } from '@angular/common/http'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AppSharedModule } from '../../../shared/shared.module'; import { AppSharedModule } from '../../../shared/shared.module';
import { RdioScannerAdminComponent } from './admin.component'; import { RdioScannerAdminComponent } from './admin.component';
@ -45,8 +45,7 @@ import { RdioScannerAdminImportTalkgroupsComponent } from './tools/import-talkgr
import { RdioScannerAdminImportUnitsComponent } from './tools/import-units/import-units.component'; import { RdioScannerAdminImportUnitsComponent } from './tools/import-units/import-units.component';
import { RdioScannerAdminPasswordComponent } from './tools/password/password.component'; import { RdioScannerAdminPasswordComponent } from './tools/password/password.component';
@NgModule({ @NgModule({ declarations: [
declarations: [
RdioScannerAdminComponent, RdioScannerAdminComponent,
RdioScannerAdminConfigComponent, RdioScannerAdminConfigComponent,
RdioScannerAdminAccessComponent, RdioScannerAdminAccessComponent,
@ -71,9 +70,5 @@ import { RdioScannerAdminPasswordComponent } from './tools/password/password.com
RdioScannerAdminToolsComponent, RdioScannerAdminToolsComponent,
RdioScannerAdminUnitComponent, RdioScannerAdminUnitComponent,
], ],
entryComponents: [RdioScannerAdminSystemsSelectComponent], exports: [RdioScannerAdminComponent], imports: [AppSharedModule], providers: [RdioScannerAdminService, provideHttpClient(withInterceptorsFromDi())] })
exports: [RdioScannerAdminComponent],
imports: [AppSharedModule, HttpClientModule],
providers: [RdioScannerAdminService],
})
export class RdioScannerAdminModule { } export class RdioScannerAdminModule { }

View file

@ -4,15 +4,20 @@
internally on the client side and reused thereafter.</p> internally on the client side and reused thereafter.</p>
<button type="button" mat-button color="accent" (click)="add()">New access</button> <button type="button" mat-button color="accent" (click)="add()">New access</button>
</div> </div>
<p *ngIf="!accesses.length" class="mat-small text-center">No access is defined, your instance is open and accessible to @if (!accesses.length) {
<p class="mat-small text-center">No access is defined, your instance is open and accessible to
all</p> all</p>
}
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="accesses" (cdkDropListDropped)="drop($event)"> <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="accesses" (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let access of accesses; index as i" cdkDrag> @for (access of accesses; track access; let i = $index) {
<mat-expansion-panel cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
{{ access.value.ident || 'NewAccess' }} {{ access.value.ident || 'NewAccess' }}
<mat-icon *ngIf="access.invalid" color="warn">error</mat-icon> @if (access.invalid) {
<mat-icon color="warn">error</mat-icon>
}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-container [formGroup]="access"> <ng-container [formGroup]="access">
@ -26,12 +31,16 @@
<button type="button" mat-icon-button matSuffix (click)="hideCode = !hideCode"> <button type="button" mat-icon-button matSuffix (click)="hideCode = !hideCode">
<mat-icon>{{ hideCode ? 'visibility_off' : 'visibility' }}</mat-icon> <mat-icon>{{ hideCode ? 'visibility_off' : 'visibility' }}</mat-icon>
</button> </button>
<mat-error *ngIf="access.get('code')?.hasError('duplicate')"> @if (access.get('code')?.hasError('duplicate')) {
<mat-error>
Code is already defined Code is already defined
</mat-error> </mat-error>
<mat-error *ngIf="access.get('code')?.hasError('required')"> }
@if (access.get('code')?.hasError('required')) {
<mat-error>
Code is required Code is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -42,9 +51,11 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="ident" placeholder="Ident"> <input type="text" matInput formControlName="ident" placeholder="Ident">
<mat-error *ngIf="access.get('ident')?.hasError('required')"> @if (access.get('ident')?.hasError('required')) {
<mat-error>
Ident is required Ident is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -74,8 +85,12 @@
<span class="mat-body">Access</span><br> <span class="mat-body">Access</span><br>
<span class="mat-caption"> <span class="mat-caption">
The access code allows access to <u> The access code allows access to <u>
<ng-container *ngIf="access.value.systems === '*'">all</ng-container> @if (access.value.systems === '*') {
<ng-container *ngIf="access.value.systems !== '*'">some</ng-container> all
}
@if (access.value.systems !== '*') {
some
}
</u> systems and talkgroups. </u> systems and talkgroups.
</span> </span>
</p> </p>
@ -92,4 +107,5 @@
</div> </div>
</ng-container> </ng-container>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>

View file

@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
@Component({ @Component({
selector: 'rdio-scanner-admin-access', selector: 'rdio-scanner-admin-access',
templateUrl: './access.component.html', templateUrl: './access.component.html',
standalone: false
}) })
export class RdioScannerAdminAccessComponent { export class RdioScannerAdminAccessComponent {
@Input() form: FormArray | undefined; @Input() form: FormArray | undefined;

View file

@ -33,10 +33,10 @@
<button type="button" mat-icon-button matSuffix (click)="copy(key)"> <button type="button" mat-icon-button matSuffix (click)="copy(key)">
<mat-icon>content_copy</mat-icon> <mat-icon>content_copy</mat-icon>
</button> </button>
<mat-error *ngIf="apiKey.get('key')?.hasError('duplicate')"> <mat-error *ngIf="apiKey.get('key').hasError('duplicate')">
Key is already defined Key is already defined
</mat-error> </mat-error>
<mat-error *ngIf="apiKey.get('key')?.hasError('required')"> <mat-error *ngIf="apiKey.get('key').hasError('required')">
Key is required Key is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -49,7 +49,7 @@
</p> </p>
<mat-form-field floatLabel="never"> <mat-form-field floatLabel="never">
<input type="text" matInput formControlName="ident" placeholder="Ident"> <input type="text" matInput formControlName="ident" placeholder="Ident">
<mat-error *ngIf="apiKey.get('ident')?.hasError('required')"> <mat-error *ngIf="apiKey.get('ident').hasError('required')">
Ident is required Ident is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

View file

@ -3,14 +3,19 @@
authenticate with an API key to exchange audio files.</p> authenticate with an API key to exchange audio files.</p>
<button type="button" mat-button color="accent" (click)="add()">New API key</button> <button type="button" mat-button color="accent" (click)="add()">New API key</button>
</div> </div>
<p *ngIf="!apikeys?.length" class="mat-small text-center">No defined API keys</p> @if (!apikeys.length) {
<p class="mat-small text-center">No defined API keys</p>
}
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="apikeys" (cdkDropListDropped)="drop($event)"> <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="apikeys" (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let apikey of apikeys; index as i" cdkDrag> @for (apikey of apikeys; track apikey; let i = $index) {
<mat-expansion-panel cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
{{ apikey.value.ident || 'NewApikey' }} {{ apikey.value.ident || 'NewApikey' }}
<mat-icon *ngIf="apikey.invalid" color="warn">error</mat-icon> @if (apikey.invalid) {
<mat-icon color="warn">error</mat-icon>
}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-container [formGroup]="apikey"> <ng-container [formGroup]="apikey">
@ -33,12 +38,16 @@
<button type="button" mat-icon-button matSuffix (click)="copy(key)"> <button type="button" mat-icon-button matSuffix (click)="copy(key)">
<mat-icon>content_copy</mat-icon> <mat-icon>content_copy</mat-icon>
</button> </button>
<mat-error *ngIf="apikey.get('key')?.hasError('duplicate')"> @if (apikey.get('key')!.hasError('duplicate')) {
<mat-error>
Key is already defined Key is already defined
</mat-error> </mat-error>
<mat-error *ngIf="apikey.get('key')?.hasError('required')"> }
@if (apikey.get('key')!.hasError('required')) {
<mat-error>
Key is required Key is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -49,9 +58,11 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="ident" placeholder="Ident"> <input type="text" matInput formControlName="ident" placeholder="Ident">
<mat-error *ngIf="apikey.get('ident')?.hasError('required')"> @if (apikey.get('ident')!.hasError('required')) {
<mat-error>
Ident is required Ident is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -59,8 +70,12 @@
<span class="mat-body">Access</span><br> <span class="mat-body">Access</span><br>
<span class="mat-caption"> <span class="mat-caption">
This API key allows access to <u> This API key allows access to <u>
<ng-container *ngIf="apikey.value.systems === '*'">all</ng-container> @if (apikey.value.systems === '*') {
<ng-container *ngIf="apikey.value.systems !== '*'">some</ng-container> all
}
@if (apikey.value.systems !== '*') {
some
}
</u> systems and talkgroups. </u> systems and talkgroups.
</span> </span>
</p> </p>
@ -77,4 +92,5 @@
</div> </div>
</ng-container> </ng-container>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>

View file

@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
@Component({ @Component({
selector: 'rdio-scanner-admin-apikeys', selector: 'rdio-scanner-admin-apikeys',
templateUrl: './apikeys.component.html', templateUrl: './apikeys.component.html',
standalone: false
}) })
export class RdioScannerAdminApikeysComponent { export class RdioScannerAdminApikeysComponent {
@Input() form: FormArray | undefined; @Input() form: FormArray | undefined;

View file

@ -1,11 +1,11 @@
<form *ngIf="form" autocomplete="off" [formGroup]="form" (ngSubmit)="save()"> <form *ngIf="form as f" autocomplete="off" [formGroup]="f" (ngSubmit)="save()">
<mat-accordion displayMode="flat"> <mat-accordion displayMode="flat">
<mat-expansion-panel (afterCollapse)="accessComponent.closeAll()"> <mat-expansion-panel (afterCollapse)="accessComponent.closeAll()">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon>manage_accounts</mat-icon> <mat-icon>manage_accounts</mat-icon>
Access Access
<mat-icon *ngIf="form?.get('access')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('access')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-access #accessComponent [form]="access"></rdio-scanner-admin-access> <rdio-scanner-admin-access #accessComponent [form]="access"></rdio-scanner-admin-access>
@ -15,7 +15,7 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>vpn_key</mat-icon> <mat-icon>vpn_key</mat-icon>
ApiKey ApiKey
<mat-icon *ngIf="form?.get('apikey')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('apikey')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-apikeys #apikeyComponent [form]="apikeys"></rdio-scanner-admin-apikeys> <rdio-scanner-admin-apikeys #apikeyComponent [form]="apikeys"></rdio-scanner-admin-apikeys>
@ -25,7 +25,7 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>folder</mat-icon> <mat-icon>folder</mat-icon>
Dirwatch Dirwatch
<mat-icon *ngIf="form?.get('dirwatch')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('dirwatch')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-dirwatch #dirwatchComponent [form]="dirwatch"></rdio-scanner-admin-dirwatch> <rdio-scanner-admin-dirwatch #dirwatchComponent [form]="dirwatch"></rdio-scanner-admin-dirwatch>
@ -35,7 +35,7 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>share</mat-icon> <mat-icon>share</mat-icon>
Downstreams Downstreams
<mat-icon *ngIf="form?.get('downstreams')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('downstreams')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-downstreams #downstreamsComponent [form]="downstreams"></rdio-scanner-admin-downstreams> <rdio-scanner-admin-downstreams #downstreamsComponent [form]="downstreams"></rdio-scanner-admin-downstreams>
@ -45,7 +45,7 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>workspaces</mat-icon> <mat-icon>workspaces</mat-icon>
Groups Groups
<mat-icon *ngIf="form?.get('groups')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('groups')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-groups [form]="groups"></rdio-scanner-admin-groups> <rdio-scanner-admin-groups [form]="groups"></rdio-scanner-admin-groups>
@ -55,7 +55,7 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>tune</mat-icon> <mat-icon>tune</mat-icon>
Options Options
<mat-icon *ngIf="form?.get('options')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('options')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-options [form]="options"></rdio-scanner-admin-options> <rdio-scanner-admin-options [form]="options"></rdio-scanner-admin-options>
@ -65,7 +65,7 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>podcasts</mat-icon> <mat-icon>podcasts</mat-icon>
Systems Systems
<mat-icon *ngIf="form?.get('systems')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('systems')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-systems #systemsComponent [form]="systems"></rdio-scanner-admin-systems> <rdio-scanner-admin-systems #systemsComponent [form]="systems"></rdio-scanner-admin-systems>
@ -75,16 +75,16 @@
<mat-panel-title> <mat-panel-title>
<mat-icon>sell</mat-icon> <mat-icon>sell</mat-icon>
Tags Tags
<mat-icon *ngIf="form?.get('tags')?.invalid" color="warn">error</mat-icon> <mat-icon *ngIf="f.get('tags')?.invalid" color="warn">error</mat-icon>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<rdio-scanner-admin-tags [form]="tags"></rdio-scanner-admin-tags> <rdio-scanner-admin-tags [form]="tags"></rdio-scanner-admin-tags>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
<div class="row bottom"> <div class="row bottom">
<button type="button" mat-raised-button [disabled]="form.disabled || form.pristine" <button type="button" mat-raised-button [disabled]="f.disabled || f.pristine"
(click)="reset()">Reset</button> (click)="reset()">Reset</button>
<button type="submit" mat-raised-button color="primary" <button type="submit" mat-raised-button color="primary"
[disabled]="form.disabled || form.pristine || !form.valid">Save</button> [disabled]="f.disabled || f.pristine || !f.valid">Save</button>
</div> </div>
</form> </form>

View file

@ -21,6 +21,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
import { FormArray, FormControl, FormGroup } from '@angular/forms'; import { FormArray, FormControl, FormGroup } from '@angular/forms';
import { MatExpansionPanel } from '@angular/material/expansion'; import { MatExpansionPanel } from '@angular/material/expansion';
import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service'; import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -28,6 +30,7 @@ import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
selector: 'rdio-scanner-admin-config', selector: 'rdio-scanner-admin-config',
styleUrls: ['./config.component.scss'], styleUrls: ['./config.component.scss'],
templateUrl: './config.component.html', templateUrl: './config.component.html',
standalone: false
}) })
export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit { export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
docker = false; docker = false;
@ -68,7 +71,7 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
private config: Config | undefined; private config: Config | undefined;
private eventSubscription; private readonly destroy$ = new Subject<void>();
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined; @ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
@ -76,10 +79,9 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
private adminService: RdioScannerAdminService, private adminService: RdioScannerAdminService,
private ngChangeDetectorRef: ChangeDetectorRef, private ngChangeDetectorRef: ChangeDetectorRef,
) { ) {
this.eventSubscription = this.adminService.event.subscribe(async (event: AdminEvent) => { this.adminService.event.pipe(takeUntil(this.destroy$)).subscribe(async (event: AdminEvent) => {
if ('authenticated' in event && event.authenticated === true) { if ('authenticated' in event && event.authenticated === true) {
this.config = await this.adminService.getConfig(); this.config = await this.adminService.getConfig();
this.reset(); this.reset();
} }
@ -98,7 +100,8 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.eventSubscription.unsubscribe(); this.destroy$.next();
this.destroy$.complete();
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@ -116,12 +119,17 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
reset(config = this.config, options?: { dirty?: boolean }): void { reset(config = this.config, options?: { dirty?: boolean }): void {
this.form = this.adminService.newConfigForm(config); this.form = this.adminService.newConfigForm(config);
this.form.statusChanges.subscribe(() => { this.form.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.ngChangeDetectorRef.markForCheck(); this.ngChangeDetectorRef.markForCheck();
}); });
this.groups.valueChanges.subscribe(() => { const systemsArray = this.systems;
this.systems.controls.forEach((system) => { const groupsArray = this.groups;
const tagsArray = this.tags;
groupsArray?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
if (!systemsArray) { return; }
systemsArray.controls.forEach((system) => {
const talkgroups = system.get('talkgroups') as FormArray; const talkgroups = system.get('talkgroups') as FormArray;
talkgroups.controls.forEach((talkgroup) => { talkgroups.controls.forEach((talkgroup) => {
@ -136,8 +144,9 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
}); });
}); });
this.tags.valueChanges.subscribe(() => { tagsArray?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.systems.controls.forEach((system) => { if (!systemsArray) { return; }
systemsArray.controls.forEach((system) => {
const talkgroups = system.get('talkgroups') as FormArray; const talkgroups = system.get('talkgroups') as FormArray;
talkgroups.controls.forEach((talkgroup) => { talkgroups.controls.forEach((talkgroup) => {
@ -160,8 +169,10 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
} }
async save(): Promise<void> { async save(): Promise<void> {
this.form?.markAsPristine(); if (!this.form) { return; }
await this.adminService.saveConfig(this.form?.getRawValue()); this.form.markAsPristine();
await this.adminService.saveConfig(this.form.getRawValue());
} }
} }

View file

@ -4,12 +4,15 @@
</div> </div>
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="dirwatches" <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="dirwatches"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let dirwatch of dirwatches; index as i" cdkDrag> @for (dirwatch of dirwatches; track dirwatch; let i = $index) {
<mat-expansion-panel cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
{{ dirwatch.value.directory || 'NewDirwatch' }} {{ dirwatch.value.directory || 'NewDirwatch' }}
<mat-icon *ngIf="dirwatch.invalid" color="warn">error</mat-icon> @if (dirwatch.invalid) {
<mat-icon color="warn">error</mat-icon>
}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-container [formGroup]="dirwatch"> <ng-container [formGroup]="dirwatch">
@ -61,38 +64,45 @@
<span class="mat-body">Directory</span><br> <span class="mat-body">Directory</span><br>
<span class="mat-caption"> <span class="mat-caption">
Path of the Path of the
<ng-container [ngSwitch]="dirwatch.get('type')?.value"> @switch (dirwatch.get('type')?.value) {
<ng-container *ngSwitchCase="'dsdplus'"> @case ('dsdplus') {
<b>Record</b>, <b>1R-Record</b> or <b>VC-Record</b> <b>Record</b>, <b>1R-Record</b> or <b>VC-Record</b>
</ng-container> }
<ng-container *ngSwitchCase="'sdr-trunk'"> @case ('sdr-trunk') {
<b>Recordings</b> <b>Recordings</b>
</ng-container> }
<ng-container *ngSwitchCase="'trunk-recorder'"> @case ('trunk-recorder') {
<b>captureDir</b> <b>captureDir</b>
</ng-container> }
<ng-container *ngSwitchDefault> @default {
local local
</ng-container> }
</ng-container> }
directory to monitor for file ingestion. Note that dirwatch is not compatible with networked directory to monitor for file ingestion. Note that dirwatch is not compatible with networked
disk. disk.
</span> </span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="directory" placeholder="Directory"> <input type="text" matInput formControlName="directory" placeholder="Directory">
<mat-error *ngIf="dirwatch.get('directory')?.hasError('required')"> @if (dirwatch.get('directory')?.hasError('required')) {
<mat-error>
Directory is required Directory is required
</mat-error> </mat-error>
<mat-error *ngIf="dirwatch.get('directory')?.hasError('duplicate')"> }
@if (dirwatch.get('directory')?.hasError('duplicate')) {
<mat-error>
Directory is already defined Directory is already defined
</mat-error> </mat-error>
<mat-error *ngIf="dirwatch.get('directory')?.hasError('network')"> }
@if (dirwatch.get('directory')?.hasError('network')) {
<mat-error>
Incompatible network folder Incompatible network folder
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default','dsdplus','trunk-recorder'].includes(dirwatch.get('type')?.value)"> @if (['default','dsdplus','trunk-recorder'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">Extension</span><br> <span class="mat-body">Extension</span><br>
<span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3", <span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3",
@ -100,12 +110,16 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="extension" placeholder="Extension"> <input type="text" matInput formControlName="extension" placeholder="Extension">
<mat-error *ngIf="dirwatch.get('extension')?.hasError('invalid')"> @if (dirwatch.get('extension')?.hasError('invalid')) {
<mat-error>
Invalid extension Invalid extension
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)"> }
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">System</span><br> <span class="mat-body">System</span><br>
<span class="mat-caption">System to where the audio files should go.</span> <span class="mat-caption">System to where the audio files should go.</span>
@ -113,18 +127,24 @@
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select #sysId formControlName="systemId" placeholder="System"> <mat-select #sysId formControlName="systemId" placeholder="System">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<ng-container *ngFor="let system of systems"> @for (system of systems; track system) {
<mat-option *ngIf="system.value.id > 0" [value]="system.value.id"> @if (system.value.id > 0) {
<mat-option [value]="system.value.id">
{{ system.value.label }} {{ system.value.label }}
</mat-option> </mat-option>
</ng-container> }
}
</mat-select> </mat-select>
<mat-error *ngIf="dirwatch.get('systemId')?.hasError('required')"> @if (dirwatch.get('systemId')?.hasError('required')) {
<mat-error>
System is required System is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)"> }
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">Site</span><br> <span class="mat-body">Site</span><br>
<span class="mat-caption">Site to where the audio files should go.</span> <span class="mat-caption">Site to where the audio files should go.</span>
@ -132,15 +152,19 @@
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="siteId" placeholder="Site"> <mat-select formControlName="siteId" placeholder="Site">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<ng-container *ngFor="let site of sites[dirwatch.value.systemId] || []"> @for (site of sites[dirwatch.value.systemId] || []; track site) {
<mat-option *ngIf="site.value.id > 0" [value]="site.value.id"> @if (site.value.id > 0) {
<mat-option [value]="site.value.id">
{{ site.value.label }} {{ site.value.label }}
</mat-option> </mat-option>
</ng-container> }
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)"> }
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">Talkgroup</span><br> <span class="mat-body">Talkgroup</span><br>
<span class="mat-caption">Talkgroup to where the audio files should go.</span> <span class="mat-caption">Talkgroup to where the audio files should go.</span>
@ -148,18 +172,24 @@
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="talkgroupId" placeholder="Talkgroup"> <mat-select formControlName="talkgroupId" placeholder="Talkgroup">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<ng-container *ngFor="let talkgroup of talkgroups[dirwatch.value.systemId] || []"> @for (talkgroup of talkgroups[dirwatch.value.systemId] || []; track talkgroup) {
<mat-option *ngIf="talkgroup.value.id > 0" [value]="talkgroup.value.id"> @if (talkgroup.value.id > 0) {
<mat-option [value]="talkgroup.value.id">
{{ talkgroup.value.label }} {{ talkgroup.value.label }}
</mat-option> </mat-option>
</ng-container> }
}
</mat-select> </mat-select>
<mat-error *ngIf="dirwatch.get('talkgroupId')?.hasError('required')"> @if (dirwatch.get('talkgroupId')?.hasError('required')) {
<mat-error>
Talkgroup is required Talkgroup is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)"> }
@if (['default'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">Mask</span><br> <span class="mat-body">Mask</span><br>
<span class="mat-caption">Some metadata can be extracted from the file name of the audio file using <span class="mat-caption">Some metadata can be extracted from the file name of the audio file using
@ -199,24 +229,32 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="mask" placeholder="Mask"> <input type="text" matInput formControlName="mask" placeholder="Mask">
<mat-error *ngIf="dirwatch.get('mask')?.hasError('invalid')"> @if (dirwatch.get('mask')?.hasError('invalid')) {
<mat-error>
Invalid mask Invalid mask
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)"> }
@if (['default'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">Frequency</span><br> <span class="mat-body">Frequency</span><br>
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span> <span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" matInput formControlName="frequency" placeholder="Frequency"> <input type="number" matInput formControlName="frequency" placeholder="Frequency">
<mat-error *ngIf="dirwatch.get('frequency')?.errors"> @if (dirwatch.get('frequency')?.errors) {
<mat-error>
Invalid frequency Invalid frequency
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)"> }
@if (['default'].includes(dirwatch.get('type')?.value)) {
<div class="row">
<p> <p>
<span class="mat-body">Delay</span><br> <span class="mat-body">Delay</span><br>
<span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the <span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the
@ -225,11 +263,14 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" matInput formControlName="delay" min="2000" placeholder="Delay"> <input type="number" matInput formControlName="delay" min="2000" placeholder="Delay">
<mat-error *ngIf="dirwatch.get('delay')?.hasError('min')"> @if (dirwatch.get('delay')?.hasError('min')) {
<mat-error>
Delay value cannot be less than 2000 milliseconds. Delay value cannot be less than 2000 milliseconds.
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
}
<div class="row bottom"> <div class="row bottom">
<button type="button" mat-button color="warn" (click)="remove(i)"> <button type="button" mat-button color="warn" (click)="remove(i)">
Delete dirwatch Delete dirwatch
@ -237,4 +278,5 @@
</div> </div>
</ng-container> </ng-container>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>

View file

@ -26,6 +26,7 @@ import { RdioScannerAdminService } from '../../admin.service';
@Component({ @Component({
selector: 'rdio-scanner-admin-dirwatch', selector: 'rdio-scanner-admin-dirwatch',
templateUrl: './dirwatch.component.html', templateUrl: './dirwatch.component.html',
standalone: false
}) })
export class RdioScannerAdminDirwatchComponent implements OnChanges { export class RdioScannerAdminDirwatchComponent implements OnChanges {
@Input() form: FormArray | undefined; @Input() form: FormArray | undefined;

View file

@ -2,14 +2,19 @@
<p class="mat-body">Ingested audio calls can be sent downstream to other instances.</p> <p class="mat-body">Ingested audio calls can be sent downstream to other instances.</p>
<button type="button" mat-button color="accent" (click)="add()">New downstream</button> <button type="button" mat-button color="accent" (click)="add()">New downstream</button>
</div> </div>
<p *ngIf="!downstreams.length" class="mat-small text-center">No defined downstreams</p> @if (!downstreams.length) {
<p class="mat-small text-center">No defined downstreams</p>
}
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="downstreams" (cdkDropListDropped)="drop($event)"> <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="downstreams" (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let downstream of downstreams; index as i" cdkDrag> @for (downstream of downstreams; track downstream; let i = $index) {
<mat-expansion-panel cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
{{ downstream.value.url || 'NewDownstream' }} {{ downstream.value.url || 'NewDownstream' }}
<mat-icon *ngIf="downstream.invalid" color="warn">error</mat-icon> @if (downstream.invalid) {
<mat-icon color="warn">error</mat-icon>
}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-container [formGroup]="downstream"> <ng-container [formGroup]="downstream">
@ -29,12 +34,16 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input #key type="text" matInput formControlName="apikey" placeholder="API key"> <input #key type="text" matInput formControlName="apikey" placeholder="API key">
<mat-error *ngIf="downstream.get('apikey')?.hasError('duplicate')"> @if (downstream.get('apikey')?.hasError('duplicate')) {
<mat-error>
API key is already defined API key is already defined
</mat-error> </mat-error>
<mat-error *ngIf="downstream.get('apikey')?.hasError('required')"> }
@if (downstream.get('apikey')?.hasError('required')) {
<mat-error>
API key is required API key is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -44,15 +53,21 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="url" placeholder="URL"> <input type="text" matInput formControlName="url" placeholder="URL">
<mat-error *ngIf="downstream.get('url')?.hasError('required')"> @if (downstream.get('url')?.hasError('required')) {
<mat-error>
URL is required URL is required
</mat-error> </mat-error>
<mat-error *ngIf="downstream.get('url')?.hasError('invalid')"> }
@if (downstream.get('url')?.hasError('invalid')) {
<mat-error>
URL is invalid URL is invalid
</mat-error> </mat-error>
<mat-error *ngIf="downstream.get('url')?.hasError('duplicate')"> }
@if (downstream.get('url')?.hasError('duplicate')) {
<mat-error>
URL is already defined URL is already defined
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -60,8 +75,12 @@
<span class="mat-body">Access</span><br> <span class="mat-body">Access</span><br>
<span class="mat-caption"> <span class="mat-caption">
This downstream allows access to <u> This downstream allows access to <u>
<ng-container *ngIf="downstream.value.systems === '*'">all</ng-container> @if (downstream.value.systems === '*') {
<ng-container *ngIf="downstream.value.systems !== '*'">some</ng-container> all
}
@if (downstream.value.systems !== '*') {
some
}
</u> systems and talkgroups. </u> systems and talkgroups.
</span> </span>
</p> </p>
@ -78,4 +97,5 @@
</div> </div>
</ng-container> </ng-container>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>

View file

@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
@Component({ @Component({
selector: 'rdio-scanner-admin-downstreams', selector: 'rdio-scanner-admin-downstreams',
templateUrl: './downstreams.component.html', templateUrl: './downstreams.component.html',
standalone: false
}) })
export class RdioScannerAdminDownstreamsComponent { export class RdioScannerAdminDownstreamsComponent {
@Input() form: FormArray | undefined; @Input() form: FormArray | undefined;

View file

@ -3,10 +3,10 @@
active talkgroups.</p> active talkgroups.</p>
<button type="button" mat-button color="accent" (click)="add()">New Group</button> <button type="button" mat-button color="accent" (click)="add()">New Group</button>
</div> </div>
<p *ngIf="!groups?.length" class="mat-small text-center">No defined Groups</p> <p *ngIf="!groups.length" class="mat-small text-center">No defined Groups</p>
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="groups" <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="groups"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let group of groups; index as i" cdkDrag> <mat-expansion-panel *ngFor="let group of groups; let i = index" cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
@ -23,7 +23,7 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="label" placeholder="Label"> <input type="text" matInput formControlName="label" placeholder="Label">
<mat-error *ngIf="form?.get('label')?.hasError('required')"> <mat-error *ngIf="group.get('label')!.hasError('required')">
Label is required Label is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

View file

@ -18,35 +18,60 @@
*/ */
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, Input, QueryList, ViewChildren } from '@angular/core'; import { Component, Input, QueryList, ViewChildren, OnDestroy } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { FormArray, FormGroup } from '@angular/forms'; import { FormArray, FormGroup } from '@angular/forms';
import { MatExpansionPanel } from '@angular/material/expansion'; import { MatExpansionPanel } from '@angular/material/expansion';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { RdioScannerAdminService } from '../../admin.service'; import { RdioScannerAdminService } from '../../admin.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'rdio-scanner-admin-groups', selector: 'rdio-scanner-admin-groups',
templateUrl: './groups.component.html', templateUrl: './groups.component.html',
standalone: false
}) })
export class RdioScannerAdminGroupsComponent { export class RdioScannerAdminGroupsComponent implements OnDestroy {
@Input() form: FormArray | undefined; private _form: FormArray | undefined;
private formSub: Subscription | undefined;
private cachedGroups: FormGroup[] | undefined;
private cacheValid = false;
readonly leds: string[] = [];
readonly alertsArray: string[] = [];
leds: string[]; @Input()
set form(value: FormArray | undefined) {
this._form = value;
this.cacheValid = false;
this.cachedGroups = undefined;
this.formSub?.unsubscribe();
if (this._form) {
this.formSub = this._form.valueChanges.subscribe(() => {
this.cacheValid = false;
});
}
}
get form(): FormArray | undefined {
return this._form;
}
get alerts(): string[] { get alerts(): string[] {
return Object.keys(this.adminService.Alerts || {}); return this.alertsArray;
} }
get groups(): FormGroup[] { get groups(): FormGroup[] {
return this.form?.controls if (this.cacheValid && this.cachedGroups) return this.cachedGroups;
.sort((a, b) => a.value.order - b.value.order) as FormGroup[]; const controls = (this._form?.controls?.slice() || []) as FormGroup[];
this.cachedGroups = controls.sort((a, b) => (a.value.order || 0) - (b.value.order || 0));
this.cacheValid = true;
return this.cachedGroups;
} }
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined; @ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) { constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) {
this.leds = this.adminService.getLeds(); this.leds = this.adminService.getLeds();
this.alertsArray = Object.keys(this.adminService.Alerts || {});
} }
add(): void { add(): void {
@ -57,6 +82,7 @@ export class RdioScannerAdminGroupsComponent {
this.form?.insert(0, group); this.form?.insert(0, group);
this.form?.markAsDirty(); this.form?.markAsDirty();
this.cacheValid = false;
} }
closeAll(): void { closeAll(): void {
@ -70,6 +96,7 @@ export class RdioScannerAdminGroupsComponent {
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false })); event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
this.form?.markAsDirty(); this.form?.markAsDirty();
this.cacheValid = false;
} }
} }
@ -81,5 +108,10 @@ export class RdioScannerAdminGroupsComponent {
this.form?.removeAt(index); this.form?.removeAt(index);
this.form?.markAsDirty(); this.form?.markAsDirty();
this.cacheValid = false;
}
ngOnDestroy(): void {
this.formSub?.unsubscribe();
} }
} }

View file

@ -1,4 +1,5 @@
<ng-container *ngIf="form" [formGroup]="form"> <ng-container *ngIf="form">
<ng-container [formGroup]="form">
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">12-Hour Time Format</span><br> <span class="mat-body">12-Hour Time Format</span><br>
@ -25,7 +26,7 @@
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Auto Populate</span><br> <span class="mat-body">Auto Populate</span><br>
<span class="mat-caption">Globaly allows the automatic creation of unconfigured systems and <span class="mat-caption">Globally allows the automatic creation of unconfigured systems and
talkgroups.</span> talkgroups.</span>
</p> </p>
<div> <div>
@ -50,10 +51,10 @@
</p> </p>
<mat-form-field> <mat-form-field>
<input type="number" min="0" step="1" matInput formControlName="dimmerDelay"> <input type="number" min="0" step="1" matInput formControlName="dimmerDelay">
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('required')"> <mat-error *ngIf="form.get('dimmerDelay')?.hasError('required')">
Dimmer delay is required Dimmer delay is required
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('min')"> <mat-error *ngIf="form.get('dimmerDelay')?.hasError('min')">
Dimmer delay is invalid Dimmer delay is invalid
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -75,10 +76,10 @@
</p> </p>
<mat-form-field> <mat-form-field>
<input type="number" min="0" step="1" matInput formControlName="duplicateDetectionTimeFrame"> <input type="number" min="0" step="1" matInput formControlName="duplicateDetectionTimeFrame">
<mat-error *ngIf="form?.get('duplicateDetectionTimeFrame')?.hasError('required')"> <mat-error *ngIf="form.get('duplicateDetectionTimeFrame')?.hasError('required')">
Duplicate call time frame is required Duplicate call time frame is required
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('duplicateDetectionTimeFrame')?.hasError('min')"> <mat-error *ngIf="form.get('duplicateDetectionTimeFrame')?.hasError('min')">
Duplicate call time frame is invalid Duplicate call time frame is invalid
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -111,10 +112,10 @@
</p> </p>
<mat-form-field> <mat-form-field>
<input type="number" min="0" step="1" matInput formControlName="maxClients"> <input type="number" min="0" step="1" matInput formControlName="maxClients">
<mat-error *ngIf="form?.get('maxClients')?.hasError('required')"> <mat-error *ngIf="form.get('maxClients')?.hasError('required')">
Max clients is required Max clients is required
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('maxClients')?.hasError('min')"> <mat-error *ngIf="form.get('maxClients')?.hasError('min')">
Max clients is invalid Max clients is invalid
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -137,10 +138,10 @@
</p> </p>
<mat-form-field> <mat-form-field>
<input type="number" min="0" step="1" matInput formControlName="pruneDays"> <input type="number" min="0" step="1" matInput formControlName="pruneDays">
<mat-error *ngIf="form?.get('pruneDays')?.hasError('required')"> <mat-error *ngIf="form.get('pruneDays')?.hasError('required')">
Prune days is required Prune days is required
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('pruneDays')?.hasError('min')"> <mat-error *ngIf="form.get('pruneDays')?.hasError('min')">
Prune days is invalid Prune days is invalid
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -164,3 +165,4 @@
</div> </div>
</div> </div>
</ng-container> </ng-container>
</ng-container>

View file

@ -17,13 +17,19 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { Component, Input } from '@angular/core'; import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
@Component({ @Component({
selector: 'rdio-scanner-admin-options', selector: 'rdio-scanner-admin-options',
templateUrl: './options.component.html', templateUrl: './options.component.html',
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RdioScannerAdminOptionsComponent { export class RdioScannerAdminOptionsComponent {
@Input() form: FormGroup | undefined; @Input() form: FormGroup | null = null;
get controls() {
return this.form ? this.form.controls : {};
}
} }

View file

@ -7,25 +7,30 @@
</mat-checkbox> </mat-checkbox>
<h3 class="mat-caption">Groups</h3> <h3 class="mat-caption">Groups</h3>
<div class="flex-row wrap" formArrayName="groups"> <div class="flex-row wrap" formArrayName="groups">
<div *ngFor="let group of configGroups; index as groupIndex" [formGroupName]="groupIndex"> <ng-container *ngFor="let group of configGroups; let groupIndex = index">
<div [formGroupName]="groupIndex">
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked" <mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
[indeterminate]="indeterminate.groups[groupIndex]"> [indeterminate]="indeterminate.groups[groupIndex]">
{{ group.value.label }} {{ group.value.label }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</ng-container>
</div> </div>
<h3 class="mat-caption">Tags</h3> <h3 class="mat-caption">Tags</h3>
<div class="flex-row wrap" formArrayName="tags"> <div class="flex-row wrap" formArrayName="tags">
<div *ngFor="let tag of configTags; index as tagIndex" [formGroupName]="tagIndex"> <ng-container *ngFor="let tag of configTags; let tagIndex = index">
<div [formGroupName]="tagIndex">
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked" <mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
[indeterminate]="indeterminate.tags[tagIndex]"> [indeterminate]="indeterminate.tags[tagIndex]">
{{ tag.value.label }} {{ tag.value.label }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</ng-container>
</div> </div>
<h3 class="mat-caption">Systems</h3> <h3 class="mat-caption">Systems</h3>
<div formArrayName="systems"> <div formArrayName="systems">
<div *ngFor="let system of configSystems; index as systemIndex" [formGroupName]="systemIndex"> <ng-container *ngFor="let system of configSystems; let systemIndex = index">
<div [formGroupName]="systemIndex">
<div class="flex-row margin"> <div class="flex-row margin">
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all" <mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
[indeterminate]="indeterminate.systems[systemIndex]"> [indeterminate]="indeterminate.systems[systemIndex]">
@ -33,14 +38,16 @@
</mat-checkbox> </mat-checkbox>
</div> </div>
<div class="flex-row wrap indent margin" formArrayName="talkgroups"> <div class="flex-row wrap indent margin" formArrayName="talkgroups">
<div *ngFor="let talkgroup of configTalkgroups[systemIndex]; index as talkgroupIndex" <ng-container *ngFor="let talkgroup of configTalkgroups[systemIndex]; let talkgroupIndex = index">
[formGroupName]="talkgroupIndex"> <div [formGroupName]="talkgroupIndex">
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"> <mat-checkbox color="primary" [disableRipple]="true" formControlName="checked">
{{ talkgroup.value.label }} {{ talkgroup.value.label }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</ng-container>
</div> </div>
</div> </div>
</ng-container>
</div> </div>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>

View file

@ -17,10 +17,11 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { Component, Inject, ViewEncapsulation } from '@angular/core'; import { Component, Inject, ViewEncapsulation, OnDestroy } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Access } from '../../../admin.service'; import { Access } from '../../../admin.service';
import { Subscription } from 'rxjs';
interface System { interface System {
all: boolean; all: boolean;
@ -38,8 +39,9 @@ interface Talkgroup {
selector: 'rdio-scanner-admin-systems-selection', selector: 'rdio-scanner-admin-systems-selection',
styleUrls: ['./select.component.scss'], styleUrls: ['./select.component.scss'],
templateUrl: './select.component.html', templateUrl: './select.component.html',
standalone: false
}) })
export class RdioScannerAdminSystemsSelectComponent { export class RdioScannerAdminSystemsSelectComponent implements OnDestroy {
indeterminate = { indeterminate = {
everything: false, everything: false,
groups: [] as boolean[], groups: [] as boolean[],
@ -53,22 +55,21 @@ export class RdioScannerAdminSystemsSelectComponent {
get configGroups(): FormGroup[] { get configGroups(): FormGroup[] {
const faGroups = this.access.root.get('groups') as FormArray; const faGroups = this.access.root.get('groups') as FormArray;
return faGroups.controls as FormGroup[]; return faGroups.controls as FormGroup[];
} }
get configSystems(): FormGroup[] { get configSystems(): FormGroup[] {
const faSystems = this.access.root.get('systems') as FormArray; const faSystems = this.access.root.get('systems') as FormArray;
return faSystems.controls as FormGroup[]; return faSystems.controls as FormGroup[];
} }
get configTags(): FormGroup[] { get configTags(): FormGroup[] {
const faTags = this.access.root.get('tags') as FormArray; const faTags = this.access.root.get('tags') as FormArray;
return faTags.controls as FormGroup[]; return faTags.controls as FormGroup[];
} }
private subs = new Subscription();
constructor( constructor(
@Inject(MAT_DIALOG_DATA) public access: FormGroup, @Inject(MAT_DIALOG_DATA) public access: FormGroup,
private matDialogRef: MatDialogRef<RdioScannerAdminSystemsSelectComponent>, private matDialogRef: MatDialogRef<RdioScannerAdminSystemsSelectComponent>,
@ -76,7 +77,6 @@ export class RdioScannerAdminSystemsSelectComponent {
) { ) {
this.configTalkgroups = this.configSystems.map((fgSystem) => { this.configTalkgroups = this.configSystems.map((fgSystem) => {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
return faTalkgroups.controls as FormGroup[]; return faTalkgroups.controls as FormGroup[];
}); });
@ -92,179 +92,140 @@ export class RdioScannerAdminSystemsSelectComponent {
const faSystems = this.select.get('systems') as FormArray; const faSystems = this.select.get('systems') as FormArray;
const faTags = this.select.get('tags') as FormArray; const faTags = this.select.get('tags') as FormArray;
this.configGroups.forEach((configGroup) => { for (const configGroup of this.configGroups) {
const fgGroup = this.ngFormBuilder.group({ const fgGroup = this.ngFormBuilder.group({
id: this.ngFormBuilder.control(configGroup.get('id')?.value), id: this.ngFormBuilder.control(configGroup.get('id')?.value),
checked: this.ngFormBuilder.control(false), checked: this.ngFormBuilder.control(false),
}); });
faGroups.push(fgGroup); faGroups.push(fgGroup);
this.subs.add(fgGroup.valueChanges.subscribe((vGroup) => {
fgGroup.valueChanges.subscribe((vGroup) => { for (const fgSystem of faSystems.controls) {
faSystems.controls.forEach((fgSystem) => {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const fgTalkgroup of faTalkgroups.controls) {
faTalkgroups.controls.forEach((fgTalkgroup) => { const ids = fgTalkgroup.get('groupIds')?.value;
if (fgTalkgroup.get('groupIds')?.value.includes(vGroup.id) && fgTalkgroup.get('checked')?.value !== vGroup.checked) { if (ids && ids.includes(vGroup.id) && fgTalkgroup.get('checked')?.value !== vGroup.checked) {
fgTalkgroup.get('checked')?.setValue(vGroup.checked); fgTalkgroup.get('checked')?.setValue(vGroup.checked);
} }
}); }
}); }
}); }));
}); }
this.configSystems.forEach((configSystem, index) => { for (let index = 0; index < this.configSystems.length; index++) {
const configSystem = this.configSystems[index];
const fcSystemAll = this.ngFormBuilder.control(false); const fcSystemAll = this.ngFormBuilder.control(false);
const faSystemTalkgroups = this.ngFormBuilder.array<FormGroup>([]);
const faSystemTalkgroups = this.ngFormBuilder.array<FormGroup<{
checked: FormControl<boolean>;
groupIds: FormControl<number[]>;
id: FormControl<number>;
tagId: FormControl<number>;
}>>([]);
const fgSystem = this.ngFormBuilder.group({ const fgSystem = this.ngFormBuilder.group({
all: fcSystemAll, all: fcSystemAll,
id: this.ngFormBuilder.control(configSystem.get('systemRef')?.value), id: this.ngFormBuilder.control(configSystem.get('systemRef')?.value),
talkgroups: faSystemTalkgroups talkgroups: faSystemTalkgroups
}); });
this.configTalkgroups[index].forEach((configTalkgroup) => { for (const configTalkgroup of this.configTalkgroups[index]) {
const fgSystemTalkgroup = this.ngFormBuilder.group({ const fgSystemTalkgroup = this.ngFormBuilder.group({
checked: this.ngFormBuilder.nonNullable.control(false), checked: this.ngFormBuilder.nonNullable.control(false),
groupIds: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('groupIds')?.value), groupIds: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('groupIds')?.value),
id: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('talkgroupRef')?.value), id: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('talkgroupRef')?.value),
tagId: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('tagId')?.value), tagId: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('tagId')?.value),
}); });
faSystemTalkgroups.push(fgSystemTalkgroup); faSystemTalkgroups.push(fgSystemTalkgroup);
this.subs.add(fgSystemTalkgroup.valueChanges.subscribe(() => {
fgSystemTalkgroup.valueChanges.subscribe(() => { const vAll = faSystemTalkgroups.controls.every((t) => t.get('checked')?.value);
const vAll = faSystemTalkgroups.controls.every((systemTalkgroup) => systemTalkgroup.get('checked')?.value);
fcSystemAll.setValue(vAll, { emitEvent: false }); fcSystemAll.setValue(vAll, { emitEvent: false });
}); }));
}); }
faSystems.push(fgSystem); faSystems.push(fgSystem);
fgSystem.valueChanges.subscribe(() => { this.subs.add(fgSystem.valueChanges.subscribe(() => {
this.rebuildGroupIndeterminates(); this.rebuildGroupIndeterminates();
this.rebuildTagIndeterminates(); this.rebuildTagIndeterminates();
}); }));
fcSystemAll.valueChanges.subscribe((vAll) => { this.subs.add(fcSystemAll.valueChanges.subscribe((vAll) => {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const fgTalkgroup of faTalkgroups.controls) {
faTalkgroups.controls.forEach((fgTalkgroup) => fgTalkgroup.get('checked')?.setValue(vAll)); fgTalkgroup.get('checked')?.setValue(vAll);
}
this.rebuildGroupIndeterminates(); this.rebuildGroupIndeterminates();
this.rebuildTagIndeterminates(); this.rebuildTagIndeterminates();
}); }));
faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => { this.subs.add(faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => {
let off = 0;
let on = 0; let on = 0;
let off = 0;
vSystemTalkgroups.forEach((vSystemTalkgroup) => { for (const v of vSystemTalkgroups) {
if (vSystemTalkgroup.checked) { if (v.checked) on++; else off++;
on++;
} else {
off++;
} }
});
this.indeterminate.systems[index] = !!off && !!on; this.indeterminate.systems[index] = !!off && !!on;
faSystems.at(index).get('all')?.setValue(!off && on, { emitEvent: false }); faSystems.at(index).get('all')?.setValue(!off && on, { emitEvent: false });
}); }));
}); }
this.configTags.forEach((configTag) => { for (const configTag of this.configTags) {
const fgTag = this.ngFormBuilder.group({ const fgTag = this.ngFormBuilder.group({
id: this.ngFormBuilder.control(configTag.value.id), id: this.ngFormBuilder.control(configTag.value.id),
checked: this.ngFormBuilder.control(false), checked: this.ngFormBuilder.control(false),
}); });
faTags.push(fgTag); faTags.push(fgTag);
this.subs.add(fgTag.valueChanges.subscribe((vTag) => {
fgTag.valueChanges.subscribe((vTag) => { for (const fgSystem of faSystems.controls) {
faSystems.controls.forEach((fgSystem) => {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const fgTalkgroup of faTalkgroups.controls) {
faTalkgroups.controls.forEach((fgTalkgroup) => {
if (fgTalkgroup.value.tagId === vTag.id && fgTalkgroup.value.checked !== vTag.checked) { if (fgTalkgroup.value.tagId === vTag.id && fgTalkgroup.value.checked !== vTag.checked) {
fgTalkgroup.get('checked')?.setValue(vTag.checked); fgTalkgroup.get('checked')?.setValue(vTag.checked);
} }
});
});
});
});
fcAll.valueChanges.subscribe((vAll) => {
faSystems.controls.flatMap((fgSystem) => {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
return faTalkgroups.controls;
}).concat(faGroups.controls, faTags.controls).forEach((control) => {
control.get('checked')?.setValue(vAll);
});
});
faSystems.valueChanges.subscribe((vSystems) => {
let off = 0;
let on = 0;
vSystems.forEach((vSystem: System) => {
if (vSystem.all) {
on++;
} else {
off++;
} }
}); }
}));
}
this.subs.add(fcAll.valueChanges.subscribe((vAll) => {
for (const fgSystem of faSystems.controls) {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const fgTalkgroup of faTalkgroups.controls) {
fgTalkgroup.get('checked')?.setValue(vAll);
}
}
for (const fg of faGroups.controls) fg.get('checked')?.setValue(vAll);
for (const fg of faTags.controls) fg.get('checked')?.setValue(vAll);
}));
this.subs.add(faSystems.valueChanges.subscribe((vSystems: System[]) => {
let on = 0;
let off = 0;
for (const vSystem of vSystems) {
if (vSystem.all) on++; else off++;
}
this.indeterminate.everything = !!off && !!on; this.indeterminate.everything = !!off && !!on;
fcAll.setValue(!off && on, { emitEvent: false }); fcAll.setValue(!off && on, { emitEvent: false });
}); }));
const vAccess: Access = this.access.value; const vAccess: Access = this.access.value;
if (vAccess.systems === '*') { if (vAccess.systems === '*') {
this.select.get('all')?.setValue(true); this.select.get('all')?.setValue(true);
} else if (Array.isArray(vAccess.systems)) { } else if (Array.isArray(vAccess.systems)) {
vAccess.systems.forEach((vSystem) => { for (const vSystem of vAccess.systems) {
if (typeof vSystem === 'number') { if (typeof vSystem === 'number') {
faSystems.controls.find((fgSystem) => fgSystem.get('id')?.value === vSystem)?.get('all')?.setValue(true); faSystems.controls.find((fgSystem) => fgSystem.get('id')?.value === vSystem)?.get('all')?.setValue(true);
} else if (vSystem && typeof vSystem === 'object') {
} else if (vSystem !== null && typeof vSystem === 'object') { const fgSystem = faSystems.controls.find((fg) => fg.get('id')?.value === vSystem.id);
const fgSystem = faSystems.controls.find((fg) => {
return fg.get('id')?.value === vSystem.id;
});
if (fgSystem) { if (fgSystem) {
if (vSystem.talkgroups === '*') { if (vSystem.talkgroups === '*') {
fgSystem.get('all')?.setValue(true); fgSystem.get('all')?.setValue(true);
} else if (Array.isArray(vSystem.talkgroups)) { } else if (Array.isArray(vSystem.talkgroups)) {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const talkgroup of vSystem.talkgroups) {
vSystem.talkgroups.forEach((talkgroup: { id: number } | number) => {
const talkgroupId = typeof talkgroup === 'number' ? talkgroup : talkgroup.id; const talkgroupId = typeof talkgroup === 'number' ? talkgroup : talkgroup.id;
const fgTalkgroup = faTalkgroups.controls.find((fg) => fg.get('id')?.value === talkgroupId); const fgTalkgroup = faTalkgroups.controls.find((fg) => fg.get('id')?.value === talkgroupId);
fgTalkgroup?.get('checked')?.setValue(true); fgTalkgroup?.get('checked')?.setValue(true);
}); }
fgSystem.updateValueAndValidity();
fgSystem?.updateValueAndValidity(); }
} }
} }
} }
});
} }
} }
@ -273,11 +234,7 @@ export class RdioScannerAdminSystemsSelectComponent {
return system['all'] || system['talkgroups'].some((talkgroup: Talkgroup) => talkgroup.checked); return system['all'] || system['talkgroups'].some((talkgroup: Talkgroup) => talkgroup.checked);
}).map((system: System) => { }).map((system: System) => {
if (system['all']) { if (system['all']) {
return { return { id: system['id'], talkgroups: '*' };
id: system['id'],
talkgroups: '*',
};
} else { } else {
return { return {
id: system['id'], id: system['id'],
@ -297,61 +254,47 @@ export class RdioScannerAdminSystemsSelectComponent {
private rebuildGroupIndeterminates(): void { private rebuildGroupIndeterminates(): void {
const faGroups = this.select.get('groups') as FormArray; const faGroups = this.select.get('groups') as FormArray;
const faSystems = this.select.get('systems') as FormArray; const faSystems = this.select.get('systems') as FormArray;
faGroups.controls.forEach((fgGroup, index) => { for (let index = 0; index < faGroups.length; index++) {
let off = 0; const fgGroup = faGroups.at(index);
let on = 0; let on = 0;
let off = 0;
faSystems.controls.forEach((fgSystem) => { for (const fgSystem of faSystems.controls) {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const fgTalkgroup of faTalkgroups.controls) {
faTalkgroups.controls.forEach((fgTalkgroup) => {
if (fgTalkgroup.get('groupIds')?.value.includes(fgGroup.get('id')?.value)) { if (fgTalkgroup.get('groupIds')?.value.includes(fgGroup.get('id')?.value)) {
if (fgTalkgroup.get('checked')?.value) { if (fgTalkgroup.get('checked')?.value) on++; else off++;
on++; }
} else {
off++;
} }
} }
});
});
this.indeterminate.groups[index] = !!off && !!on; this.indeterminate.groups[index] = !!off && !!on;
fgGroup.get('checked')?.setValue(!off && on, { emitEvent: false }); fgGroup.get('checked')?.setValue(!off && on, { emitEvent: false });
}); }
} }
private rebuildTagIndeterminates(): void { private rebuildTagIndeterminates(): void {
const faTags = this.select.get('tags') as FormArray; const faTags = this.select.get('tags') as FormArray;
const faSystems = this.select.get('systems') as FormArray; const faSystems = this.select.get('systems') as FormArray;
faTags.controls.forEach((fgTag, index) => { for (let index = 0; index < faTags.length; index++) {
let off = 0; const fgTag = faTags.at(index);
let on = 0; let on = 0;
let off = 0;
faSystems.controls.forEach((fgSystem) => { for (const fgSystem of faSystems.controls) {
const faTalkgroups = fgSystem.get('talkgroups') as FormArray; const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
for (const fgTalkgroup of faTalkgroups.controls) {
faTalkgroups.controls.forEach((fgTalkgroup) => {
if (fgTalkgroup.value.tagId === fgTag.value.id) { if (fgTalkgroup.value.tagId === fgTag.value.id) {
if (fgTalkgroup.value.checked) { if (fgTalkgroup.value.checked) on++; else off++;
on++; }
} else {
off++;
} }
} }
});
});
this.indeterminate.tags[index] = !!off && !!on; this.indeterminate.tags[index] = !!off && !!on;
fgTag.get('checked')?.setValue(!off && on, { emitEvent: false }); fgTag.get('checked')?.setValue(!off && on, { emitEvent: false });
}); }
}
ngOnDestroy(): void {
this.subs.unsubscribe();
} }
} }

View file

@ -1,34 +1,40 @@
<ng-container *ngIf="form" [formGroup]="form"> <ng-container *ngIf="form as f" [formGroup]="f">
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Id</span><br> <span class="mat-body">Id</span><br>
<span class="mat-caption">Site identifier in decimal format.</span> <span class="mat-caption">Site identifier in decimal format.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" min="1" step="1" matInput formControlName="siteRef" placeholder="Id"> <ng-container *ngIf="f.get('siteRef') as siteRef">
<mat-error *ngIf="form?.get('siteRef')?.hasError('duplicate')"> <input type="number" min="1" step="1" matInput formControlName="siteRef" placeholder="Id" />
<mat-error *ngIf="siteRef.hasError('duplicate')">
Id is already defined Id is already defined
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('siteRef')?.hasError('min')"> <mat-error *ngIf="siteRef.hasError('min')">
Id is invalid Id is invalid
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('siteRef')?.hasError('required')"> <mat-error *ngIf="siteRef.hasError('required')">
Id is required Id is required
</mat-error> </mat-error>
</ng-container>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Label</span><br> <span class="mat-body">Label</span><br>
<span class="mat-caption">Site label.</span> <span class="mat-caption">Site label.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="label" placeholder="Label"> <ng-container *ngIf="f.get('label') as labelCtrl">
<mat-error *ngIf="form?.get('label')?.hasError('required')"> <input type="text" matInput formControlName="label" placeholder="Label" />
<mat-error *ngIf="labelCtrl.hasError('required')">
Label is required Label is required
</mat-error> </mat-error>
</ng-container>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row bottom"> <div class="row bottom">
<button type="button" mat-button color="warn" (click)="remove.emit()"> <button type="button" mat-button color="warn" (click)="remove.emit()">
Delete site Delete site

View file

@ -17,15 +17,36 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup, AbstractControl } from '@angular/forms';
@Component({ @Component({
selector: 'rdio-scanner-admin-site', selector: 'rdio-scanner-admin-site',
templateUrl: './site.component.html', templateUrl: './site.component.html',
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RdioScannerAdminSiteComponent { export class RdioScannerAdminSiteComponent {
@Input() form: FormGroup | undefined; private _form: FormGroup | undefined;
@Input()
set form(f: FormGroup | undefined) {
this._form = f;
this.cd.markForCheck();
}
get form(): FormGroup | undefined {
return this._form;
}
get siteRef(): AbstractControl | null | undefined {
return this._form?.get('siteRef');
}
get labelControl(): AbstractControl | null | undefined {
return this._form?.get('label');
}
@Output() remove = new EventEmitter<void>(); @Output() remove = new EventEmitter<void>();
constructor(private readonly cd: ChangeDetectorRef) {}
} }

View file

@ -68,7 +68,7 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="led" placeholder="Color"> <mat-select formControlName="led" placeholder="Color">
<mat-option [value]="''">Default</mat-option> <mat-option value="">Default</mat-option>
<mat-option *ngFor="let led of leds" [value]="led"> <mat-option *ngFor="let led of leds" [value]="led">
{{ led | titlecase }} {{ led | titlecase }}
</mat-option> </mat-option>
@ -82,8 +82,8 @@
has an effect if the auto populate option is enabled.</span> has an effect if the auto populate option is enabled.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<textarea type="text" matInput formControlName="blacklists" placeholder="Blacklists"></textarea> <textarea matInput formControlName="blacklists" placeholder="Blacklists"></textarea>
<mat-error *ngIf="form?.get('blacklists')?.hasError('invalid')"> <mat-error *ngIf="form.get('blacklists')!.hasError('invalid')">
Comma separated list of talkgroup Ids Comma separated list of talkgroup Ids
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -134,9 +134,9 @@
<p class="mat-caption">System sites, you can drag and drop sites to rearrange them.</p> <p class="mat-caption">System sites, you can drag and drop sites to rearrange them.</p>
<button type="button" mat-button color="accent" (click)="addSite()">New Site</button> <button type="button" mat-button color="accent" (click)="addSite()">New Site</button>
</div> </div>
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="sites" <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="sites"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let site of sites; index as i" [formGroup]="site" cdkDrag> <mat-expansion-panel *ngFor="let site of sites; let i = index; trackBy: trackById" [formGroup]="site" cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
@ -167,10 +167,9 @@
option is enabled.</p> option is enabled.</p>
<button type="button" mat-button color="accent" (click)="addTalkgroup()">New talkgroup</button> <button type="button" mat-button color="accent" (click)="addTalkgroup()">New talkgroup</button>
</div> </div>
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64"
[cdkDropListData]="talkgroups" (cdkDropListDropped)="drop($event)"> [cdkDropListData]="talkgroups" (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let talkgroup of talkgroups; index as i" [formGroup]="talkgroup" <mat-expansion-panel *ngFor="let talkgroup of talkgroups; let i = index; trackBy: trackById" [formGroup]="talkgroup" cdkDrag>
cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
@ -202,9 +201,9 @@
screen.<br>You can drag and drop them to rearrange</p> screen.<br>You can drag and drop them to rearrange</p>
<button type="button" mat-button color="accent" (click)="addUnit()">New unit</button> <button type="button" mat-button color="accent" (click)="addUnit()">New unit</button>
</div> </div>
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="units" <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="units"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let unit of units; index as i" [formGroup]="unit" cdkDrag> <mat-expansion-panel *ngFor="let unit of units; let i = index; trackBy: trackById" [formGroup]="unit" cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>

View file

@ -27,6 +27,7 @@ import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
@Component({ @Component({
selector: 'rdio-scanner-admin-system', selector: 'rdio-scanner-admin-system',
templateUrl: './system.component.html', templateUrl: './system.component.html',
standalone: false
}) })
export class RdioScannerAdminSystemComponent { export class RdioScannerAdminSystemComponent {
@Input() form = new FormGroup({}); @Input() form = new FormGroup({});
@ -47,23 +48,22 @@ export class RdioScannerAdminSystemComponent {
get sites(): FormGroup[] { get sites(): FormGroup[] {
const sites = this.form.get('sites') as FormArray | null; const sites = this.form.get('sites') as FormArray | null;
return (sites?.controls as FormGroup[]) || [];
return sites?.controls
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
} }
get talkgroups(): FormGroup[] { get talkgroups(): FormGroup[] {
const talkgroups = this.form.get('talkgroups') as FormArray | null; const talkgroups = this.form.get('talkgroups') as FormArray | null;
return (talkgroups?.controls as FormGroup[]) || [];
return talkgroups?.controls
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
} }
get units(): FormGroup[] { get units(): FormGroup[] {
const units = this.form.get('units') as FormArray | null; const units = this.form.get('units') as FormArray | null;
return (units?.controls as FormGroup[]) || [];
}
return units?.controls // trackBy to preserve identity in ngFor lists
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[]; trackById(_index: number, group: FormGroup): any {
return group.get('id')?.value ?? _index;
} }
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined; @ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;

View file

@ -4,8 +4,8 @@
systems to rearrange their order.</p> systems to rearrange their order.</p>
<button type="button" mat-button color="accent" (click)="add()">New system</button> <button type="button" mat-button color="accent" (click)="add()">New system</button>
</div> </div>
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)"> <mat-accordion *ngIf="systems" displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let system of systems; index as i" [formGroup]="system" <mat-expansion-panel *ngFor="let system of systems; let i = index" [formGroup]="system"
(afterCollapse)="systemComponent.closeAll()" cdkDrag> (afterCollapse)="systemComponent.closeAll()" cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>

View file

@ -18,26 +18,44 @@
*/ */
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, Input, QueryList, ViewChildren } from '@angular/core'; import { Component, Input, QueryList, ViewChildren, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { FormArray, FormGroup } from '@angular/forms'; import { FormArray, FormGroup } from '@angular/forms';
import { MatExpansionPanel } from '@angular/material/expansion'; import { MatExpansionPanel } from '@angular/material/expansion';
import { RdioScannerAdminService } from '../../admin.service'; import { RdioScannerAdminService } from '../../admin.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'rdio-scanner-admin-systems', selector: 'rdio-scanner-admin-systems',
templateUrl: './systems.component.html', templateUrl: './systems.component.html',
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RdioScannerAdminSystemsComponent { export class RdioScannerAdminSystemsComponent implements OnDestroy {
@Input() form: FormArray | undefined; private _form: FormArray | undefined;
private subs = new Subscription();
private _systemsCache: FormGroup[] = [];
@Input()
set form(f: FormArray | undefined) {
if (this._form === f) { return; }
this.subs.unsubscribe();
this.subs = new Subscription();
this._form = f;
this.updateCache();
if (this._form) {
const s = this._form.valueChanges.subscribe(() => this.updateCache());
this.subs.add(s);
}
}
get form(): FormArray | undefined { return this._form; }
get systems(): FormGroup[] { get systems(): FormGroup[] {
return this.form?.controls return this._systemsCache;
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
} }
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined; @ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
constructor(private adminService: RdioScannerAdminService) { } constructor(private adminService: RdioScannerAdminService, private cd: ChangeDetectorRef) { }
add(): void { add(): void {
const system = this.adminService.newSystemForm(); const system = this.adminService.newSystemForm();
@ -47,6 +65,8 @@ export class RdioScannerAdminSystemsComponent {
this.form?.insert(0, system); this.form?.insert(0, system);
this.form?.markAsDirty(); this.form?.markAsDirty();
this.updateCache();
} }
closeAll(): void { closeAll(): void {
@ -60,6 +80,8 @@ export class RdioScannerAdminSystemsComponent {
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false })); event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
this.form?.markAsDirty(); this.form?.markAsDirty();
this.updateCache();
} }
} }
@ -67,5 +89,23 @@ export class RdioScannerAdminSystemsComponent {
this.form?.removeAt(index); this.form?.removeAt(index);
this.form?.markAsDirty(); this.form?.markAsDirty();
this.updateCache();
}
private updateCache(): void {
if (!this._form) {
this._systemsCache = [];
this.cd.markForCheck();
return;
}
const arr = [...this._form.controls] as FormGroup[];
arr.sort((a, b) => (a.value.order || 0) - (b.value.order || 0));
this._systemsCache = arr;
this.cd.markForCheck();
}
ngOnDestroy(): void {
this.subs.unsubscribe();
} }
} }

View file

@ -1,4 +1,5 @@
<ng-container *ngIf="form" [formGroup]="form"> <ng-container *ngIf="form">
<ng-container [formGroup]="form">
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Id</span><br> <span class="mat-body">Id</span><br>
@ -6,13 +7,13 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" min="1" step="1" matInput formControlName="talkgroupRef" placeholder="Id"> <input type="number" min="1" step="1" matInput formControlName="talkgroupRef" placeholder="Id">
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('duplicate')"> <mat-error *ngIf="form.get('talkgroupRef')!.hasError('duplicate')">
Id is already defined Id is already defined
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('min')"> <mat-error *ngIf="form.get('talkgroupRef')!.hasError('min')">
Id is invalid Id is invalid
</mat-error> </mat-error>
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('required')"> <mat-error *ngIf="form.get('talkgroupRef')!.hasError('required')">
Id is required Id is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -24,7 +25,7 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="label" placeholder="Label"> <input type="text" matInput formControlName="label" placeholder="Label">
<mat-error *ngIf="form?.get('label')?.hasError('required')"> <mat-error *ngIf="form.get('label')!.hasError('required')">
Label is required Label is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -36,7 +37,7 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="name" placeholder="Name"> <input type="text" matInput formControlName="name" placeholder="Name">
<mat-error *ngIf="form?.get('name')?.hasError('required')"> <mat-error *ngIf="form.get('name')!.hasError('required')">
Name is required Name is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -44,7 +45,7 @@
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Groups</span><br> <span class="mat-body">Groups</span><br>
<span class="mat-caption">Groups to which this talkgoups belongs.</span> <span class="mat-caption">Groups to which this talkgroups belongs.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="groupIds" placeholder="Groups" [multiple]="true"> <mat-select formControlName="groupIds" placeholder="Groups" [multiple]="true">
@ -52,7 +53,7 @@
{{ group.label }} {{ group.label }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="form?.get('groupId')?.hasError('required')"> <mat-error *ngIf="form.get('groupIds')?.hasError('required')">
Group is required Group is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -60,7 +61,7 @@
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Tag</span><br> <span class="mat-body">Tag</span><br>
<span class="mat-caption">Tag to which this talkgoups belongs.</span> <span class="mat-caption">Tag to which this talkgroups belongs.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="tagId" placeholder="Tag"> <mat-select formControlName="tagId" placeholder="Tag">
@ -68,7 +69,7 @@
{{ tag.label }} {{ tag.label }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="form?.get('tagId')?.hasError('required')"> <mat-error *ngIf="form.get('tagId')!.hasError('required')">
Tag is required Tag is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -80,7 +81,7 @@
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span> assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)"> <mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event.value)">
<mat-option value="">No Alert</mat-option> <mat-option value="">No Alert</mat-option>
<mat-option *ngFor="let alert of alerts" [value]="alert"> <mat-option *ngFor="let alert of alerts" [value]="alert">
{{ alert | titlecase }} {{ alert | titlecase }}
@ -104,7 +105,7 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" min="0" matInput formControlName="frequency" placeholder="Frequency"> <input type="number" min="0" matInput formControlName="frequency" placeholder="Frequency">
<mat-error *ngIf="form?.get('frequency')?.errors"> <mat-error *ngIf="form.get('frequency')?.errors">
Invalid frequency Invalid frequency
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -117,7 +118,7 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="led" placeholder="Color"> <mat-select formControlName="led" placeholder="Color">
<mat-option [value]="''">Default</mat-option> <mat-option value="">Default</mat-option>
<mat-option *ngFor="let led of leds" [value]="led"> <mat-option *ngFor="let led of leds" [value]="led">
{{ led | titlecase }} {{ led | titlecase }}
</mat-option> </mat-option>
@ -166,3 +167,4 @@
</button> </button>
</div> </div>
</ng-container> </ng-container>
</ng-container>

View file

@ -19,12 +19,12 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { MatSelectChange } from '@angular/material/select';
import { RdioScannerAdminService, Group, Tag } from '../../../admin.service'; import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
@Component({ @Component({
selector: 'rdio-scanner-admin-talkgroup', selector: 'rdio-scanner-admin-talkgroup',
templateUrl: './talkgroup.component.html', templateUrl: './talkgroup.component.html',
standalone: false
}) })
export class RdioScannerAdminTalkgroupComponent { export class RdioScannerAdminTalkgroupComponent {
@Input() form: FormGroup | undefined; @Input() form: FormGroup | undefined;
@ -33,25 +33,25 @@ export class RdioScannerAdminTalkgroupComponent {
@Output() remove = new EventEmitter<void>(); @Output() remove = new EventEmitter<void>();
leds: string[]; leds: string[] = [];
get alerts(): string[] { get alerts(): string[] {
return Object.keys(this.adminService.Alerts || {}); return Object.keys(this.adminService.Alerts || {});
} }
get groups(): Group[] { get groups(): Group[] {
return this.form?.root.get('groups')?.value as Group[]; return (this.form?.root.get('groups')?.value as Group[]) || [];
} }
get tags(): Tag[] { get tags(): Tag[] {
return this.form?.root.get('tags')?.value as Tag[]; return (this.form?.root.get('tags')?.value as Tag[]) || [];
} }
constructor(private adminService: RdioScannerAdminService) { constructor(private adminService: RdioScannerAdminService) {
this.leds = this.adminService.getLeds(); this.leds = this.adminService.getLeds() || [];
} }
async playAlert(event: MatSelectChange): Promise<void> { async playAlert(value: string | null): Promise<void> {
if (event.value) await this.adminService.playAlert(event.value); if (value) await this.adminService.playAlert(value);
} }
} }

View file

@ -1,4 +1,5 @@
<ng-container *ngIf="form" [formGroup]="form"> <ng-container *ngIf="form as f">
<ng-container [formGroup]="f">
<div class="row"> <div class="row">
<p> <p>
<span class="mat-body">Id</span><br> <span class="mat-body">Id</span><br>
@ -7,15 +8,9 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" min="1" step="1" matInput formControlName="unitRef" placeholder="Id"> <input type="number" min="1" step="1" matInput formControlName="unitRef" placeholder="Id">
<mat-error *ngIf="form?.get('unitRef')?.hasError('duplicate')"> <mat-error *ngIf="f.get('unitRef')!.hasError('duplicate')">Id value is already defined</mat-error>
Id value is already defined <mat-error *ngIf="f.get('unitRef')!.hasError('min')">Id value is invalid</mat-error>
</mat-error> <mat-error *ngIf="f.get('unitRef')!.hasError('required')">Id value is required</mat-error>
<mat-error *ngIf="form?.get('unitRef')?.hasError('min')">
Id value is invalid
</mat-error>
<mat-error *ngIf="form?.get('unitRef')?.hasError('required')">
Id value is required
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -25,12 +20,8 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" min="1" step="1" matInput formControlName="unitFrom" placeholder="From"> <input type="number" min="1" step="1" matInput formControlName="unitFrom" placeholder="From">
<mat-error *ngIf="form?.get('unitFrom')?.hasError('range')"> <mat-error *ngIf="f.get('unitFrom')!.hasError('range')">From value should be lower than To value</mat-error>
From value should be lower than To value <mat-error *ngIf="f.get('unitFrom')!.hasError('min')">From value is invalid</mat-error>
</mat-error>
<mat-error *ngIf="form?.get('unitFrom')?.hasError('min')">
From value To is invalid
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -40,12 +31,8 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="number" min="1" step="1" matInput formControlName="unitTo" placeholder="To"> <input type="number" min="1" step="1" matInput formControlName="unitTo" placeholder="To">
<mat-error *ngIf="form?.get('unitTo')?.hasError('range')"> <mat-error *ngIf="f.get('unitTo')!.hasError('range')">To value should be higher than From value</mat-error>
To value should be higher than From value <mat-error *ngIf="f.get('unitTo')!.hasError('min')">To value is invalid</mat-error>
</mat-error>
<mat-error *ngIf="form?.get('unitTo')?.hasError('min')">
To value is invalid
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -55,14 +42,11 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="label" placeholder="Label"> <input type="text" matInput formControlName="label" placeholder="Label">
<mat-error *ngIf="form?.get('label')?.hasError('required')"> <mat-error *ngIf="f.get('label')!.hasError('required')">Label is required</mat-error>
Label is required
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row bottom"> <div class="row bottom">
<button type="button" mat-button color="warn" (click)="remove.emit()"> <button type="button" mat-button color="warn" (click)="remove.emit()">Delete unit</button>
Delete unit
</button>
</div> </div>
</ng-container> </ng-container>
</ng-container>

View file

@ -17,15 +17,17 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
@Component({ @Component({
selector: 'rdio-scanner-admin-unit', selector: 'rdio-scanner-admin-unit',
templateUrl: './unit.component.html', templateUrl: './unit.component.html',
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RdioScannerAdminUnitComponent { export class RdioScannerAdminUnitComponent {
@Input() form: FormGroup | undefined; @Input() form: FormGroup | null = null;
@Output() remove = new EventEmitter<void>(); @Output() readonly remove = new EventEmitter<void>();
} }

View file

@ -3,15 +3,20 @@
based on their tag.</p> based on their tag.</p>
<button type="button" mat-button color="accent" (click)="add()">New Tag</button> <button type="button" mat-button color="accent" (click)="add()">New Tag</button>
</div> </div>
<p *ngIf="!tags?.length" class="mat-small text-center">No defined Tags</p> @if (!tags.length) {
<p class="mat-small text-center">No defined Tags</p>
}
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="tags" <mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="tags"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<mat-expansion-panel *ngFor="let tag of tags; index as i" cdkDrag> @for (tag of tags; track tag; let i = $index) {
<mat-expansion-panel cdkDrag>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
<mat-icon cdkDragHandle>drag_indicator</mat-icon> <mat-icon cdkDragHandle>drag_indicator</mat-icon>
{{ tag.value.label || 'NewTag' }} {{ tag.value.label || 'NewTag' }}
<mat-icon *ngIf="tag.invalid" color="warn">error</mat-icon> @if (tag.invalid) {
<mat-icon color="warn">error</mat-icon>
}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-container [formGroup]="tag"> <ng-container [formGroup]="tag">
@ -22,9 +27,11 @@
</p> </p>
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<input type="text" matInput formControlName="label" placeholder="Label"> <input type="text" matInput formControlName="label" placeholder="Label">
<mat-error *ngIf="form?.get('label')?.hasError('required')"> @if (tag.get('label')!.hasError('required')) {
<mat-error>
Label is required Label is required
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row"> <div class="row">
@ -36,9 +43,11 @@
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)"> <mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
<mat-option value="">No Alert</mat-option> <mat-option value="">No Alert</mat-option>
<mat-option *ngFor="let alert of alerts" [value]="alert"> @for (alert of alerts; track alert) {
<mat-option [value]="alert">
{{ alert | titlecase }} {{ alert | titlecase }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -51,9 +60,11 @@
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select formControlName="led" placeholder="Color"> <mat-select formControlName="led" placeholder="Color">
<mat-option [value]="''">Default</mat-option> <mat-option [value]="''">Default</mat-option>
<mat-option *ngFor="let led of leds" [value]="led"> @for (led of leds; track led) {
<mat-option [value]="led">
{{ led | titlecase }} {{ led | titlecase }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -64,4 +75,5 @@
</div> </div>
</ng-container> </ng-container>
</mat-expansion-panel> </mat-expansion-panel>
}
</mat-accordion> </mat-accordion>

View file

@ -28,6 +28,7 @@ import { RdioScannerAdminService } from '../../admin.service';
@Component({ @Component({
selector: 'rdio-scanner-admin-tags', selector: 'rdio-scanner-admin-tags',
templateUrl: './tags.component.html', templateUrl: './tags.component.html',
standalone: false
}) })
export class RdioScannerAdminTagsComponent { export class RdioScannerAdminTagsComponent {
@Input() form: FormArray | undefined; @Input() form: FormArray | undefined;

View file

@ -2,8 +2,17 @@
<p class="mat-body-2">Please enter the admin password to gain access to the administrative dashboard</p> <p class="mat-body-2">Please enter the admin password to gain access to the administrative dashboard</p>
<mat-form-field hideRequiredMarker> <mat-form-field hideRequiredMarker>
<mat-label>Password</mat-label> <mat-label>Password</mat-label>
<input matInput formControlName="password" type="password" required> <input
<mat-error>{{ message }}</mat-error> matInput
formControlName="password"
type="password"
autocomplete="current-password"
[attr.aria-invalid]="form.get('password')?.invalid"
required
>
<mat-error *ngIf="(form.get('password')?.invalid && form.get('password')?.touched) || message">
{{ message || 'Password is required' }}
</mat-error>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" type="submit" [disabled]="form.disabled || form.invalid">Login</button> <button mat-raised-button color="primary" type="submit" [disabled]="form.invalid || form.disabled">Login</button>
</form> </form>

View file

@ -25,6 +25,7 @@ import { RdioScannerAdminService } from '../admin.service';
selector: 'rdio-scanner-admin-login', selector: 'rdio-scanner-admin-login',
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
templateUrl: './login.component.html', templateUrl: './login.component.html',
standalone: false
}) })
export class RdioScannerAdminLoginComponent { export class RdioScannerAdminLoginComponent {
@Output() loggedIn = new EventEmitter<void>(); @Output() loggedIn = new EventEmitter<void>();

View file

@ -4,9 +4,15 @@
<span>Level</span> <span>Level</span>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let log"> <mat-cell *matCellDef="let log">
<mat-icon *ngIf="log?.level === 'error'" class="error">error</mat-icon> @if (log?.level === 'error') {
<mat-icon *ngIf="log?.level === 'info'" class="info">notifications</mat-icon> <mat-icon class="error">error</mat-icon>
<mat-icon *ngIf="log?.level === 'warn'" class="warn">warning</mat-icon> }
@if (log?.level === 'info') {
<mat-icon class="info">notifications</mat-icon>
}
@if (log?.level === 'warn') {
<mat-icon class="warn">warning</mat-icon>
}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="date"> <ng-container matColumnDef="date">

View file

@ -27,6 +27,7 @@ import { Log, LogsQuery, LogsQueryOptions, RdioScannerAdminService } from '../ad
selector: 'rdio-scanner-admin-logs', selector: 'rdio-scanner-admin-logs',
styleUrls: ['./logs.component.scss'], styleUrls: ['./logs.component.scss'],
templateUrl: './logs.component.html', templateUrl: './logs.component.html',
standalone: false
}) })
export class RdioScannerAdminLogsComponent { export class RdioScannerAdminLogsComponent {
form: FormGroup; form: FormGroup;

View file

@ -1,4 +1,5 @@
<mat-card appearance="outlined" *ngIf="todos.length"> <ng-container *ngIf="todos.length">
<mat-card appearance="outlined">
<mat-card-title>Todos</mat-card-title> <mat-card-title>Todos</mat-card-title>
<mat-list> <mat-list>
<mat-list-item *ngFor="let todo of todos"> <mat-list-item *ngFor="let todo of todos">
@ -10,3 +11,4 @@
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</mat-card> </mat-card>
</ng-container>

View file

@ -1,12 +1,12 @@
.info { .mat-mdc-list-item .info {
color: green; color: green;
} }
.warn { .mat-mdc-list-item .warn {
color: orange; color: orange;
} }
.todo { .mat-mdc-list-item .todo {
text-overflow: unset; text-overflow: unset;
white-space: normal; white-space: normal;
} }

View file

@ -29,6 +29,7 @@ interface Todo {
selector: 'rdio-scanner-admin-todos', selector: 'rdio-scanner-admin-todos',
styleUrls: ['./todos.component.scss'], styleUrls: ['./todos.component.scss'],
templateUrl: './todos.component.html', templateUrl: './todos.component.html',
standalone: false
}) })
export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit { export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit {
todos: Todo[] = []; todos: Todo[] = [];

View file

@ -17,8 +17,8 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { DOCUMENT } from '@angular/common';
import { Component, EventEmitter, Inject, Output } from '@angular/core'; import { Component, EventEmitter, Inject, Output, DOCUMENT } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Config, RdioScannerAdminService } from '../../admin.service'; import { Config, RdioScannerAdminService } from '../../admin.service';
import packageInfo from '../../../../../../../package.json'; import packageInfo from '../../../../../../../package.json';
@ -27,6 +27,7 @@ import packageInfo from '../../../../../../../package.json';
selector: 'rdio-scanner-admin-import-export-config', selector: 'rdio-scanner-admin-import-export-config',
styleUrls: ['./import-export-config.component.scss'], styleUrls: ['./import-export-config.component.scss'],
templateUrl: './import-export-config.component.html', templateUrl: './import-export-config.component.html',
standalone: false
}) })
export class RdioScannerAdminImportExportConfigComponent { export class RdioScannerAdminImportExportConfigComponent {
@Output() config = new EventEmitter<Config>(); @Output() config = new EventEmitter<Config>();

View file

@ -5,7 +5,7 @@
<section> <section>
<p class="mat-body">step 1: read the CSV file</p> <p class="mat-body">step 1: read the CSV file</p>
<p class="text-center"> <p class="text-center">
<ng-container *ngIf="csv.length === 0"> @if (csv.length === 0) {
<button mat-raised-button (click)="input.click()">Read the CSV file</button> <button mat-raised-button (click)="input.click()">Read the CSV file</button>
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)"> <input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
<br> <br>
@ -19,10 +19,10 @@
<span class="mat-body-1">Tag</span>, <span class="mat-body-1">Tag</span>,
<span class="mat-body-1">Group</span> <span class="mat-body-1">Group</span>
</p> </p>
</ng-container> }
<ng-container *ngIf="csv.length !== 0"> @if (csv.length !== 0) {
<button mat-raised-button (click)="csv=[]">Reset</button> <button mat-raised-button (click)="csv=[]">Reset</button>
</ng-container> }
</p> </p>
</section> </section>
@ -31,9 +31,11 @@
<p class="text-center"> <p class="text-center">
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select [(ngModel)]="system"> <mat-select [(ngModel)]="system">
<mat-option *ngFor="let system of baseConfig.systems" [value]="system"> @for (system of baseConfig.systems; track system) {
<mat-option [value]="system">
{{ system.label}} {{ system.label}}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</p> </p>
@ -95,7 +97,9 @@
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row> <mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
</mat-table> </mat-table>
</div> </div>
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p> @if (!csv.length) {
<p class="mat-body text-center">No CSV data yet</p>
}
</section> </section>
<section> <section>
@ -105,9 +109,11 @@
Import to configuration Import to configuration
</button> </button>
</p> </p>
<p *ngIf="csv.length > 800" class="mat-caption mat-error text-center text-uppercase"> @if (csv.length > 800) {
<p class="mat-caption mat-error text-center text-uppercase">
Warning, you are trying to import more than 800 talkgroups, {{csv.length}} in this case. The graphic interface Warning, you are trying to import more than 800 talkgroups, {{csv.length}} in this case. The graphic interface
has not designed for this number of talkgroups. You can continue, but expect Rdio Scanner to be quite slow, if has not designed for this number of talkgroups. You can continue, but expect Rdio Scanner to be quite slow, if
not unusable. not unusable.
</p> </p>
}
</section> </section>

View file

@ -24,6 +24,7 @@ import { Config, RdioScannerAdminService, System } from '../../admin.service';
selector: 'rdio-scanner-admin-import-talkgroups', selector: 'rdio-scanner-admin-import-talkgroups',
styleUrls: ['./import-talkgroups.component.scss'], styleUrls: ['./import-talkgroups.component.scss'],
templateUrl: './import-talkgroups.component.html', templateUrl: './import-talkgroups.component.html',
standalone: false
}) })
export class RdioScannerAdminImportTalkgroupsComponent implements OnInit { export class RdioScannerAdminImportTalkgroupsComponent implements OnInit {
@Output() config = new EventEmitter<Config>(); @Output() config = new EventEmitter<Config>();

View file

@ -1,7 +1,7 @@
<section> <section>
<p class="mat-body">step 1: read the CSV file</p> <p class="mat-body">step 1: read the CSV file</p>
<p class="text-center"> <p class="text-center">
<ng-container *ngIf="csv.length === 0"> @if (csv.length === 0) {
<button mat-raised-button (click)="input.click()">Read the CSV file</button> <button mat-raised-button (click)="input.click()">Read the CSV file</button>
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)"> <input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
<br> <br>
@ -10,10 +10,10 @@
<span>Id</span>, <span>Id</span>,
<span>Label</span>, <span>Label</span>,
</p> </p>
</ng-container> }
<ng-container *ngIf="csv.length !== 0"> @if (csv.length !== 0) {
<button mat-raised-button (click)="csv=[]">Reset</button> <button mat-raised-button (click)="csv=[]">Reset</button>
</ng-container> }
</p> </p>
</section> </section>
@ -22,9 +22,11 @@
<p class="text-center"> <p class="text-center">
<mat-form-field floatLabel="auto"> <mat-form-field floatLabel="auto">
<mat-select [(ngModel)]="system"> <mat-select [(ngModel)]="system">
<mat-option *ngFor="let system of baseConfig.systems" [value]="system"> @for (system of baseConfig.systems; track system) {
<mat-option [value]="system">
{{ system.label}} {{ system.label}}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</p> </p>
@ -62,7 +64,9 @@
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row> <mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
</mat-table> </mat-table>
</div> </div>
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p> @if (!csv.length) {
<p class="mat-body text-center">No CSV data yet</p>
}
</section> </section>
<section> <section>
@ -72,9 +76,11 @@
Import to configuration Import to configuration
</button> </button>
</p> </p>
<p *ngIf="csv.length > 1000" class="mat-caption mat-error text-center text-uppercase"> @if (csv.length > 1000) {
<p class="mat-caption mat-error text-center text-uppercase">
Warning, you are trying to import more than 1000 units, {{csv.length}} in this case. The graphic interface has Warning, you are trying to import more than 1000 units, {{csv.length}} in this case. The graphic interface has
not designed for this number of units. You can continue, but expect Rdio Scanner to be quite slow, if not not designed for this number of units. You can continue, but expect Rdio Scanner to be quite slow, if not
unusable. unusable.
</p> </p>
}
</section> </section>

View file

@ -24,6 +24,7 @@ import { Config, RdioScannerAdminService, System } from '../../admin.service';
selector: 'rdio-scanner-admin-import-units', selector: 'rdio-scanner-admin-import-units',
styleUrls: ['./import-units.component.scss'], styleUrls: ['./import-units.component.scss'],
templateUrl: './import-units.component.html', templateUrl: './import-units.component.html',
standalone: false
}) })
export class RdioScannerAdminImportUnitsComponent implements OnInit{ export class RdioScannerAdminImportUnitsComponent implements OnInit{
@Output() config = new EventEmitter<Config>(); @Output() config = new EventEmitter<Config>();

View file

@ -1,17 +1,20 @@
<p class="mat-body"> <p class="mat-body">
Change the administrator password which gives access to the server administration dashboard. Change the administrator password which gives access to the server administration dashboard.
</p> </p>
<form [formGroup]="form"> <form [formGroup]="form" (ngSubmit)="save()">
<mat-form-field>
<input matInput type="text" name="username" value="admin" autocomplete="username" tabindex="-1" aria-label="Username (for password manager)" readonly>
</mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Current password</mat-label> <mat-label>Current password</mat-label>
<input type="password" matInput formControlName="currentPassword" required> <input type="password" matInput formControlName="currentPassword" required autocomplete="current-password">
<mat-error *ngIf="form.get('currentPassword')?.hasError('required')"> <mat-error *ngIf="form.get('currentPassword')?.hasError('required')">
Current password is required Current password is required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>New password</mat-label> <mat-label>New password</mat-label>
<input type="password" matInput formControlName="newPassword" required> <input type="password" matInput formControlName="newPassword" required minlength="8" autocomplete="new-password">
<mat-error *ngIf="form.get('newPassword')?.hasError('required')"> <mat-error *ngIf="form.get('newPassword')?.hasError('required')">
New password is required New password is required
</mat-error> </mat-error>
@ -21,18 +24,16 @@
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Verify new password</mat-label> <mat-label>Verify new password</mat-label>
<input type="password" matInput formControlName="verifyNewPassword" required> <input type="password" matInput formControlName="verifyNewPassword" required autocomplete="new-password">
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('required')"> <mat-error *ngIf="form.get('verifyNewPassword')?.hasError('required')">
New password verification is required New password verification is required
</mat-error> </mat-error>
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('invalid')"> <mat-error *ngIf="form.hasError('passwordMismatch')">
Password don't match Passwords don't match
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div class="row bottom"> <div class="row bottom">
<button type="button" mat-raised-button [disabled]="form.disabled || form.untouched" <button type="button" mat-raised-button [disabled]="form.disabled || form.untouched" (click)="reset()">Reset</button>
(click)="reset();">Reset</button> <button type="submit" mat-raised-button color="primary" [disabled]="form.disabled || form.pristine || form.invalid">Save</button>
<button type="button" mat-raised-button color="primary"
[disabled]="form.disabled || form.pristine || !form.valid" (click)="save()">Save</button>
</div> </div>
</form> </form>

View file

@ -18,61 +18,68 @@
*/ */
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, ValidationErrors, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { RdioScannerAdminService } from '../../admin.service'; import { RdioScannerAdminService } from '../../admin.service';
@Component({ @Component({
selector: 'rdio-scanner-admin-password', selector: 'rdio-scanner-admin-password',
styleUrls: ['./password.component.scss'], styleUrls: ['./password.component.scss'],
templateUrl: './password.component.html', templateUrl: './password.component.html',
standalone: false
}) })
export class RdioScannerAdminPasswordComponent { export class RdioScannerAdminPasswordComponent {
form: FormGroup; form: FormGroup;
constructor( constructor(
private adminService: RdioScannerAdminService, private readonly adminService: RdioScannerAdminService,
private matSnackBar: MatSnackBar, private readonly matSnackBar: MatSnackBar,
private ngFormBuilder: FormBuilder, private readonly ngFormBuilder: FormBuilder,
) { ) {
this.form = this.ngFormBuilder.group({ this.form = this.ngFormBuilder.group({
currentPassword: [null, Validators.required], currentPassword: [null, Validators.required],
newPassword: [null, [Validators.required, Validators.minLength(8)]], newPassword: [null, [Validators.required, Validators.minLength(8)]],
verifyNewPassword: [null, [Validators.required, this.validatePassword()]], verifyNewPassword: [null, Validators.required],
}); }, { validators: this.passwordsMatchValidator });
this.form.get('newPassword')?.valueChanges.subscribe(() =>
this.form.get('verifyNewPassword')?.updateValueAndValidity({ onlySelf: true })
);
} }
private validatePassword(): ValidatorFn { private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
return (control: AbstractControl): ValidationErrors | null => { const newPwd = group.get('newPassword')?.value;
return typeof control.value === 'string' && control.value.length const verify = group.get('verifyNewPassword')?.value;
? control.value === control.parent?.value.newPassword return newPwd && verify && newPwd !== verify ? { passwordMismatch: true } : null;
? null : { invalid: true } : null;
}
} }
get currentPassword() { return this.form.get('currentPassword'); }
get newPassword() { return this.form.get('newPassword'); }
get verifyNewPassword() { return this.form.get('verifyNewPassword'); }
reset(): void { reset(): void {
this.form.reset(); this.form.reset();
} }
async save(): Promise<void> { async save(): Promise<void> {
const config: MatSnackBarConfig = { duration: 5000 }; if (this.form.invalid) {
const currentPassword = this.form.get('currentPassword')?.value; this.form.markAllAsTouched();
const newPassword = this.form.get('newPassword')?.value; return;
}
const config = { duration: 5000 };
const currentPassword = this.currentPassword?.value;
const newPassword = this.newPassword?.value;
this.form.disable(); this.form.disable();
try { try {
if (currentPassword && newPassword) {
await this.adminService.changePassword(currentPassword, newPassword); await this.adminService.changePassword(currentPassword, newPassword);
}
this.matSnackBar.open('Password changed successfully', '', config); this.matSnackBar.open('Password changed successfully', '', config);
this.form.reset(); this.form.reset();
} catch (_) { } catch {
this.matSnackBar.open('Unable to change password', '', config); this.matSnackBar.open('Unable to change password', '', config);
} } finally {
this.form.enable(); this.form.enable();
} }
} }
}

View file

@ -24,6 +24,7 @@ import { Config } from '../admin.service';
@Component({ @Component({
selector: 'rdio-scanner-admin-tools', selector: 'rdio-scanner-admin-tools',
templateUrl: './tools.component.html', templateUrl: './tools.component.html',
standalone: false
}) })
export class RdioScannerAdminToolsComponent { export class RdioScannerAdminToolsComponent {
@Output() config = new EventEmitter<Config>(); @Output() config = new EventEmitter<Config>();

View file

@ -8,8 +8,12 @@
<span>{{ clock | date:timeFormat }}</span> <span>{{ clock | date:timeFormat }}</span>
</div> </div>
<div> <div>
<span *ngIf="!linked">NO LINK</span> @if (!linked) {
<span *ngIf="linked && showListenersCount">L: {{ listeners }}</span> <span>NO LINK</span>
}
@if (linked && showListenersCount) {
<span>L: {{ listeners }}</span>
}
</div> </div>
<div> <div>
<span>Q: {{ callQueue }}</span> <span>Q: {{ callQueue }}</span>
@ -55,16 +59,20 @@
</div> </div>
<div></div> <div></div>
<div> <div>
<span *ngIf="callUnit">UID: {{ callUnit }}</span> @if (callUnit) {
<span>UID: {{ callUnit }}</span>
}
</div> </div>
</div> </div>
<div class="row right small"> <div class="row right small">
<div></div> <div></div>
<div></div> <div></div>
<div> <div>
<div *ngIf="tempAvoid"> @if (tempAvoid) {
<div>
<span class="flag" [ngClass]="{ flaged: avoided || patched }">&#x23f2;&#xFE0E; {{ tempAvoid }}M</span> <span class="flag" [ngClass]="{ flaged: avoided || patched }">&#x23f2;&#xFE0E; {{ tempAvoid }}M</span>
</div> </div>
}
<div> <div>
<span class="flag" [ngClass]="{ flaged: avoided }">AVOID</span> <span class="flag" [ngClass]="{ flaged: avoided }">AVOID</span>
</div> </div>
@ -82,12 +90,16 @@
<mat-form-field> <mat-form-field>
<input #password matInput type="password" autocomplete="off" formControlName="password" <input #password matInput type="password" autocomplete="off" formControlName="password"
placeholder="Unlock code" (blur)="authFocus()"> placeholder="Unlock code" (blur)="authFocus()">
<mat-error *ngIf="authForm.get('password')?.hasError('expired')"> @if (authForm.get('password')?.hasError('expired')) {
<mat-error>
This unlock code has expired This unlock code has expired
</mat-error> </mat-error>
<mat-error *ngIf="authForm.get('password')?.hasError('tooMany')"> }
@if (authForm.get('password')?.hasError('tooMany')) {
<mat-error>
Too many connections Too many connections
</mat-error> </mat-error>
}
</mat-form-field> </mat-form-field>
</form> </form>
</div> </div>
@ -109,7 +121,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let previousCall of callHistory" @for (previousCall of callHistory; track $index) {
<tr
[ngClass]="{ 'replaying': previousCall?.id !== null && previousCall?.id === call?.id} "> [ngClass]="{ 'replaying': previousCall?.id !== null && previousCall?.id === call?.id} ">
<td> <td>
<span>{{ previousCall?.dateTime | date:timeFormat }}</span> <span>{{ previousCall?.dateTime | date:timeFormat }}</span>
@ -124,6 +137,7 @@
<span>{{ previousCall?.talkgroupData?.name || previousCall?.frequency }}</span> <span>{{ previousCall?.talkgroupData?.name || previousCall?.frequency }}</span>
</td> </td>
</tr> </tr>
}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -170,8 +184,10 @@
</button> </button>
</div> </div>
</div> </div>
<div class="rdio-help" *ngIf="email"> @if (email) {
<div class="rdio-help">
<button mat-icon-button (click)="showHelp()"> <button mat-icon-button (click)="showHelp()">
<mat-icon>help_center</mat-icon> <mat-icon>help_center</mat-icon>
</button> </button>
</div> </div>
}

View file

@ -42,6 +42,7 @@ import { RdioScannerSupportComponent } from './support/support.component';
'./main.component.scss', './main.component.scss',
], ],
templateUrl: './main.component.html', templateUrl: './main.component.html',
standalone: false
}) })
export class RdioScannerMainComponent implements OnDestroy, OnInit { export class RdioScannerMainComponent implements OnDestroy, OnInit {
auth = false; auth = false;
@ -530,7 +531,7 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
} }
private isAfsSystem(call: RdioScannerCall): boolean { private isAfsSystem(call: RdioScannerCall): boolean {
return (call.systemData?.type == 'provoice' ?? false) || (call.talkgroupData?.type === 'provoice' ?? false); return (call.systemData?.type === 'provoice') || (call.talkgroupData?.type === 'provoice');
} }
private syncClock(): void { private syncClock(): void {

View file

@ -1,10 +1,10 @@
<span class="support"> <span class="support">
<ng-container *ngIf="email"> @if (email) {
For further assistance please contact <b><a href="mailto:{{ email }}" target="_blank">{{ email }}</a></b> For further assistance please contact <b><a href="mailto:{{ email }}" target="_blank">{{ email }}</a></b>
</ng-container> }
<ng-container *ngIf="!email"> @if (!email) {
For further assistance please go to <b><a href="https://github.com/chuot/rdio-scanner/discussions" For further assistance please go to <b><a href="https://github.com/chuot/rdio-scanner/discussions"
target="_blank">Rdio&nbsp;Scanner&nbsp;discussions</a></b> target="_blank">Rdio&nbsp;Scanner&nbsp;discussions</a></b>
</ng-container> }
</span> </span>
<span class="mat-caption countdown">{{ countdown }}</span> <span class="mat-caption countdown">{{ countdown }}</span>

View file

@ -25,6 +25,7 @@ import { timer } from 'rxjs';
selector: 'RdioScannerSupport', selector: 'RdioScannerSupport',
styleUrls: ['./support.component.scss'], styleUrls: ['./support.component.scss'],
templateUrl: './support.component.html', templateUrl: './support.component.html',
standalone: false
}) })
export class RdioScannerSupportComponent { export class RdioScannerSupportComponent {
countdown = 10; countdown = 10;

View file

@ -22,5 +22,6 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'app-store-badge', selector: 'app-store-badge',
templateUrl: './app-store-badge.component.svg', templateUrl: './app-store-badge.component.svg',
standalone: false
}) })
export class AppStoreBadgeComponent {} export class AppStoreBadgeComponent {}

View file

@ -22,5 +22,6 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'google-play-badge', selector: 'google-play-badge',
templateUrl: './google-play-badge.component.svg', templateUrl: './google-play-badge.component.svg',
standalone: false
}) })
export class GooglePlayBadgeComponent {} export class GooglePlayBadgeComponent {}

View file

@ -3,16 +3,16 @@
<p>Do you know that Rdio&nbsp;Scanner works natively on {{ deviceType }} devices?</p> <p>Do you know that Rdio&nbsp;Scanner works natively on {{ deviceType }} devices?</p>
</div> </div>
<div> <div>
<ng-container *ngIf="isAndroid"> @if (isAndroid) {
<a href="https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner" target="_blank"> <a href="https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner" target="_blank">
<google-play-badge></google-play-badge> <google-play-badge></google-play-badge>
</a> </a>
</ng-container> }
<ng-container *ngIf="isApple"> @if (isApple) {
<a href="https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone" target="_blank"> <a href="https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone" target="_blank">
<app-store-badge></app-store-badge> <app-store-badge></app-store-badge>
</a> </a>
</ng-container> }
</div> </div>
</div> </div>
<div class="countdown">{{ countdown }}</div> <div class="countdown">{{ countdown }}</div>

View file

@ -25,6 +25,7 @@ import { timer } from 'rxjs';
selector: 'RdioScannerNative', selector: 'RdioScannerNative',
styleUrls: ['./native.component.scss'], styleUrls: ['./native.component.scss'],
templateUrl: './native.component.html', templateUrl: './native.component.html',
standalone: false
}) })
export class RdioScannerNativeComponent implements OnInit { export class RdioScannerNativeComponent implements OnInit {
countdown = 10; countdown = 10;

View file

@ -29,6 +29,7 @@ import { RdioScannerNativeComponent } from './native/native.component';
selector: 'rdio-scanner', selector: 'rdio-scanner',
styleUrls: ['./rdio-scanner.component.scss'], styleUrls: ['./rdio-scanner.component.scss'],
templateUrl: './rdio-scanner.component.html', templateUrl: './rdio-scanner.component.html',
standalone: false
}) })
export class RdioScannerComponent implements OnDestroy, OnInit { export class RdioScannerComponent implements OnDestroy, OnInit {
private eventSubscription; private eventSubscription;

View file

@ -17,8 +17,8 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { DOCUMENT } from '@angular/common';
import { EventEmitter, Inject, Injectable, OnDestroy } from '@angular/core'; import { EventEmitter, Inject, Injectable, OnDestroy, DOCUMENT } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { interval, Subscription, timer } from 'rxjs'; import { interval, Subscription, timer } from 'rxjs';
import { takeWhile } from 'rxjs/operators'; import { takeWhile } from 'rxjs/operators';

View file

@ -3,19 +3,27 @@
<ng-container matColumnDef="control"> <ng-container matColumnDef="control">
<mat-header-cell *matHeaderCellDef></mat-header-cell> <mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
<button *ngIf="downloadMode.checked && row" mat-icon-button (click)="download(+row.id)"> @if (downloadMode.checked && row) {
<button mat-icon-button (click)="download(+row.id)">
<mat-icon>save_alt</mat-icon> <mat-icon>save_alt</mat-icon>
</button> </button>
<button *ngIf="row && !downloadMode.checked && !paused && row?.id != call?.id && row?.id != callPending" }
@if (row && !downloadMode.checked && !paused && row?.id != call?.id && row?.id != callPending) {
<button
mat-icon-button (click)="play(+row.id)"> mat-icon-button (click)="play(+row.id)">
<mat-icon>play_arrow</mat-icon> <mat-icon>play_arrow</mat-icon>
</button> </button>
<button *ngIf="row && !downloadMode.checked && row?.id == callPending" mat-icon-button> }
@if (row && !downloadMode.checked && row?.id == callPending) {
<button mat-icon-button>
<mat-icon class="spin">cached</mat-icon> <mat-icon class="spin">cached</mat-icon>
</button> </button>
<button *ngIf="row && !downloadMode.checked && row?.id == call?.id" mat-icon-button (click)="stop()"> }
@if (row && !downloadMode.checked && row?.id == call?.id) {
<button mat-icon-button (click)="stop()">
<mat-icon>stop</mat-icon> <mat-icon>stop</mat-icon>
</button> </button>
}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="date"> <ng-container matColumnDef="date">
@ -105,9 +113,11 @@
<mat-option [value]="-1"> <mat-option [value]="-1">
All Systems All Systems
</mat-option> </mat-option>
<mat-option *ngFor="let system of optionsSystem; index as i" [value]="i"> @for (system of optionsSystem; track system; let i = $index) {
<mat-option [value]="i">
{{ system }} {{ system }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
@ -118,9 +128,11 @@
<mat-option [value]="-1"> <mat-option [value]="-1">
All Talkgroups All Talkgroups
</mat-option> </mat-option>
<mat-option *ngFor="let talkgroup of optionsTalkgroup; index as i" [value]="i"> @for (talkgroup of optionsTalkgroup; track talkgroup; let i = $index) {
<mat-option [value]="i">
{{ talkgroup }} {{ talkgroup }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
@ -131,9 +143,11 @@
<mat-option [value]="-1"> <mat-option [value]="-1">
All Groups All Groups
</mat-option> </mat-option>
<mat-option *ngFor="let group of optionsGroup; index as i" [value]="i"> @for (group of optionsGroup; track group; let i = $index) {
<mat-option [value]="i">
{{ group }} {{ group }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
@ -144,9 +158,11 @@
<mat-option [value]="-1"> <mat-option [value]="-1">
All Tags All Tags
</mat-option> </mat-option>
<mat-option *ngFor="let tag of optionsTag; index as i" [value]="i"> @for (tag of optionsTag; track tag; let i = $index) {
<mat-option [value]="i">
{{ tag }} {{ tag }}
</mat-option> </mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>

View file

@ -37,6 +37,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
selector: 'rdio-scanner-search', selector: 'rdio-scanner-search',
styleUrls: ['./search.component.scss'], styleUrls: ['./search.component.scss'],
templateUrl: './search.component.html', templateUrl: './search.component.html',
standalone: false
}) })
export class RdioScannerSearchComponent implements OnDestroy { export class RdioScannerSearchComponent implements OnDestroy {
call: RdioScannerCall | undefined; call: RdioScannerCall | undefined;

View file

@ -1,9 +1,12 @@
<fieldset *ngIf="categories?.length && (categories?.length || 0) >= 2" class="fieldset"> @if (categories?.length && (categories?.length || 0) >= 2) {
<fieldset class="fieldset">
<div> <div>
<button *ngFor="let category of categories" class="rdio-button" [ngClass]="category.status" @for (category of categories; track category) {
<button class="rdio-button" [ngClass]="category.status"
(click)="toggle(category)"> (click)="toggle(category)">
{{ category.label }} {{ category.label }}
</button> </button>
}
<button class="rdio-button-mini all-off" (click)="avoid({ all: false })"> <button class="rdio-button-mini all-off" (click)="avoid({ all: false })">
OFF OFF
</button> </button>
@ -12,28 +15,33 @@
</button> </button>
</div> </div>
</fieldset> </fieldset>
<ng-container *ngFor="let system of systems"> }
<fieldset *ngIf="system.talkgroups.length" class="fieldset"> @for (system of systems; track system) {
@if (system.talkgroups.length) {
<fieldset class="fieldset">
<legend> <legend>
{{ system.label }} {{ system.label }}
</legend> </legend>
<div> <div>
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button" @for (talkgroup of system.talkgroups; track talkgroup) {
<button class="rdio-button"
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]).active, on: map[system.id] && map[system.id][talkgroup.id].active, blink: map[system.id][talkgroup.id].minutes }" [ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]).active, on: map[system.id] && map[system.id][talkgroup.id].active, blink: map[system.id][talkgroup.id].minutes }"
(click)="avoid({ system: system, talkgroup: talkgroup })"> (click)="avoid({ system: system, talkgroup: talkgroup })">
{{ talkgroup.label }} {{ talkgroup.label }}
</button> </button>
<ng-container *ngIf="system.talkgroups.length > 1"> }
@if (system.talkgroups.length > 1) {
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })"> <button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
OFF OFF
</button> </button>
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })"> <button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
ON ON
</button> </button>
</ng-container> }
</div> </div>
</fieldset> </fieldset>
</ng-container> }
<ng-container *ngIf="systems?.length === 0"> }
@if (systems?.length === 0) {
No talkgroup configured! No talkgroup configured!
</ng-container> }

View file

@ -36,6 +36,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
'./select.component.scss', './select.component.scss',
], ],
templateUrl: './select.component.html', templateUrl: './select.component.html',
standalone: false
}) })
export class RdioScannerSelectComponent implements OnDestroy { export class RdioScannerSelectComponent implements OnDestroy {
categories: RdioScannerCategory[] | undefined; categories: RdioScannerCategory[] | undefined;

View file

@ -24,6 +24,7 @@ import packageInfo from '../../../../../package.json';
selector: 'rdio-scanner-admin-page', selector: 'rdio-scanner-admin-page',
styleUrls: ['./admin.component.scss'], styleUrls: ['./admin.component.scss'],
templateUrl: './admin.component.html', templateUrl: './admin.component.html',
standalone: false
}) })
export class RdioScannerAdminPageComponent { export class RdioScannerAdminPageComponent {
version = packageInfo.version; version = packageInfo.version;

View file

@ -22,5 +22,6 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'rdio-scanner-main-page', selector: 'rdio-scanner-main-page',
template: '<rdio-scanner></rdio-scanner>', template: '<rdio-scanner></rdio-scanner>',
standalone: false
}) })
export class RdioScannerMainPageComponent { } export class RdioScannerMainPageComponent { }

View file

@ -22,5 +22,6 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'rdio-scanner-page', selector: 'rdio-scanner-page',
template: '<router-outlet></router-outlet>', template: '<router-outlet></router-outlet>',
standalone: false
}) })
export class RdioScannerPageComponent { } export class RdioScannerPageComponent { }

View file

@ -22,5 +22,6 @@ import { Component } from '@angular/core';
@Component({ @Component({
templateUrl: './update.component.html', templateUrl: './update.component.html',
styleUrls: ['./update.component.scss'], styleUrls: ['./update.component.scss'],
standalone: false
}) })
export class AppUpdateComponent { } export class AppUpdateComponent { }

View file

@ -17,8 +17,8 @@
* **************************************************************************** * ****************************************************************************
*/ */
import { enableProdMode } from '@angular/core'; import { enableProdMode, provideZoneChangeDetection } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
@ -27,4 +27,4 @@ if (environment.production) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule).catch((err) => console.error(err)); platformBrowser().bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], }).catch((err) => console.error(err));

View file

@ -1,20 +1,21 @@
@use "@angular/material" as mat; @use "@angular/material" as mat;
@import "./styles/material-icons"; @use "./styles/material-icons" as *;
@import "./styles/roboto-fonts"; @use "./styles/roboto-fonts" as *;
@include mat.core(); @include mat.elevation-classes();
@include mat.app-background();
$rdio-scanner-font-family: Roboto, "Helvetica Neue", sans-serif; $rdio-scanner-font-family: Roboto, "Helvetica Neue", sans-serif;
$rdio-scanner-primary: mat.define-palette(mat.$grey-palette, 800, 50); $rdio-scanner-primary: mat.m2-define-palette(mat.$m2-grey-palette, 800, 50);
$rdio-scanner-accent: mat.define-palette(mat.$green-palette); $rdio-scanner-accent: mat.m2-define-palette(mat.$m2-green-palette);
$rdio-scanner-warn: mat.define-palette(mat.$red-palette); $rdio-scanner-warn: mat.m2-define-palette(mat.$m2-red-palette);
$rdio-scanner-typography: mat.define-typography-config( $rdio-scanner-typography: mat.m2-define-typography-config(
$font-family: $rdio-scanner-font-family, $font-family: $rdio-scanner-font-family,
); );
$rdio-scanner-theme: mat.define-light-theme( $rdio-scanner-theme: mat.m2-define-light-theme(
( (
color: ( color: (
primary: $rdio-scanner-primary, primary: $rdio-scanner-primary,

View file

@ -5,24 +5,19 @@
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"esModuleInterop": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "bundler",
"importHelpers": true, "importHelpers": true,
"target": "es2022", "target": "es2022",
"module": "es2022", "module": "es2022",
"lib": [ "resolveJsonModule": true
"es2022",
"dom"
],
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,