Merge branch 'feature/image-resources' of github.com:thingsboard/thingsboard into feature/image-resources

This commit is contained in:
Andrii Shvaika 2023-11-27 18:39:52 +02:00
commit 25a1c551c7
30 changed files with 628 additions and 96 deletions

View File

@ -40,8 +40,8 @@
<input matInput formControlName="left" type="number" step="1" min="0" max="100">
</mat-form-field>
<div class="tb-image-preview-container">
<div *ngIf="!safeImageUrl; else elseBlock">{{ 'dashboard.no-image' | translate }}</div>
<ng-template #elseBlock><img class="tb-image-preview" [src]="safeImageUrl" /></ng-template>
<div *ngIf="!imageUrl; else elseBlock">{{ 'dashboard.no-image' | translate }}</div>
<ng-template #elseBlock><img class="tb-image-preview" [src]="imageUrl | image | async" /></ng-template>
</div>
<mat-form-field class="rect-field">
<mat-label>Right %</mat-label>
@ -62,9 +62,9 @@
</button>
</div>
<div [formGroup]="dashboardImageFormGroup">
<tb-image-input [showPreview]="false" label="{{'dashboard.image' | translate}}"
<tb-gallery-image-input label="{{'dashboard.image' | translate}}"
formControlName="dashboardImage">
</tb-image-input>
</tb-gallery-image-input>
</div>
</fieldset>
<div *ngIf="takingScreenshot$ | async" class="taking-screenshot-progress tb-absolute-fill" fxLayout="column"

View File

@ -53,7 +53,7 @@ export class DashboardImageDialogComponent extends DialogComponent<DashboardImag
);
dashboardId: DashboardId;
safeImageUrl?: SafeUrl;
imageUrl?: string;
dashboardElement: HTMLElement;
dashboardRectFormGroup: UntypedFormGroup;
@ -165,10 +165,6 @@ export class DashboardImageDialogComponent extends DialogComponent<DashboardImag
}
private updateImage(imageUrl: string) {
if (imageUrl) {
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(imageUrl);
} else {
this.safeImageUrl = null;
}
this.imageUrl = imageUrl;
}
}

View File

@ -70,7 +70,7 @@
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</tb-states-component>
<img *ngIf="showDashboardLogo()" [src]="dashboardLogo"
<img *ngIf="showDashboardLogo()" [src]="dashboardLogo | image | async"
aria-label="dashboard_logo" class="dashboard_logo"/>
</div>
<div class="tb-dashboard-action-panel">

View File

@ -668,12 +668,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
public get dashboardLogo(): SafeUrl {
if (!this.dashboardLogoCache) {
const logo = this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo;
this.dashboardLogoCache = this.sanitizer.bypassSecurityTrustUrl(logo);
}
return this.dashboardLogoCache;
public get dashboardLogo(): string {
return this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo;
}
public showRightLayoutSwitch(): boolean {

View File

@ -55,10 +55,10 @@
<mat-slide-toggle formControlName="showDashboardLogo">
{{ 'dashboard.display-dashboard-logo' | translate }}
</mat-slide-toggle>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'dashboard.dashboard-logo-image' | translate}}"
formControlName="dashboardLogoUrl">
</tb-image-input>
</tb-gallery-image-input>
</fieldset>
<fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px">
<legend class="group-title" translate>dashboard.toolbar-settings</legend>
@ -144,10 +144,10 @@
openOnInput
formControlName="backgroundColor">
</tb-color-input>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'dashboard.background-image' | translate}}"
formControlName="backgroundImageUrl">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>dashboard.background-size-mode</mat-label>
<mat-select formControlName="backgroundSizeMode">

View File

@ -23,7 +23,7 @@
<div class="mat-content"
style="position: relative; width: 100%; height: 100%;" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="dashboardCheatSheet"
[style.backgroundImage]="backgroundImage"
[style.backgroundImage]="backgroundImage$ | async"
[ngStyle]="dashboardStyle">
<section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center"
style="display: flex; z-index: 1; pointer-events: none;"
@ -38,7 +38,7 @@
</button>
</section>
<tb-dashboard #dashboard [dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage"
[backgroundImage]="backgroundImage$ | async"
[widgets]="layoutCtx.widgets"
[widgetLayouts]="layoutCtx.widgetLayouts"
[columns]="layoutCtx.gridSettings.columns"

View File

@ -27,13 +27,15 @@ import {
IDashboardComponent,
WidgetContextMenuItem
} from '@home/models/dashboard-component.models';
import { Subscription } from 'rxjs';
import { Observable, of, Subscription } from 'rxjs';
import { Hotkey } from 'angular2-hotkeys';
import { TranslateService } from '@ngx-translate/core';
import { ItemBufferService } from '@app/core/services/item-buffer.service';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { map } from 'rxjs/operators';
@Component({
selector: 'tb-dashboard-layout',
@ -44,7 +46,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
layoutCtxValue: DashboardPageLayoutContext;
dashboardStyle: {[klass: string]: any} = null;
backgroundImage: SafeStyle | string;
backgroundImage$: Observable<SafeStyle | string>;
hotKeys: Hotkey[] = [];
@ -92,6 +94,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private itembuffer: ItemBufferService,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer) {
super(store);
this.initHotKeys();
@ -188,8 +191,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
'background-attachment': 'scroll',
'background-size': this.layoutCtx.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'};
this.backgroundImage = this.layoutCtx.gridSettings.backgroundImageUrl ?
this.sanitizer.bypassSecurityTrustStyle('url(' + this.layoutCtx.gridSettings.backgroundImageUrl + ')') : 'none';
this.backgroundImage$ = this.layoutCtx.gridSettings.backgroundImageUrl ?
this.imagePipe.transform(this.layoutCtx.gridSettings.backgroundImageUrl, {asString: true, ignoreLoadingImage: true}).pipe(
map((imageUrl) => this.sanitizer.bypassSecurityTrustStyle('url(' + imageUrl + ')'))
) : of('none');
}
reload() {

View File

@ -181,13 +181,6 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
import {
ExportWidgetsBundleDialogComponent
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent } from '@home/components/image/image-dialog.component';
import { ImagesInUseDialogComponent } from '@home/components/image/images-in-use-dialog.component';
import { ImageReferencesComponent } from '@home/components/image/image-references.component';
import { GalleryImageInputComponent } from '@home/components/image/gallery-image-input.component';
@NgModule({
declarations:
@ -332,14 +325,7 @@ import { GalleryImageInputComponent } from '@home/components/image/gallery-image
RateLimitsComponent,
RateLimitsTextComponent,
RateLimitsDetailsDialogComponent,
SendNotificationButtonComponent,
ScrollGridComponent,
ImageGalleryComponent,
UploadImageDialogComponent,
ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent,
GalleryImageInputComponent
SendNotificationButtonComponent
],
imports: [
CommonModule,
@ -477,14 +463,7 @@ import { GalleryImageInputComponent } from '@home/components/image/gallery-image
RateLimitsComponent,
RateLimitsTextComponent,
RateLimitsDetailsDialogComponent,
SendNotificationButtonComponent,
ScrollGridComponent,
ImageGalleryComponent,
UploadImageDialogComponent,
ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent,
GalleryImageInputComponent
SendNotificationButtonComponent
],
providers: [
WidgetComponentService,

View File

@ -68,6 +68,7 @@
<div *ngIf="linkType === ImageLinkType.none && !disabled" class="tb-image-select-buttons-container">
<button #browseGalleryButton
mat-stroked-button
type="button"
color="primary"
class="tb-image-select-button"
(click)="toggleGallery($event, browseGalleryButton)">
@ -75,6 +76,7 @@
<span translate>image.browse-from-gallery</span>
</button>
<button mat-stroked-button
type="button"
color="primary"
class="tb-image-select-button"
(click)="setLink($event)">

View File

@ -40,7 +40,7 @@ import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
enum ImageLinkType {
export enum ImageLinkType {
none = 'none',
base64 = 'base64',
external = 'external',

View File

@ -0,0 +1,111 @@
<!--
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 class="tb-container">
<label class="tb-title" *ngIf="label" [ngClass]="{'tb-error': !disabled && (required && !imageUrls.length), 'tb-required': !disabled && required}">{{label}}</label>
<div class="images-container" dndDropzone [dndHorizontal]="true" dndEffectAllowed="move"
[dndDisableIf]="disabled" (dndDrop)="imageDrop($event)"
fxLayout="row" fxLayoutGap="8px" [ngClass]="{'no-images': !imageUrls.length}">
<div dndPlaceholderRef class="image-card image-dnd-placeholder"></div>
<div *ngFor="let imageUrl of imageUrls; let $index = index;"
(dndStart)="imageDragStart($index)"
(dndEnd)="imageDragEnd()"
[dndDraggable]="imageUrl"
[dndDisableIf]="disabled"
dndEffectAllowed="move"
[ngClass]="{'image-dragging': dragIndex === $index}"
class="image-card" fxLayout="column">
<span class="image-title">{{ 'image-input.images' | translate }} [{{ $index }}]</span>
<div class="image-content-container" fxLayout="row">
<div dndHandle *ngIf="!disabled" class="tb-image-action-container tb-drag-handle">
<mat-icon color="primary">drag_indicator</mat-icon>
</div>
<div class="tb-image-preview-container">
<img class="tb-image-preview" [src]="imageUrl | image: {preview: true} | async" />
</div>
<div *ngIf="!disabled" class="tb-image-action-container">
<button mat-icon-button color="primary"
type="button"
(click)="clearImage($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
<div class="no-images-prompt" *ngIf="!imageUrls.length">{{ 'image-input.no-images' | translate }}</div>
</div>
<div *ngIf="!disabled" class="tb-image-select-container">
<div class="tb-image-container" *ngIf="linkType === ImageLinkType.external">
<img *ngIf="externalLinkControl.value; else noImage" class="tb-image-preview" [src]="externalLinkControl.value | image | async">
</div>
<div *ngIf="linkType === ImageLinkType.external" class="tb-image-info-container">
<div class="tb-external-image-container">
<div class="tb-external-link-label">
{{ 'image.image-link' | translate }}
</div>
<div class="tb-external-link-input-container">
<mat-form-field class="tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput [formControl]="externalLinkControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<button class="tb-image-decline-btn"
type="button"
mat-icon-button
matTooltip="{{ 'action.decline' | translate }}"
matTooltipPosition="above"
(click)="declineLink($event)">
<mat-icon>close</mat-icon>
</button>
<button class="tb-image-apply-btn"
type="button"
[disabled]="!externalLinkControl.value"
color="primary"
mat-icon-button
matTooltip="{{ 'action.apply' | translate }}"
matTooltipPosition="above"
(click)="applyLink($event)">
<mat-icon>check</mat-icon>
</button>
</div>
</div>
</div>
<div *ngIf="linkType === ImageLinkType.none" class="tb-image-select-buttons-container">
<button #browseGalleryButton
mat-stroked-button
type="button"
color="primary"
class="tb-image-select-button"
(click)="toggleGallery($event, browseGalleryButton)">
<tb-icon matButtonIcon>filter</tb-icon>
<span translate>image.browse-from-gallery</span>
</button>
<button mat-stroked-button
type="button"
color="primary"
class="tb-image-select-button"
(click)="setLink($event)">
<tb-icon matButtonIcon>link</tb-icon>
<span translate>image.set-link</span>
</button>
</div>
</div>
</div>
<ng-template #noImage>
<div class="tb-no-image">{{ 'image.no-image-selected' | translate }}</div>
</ng-template>

View File

@ -0,0 +1,225 @@
/**
* 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 "../../../../../scss/constants";
$imagesContainerHeight: 106px !default;
$selectContainerHeight: 96px !default;
$previewSize: 64px !default;
.image-card {
margin-bottom: 8px;
&.image-dnd-placeholder {
height: 82px;
width: 146px;
border: 2px dashed rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
&.image-dragging {
display: none !important;
}
.image-title {
font-size: 11px;
font-weight: 400;
line-height: 14px;
color: rgba(0, 0, 0, 0.6);
padding-bottom: 4px;
}
.image-content-container {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 4px;
height: $previewSize;
}
.tb-image-preview {
width: auto;
max-width: $previewSize - 2px;
height: auto;
max-height: $previewSize - 2px;
}
.tb-image-preview-container {
position: relative;
width: $previewSize;
height: $previewSize;
margin-top: -1px;
margin-bottom: -1px;
border: 1px solid rgba(0, 0, 0, 0.54);
.tb-image-preview {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.tb-image-action-container {
position: relative;
height: $previewSize - 2px;
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
}
}
:host {
.tb-container {
margin-top: 0;
padding: 0 0 16px;
display: flex;
flex-direction: column;
gap: 8px;
label.tb-title {
display: block;
padding-bottom: 0;
}
}
.images-container {
padding: 12px 12px 4px;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
flex-wrap: wrap;
margin-bottom: 8px;
&.no-images {
height: $imagesContainerHeight;
padding-bottom: 12px;
align-items: center;
justify-content: center;
}
}
.no-images-prompt {
font-size: 18px;
color: rgba(0, 0, 0, 0.54);
}
.tb-image-select-container {
width: 100%;
height: $selectContainerHeight;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
.tb-image-container {
width: $selectContainerHeight - 1px;
height: $selectContainerHeight - 2px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
border-right: 1px solid rgba(0, 0, 0, 0.12);
background: #fff;
overflow: hidden;
.tb-image-preview {
width: auto;
max-width: $selectContainerHeight - 2px;
height: auto;
max-height: $selectContainerHeight - 2px;
}
.tb-no-image {
text-align: center;
color: rgba(0, 0, 0, 0.38);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.4px;
}
}
.tb-image-info-container {
display: flex;
flex: 1;
align-self: stretch;
padding: 0 8px;
justify-content: flex-end;
align-items: center;
gap: 4px;
.tb-external-image-container {
display: flex;
flex: 1;
align-self: stretch;
padding: 16px 8px 0 16px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
.tb-external-link-label {
color: rgba(0, 0, 0, 0.54);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.4px;
}
.tb-external-link-input-container {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 4px;
align-self: stretch;
.tb-inline-field {
width: 100%;
}
.tb-image-decline-btn {
color: rgba(0,0,0,0.38);
}
}
}
}
.tb-image-select-buttons-container {
display: flex;
flex: 1;
align-self: stretch;
padding: 8px;
gap: 8px;
justify-content: center;
align-items: flex-start;
.tb-image-select-button {
width: 100%;
height: 100%;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
padding: 8px;
line-height: normal;
font-size: 12px;
@media #{$mat-gt-xs} {
padding: 16px;
}
.mat-icon {
width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
}
}
}
}
}

View File

@ -0,0 +1,180 @@
///
/// 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 { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, } from '@angular/forms';
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { DndDropEvent } from 'ngx-drag-drop';
import { isUndefined } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ImageLinkType } from '@home/components/image/gallery-image-input.component';
import { TbPopoverService } from '@shared/components/popover.service';
import { MatButton } from '@angular/material/button';
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
@Component({
selector: 'tb-multiple-gallery-image-input',
templateUrl: './multiple-gallery-image-input.component.html',
styleUrls: ['./multiple-gallery-image-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MultipleGalleryImageInputComponent),
multi: true
}
]
})
export class MultipleGalleryImageInputComponent extends PageComponent implements OnDestroy, ControlValueAccessor {
@Input()
label: string;
@Input()
@coerceBoolean()
required = false;
@Input()
disabled: boolean;
imageUrls: string[];
ImageLinkType = ImageLinkType;
linkType: ImageLinkType = ImageLinkType.none;
externalLinkControl = new FormControl(null);
dragIndex: number;
private propagateChange = null;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private popoverService: TbPopoverService) {
super(store);
}
ngOnDestroy() {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string[]): void {
this.reset();
this.imageUrls = value || [];
}
private updateModel() {
this.cd.markForCheck();
this.propagateChange(this.imageUrls);
}
private reset() {
this.linkType = ImageLinkType.none;
this.externalLinkControl.setValue(null, {emitEvent: false});
}
clearImage(index: number) {
this.imageUrls.splice(index, 1);
this.updateModel();
}
setLink($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.linkType = ImageLinkType.external;
}
declineLink($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.reset();
}
applyLink($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.imageUrls.push(this.externalLinkControl.value);
this.reset();
this.updateModel();
}
toggleGallery($event: Event, browseGalleryButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = browseGalleryButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
pageMode: false,
popoverMode: true,
mode: 'grid',
selectionMode: true
};
const imageGalleryPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ImageGalleryComponent, 'top', true, null,
ctx,
{},
{}, {}, true);
imageGalleryPopover.tbComponentRef.instance.imageSelected.subscribe((image) => {
imageGalleryPopover.hide();
this.imageUrls.push(image.link);
this.updateModel();
});
}
}
imageDragStart(index: number) {
setTimeout(() => {
this.dragIndex = index;
this.cd.markForCheck();
});
}
imageDragEnd() {
this.dragIndex = -1;
this.cd.markForCheck();
}
imageDrop(event: DndDropEvent) {
let index = event.index;
if (isUndefined(index)) {
index = this.imageUrls.length;
}
moveItemInArray(this.imageUrls, this.dragIndex, index);
this.dragIndex = -1;
this.updateModel();
}
}

View File

@ -78,11 +78,10 @@
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'device-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>

View File

@ -83,11 +83,10 @@
[ruleChainType]="edgeRuleChainType">
<span tb-hint>{{'asset-profile.default-edge-rule-chain-hint' | translate}}</span>
</tb-rule-chain-autocomplete>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'asset-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>asset-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>

View File

@ -108,11 +108,10 @@
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'device-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>

View File

@ -22,6 +22,14 @@ import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
import { AlarmCommentDialogComponent } from '@home/components/alarm/alarm-comment-dialog.component';
import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.component';
import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
import { ImageGalleryComponent } from '@home/components/image/image-gallery.component';
import { UploadImageDialogComponent } from '@home/components/image/upload-image-dialog.component';
import { ImageDialogComponent } from '@home/components/image/image-dialog.component';
import { ImageReferencesComponent } from '@home/components/image/image-references.component';
import { ImagesInUseDialogComponent } from '@home/components/image/images-in-use-dialog.component';
import { GalleryImageInputComponent } from '@home/components/image/gallery-image-input.component';
import { MultipleGalleryImageInputComponent } from '@home/components/image/multiple-gallery-image-input.component';
@NgModule({
providers: [
@ -32,7 +40,15 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co
AlarmDetailsDialogComponent,
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
AlarmAssigneeComponent,
ScrollGridComponent,
ImageGalleryComponent,
UploadImageDialogComponent,
ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent,
GalleryImageInputComponent,
MultipleGalleryImageInputComponent
],
imports: [
CommonModule,
@ -42,7 +58,15 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co
AlarmDetailsDialogComponent,
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
AlarmAssigneeComponent,
ScrollGridComponent,
ImageGalleryComponent,
UploadImageDialogComponent,
ImageDialogComponent,
ImageReferencesComponent,
ImagesInUseDialogComponent,
GalleryImageInputComponent,
MultipleGalleryImageInputComponent
]
})
export class SharedHomeComponentsModule { }

View File

@ -32,7 +32,7 @@ import {
WidgetUnitedMapSettings
} from './map-models';
import { Marker } from './markers';
import { Observable, of } from 'rxjs';
import { map, Observable, of, switchMap } from 'rxjs';
import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { Circle } from './circle';
@ -63,6 +63,7 @@ import {
import { MatDialog } from '@angular/material/dialog';
import { FormattedData, ReplaceInfo } from '@shared/models/widget.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { ImagePipe } from '@shared/pipe/image.pipe';
export default abstract class LeafletMap {
@ -895,16 +896,27 @@ export default abstract class LeafletMap {
const currentImage: MarkerImageInfo = this.options.useMarkerImageFunction ?
safeExecute(this.options.parsedMarkerImageFunction,
[data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage;
const imageSize = `height: ${this.options.markerImageSize || 34}px; width: ${this.options.markerImageSize || 34}px;`;
const style = currentImage ? 'background-image: url(' + currentImage.url + '); ' + imageSize : '';
this.options.icon = { icon: L.divIcon({
const imageUrl$ =
currentImage
? this.ctx.$injector.get(ImagePipe).transform(currentImage.url, {asString: true, ignoreLoadingImage: true})
: of(null);
this.options.icon$ = imageUrl$.pipe(
map((imageUrl) => {
const size = this.options.useMarkerImageFunction && currentImage ? currentImage.size : this.options.markerImageSize;
const imageSize = `height: ${size || 34}px; width: ${size || 34}px;`;
const style = imageUrl ? 'background-image: url(' + imageUrl + '); ' + imageSize : '';
return { icon: L.divIcon({
html: `<div class="arrow"
style="transform: translate(-10px, -10px)
rotate(${data.rotationAngle}deg);
${style}"><div>`
}), size: [30, 30]};
}), size: [size, size]};
})
);
this.options.icon = null;
} else {
this.options.icon = null;
this.options.icon$ = null;
}
if (this.markers.get(data.entityName)) {
m = this.updateMarker(data.entityName, data, markersData, this.options);

View File

@ -17,6 +17,7 @@
import { Datasource, FormattedData } from '@app/shared/models/widget.models';
import tinycolor from 'tinycolor2';
import { BaseIconOptions, Icon } from 'leaflet';
import { Observable } from 'rxjs';
export const DEFAULT_MAP_PAGE_SIZE = 16384;
export const DEFAULT_ZOOM_LEVEL = 8;
@ -337,6 +338,7 @@ export interface WidgetMarkersSettings extends MarkersSettings, WidgetToolipSett
currentImage: MarkerImageInfo;
tinyColor: tinycolor.Instance;
icon: MarkerIconInfo;
icon$?: Observable<MarkerIconInfo>;
}
export const defaultMarkersSettings: MarkersSettings = {

View File

@ -155,6 +155,11 @@ export class Marker {
if (this.settings.icon) {
onMarkerIconReady(this.settings.icon);
return;
} else if (this.settings.icon$) {
this.settings.icon$.subscribe((res) => {
onMarkerIconReady(res);
});
return;
}
const currentImage: MarkerImageInfo = this.settings.useMarkerImageFunction ?
safeExecute(this.settings.parsedMarkerImageFunction,

View File

@ -16,9 +16,9 @@
-->
<section class="tb-widget-settings" [formGroup]="providerSettingsFormGroup">
<tb-image-input label="{{ 'widgets.maps.image-map-background' | translate }}"
<tb-gallery-image-input label="{{ 'widgets.maps.image-map-background' | translate }}"
formControlName="mapImageUrl">
</tb-image-input>
</tb-gallery-image-input>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.image-map-background-from-entity-attribute</legend>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">

View File

@ -170,10 +170,10 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-image-input [fxShow]="!markersSettingsFormGroup.get('useMarkerImageFunction').value"
<tb-gallery-image-input [fxShow]="!markersSettingsFormGroup.get('useMarkerImageFunction').value"
label="{{ 'widgets.maps.custom-marker-image' | translate }}"
formControlName="markerImage">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field [fxShow]="!markersSettingsFormGroup.get('useMarkerImageFunction').value"
fxFlex class="mat-block">
<mat-label translate>widgets.maps.custom-marker-image-size</mat-label>
@ -186,10 +186,10 @@
functionTitle="{{ 'widgets.maps.marker-image-function' | translate }}"
helpId="widget/lib/map/marker_image_fn">
</tb-js-func>
<tb-multiple-image-input [fxShow]="markersSettingsFormGroup.get('useMarkerImageFunction').value"
<tb-multiple-gallery-image-input [fxShow]="markersSettingsFormGroup.get('useMarkerImageFunction').value"
label="{{ 'widgets.maps.marker-images' | translate }}"
formControlName="markerImages">
</tb-multiple-image-input>
</tb-multiple-gallery-image-input>
</ng-template>
</mat-expansion-panel>
</fieldset>

View File

@ -70,10 +70,10 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-image-input [fxShow]="!tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').value"
<tb-gallery-image-input [fxShow]="!tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').value"
label="{{ 'widgets.maps.custom-marker-image' | translate }}"
formControlName="markerImage">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field [fxShow]="!tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').value"
fxFlex class="mat-block">
<mat-label translate>widgets.maps.custom-marker-image-size</mat-label>
@ -86,10 +86,10 @@
functionTitle="{{ 'widgets.maps.marker-image-function' | translate }}"
helpId="widget/lib/map/marker_image_fn">
</tb-js-func>
<tb-multiple-image-input [fxShow]="tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').value"
<tb-multiple-gallery-image-input [fxShow]="tripAnimationMarkerSettingsFormGroup.get('useMarkerImageFunction').value"
label="{{ 'widgets.maps.marker-images' | translate }}"
formControlName="markerImages">
</tb-multiple-image-input>
</tb-multiple-gallery-image-input>
</ng-template>
</mat-expansion-panel>
</fieldset>

View File

@ -33,6 +33,7 @@ import { QueueComponent } from '@home/pages/admin/queue/queue.component';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@NgModule({
declarations:
@ -54,6 +55,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
imports: [
CommonModule,
SharedModule,
SharedHomeComponentsModule,
HomeComponentsModule,
AdminRoutingModule
]

View File

@ -125,11 +125,10 @@
</mat-form-field>
</div>
<div translate>dashboard.mobile-app-settings</div>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'dashboard.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
</tb-gallery-image-input>
<mat-checkbox fxFlex formControlName="mobileHide">
{{ 'dashboard.mobile-hide' | translate }}
</mat-checkbox>

View File

@ -24,6 +24,7 @@ import { DashboardRoutingModule } from './dashboard-routing.module';
import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@NgModule({
declarations: [
@ -35,6 +36,7 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
imports: [
CommonModule,
SharedModule,
SharedHomeComponentsModule,
HomeComponentsModule,
HomeDialogsModule,
DashboardRoutingModule

View File

@ -240,11 +240,10 @@
<mat-tab label="{{ 'widget.widget-settings' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<div class="mat-padding">
<tb-image-input fxFlex label="{{'widget.image-preview' | translate}}"
maxSizeByte="524288"
<tb-gallery-image-input fxFlex label="{{'widget.image-preview' | translate}}"
[(ngModel)]="widget.image"
(ngModelChange)="isDirty = true" >
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>widget.description</mat-label>
<textarea matInput #descriptionInput

View File

@ -28,6 +28,7 @@ import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component';
import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component';
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@NgModule({
declarations: [
@ -45,6 +46,7 @@ import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-
imports: [
CommonModule,
SharedModule,
SharedHomeComponentsModule,
HomeComponentsModule,
WidgetLibraryRoutingModule
]

View File

@ -54,11 +54,10 @@
{{ 'widget.title-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
<tb-gallery-image-input fxFlex
label="{{'widget.image-preview' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
</tb-gallery-image-input>
<mat-form-field class="mat-block">
<mat-label translate>widget.description</mat-label>
<textarea matInput formControlName="description" rows="2" maxlength="1024" #descriptionInput></textarea>

View File

@ -58,11 +58,6 @@
label="{{'widgets-bundle.image-preview' | translate}}"
formControlName="image">
</tb-gallery-image-input>
<!--tb-image-input fxFlex
label="{{'widgets-bundle.image-preview' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input-->
<mat-form-field class="mat-block">
<mat-label translate>widgets-bundle.description</mat-label>
<textarea matInput formControlName="description" rows="2" maxlength="1024" #descriptionInput></textarea>