mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-13 08:33:00 -06:00
feat: update to angular 21
This commit is contained in:
parent
c9cc7f0a3c
commit
53a426bea9
|
|
@ -16,14 +16,15 @@
|
|||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "rdio-scanner",
|
||||
"architect": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "../server/webapp",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
|
|
@ -36,15 +37,16 @@
|
|||
],
|
||||
"scripts": [],
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json"
|
||||
"ngswConfigPath": "ngsw-config.json",
|
||||
"main": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1500kb",
|
||||
"maximumError": "1500kb"
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
|
@ -61,9 +63,7 @@
|
|||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
|
|
@ -75,11 +75,11 @@
|
|||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "rdio-scanner:build:production"
|
||||
"buildTarget": "rdio-scanner:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "rdio-scanner:build:development",
|
||||
"proxyConfig": "src/proxy.conf.js"
|
||||
"proxyConfig": "src/proxy.conf.js",
|
||||
"buildTarget": "rdio-scanner:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
|
|
@ -87,14 +87,16 @@
|
|||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "rdio-scanner:build"
|
||||
"buildTarget": "rdio-scanner:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"inlineStyleLanguage": "scss",
|
||||
|
|
@ -114,5 +116,31 @@
|
|||
},
|
||||
"cli": {
|
||||
"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": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
15340
client/package-lock.json
generated
15340
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,32 +1,34 @@
|
|||
{
|
||||
"author": "Chrystian Huot <chrystian.huot@saubeo.solutions>",
|
||||
"author": "Chrystian Huot <chrystian@huot.qc.ca>",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^15.0.4",
|
||||
"@angular/cdk": "^15.0.3",
|
||||
"@angular/common": "^15.0.4",
|
||||
"@angular/compiler": "^15.0.4",
|
||||
"@angular/core": "^15.0.4",
|
||||
"@angular/forms": "^15.0.4",
|
||||
"@angular/material": "^15.0.3",
|
||||
"@angular/platform-browser": "^15.0.4",
|
||||
"@angular/platform-browser-dynamic": "^15.0.4",
|
||||
"@angular/router": "^15.0.4",
|
||||
"@angular/service-worker": "^15.0.4",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.11.4"
|
||||
"@angular/animations": "^21.0.6",
|
||||
"@angular/cdk": "^21.0.5",
|
||||
"@angular/common": "^21.0.6",
|
||||
"@angular/compiler": "^21.0.6",
|
||||
"@angular/core": "^21.0.6",
|
||||
"@angular/forms": "^21.0.6",
|
||||
"@angular/material": "^21.0.5",
|
||||
"@angular/platform-browser": "^21.0.6",
|
||||
"@angular/platform-browser-dynamic": "^21.0.6",
|
||||
"@angular/router": "^21.0.6",
|
||||
"@angular/service-worker": "^21.0.6",
|
||||
"material-icons": "^1.13.14",
|
||||
"rxjs": "~7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^15.0.4",
|
||||
"@angular/cli": "^15.0.4",
|
||||
"@angular/compiler-cli": "^15.0.4",
|
||||
"@types/node": "^18.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"@angular-devkit/build-angular": "^21.0.4",
|
||||
"@angular/build": "^21.0.4",
|
||||
"@angular/cli": "^21.0.4",
|
||||
"@angular/compiler-cli": "^21.0.6",
|
||||
"@types/node": "^25.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"eslint": "^8.25.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.8.4"
|
||||
"eslint": "^9.39.2",
|
||||
"ts-node": "~10.9.2",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0"
|
||||
|
|
@ -36,7 +38,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"build": "ng build --base-href ./ --configuration production",
|
||||
"start": "ng serve"
|
||||
"serve": "ng serve"
|
||||
},
|
||||
"version": "7.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,6 @@ import { Component } from '@angular/core';
|
|||
selector: 'app-root',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
templateUrl: './app.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class AppComponent { }
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<ng-container *ngIf="!authenticated">
|
||||
@if (!authenticated) {
|
||||
<rdio-scanner-admin-login></rdio-scanner-admin-login>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="authenticated">
|
||||
}
|
||||
@if (authenticated) {
|
||||
<rdio-scanner-admin-todos></rdio-scanner-admin-todos>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel #configPanel (afterCollapse)="configComponent?.closeAll()">
|
||||
<mat-expansion-panel #configPanel (afterCollapse)="configComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>settings</mat-icon>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</mat-expansion-panel-header>
|
||||
<rdio-scanner-admin-logs #logsComponent></rdio-scanner-admin-logs>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (afterCollapse)="toolsComponent?.closeAll()">
|
||||
<mat-expansion-panel (afterCollapse)="toolsComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>build</mat-icon>
|
||||
|
|
@ -40,4 +40,4 @@
|
|||
</mat-expansion-panel-header>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</ng-container>
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import { AdminEvent, RdioScannerAdminService, Group, Tag } from './admin.service
|
|||
selector: 'rdio-scanner-admin',
|
||||
styleUrls: ['./admin.component.scss'],
|
||||
templateUrl: './admin.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminComponent implements OnDestroy {
|
||||
authenticated = true;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AppSharedModule } from '../../../shared/shared.module';
|
||||
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 { RdioScannerAdminPasswordComponent } from './tools/password/password.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@NgModule({ declarations: [
|
||||
RdioScannerAdminComponent,
|
||||
RdioScannerAdminConfigComponent,
|
||||
RdioScannerAdminAccessComponent,
|
||||
|
|
@ -71,9 +70,5 @@ import { RdioScannerAdminPasswordComponent } from './tools/password/password.com
|
|||
RdioScannerAdminToolsComponent,
|
||||
RdioScannerAdminUnitComponent,
|
||||
],
|
||||
entryComponents: [RdioScannerAdminSystemsSelectComponent],
|
||||
exports: [RdioScannerAdminComponent],
|
||||
imports: [AppSharedModule, HttpClientModule],
|
||||
providers: [RdioScannerAdminService],
|
||||
})
|
||||
exports: [RdioScannerAdminComponent], imports: [AppSharedModule], providers: [RdioScannerAdminService, provideHttpClient(withInterceptorsFromDi())] })
|
||||
export class RdioScannerAdminModule { }
|
||||
|
|
|
|||
|
|
@ -4,15 +4,20 @@
|
|||
internally on the client side and reused thereafter.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New access</button>
|
||||
</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>
|
||||
}
|
||||
<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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ 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-expansion-panel-header>
|
||||
<ng-container [formGroup]="access">
|
||||
|
|
@ -26,12 +31,16 @@
|
|||
<button type="button" mat-icon-button matSuffix (click)="hideCode = !hideCode">
|
||||
<mat-icon>{{ hideCode ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="access.get('code')?.hasError('duplicate')">
|
||||
@if (access.get('code')?.hasError('duplicate')) {
|
||||
<mat-error>
|
||||
Code is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="access.get('code')?.hasError('required')">
|
||||
}
|
||||
@if (access.get('code')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Code is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -42,9 +51,11 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -74,8 +85,12 @@
|
|||
<span class="mat-body">Access</span><br>
|
||||
<span class="mat-caption">
|
||||
The access code allows access to <u>
|
||||
<ng-container *ngIf="access.value.systems === '*'">all</ng-container>
|
||||
<ng-container *ngIf="access.value.systems !== '*'">some</ng-container>
|
||||
@if (access.value.systems === '*') {
|
||||
all
|
||||
}
|
||||
@if (access.value.systems !== '*') {
|
||||
some
|
||||
}
|
||||
</u> systems and talkgroups.
|
||||
</span>
|
||||
</p>
|
||||
|
|
@ -92,4 +107,5 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-access',
|
||||
templateUrl: './access.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminAccessComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@
|
|||
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="apiKey.get('key')?.hasError('duplicate')">
|
||||
<mat-error *ngIf="apiKey.get('key').hasError('duplicate')">
|
||||
Key is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="apiKey.get('key')?.hasError('required')">
|
||||
<mat-error *ngIf="apiKey.get('key').hasError('required')">
|
||||
Key is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="never">
|
||||
<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
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,19 @@
|
|||
authenticate with an API key to exchange audio files.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New API key</button>
|
||||
</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-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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ 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-expansion-panel-header>
|
||||
<ng-container [formGroup]="apikey">
|
||||
|
|
@ -33,12 +38,16 @@
|
|||
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="apikey.get('key')?.hasError('duplicate')">
|
||||
@if (apikey.get('key')!.hasError('duplicate')) {
|
||||
<mat-error>
|
||||
Key is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="apikey.get('key')?.hasError('required')">
|
||||
}
|
||||
@if (apikey.get('key')!.hasError('required')) {
|
||||
<mat-error>
|
||||
Key is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -49,9 +58,11 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -59,8 +70,12 @@
|
|||
<span class="mat-body">Access</span><br>
|
||||
<span class="mat-caption">
|
||||
This API key allows access to <u>
|
||||
<ng-container *ngIf="apikey.value.systems === '*'">all</ng-container>
|
||||
<ng-container *ngIf="apikey.value.systems !== '*'">some</ng-container>
|
||||
@if (apikey.value.systems === '*') {
|
||||
all
|
||||
}
|
||||
@if (apikey.value.systems !== '*') {
|
||||
some
|
||||
}
|
||||
</u> systems and talkgroups.
|
||||
</span>
|
||||
</p>
|
||||
|
|
@ -77,4 +92,5 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-apikeys',
|
||||
templateUrl: './apikeys.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminApikeysComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
|
|
|||
|
|
@ -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-expansion-panel (afterCollapse)="accessComponent.closeAll()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-access #accessComponent [form]="access"></rdio-scanner-admin-access>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>vpn_key</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-apikeys #apikeyComponent [form]="apikeys"></rdio-scanner-admin-apikeys>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>folder</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-dirwatch #dirwatchComponent [form]="dirwatch"></rdio-scanner-admin-dirwatch>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>share</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-downstreams #downstreamsComponent [form]="downstreams"></rdio-scanner-admin-downstreams>
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>workspaces</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-groups [form]="groups"></rdio-scanner-admin-groups>
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>tune</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-options [form]="options"></rdio-scanner-admin-options>
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>podcasts</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-systems #systemsComponent [form]="systems"></rdio-scanner-admin-systems>
|
||||
|
|
@ -75,16 +75,16 @@
|
|||
<mat-panel-title>
|
||||
<mat-icon>sell</mat-icon>
|
||||
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-expansion-panel-header>
|
||||
<rdio-scanner-admin-tags [form]="tags"></rdio-scanner-admin-tags>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
|
|
@ -21,6 +21,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
|
|||
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
@ -28,6 +30,7 @@ import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
|
|||
selector: 'rdio-scanner-admin-config',
|
||||
styleUrls: ['./config.component.scss'],
|
||||
templateUrl: './config.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||
docker = false;
|
||||
|
|
@ -68,7 +71,7 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
|||
|
||||
private config: Config | undefined;
|
||||
|
||||
private eventSubscription;
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
|
|
@ -76,10 +79,9 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
|||
private adminService: RdioScannerAdminService,
|
||||
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) {
|
||||
this.config = await this.adminService.getConfig();
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +100,8 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.eventSubscription.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
|
|
@ -116,12 +119,17 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
|||
reset(config = this.config, options?: { dirty?: boolean }): void {
|
||||
this.form = this.adminService.newConfigForm(config);
|
||||
|
||||
this.form.statusChanges.subscribe(() => {
|
||||
this.form.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.ngChangeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.groups.valueChanges.subscribe(() => {
|
||||
this.systems.controls.forEach((system) => {
|
||||
const systemsArray = this.systems;
|
||||
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;
|
||||
|
||||
talkgroups.controls.forEach((talkgroup) => {
|
||||
|
|
@ -136,8 +144,9 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
|||
});
|
||||
});
|
||||
|
||||
this.tags.valueChanges.subscribe(() => {
|
||||
this.systems.controls.forEach((system) => {
|
||||
tagsArray?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
if (!systemsArray) { return; }
|
||||
systemsArray.controls.forEach((system) => {
|
||||
const talkgroups = system.get('talkgroups') as FormArray;
|
||||
|
||||
talkgroups.controls.forEach((talkgroup) => {
|
||||
|
|
@ -160,8 +169,10 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="dirwatches"
|
||||
(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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ 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-expansion-panel-header>
|
||||
<ng-container [formGroup]="dirwatch">
|
||||
|
|
@ -61,38 +64,45 @@
|
|||
<span class="mat-body">Directory</span><br>
|
||||
<span class="mat-caption">
|
||||
Path of the
|
||||
<ng-container [ngSwitch]="dirwatch.get('type')?.value">
|
||||
<ng-container *ngSwitchCase="'dsdplus'">
|
||||
@switch (dirwatch.get('type')?.value) {
|
||||
@case ('dsdplus') {
|
||||
<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>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'trunk-recorder'">
|
||||
}
|
||||
@case ('trunk-recorder') {
|
||||
<b>captureDir</b>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
}
|
||||
@default {
|
||||
local
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
directory to monitor for file ingestion. Note that dirwatch is not compatible with networked
|
||||
disk.
|
||||
</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="dirwatch.get('directory')?.hasError('duplicate')">
|
||||
}
|
||||
@if (dirwatch.get('directory')?.hasError('duplicate')) {
|
||||
<mat-error>
|
||||
Directory is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="dirwatch.get('directory')?.hasError('network')">
|
||||
}
|
||||
@if (dirwatch.get('directory')?.hasError('network')) {
|
||||
<mat-error>
|
||||
Incompatible network folder
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</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>
|
||||
<span class="mat-body">Extension</span><br>
|
||||
<span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3",
|
||||
|
|
@ -100,12 +110,16 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)">
|
||||
}
|
||||
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">System</span><br>
|
||||
<span class="mat-caption">System to where the audio files should go.</span>
|
||||
|
|
@ -113,18 +127,24 @@
|
|||
<mat-form-field floatLabel="auto">
|
||||
<mat-select #sysId formControlName="systemId" placeholder="System">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<ng-container *ngFor="let system of systems">
|
||||
<mat-option *ngIf="system.value.id > 0" [value]="system.value.id">
|
||||
@for (system of systems; track system) {
|
||||
@if (system.value.id > 0) {
|
||||
<mat-option [value]="system.value.id">
|
||||
{{ system.value.label }}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</mat-select>
|
||||
<mat-error *ngIf="dirwatch.get('systemId')?.hasError('required')">
|
||||
@if (dirwatch.get('systemId')?.hasError('required')) {
|
||||
<mat-error>
|
||||
System is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)">
|
||||
}
|
||||
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Site</span><br>
|
||||
<span class="mat-caption">Site to where the audio files should go.</span>
|
||||
|
|
@ -132,15 +152,19 @@
|
|||
<mat-form-field floatLabel="auto">
|
||||
<mat-select formControlName="siteId" placeholder="Site">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<ng-container *ngFor="let site of sites[dirwatch.value.systemId] || []">
|
||||
<mat-option *ngIf="site.value.id > 0" [value]="site.value.id">
|
||||
@for (site of sites[dirwatch.value.systemId] || []; track site) {
|
||||
@if (site.value.id > 0) {
|
||||
<mat-option [value]="site.value.id">
|
||||
{{ site.value.label }}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)">
|
||||
}
|
||||
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Talkgroup</span><br>
|
||||
<span class="mat-caption">Talkgroup to where the audio files should go.</span>
|
||||
|
|
@ -148,18 +172,24 @@
|
|||
<mat-form-field floatLabel="auto">
|
||||
<mat-select formControlName="talkgroupId" placeholder="Talkgroup">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<ng-container *ngFor="let talkgroup of talkgroups[dirwatch.value.systemId] || []">
|
||||
<mat-option *ngIf="talkgroup.value.id > 0" [value]="talkgroup.value.id">
|
||||
@for (talkgroup of talkgroups[dirwatch.value.systemId] || []; track talkgroup) {
|
||||
@if (talkgroup.value.id > 0) {
|
||||
<mat-option [value]="talkgroup.value.id">
|
||||
{{ talkgroup.value.label }}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</mat-select>
|
||||
<mat-error *ngIf="dirwatch.get('talkgroupId')?.hasError('required')">
|
||||
@if (dirwatch.get('talkgroupId')?.hasError('required')) {
|
||||
<mat-error>
|
||||
Talkgroup is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)">
|
||||
}
|
||||
@if (['default'].includes(dirwatch.get('type')?.value)) {
|
||||
<div class="row">
|
||||
<p>
|
||||
<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
|
||||
|
|
@ -193,30 +223,38 @@
|
|||
<li><b>#UNITLBL</b> - extract the unit label. Also requires #UNIT.</li>
|
||||
<li><b>#ZTIME</b> - extract the zulu time like 0453439 (HHMMSS),
|
||||
08-34-39 (HH-MM-SS) or 04:34:39 (HH:MM:SS).</li>
|
||||
</ul>
|
||||
Example: cymx_#TG_#DATE_#TIME_#HZ
|
||||
</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
</ul>
|
||||
Example: cymx_#TG_#DATE_#TIME_#HZ
|
||||
</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)">
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
@if (['default'].includes(dirwatch.get('type')?.value)) {
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Frequency</span><br>
|
||||
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="number" matInput formControlName="frequency" placeholder="Frequency">
|
||||
<mat-error *ngIf="dirwatch.get('frequency')?.errors">
|
||||
@if (dirwatch.get('frequency')?.errors) {
|
||||
<mat-error>
|
||||
Invalid frequency
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)">
|
||||
}
|
||||
@if (['default'].includes(dirwatch.get('type')?.value)) {
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Delay</span><br>
|
||||
<span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the
|
||||
|
|
@ -225,16 +263,20 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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.
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
}
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||
Delete dirwatch
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
|
@ -26,6 +26,7 @@ import { RdioScannerAdminService } from '../../admin.service';
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-dirwatch',
|
||||
templateUrl: './dirwatch.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminDirwatchComponent implements OnChanges {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@
|
|||
<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>
|
||||
</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-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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ 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-expansion-panel-header>
|
||||
<ng-container [formGroup]="downstream">
|
||||
|
|
@ -29,12 +34,16 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="downstream.get('apikey')?.hasError('required')">
|
||||
}
|
||||
@if (downstream.get('apikey')?.hasError('required')) {
|
||||
<mat-error>
|
||||
API key is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -44,15 +53,21 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="downstream.get('url')?.hasError('invalid')">
|
||||
}
|
||||
@if (downstream.get('url')?.hasError('invalid')) {
|
||||
<mat-error>
|
||||
URL is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="downstream.get('url')?.hasError('duplicate')">
|
||||
}
|
||||
@if (downstream.get('url')?.hasError('duplicate')) {
|
||||
<mat-error>
|
||||
URL is already defined
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -60,8 +75,12 @@
|
|||
<span class="mat-body">Access</span><br>
|
||||
<span class="mat-caption">
|
||||
This downstream allows access to <u>
|
||||
<ng-container *ngIf="downstream.value.systems === '*'">all</ng-container>
|
||||
<ng-container *ngIf="downstream.value.systems !== '*'">some</ng-container>
|
||||
@if (downstream.value.systems === '*') {
|
||||
all
|
||||
}
|
||||
@if (downstream.value.systems !== '*') {
|
||||
some
|
||||
}
|
||||
</u> systems and talkgroups.
|
||||
</span>
|
||||
</p>
|
||||
|
|
@ -78,4 +97,5 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-downstreams',
|
||||
templateUrl: './downstreams.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminDownstreamsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
active talkgroups.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New Group</button>
|
||||
</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"
|
||||
(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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
|
|||
|
|
@ -18,35 +18,60 @@
|
|||
*/
|
||||
|
||||
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 { FormArray, FormGroup } from '@angular/forms';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-groups',
|
||||
templateUrl: './groups.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminGroupsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
export class RdioScannerAdminGroupsComponent implements OnDestroy {
|
||||
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[] {
|
||||
return Object.keys(this.adminService.Alerts || {});
|
||||
return this.alertsArray;
|
||||
}
|
||||
|
||||
get groups(): FormGroup[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => a.value.order - b.value.order) as FormGroup[];
|
||||
if (this.cacheValid && this.cachedGroups) return this.cachedGroups;
|
||||
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;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) {
|
||||
this.leds = this.adminService.getLeds();
|
||||
this.alertsArray = Object.keys(this.adminService.Alerts || {});
|
||||
}
|
||||
|
||||
add(): void {
|
||||
|
|
@ -57,6 +82,7 @@ export class RdioScannerAdminGroupsComponent {
|
|||
this.form?.insert(0, group);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
this.cacheValid = false;
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
|
|
@ -70,6 +96,7 @@ export class RdioScannerAdminGroupsComponent {
|
|||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
this.cacheValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,5 +108,10 @@ export class RdioScannerAdminGroupsComponent {
|
|||
this.form?.removeAt(index);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
this.cacheValid = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.formSub?.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<ng-container *ngIf="form">
|
||||
<ng-container [formGroup]="form">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">12-Hour Time Format</span><br>
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
<div class="row">
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
<div>
|
||||
|
|
@ -50,10 +51,10 @@
|
|||
</p>
|
||||
<mat-form-field>
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('min')">
|
||||
<mat-error *ngIf="form.get('dimmerDelay')?.hasError('min')">
|
||||
Dimmer delay is invalid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -75,10 +76,10 @@
|
|||
</p>
|
||||
<mat-form-field>
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('duplicateDetectionTimeFrame')?.hasError('min')">
|
||||
<mat-error *ngIf="form.get('duplicateDetectionTimeFrame')?.hasError('min')">
|
||||
Duplicate call time frame is invalid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -111,10 +112,10 @@
|
|||
</p>
|
||||
<mat-form-field>
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('maxClients')?.hasError('min')">
|
||||
<mat-error *ngIf="form.get('maxClients')?.hasError('min')">
|
||||
Max clients is invalid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -137,10 +138,10 @@
|
|||
</p>
|
||||
<mat-form-field>
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('pruneDays')?.hasError('min')">
|
||||
<mat-error *ngIf="form.get('pruneDays')?.hasError('min')">
|
||||
Prune days is invalid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -163,4 +164,5 @@
|
|||
<mat-slide-toggle color="primary" formControlName="sortTalkgroups"></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
@ -17,13 +17,19 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-options',
|
||||
templateUrl: './options.component.html',
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RdioScannerAdminOptionsComponent {
|
||||
@Input() form: FormGroup | undefined;
|
||||
@Input() form: FormGroup | null = null;
|
||||
|
||||
get controls() {
|
||||
return this.form ? this.form.controls : {};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,25 +7,30 @@
|
|||
</mat-checkbox>
|
||||
<h3 class="mat-caption">Groups</h3>
|
||||
<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"
|
||||
[indeterminate]="indeterminate.groups[groupIndex]">
|
||||
{{ group.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<h3 class="mat-caption">Tags</h3>
|
||||
<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"
|
||||
[indeterminate]="indeterminate.tags[tagIndex]">
|
||||
{{ tag.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<h3 class="mat-caption">Systems</h3>
|
||||
<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">
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
||||
[indeterminate]="indeterminate.systems[systemIndex]">
|
||||
|
|
@ -33,14 +38,16 @@
|
|||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="flex-row wrap indent margin" formArrayName="talkgroups">
|
||||
<div *ngFor="let talkgroup of configTalkgroups[systemIndex]; index as talkgroupIndex"
|
||||
[formGroupName]="talkgroupIndex">
|
||||
<ng-container *ngFor="let talkgroup of configTalkgroups[systemIndex]; let talkgroupIndex = index">
|
||||
<div [formGroupName]="talkgroupIndex">
|
||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked">
|
||||
{{ talkgroup.value.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Access } from '../../../admin.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
interface System {
|
||||
all: boolean;
|
||||
|
|
@ -38,8 +39,9 @@ interface Talkgroup {
|
|||
selector: 'rdio-scanner-admin-systems-selection',
|
||||
styleUrls: ['./select.component.scss'],
|
||||
templateUrl: './select.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminSystemsSelectComponent {
|
||||
export class RdioScannerAdminSystemsSelectComponent implements OnDestroy {
|
||||
indeterminate = {
|
||||
everything: false,
|
||||
groups: [] as boolean[],
|
||||
|
|
@ -53,22 +55,21 @@ export class RdioScannerAdminSystemsSelectComponent {
|
|||
|
||||
get configGroups(): FormGroup[] {
|
||||
const faGroups = this.access.root.get('groups') as FormArray;
|
||||
|
||||
return faGroups.controls as FormGroup[];
|
||||
}
|
||||
|
||||
get configSystems(): FormGroup[] {
|
||||
const faSystems = this.access.root.get('systems') as FormArray;
|
||||
|
||||
return faSystems.controls as FormGroup[];
|
||||
}
|
||||
|
||||
get configTags(): FormGroup[] {
|
||||
const faTags = this.access.root.get('tags') as FormArray;
|
||||
|
||||
return faTags.controls as FormGroup[];
|
||||
}
|
||||
|
||||
private subs = new Subscription();
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public access: FormGroup,
|
||||
private matDialogRef: MatDialogRef<RdioScannerAdminSystemsSelectComponent>,
|
||||
|
|
@ -76,7 +77,6 @@ export class RdioScannerAdminSystemsSelectComponent {
|
|||
) {
|
||||
this.configTalkgroups = this.configSystems.map((fgSystem) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
return faTalkgroups.controls as FormGroup[];
|
||||
});
|
||||
|
||||
|
|
@ -92,179 +92,140 @@ export class RdioScannerAdminSystemsSelectComponent {
|
|||
const faSystems = this.select.get('systems') 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({
|
||||
id: this.ngFormBuilder.control(configGroup.get('id')?.value),
|
||||
checked: this.ngFormBuilder.control(false),
|
||||
});
|
||||
|
||||
faGroups.push(fgGroup);
|
||||
|
||||
fgGroup.valueChanges.subscribe((vGroup) => {
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
this.subs.add(fgGroup.valueChanges.subscribe((vGroup) => {
|
||||
for (const fgSystem of faSystems.controls) {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
if (fgTalkgroup.get('groupIds')?.value.includes(vGroup.id) && fgTalkgroup.get('checked')?.value !== vGroup.checked) {
|
||||
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||
const ids = fgTalkgroup.get('groupIds')?.value;
|
||||
if (ids && ids.includes(vGroup.id) && fgTalkgroup.get('checked')?.value !== 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 faSystemTalkgroups = this.ngFormBuilder.array<FormGroup<{
|
||||
checked: FormControl<boolean>;
|
||||
groupIds: FormControl<number[]>;
|
||||
id: FormControl<number>;
|
||||
tagId: FormControl<number>;
|
||||
}>>([]);
|
||||
|
||||
const faSystemTalkgroups = this.ngFormBuilder.array<FormGroup>([]);
|
||||
const fgSystem = this.ngFormBuilder.group({
|
||||
all: fcSystemAll,
|
||||
id: this.ngFormBuilder.control(configSystem.get('systemRef')?.value),
|
||||
talkgroups: faSystemTalkgroups
|
||||
});
|
||||
|
||||
this.configTalkgroups[index].forEach((configTalkgroup) => {
|
||||
for (const configTalkgroup of this.configTalkgroups[index]) {
|
||||
const fgSystemTalkgroup = this.ngFormBuilder.group({
|
||||
checked: this.ngFormBuilder.nonNullable.control(false),
|
||||
groupIds: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('groupIds')?.value),
|
||||
id: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('talkgroupRef')?.value),
|
||||
tagId: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('tagId')?.value),
|
||||
});
|
||||
|
||||
faSystemTalkgroups.push(fgSystemTalkgroup);
|
||||
|
||||
fgSystemTalkgroup.valueChanges.subscribe(() => {
|
||||
const vAll = faSystemTalkgroups.controls.every((systemTalkgroup) => systemTalkgroup.get('checked')?.value);
|
||||
|
||||
this.subs.add(fgSystemTalkgroup.valueChanges.subscribe(() => {
|
||||
const vAll = faSystemTalkgroups.controls.every((t) => t.get('checked')?.value);
|
||||
fcSystemAll.setValue(vAll, { emitEvent: false });
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
faSystems.push(fgSystem);
|
||||
|
||||
fgSystem.valueChanges.subscribe(() => {
|
||||
this.subs.add(fgSystem.valueChanges.subscribe(() => {
|
||||
this.rebuildGroupIndeterminates();
|
||||
this.rebuildTagIndeterminates();
|
||||
});
|
||||
}));
|
||||
|
||||
fcSystemAll.valueChanges.subscribe((vAll) => {
|
||||
this.subs.add(fcSystemAll.valueChanges.subscribe((vAll) => {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => fgTalkgroup.get('checked')?.setValue(vAll));
|
||||
|
||||
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||
fgTalkgroup.get('checked')?.setValue(vAll);
|
||||
}
|
||||
this.rebuildGroupIndeterminates();
|
||||
this.rebuildTagIndeterminates();
|
||||
});
|
||||
}));
|
||||
|
||||
faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => {
|
||||
let off = 0;
|
||||
this.subs.add(faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => {
|
||||
let on = 0;
|
||||
|
||||
vSystemTalkgroups.forEach((vSystemTalkgroup) => {
|
||||
if (vSystemTalkgroup.checked) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
let off = 0;
|
||||
for (const v of vSystemTalkgroups) {
|
||||
if (v.checked) on++; else off++;
|
||||
}
|
||||
});
|
||||
|
||||
this.indeterminate.systems[index] = !!off && !!on;
|
||||
|
||||
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({
|
||||
id: this.ngFormBuilder.control(configTag.value.id),
|
||||
checked: this.ngFormBuilder.control(false),
|
||||
});
|
||||
|
||||
faTags.push(fgTag);
|
||||
|
||||
fgTag.valueChanges.subscribe((vTag) => {
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
this.subs.add(fgTag.valueChanges.subscribe((vTag) => {
|
||||
for (const fgSystem of faSystems.controls) {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||
if (fgTalkgroup.value.tagId === vTag.id && fgTalkgroup.value.checked !== 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;
|
||||
|
||||
fcAll.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
}));
|
||||
|
||||
const vAccess: Access = this.access.value;
|
||||
|
||||
if (vAccess.systems === '*') {
|
||||
this.select.get('all')?.setValue(true);
|
||||
|
||||
} else if (Array.isArray(vAccess.systems)) {
|
||||
vAccess.systems.forEach((vSystem) => {
|
||||
for (const vSystem of vAccess.systems) {
|
||||
if (typeof vSystem === 'number') {
|
||||
faSystems.controls.find((fgSystem) => fgSystem.get('id')?.value === vSystem)?.get('all')?.setValue(true);
|
||||
|
||||
} else if (vSystem !== null && typeof vSystem === 'object') {
|
||||
const fgSystem = faSystems.controls.find((fg) => {
|
||||
return fg.get('id')?.value === vSystem.id;
|
||||
});
|
||||
|
||||
} else if (vSystem && typeof vSystem === 'object') {
|
||||
const fgSystem = faSystems.controls.find((fg) => fg.get('id')?.value === vSystem.id);
|
||||
if (fgSystem) {
|
||||
if (vSystem.talkgroups === '*') {
|
||||
fgSystem.get('all')?.setValue(true);
|
||||
|
||||
} else if (Array.isArray(vSystem.talkgroups)) {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
vSystem.talkgroups.forEach((talkgroup: { id: number } | number) => {
|
||||
for (const talkgroup of vSystem.talkgroups) {
|
||||
const talkgroupId = typeof talkgroup === 'number' ? talkgroup : talkgroup.id;
|
||||
|
||||
const fgTalkgroup = faTalkgroups.controls.find((fg) => fg.get('id')?.value === talkgroupId);
|
||||
|
||||
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);
|
||||
}).map((system: System) => {
|
||||
if (system['all']) {
|
||||
return {
|
||||
id: system['id'],
|
||||
talkgroups: '*',
|
||||
};
|
||||
|
||||
return { id: system['id'], talkgroups: '*' };
|
||||
} else {
|
||||
return {
|
||||
id: system['id'],
|
||||
|
|
@ -297,61 +254,47 @@ export class RdioScannerAdminSystemsSelectComponent {
|
|||
|
||||
private rebuildGroupIndeterminates(): void {
|
||||
const faGroups = this.select.get('groups') as FormArray;
|
||||
|
||||
const faSystems = this.select.get('systems') as FormArray;
|
||||
|
||||
faGroups.controls.forEach((fgGroup, index) => {
|
||||
let off = 0;
|
||||
for (let index = 0; index < faGroups.length; index++) {
|
||||
const fgGroup = faGroups.at(index);
|
||||
let on = 0;
|
||||
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
let off = 0;
|
||||
for (const fgSystem of faSystems.controls) {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||
if (fgTalkgroup.get('groupIds')?.value.includes(fgGroup.get('id')?.value)) {
|
||||
if (fgTalkgroup.get('checked')?.value) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
if (fgTalkgroup.get('checked')?.value) on++; else off++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.indeterminate.groups[index] = !!off && !!on;
|
||||
|
||||
fgGroup.get('checked')?.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private rebuildTagIndeterminates(): void {
|
||||
const faTags = this.select.get('tags') as FormArray;
|
||||
|
||||
const faSystems = this.select.get('systems') as FormArray;
|
||||
|
||||
faTags.controls.forEach((fgTag, index) => {
|
||||
let off = 0;
|
||||
for (let index = 0; index < faTags.length; index++) {
|
||||
const fgTag = faTags.at(index);
|
||||
let on = 0;
|
||||
|
||||
faSystems.controls.forEach((fgSystem) => {
|
||||
let off = 0;
|
||||
for (const fgSystem of faSystems.controls) {
|
||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||
|
||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
||||
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||
if (fgTalkgroup.value.tagId === fgTag.value.id) {
|
||||
if (fgTalkgroup.value.checked) {
|
||||
on++;
|
||||
|
||||
} else {
|
||||
off++;
|
||||
if (fgTalkgroup.value.checked) on++; else off++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.indeterminate.tags[index] = !!off && !!on;
|
||||
|
||||
fgTag.get('checked')?.setValue(!off && on, { emitEvent: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,40 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<ng-container *ngIf="form as f" [formGroup]="f">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Id</span><br>
|
||||
<span class="mat-caption">Site identifier in decimal format.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="number" min="1" step="1" matInput formControlName="siteRef" placeholder="Id">
|
||||
<mat-error *ngIf="form?.get('siteRef')?.hasError('duplicate')">
|
||||
<ng-container *ngIf="f.get('siteRef') as siteRef">
|
||||
<input type="number" min="1" step="1" matInput formControlName="siteRef" placeholder="Id" />
|
||||
<mat-error *ngIf="siteRef.hasError('duplicate')">
|
||||
Id is already defined
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('siteRef')?.hasError('min')">
|
||||
<mat-error *ngIf="siteRef.hasError('min')">
|
||||
Id is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('siteRef')?.hasError('required')">
|
||||
<mat-error *ngIf="siteRef.hasError('required')">
|
||||
Id is required
|
||||
</mat-error>
|
||||
</ng-container>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Label</span><br>
|
||||
<span class="mat-caption">Site label.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
||||
<ng-container *ngIf="f.get('label') as labelCtrl">
|
||||
<input type="text" matInput formControlName="label" placeholder="Label" />
|
||||
<mat-error *ngIf="labelCtrl.hasError('required')">
|
||||
Label is required
|
||||
</mat-error>
|
||||
</ng-container>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||
Delete site
|
||||
|
|
|
|||
|
|
@ -17,15 +17,36 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { FormGroup, AbstractControl } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-site',
|
||||
templateUrl: './site.component.html',
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
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>();
|
||||
|
||||
constructor(private readonly cd: ChangeDetectorRef) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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">
|
||||
{{ led | titlecase }}
|
||||
</mat-option>
|
||||
|
|
@ -82,8 +82,8 @@
|
|||
has an effect if the auto populate option is enabled.</span>
|
||||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<textarea type="text" matInput formControlName="blacklists" placeholder="Blacklists"></textarea>
|
||||
<mat-error *ngIf="form?.get('blacklists')?.hasError('invalid')">
|
||||
<textarea matInput formControlName="blacklists" placeholder="Blacklists"></textarea>
|
||||
<mat-error *ngIf="form.get('blacklists')!.hasError('invalid')">
|
||||
Comma separated list of talkgroup Ids
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -134,9 +134,9 @@
|
|||
<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>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="sites"
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="sites"
|
||||
(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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
|
|
@ -167,10 +167,9 @@
|
|||
option is enabled.</p>
|
||||
<button type="button" mat-button color="accent" (click)="addTalkgroup()">New talkgroup</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64"
|
||||
[cdkDropListData]="talkgroups" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let talkgroup of talkgroups; index as i" [formGroup]="talkgroup"
|
||||
cdkDrag>
|
||||
<mat-expansion-panel *ngFor="let talkgroup of talkgroups; let i = index; trackBy: trackById" [formGroup]="talkgroup" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
|
|
@ -202,9 +201,9 @@
|
|||
screen.<br>You can drag and drop them to rearrange</p>
|
||||
<button type="button" mat-button color="accent" (click)="addUnit()">New unit</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="units"
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="units"
|
||||
(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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-system',
|
||||
templateUrl: './system.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminSystemComponent {
|
||||
@Input() form = new FormGroup({});
|
||||
|
|
@ -47,23 +48,22 @@ export class RdioScannerAdminSystemComponent {
|
|||
|
||||
get sites(): FormGroup[] {
|
||||
const sites = this.form.get('sites') as FormArray | null;
|
||||
|
||||
return sites?.controls
|
||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
||||
return (sites?.controls as FormGroup[]) || [];
|
||||
}
|
||||
|
||||
get talkgroups(): FormGroup[] {
|
||||
const talkgroups = this.form.get('talkgroups') as FormArray | null;
|
||||
|
||||
return talkgroups?.controls
|
||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
||||
return (talkgroups?.controls as FormGroup[]) || [];
|
||||
}
|
||||
|
||||
get units(): FormGroup[] {
|
||||
const units = this.form.get('units') as FormArray | null;
|
||||
return (units?.controls as FormGroup[]) || [];
|
||||
}
|
||||
|
||||
return units?.controls
|
||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
||||
// trackBy to preserve identity in ngFor lists
|
||||
trackById(_index: number, group: FormGroup): any {
|
||||
return group.get('id')?.value ?? _index;
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
systems to rearrange their order.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New system</button>
|
||||
</div>
|
||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let system of systems; index as i" [formGroup]="system"
|
||||
<mat-accordion *ngIf="systems" displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)">
|
||||
<mat-expansion-panel *ngFor="let system of systems; let i = index" [formGroup]="system"
|
||||
(afterCollapse)="systemComponent.closeAll()" cdkDrag>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
|
|
|
|||
|
|
@ -18,26 +18,44 @@
|
|||
*/
|
||||
|
||||
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 { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-systems',
|
||||
templateUrl: './systems.component.html',
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RdioScannerAdminSystemsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
export class RdioScannerAdminSystemsComponent implements OnDestroy {
|
||||
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[] {
|
||||
return this.form?.controls
|
||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
||||
return this._systemsCache;
|
||||
}
|
||||
|
||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||
|
||||
constructor(private adminService: RdioScannerAdminService) { }
|
||||
constructor(private adminService: RdioScannerAdminService, private cd: ChangeDetectorRef) { }
|
||||
|
||||
add(): void {
|
||||
const system = this.adminService.newSystemForm();
|
||||
|
|
@ -47,6 +65,8 @@ export class RdioScannerAdminSystemsComponent {
|
|||
this.form?.insert(0, system);
|
||||
|
||||
this.form?.markAsDirty();
|
||||
|
||||
this.updateCache();
|
||||
}
|
||||
|
||||
closeAll(): void {
|
||||
|
|
@ -60,6 +80,8 @@ export class RdioScannerAdminSystemsComponent {
|
|||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||
|
||||
this.form?.markAsDirty();
|
||||
|
||||
this.updateCache();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,5 +89,23 @@ export class RdioScannerAdminSystemsComponent {
|
|||
this.form?.removeAt(index);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<ng-container *ngIf="form">
|
||||
<ng-container [formGroup]="form">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Id</span><br>
|
||||
|
|
@ -6,13 +7,13 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('min')">
|
||||
<mat-error *ngIf="form.get('talkgroupRef')!.hasError('min')">
|
||||
Id is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('required')">
|
||||
<mat-error *ngIf="form.get('talkgroupRef')!.hasError('required')">
|
||||
Id is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -24,7 +25,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
<div class="row">
|
||||
<p>
|
||||
<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>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<mat-select formControlName="groupIds" placeholder="Groups" [multiple]="true">
|
||||
|
|
@ -52,7 +53,7 @@
|
|||
{{ group.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form?.get('groupId')?.hasError('required')">
|
||||
<mat-error *ngIf="form.get('groupIds')?.hasError('required')">
|
||||
Group is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
<div class="row">
|
||||
<p>
|
||||
<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>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<mat-select formControlName="tagId" placeholder="Tag">
|
||||
|
|
@ -68,7 +69,7 @@
|
|||
{{ tag.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form?.get('tagId')?.hasError('required')">
|
||||
<mat-error *ngIf="form.get('tagId')!.hasError('required')">
|
||||
Tag is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -80,7 +81,7 @@
|
|||
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||
</p>
|
||||
<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 *ngFor="let alert of alerts" [value]="alert">
|
||||
{{ alert | titlecase }}
|
||||
|
|
@ -104,7 +105,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -117,7 +118,7 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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">
|
||||
{{ led | titlecase }}
|
||||
</mat-option>
|
||||
|
|
@ -165,4 +166,5 @@
|
|||
Delete talkgroup
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
@ -19,12 +19,12 @@
|
|||
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-talkgroup',
|
||||
templateUrl: './talkgroup.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminTalkgroupComponent {
|
||||
@Input() form: FormGroup | undefined;
|
||||
|
|
@ -33,25 +33,25 @@ export class RdioScannerAdminTalkgroupComponent {
|
|||
|
||||
@Output() remove = new EventEmitter<void>();
|
||||
|
||||
leds: string[];
|
||||
leds: string[] = [];
|
||||
|
||||
get alerts(): string[] {
|
||||
return Object.keys(this.adminService.Alerts || {});
|
||||
}
|
||||
|
||||
get groups(): Group[] {
|
||||
return this.form?.root.get('groups')?.value as Group[];
|
||||
return (this.form?.root.get('groups')?.value as Group[]) || [];
|
||||
}
|
||||
|
||||
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) {
|
||||
this.leds = this.adminService.getLeds();
|
||||
this.leds = this.adminService.getLeds() || [];
|
||||
}
|
||||
|
||||
async playAlert(event: MatSelectChange): Promise<void> {
|
||||
if (event.value) await this.adminService.playAlert(event.value);
|
||||
async playAlert(value: string | null): Promise<void> {
|
||||
if (value) await this.adminService.playAlert(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<ng-container *ngIf="form" [formGroup]="form">
|
||||
<ng-container *ngIf="form as f">
|
||||
<ng-container [formGroup]="f">
|
||||
<div class="row">
|
||||
<p>
|
||||
<span class="mat-body">Id</span><br>
|
||||
|
|
@ -7,15 +8,9 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="number" min="1" step="1" matInput formControlName="unitRef" placeholder="Id">
|
||||
<mat-error *ngIf="form?.get('unitRef')?.hasError('duplicate')">
|
||||
Id value is already defined
|
||||
</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-error *ngIf="f.get('unitRef')!.hasError('duplicate')">Id value is already defined</mat-error>
|
||||
<mat-error *ngIf="f.get('unitRef')!.hasError('min')">Id value is invalid</mat-error>
|
||||
<mat-error *ngIf="f.get('unitRef')!.hasError('required')">Id value is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -25,12 +20,8 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="number" min="1" step="1" matInput formControlName="unitFrom" placeholder="From">
|
||||
<mat-error *ngIf="form?.get('unitFrom')?.hasError('range')">
|
||||
From value should be lower than To value
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('unitFrom')?.hasError('min')">
|
||||
From value To is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f.get('unitFrom')!.hasError('range')">From value should be lower than To value</mat-error>
|
||||
<mat-error *ngIf="f.get('unitFrom')!.hasError('min')">From value is invalid</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -40,12 +31,8 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="number" min="1" step="1" matInput formControlName="unitTo" placeholder="To">
|
||||
<mat-error *ngIf="form?.get('unitTo')?.hasError('range')">
|
||||
To value should be higher than From value
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form?.get('unitTo')?.hasError('min')">
|
||||
To value is invalid
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f.get('unitTo')!.hasError('range')">To value should be higher than From value</mat-error>
|
||||
<mat-error *ngIf="f.get('unitTo')!.hasError('min')">To value is invalid</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -55,14 +42,11 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
||||
Label is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="f.get('label')!.hasError('required')">Label is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||
Delete unit
|
||||
</button>
|
||||
<button type="button" mat-button color="warn" (click)="remove.emit()">Delete unit</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
@ -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';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-unit',
|
||||
templateUrl: './unit.component.html',
|
||||
standalone: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RdioScannerAdminUnitComponent {
|
||||
@Input() form: FormGroup | undefined;
|
||||
@Input() form: FormGroup | null = null;
|
||||
|
||||
@Output() remove = new EventEmitter<void>();
|
||||
@Output() readonly remove = new EventEmitter<void>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@
|
|||
based on their tag.</p>
|
||||
<button type="button" mat-button color="accent" (click)="add()">New Tag</button>
|
||||
</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"
|
||||
(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-panel-title>
|
||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ 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-expansion-panel-header>
|
||||
<ng-container [formGroup]="tag">
|
||||
|
|
@ -22,9 +27,11 @@
|
|||
</p>
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
@ -36,9 +43,11 @@
|
|||
<mat-form-field floatLabel="auto">
|
||||
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
||||
<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 }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
@ -51,9 +60,11 @@
|
|||
<mat-form-field floatLabel="auto">
|
||||
<mat-select formControlName="led" placeholder="Color">
|
||||
<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 }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
@ -64,4 +75,5 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminService } from '../../admin.service';
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-tags',
|
||||
templateUrl: './tags.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminTagsComponent {
|
||||
@Input() form: FormArray | undefined;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,17 @@
|
|||
<p class="mat-body-2">Please enter the admin password to gain access to the administrative dashboard</p>
|
||||
<mat-form-field hideRequiredMarker>
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput formControlName="password" type="password" required>
|
||||
<mat-error>{{ message }}</mat-error>
|
||||
<input
|
||||
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>
|
||||
<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>
|
||||
|
|
@ -25,6 +25,7 @@ import { RdioScannerAdminService } from '../admin.service';
|
|||
selector: 'rdio-scanner-admin-login',
|
||||
styleUrls: ['./login.component.scss'],
|
||||
templateUrl: './login.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminLoginComponent {
|
||||
@Output() loggedIn = new EventEmitter<void>();
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@
|
|||
<span>Level</span>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let log">
|
||||
<mat-icon *ngIf="log?.level === 'error'" class="error">error</mat-icon>
|
||||
<mat-icon *ngIf="log?.level === 'info'" class="info">notifications</mat-icon>
|
||||
<mat-icon *ngIf="log?.level === 'warn'" class="warn">warning</mat-icon>
|
||||
@if (log?.level === 'error') {
|
||||
<mat-icon class="error">error</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>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="date">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { Log, LogsQuery, LogsQueryOptions, RdioScannerAdminService } from '../ad
|
|||
selector: 'rdio-scanner-admin-logs',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
templateUrl: './logs.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminLogsComponent {
|
||||
form: FormGroup;
|
||||
|
|
|
|||
|
|
@ -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-list>
|
||||
<mat-list-item *ngFor="let todo of todos">
|
||||
|
|
@ -9,4 +10,5 @@
|
|||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
.info {
|
||||
.mat-mdc-list-item .info {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.warn {
|
||||
.mat-mdc-list-item .warn {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.todo {
|
||||
.mat-mdc-list-item .todo {
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ interface Todo {
|
|||
selector: 'rdio-scanner-admin-todos',
|
||||
styleUrls: ['./todos.component.scss'],
|
||||
templateUrl: './todos.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit {
|
||||
todos: Todo[] = [];
|
||||
|
|
|
|||
|
|
@ -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 { Config, RdioScannerAdminService } from '../../admin.service';
|
||||
import packageInfo from '../../../../../../../package.json';
|
||||
|
|
@ -27,6 +27,7 @@ import packageInfo from '../../../../../../../package.json';
|
|||
selector: 'rdio-scanner-admin-import-export-config',
|
||||
styleUrls: ['./import-export-config.component.scss'],
|
||||
templateUrl: './import-export-config.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminImportExportConfigComponent {
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<p class="mat-body note">Before going any further, note that there is no need to preload talkgroups into a system as the
|
||||
autopopulate feature will automatically create them as audio files are ingested. So give the autopopulate feature a
|
||||
chance before trying to import talkgroups.</p>
|
||||
chance before trying to import talkgroups.</p>
|
||||
|
||||
<section>
|
||||
<p class="mat-body">step 1: read the CSV file</p>
|
||||
<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>
|
||||
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
||||
<br>
|
||||
|
|
@ -19,27 +19,29 @@
|
|||
<span class="mat-body-1">Tag</span>,
|
||||
<span class="mat-body-1">Group</span>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="csv.length !== 0">
|
||||
}
|
||||
@if (csv.length !== 0) {
|
||||
<button mat-raised-button (click)="csv=[]">Reset</button>
|
||||
</ng-container>
|
||||
}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section>
|
||||
<p class="mat-body">step 2: select system</p>
|
||||
<p class="text-center">
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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}}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section>
|
||||
<p class="mat-body">step 3: review talkgroups to import</p>
|
||||
<div class="scroll">
|
||||
<mat-table #table [dataSource]="csv">
|
||||
|
|
@ -95,19 +97,23 @@
|
|||
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p>
|
||||
</section>
|
||||
@if (!csv.length) {
|
||||
<p class="mat-body text-center">No CSV data yet</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section>
|
||||
<p class="mat-body">step 4: import to configuration</p>
|
||||
<p class="text-center">
|
||||
<button mat-raised-button [disabled]="!csv.length" (click)="import()">
|
||||
Import to configuration
|
||||
</button>
|
||||
</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
|
||||
has not designed for this number of talkgroups. You can continue, but expect Rdio Scanner to be quite slow, if
|
||||
not unusable.
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
</section>
|
||||
|
|
@ -24,6 +24,7 @@ import { Config, RdioScannerAdminService, System } from '../../admin.service';
|
|||
selector: 'rdio-scanner-admin-import-talkgroups',
|
||||
styleUrls: ['./import-talkgroups.component.scss'],
|
||||
templateUrl: './import-talkgroups.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminImportTalkgroupsComponent implements OnInit {
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<section>
|
||||
<p class="mat-body">step 1: read the CSV file</p>
|
||||
<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>
|
||||
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
||||
<br>
|
||||
|
|
@ -10,27 +10,29 @@
|
|||
<span>Id</span>,
|
||||
<span>Label</span>,
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="csv.length !== 0">
|
||||
}
|
||||
@if (csv.length !== 0) {
|
||||
<button mat-raised-button (click)="csv=[]">Reset</button>
|
||||
</ng-container>
|
||||
}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section>
|
||||
<p class="mat-body">step 2: select system</p>
|
||||
<p class="text-center">
|
||||
<mat-form-field floatLabel="auto">
|
||||
<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}}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section>
|
||||
<p class="mat-body">step 3: review talkgroups to import</p>
|
||||
<div class="scroll">
|
||||
<mat-table #table [dataSource]="csv">
|
||||
|
|
@ -62,19 +64,23 @@
|
|||
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p>
|
||||
</section>
|
||||
@if (!csv.length) {
|
||||
<p class="mat-body text-center">No CSV data yet</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section>
|
||||
<p class="mat-body">step 4: import to configuration</p>
|
||||
<p class="text-center">
|
||||
<button mat-raised-button [disabled]="!csv.length || system === undefined" (click)="import()">
|
||||
Import to configuration
|
||||
</button>
|
||||
</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
|
||||
not designed for this number of units. You can continue, but expect Rdio Scanner to be quite slow, if not
|
||||
unusable.
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
</section>
|
||||
|
|
@ -24,6 +24,7 @@ import { Config, RdioScannerAdminService, System } from '../../admin.service';
|
|||
selector: 'rdio-scanner-admin-import-units',
|
||||
styleUrls: ['./import-units.component.scss'],
|
||||
templateUrl: './import-units.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminImportUnitsComponent implements OnInit{
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<p class="mat-body">
|
||||
Change the administrator password which gives access to the server administration dashboard.
|
||||
</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-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')">
|
||||
Current password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<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')">
|
||||
New password is required
|
||||
</mat-error>
|
||||
|
|
@ -21,18 +24,16 @@
|
|||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<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')">
|
||||
New password verification is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('invalid')">
|
||||
Password don't match
|
||||
<mat-error *ngIf="form.hasError('passwordMismatch')">
|
||||
Passwords don't match
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<div class="row bottom">
|
||||
<button type="button" mat-raised-button [disabled]="form.disabled || form.untouched"
|
||||
(click)="reset();">Reset</button>
|
||||
<button type="button" mat-raised-button color="primary"
|
||||
[disabled]="form.disabled || form.pristine || !form.valid" (click)="save()">Save</button>
|
||||
<button type="button" mat-raised-button [disabled]="form.disabled || form.untouched" (click)="reset()">Reset</button>
|
||||
<button type="submit" mat-raised-button color="primary" [disabled]="form.disabled || form.pristine || form.invalid">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -18,61 +18,68 @@
|
|||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, ValidationErrors, Validators } from '@angular/forms';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
||||
import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RdioScannerAdminService } from '../../admin.service';
|
||||
|
||||
@Component({
|
||||
selector: 'rdio-scanner-admin-password',
|
||||
styleUrls: ['./password.component.scss'],
|
||||
templateUrl: './password.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminPasswordComponent {
|
||||
form: FormGroup;
|
||||
|
||||
constructor(
|
||||
private adminService: RdioScannerAdminService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
private ngFormBuilder: FormBuilder,
|
||||
private readonly adminService: RdioScannerAdminService,
|
||||
private readonly matSnackBar: MatSnackBar,
|
||||
private readonly ngFormBuilder: FormBuilder,
|
||||
) {
|
||||
this.form = this.ngFormBuilder.group({
|
||||
currentPassword: [null, Validators.required],
|
||||
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 {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return typeof control.value === 'string' && control.value.length
|
||||
? control.value === control.parent?.value.newPassword
|
||||
? null : { invalid: true } : null;
|
||||
}
|
||||
private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
|
||||
const newPwd = group.get('newPassword')?.value;
|
||||
const verify = group.get('verifyNewPassword')?.value;
|
||||
return newPwd && verify && newPwd !== verify ? { passwordMismatch: 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 {
|
||||
this.form.reset();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const config: MatSnackBarConfig = { duration: 5000 };
|
||||
const currentPassword = this.form.get('currentPassword')?.value;
|
||||
const newPassword = this.form.get('newPassword')?.value;
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { duration: 5000 };
|
||||
const currentPassword = this.currentPassword?.value;
|
||||
const newPassword = this.newPassword?.value;
|
||||
|
||||
this.form.disable();
|
||||
|
||||
try {
|
||||
if (currentPassword && newPassword) {
|
||||
await this.adminService.changePassword(currentPassword, newPassword);
|
||||
}
|
||||
|
||||
this.matSnackBar.open('Password changed successfully', '', config);
|
||||
|
||||
this.form.reset();
|
||||
} catch (_) {
|
||||
} catch {
|
||||
this.matSnackBar.open('Unable to change password', '', config);
|
||||
}
|
||||
|
||||
} finally {
|
||||
this.form.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { Config } from '../admin.service';
|
|||
@Component({
|
||||
selector: 'rdio-scanner-admin-tools',
|
||||
templateUrl: './tools.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminToolsComponent {
|
||||
@Output() config = new EventEmitter<Config>();
|
||||
|
|
|
|||
|
|
@ -8,8 +8,12 @@
|
|||
<span>{{ clock | date:timeFormat }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span *ngIf="!linked">NO LINK</span>
|
||||
<span *ngIf="linked && showListenersCount">L: {{ listeners }}</span>
|
||||
@if (!linked) {
|
||||
<span>NO LINK</span>
|
||||
}
|
||||
@if (linked && showListenersCount) {
|
||||
<span>L: {{ listeners }}</span>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<span>Q: {{ callQueue }}</span>
|
||||
|
|
@ -55,16 +59,20 @@
|
|||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<span *ngIf="callUnit">UID: {{ callUnit }}</span>
|
||||
@if (callUnit) {
|
||||
<span>UID: {{ callUnit }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row right small">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div *ngIf="tempAvoid">
|
||||
@if (tempAvoid) {
|
||||
<div>
|
||||
<span class="flag" [ngClass]="{ flaged: avoided || patched }">⏲︎ {{ tempAvoid }}M</span>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<span class="flag" [ngClass]="{ flaged: avoided }">AVOID</span>
|
||||
</div>
|
||||
|
|
@ -82,12 +90,16 @@
|
|||
<mat-form-field>
|
||||
<input #password matInput type="password" autocomplete="off" formControlName="password"
|
||||
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
|
||||
</mat-error>
|
||||
<mat-error *ngIf="authForm.get('password')?.hasError('tooMany')">
|
||||
}
|
||||
@if (authForm.get('password')?.hasError('tooMany')) {
|
||||
<mat-error>
|
||||
Too many connections
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -109,7 +121,8 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let previousCall of callHistory"
|
||||
@for (previousCall of callHistory; track $index) {
|
||||
<tr
|
||||
[ngClass]="{ 'replaying': previousCall?.id !== null && previousCall?.id === call?.id} ">
|
||||
<td>
|
||||
<span>{{ previousCall?.dateTime | date:timeFormat }}</span>
|
||||
|
|
@ -124,6 +137,7 @@
|
|||
<span>{{ previousCall?.talkgroupData?.name || previousCall?.frequency }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -170,8 +184,10 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rdio-help" *ngIf="email">
|
||||
@if (email) {
|
||||
<div class="rdio-help">
|
||||
<button mat-icon-button (click)="showHelp()">
|
||||
<mat-icon>help_center</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ import { RdioScannerSupportComponent } from './support/support.component';
|
|||
'./main.component.scss',
|
||||
],
|
||||
templateUrl: './main.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
||||
auth = false;
|
||||
|
|
@ -530,7 +531,7 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<span class="support">
|
||||
<ng-container *ngIf="email">
|
||||
@if (email) {
|
||||
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"
|
||||
target="_blank">Rdio Scanner discussions</a></b>
|
||||
</ng-container>
|
||||
}
|
||||
</span>
|
||||
<span class="mat-caption countdown">{{ countdown }}</span>
|
||||
|
|
@ -25,6 +25,7 @@ import { timer } from 'rxjs';
|
|||
selector: 'RdioScannerSupport',
|
||||
styleUrls: ['./support.component.scss'],
|
||||
templateUrl: './support.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerSupportComponent {
|
||||
countdown = 10;
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
|||
@Component({
|
||||
selector: 'app-store-badge',
|
||||
templateUrl: './app-store-badge.component.svg',
|
||||
standalone: false
|
||||
})
|
||||
export class AppStoreBadgeComponent {}
|
||||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
|||
@Component({
|
||||
selector: 'google-play-badge',
|
||||
templateUrl: './google-play-badge.component.svg',
|
||||
standalone: false
|
||||
})
|
||||
export class GooglePlayBadgeComponent {}
|
||||
|
|
@ -3,16 +3,16 @@
|
|||
<p>Do you know that Rdio Scanner works natively on {{ deviceType }} devices?</p>
|
||||
</div>
|
||||
<div>
|
||||
<ng-container *ngIf="isAndroid">
|
||||
@if (isAndroid) {
|
||||
<a href="https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner" target="_blank">
|
||||
<google-play-badge></google-play-badge>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isApple">
|
||||
}
|
||||
@if (isApple) {
|
||||
<a href="https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone" target="_blank">
|
||||
<app-store-badge></app-store-badge>
|
||||
</a>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="countdown">{{ countdown }}</div>
|
||||
|
|
@ -25,6 +25,7 @@ import { timer } from 'rxjs';
|
|||
selector: 'RdioScannerNative',
|
||||
styleUrls: ['./native.component.scss'],
|
||||
templateUrl: './native.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerNativeComponent implements OnInit {
|
||||
countdown = 10;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { RdioScannerNativeComponent } from './native/native.component';
|
|||
selector: 'rdio-scanner',
|
||||
styleUrls: ['./rdio-scanner.component.scss'],
|
||||
templateUrl: './rdio-scanner.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerComponent implements OnDestroy, OnInit {
|
||||
private eventSubscription;
|
||||
|
|
|
|||
|
|
@ -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 { interval, Subscription, timer } from 'rxjs';
|
||||
import { takeWhile } from 'rxjs/operators';
|
||||
|
|
|
|||
|
|
@ -3,19 +3,27 @@
|
|||
<ng-container matColumnDef="control">
|
||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||
<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>
|
||||
</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>play_arrow</mat-icon>
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="date">
|
||||
|
|
@ -105,9 +113,11 @@
|
|||
<mat-option [value]="-1">
|
||||
All Systems
|
||||
</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 }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
|
|
@ -118,9 +128,11 @@
|
|||
<mat-option [value]="-1">
|
||||
All Talkgroups
|
||||
</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 }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
|
|
@ -131,9 +143,11 @@
|
|||
<mat-option [value]="-1">
|
||||
All Groups
|
||||
</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 }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
|
|
@ -144,9 +158,11 @@
|
|||
<mat-option [value]="-1">
|
||||
All Tags
|
||||
</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 }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
|
|||
selector: 'rdio-scanner-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerSearchComponent implements OnDestroy {
|
||||
call: RdioScannerCall | undefined;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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)">
|
||||
{{ category.label }}
|
||||
</button>
|
||||
}
|
||||
<button class="rdio-button-mini all-off" (click)="avoid({ all: false })">
|
||||
OFF
|
||||
</button>
|
||||
|
|
@ -11,29 +14,34 @@
|
|||
ON
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<ng-container *ngFor="let system of systems">
|
||||
<fieldset *ngIf="system.talkgroups.length" class="fieldset">
|
||||
</fieldset>
|
||||
}
|
||||
@for (system of systems; track system) {
|
||||
@if (system.talkgroups.length) {
|
||||
<fieldset class="fieldset">
|
||||
<legend>
|
||||
{{ system.label }}
|
||||
</legend>
|
||||
<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 }"
|
||||
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
||||
{{ talkgroup.label }}
|
||||
</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 })">
|
||||
OFF
|
||||
</button>
|
||||
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
||||
ON
|
||||
</button>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="systems?.length === 0">
|
||||
}
|
||||
}
|
||||
@if (systems?.length === 0) {
|
||||
No talkgroup configured!
|
||||
</ng-container>
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
|
|||
'./select.component.scss',
|
||||
],
|
||||
templateUrl: './select.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerSelectComponent implements OnDestroy {
|
||||
categories: RdioScannerCategory[] | undefined;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import packageInfo from '../../../../../package.json';
|
|||
selector: 'rdio-scanner-admin-page',
|
||||
styleUrls: ['./admin.component.scss'],
|
||||
templateUrl: './admin.component.html',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerAdminPageComponent {
|
||||
version = packageInfo.version;
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
|||
@Component({
|
||||
selector: 'rdio-scanner-main-page',
|
||||
template: '<rdio-scanner></rdio-scanner>',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerMainPageComponent { }
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
|||
@Component({
|
||||
selector: 'rdio-scanner-page',
|
||||
template: '<router-outlet></router-outlet>',
|
||||
standalone: false
|
||||
})
|
||||
export class RdioScannerPageComponent { }
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
|||
@Component({
|
||||
templateUrl: './update.component.html',
|
||||
styleUrls: ['./update.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AppUpdateComponent { }
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { enableProdMode, provideZoneChangeDetection } from '@angular/core';
|
||||
import { platformBrowser } from '@angular/platform-browser';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
|
@ -27,4 +27,4 @@ if (environment.production) {
|
|||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule).catch((err) => console.error(err));
|
||||
platformBrowser().bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], }).catch((err) => console.error(err));
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
@use "@angular/material" as mat;
|
||||
|
||||
@import "./styles/material-icons";
|
||||
@import "./styles/roboto-fonts";
|
||||
@use "./styles/material-icons" as *;
|
||||
@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-primary: mat.define-palette(mat.$grey-palette, 800, 50);
|
||||
$rdio-scanner-accent: mat.define-palette(mat.$green-palette);
|
||||
$rdio-scanner-warn: mat.define-palette(mat.$red-palette);
|
||||
$rdio-scanner-typography: mat.define-typography-config(
|
||||
$rdio-scanner-primary: mat.m2-define-palette(mat.$m2-grey-palette, 800, 50);
|
||||
$rdio-scanner-accent: mat.m2-define-palette(mat.$m2-green-palette);
|
||||
$rdio-scanner-warn: mat.m2-define-palette(mat.$m2-red-palette);
|
||||
$rdio-scanner-typography: mat.m2-define-typography-config(
|
||||
$font-family: $rdio-scanner-font-family,
|
||||
);
|
||||
|
||||
$rdio-scanner-theme: mat.define-light-theme(
|
||||
$rdio-scanner-theme: mat.m2-define-light-theme(
|
||||
(
|
||||
color: (
|
||||
primary: $rdio-scanner-primary,
|
||||
|
|
|
|||
|
|
@ -5,24 +5,19 @@
|
|||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue