Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Godind/issue323 #403

Merged
merged 4 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/core/interfaces/widgets-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ export interface IDynamicControl {
value?: any;
/** The color of the control */
color: string;
/** If the control should use numeric path instead of boolean */
isNumeric: boolean;
}

/**
Expand Down Expand Up @@ -298,7 +300,7 @@ export interface IWidgetPath {
isPathConfigurable: boolean;
/** Hide numeric path type filter */
showPathSkUnitsFilter?: boolean;
/** Numeric path type filter to limiting path search results list based on SK Meta Units. Use valid Sk Units type or null to list all types */
/** Numeric path type filter to limiting path search results list based on SK Meta Units. Use valid Sk Units type, 'unitless' for paths with no meta units or null to list all types (no filter) */
pathSkUnitsFilter?: TValidSkUnits;
/** Used in Widget Options UI and by observeDataStream() method to convert Signal K transmitted values to a specified format. Also used as a source to identify conversion group. */
convertUnitTo?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/services/units.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Qty from 'js-quantities';
import { AppSettingsService } from './app-settings.service';
import { Subscription } from 'rxjs';

export type TValidSkUnits = 's' | 'Hz' | 'm3' | 'm3/s' | 'kg/s' | 'kg/m3' | 'deg' | 'rad' | 'rad/s' | 'A' | 'C' | 'V' | 'W' | 'Nm' | 'J' | 'ohm' | 'm' | 'm/s' | 'm2' | 'K' | 'Pa' | 'kg' | 'ratio' | 'm/s2' | 'rad/s2' | 'N' | 'T' | 'Lux' | 'Pa/s' | 'Pa.s' | null;
export type TValidSkUnits = 's' | 'Hz' | 'm3' | 'm3/s' | 'kg/s' | 'kg/m3' | 'deg' | 'rad' | 'rad/s' | 'A' | 'C' | 'V' | 'W' | 'Nm' | 'J' | 'ohm' | 'm' | 'm/s' | 'm2' | 'K' | 'Pa' | 'kg' | 'ratio' | 'm/s2' | 'rad/s2' | 'N' | 'T' | 'Lux' | 'Pa/s' | 'Pa.s' | 'unitless' | null;

/**
* Interface for a list of possible Kip value type conversions for a given path.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div [formGroup]="ctrlFormGroup" class="ctrl-grid rounded-card">
<div class="controls flex-container">
<div class="flex-label">
<mat-form-field class="full-width" appearance="outline" floatLabel="always">
<mat-form-field class="full-width">
<mat-label>Label</mat-label>
<input matInput
type="string"
Expand All @@ -10,7 +10,7 @@
</mat-form-field>
</div>
<div class="flex-settings">
<mat-form-field class="settings" appearance="outline" floatLabel="always">
<mat-form-field class="settings">
<mat-label>Type</mat-label>
<mat-select
placeholder="Select Type..."
Expand All @@ -24,7 +24,7 @@
</mat-form-field>
</div>
<div class="flex-settings">
<mat-form-field class="settings" appearance="outline" floatLabel="always">
<mat-form-field class="settings">
<mat-label>Color</mat-label>
<mat-select
placeholder="Select Color..."
Expand All @@ -38,16 +38,26 @@
</mat-select>
</mat-form-field>
</div>
<div class="flex-full-width">
<mat-checkbox
formControlName="isNumeric">
Use numeric path
</mat-checkbox>
</div>
</div>
<div class="actions">
<div class="flex-actions">
<div class="btn-grid">
<button class="up" type="button" mat-icon-button color="primary" *ngIf="controlIndex !== 0" (click)="moveCtrlUp()" aria-label="Move control up in the list">
<i class="fa-solid fa-caret-up"></i>
</button>
<button class="down" type="button" mat-icon-button color="primary" *ngIf="controlIndex !== arrayLength - 1" (click)="moveCtrlDown()" aria-label="Move control down in the list">
<i class="fa-solid fa-caret-down"></i>
</button>
@if (controlIndex !== 0) {
<button class="up" type="button" mat-icon-button color="primary" (click)="moveCtrlUp()" aria-label="Move control up in the list">
<i class="fa-solid fa-caret-up"></i>
</button>
}
@if (controlIndex !== arrayLength - 1) {
<button class="down" type="button" mat-icon-button color="primary" (click)="moveCtrlDown()" aria-label="Move control down in the list">
<i class="fa-solid fa-caret-down"></i>
</button>
}
<button class="delete" type="button" mat-icon-button color="primary" (click)="deleteControl()" aria-label="Delete control">
<i class="fa-solid fa-trash-can"></i>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@
flex-grow: 0;
flex-shrink: 0;
}

.flex-full-width {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconButton } from '@angular/material/button';
import { NgIf } from '@angular/common';
import { MatOption } from '@angular/material/core';
import { MatSelect } from '@angular/material/select';
import { MatInput } from '@angular/material/input';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatCheckboxModule } from '@angular/material/checkbox';

export interface IDeleteEventObj {
ctrlIndex: number,
Expand All @@ -15,9 +15,9 @@ export interface IDeleteEventObj {
@Component({
selector: 'boolean-control-config',
templateUrl: './boolean-control-config.component.html',
styleUrls: ['./boolean-control-config.component.css'],
styleUrls: ['./boolean-control-config.component.scss'],
standalone: true,
imports: [FormsModule, ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, NgIf, MatIconButton]
imports: [FormsModule, ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, MatIconButton, MatCheckboxModule]
})
export class BooleanControlConfigComponent implements OnInit {
@Input() ctrlFormGroup!: UntypedFormGroup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class BooleanMultiControlOptionsComponent implements OnInit, OnDestroy {
type: ['1', Validators.required],
pathID:[newUUID],
color:['text'],
isNumeric: [false],
value:[null]
}
));
Expand All @@ -72,7 +73,7 @@ export class BooleanMultiControlOptionsComponent implements OnInit, OnDestroy {
pathType: 'boolean',
isPathConfigurable: true,
showPathSkUnitsFilter: false,
pathSkUnitsFilter: null,
pathSkUnitsFilter: 'unitless',
convertUnitTo: 'unitless',
sampleTime: 500
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export class ModalWidgetConfigComponent implements OnInit {
const pathIDCtrl = fg.get('pathID') as UntypedFormControl;
if (pathIDCtrl.value == ctrl.pathID) {
fg.controls['description'].setValue(ctrl.ctrlLabel);
fg.controls['pathType'].setValue(ctrl.isNumeric ? 'number' : 'boolean');
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</div>


@if ((pathFormGroup.value.pathType == 'number' || pathFormGroup.value.pathType == 'boolean') && showPathSkUnitsFilter) {
@if (pathFormGroup.value.pathType == 'number' && showPathSkUnitsFilter) {
<div class="flex-field-fixed">
<mat-form-field floatLabel="auto" appearance="fill" class="filter-path">
<mat-label>
Expand All @@ -28,10 +28,10 @@
</div>
}
<div class="flex-field-to-100">
<mat-form-field floatLabel="auto" appearance="fill" subscriptSizing="dynamic" class="pathField">
<mat-form-field floatLabel="auto" appearance="fill" class="pathField">
<mat-label>Signal K Path</mat-label>
<input #pathInput type="text" matInput
placeholder="Select path (note dropdown limited to 50, type to use autocomplete)"
placeholder="Type to use autocomplete or select from list"
formControlName="path"
required
[matAutocomplete]="pathAutoComplete"
Expand All @@ -41,6 +41,15 @@
<span matIconSuffix class="fa-solid fa-close"></span>
</button>
}
@if (pathFormGroup.controls['path'].errors?.['required']) {
<mat-error>
Path is required. Please enter a valid Signal K path.
</mat-error>
} @else if (pathFormGroup.controls['path'].errors?.['requireMatch']) {
<mat-error>
Path not recognized. Please enter a path from the server's published list.
</mat-error>
}
<mat-autocomplete #pathAutoComplete="matAutocomplete">
@for (option of filteredPaths | async; track option) {
<mat-option [value]="option.path" style="min-height: 48px; line-height: 1.15; height: auto; padding: 8px 16px; white-space: normal;">
Expand All @@ -54,15 +63,6 @@
</mat-option>
}
</mat-autocomplete>
@if (pathFormGroup.controls['path'].errors?.['required']) {
<mat-error>
A valid Signal K path is required.
</mat-error>
} @else if (pathFormGroup.controls['path'].errors?.['requireMatch']) {
<mat-error>
The requested path does not match a path published by the server.
</mat-error>
}
</mat-form-field>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import { DataService } from '../../core/services/data.service';
import { IPathMetaData } from "../../core/interfaces/app-interfaces";
import { IConversionPathList, ISkBaseUnit, UnitsService } from '../../core/services/units.service';
import { UntypedFormGroup, UntypedFormControl, Validators, ValidatorFn, AbstractControl, ValidationErrors, FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
import { debounceTime, map, startWith } from 'rxjs/operators';
import { BehaviorSubject, Subscription } from 'rxjs'
import { debounce, map, startWith } from 'rxjs/operators';
import { BehaviorSubject, Subscription, timer } from 'rxjs'
import { MatSelect } from '@angular/material/select';
import { MatOption, MatOptgroup } from '@angular/material/core';
import { MatIconButton } from '@angular/material/button';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInput } from '@angular/material/input';
import { MatFormField, MatLabel, MatSuffix, MatError } from '@angular/material/form-field';
import { AsyncPipe } from '@angular/common';
import { start } from 'repl';


function requirePathMatch(allPathsAndMeta: IPathMetaData[]): ValidatorFn {
function requirePathMatch(getPaths: () => IPathMetaData[]): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const allPathsAndMeta = getPaths();
const pathFound = allPathsAndMeta.some(array => array.path === control.value);
return pathFound ? null : { requireMatch: true };
};
Expand Down Expand Up @@ -44,19 +46,19 @@ export class ModalPathControlConfigComponent implements OnInit, OnChanges, OnDes
public showPathSkUnitsFilter: boolean = false;
public pathSkUnitsFilterControl = new FormControl<ISkBaseUnit | null>(null);
public pathSkUnitsFiltersList: ISkBaseUnit[];
public readonly unitlessUnit: ISkBaseUnit = {unit: 'unitless', properties: {display: '(null)', quantity: 'Unitless', quantityDisplay: '(null)', description: '', }};

constructor(
private DataService: DataService,
private data: DataService,
private units: UnitsService
) { }

ngOnInit() {
this.getPaths(this.filterSelfPaths);

// Path Unit filter setup
this.pathSkUnitsFiltersList = this.units.skBaseUnits.sort((a, b) => {
return a.properties.quantity > b.properties.quantity ? 1 : -1;
});
this.pathSkUnitsFiltersList.unshift(this.unitlessUnit);

if (this.pathFormGroup.value.pathSkUnitsFilter) {
this.pathSkUnitsFilterControl.setValue(this.pathSkUnitsFiltersList.find(item => item.unit === this.pathFormGroup.value.pathSkUnitsFilter), {onlySelf: true});
Expand All @@ -67,88 +69,72 @@ export class ModalPathControlConfigComponent implements OnInit, OnChanges, OnDes
}

// add path validator fn and validate
this.pathFormGroup.controls['path'].setValidators([Validators.required, requirePathMatch(this.availablePaths)]);
this.pathFormGroup.controls['path'].setValidators([Validators.required, requirePathMatch(() => this.getPaths())]);
this.pathFormGroup.controls['path'].updateValueAndValidity({onlySelf: true, emitEvent: false});
this.pathFormGroup.controls['path'].valid ? this.enableFormFields(true) : this.disablePathFields();

// If SampleTime control is not present because the path property is missing, add it.
if (!this.pathFormGroup.controls['sampleTime']) {
this.pathFormGroup.addControl('sampleTime', new UntypedFormControl('500', Validators.required));
this.pathFormGroup.controls['sampleTime'].updateValueAndValidity();
this.pathFormGroup.controls['sampleTime'].updateValueAndValidity({onlySelf: true, emitEvent: false});
}

// Populate sources and units for this path (or just the current or default setting if we know nothing about the path)
this.updateSourcesAndUnits();

// subscribe to path formControl changes
this.pathValueChange$ = this.pathFormGroup.controls['path'].valueChanges.pipe(
debounceTime(200),
debounce(value => value === '' ? timer(0) : timer(350)),
startWith(''),
map(value => this._filterPaths(value || '')))
map(value => this.filterPaths(value || '')))
.subscribe((paths) => {
this.filteredPaths.next(paths);
if (this.pathFormGroup.controls['path'].valid) {
this.enableFormFields(true);
} else {
this.disablePathFields();
}
this.pathFormGroup.updateValueAndValidity();
});
this.pathFormGroup.controls['path'].valid ? this.enableFormFields(true) : this.disablePathFields();
}
);

this.pathFormGroup.controls['pathType'].valueChanges.subscribe((pathType) => {
this.pathSkUnitsFilterControl.setValue(this.unitlessUnit);
this.pathFormGroup.controls['path'].updateValueAndValidity();
});
}

ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
//subscribe to filterSelfPaths parent formControl changes
if (changes['filterSelfPaths'] && !changes['filterSelfPaths'].firstChange) {
this.getPaths(this.filterSelfPaths);
} else if (changes['pathFormGroup'] && !changes['pathFormGroup'].firstChange) {
this.pathFormGroup.updateValueAndValidity();
}
else if (changes['pathFormGroup']) {
this.pathFormGroup.updateValueAndValidity();
} else {
console.error('[modal-path-selector] Unmapped OnChange event')
this.pathFormGroup.controls['path'].updateValueAndValidity();
}
}

private getPaths(isOnlySef: boolean) {
this.availablePaths = this.DataService.getPathsAndMetaByType(this.pathFormGroup.value.pathType, isOnlySef).sort();
private getPaths(): IPathMetaData[] {
const pathType = this.pathFormGroup.controls['pathType'].value;
const filterSelfPaths = this.filterSelfPaths;
return this.data.getPathsAndMetaByType(pathType, filterSelfPaths).sort();
}

private _filterPaths(value: string): IPathMetaData[] {
const filterValue = value.toLowerCase();

let filteredPaths = this.availablePaths;
public filterPaths(searchString: string) {
const filterString = searchString.toLowerCase();
let filteredPaths = this.getPaths();

// If a unit filter is set, apply it first
if (this.pathSkUnitsFilterControl.value != null) {
filteredPaths = filteredPaths.filter(item =>
item.meta && item.meta.units && item.meta.units === this.pathSkUnitsFilterControl.value.unit
(item.meta && item.meta.units && item.meta.units === this.pathSkUnitsFilterControl.value.unit) ||
(!item.meta || !item.meta.units) && this.pathSkUnitsFilterControl.value.unit === 'unitless'
);
}

// Then filter based on string
filteredPaths = filteredPaths.filter(item => item.path.toLowerCase().includes(filterValue));

return filteredPaths;
}

private updateSourcesAndUnits() {
if ((!this.pathFormGroup.value.path) || (this.pathFormGroup.value.path == '') || (!this.pathFormGroup.controls['path'].valid)) {
this.disablePathFields();
} else {
this.enableFormFields();
}
// Then filter based on the path
filteredPaths = filteredPaths.filter(item => item.path.toLowerCase().includes(filterString));
this.filteredPaths.next(filteredPaths);
}

private enableFormFields(setValues?: boolean): void {
let pathObject = this.DataService.getPathObject(this.pathFormGroup.controls['path'].value);
let pathObject = this.data.getPathObject(this.pathFormGroup.controls['path'].value);
if (pathObject != null) {
this.pathFormGroup.controls['sampleTime'].enable({onlySelf: true});
this.pathFormGroup.controls['sampleTime'].enable({onlySelf: false});
if (this.pathFormGroup.controls['pathType'].value == 'number') { // convertUnitTo control not present unless pathType is number
this.unitList = this.units.getConversionsForPath(this.pathFormGroup.controls['path'].value); // array of Group or Groups: "angle", "speed", etc...
if (setValues) {
this.pathFormGroup.controls['convertUnitTo'].setValue(this.unitList.default, {onlySelf: true});
}
this.pathFormGroup.controls['convertUnitTo'].enable({onlySelf: true});
this.pathFormGroup.controls['convertUnitTo'].enable({onlySelf: false});
}

if (Object.keys(pathObject.sources).length == 1) {
Expand All @@ -164,7 +150,7 @@ export class ModalPathControlConfigComponent implements OnInit, OnChanges, OnDes
this.pathFormGroup.controls['source'].reset();
}
}
this.pathFormGroup.controls['source'].enable({onlySelf: true});
this.pathFormGroup.controls['source'].enable({onlySelf: false});
} else {
// we don't know this path. Maybe and old saved path...
this.disablePathFields();
Expand All @@ -173,11 +159,11 @@ export class ModalPathControlConfigComponent implements OnInit, OnChanges, OnDes

private disablePathFields(): void {
this.pathFormGroup.controls['source'].reset('', {onlySelf: true});
this.pathFormGroup.controls['source'].disable({onlySelf: true});
this.pathFormGroup.controls['sampleTime'].disable({onlySelf: true});
this.pathFormGroup.controls['source'].disable({onlySelf: false});
this.pathFormGroup.controls['sampleTime'].disable({onlySelf: false});
if (this.pathFormGroup.controls['pathType'].value == 'number') { // convertUnitTo control not present unless pathType is number
this.pathFormGroup.controls['convertUnitTo'].reset('', {onlySelf: true});
this.pathFormGroup.controls['convertUnitTo'].disable({onlySelf: true});
this.pathFormGroup.controls['convertUnitTo'].disable({onlySelf: false});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class PathsOptionsComponent implements OnInit, OnChanges {
source: [newPath.source, Validators.required],
pathType: [newPath.pathType],
isPathConfigurable: [newPath.isPathConfigurable],
showPathSkUnitsFilter: [newPath.showPathSkUnitsFilter],
pathSkUnitsFilter: [newPath.pathSkUnitsFilter],
convertUnitTo: [newPath.convertUnitTo],
sampleTime: [newPath.sampleTime]
})
Expand Down
Loading