16 changed files with 848 additions and 10 deletions
@ -0,0 +1,224 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2020 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<section [formGroup]="alarmScheduleForm" fxLayout="column"> |
||||
|
<mat-form-field class="mat-block" hideRequiredMarker floatLabel="always"> |
||||
|
<mat-label> </mat-label> |
||||
|
<mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}"> |
||||
|
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType"> |
||||
|
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="alarmScheduleForm.get('type').hasError('required')"> |
||||
|
{{ 'device-profile.schedule-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div *ngIf="alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME"> |
||||
|
<tb-timezone-select |
||||
|
[defaultTimezone]="defaultTimezone" |
||||
|
required |
||||
|
formControlName="timezone"> |
||||
|
</tb-timezone-select> |
||||
|
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> |
||||
|
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> |
||||
|
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="16px" style="padding-bottom: 16px;"> |
||||
|
<div fxLayout="row" fxLayoutGap="16px"> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(0)"> |
||||
|
{{ 'device-profile.schedule-day.monday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(1)"> |
||||
|
{{ 'device-profile.schedule-day.tuesday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(2)"> |
||||
|
{{ 'device-profile.schedule-day.wednesday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(3)"> |
||||
|
{{ 'device-profile.schedule-day.thursday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="16px"> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(4)"> |
||||
|
{{ 'device-profile.schedule-day.friday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(5)"> |
||||
|
{{ 'device-profile.schedule-day.saturday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<mat-checkbox [formControl]="weeklyRepeatControl(6)"> |
||||
|
{{ 'device-profile.schedule-day.sunday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> |
||||
|
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</section> |
||||
|
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> |
||||
|
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> |
||||
|
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap.gt-sm="16px" formArrayName="items"> |
||||
|
<div fxLayout="column" fxFlex fxFlex.gt-sm="50"> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 0)"> |
||||
|
{{ 'device-profile.schedule-day.monday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 1)"> |
||||
|
{{ 'device-profile.schedule-day.tuesday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 2)"> |
||||
|
{{ 'device-profile.schedule-day.wednesday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 3)"> |
||||
|
{{ 'device-profile.schedule-day.thursday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="column" fxFlex fxFlex.gt-sm="50"> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 4)"> |
||||
|
{{ 'device-profile.schedule-day.friday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 5)"> |
||||
|
{{ 'device-profile.schedule-day.saturday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 6)"> |
||||
|
{{ 'device-profile.schedule-day.sunday' | translate }} |
||||
|
</mat-checkbox> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxFlex> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex="100px"> |
||||
|
<mat-label translate>device-profile.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,259 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormArray, |
||||
|
FormBuilder, |
||||
|
FormControl, |
||||
|
FormGroup, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models'; |
||||
|
import { isDefined, isDefinedAndNotNull } from '@core/utils'; |
||||
|
import * as _moment from 'moment-timezone'; |
||||
|
import { MatCheckboxChange } from '@angular/material/checkbox'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-schedule', |
||||
|
templateUrl: './alarm-schedule.component.html', |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmScheduleComponent), |
||||
|
multi: true |
||||
|
}, { |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AlarmScheduleComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { |
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
alarmScheduleForm: FormGroup; |
||||
|
|
||||
|
defaultTimezone = _moment.tz.guess(); |
||||
|
|
||||
|
alarmScheduleTypes = Object.keys(AlarmScheduleType); |
||||
|
alarmScheduleType = AlarmScheduleType; |
||||
|
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; |
||||
|
|
||||
|
private modelValue: AlarmSchedule; |
||||
|
|
||||
|
private defaultItems = Array.from({length: 7}, (value, i) => ({ |
||||
|
enabled: true, |
||||
|
dayOfWeek: i |
||||
|
})); |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.alarmScheduleForm = this.fb.group({ |
||||
|
type: [AlarmScheduleType.ANY_TIME, Validators.required], |
||||
|
timezone: [null, Validators.required], |
||||
|
daysOfWeek: this.fb.array(new Array(7).fill(false)), |
||||
|
startsOn: [0, Validators.required], |
||||
|
endsOn: [0, Validators.required], |
||||
|
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i))) |
||||
|
}); |
||||
|
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { |
||||
|
this.alarmScheduleForm.reset({type, items: this.defaultItems}, {emitEvent: false}); |
||||
|
this.updateValidators(type, true); |
||||
|
this.alarmScheduleForm.updateValueAndValidity(); |
||||
|
}); |
||||
|
this.alarmScheduleForm.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.alarmScheduleForm.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.alarmScheduleForm.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: AlarmSchedule): void { |
||||
|
this.modelValue = value; |
||||
|
if (!isDefinedAndNotNull(this.modelValue)) { |
||||
|
this.modelValue = { |
||||
|
type: AlarmScheduleType.ANY_TIME |
||||
|
}; |
||||
|
} |
||||
|
switch (this.modelValue.type) { |
||||
|
case AlarmScheduleType.SPECIFIC_TIME: |
||||
|
let daysOfWeek = new Array(7).fill(false); |
||||
|
if (isDefined(this.modelValue.daysOfWeek)) { |
||||
|
daysOfWeek = daysOfWeek.map((item, index) => this.modelValue.daysOfWeek.indexOf(index + 1) > -1); |
||||
|
} |
||||
|
this.alarmScheduleForm.patchValue({ |
||||
|
type: this.modelValue.type, |
||||
|
timezone: this.modelValue.timezone, |
||||
|
daysOfWeek, |
||||
|
startsOn: this.timestampToTime(this.modelValue.startsOn), |
||||
|
endsOn: this.timestampToTime(this.modelValue.endsOn) |
||||
|
}, {emitEvent: false}); |
||||
|
break; |
||||
|
case AlarmScheduleType.CUSTOM: |
||||
|
if (this.modelValue.items) { |
||||
|
const alarmDays = []; |
||||
|
this.modelValue.items |
||||
|
.sort((a, b) => a.dayOfWeek - b.dayOfWeek) |
||||
|
.forEach((item, index) => { |
||||
|
if (item.enabled) { |
||||
|
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); |
||||
|
this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); |
||||
|
this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent: false}); |
||||
|
} |
||||
|
alarmDays.push({ |
||||
|
enabled: item.enabled, |
||||
|
startsOn: this.timestampToTime(item.startsOn), |
||||
|
endsOn: this.timestampToTime(item.endsOn) |
||||
|
}); |
||||
|
}); |
||||
|
this.alarmScheduleForm.patchValue({ |
||||
|
type: this.modelValue.type, |
||||
|
timezone: this.modelValue.timezone, |
||||
|
items: alarmDays |
||||
|
}, {emitEvent: false}); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); |
||||
|
} |
||||
|
this.updateValidators(this.modelValue.type); |
||||
|
} |
||||
|
|
||||
|
validate(control: FormGroup): ValidationErrors | null { |
||||
|
return this.alarmScheduleForm.valid ? null : { |
||||
|
alarmScheduler: { |
||||
|
valid: false |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
weeklyRepeatControl(index: number): FormControl { |
||||
|
return (this.alarmScheduleForm.get('daysOfWeek') as FormArray).at(index) as FormControl; |
||||
|
} |
||||
|
|
||||
|
private updateValidators(type: AlarmScheduleType, changedType = false){ |
||||
|
switch (type){ |
||||
|
case AlarmScheduleType.ANY_TIME: |
||||
|
this.alarmScheduleForm.get('timezone').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('items').disable({emitEvent: false}); |
||||
|
break; |
||||
|
case AlarmScheduleType.SPECIFIC_TIME: |
||||
|
this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('daysOfWeek').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('startsOn').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('endsOn').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('items').disable({emitEvent: false}); |
||||
|
break; |
||||
|
case AlarmScheduleType.CUSTOM: |
||||
|
this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); |
||||
|
if (changedType) { |
||||
|
this.alarmScheduleForm.get('items').enable({emitEvent: false}); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value = this.alarmScheduleForm.value; |
||||
|
if (this.modelValue) { |
||||
|
if (isDefined(value.daysOfWeek)) { |
||||
|
value.daysOfWeek = value.daysOfWeek |
||||
|
.map((day: boolean, index: number) => day ? index + 1 : null) |
||||
|
.filter(day => !!day); |
||||
|
} |
||||
|
if (isDefined(value.startsOn) && value.startsOn !== 0) { |
||||
|
value.startsOn = this.timeToTimestamp(value.startsOn); |
||||
|
} |
||||
|
if (isDefined(value.endsOn) && value.endsOn !== 0) { |
||||
|
value.endsOn = this.timeToTimestamp(value.endsOn); |
||||
|
} |
||||
|
if (isDefined(value.items)){ |
||||
|
value.items = this.alarmScheduleForm.getRawValue().items; |
||||
|
value.items = value.items.map((item) => { |
||||
|
return { ...item, startsOn: this.timeToTimestamp(item.startsOn), endsOn: this.timeToTimestamp(item.endsOn)}; |
||||
|
}); |
||||
|
} |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private timeToTimestamp(date: Date | number): number { |
||||
|
if (typeof date === 'number' || date === null) { |
||||
|
return 0; |
||||
|
} |
||||
|
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); |
||||
|
} |
||||
|
|
||||
|
private timestampToTime(time = 0): Date { |
||||
|
return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); |
||||
|
} |
||||
|
|
||||
|
private defaultItemsScheduler(index): FormGroup { |
||||
|
return this.fb.group({ |
||||
|
enabled: [true], |
||||
|
dayOfWeek: [index], |
||||
|
startsOn: [0, Validators.required], |
||||
|
endsOn: [0, Validators.required] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
changeCustomScheduler($event: MatCheckboxChange, index: number) { |
||||
|
const value = $event.checked; |
||||
|
if (value) { |
||||
|
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); |
||||
|
this.itemsSchedulerForm.at(index).get('endsOn').enable(); |
||||
|
} else { |
||||
|
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); |
||||
|
this.itemsSchedulerForm.at(index).get('endsOn').disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private get itemsSchedulerForm(): FormArray { |
||||
|
return this.alarmScheduleForm.get('items') as FormArray; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2020 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block"> |
||||
|
<mat-label translate>timezone.timezone</mat-label> |
||||
|
<input matInput type="text" placeholder="{{ 'timezone.select-timezone' | translate }}" |
||||
|
#timezoneInput |
||||
|
formControlName="timezone" |
||||
|
(focusin)="onFocus()" |
||||
|
[required]="required" |
||||
|
[matAutocomplete]="timezoneAutocomplete"> |
||||
|
<button *ngIf="selectTimezoneFormGroup.get('timezone').value && !disabled" |
||||
|
type="button" style="margin-right: 1px" |
||||
|
matSuffix mat-button mat-icon-button aria-label="Clear" |
||||
|
(mousedown)="ignoreClosePanel = true" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete class="tb-autocomplete" |
||||
|
#timezoneAutocomplete="matAutocomplete" |
||||
|
(closed)="onPanelClosed()" |
||||
|
(optionSelected)="ignoreClosePanel = true" |
||||
|
[displayWith]="displayTimezoneFn"> |
||||
|
<mat-option *ngFor="let timezone of filteredTimezones | async" [value]="timezone"> |
||||
|
<span [innerHTML]="displayTimezoneFn(timezone) | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredTimezones | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="selectTimezoneFormGroup.get('timezone').hasError('required')"> |
||||
|
{{ 'timezone.timezone-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,221 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { map, mergeMap, share, tap } from 'rxjs/operators'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@app/core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import * as _moment from 'moment-timezone'; |
||||
|
import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
||||
|
|
||||
|
interface TimezoneInfo { |
||||
|
id: string; |
||||
|
name: string; |
||||
|
offset: string; |
||||
|
nOffset: number; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timezone-select', |
||||
|
templateUrl: './timezone-select.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => TimezoneSelectComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
||||
|
|
||||
|
selectTimezoneFormGroup: FormGroup; |
||||
|
|
||||
|
modelValue: string | null; |
||||
|
|
||||
|
defaultTimezoneId: string = null; |
||||
|
|
||||
|
defaultTimezoneInfo: TimezoneInfo = null; |
||||
|
|
||||
|
timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => { |
||||
|
const tz = _moment.tz(zoneName); |
||||
|
return { |
||||
|
id: zoneName, |
||||
|
name: zoneName.replace(/_/g, ' '), |
||||
|
offset: `UTC${tz.format('Z')}`, |
||||
|
nOffset: tz.utcOffset() |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
@Input() |
||||
|
set defaultTimezone(timezone: string) { |
||||
|
if (this.defaultTimezoneId !== timezone) { |
||||
|
this.defaultTimezoneId = timezone; |
||||
|
if (this.defaultTimezoneId) { |
||||
|
this.defaultTimezoneInfo = |
||||
|
this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId); |
||||
|
} else { |
||||
|
this.defaultTimezoneInfo = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
this.requiredValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@ViewChild('timezoneInput', {static: true, read: MatAutocompleteTrigger}) timezoneInputTrigger: MatAutocompleteTrigger; |
||||
|
|
||||
|
filteredTimezones: Observable<Array<TimezoneInfo>>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
ignoreClosePanel = false; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
private ngZone: NgZone, |
||||
|
private fb: FormBuilder) { |
||||
|
this.selectTimezoneFormGroup = this.fb.group({ |
||||
|
timezone: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.filteredTimezones = this.selectTimezoneFormGroup.get('timezone').valueChanges |
||||
|
.pipe( |
||||
|
tap(value => { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = value.id; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
if (value === null) { |
||||
|
this.clear(); |
||||
|
} |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
||||
|
mergeMap(name => this.fetchTimezones(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.selectTimezoneFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.selectTimezoneFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string | null): void { |
||||
|
this.searchText = ''; |
||||
|
let foundTimezone: TimezoneInfo = null; |
||||
|
if (value !== null) { |
||||
|
foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value); |
||||
|
} |
||||
|
if (foundTimezone !== null) { |
||||
|
this.modelValue = value; |
||||
|
this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); |
||||
|
} else { |
||||
|
if (this.defaultTimezoneInfo) { |
||||
|
this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false}); |
||||
|
setTimeout(() => { |
||||
|
this.updateView(this.defaultTimezoneInfo.id); |
||||
|
}, 0); |
||||
|
} else { |
||||
|
this.modelValue = null; |
||||
|
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.selectTimezoneFormGroup.get('timezone').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onPanelClosed() { |
||||
|
if (this.ignoreClosePanel) { |
||||
|
this.ignoreClosePanel = false; |
||||
|
} else { |
||||
|
if (!this.modelValue && this.defaultTimezoneInfo) { |
||||
|
this.ngZone.run(() => { |
||||
|
this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true}); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView(value: string | null) { |
||||
|
if (this.modelValue !== value) { |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayTimezoneFn(timezone?: TimezoneInfo): string | undefined { |
||||
|
return timezone ? `${timezone.name} (${timezone.offset})` : undefined; |
||||
|
} |
||||
|
|
||||
|
fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { |
||||
|
this.searchText = searchText; |
||||
|
let result = this.timezones; |
||||
|
if (searchText && searchText.length) { |
||||
|
result = this.timezones.filter((timezoneInfo) => |
||||
|
timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())); |
||||
|
} |
||||
|
return of(result); |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.timezoneInputTrigger.openPanel(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue