20 changed files with 889 additions and 266 deletions
@ -0,0 +1,33 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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. |
||||
|
|
||||
|
--> |
||||
|
<form class="tb-add-quick-link-dialog" [formGroup]="addQuickLinkFormGroup"> |
||||
|
<mat-toolbar style="background: transparent;"> |
||||
|
<h2 translate>widgets.quick-links.add-link-title</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<tb-quick-link formControlName="link" [addOnly]="true" |
||||
|
(quickLinkAdded)="add($event)"> |
||||
|
</tb-quick-link> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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. |
||||
|
*/ |
||||
|
:host { |
||||
|
.tb-add-quick-link-dialog { |
||||
|
width: 480px; |
||||
|
.mat-toolbar-single-row { |
||||
|
padding: 0 24px; |
||||
|
} |
||||
|
h2 { |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
.mat-icon { |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, OnInit, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-add-quick-link-dialog', |
||||
|
templateUrl: './add-quick-link-dialog.component.html', |
||||
|
styleUrls: ['./add-quick-link-dialog.component.scss'] |
||||
|
}) |
||||
|
export class AddQuickLinkDialogComponent extends |
||||
|
DialogComponent<AddQuickLinkDialogComponent, string> implements OnInit { |
||||
|
|
||||
|
addQuickLinkFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<AddQuickLinkDialogComponent, string>, |
||||
|
public fb: UntypedFormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.addQuickLinkFormGroup = this.fb.group({ |
||||
|
link: [null, [Validators.required]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(link: string): void { |
||||
|
this.dialogRef.close(link); |
||||
|
} |
||||
|
} |
||||
@ -1,63 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
Copyright © 2016-2023 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. |
|
||||
|
|
||||
--> |
|
||||
<form class="tb-edit-doc-links-dialog" [formGroup]="editDocLinksFormGroup"> |
|
||||
<mat-toolbar style="background: transparent;"> |
|
||||
<h2 translate>widgets.documentation.title</h2> |
|
||||
<span fxFlex></span> |
|
||||
<button mat-icon-button |
|
||||
(click)="close()" |
|
||||
type="button"> |
|
||||
<mat-icon class="material-icons">close</mat-icon> |
|
||||
</button> |
|
||||
</mat-toolbar> |
|
||||
<div mat-dialog-content> |
|
||||
<div class="tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
|
||||
(cdkDropListDropped)="docLinkDrop($event)" [cdkDropListDisabled]="editMode || addMode"> |
|
||||
<div cdkDrag class="tb-draggable" *ngFor="let docLinkControl of docLinksFormArray().controls; trackBy: trackByDocLink; |
|
||||
let $index = index; last as isLast;" |
|
||||
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
|
||||
<tb-doc-link fxFlex #docLinkComponent [formControl]="docLinkControl" |
|
||||
[disableEdit]="editMode || addMode" |
|
||||
(editModeChanged)="editMode = $event" |
|
||||
(docLinkUpdated)="update()" |
|
||||
(docLinkDeleted)="deleteLink($index)"> |
|
||||
</tb-doc-link> |
|
||||
<div *ngIf="!docLinkComponent.isEditing()" |
|
||||
cdkDragHandle |
|
||||
matTooltip="{{ 'action.drag' | translate }}" |
|
||||
matTooltipPosition="above" |
|
||||
class="tb-drag-handle"> |
|
||||
<mat-icon>drag_indicator</mat-icon> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div mat-dialog-actions> |
|
||||
<div *ngIf="!editMode && !addMode" fxFlex class="tb-add-doc-button" |
|
||||
matTooltip="{{ 'widgets.documentation.add-link' | translate }}" |
|
||||
matTooltipPosition="above" |
|
||||
(click)="addLink()"> |
|
||||
<mat-icon class="tb-add-icon">add</mat-icon> |
|
||||
</div> |
|
||||
<tb-doc-link *ngIf="addMode" fxFlex [(ngModel)]="addingDocLink" |
|
||||
[ngModelOptions]="{standalone: true}" |
|
||||
(docLinkAdded)="linkAdded($event)" |
|
||||
(docLinkAddCanceled)="addMode = false"> |
|
||||
</tb-doc-link> |
|
||||
</div> |
|
||||
</form> |
|
||||
@ -1,114 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2023 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, Inject, OnInit } from '@angular/core'; |
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|
||||
import { Store } from '@ngrx/store'; |
|
||||
import { AppState } from '@core/core.state'; |
|
||||
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|
||||
import { DialogComponent } from '@shared/components/dialog.component'; |
|
||||
import { Router } from '@angular/router'; |
|
||||
import { DocumentationLink, DocumentationLinks } from '@shared/models/user-settings.models'; |
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
|
||||
import { UserSettingsService } from '@core/http/user-settings.service'; |
|
||||
|
|
||||
export interface EditDocLinksDialogData { |
|
||||
docLinks: DocumentationLinks; |
|
||||
} |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-edit-doc-links-dialog', |
|
||||
templateUrl: './edit-doc-links-dialog.component.html', |
|
||||
styleUrls: ['./edit-doc-links-dialog.component.scss'] |
|
||||
}) |
|
||||
export class EditDocLinksDialogComponent extends |
|
||||
DialogComponent<EditDocLinksDialogComponent, boolean> implements OnInit { |
|
||||
|
|
||||
updated = false; |
|
||||
addMode = false; |
|
||||
editMode = false; |
|
||||
|
|
||||
docLinks = this.data.docLinks; |
|
||||
addingDocLink: Partial<DocumentationLink>; |
|
||||
|
|
||||
editDocLinksFormGroup: UntypedFormGroup; |
|
||||
|
|
||||
constructor(protected store: Store<AppState>, |
|
||||
protected router: Router, |
|
||||
@Inject(MAT_DIALOG_DATA) public data: EditDocLinksDialogData, |
|
||||
public dialogRef: MatDialogRef<EditDocLinksDialogComponent, boolean>, |
|
||||
public fb: UntypedFormBuilder, |
|
||||
private userSettingsService: UserSettingsService) { |
|
||||
super(store, router, dialogRef); |
|
||||
} |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
const docLinksControls: Array<AbstractControl> = []; |
|
||||
for (const docLink of this.docLinks.links) { |
|
||||
docLinksControls.push(this.fb.control(docLink, [Validators.required])); |
|
||||
} |
|
||||
this.editDocLinksFormGroup = this.fb.group({ |
|
||||
links: this.fb.array(docLinksControls) |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
docLinksFormArray(): UntypedFormArray { |
|
||||
return this.editDocLinksFormGroup.get('links') as UntypedFormArray; |
|
||||
} |
|
||||
|
|
||||
trackByDocLink(index: number, docLinkControl: AbstractControl): any { |
|
||||
return docLinkControl; |
|
||||
} |
|
||||
|
|
||||
docLinkDrop(event: CdkDragDrop<string[]>) { |
|
||||
const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray; |
|
||||
const docLink = docLinksArray.at(event.previousIndex); |
|
||||
docLinksArray.removeAt(event.previousIndex); |
|
||||
docLinksArray.insert(event.currentIndex, docLink); |
|
||||
this.update(); |
|
||||
} |
|
||||
|
|
||||
addLink() { |
|
||||
this.addingDocLink = { icon: 'notifications' }; |
|
||||
this.addMode = true; |
|
||||
} |
|
||||
|
|
||||
linkAdded(docLink: DocumentationLink) { |
|
||||
this.addMode = false; |
|
||||
const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray; |
|
||||
const docLinkControl = this.fb.control(docLink, [Validators.required]); |
|
||||
docLinksArray.push(docLinkControl); |
|
||||
this.update(); |
|
||||
} |
|
||||
|
|
||||
deleteLink(index: number) { |
|
||||
(this.editDocLinksFormGroup.get('links') as UntypedFormArray).removeAt(index); |
|
||||
this.update(); |
|
||||
} |
|
||||
|
|
||||
update() { |
|
||||
if (this.editDocLinksFormGroup.valid) { |
|
||||
const docLinks: DocumentationLinks = this.editDocLinksFormGroup.value; |
|
||||
this.userSettingsService.updateDocumentationLinks(docLinks).subscribe(() => { |
|
||||
this.updated = true; |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
close(): void { |
|
||||
this.dialogRef.close(this.updated); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,95 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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. |
||||
|
|
||||
|
--> |
||||
|
<form class="tb-edit-links-dialog" [formGroup]="editLinksFormGroup"> |
||||
|
<mat-toolbar style="background: transparent;"> |
||||
|
<h2>{{ (mode === 'docs' ? 'widgets.documentation.title' : 'widgets.quick-links.title') | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="close()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<div class="tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
||||
|
(cdkDropListDropped)="linkDrop($event)" [cdkDropListDisabled]="editMode || addMode"> |
||||
|
<div cdkDrag class="tb-draggable" *ngFor="let linkControl of linksFormArray().controls; trackBy: trackByLink; |
||||
|
let $index = index; last as isLast;" |
||||
|
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
||||
|
<ng-container [ngSwitch]="mode"> |
||||
|
<ng-template [ngSwitchCase]="'docs'"> |
||||
|
<tb-doc-link fxFlex #docLink [formControl]="linkControl" |
||||
|
[disableEdit]="editMode || addMode" |
||||
|
(editModeChanged)="editMode = $event" |
||||
|
(docLinkUpdated)="update()" |
||||
|
(docLinkDeleted)="deleteLink($index)"> |
||||
|
</tb-doc-link> |
||||
|
<ng-container *ngIf="!docLink.isEditing()"> |
||||
|
<ng-container *ngTemplateOutlet="dragHandle"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="'quickLinks'"> |
||||
|
<tb-quick-link fxFlex #quickLink [formControl]="linkControl" |
||||
|
[disableEdit]="editMode || addMode" |
||||
|
(editModeChanged)="editMode = $event" |
||||
|
(quickLinkUpdated)="update()" |
||||
|
(quickLinkDeleted)="deleteLink($index)"> |
||||
|
</tb-quick-link> |
||||
|
<ng-container *ngIf="!quickLink.isEditing()"> |
||||
|
<ng-container *ngTemplateOutlet="dragHandle"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
<ng-template #dragHandle> |
||||
|
<div cdkDragHandle |
||||
|
matTooltip="{{ 'action.drag' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-drag-handle"> |
||||
|
<mat-icon>drag_indicator</mat-icon> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions> |
||||
|
<div *ngIf="!editMode && !addMode" fxFlex class="tb-add-link-button" |
||||
|
matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="addLink()"> |
||||
|
<mat-icon class="tb-add-icon">add</mat-icon> |
||||
|
</div> |
||||
|
<ng-container *ngIf="addMode"> |
||||
|
<ng-container [ngSwitch]="mode"> |
||||
|
<ng-template [ngSwitchCase]="'docs'"> |
||||
|
<tb-doc-link fxFlex [(ngModel)]="addingLink" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
(docLinkAdded)="linkAdded($event)" |
||||
|
(docLinkAddCanceled)="addMode = false"> |
||||
|
</tb-doc-link> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="'quickLinks'"> |
||||
|
<tb-quick-link fxFlex [(ngModel)]="addingLink" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
(quickLinkAdded)="linkAdded($event)" |
||||
|
(quickLinkAddCanceled)="addMode = false"> |
||||
|
</tb-quick-link> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,122 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, Inject, OnInit } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DocumentationLink, DocumentationLinks, QuickLinks } from '@shared/models/user-settings.models'; |
||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
||||
|
import { UserSettingsService } from '@core/http/user-settings.service'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
export interface EditLinksDialogData { |
||||
|
mode: 'docs' | 'quickLinks'; |
||||
|
links: DocumentationLinks | QuickLinks; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-edit-links-dialog', |
||||
|
templateUrl: './edit-links-dialog.component.html', |
||||
|
styleUrls: ['./edit-links-dialog.component.scss'] |
||||
|
}) |
||||
|
export class EditLinksDialogComponent extends |
||||
|
DialogComponent<EditLinksDialogComponent, boolean> implements OnInit { |
||||
|
|
||||
|
updated = false; |
||||
|
addMode = false; |
||||
|
editMode = false; |
||||
|
|
||||
|
links = this.data.links; |
||||
|
mode = this.data.mode; |
||||
|
addingLink: Partial<DocumentationLink> | string; |
||||
|
|
||||
|
editLinksFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EditLinksDialogData, |
||||
|
public dialogRef: MatDialogRef<EditLinksDialogComponent, boolean>, |
||||
|
public fb: UntypedFormBuilder, |
||||
|
private userSettingsService: UserSettingsService) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
const linksControls: Array<AbstractControl> = []; |
||||
|
for (const link of this.links.links) { |
||||
|
linksControls.push(this.fb.control(link, [Validators.required])); |
||||
|
} |
||||
|
this.editLinksFormGroup = this.fb.group({ |
||||
|
links: this.fb.array(linksControls) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
linksFormArray(): UntypedFormArray { |
||||
|
return this.editLinksFormGroup.get('links') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
trackByLink(index: number, linkControl: AbstractControl): any { |
||||
|
return linkControl; |
||||
|
} |
||||
|
|
||||
|
linkDrop(event: CdkDragDrop<string[]>) { |
||||
|
const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray; |
||||
|
const link = linksArray.at(event.previousIndex); |
||||
|
linksArray.removeAt(event.previousIndex); |
||||
|
linksArray.insert(event.currentIndex, link); |
||||
|
this.update(); |
||||
|
} |
||||
|
|
||||
|
addLink() { |
||||
|
this.addingLink = this.mode === 'docs' ? { icon: 'notifications' } : null; |
||||
|
this.addMode = true; |
||||
|
} |
||||
|
|
||||
|
linkAdded(link: DocumentationLink | string) { |
||||
|
this.addMode = false; |
||||
|
const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray; |
||||
|
const linkControl = this.fb.control(link, [Validators.required]); |
||||
|
linksArray.push(linkControl); |
||||
|
this.update(); |
||||
|
} |
||||
|
|
||||
|
deleteLink(index: number) { |
||||
|
(this.editLinksFormGroup.get('links') as UntypedFormArray).removeAt(index); |
||||
|
this.update(); |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
if (this.editLinksFormGroup.valid) { |
||||
|
let updateObservable: Observable<void>; |
||||
|
if (this.mode === 'docs') { |
||||
|
updateObservable = this.userSettingsService.updateDocumentationLinks(this.editLinksFormGroup.value); |
||||
|
} else { |
||||
|
updateObservable = this.userSettingsService.updateQuickLinks(this.editLinksFormGroup.value); |
||||
|
} |
||||
|
updateObservable.subscribe(() => { |
||||
|
this.updated = true; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(this.updated); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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. |
||||
|
*/ |
||||
|
|
||||
|
.tb-autocomplete.tb-quick-links { |
||||
|
.mat-mdc-option { |
||||
|
border-bottom: none; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
.mat-icon { |
||||
|
margin-right: 10px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
font-size: 20px; |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="row" *ngIf="addMode || editMode; else quickLinkTemplate"> |
||||
|
<div fxFlex class="tb-edit-link" [ngClass]="{'edit-mode': editMode}" [formGroup]="editQuickLinkFormGroup" fxLayout="column"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label translate>widgets.quick-links.quick-link</mat-label> |
||||
|
<input matInput type="text" |
||||
|
#linkInput |
||||
|
formControlName="link" |
||||
|
(focusin)="onFocus()" |
||||
|
required |
||||
|
[matAutocomplete]="linkAutocomplete"> |
||||
|
<mat-icon matPrefix *ngIf="quickLink && !quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon> |
||||
|
<mat-icon matPrefix *ngIf="quickLink && quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon> |
||||
|
<button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled" |
||||
|
type="button" |
||||
|
matSuffix mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete |
||||
|
class="tb-autocomplete tb-quick-links" |
||||
|
#linkAutocomplete="matAutocomplete" |
||||
|
[displayWith]="displayLinkFn"> |
||||
|
<mat-option *ngFor="let link of filteredLinks | async" [value]="link"> |
||||
|
<mat-icon *ngIf="!link.isMdiIcon">{{ link.icon }}</mat-icon> |
||||
|
<mat-icon *ngIf="link.isMdiIcon" [svgIcon]="link.icon"></mat-icon> |
||||
|
<span [innerHTML]="link.name | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get('widgets.quick-links.no-links-matching', {name: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="editQuickLinkFormGroup.get('link').hasError('required')"> |
||||
|
{{ 'widgets.quick-links.quick-link-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div *ngIf="addMode" fxLayout="row" fxLayoutAlign="end" fxLayoutGap="8px"> |
||||
|
<button *ngIf="!addOnly" mat-button color="primary" (click)="cancelAdd()">{{ 'action.cancel' | translate }}</button> |
||||
|
<button mat-raised-button color="primary" (click)="add()">{{ 'action.add' | translate }}</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons"> |
||||
|
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button> |
||||
|
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #quickLinkTemplate> |
||||
|
<div fxLayout="row"> |
||||
|
<div fxFlex class="tb-link"> |
||||
|
<div class="tb-link-container"> |
||||
|
<div class="tb-link-icon-container"> |
||||
|
<mat-icon *ngIf="!quickLink?.isMdiIcon" color="primary">{{ quickLink?.icon }}</mat-icon> |
||||
|
<mat-icon *ngIf="quickLink?.isMdiIcon" color="primary" [svgIcon]="quickLink?.icon"></mat-icon> |
||||
|
</div> |
||||
|
<div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" class="tb-edit-buttons"> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.edit' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="switchToEditMode()"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.delete' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="delete()"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -0,0 +1,318 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, |
||||
|
ElementRef, |
||||
|
EventEmitter, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
SkipSelf, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
FormGroupDirective, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
NgForm, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors |
||||
|
} from '@angular/forms'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MenuService } from '@core/services/menu.service'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { MenuSection } from '@core/services/menu.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { catchError, distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Direction } from '@shared/models/page/sort-order'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-quick-link', |
||||
|
templateUrl: './quick-link.component.html', |
||||
|
styleUrls: ['./link.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => QuickLinkComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{provide: ErrorStateMatcher, useExisting: QuickLinkComponent} |
||||
|
] |
||||
|
}) |
||||
|
export class QuickLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
addOnly = false; |
||||
|
|
||||
|
@Input() |
||||
|
disableEdit = false; |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkAdded = new EventEmitter<string>(); |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkAddCanceled = new EventEmitter<void>(); |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkUpdated = new EventEmitter<string>(); |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkDeleted = new EventEmitter<void>(); |
||||
|
|
||||
|
@Output() |
||||
|
editModeChanged = new EventEmitter<boolean>(); |
||||
|
|
||||
|
@ViewChild('linkInput', {static: false}) linkInput: ElementRef; |
||||
|
|
||||
|
filteredLinks: Observable<Array<MenuSection>>; |
||||
|
|
||||
|
private allLinksObservable$: Observable<Array<MenuSection>> = null; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
editMode = false; |
||||
|
addMode = false; |
||||
|
|
||||
|
quickLink: MenuSection; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
public editQuickLinkFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private submitted = false; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private menuService: MenuService, |
||||
|
public translate: TranslateService, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.addMode = this.addOnly; |
||||
|
this.editQuickLinkFormGroup = this.fb.group({ |
||||
|
link: [null, [this.requiredLinkValidator]] |
||||
|
}); |
||||
|
this.filteredLinks = this.editQuickLinkFormGroup.get('link').valueChanges |
||||
|
.pipe( |
||||
|
tap(value => { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = value; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : |
||||
|
((value as any).translated ? value.name : this.translate.instant(value.name))) : ''), |
||||
|
distinctUntilChanged(), |
||||
|
switchMap(name => this.fetchLinks(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
requiredLinkValidator(control: AbstractControl): ValidationErrors | null { |
||||
|
const value = control.value; |
||||
|
if (!value || typeof value === 'string') { |
||||
|
return {required: true}; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.editQuickLinkFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.editQuickLinkFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string): void { |
||||
|
if (value) { |
||||
|
this.menuService.menuLinkById(value).subscribe( |
||||
|
(link) => { |
||||
|
this.quickLink = link; |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue( |
||||
|
link, {emitEvent: false} |
||||
|
); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.quickLink = null; |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue( |
||||
|
value, {emitEvent: false} |
||||
|
); |
||||
|
if (!this.editQuickLinkFormGroup.valid) { |
||||
|
this.addMode = true; |
||||
|
this.editModeChanged.emit(true); |
||||
|
} |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
updateView(value: MenuSection | null) { |
||||
|
if (this.quickLink !== value) { |
||||
|
this.quickLink = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayLinkFn = (link?: MenuSection): string | undefined => |
||||
|
link ? ((link as any).translated ? link.name : this.translate.instant(link.name)) : undefined; |
||||
|
|
||||
|
fetchLinks(searchText?: string): Observable<Array<MenuSection>> { |
||||
|
this.searchText = searchText; |
||||
|
const pageLink = new PageLink(100, 0, searchText, { |
||||
|
property: 'name', |
||||
|
direction: Direction.ASC |
||||
|
}); |
||||
|
return this.getLinks(pageLink).pipe( |
||||
|
catchError(() => of(emptyPageData<MenuSection>())), |
||||
|
map(pageData => pageData.data) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getLinks(pageLink: PageLink): Observable<PageData<MenuSection>> { |
||||
|
return this.allLinks().pipe( |
||||
|
map((links) => pageLink.filterData(links)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
allLinks(): Observable<Array<MenuSection>> { |
||||
|
if (this.allLinksObservable$ === null) { |
||||
|
this.allLinksObservable$ = this.menuService.availableMenuLinks().pipe( |
||||
|
map((links) => { |
||||
|
const result = deepClone(links); |
||||
|
for (const link of result) { |
||||
|
link.name = this.translate.instant(link.name); |
||||
|
(link as any).translated = true; |
||||
|
} |
||||
|
return result; |
||||
|
}), |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} |
||||
|
return this.allLinksObservable$; |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.editQuickLinkFormGroup.get('link').updateValueAndValidity({onlySelf: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue(''); |
||||
|
setTimeout(() => { |
||||
|
this.linkInput.nativeElement.blur(); |
||||
|
this.linkInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
switchToEditMode() { |
||||
|
if (!this.disableEdit && !this.editMode) { |
||||
|
this.submitted = false; |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue( |
||||
|
this.quickLink, {emitEvent: false} |
||||
|
); |
||||
|
this.editMode = true; |
||||
|
this.editModeChanged.emit(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
apply() { |
||||
|
this.submitted = true; |
||||
|
this.updateModel(); |
||||
|
if (this.quickLink) { |
||||
|
this.editMode = false; |
||||
|
this.editModeChanged.emit(false); |
||||
|
this.quickLinkUpdated.next(this.quickLink.id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancelEdit() { |
||||
|
this.submitted = false; |
||||
|
this.editMode = false; |
||||
|
this.editModeChanged.emit(false); |
||||
|
} |
||||
|
|
||||
|
add() { |
||||
|
this.submitted = true; |
||||
|
this.updateModel(); |
||||
|
if (this.quickLink) { |
||||
|
if (!this.addOnly) { |
||||
|
this.addMode = false; |
||||
|
this.editModeChanged.emit(false); |
||||
|
} |
||||
|
this.quickLinkAdded.next(this.quickLink.id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancelAdd() { |
||||
|
this.editModeChanged.emit(false); |
||||
|
this.quickLinkAddCanceled.emit(); |
||||
|
} |
||||
|
|
||||
|
delete() { |
||||
|
this.quickLinkDeleted.emit(); |
||||
|
} |
||||
|
|
||||
|
isEditing() { |
||||
|
return this.editMode || this.addMode; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
if (this.quickLink) { |
||||
|
this.propagateChange(this.quickLink.id); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue