Browse Source

UI: Update Widget action form style.

pull/10084/head
Igor Kulikov 2 years ago
parent
commit
3fcb913645
  1. 12
      ui-ngx/src/app/modules/common/modules-map.ts
  2. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  3. 30
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
  4. 280
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  5. 350
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  6. 3
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.html
  7. 2
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.scss
  8. 4
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.ts
  9. 1
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.html
  10. 0
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.scss
  11. 2
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.ts
  12. 24
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-action.models.ts
  13. 0
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-css.raw
  14. 0
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-html.raw
  15. 0
      ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-js.raw
  16. 56
      ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.component.html
  17. 35
      ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.component.ts
  18. 40
      ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.models.ts
  19. 234
      ui-ngx/src/app/modules/home/components/widget/config/action/widget-action.component.html
  20. 463
      ui-ngx/src/app/modules/home/components/widget/config/action/widget-action.component.ts
  21. 17
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  22. 33
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html
  23. 124
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html
  25. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts
  26. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  27. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  28. 24
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html
  29. 40
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  30. 12
      ui-ngx/src/app/shared/components/js-func.component.html
  31. 6
      ui-ngx/src/app/shared/components/js-func.component.scss
  32. 20
      ui-ngx/src/app/shared/components/js-func.component.ts
  33. 4
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  34. 24
      ui-ngx/src/app/shared/models/widget.models.ts
  35. 6
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  36. 2
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  37. 2
      ui-ngx/src/assets/locale/locale.constant-da_DK.json
  38. 2
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  39. 2
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  40. 19
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  41. 6
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  42. 2
      ui-ngx/src/assets/locale/locale.constant-fa_IR.json
  43. 2
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  44. 2
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  45. 2
      ui-ngx/src/assets/locale/locale.constant-ja_JP.json
  46. 2
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  47. 2
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  48. 2
      ui-ngx/src/assets/locale/locale.constant-lv_LV.json
  49. 6
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  50. 2
      ui-ngx/src/assets/locale/locale.constant-pt_BR.json
  51. 2
      ui-ngx/src/assets/locale/locale.constant-ro_RO.json
  52. 2
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  53. 2
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  54. 2
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  55. 6
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  56. 6
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json
  57. 6
      ui-ngx/src/form.scss

12
ui-ngx/src/app/modules/common/modules-map.ts

@ -226,9 +226,9 @@ import * as DataKeyConfigComponent from '@home/components/widget/config/data-key
import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component';
import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component';
import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
import * as CustomActionPrettyEditorComponent from '@home/components/widget/action/custom-action-pretty-editor.component';
import * as MobileActionEditorComponent from '@home/components/widget/action/mobile-action-editor.component';
import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/config/action/custom-action-pretty-resources-tabs.component';
import * as CustomActionPrettyEditorComponent from '@home/components/widget/config/action/custom-action-pretty-editor.component';
import * as MobileActionEditorComponent from '@home/components/widget/config/action/mobile-action-editor.component';
import * as CustomDialogService from '@home/components/widget/dialog/custom-dialog.service';
import * as CustomDialogContainerComponent from '@home/components/widget/dialog/custom-dialog-container.component';
import * as ImportDialogComponent from '@shared/import-export/import-dialog.component';
@ -541,9 +541,9 @@ class ModulesMap implements IModulesMap {
'@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
'@home/components/widget/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,
'@home/components/widget/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent,
'@home/components/widget/action/mobile-action-editor.component': MobileActionEditorComponent,
'@home/components/widget/config/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,
'@home/components/widget/config/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent,
'@home/components/widget/config/action/mobile-action-editor.component': MobileActionEditorComponent,
'@home/components/widget/dialog/custom-dialog.service': CustomDialogService,
'@home/components/widget/dialog/custom-dialog-container.component': CustomDialogContainerComponent,
'@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent,

9
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -46,9 +46,6 @@ import { EntityFilterComponent } from '@home/components/entity/entity-filter.com
import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component';
import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
import { CustomActionPrettyEditorComponent } from '@home/components/widget/action/custom-action-pretty-editor.component';
import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { CustomDialogContainerComponent } from '@home/components/widget/dialog/custom-dialog-container.component';
import { AddWidgetToDashboardDialogComponent } from '@home/components/attribute/add-widget-to-dashboard-dialog.component';
@ -218,9 +215,6 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
ManageWidgetActionsComponent,
WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
CustomDialogContainerComponent,
SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent,
@ -359,9 +353,6 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
ManageWidgetActionsComponent,
WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
CustomDialogContainerComponent,
SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent,

30
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts

@ -15,7 +15,6 @@
///
import {
CustomActionDescriptor,
WidgetActionDescriptor,
WidgetActionSource,
widgetActionTypeTranslationMap
@ -27,11 +26,7 @@ import { TranslateService } from '@ngx-translate/core';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, publishReplay, refCount } from 'rxjs/operators';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, isDefined, isUndefined } from '@core/utils';
import customSampleJs from '!raw-loader!./custom-sample-js.raw';
import customSampleCss from '!raw-loader!./custom-sample-css.raw';
import customSampleHtml from '!raw-loader!./custom-sample-html.raw';
import { deepClone } from '@core/utils';
export interface WidgetActionCallbacks {
fetchDashboardStates: (query: string) => Array<string>;
@ -48,32 +43,13 @@ export interface WidgetActionDescriptorInfo extends WidgetActionDescriptor {
typeName?: string;
}
export function toWidgetActionDescriptor(action: WidgetActionDescriptorInfo): WidgetActionDescriptor {
export const toWidgetActionDescriptor = (action: WidgetActionDescriptorInfo): WidgetActionDescriptor => {
const copy = deepClone(action);
delete copy.actionSourceId;
delete copy.actionSourceName;
delete copy.typeName;
return copy;
}
export function toCustomAction(action: WidgetActionDescriptorInfo): CustomActionDescriptor {
let result: CustomActionDescriptor;
if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) {
result = {
customHtml: customSampleHtml,
customCss: customSampleCss,
customFunction: customSampleJs
};
} else {
result = {
customHtml: action.customHtml,
customCss: action.customCss,
customFunction: action.customFunction
};
}
result.customResources = action && isDefined(action.customResources) ? deepClone(action.customResources) : [];
return result;
}
};
export class WidgetActionsDatasource implements DataSource<WidgetActionDescriptorInfo> {

280
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -27,219 +27,82 @@
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>widget-config.action-source</mat-label>
<mat-select required formControlName="actionSourceId">
<mat-option *ngFor="let actionSourceItem of data.actionsData.actionSources | keyvalue" [value]="actionSourceItem.key">
{{ actionSourceName(actionSourceItem.value) }}
</mat-option>
</mat-select>
<mat-error *ngIf="widgetActionFormGroup.get('actionSourceId').hasError('required')">
{{ 'widget-config.action-source-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>widget-config.action-name</mat-label>
<input required matInput formControlName="name">
<mat-hint></mat-hint>
<mat-error *ngIf="widgetActionFormGroup.get('name').hasError('required')">
{{ 'widget-config.action-name-required' | translate }}
</mat-error>
<mat-error *ngIf="widgetActionFormGroup.get('name').hasError('actionNameNotUnique')"
[innerHTML]="'widget-config.action-name-not-unique' | translate">
</mat-error>
</mat-form-field>
<tb-material-icon-select
formControlName="icon">
</tb-material-icon-select>
<mat-checkbox *ngIf="displayShowWidgetActionForm()" formControlName="useShowWidgetActionFunction" style="padding-bottom: 16px;">
{{ 'widget-config.show-hide-action-using-function' | translate }}
</mat-checkbox>
<tb-js-func *ngIf="displayShowWidgetActionForm() && widgetActionFormGroup.get('useShowWidgetActionFunction').value"
formControlName="showWidgetActionFunction"
[helpId]="getWidgetActionFunctionHelpId()"
[functionArgs]="['widgetContext', 'data']"
[globalVariables]="functionScopeVariables"
[validationArgs]="[]"
[resultType]="'boolean'"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
<mat-form-field class="mat-block">
<mat-label translate>widget-config.action-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType">
{{ widgetActionTypeTranslations.get(widgetActionType[actionType]) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="widgetActionFormGroup.get('type').hasError('required')">
{{ 'widget-config.action-type-required' | translate }}
</mat-error>
</mat-form-field>
<section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
<ng-template [ngSwitchCase]="widgetActionType.openDashboard">
<div class="mat-caption tb-required"
style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
<tb-dashboard-autocomplete
formControlName="targetDashboardId"
required
[selectFirstDashboard]="true"
></tb-dashboard-autocomplete>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<mat-form-field class="mat-block">
<input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}"
#dashboardStateInput
formControlName="targetDashboardStateId"
[required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState"
(focusin)="onFocus()"
[matAutocomplete]="targetDashboardStateAutocomplete">
<button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearTargetDashboardState()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#targetDashboardStateAutocomplete="matAutocomplete">
<mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state">
<span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required')">
{{ 'widget-action.target-dashboard-state-required' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ?
widgetActionFormGroup.get('type').value : ''">
<mat-checkbox formControlName="openRightLayout">
{{ 'widget-action.open-right-layout' | translate }}
</mat-checkbox>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<mat-checkbox formControlName="openNewBrowserTab">
{{ 'widget-action.open-new-browser-tab' | translate }}
</mat-checkbox>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<mat-checkbox *ngIf="data.widgetType !== widgetType.static" formControlName="setEntityId">
{{ 'widget-action.set-entity-from-widget' | translate }}
</mat-checkbox>
<mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
floatLabel="always"
class="mat-block">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
formControlName="stateEntityParamName">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ?
widgetActionFormGroup.get('type').value : ''">
<mat-form-field class="mat-block">
<mat-label translate>widget-action.state-display-type</mat-label>
<mat-select formControlName="stateDisplayType">
<mat-option *ngFor="let displayType of allStateDisplayTypes" [value]="displayType">
{{ stateDisplayTypeName(displayType) }}
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-row">
<div class="fixed-title-width">{{'widget-config.action-source' | translate}}*</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="actionSourceId" placeholder="{{ 'widget-config.select-action-source' | translate }}">
<mat-option *ngFor="let actionSourceItem of data.actionsData.actionSources | keyvalue" [value]="actionSourceItem.key">
{{ actionSourceName(actionSourceItem.value) }}
</mat-option>
</mat-select>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-config.action-source-required' | translate"
*ngIf="widgetActionFormGroup.get('actionSourceId').hasError('required')
&& widgetActionFormGroup.get('actionSourceId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<section fxLayout="column" [formGroup]="stateDisplayTypeFormGroup" [ngSwitch]="actionTypeFormGroup.get('stateDisplayType').value">
<ng-template [ngSwitchCase]="'separateDialog'">
<mat-form-field class="mat-block">
<mat-label translate>widget-action.dialog-title</mat-label>
<input matInput formControlName="dialogTitle">
</mat-form-field>
<mat-checkbox formControlName="dialogHideDashboardToolbar">
{{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>widget-action.dialog-width</mat-label>
<input type="number" min="1" max="100" step="1" matInput formControlName="dialogWidth">
<mat-error *ngIf="stateDisplayTypeFormGroup.get('dialogWidth').hasError('min')">
{{ 'widget-action.dialog-size-range-error' | translate }}
</mat-error>
<mat-error *ngIf="stateDisplayTypeFormGroup.get('dialogWidth').hasError('max')">
{{ 'widget-action.dialog-size-range-error' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>widget-action.dialog-height</mat-label>
<input type="number" min="1" max="100" step="1" matInput formControlName="dialogHeight">
<mat-error *ngIf="stateDisplayTypeFormGroup.get('dialogHeight').hasError('min')">
{{ 'widget-action.dialog-size-range-error' | translate }}
</mat-error>
<mat-error *ngIf="stateDisplayTypeFormGroup.get('dialogHeight').hasError('max')">
{{ 'widget-action.dialog-size-range-error' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="'popover'">
<mat-form-field class="mat-block">
<mat-label translate>widget-action.popover-preferred-placement</mat-label>
<mat-select formControlName="popoverPreferredPlacement">
<mat-option *ngFor="let placement of allPopoverPlacements" [value]="placement">
{{ popoverPlacementName(placement) }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="popoverHideOnClickOutside">
{{ 'widget-action.popover-hide-on-click-outside' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="popoverHideDashboardToolbar">
{{ 'widget-action.popover-hide-dashboard-toolbar' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>widget-action.popover-width</mat-label>
<input matInput formControlName="popoverWidth">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>widget-action.popover-height</mat-label>
<input matInput formControlName="popoverHeight">
</mat-form-field>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
label="{{ 'widget-action.popover-style' | translate }}"
formControlName="popoverStyle"
></tb-json-object-edit>
</ng-template>
<ng-template [ngSwitchCase]="'normal'">
</div>
<div class="tb-form-row">
<div class="fixed-title-width">{{'widget-config.action-name' | translate}}*</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(widgetActionFormGroup.get('name').hasError('required')
? 'widget-config.action-name-required'
: 'widget-config.action-name-not-unique') | translate"
*ngIf="widgetActionFormGroup.get('name').invalid
&& (widgetActionFormGroup.get('name').touched || widgetActionFormGroup.get('name').hasError('actionNameNotUnique'))"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{'widget-config.icon' | translate}}</div>
<tb-material-icon-select asBoxInput
formControlName="icon">
</tb-material-icon-select>
</div>
<div class="tb-form-panel stroked tb-slide-toggle" *ngIf="displayShowWidgetActionForm()">
<mat-expansion-panel class="tb-settings" [expanded]="widgetActionFormGroup.get('useShowWidgetActionFunction').value"
[disabled]="!widgetActionFormGroup.get('useShowWidgetActionFunction').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="useShowWidgetActionFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.show-hide-action-using-function' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func formControlName="showWidgetActionFunction"
functionTitle="{{ 'widget-config.show-action-function' | translate }}"
[helpId]="getWidgetActionFunctionHelpId()"
[functionArgs]="['widgetContext', 'data']"
[globalVariables]="functionScopeVariables"
[validationArgs]="[]"
[resultType]="'boolean'"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
</section>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.custom">
<tb-js-func
formControlName="customFunction"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[validationArgs]="[]"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/custom_action_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.customPretty">
<tb-custom-action-pretty-editor
formControlName="customAction">
</tb-custom-action-pretty-editor>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.mobileAction">
<tb-mobile-action-editor #mobileActionEditor
formControlName="mobileAction">
</tb-mobile-action-editor>
</ng-template>
</section>
</mat-expansion-panel>
</div>
<tb-widget-action
formControlName="widgetAction"
[callbacks]="data.callbacks"
[widgetType]="data.widgetType">
</tb-widget-action>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
@ -251,8 +114,7 @@
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || widgetActionFormGroup.invalid || actionTypeFormGroup.invalid || stateDisplayTypeFormGroup?.invalid ||
(!widgetActionFormGroup.dirty && !actionTypeFormGroup.dirty && !stateDisplayTypeFormGroup?.dirty)">
[disabled]="(isLoading$ | async) || widgetActionFormGroup.invalid || !widgetActionFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
</div>

350
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -14,46 +14,38 @@
/// limitations under the License.
///
import { Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
ValidatorFn,
Validators
} from '@angular/forms';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
toCustomAction,
WidgetActionCallbacks,
WidgetActionDescriptorInfo,
WidgetActionsData
} from '@home/components/widget/action/manage-widget-actions.component.models';
import { UtilsService } from '@core/services/utils.service';
import {
actionDescriptorToAction,
WidgetActionSource,
WidgetActionType,
widgetActionTypeTranslationMap
widgetType
} from '@shared/models/widget.models';
import { map, mergeMap, startWith, takeUntil, tap } from 'rxjs/operators';
import { DashboardService } from '@core/http/dashboard.service';
import { Dashboard } from '@shared/models/dashboard.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models';
import { isDefinedAndNotNull } from '@core/utils';
import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component';
import { widgetType } from '@shared/models/widget.models';
import { takeUntil } from 'rxjs/operators';
import { CustomActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import { WidgetService } from '@core/http/widget.service';
import { TranslateService } from '@ngx-translate/core';
import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models';
export interface WidgetActionDialogData {
isAdd: boolean;
@ -63,18 +55,6 @@ export interface WidgetActionDialogData {
widgetType: widgetType;
}
const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const;
type stateDisplayTypeTuple = typeof stateDisplayTypes;
export type stateDisplayType = stateDisplayTypeTuple[number];
const stateDisplayTypesTranslations = new Map<stateDisplayType, string>(
[
['normal', 'widget-action.open-normal'],
['separateDialog', 'widget-action.open-in-separate-dialog'],
['popover', 'widget-action.open-in-popover'],
]
);
@Component({
selector: 'tb-widget-action-dialog',
templateUrl: './widget-action-dialog.component.html',
@ -82,49 +62,25 @@ const stateDisplayTypesTranslations = new Map<stateDisplayType, string>(
styleUrls: []
})
export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDialogComponent,
WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher {
@ViewChild('dashboardStateInput') dashboardStateInput: ElementRef;
@ViewChild('mobileActionEditor', {static: false}) mobileActionEditor: MobileActionEditorComponent;
WidgetActionDescriptorInfo> implements OnInit, OnDestroy, ErrorStateMatcher {
private destroy$ = new Subject<void>();
private dashboard: Dashboard;
widgetActionFormGroup: UntypedFormGroup;
actionTypeFormGroup: UntypedFormGroup;
actionTypeFormGroupSubscriptions: Subscription[] = [];
stateDisplayTypeFormGroup: UntypedFormGroup;
isAdd: boolean;
action: WidgetActionDescriptorInfo;
widgetActionTypes = Object.keys(WidgetActionType);
widgetActionTypeTranslations = widgetActionTypeTranslationMap;
widgetActionType = WidgetActionType;
filteredDashboardStates: Observable<Array<string>>;
targetDashboardStateSearchText = '';
selectedDashboardStateIds: Observable<Array<string>>;
customActionEditorCompleter = CustomActionEditorCompleter;
submitted = false;
widgetType = widgetType;
functionScopeVariables: string[];
allStateDisplayTypes = stateDisplayTypes;
allPopoverPlacements = PopoverPlacements;
constructor(protected store: Store<AppState>,
protected router: Router,
private utils: UtilsService,
private dashboardService: DashboardService,
private dashboardUtils: DashboardUtilsService,
private widgetService: WidgetService,
private translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>,
@ -136,7 +92,11 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
id: this.utils.guid(),
name: '',
icon: 'more_horiz',
type: null
type: WidgetActionType.updateDashboardState,
targetDashboardStateId: null,
openRightLayout: false,
setEntityId: data.widgetType !== widgetType.static,
stateEntityParamName: null
};
} else {
this.action = this.data.action;
@ -156,15 +116,9 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
this.fb.control(this.action.useShowWidgetActionFunction, []));
this.widgetActionFormGroup.addControl('showWidgetActionFunction',
this.fb.control(this.action.showWidgetActionFunction || 'return true;', []));
this.widgetActionFormGroup.addControl('type',
this.fb.control(this.action.type, [Validators.required]));
this.widgetActionFormGroup.addControl('widgetAction',
this.fb.control(actionDescriptorToAction(this.action), [Validators.required]));
this.updateShowWidgetActionForm();
this.updateActionTypeFormGroup(this.action.type, this.action);
this.widgetActionFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type: WidgetActionType) => {
this.updateActionTypeFormGroup(type);
});
this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
@ -209,233 +163,6 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
this.widgetActionFormGroup.get('showWidgetActionFunction').updateValueAndValidity();
}
private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) {
this.actionTypeFormGroupSubscriptions.forEach(s => s.unsubscribe());
this.actionTypeFormGroupSubscriptions.length = 0;
this.actionTypeFormGroup = this.fb.group({});
if (type) {
switch (type) {
case WidgetActionType.openDashboard:
case WidgetActionType.openDashboardState:
case WidgetActionType.updateDashboardState:
this.actionTypeFormGroup.addControl(
'targetDashboardStateId',
this.fb.control(action ? action.targetDashboardStateId : null,
type === WidgetActionType.openDashboardState ? [Validators.required] : [])
);
this.actionTypeFormGroup.addControl(
'setEntityId',
this.fb.control(this.data.widgetType === widgetType.static ? false : action ? action.setEntityId : true, [])
);
this.actionTypeFormGroup.addControl(
'stateEntityParamName',
this.fb.control(action ? action.stateEntityParamName : null, [])
);
if (type === WidgetActionType.openDashboard) {
this.actionTypeFormGroup.addControl(
'openNewBrowserTab',
this.fb.control(action ? action.openNewBrowserTab : false, [])
);
this.actionTypeFormGroup.addControl(
'targetDashboardId',
this.fb.control(action ? action.targetDashboardId : null,
[Validators.required])
);
this.setupSelectedDashboardStateIds();
} else {
if (type === WidgetActionType.openDashboardState) {
const displayType = this.getStateDisplayType(action);
this.actionTypeFormGroup.addControl(
'stateDisplayType',
this.fb.control(this.getStateDisplayType(action), [Validators.required])
);
this.updateStateDisplayTypeFormGroup(displayType, action);
this.actionTypeFormGroupSubscriptions.push(
this.actionTypeFormGroup.get('stateDisplayType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((displayTypeValue: stateDisplayType) => {
this.updateStateDisplayTypeFormGroup(displayTypeValue);
})
);
}
this.actionTypeFormGroup.addControl(
'openRightLayout',
this.fb.control(action ? action.openRightLayout : false, [])
);
}
this.setupFilteredDashboardStates();
break;
case WidgetActionType.custom:
this.actionTypeFormGroup.addControl(
'customFunction',
this.fb.control(action ? action.customFunction : null, [])
);
break;
case WidgetActionType.customPretty:
this.actionTypeFormGroup.addControl(
'customAction',
this.fb.control(toCustomAction(action), [Validators.required])
);
break;
case WidgetActionType.mobileAction:
this.actionTypeFormGroup.addControl(
'mobileAction',
this.fb.control(action ? action.mobileAction : null, [Validators.required])
);
break;
}
}
}
private updateStateDisplayTypeFormGroup(displayType?: stateDisplayType, action?: WidgetActionDescriptorInfo) {
this.stateDisplayTypeFormGroup = this.fb.group({});
if (displayType) {
switch (displayType) {
case 'normal':
break;
case 'separateDialog':
this.stateDisplayTypeFormGroup.addControl(
'dialogTitle',
this.fb.control(action ? action.dialogTitle : '', [])
);
this.stateDisplayTypeFormGroup.addControl(
'dialogHideDashboardToolbar',
this.fb.control(action && isDefinedAndNotNull(action.dialogHideDashboardToolbar)
? action.dialogHideDashboardToolbar : true, [])
);
this.stateDisplayTypeFormGroup.addControl(
'dialogWidth',
this.fb.control(action ? action.dialogWidth : null, [Validators.min(1), Validators.max(100)])
);
this.stateDisplayTypeFormGroup.addControl(
'dialogHeight',
this.fb.control(action ? action.dialogHeight : null, [Validators.min(1), Validators.max(100)])
);
break;
case 'popover':
this.stateDisplayTypeFormGroup.addControl(
'popoverPreferredPlacement',
this.fb.control(action && isDefinedAndNotNull(action.popoverPreferredPlacement)
? action.popoverPreferredPlacement : 'top', [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverHideOnClickOutside',
this.fb.control(action && isDefinedAndNotNull(action.popoverHideOnClickOutside)
? action.popoverHideOnClickOutside : true, [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverHideDashboardToolbar',
this.fb.control(action && isDefinedAndNotNull(action.popoverHideDashboardToolbar)
? action.popoverHideDashboardToolbar : true, [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverWidth',
this.fb.control(action && isDefinedAndNotNull(action.popoverWidth) ? action.popoverWidth : '25vw', [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverHeight',
this.fb.control(action && isDefinedAndNotNull(action.popoverHeight) ? action.popoverHeight : '25vh', [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverStyle',
this.fb.control(action && isDefinedAndNotNull(action.popoverStyle) ? action.popoverStyle : {}, [])
);
break;
}
}
}
private getStateDisplayType(action?: WidgetActionDescriptorInfo): stateDisplayType {
let res: stateDisplayType = 'normal';
if (action) {
if (action.openInSeparateDialog) {
res = 'separateDialog';
} else if (action.openInPopover) {
res = 'popover';
}
}
return res;
}
private setupSelectedDashboardStateIds() {
this.selectedDashboardStateIds =
this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe(
tap((dashboardId) => {
if (!dashboardId) {
this.actionTypeFormGroup.get('targetDashboardStateId')
.patchValue('', {emitEvent: true});
}
this.targetDashboardStateSearchText = '';
}),
mergeMap((dashboardId) => {
if (dashboardId) {
if (this.dashboard?.id.id === dashboardId) {
return of(this.dashboard);
} else {
return this.dashboardService.getDashboard(dashboardId);
}
} else {
return of(null);
}
}),
map((dashboard: Dashboard) => {
if (dashboard) {
if (this.dashboard?.id.id !== dashboard.id.id) {
this.dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard);
}
return Object.keys(this.dashboard.configuration.states);
} else {
return [];
}
})
);
}
private setupFilteredDashboardStates() {
this.targetDashboardStateSearchText = '';
this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges
.pipe(
startWith(''),
map(value => value ? value : ''),
mergeMap(name => this.fetchDashboardStates(name)),
takeUntil(this.destroy$)
);
}
private fetchDashboardStates(searchText?: string): Observable<Array<string>> {
this.targetDashboardStateSearchText = searchText;
if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) {
return this.selectedDashboardStateIds.pipe(
map(stateIds => {
const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds;
if (result && result.length) {
return result;
} else {
return [searchText];
}
})
);
} else {
return of(this.data.callbacks.fetchDashboardStates(searchText));
}
}
private createFilterForDashboardState(query: string): (stateId: string) => boolean {
const lowercaseQuery = query.toLowerCase();
return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0;
}
public clearTargetDashboardState(value: string = '') {
this.dashboardStateInput.nativeElement.value = value;
this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.dashboardStateInput.nativeElement.blur();
this.dashboardStateInput.nativeElement.focus();
}, 0);
}
private validateActionName(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value;
@ -474,53 +201,16 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
}
}
public stateDisplayTypeName(displayType: stateDisplayType): string {
if (displayType) {
return this.translate.instant(stateDisplayTypesTranslations.get(displayType)) + '';
} else {
return '';
}
}
public popoverPlacementName(placement: PopoverPlacement): string {
if (placement) {
return this.translate.instant(`widget-action.popover-placement-${placement}`) + '';
} else {
return '';
}
}
onFocus(): void {
this.actionTypeFormGroup.get('targetDashboardId').updateValueAndValidity({onlySelf: true, emitEvent: true});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.mobileActionEditor != null) {
this.mobileActionEditor.validateOnSubmit();
}
if (this.widgetActionFormGroup.valid && this.actionTypeFormGroup.valid) {
const type: WidgetActionType = this.widgetActionFormGroup.get('type').value;
let result: WidgetActionDescriptorInfo;
if (type === WidgetActionType.customPretty) {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value};
} else {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};
}
if (this.actionTypeFormGroup.get('stateDisplayType') &&
this.actionTypeFormGroup.get('stateDisplayType').value !== 'normal') {
result = {...result, ...this.stateDisplayTypeFormGroup.value};
result.openInSeparateDialog = this.actionTypeFormGroup.get('stateDisplayType').value === 'separateDialog';
result.openInPopover = this.actionTypeFormGroup.get('stateDisplayType').value === 'popover';
} else {
result.openInSeparateDialog = false;
result.openInPopover = false;
}
delete (result as any).stateDisplayType;
if (this.widgetActionFormGroup.valid) {
const result: WidgetActionDescriptorInfo =
{...this.widgetActionFormGroup.value, ...this.widgetActionFormGroup.get('widgetAction').value};
delete (result as any).widgetAction;
result.id = this.action.id;
this.dialogRef.close(result);
}

3
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-custom-action-pretty mat-elevation-z1" tb-fullscreen [fullscreen]="fullscreen">
<div class="tb-custom-action-pretty" tb-fullscreen [fullscreen]="fullscreen">
<div fxLayout="row" fxLayoutAlign="end center" class="tb-action-expand-button" [ngClass]="{'tb-fullscreen-editor': fullscreen}">
<button *ngIf="!fullscreen"
mat-icon-button fxHide.lt-md
@ -55,6 +55,7 @@
[disableUndefinedCheck]="true"
[validationArgs]="[]"
[editorCompleter]="customPrettyActionEditorCompleter"
functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}"
helpId="widget/action/custom_pretty_action_fn">
</tb-js-func>
</div>

2
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.scss

@ -64,7 +64,7 @@
.gutter.gutter-horizontal {
cursor: col-resize;
background-image: url("../../../../../../assets/split.js/grips/vertical.png");
background-image: url("../../../../../../../assets/split.js/grips/vertical.png");
}
.tb-split.tb-split-horizontal,

4
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.ts

@ -15,7 +15,7 @@
///
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../../../../src/typings/split.js.typings.d.ts" />
/// <reference path="../../../../../../../../src/typings/split.js.typings.d.ts" />
import {
AfterViewInit,
@ -35,7 +35,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { combineLatest } from 'rxjs';
import { CustomActionDescriptor } from '@shared/models/widget.models';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/action/custom-action.models';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
@Component({
selector: 'tb-custom-action-pretty-editor',

1
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.html

@ -100,6 +100,7 @@
[disableUndefinedCheck]="true"
[validationArgs]="[]"
[editorCompleter]="customPrettyActionEditorCompleter"
functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}"
helpId="widget/action/custom_pretty_action_fn">
</tb-js-func>
</mat-tab>

0
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.scss → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.scss

2
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.ts

@ -35,7 +35,7 @@ import { CustomActionDescriptor } from '@shared/models/widget.models';
import { Ace } from 'ace-builds';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/action/custom-action.models';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from } from 'rxjs';
import { map, tap } from 'rxjs/operators';

24
ui-ngx/src/app/modules/home/components/widget/action/custom-action.models.ts → ui-ngx/src/app/modules/home/components/widget/config/action/custom-action.models.ts

@ -17,6 +17,11 @@
import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { widgetContextCompletions } from '@shared/models/ace/widget-completion.models';
import { entityIdHref, entityTypeHref, serviceCompletions } from '@shared/models/ace/service-completion.models';
import { CustomActionDescriptor, WidgetAction } from '@shared/models/widget.models';
import { deepClone, isDefined, isUndefined } from '@core/utils';
import customSampleJs from '!raw-loader!./custom-sample-js.raw';
import customSampleCss from '!raw-loader!./custom-sample-css.raw';
import customSampleHtml from '!raw-loader!./custom-sample-html.raw';
const customActionCompletions: TbEditorCompletions = {
...{
@ -73,5 +78,24 @@ const customPrettyActionCompletions: TbEditorCompletions = {
...customActionCompletions
};
export const toCustomAction = (action: WidgetAction): CustomActionDescriptor => {
let result: CustomActionDescriptor;
if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) {
result = {
customHtml: customSampleHtml,
customCss: customSampleCss,
customFunction: customSampleJs
};
} else {
result = {
customHtml: action.customHtml,
customCss: action.customCss,
customFunction: action.customFunction
};
}
result.customResources = action && isDefined(action.customResources) ? deepClone(action.customResources) : [];
return result;
};
export const CustomActionEditorCompleter = new TbEditorCompleter(customActionCompletions);
export const CustomPrettyActionEditorCompleter = new TbEditorCompleter(customPrettyActionCompletions);

0
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw → ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-css.raw

0
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw → ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-html.raw

0
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw → ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-js.raw

56
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html → ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.component.html

@ -15,19 +15,27 @@
limitations under the License.
-->
<div [formGroup]="mobileActionFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>widget-action.mobile.action-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let actionType of mobileActionTypes" [value]="actionType">
{{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="mobileActionFormGroup.get('type').hasError('required')">
{{ 'widget-action.mobile.action-type-required' | translate }}
</mat-error>
</mat-form-field>
<div [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value">
<div class="tb-form-panel no-padding no-border" [formGroup]="mobileActionFormGroup">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.mobile.action-type' | translate }}*</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="type" placeholder="{{ 'widget-action.mobile.select-action-type' | translate }}">
<mat-option *ngFor="let actionType of mobileActionTypes" [value]="actionType">
{{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }}
</mat-option>
</mat-select>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-action.mobile.action-type-required' | translate"
*ngIf="mobileActionFormGroup.get('type').hasError('required')
&& mobileActionFormGroup.get('type').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<ng-container [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value">
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ?
mobileActionFormGroup.get('type').value : ''">
@ -37,6 +45,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_get_location_fn"
></tb-js-func>
</ng-template>
@ -47,6 +56,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_get_phone_number_fn"
></tb-js-func>
</ng-template>
@ -60,6 +70,7 @@
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_image_fn"
></tb-js-func>
</ng-template>
@ -70,6 +81,7 @@
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_qr_code_fn"
></tb-js-func>
</ng-template>
@ -80,6 +92,7 @@
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_location_fn"
></tb-js-func>
</ng-template>
@ -93,24 +106,27 @@
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_launch_result_fn"
></tb-js-func>
</ng-template>
</div>
</ng-container>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleEmptyResultFunction"
functionName="handleEmptyResult"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_handle_empty_result_fn"
></tb-js-func>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleErrorFunction"
functionName="handleError"
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_handle_error_fn"
formControlName="handleErrorFunction"
functionName="handleError"
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_handle_error_fn"
></tb-js-func>
</div>

35
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts → ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.component.ts

@ -14,18 +14,22 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, QueryList, ViewChildren } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
WidgetActionType,
WidgetMobileActionDescriptor,
WidgetMobileActionType,
widgetMobileActionTypeTranslationMap
} from '@shared/models/widget.models';
import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models';
import { JsFuncComponent } from '@shared/components/js-func.component';
import { CustomActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import {
getDefaultGetLocationFunction,
getDefaultGetPhoneNumberFunction,
@ -35,7 +39,7 @@ import {
getDefaultProcessLaunchResultFunction,
getDefaultProcessLocationFunction,
getDefaultProcessQrCodeFunction
} from '@home/components/widget/action/mobile-action-editor.models';
} from '@home/components/widget/config/action/mobile-action-editor.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
@ -50,8 +54,6 @@ import { WidgetService } from '@core/http/widget.service';
})
export class MobileActionEditorComponent implements ControlValueAccessor, OnInit {
@ViewChildren(JsFuncComponent) jsFuncComponents: QueryList<JsFuncComponent>;
mobileActionTypes = Object.keys(WidgetMobileActionType);
mobileActionTypeTranslations = widgetMobileActionTypeTranslationMap;
mobileActionType = WidgetMobileActionType;
@ -75,10 +77,9 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
private propagateChange = (_v: any) => { };
constructor(private store: Store<AppState>,
private fb: UntypedFormBuilder,
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService) {
this.functionScopeVariables = this.widgetService.getWidgetScopeVariables();
}
@ -87,7 +88,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
ngOnInit() {
@ -158,7 +159,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
}
this.mobileActionTypeFormGroup = this.fb.group({});
if (type) {
let processLaunchResultFunction;
let processLaunchResultFunction: string;
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
case WidgetMobileActionType.takePhoto:
@ -253,9 +254,5 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
});
}
public validateOnSubmit() {
for (const jsFuncComponent of this.jsFuncComponents.toArray()) {
jsFuncComponent.validateOnSubmit();
}
}
protected readonly WidgetActionType = WidgetActionType;
}

40
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts → ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.models.ts

@ -24,6 +24,7 @@ const processImageFunctionTemplate =
'\n' +
'function showImageDialog(title, imageUrl) {\n' +
' setTimeout(function() {\n' +
// eslint-disable-next-line max-len
' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' +
' }, 100);\n' +
'}\n' +
@ -82,6 +83,7 @@ const processImageFunctionTemplate =
'}\n';
const processLaunchResultFunctionTemplate =
// eslint-disable-next-line max-len
'// Optional function body to process result of attempt to launch external mobile application (for ex. map application or phone call application). \n' +
'// - launched - boolean value indicating if the external application was successfully launched.\n\n' +
'showLaunchStatusDialog(\'--TITLE--\', launched);\n' +
@ -166,6 +168,7 @@ const getLocationFunctionTemplate =
'\n' +
'function getLocationFromEntityAttributes() {\n' +
' if (entityId) {\n' +
// eslint-disable-next-line max-len
' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'latitude\', \'longitude\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' +
' var res = [0,0];\n' +
' if (attributeData && attributeData.length === 2) {\n' +
@ -188,6 +191,7 @@ const getPhoneNumberFunctionTemplate =
'\n' +
'function getPhoneNumberFromEntityAttributes() {\n' +
' if (entityId) {\n' +
// eslint-disable-next-line max-len
' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'phone\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' +
' var res = 0;\n' +
' if (attributeData && attributeData.length === 1) {\n' +
@ -200,8 +204,8 @@ const getPhoneNumberFunctionTemplate =
' }\n' +
'}\n';
export function getDefaultProcessImageFunction(type: WidgetMobileActionType): string {
let title;
export const getDefaultProcessImageFunction = (type: WidgetMobileActionType): string => {
let title: string;
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
title = 'Gallery picture';
@ -214,10 +218,10 @@ export function getDefaultProcessImageFunction(type: WidgetMobileActionType): st
break;
}
return processImageFunctionTemplate.replace('--TITLE--', title);
}
};
export function getDefaultProcessLaunchResultFunction(type: WidgetMobileActionType): string {
let title;
export const getDefaultProcessLaunchResultFunction = (type: WidgetMobileActionType): string => {
let title: string;
switch (type) {
case WidgetMobileActionType.mapLocation:
title = 'Map location';
@ -230,25 +234,17 @@ export function getDefaultProcessLaunchResultFunction(type: WidgetMobileActionTy
break;
}
return processLaunchResultFunctionTemplate.replace('--TITLE--', title);
}
};
export function getDefaultProcessQrCodeFunction() {
return processQrCodeFunction;
}
export const getDefaultProcessQrCodeFunction = () => processQrCodeFunction;
export function getDefaultProcessLocationFunction() {
return processLocationFunction;
}
export const getDefaultProcessLocationFunction = () => processLocationFunction;
export function getDefaultGetLocationFunction() {
return getLocationFunctionTemplate;
}
export const getDefaultGetLocationFunction = () => getLocationFunctionTemplate;
export function getDefaultGetPhoneNumberFunction() {
return getPhoneNumberFunctionTemplate;
}
export const getDefaultGetPhoneNumberFunction = () => getPhoneNumberFunctionTemplate;
export function getDefaultHandleEmptyResultFunction(type: WidgetMobileActionType): string {
export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType): string => {
let message = 'Mobile action was cancelled!';
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
@ -277,9 +273,9 @@ export function getDefaultHandleEmptyResultFunction(type: WidgetMobileActionType
break;
}
return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message);
}
};
export function getDefaultHandleErrorFunction(type: WidgetMobileActionType): string {
export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): string => {
let title = 'Mobile action failed';
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
@ -308,4 +304,4 @@ export function getDefaultHandleErrorFunction(type: WidgetMobileActionType): str
break;
}
return handleErrorFunctionTemplate.replace('--TITLE--', title);
}
};

234
ui-ngx/src/app/modules/home/components/widget/config/action/widget-action.component.html

@ -0,0 +1,234 @@
<!--
Copyright © 2016-2024 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-form-panel no-padding no-border" [formGroup]="widgetActionFormGroup">
<div class="tb-form-row">
<div translate>widget-config.action</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="type" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType">
{{ widgetActionTypeTranslations.get(widgetActionType[actionType]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
<ng-template [ngSwitchCase]="widgetActionType.openDashboard">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.target-dashboard' | translate }}*</div>
<tb-dashboard-autocomplete fxFlex
formControlName="targetDashboardId"
required
requiredText="widget-action.target-dashboard-required"
placeholder="{{ 'widget-action.select-target-dashboard' | translate }}"
inlineField
[selectFirstDashboard]="true"
></tb-dashboard-autocomplete>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.target-dashboard-state' | translate }}
{{widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ? '*' : ''}}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"
[class.tb-suffix-absolute]="!actionTypeFormGroup.get('targetDashboardStateId').value">
<input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}"
#dashboardStateInput
formControlName="targetDashboardStateId"
[required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState"
(focusin)="onDashboardStateInputFocus()"
[matAutocomplete]="targetDashboardStateAutocomplete">
<button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearTargetDashboardState()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-action.target-dashboard-state-required' | translate"
*ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required')
&& actionTypeFormGroup.get('targetDashboardStateId').touched"
class="tb-error">
warning
</mat-icon>
<mat-autocomplete
class="tb-autocomplete"
#targetDashboardStateAutocomplete="matAutocomplete">
<mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state">
<span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ?
widgetActionFormGroup.get('type').value : ''">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="openRightLayout">
{{ 'widget-action.open-right-layout' | translate }}
</mat-slide-toggle>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="openNewBrowserTab">
{{ 'widget-action.open-new-browser-tab' | translate }}
</mat-slide-toggle>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<div class="tb-form-row" *ngIf="widgetType !== WidgetType.static">
<mat-slide-toggle class="mat-slide" formControlName="setEntityId">
{{ 'widget-action.set-entity-from-widget' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row" *ngIf="actionTypeFormGroup.get('setEntityId').value">
<div class="fixed-title-width">{{ 'alias.state-entity-parameter-name' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
formControlName="stateEntityParamName">
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ?
widgetActionFormGroup.get('type').value : ''">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.state-display-type' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="stateDisplayType">
<mat-option *ngFor="let displayType of allStateDisplayTypes" [value]="displayType">
{{ stateDisplayTypeName(displayType) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container [formGroup]="stateDisplayTypeFormGroup" [ngSwitch]="actionTypeFormGroup.get('stateDisplayType').value">
<ng-template [ngSwitchCase]="'separateDialog'">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.dialog-title' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ 'widget-config.set' | translate }}" formControlName="dialogTitle">
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="dialogHideDashboardToolbar">
{{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-action.dialog-width' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-suffix-absolute number">
<input type="number" min="1" max="100" step="1" matInput formControlName="dialogWidth">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-action.dialog-size-range-error' | translate"
*ngIf="stateDisplayTypeFormGroup.get('dialogWidth').invalid && stateDisplayTypeFormGroup.get('dialogWidth').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-action.dialog-height' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-suffix-absolute number">
<input type="number" min="1" max="100" step="1" matInput formControlName="dialogHeight">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widget-action.dialog-size-range-error' | translate"
*ngIf="stateDisplayTypeFormGroup.get('dialogHeight').invalid && stateDisplayTypeFormGroup.get('dialogHeight').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="'popover'">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.popover-preferred-placement' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="popoverPreferredPlacement">
<mat-option *ngFor="let placement of allPopoverPlacements" [value]="placement">
{{ popoverPlacementName(placement) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="popoverHideOnClickOutside">
{{ 'widget-action.popover-hide-on-click-outside' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="popoverHideDashboardToolbar">
{{ 'widget-action.popover-hide-dashboard-toolbar' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-action.popover-width' | translate }}</div>
<tb-css-size-input formControlName="popoverWidth">
</tb-css-size-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-action.popover-height' | translate }}</div>
<tb-css-size-input formControlName="popoverHeight">
</tb-css-size-input>
</div>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
label="{{ 'widget-action.popover-style' | translate }}"
formControlName="popoverStyle"
></tb-json-object-edit>
</ng-template>
<ng-template [ngSwitchCase]="'normal'">
</ng-template>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.custom">
<tb-js-func
formControlName="customFunction"
functionTitle="{{ 'widget-action.custom-action-function' | translate }}"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[validationArgs]="[]"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/custom_action_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.customPretty">
<tb-custom-action-pretty-editor
formControlName="customAction">
</tb-custom-action-pretty-editor>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.mobileAction">
<tb-mobile-action-editor formControlName="mobileAction">
</tb-mobile-action-editor>
</ng-template>
</ng-container>
</div>

463
ui-ngx/src/app/modules/home/components/widget/config/action/widget-action.component.ts

@ -0,0 +1,463 @@
///
/// Copyright © 2016-2024 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 {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
Validators
} from '@angular/forms';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
WidgetAction,
WidgetActionType,
widgetActionTypeTranslationMap,
widgetType
} from '@shared/models/widget.models';
import { WidgetService } from '@core/http/widget.service';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import { map, mergeMap, share, startWith, takeUntil, tap } from 'rxjs/operators';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { Dashboard } from '@shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { isDefinedAndNotNull } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models';
import {
CustomActionEditorCompleter,
toCustomAction
} from '@home/components/widget/config/action/custom-action.models';
const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const;
type stateDisplayTypeTuple = typeof stateDisplayTypes;
export type stateDisplayType = stateDisplayTypeTuple[number];
const stateDisplayTypesTranslations = new Map<stateDisplayType, string>(
[
['normal', 'widget-action.open-normal'],
['separateDialog', 'widget-action.open-in-separate-dialog'],
['popover', 'widget-action.open-in-popover'],
]
);
@Component({
selector: 'tb-widget-action',
templateUrl: './widget-action.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetActionComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => WidgetActionComponent),
multi: true,
}
]
})
export class WidgetActionComponent implements ControlValueAccessor, OnInit, Validator {
@ViewChild('dashboardStateInput', {static: false}) dashboardStateInput: ElementRef;
@Input()
disabled: boolean;
@Input()
widgetType: widgetType;
@Input()
callbacks: WidgetActionCallbacks;
widgetActionTypes = Object.keys(WidgetActionType);
widgetActionTypeTranslations = widgetActionTypeTranslationMap;
widgetActionType = WidgetActionType;
allStateDisplayTypes = stateDisplayTypes;
allPopoverPlacements = PopoverPlacements;
WidgetType = widgetType;
filteredDashboardStates: Observable<Array<string>>;
targetDashboardStateSearchText = '';
selectedDashboardStateIds: Observable<Array<string>>;
customActionEditorCompleter = CustomActionEditorCompleter;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
widgetActionFormGroup: UntypedFormGroup;
actionTypeFormGroup: UntypedFormGroup;
stateDisplayTypeFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};
private actionTypeFormGroupSubscriptions: Subscription[] = [];
private stateDisplayTypeFormGroupSubscriptions: Subscription[] = [];
private destroy$ = new Subject<void>();
private dashboard: Dashboard;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
private dashboardService: DashboardService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.widgetActionFormGroup.disable({emitEvent: false});
if (this.actionTypeFormGroup) {
this.actionTypeFormGroup.disable({emitEvent: false});
}
if (this.stateDisplayTypeFormGroup) {
this.stateDisplayTypeFormGroup.disable({emitEvent: false});
}
} else {
this.widgetActionFormGroup.enable({emitEvent: false});
}
}
ngOnInit() {
this.widgetActionFormGroup = this.fb.group({});
this.widgetActionFormGroup.addControl('type',
this.fb.control(null, [Validators.required]));
this.widgetActionFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type: WidgetActionType) => {
this.updateActionTypeFormGroup(type);
});
this.widgetActionFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.widgetActionUpdated();
});
}
writeValue(widgetAction?: WidgetAction): void {
this.widgetActionFormGroup.patchValue({
type: widgetAction?.type
}, {emitEvent: false});
this.updateActionTypeFormGroup(widgetAction?.type, widgetAction);
}
validate(_c: UntypedFormControl) {
return (this.widgetActionFormGroup.valid &&
this.actionTypeFormGroup.valid && (!this.stateDisplayTypeFormGroup || this.stateDisplayTypeFormGroup.valid)) ? null : {
widgetAction: {
valid: false,
}
};
}
clearTargetDashboardState(value: string = '') {
this.dashboardStateInput.nativeElement.value = value;
this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.dashboardStateInput.nativeElement.blur();
this.dashboardStateInput.nativeElement.focus();
}, 0);
}
onDashboardStateInputFocus(): void {
this.actionTypeFormGroup.get('targetDashboardStateId').updateValueAndValidity({onlySelf: true, emitEvent: true});
}
stateDisplayTypeName(displayType: stateDisplayType): string {
if (displayType) {
return this.translate.instant(stateDisplayTypesTranslations.get(displayType)) + '';
} else {
return '';
}
}
popoverPlacementName(placement: PopoverPlacement): string {
if (placement) {
return this.translate.instant(`widget-action.popover-placement-${placement}`) + '';
} else {
return '';
}
}
private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetAction) {
this.actionTypeFormGroupSubscriptions.forEach(s => s.unsubscribe());
this.actionTypeFormGroupSubscriptions.length = 0;
this.actionTypeFormGroup = this.fb.group({});
if (type) {
switch (type) {
case WidgetActionType.openDashboard:
case WidgetActionType.openDashboardState:
case WidgetActionType.updateDashboardState:
this.actionTypeFormGroup.addControl(
'targetDashboardStateId',
this.fb.control(action ? action.targetDashboardStateId : null,
type === WidgetActionType.openDashboardState ? [Validators.required] : [])
);
this.actionTypeFormGroup.addControl(
'setEntityId',
this.fb.control(this.widgetType === widgetType.static ? false : action ? action.setEntityId : true, [])
);
this.actionTypeFormGroup.addControl(
'stateEntityParamName',
this.fb.control(action ? action.stateEntityParamName : null, [])
);
if (type === WidgetActionType.openDashboard) {
this.actionTypeFormGroup.addControl(
'openNewBrowserTab',
this.fb.control(action ? action.openNewBrowserTab : false, [])
);
this.actionTypeFormGroup.addControl(
'targetDashboardId',
this.fb.control(action ? action.targetDashboardId : null,
[Validators.required])
);
this.setupSelectedDashboardStateIds();
} else {
if (type === WidgetActionType.openDashboardState) {
const displayType = this.getStateDisplayType(action);
this.actionTypeFormGroup.addControl(
'stateDisplayType',
this.fb.control(this.getStateDisplayType(action), [Validators.required])
);
this.updateStateDisplayTypeFormGroup(displayType, action);
this.actionTypeFormGroupSubscriptions.push(
this.actionTypeFormGroup.get('stateDisplayType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((displayTypeValue: stateDisplayType) => {
this.updateStateDisplayTypeFormGroup(displayTypeValue);
})
);
}
this.actionTypeFormGroup.addControl(
'openRightLayout',
this.fb.control(action ? action.openRightLayout : false, [])
);
}
this.setupFilteredDashboardStates();
break;
case WidgetActionType.custom:
this.actionTypeFormGroup.addControl(
'customFunction',
this.fb.control(action ? action.customFunction : null, [])
);
break;
case WidgetActionType.customPretty:
this.actionTypeFormGroup.addControl(
'customAction',
this.fb.control(toCustomAction(action), [Validators.required])
);
break;
case WidgetActionType.mobileAction:
this.actionTypeFormGroup.addControl(
'mobileAction',
this.fb.control(action ? action.mobileAction : null, [Validators.required])
);
break;
}
}
this.actionTypeFormGroupSubscriptions.push(
this.actionTypeFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.widgetActionUpdated();
})
);
}
private updateStateDisplayTypeFormGroup(displayType?: stateDisplayType, action?: WidgetAction) {
this.stateDisplayTypeFormGroupSubscriptions.forEach(s => s.unsubscribe());
this.stateDisplayTypeFormGroupSubscriptions.length = 0;
this.stateDisplayTypeFormGroup = this.fb.group({});
if (displayType) {
switch (displayType) {
case 'normal':
break;
case 'separateDialog':
this.stateDisplayTypeFormGroup.addControl(
'dialogTitle',
this.fb.control(action ? action.dialogTitle : '', [])
);
this.stateDisplayTypeFormGroup.addControl(
'dialogHideDashboardToolbar',
this.fb.control(action && isDefinedAndNotNull(action.dialogHideDashboardToolbar)
? action.dialogHideDashboardToolbar : true, [])
);
this.stateDisplayTypeFormGroup.addControl(
'dialogWidth',
this.fb.control(action ? action.dialogWidth : null, [Validators.min(1), Validators.max(100)])
);
this.stateDisplayTypeFormGroup.addControl(
'dialogHeight',
this.fb.control(action ? action.dialogHeight : null, [Validators.min(1), Validators.max(100)])
);
break;
case 'popover':
this.stateDisplayTypeFormGroup.addControl(
'popoverPreferredPlacement',
this.fb.control(action && isDefinedAndNotNull(action.popoverPreferredPlacement)
? action.popoverPreferredPlacement : 'top', [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverHideOnClickOutside',
this.fb.control(action && isDefinedAndNotNull(action.popoverHideOnClickOutside)
? action.popoverHideOnClickOutside : true, [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverHideDashboardToolbar',
this.fb.control(action && isDefinedAndNotNull(action.popoverHideDashboardToolbar)
? action.popoverHideDashboardToolbar : true, [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverWidth',
this.fb.control(action && isDefinedAndNotNull(action.popoverWidth) ? action.popoverWidth : '25vw', [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverHeight',
this.fb.control(action && isDefinedAndNotNull(action.popoverHeight) ? action.popoverHeight : '25vh', [])
);
this.stateDisplayTypeFormGroup.addControl(
'popoverStyle',
this.fb.control(action && isDefinedAndNotNull(action.popoverStyle) ? action.popoverStyle : {}, [])
);
break;
}
}
this.stateDisplayTypeFormGroupSubscriptions.push(
this.stateDisplayTypeFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.widgetActionUpdated();
})
);
}
private setupSelectedDashboardStateIds() {
this.selectedDashboardStateIds =
this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe(
tap((dashboardId) => {
if (!dashboardId) {
this.actionTypeFormGroup.get('targetDashboardStateId')
.patchValue('', {emitEvent: true});
}
this.targetDashboardStateSearchText = '';
}),
mergeMap((dashboardId) => {
if (dashboardId) {
if (this.dashboard?.id.id === dashboardId) {
return of(this.dashboard);
} else {
return this.dashboardService.getDashboard(dashboardId);
}
} else {
return of(null);
}
}),
map((dashboard: Dashboard) => {
if (dashboard) {
if (this.dashboard?.id.id !== dashboard.id.id) {
this.dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard);
}
return Object.keys(this.dashboard.configuration.states);
} else {
return [];
}
}),
share()
);
}
private setupFilteredDashboardStates() {
this.targetDashboardStateSearchText = '';
this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges
.pipe(
startWith(''),
map(value => value ? value : ''),
mergeMap(name => this.fetchDashboardStates(name)),
takeUntil(this.destroy$)
);
}
private fetchDashboardStates(searchText?: string): Observable<Array<string>> {
this.targetDashboardStateSearchText = searchText;
if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) {
return this.selectedDashboardStateIds.pipe(
map(stateIds => {
const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds;
if (result && result.length) {
return result;
} else {
return [searchText];
}
})
);
} else {
return of(this.callbacks.fetchDashboardStates(searchText));
}
}
private createFilterForDashboardState(query: string): (stateId: string) => boolean {
const lowercaseQuery = query.toLowerCase();
return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0;
}
private getStateDisplayType(action?: WidgetAction): stateDisplayType {
let res: stateDisplayType = 'normal';
if (action) {
if (action.openInSeparateDialog) {
res = 'separateDialog';
} else if (action.openInPopover) {
res = 'popover';
}
}
return res;
}
private widgetActionUpdated() {
const type: WidgetActionType = this.widgetActionFormGroup.get('type').value;
let result: WidgetAction;
if (type === WidgetActionType.customPretty) {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value};
} else {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};
}
if (this.actionTypeFormGroup.get('stateDisplayType') &&
this.actionTypeFormGroup.get('stateDisplayType').value !== 'normal') {
result = {...result, ...this.stateDisplayTypeFormGroup.value};
result.openInSeparateDialog = this.actionTypeFormGroup.get('stateDisplayType').value === 'separateDialog';
result.openInPopover = this.actionTypeFormGroup.get('stateDisplayType').value === 'popover';
} else {
result.openInSeparateDialog = false;
result.openInPopover = false;
}
delete (result as any).stateDisplayType;
this.propagateChange(result);
}
}

17
ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts

@ -33,6 +33,11 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
import { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
import { TargetDeviceComponent } from '@home/components/widget/config/target-device.component';
import { WidgetActionComponent } from '@home/components/widget/config/action/widget-action.component';
import { CustomActionPrettyResourcesTabsComponent }
from '@home/components/widget/config/action/custom-action-pretty-resources-tabs.component';
import { CustomActionPrettyEditorComponent } from '@home/components/widget/config/action/custom-action-pretty-editor.component';
import { MobileActionEditorComponent } from '@home/components/widget/config/action/mobile-action-editor.component';
@NgModule({
declarations:
@ -50,7 +55,11 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent
WidgetSettingsComponent,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent
],
imports: [
CommonModule,
@ -73,7 +82,11 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent,
WidgetSettingsCommonModule
WidgetSettingsCommonModule,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent
]
})
export class WidgetConfigComponentsModule { }

33
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.html

@ -0,0 +1,33 @@
<!--
Copyright © 2016-2024 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 [formGroup]="cssSizeFormGroup" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number">
<input matInput [required]="required"
type="number" min="0" formControlName="size" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="cssSizeFormGroup.get('size').hasError('required') ? ((requiredText ? requiredText : 'css-size.size-value-required') | translate) :
('css-size.invalid-size-value' | translate)"
*ngIf="cssSizeFormGroup.get('size').invalid && cssSizeFormGroup.get('size').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<tb-css-unit-select [allowEmpty]="allowEmptyUnit" width="" formControlName="unit"></tb-css-unit-select>
</div>

124
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-size-input.component.ts

@ -0,0 +1,124 @@
///
/// Copyright © 2016-2024 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,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
Validators
} from '@angular/forms';
import { cssUnit, resolveCssSize } from '@shared/models/widget-settings.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-css-size-input',
templateUrl: './css-size-input.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CssSizeInputComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CssSizeInputComponent),
multi: true,
}
]
})
export class CssSizeInputComponent implements OnInit, ControlValueAccessor, Validator {
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
required = false;
@Input()
requiredText: string;
@Input()
@coerceBoolean()
allowEmptyUnit = false;
cssSizeFormGroup: UntypedFormGroup;
modelValue: string;
private propagateChange = null;
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.cssSizeFormGroup = this.fb.group({
size: [null, this.required ? [Validators.required, Validators.min(0)] : [Validators.min(0)]],
unit: [null, []]
});
this.cssSizeFormGroup.valueChanges.subscribe((value: {size: number; unit: cssUnit}) => {
this.updateModel(value);
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.cssSizeFormGroup.disable({emitEvent: false});
} else {
this.cssSizeFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string): void {
this.modelValue = value;
const size = resolveCssSize(value);
this.cssSizeFormGroup.patchValue({
size: size[0],
unit: size[1]
}, {emitEvent: false});
}
validate(_c: UntypedFormControl) {
return this.cssSizeFormGroup.valid ? null : {
cssSize: {
valid: false,
}
};
}
private updateModel(value: {size: number; unit: cssUnit}): void {
const result: string = isDefinedAndNotNull(value?.size) && isDefinedAndNotNull(value?.unit)
? value.size + value.unit : '';
if (this.modelValue !== result) {
this.modelValue = result;
this.propagateChange(this.modelValue);
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-form-field appearance="outline" subscriptSizing="dynamic" [style.width]="width">
<mat-select [formControl]="cssUnitFormControl" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngIf="allowEmpty" [value]="null">
</mat-option>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts

@ -40,6 +40,9 @@ export class CssUnitSelectComponent implements OnInit, ControlValueAccessor {
@coerceBoolean()
allowEmpty = false;
@Input()
width = '100%';
cssUnitsList = cssUnits;
cssUnitFormControl: UntypedFormControl;

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -66,6 +66,7 @@ import {
import {
RpcUpdateStateSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component';
import { CssSizeInputComponent } from '@home/components/widget/lib/settings/common/css-size-input.component';
@NgModule({
declarations: [
@ -76,6 +77,7 @@ import {
ColorSettingsComponent,
ColorSettingsPanelComponent,
CssUnitSelectComponent,
CssSizeInputComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent,
BackgroundSettingsComponent,
@ -106,6 +108,7 @@ import {
ColorSettingsComponent,
ColorSettingsPanelComponent,
CssUnitSelectComponent,
CssSizeInputComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent,
BackgroundSettingsComponent,

2
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -1345,7 +1345,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.parentDashboard : this.widgetContext.dashboard,
popoverComponent: componentRef.instance
},
{width: popoverWidth, height: popoverHeight},
{width: popoverWidth || '25vw', height: popoverHeight || '25vh'},
popoverStyle,
{}
);

24
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html

@ -15,9 +15,14 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block" [floatLabel]="floatLabel"
[appearance]="appearance" [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<mat-form-field [formGroup]="selectDashboardFormGroup"
[class]="{'tb-inline-field': inlineField, 'flex': inlineField,
'tb-suffix-absolute': (inlineField && !selectDashboardFormGroup.get('dashboard').value)}"
class="mat-block"
[floatLabel]="floatLabel"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
<mat-label *ngIf="!inlineField && label">{{ label }}</mat-label>
<input matInput type="text"
#dashboardInput
placeholder="{{ placeholder }}"
@ -35,6 +40,15 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="requiredText | translate"
*ngIf="inlineField && requiredText && selectDashboardFormGroup.get('dashboard').hasError('required')
&& selectDashboardFormGroup.get('dashboard').touched"
class="tb-error">
warning
</mat-icon>
<mat-autocomplete
class="tb-autocomplete"
#dashboardAutocomplete="matAutocomplete"
@ -48,10 +62,10 @@
</span>
</mat-option>
</mat-autocomplete>
<mat-error>
<mat-error *ngIf="!inlineField">
<ng-content select="[tb-error]"></ng-content>
</mat-error>
<mat-hint>
<mat-hint *ngIf="!inlineField">>
<ng-content select="[tb-hint]"></ng-content>
</mat-hint>
</mat-form-field>

40
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -15,7 +15,13 @@
///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
@ -28,11 +34,11 @@ import { AppState } from '@app/core/core.state';
import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { getEntityDetailsPageURL } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models';
import { AuthUser } from '@shared/models/user.model';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-dashboard-autocomplete',
@ -82,14 +88,16 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@coerceBoolean()
inlineField: boolean;
@Input()
requiredText: string;
@Input()
@coerceBoolean()
required: boolean;
@Input()
disabled: boolean;
@ -106,7 +114,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
private authUser: AuthUser;
private propagateChange = (v: any) => { };
private propagateChange = (_v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
@ -118,7 +126,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
}
this.selectDashboardFormGroup = this.fb.group({
dashboard: [null]
dashboard: [null, this.required ? [Validators.required] : []]
});
}
@ -126,7 +134,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
ngOnInit() {
@ -134,7 +142,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
.pipe(
debounceTime(150),
tap(value => {
let modelValue;
let modelValue: string | DashboardInfo;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
@ -218,15 +226,13 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
fetchDashboards(searchText?: string): Observable<Array<DashboardInfo>> {
this.searchText = searchText;
const pageLink = new PageLink(10, 0, searchText, {
const pageLink = new PageLink(25, 0, searchText, {
property: 'title',
direction: Direction.ASC
});
return this.getDashboards(pageLink).pipe(
catchError(() => of(emptyPageData<DashboardInfo>())),
map(pageData => {
return pageData.data;
})
map(pageData => pageData.data)
);
}

12
ui-ngx/src/app/shared/components/js-func.component.html

@ -15,14 +15,14 @@
limitations under the License.
-->
<div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight, 'tb-js-func-title': functionTitle}"
<div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight, 'tb-hide-brackets': hideBrackets}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;" class="tb-js-func-toolbar">
<label *ngIf="functionTitle" class="tb-title no-padding"
[ngClass]="{'tb-error': !disabled && (hasErrors || !functionValid || required && !modelValue), 'tb-required': !disabled && required}">{{functionTitle + ': f(' + functionArgsString + ')' }}</label>
<label *ngIf="!functionTitle" class="tb-title no-padding"
[ngClass]="{'tb-error': !disabled && (hasErrors || !functionValid || required && !modelValue), 'tb-required': !disabled && required}">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label>
<label class="tb-title no-padding"
[ngClass]="{'tb-error': !disabled && (hasErrors || !functionValid || required && !modelValue), 'tb-required': !disabled && required}">
{{ functionLabel }}
</label>
<span fxFlex></span>
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyJs()">
{{'js-func.tidy' | translate }}
@ -43,7 +43,7 @@
<div id="tb-javascript-panel" class="tb-js-func-panel" fxLayout="column" tb-toast toastTarget="{{toastTargetId}}">
<div #javascriptEditor id="tb-javascript-input" [ngStyle]="fillHeight ? {} : {minHeight: minHeight}" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
<div *ngIf="!functionTitle" fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<div *ngIf="!hideBrackets" fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors || !functionValid || required && !modelValue}">}</label>
</div>
</div>

6
ui-ngx/src/app/shared/components/js-func.component.scss

@ -24,13 +24,13 @@
height: 100%;
}
&:not(.tb-js-func-title) {
&:not(.tb-hide-brackets) {
.tb-js-func-panel {
margin-left: 15px;
}
}
&.tb-js-func-title {
&.tb-hide-brackets {
.tb-js-func-panel {
height: calc(100% - 40px);
}
@ -48,7 +48,7 @@
}
&:not(.tb-fullscreen) {
&.tb-js-func-title {
&.tb-hide-brackets {
padding-bottom: 15px;
}
}

20
ui-ngx/src/app/shared/components/js-func.component.ts

@ -39,7 +39,8 @@ import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { beautifyJs } from '@shared/models/beautify.models';
import { ScriptLanguage } from "@shared/models/rule-node.models";
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-js-func',
@ -97,6 +98,10 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
@Input() scriptLanguage: ScriptLanguage = ScriptLanguage.JS;
@Input()
@coerceBoolean()
hideBrackets = false;
private noValidateValue: boolean;
get noValidate(): boolean {
return this.noValidateValue;
@ -115,7 +120,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
this.requiredValue = coerceBooleanProperty(value);
}
functionArgsString = '';
functionLabel: string;
fullscreen = false;
@ -130,6 +135,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
errorMarkers: number[] = [];
errorAnnotationId = -1;
private functionArgsString = '';
private propagateChange = null;
public hasErrors = false;
@ -142,6 +149,9 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
}
ngOnInit(): void {
if (this.functionTitle) {
this.hideBrackets = true;
}
if (!this.resultType || this.resultType.length === 0) {
this.resultType = 'nocheck';
}
@ -153,6 +163,12 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
this.functionArgsString += functionArg;
});
}
if (this.functionTitle) {
this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`;
} else {
this.functionLabel =
`function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`;
}
const editorElement = this.javascriptEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/javascript',

4
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -204,7 +204,7 @@ export const cssSizeToStrSize = (size?: number, unit?: cssUnit): string => (isDe
export const resolveCssSize = (strSize?: string): [number, cssUnit] => {
if (!strSize || !strSize.trim().length) {
return [0, 'px'];
return [null, 'px'];
}
let resolvedUnit: cssUnit;
let resolvedSize = strSize;
@ -218,7 +218,7 @@ export const resolveCssSize = (strSize?: string): [number, cssUnit] => {
resolvedSize = strSize.substring(0, strSize.length - resolvedUnit.length);
}
resolvedUnit = resolvedUnit || 'px';
let numericSize = 0;
let numericSize: number = null;
if (isNumeric(resolvedSize)) {
numericSize = Number(resolvedSize);
}

24
ui-ngx/src/app/shared/models/widget.models.ts

@ -632,11 +632,7 @@ export interface CustomActionDescriptor {
customModules?: Type<any>[];
}
export interface WidgetActionDescriptor extends CustomActionDescriptor {
id: string;
name: string;
icon: string;
displayName?: string;
export interface WidgetAction extends CustomActionDescriptor {
type: WidgetActionType;
targetDashboardId?: string;
targetDashboardStateId?: string;
@ -657,10 +653,28 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor {
setEntityId?: boolean;
stateEntityParamName?: string;
mobileAction?: WidgetMobileActionDescriptor;
}
export interface WidgetActionDescriptor extends WidgetAction {
id: string;
name: string;
icon: string;
displayName?: string;
useShowWidgetActionFunction?: boolean;
showWidgetActionFunction?: string;
}
export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): WidgetAction => {
const result: WidgetActionDescriptor = {...descriptor};
delete result.id;
delete result.name;
delete result.icon;
delete result.displayName;
delete result.useShowWidgetActionFunction;
delete result.showWidgetActionFunction;
return result;
};
export interface WidgetComparisonSettings {
comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor;

6
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -4584,8 +4584,8 @@
"popover-placement-leftBottom": "Esquerra inferior",
"popover-hide-on-click-outside": "Amaga la finestra emergent al clic exterior",
"popover-hide-dashboard-toolbar": "Amaga la barra d'eines del tauler a la finestra emergent",
"popover-width": "Amplada emergent en unitats del navegador (p. ex. 100 píxels, 25 vw)",
"popover-height": "Alçada emergent en unitats del navegador (p. ex. 100px, 25vh)",
"popover-width": "Amplada emergent",
"popover-height": "Alçada emergent",
"popover-style": "Estil popover",
"open-new-browser-tab": "Obrir en una nova pestanya",
"mobile": {
@ -4681,7 +4681,7 @@
"action-source-required": "Cal origen de acció.",
"action-name": "Nom",
"action-name-required": "Cal nom de acció.",
"action-name-not-unique": "Existe una acció amb el mateix nom.<br/>El nom d'acció ha de ser únic dins de la mateixa font d'acció (origen).",
"action-name-not-unique": "Existe una acció amb el mateix nom.\nEl nom d'acció ha de ser únic dins de la mateixa font d'acció (origen).",
"action-icon": "Icona",
"show-hide-action-using-function": "Mostra/amaga l'acció mitjançant la funció",
"action-type": "Tipus",

2
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -2931,7 +2931,7 @@
"action-source-required": "Zdroj akce je povinný.",
"action-name": "Název",
"action-name-required": "Název akce je povinný.",
"action-name-not-unique": "Jiná akce s identickým názvem již existuje.<br/>Název akce by měl být v rámci zdroje akce unikátní.",
"action-name-not-unique": "Jiná akce s identickým názvem již existuje.\nNázev akce by měl být v rámci zdroje akce unikátní.",
"action-icon": "Ikona",
"action-type": "Typ",
"action-type-required": "Typ akce je povinný.",

2
ui-ngx/src/assets/locale/locale.constant-da_DK.json

@ -3439,7 +3439,7 @@
"action-source-required": "Handlingskilde er påkrævet.",
"action-name": "Navn",
"action-name-required": "Handlingsnavn er påkrævet.",
"action-name-not-unique": "Der findes allerede en anden handling med samme navn.<br/>Handlingsnavnet skal være unikt inden for den samme handlingskilde.",
"action-name-not-unique": "Der findes allerede en anden handling med samme navn.\nHandlingsnavnet skal være unikt inden for den samme handlingskilde.",
"action-icon": "Ikon",
"action-type": "Type",
"action-type-required": "Handlingstype er påkrævet.",

2
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -1959,7 +1959,7 @@
"action-source-required": "Aktionsquelle ist erforderlich.",
"action-name": "Name",
"action-name-required": "Aktionsname ist erforderlich.",
"action-name-not-unique": "Eine andere Aktion mit demselben Namen ist bereits vorhanden.<br/> Der Aktionsname sollte innerhalb derselben Aktionsquelle eindeutig sein.",
"action-name-not-unique": "Eine andere Aktion mit demselben Namen ist bereits vorhanden.\n Der Aktionsname sollte innerhalb derselben Aktionsquelle eindeutig sein.",
"action-icon": "Symbol ",
"action-type": "Art",
"action-type-required": "Aktionsart ist erforderlich.",

2
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -2430,7 +2430,7 @@
"action-source-required": "Απαιτείται πηγή ενέργειας.",
"action-name": "Όνομα",
"action-name-required": "Απαιτείται όνομα ενέργειας.",
"action-name-not-unique": "Μια άλλη ενέργεια με το ίδιο όνομα υπάρχει ήδη.<br/>Το όνομα ενέργειας πρέπει να είναι μοναδικό μέσα στην ίδια πηγή ενέργειας.",
"action-name-not-unique": "Μια άλλη ενέργεια με το ίδιο όνομα υπάρχει ήδη.\nΤο όνομα ενέργειας πρέπει να είναι μοναδικό μέσα στην ίδια πηγή ενέργειας.",
"action-icon": "Εικονίδιο",
"action-type": "Τύπος",
"action-type-required": "Απαιτείται τύπος ενέργειας.",

19
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1006,6 +1006,10 @@
"edges": "Customer edge instances",
"manage-edges": "Manage edges"
},
"css-size": {
"size-value-required": "Size value is required",
"invalid-size-value": "Invalid size value"
},
"date": {
"last-update-n-ago": "Last update N ago",
"last-update-n-ago-text": "Last update {{ agoText }}",
@ -4987,6 +4991,8 @@
"target-dashboard-state-required": "Target dashboard state is required",
"set-entity-from-widget": "Set entity from widget",
"target-dashboard": "Target dashboard",
"select-target-dashboard": "Select target dashboard",
"target-dashboard-required": "Target dashboard is required.",
"open-right-layout": "Open right dashboard layout (mobile view)",
"state-display-type": "Dashboard state display option",
"open-normal": "Normal",
@ -5012,12 +5018,13 @@
"popover-placement-leftBottom": "Left bottom",
"popover-hide-on-click-outside": "Hide popover on outside click",
"popover-hide-dashboard-toolbar": "Hide dashboard toolbar in popover",
"popover-width": "Popover width in browser units (ex. 100px, 25vw)",
"popover-height": "Popover height in browser units (ex. 100px, 25vh)",
"popover-width": "Popover width",
"popover-height": "Popover height",
"popover-style": "Popover style",
"open-new-browser-tab": "Open in a new browser tab",
"mobile": {
"action-type": "Mobile action type",
"select-action-type": "Select mobile action type",
"action-type-required": "Mobile action type is required",
"take-picture-from-gallery": "Take picture from gallery",
"take-photo": "Take photo",
@ -5027,7 +5034,9 @@
"make-phone-call": "Make phone call",
"get-location": "Get phone location",
"take-screenshot": "Take screenshot"
}
},
"custom-action-function": "Custom action function",
"custom-pretty-function": "Custom action (with HTML template) function"
},
"widgets-bundle": {
"current": "Current bundle",
@ -5123,12 +5132,14 @@
"search-actions": "Search actions",
"no-actions-text": "No actions found",
"action-source": "Action source",
"select-action-source": "Select action source",
"action-source-required": "Action source is required.",
"action-name": "Name",
"action-name-required": "Action name is required.",
"action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.",
"action-name-not-unique": "Another action with the same name already exists.\nAction name should be unique within the same action source.",
"action-icon": "Icon",
"show-hide-action-using-function": "Show/hide action using function",
"show-action-function": "Show action function",
"action-type": "Type",
"action-type-required": "Action type is required.",
"edit-action": "Edit action",

6
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -4883,8 +4883,8 @@
"popover-placement-leftBottom": "Izquierda inferior",
"popover-hide-on-click-outside": "Ocultar en click fuera del popover",
"popover-hide-dashboard-toolbar": "Ocultar caja de herramientas en popover",
"popover-width": "Ancho de popover en unidades de navegador (ej. 100px, 25vw)",
"popover-height": "Altura de popover en unidades de navegador (ej. 100px, 25vh)",
"popover-width": "Ancho de popover",
"popover-height": "Altura de popover",
"popover-style": "Estilo de popover",
"open-new-browser-tab": "Abrir en una nueva pestaña",
"mobile": {
@ -4996,7 +4996,7 @@
"action-source-required": "Origen de acción requerido.",
"action-name": "Nombre",
"action-name-required": "Nombre de accion requerido.",
"action-name-not-unique": "Existe una acción con el mismo nombre.<br/>El nombre de acción debe ser único dentro de la misma fuente de acción (origen).",
"action-name-not-unique": "Existe una acción con el mismo nombre.\nEl nombre de acción debe ser único dentro de la misma fuente de acción (origen).",
"action-icon": "Icono",
"show-hide-action-using-function": "Mostrar/Ocultar acción usando función",
"action-type": "Tipo",

2
ui-ngx/src/assets/locale/locale.constant-fa_IR.json

@ -1529,7 +1529,7 @@
"action-source-required": ".منشأ اقدام مورد نياز است",
"action-name": "نام",
"action-name-required": ".نام اقدام مورد نياز است",
"action-name-not-unique": ".در حيطه يک منشأ اقدام، نام اقدام بايد منحصر بفرد باشد<br/>.در حال حاضر اقدامي ديگر با نام مشابه موجود است",
"action-name-not-unique": ".در حيطه يک منشأ اقدام، نام اقدام بايد منحصر بفرد باشد\n.در حال حاضر اقدامي ديگر با نام مشابه موجود است",
"action-icon": "شمايل",
"action-type": "نوع",
"action-type-required": ".نوع اقدام مورد نياز است",

2
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -2145,7 +2145,7 @@
"action": "Action",
"action-icon": "Icône",
"action-name": "Nom",
"action-name-not-unique": "Une autre action portant le même nom existe déjà. <br/> Le nom de l'action doit être unique dans la même source d'action.",
"action-name-not-unique": "Une autre action portant le même nom existe déjà. \n Le nom de l'action doit être unique dans la même source d'action.",
"action-name-required": "Le nom de l'action est requis",
"action-source": "Source de l'action",
"action-source-required": "Une source d'action est requise.",

2
ui-ngx/src/assets/locale/locale.constant-it_IT.json

@ -1574,7 +1574,7 @@
"action-source-required": "Sorgente azione obbligatoria.",
"action-name": "Nome",
"action-name-required": "Nome azione obbligatorio.",
"action-name-not-unique": "Un'altra azione con lo stesso nome è già presente.<br/>Il nome di una azione dovrebbe essere univoco all'interno della stessa sorgente.",
"action-name-not-unique": "Un'altra azione con lo stesso nome è già presente.\nIl nome di una azione dovrebbe essere univoco all'interno della stessa sorgente.",
"action-icon": "Icona",
"action-type": "Tipo",
"action-type-required": "Tipo azione obbligatorio.",

2
ui-ngx/src/assets/locale/locale.constant-ja_JP.json

@ -1415,7 +1415,7 @@
"action-source-required": "アクションソースが必要です。",
"action-name": "名",
"action-name-required": "アクション名は必須です。",
"action-name-not-unique": "同じ名前の別のアクションがすでに存在します。<br/>アクション名は、同じアクションソース内で一意である必要があります。",
"action-name-not-unique": "同じ名前の別のアクションがすでに存在します。\nアクション名は、同じアクションソース内で一意である必要があります。",
"action-icon": "アイコン",
"action-type": "タイプ",
"action-type-required": "アクションタイプが必要です。",

2
ui-ngx/src/assets/locale/locale.constant-ka_GE.json

@ -1665,7 +1665,7 @@
"action-source-required": "მოქმედების წყარო საჭიროა.",
"action-name": "სახელი",
"action-name-required": "მოქმედების სახელი საჭიროა.",
"action-name-not-unique": "სხვა მოქმედება იგივე სახელით უკვე არსებობს.<br/>მოქმედების სახელი უნდა იყოს უნიკალური ერთი და იგივე მონაცემთა წყაროსთვის.",
"action-name-not-unique": "სხვა მოქმედება იგივე სახელით უკვე არსებობს.\nმოქმედების სახელი უნდა იყოს უნიკალური ერთი და იგივე მონაცემთა წყაროსთვის.",
"action-icon": "ხატულა",
"action-type": "ტიპი",
"action-type-required": "მოქმედების ტიპი საჭიროა.",

2
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -2307,7 +2307,7 @@
"action-source-required": "액션 소스를 입력하세요.",
"action-name": "이름",
"action-name-required": "액션 이름을 입력하세요.",
"action-name-not-unique": "같은 이름의 액션이 이미 존재합니다.<br/>같은 액션 소스에서 액션 이름이 중복될 수 없습니다.",
"action-name-not-unique": "같은 이름의 액션이 이미 존재합니다.\n같은 액션 소스에서 액션 이름이 중복될 수 없습니다.",
"action-icon": "아이콘",
"action-type": "유형",
"action-type-required": "액션 유형을 입력하세요.",

2
ui-ngx/src/assets/locale/locale.constant-lv_LV.json

@ -1578,7 +1578,7 @@
"action-source-required": "Aktivitāšu avoti ir nepieciešami.",
"action-name": "Nosaukums",
"action-name-required": "Aktitiāšu nosaukums ir nepieciešams.",
"action-name-not-unique": "Cita aktivitāte ar tādu pašu nosaukumu jau eksistē.<br/>Aktitivātes nosaukumam ir jābūt unikālam vienā aktivitātes avotā.",
"action-name-not-unique": "Cita aktivitāte ar tādu pašu nosaukumu jau eksistē.\nAktitivātes nosaukumam ir jābūt unikālam vienā aktivitātes avotā.",
"action-icon": "Ikona",
"action-type": "Tips",
"action-type-required": "Aktivitātes tips ir nepieciešams.",

6
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -5382,8 +5382,8 @@
"popover-placement-leftBottom": "Links onder",
"popover-hide-on-click-outside": "Popover verbergen bij klikken aan de buitenkant",
"popover-hide-dashboard-toolbar": "Dashboardwerkbalk verbergen in pop-over",
"popover-width": "Popover-breedte in browsereenheden (bijv. 100px, 25vw)",
"popover-height": "Popover-hoogte in browsereenheden (bijv. 100px, 25vh)",
"popover-width": "Popover-breedte",
"popover-height": "Popover-hoogte",
"popover-style": "Popover-stijl",
"open-new-browser-tab": "Openen in een nieuw browsertabblad",
"mobile": {
@ -5481,7 +5481,7 @@
"action-source-required": "Actiebron is vereist.",
"action-name": "Naam",
"action-name-required": "De naam van de actie is vereist.",
"action-name-not-unique": "Er bestaat al een andere actie met dezelfde naam.<br/>De naam van de actie moet uniek zijn binnen dezelfde actiebron.",
"action-name-not-unique": "Er bestaat al een andere actie met dezelfde naam.\nDe naam van de actie moet uniek zijn binnen dezelfde actiebron.",
"action-icon": "Pictogram",
"show-hide-action-using-function": "Actie tonen/verbergen met behulp van functie",
"action-type": "Type",

2
ui-ngx/src/assets/locale/locale.constant-pt_BR.json

@ -1897,7 +1897,7 @@
"action-source-required": "A fonte da ação é obrigatória.",
"action-name": "Nome",
"action-name-required": "O nome da ação é obrigatório!",
"action-name-not-unique": "Já existe outra ação com o mesmo nome.<br/>O nome da ação na mesma fonte de ação deve ser exclusivo.",
"action-name-not-unique": "Já existe outra ação com o mesmo nome.\nO nome da ação na mesma fonte de ação deve ser exclusivo.",
"action-icon": "Ícone",
"action-type": "Tipo",
"action-type-required": "O tipo de ação é obrigatório.",

2
ui-ngx/src/assets/locale/locale.constant-ro_RO.json

@ -1648,7 +1648,7 @@
"action-source-required": "Sursa acțiunii este obligatorie",
"action-name": "Numele Acțiunii",
"action-name-required": "Numele acțiunii este obligatoriu",
"action-name-not-unique": "O acţiune cu acelaşi nume este deja definită<br/>Numele definit al acțiunii trebuie să fie unic in aceeaşi sursă de date",
"action-name-not-unique": "O acţiune cu acelaşi nume este deja definită\nNumele definit al acțiunii trebuie să fie unic in aceeaşi sursă de date",
"action-icon": "Pictogramă",
"action-type": "Tipul",
"action-type-required": "Tipul acțiunii este obligatoriu",

2
ui-ngx/src/assets/locale/locale.constant-sl_SI.json

@ -2308,7 +2308,7 @@
"action-source-required": "Zahtevan je vir dejanj.",
"action-name": "Ime",
"action-name-required": "Ime dejanja je obvezno.",
"action-name-not-unique": "Še eno dejanje z istim imenom že obstaja. <br/> Ime dejanja mora biti enolično v istem viru dejanj.",
"action-name-not-unique": "Še eno dejanje z istim imenom že obstaja. \n Ime dejanja mora biti enolično v istem viru dejanj.",
"action-icon": "Ikona",
"action-type": "Vrsta",
"action-type-required": "Zahtevana je vrsta dejanja.",

2
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -2951,7 +2951,7 @@
"action-source-required": "Eylem kaynağı gerekli.",
"action-name": "İsim",
"action-name-required": "Eylem ismi gerekli.",
"action-name-not-unique": "Aynı ada sahip başka bir işlem zaten var.<br/>Eylem adı, aynı eylem kaynağı içinde emsalsiz olmalıdır.",
"action-name-not-unique": "Aynı ada sahip başka bir işlem zaten var.\nEylem adı, aynı eylem kaynağı içinde emsalsiz olmalıdır.",
"action-icon": "İkon",
"action-type": "Tür",
"action-type-required": "Eylem türü gerekli.",

2
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -2390,7 +2390,7 @@
"action-source-required": "Необхідно вказати джерело дії.",
"action-name": "Назва дії",
"action-name-required": "Необхідно вказати назву дії.",
"action-name-not-unique": "Дія з такою назвою вже існує.<br/>Назва дії має бути унікальною в межах одного джерела дії.",
"action-name-not-unique": "Дія з такою назвою вже існує.\nНазва дії має бути унікальною в межах одного джерела дії.",
"action-icon": "Іконка",
"action-type": "Тип",
"action-type-required": "Необхідно вказати тип дії.",

6
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -4894,8 +4894,8 @@
"popover-placement-leftBottom": "左下",
"popover-hide-on-click-outside": "在点击弹出框外部时隐藏弹出框",
"popover-hide-dashboard-toolbar": "在弹出框中隐藏仪表板工具栏",
"popover-width": "弹出框的宽度使用浏览器单位表示(例如:100px、25vw)",
"popover-height": "弹出框的高度使用浏览器单位表示(例如:100px、25vh)",
"popover-width": "弹出宽度",
"popover-height": "弹出高度",
"popover-style": "弹出框样式",
"open-new-browser-tab": "在新的浏览器选项卡中打开",
"mobile": {
@ -5008,7 +5008,7 @@
"action-source-required": "动作源必填",
"action-name": "名称",
"action-name-required": "动作名称必填。",
"action-name-not-unique": "动作名称已经存在。<br/>相同动作源的动作名称必须唯一。",
"action-name-not-unique": "动作名称已经存在。\n相同动作源的动作名称必须唯一。",
"action-icon": "图标",
"show-hide-action-using-function": "使用函数显示/隐藏动作",
"action-type": "类型",

6
ui-ngx/src/assets/locale/locale.constant-zh_TW.json

@ -3439,8 +3439,8 @@
"popover-placement-leftBottom": "左側底部",
"popover-hide-on-click-outside": "在外部單擊時隱藏彈出視窗",
"popover-hide-dashboard-toolbar": "在彈出提示框中隱藏對話工具欄",
"popover-width": "瀏覽器單元中的彈出寬度(例如100px、25vw)",
"popover-height": "瀏覽器單元中的彈出高度 (例如100px、25vh)",
"popover-width": "彈出寬度",
"popover-height": "彈出高度",
"popover-style": "彈出提示框形式",
"open-new-browser-tab": "在新的瀏覽器選項中打開",
"mobile": {
@ -3535,7 +3535,7 @@
"action-source-required": "動作源必填",
"action-name": "動作名稱",
"action-name-required": "動作名稱必填。",
"action-name-not-unique": "動作名稱已經存在。<br/>統一動作源的動作名稱必須唯一。",
"action-name-not-unique": "動作名稱已經存在。\n統一動作源的動作名稱必須唯一。",
"action-icon": "圖示",
"show-hide-action-using-function": "使用函數顯示/隱藏動作",
"action-type": "類型",

6
ui-ngx/src/form.scss

@ -132,14 +132,14 @@
}
}
}
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
}
.tb-form-panel-title {

Loading…
Cancel
Save