\n` +
@@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions {
private markdownCopyCode(id: number) {
const copyWrapper = $('#codeWrapper' + id);
if (copyWrapper.hasClass('noChars')) {
- const text = $('#copyCodeId' + id).text();
+ const text = decodeURIComponent($('#copyCodeId' + id).text());
this.window.navigator.clipboard.writeText(text).then(() => {
import('tooltipster').then(
() => {
diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts
index 19a158cbc2..bea56db633 100644
--- a/ui-ngx/src/app/shared/components/popover.component.ts
+++ b/ui-ngx/src/app/shared/components/popover.component.ts
@@ -25,7 +25,6 @@ import {
Directive,
ElementRef,
EventEmitter,
- Injectable,
Injector,
Input,
OnChanges,
@@ -36,7 +35,6 @@ import {
Renderer2,
SimpleChanges,
TemplateRef,
- Type,
ViewChild,
ViewContainerRef,
ViewEncapsulation
@@ -54,13 +52,11 @@ import {
getPlacementName,
popoverMotion,
PopoverPlacement,
- PopoverWithTrigger,
POSITION_MAP,
PropertyMapping
} from '@shared/components/popover.models';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
-import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@@ -285,162 +281,6 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
}
}
-@Injectable()
-export class TbPopoverService {
-
- private popoverWithTriggers: PopoverWithTrigger[] = [];
-
- componentFactory: ComponentFactory
= this.resolver.resolveComponentFactory(TbPopoverComponent);
-
- constructor(private resolver: ComponentFactoryResolver) {
- }
-
- hasPopover(trigger: Element): boolean {
- const res = this.findPopoverByTrigger(trigger);
- return res !== null;
- }
-
- hidePopover(trigger: Element): boolean {
- const component: TbPopoverComponent = this.findPopoverByTrigger(trigger);
- if (component && component.tbVisible) {
- component.hide();
- return true;
- } else {
- return false;
- }
- }
-
- displayPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
- componentType: Type, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
- injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {
- const componentRef = hostView.createComponent(this.componentFactory);
- const component = componentRef.instance;
- this.popoverWithTriggers.push({
- trigger,
- popoverComponent: component
- });
- renderer.removeChild(
- renderer.parentNode(trigger),
- componentRef.location.nativeElement
- );
- const originElementRef = new ElementRef(trigger);
- component.setOverlayOrigin({ elementRef: originElementRef });
- component.tbPlacement = preferredPlacement;
- component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType);
- component.tbComponentInjector = injector;
- component.tbComponentContext = context;
- component.tbOverlayStyle = overlayStyle;
- component.tbPopoverInnerStyle = popoverStyle;
- component.tbComponentStyle = style;
- component.tbHideOnClickOutside = hideOnClickOutside;
- component.tbVisibleChange.subscribe((visible: boolean) => {
- if (!visible) {
- component.tbAnimationDone.subscribe(() => {
- componentRef.destroy();
- });
- }
- });
- component.tbDestroy.subscribe(() => {
- this.removePopoverByComponent(component);
- });
- component.show();
- return component;
- }
-
- toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId: string,
- visibleFn: (visible: boolean) => void, readyFn: (ready: boolean) => void) {
- if (this.hasPopover(trigger)) {
- this.hidePopover(trigger);
- } else {
- readyFn(false);
- const injector = Injector.create({
- parent: hostView.injector, providers: []
- });
- const componentRef = hostView.createComponent(this.componentFactory);
- const component = componentRef.instance;
- this.popoverWithTriggers.push({
- trigger,
- popoverComponent: component
- });
- renderer.removeChild(
- renderer.parentNode(trigger),
- componentRef.location.nativeElement
- );
- const originElementRef = new ElementRef(trigger);
- component.tbAnimationState = 'void';
- component.tbOverlayStyle = { opacity: '0' };
- component.setOverlayOrigin({ elementRef: originElementRef });
- component.tbPlacement = 'bottom';
- component.tbComponentFactory = this.resolver.resolveComponentFactory(HelpMarkdownComponent);
- component.tbComponentInjector = injector;
- component.tbComponentContext = {
- helpId,
- visible: true
- };
- component.tbHideOnClickOutside = true;
- component.tbVisibleChange.subscribe((visible: boolean) => {
- if (!visible) {
- visibleFn(false);
- component.tbAnimationDone.subscribe(() => {
- componentRef.destroy();
- });
- }
- });
- component.tbDestroy.subscribe(() => {
- this.removePopoverByComponent(component);
- });
- const showHelpMarkdownComponent = () => {
- component.tbOverlayStyle = { opacity: '1' };
- component.tbAnimationState = 'active';
- component.updatePosition();
- readyFn(true);
- setTimeout(() => {
- component.updatePosition();
- });
- };
- const setupHelpMarkdownComponent = (helpMarkdownComponent: HelpMarkdownComponent) => {
- if (helpMarkdownComponent.isMarkdownReady) {
- showHelpMarkdownComponent();
- } else {
- helpMarkdownComponent.markdownReady.subscribe(() => {
- showHelpMarkdownComponent();
- });
- }
- };
- if (component.tbComponentRef) {
- setupHelpMarkdownComponent(component.tbComponentRef.instance);
- } else {
- component.tbComponentChange.subscribe((helpMarkdownComponentRef) => {
- setupHelpMarkdownComponent(helpMarkdownComponentRef.instance);
- });
- }
- component.show();
- visibleFn(true);
- }
- }
-
- private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null {
- const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger));
- if (res) {
- return res.popoverComponent;
- } else {
- return null;
- }
- }
-
- private removePopoverByComponent(component: TbPopoverComponent): void {
- const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component);
- if (index > -1) {
- this.popoverWithTriggers.splice(index, 1);
- }
- }
-
- private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean {
- return element1 === element2 || element1.contains(element2) || element2.contains(element1);
- }
-}
-
-
@Component({
selector: 'tb-popover',
exportAs: 'tbPopoverComponent',
@@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
updateStyles(): void {
this.classMap = {
- [this.tbOverlayClassName]: true,
[`tb-popover-placement-${this.preferredPlacement}`]: true,
['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting
};
+ if (this.tbOverlayClassName) {
+ this.classMap[this.tbOverlayClassName] = true;
+ }
}
setOverlayOrigin(origin: CdkOverlayOrigin): void {
diff --git a/ui-ngx/src/app/shared/components/popover.service.ts b/ui-ngx/src/app/shared/components/popover.service.ts
new file mode 100644
index 0000000000..8eb3235ccd
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/popover.service.ts
@@ -0,0 +1,190 @@
+///
+/// Copyright © 2016-2021 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 {
+ ComponentFactory,
+ ComponentFactoryResolver, ElementRef, Inject,
+ Injectable, Injector,
+ Renderer2,
+ Type,
+ ViewContainerRef
+} from '@angular/core';
+import { PopoverPlacement, PopoverWithTrigger } from '@shared/components/popover.models';
+import { TbPopoverComponent } from '@shared/components/popover.component';
+import { ComponentType } from '@angular/cdk/portal';
+import { HELP_MARKDOWN_COMPONENT_TOKEN } from '@shared/components/tokens';
+
+@Injectable()
+export class TbPopoverService {
+
+ private popoverWithTriggers: PopoverWithTrigger[] = [];
+
+ componentFactory: ComponentFactory = this.resolver.resolveComponentFactory(TbPopoverComponent);
+
+ constructor(private resolver: ComponentFactoryResolver,
+ @Inject(HELP_MARKDOWN_COMPONENT_TOKEN) private helpMarkdownComponent: ComponentType) {
+ }
+
+ hasPopover(trigger: Element): boolean {
+ const res = this.findPopoverByTrigger(trigger);
+ return res !== null;
+ }
+
+ hidePopover(trigger: Element): boolean {
+ const component: TbPopoverComponent = this.findPopoverByTrigger(trigger);
+ if (component && component.tbVisible) {
+ component.hide();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ displayPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
+ componentType: Type, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
+ injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {
+ const componentRef = hostView.createComponent(this.componentFactory);
+ const component = componentRef.instance;
+ this.popoverWithTriggers.push({
+ trigger,
+ popoverComponent: component
+ });
+ renderer.removeChild(
+ renderer.parentNode(trigger),
+ componentRef.location.nativeElement
+ );
+ const originElementRef = new ElementRef(trigger);
+ component.setOverlayOrigin({ elementRef: originElementRef });
+ component.tbPlacement = preferredPlacement;
+ component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType);
+ component.tbComponentInjector = injector;
+ component.tbComponentContext = context;
+ component.tbOverlayStyle = overlayStyle;
+ component.tbPopoverInnerStyle = popoverStyle;
+ component.tbComponentStyle = style;
+ component.tbHideOnClickOutside = hideOnClickOutside;
+ component.tbVisibleChange.subscribe((visible: boolean) => {
+ if (!visible) {
+ component.tbAnimationDone.subscribe(() => {
+ componentRef.destroy();
+ });
+ }
+ });
+ component.tbDestroy.subscribe(() => {
+ this.removePopoverByComponent(component);
+ });
+ component.show();
+ return component;
+ }
+
+ toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId = '',
+ helpContent = '',
+ visibleFn: (visible: boolean) => void = () => {},
+ readyFn: (ready: boolean) => void = () => {},
+ preferredPlacement: PopoverPlacement = 'bottom',
+ overlayStyle: any = {}, helpStyle: any = {}) {
+ if (this.hasPopover(trigger)) {
+ this.hidePopover(trigger);
+ } else {
+ readyFn(false);
+ const injector = Injector.create({
+ parent: hostView.injector, providers: []
+ });
+ const componentRef = hostView.createComponent(this.componentFactory);
+ const component = componentRef.instance;
+ this.popoverWithTriggers.push({
+ trigger,
+ popoverComponent: component
+ });
+ renderer.removeChild(
+ renderer.parentNode(trigger),
+ componentRef.location.nativeElement
+ );
+ const originElementRef = new ElementRef(trigger);
+ component.tbAnimationState = 'void';
+ component.tbOverlayStyle = {...overlayStyle, opacity: '0' };
+ component.setOverlayOrigin({ elementRef: originElementRef });
+ component.tbPlacement = preferredPlacement;
+ component.tbComponentFactory = this.resolver.resolveComponentFactory(this.helpMarkdownComponent);
+ component.tbComponentInjector = injector;
+ component.tbComponentContext = {
+ helpId,
+ helpContent,
+ style: helpStyle,
+ visible: true
+ };
+ component.tbHideOnClickOutside = true;
+ component.tbVisibleChange.subscribe((visible: boolean) => {
+ if (!visible) {
+ visibleFn(false);
+ component.tbAnimationDone.subscribe(() => {
+ componentRef.destroy();
+ });
+ }
+ });
+ component.tbDestroy.subscribe(() => {
+ this.removePopoverByComponent(component);
+ });
+ const showHelpMarkdownComponent = () => {
+ component.tbOverlayStyle = {...component.tbOverlayStyle, opacity: '1' };
+ component.tbAnimationState = 'active';
+ component.updatePosition();
+ readyFn(true);
+ setTimeout(() => {
+ component.updatePosition();
+ });
+ };
+ const setupHelpMarkdownComponent = (helpMarkdownComponent: any) => {
+ if (helpMarkdownComponent.isMarkdownReady) {
+ showHelpMarkdownComponent();
+ } else {
+ helpMarkdownComponent.markdownReady.subscribe(() => {
+ showHelpMarkdownComponent();
+ });
+ }
+ };
+ if (component.tbComponentRef) {
+ setupHelpMarkdownComponent(component.tbComponentRef.instance);
+ } else {
+ component.tbComponentChange.subscribe((helpMarkdownComponentRef) => {
+ setupHelpMarkdownComponent(helpMarkdownComponentRef.instance);
+ });
+ }
+ component.show();
+ visibleFn(true);
+ }
+ }
+
+ private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null {
+ const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger));
+ if (res) {
+ return res.popoverComponent;
+ } else {
+ return null;
+ }
+ }
+
+ private removePopoverByComponent(component: TbPopoverComponent): void {
+ const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component);
+ if (index > -1) {
+ this.popoverWithTriggers.splice(index, 1);
+ }
+ }
+
+ private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean {
+ return element1 === element2 || element1.contains(element2) || element2.contains(element1);
+ }
+}
diff --git a/ui-ngx/src/app/shared/components/tokens.ts b/ui-ngx/src/app/shared/components/tokens.ts
new file mode 100644
index 0000000000..8171d52839
--- /dev/null
+++ b/ui-ngx/src/app/shared/components/tokens.ts
@@ -0,0 +1,24 @@
+///
+/// Copyright © 2016-2021 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 { InjectionToken, Type } from '@angular/core';
+import { ComponentType } from '@angular/cdk/portal';
+
+export const HELP_MARKDOWN_COMPONENT_TOKEN: InjectionToken> =
+ new InjectionToken>('HELP_MARKDOWN_COMPONENT_TOKEN');
+
+export const SHARED_MODULE_TOKEN: InjectionToken> =
+ new InjectionToken>('HELP_MARKDOWN_COMPONENT_TOKEN');
diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts
index cf95a154da..03b38345ca 100644
--- a/ui-ngx/src/app/shared/models/constants.ts
+++ b/ui-ngx/src/app/shared/models/constants.ts
@@ -54,7 +54,7 @@ export const MediaBreakpoints = {
'gt-xl': 'screen and (min-width: 5001px)'
};
-const helpBaseUrl = 'https://thingsboard.io';
+export const helpBaseUrl = 'https://thingsboard.io';
export const HelpLinks = {
linksMap: {
diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts
index 24602f758f..2b1de653ec 100644
--- a/ui-ngx/src/app/shared/shared.module.ts
+++ b/ui-ngx/src/app/shared/shared.module.ts
@@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component';
import { HelpPopupComponent } from '@shared/components/help-popup.component';
-import { TbPopoverComponent, TbPopoverDirective, TbPopoverService } from '@shared/components/popover.component';
+import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component';
import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive';
import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive';
import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
import { MarkedOptionsService } from '@shared/components/marked-options.service';
+import { TbPopoverService } from '@shared/components/popover.service';
+import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
+import { TbMarkdownComponent } from '@shared/components/markdown.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
provide: MAT_DATE_LOCALE,
useValue: 'en-GB'
},
+ { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent },
+ { provide: SHARED_MODULE_TOKEN, useValue: SharedModule },
TbPopoverService
],
declarations: [
@@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TbStringTemplateOutletDirective,
TbComponentOutletDirective,
TbPopoverDirective,
+ TbMarkdownComponent,
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
@@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TbStringTemplateOutletDirective,
TbComponentOutletDirective,
TbPopoverDirective,
+ TbMarkdownComponent,
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md
new file mode 100644
index 0000000000..a71cb598d0
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md
@@ -0,0 +1,92 @@
+#### Custom action function
+
+
+
+
+*function ($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
+
+A JavaScript function performing custom action.
+
+**Parameters:**
+
+
+ - $event:
MouseEvent - The MouseEvent object. Usually a result of a mouse click event.
+
+ - widgetContext:
WidgetContext - A reference to WidgetContext that has all necessary API
+ and data used by widget instance.
+
+ - entityId:
string - An optional string id of the target entity.
+
+ - entityName:
string - An optional string name of the target entity.
+
+ {% include widget/action/custom_additional_params %}
+ - entityLabel:
string - An optional string label of the target entity.
+
+
+
+
+
+##### Examples
+
+* Display alert dialog with entity information:
+
+```javascript
+var title;
+var content;
+if (entityName) {
+ title = entityName + ' details';
+ content = 'Entity name: ' + entityName;
+ if (additionalParams && additionalParams.entity) {
+ var entity = additionalParams.entity;
+ if (entity.id) {
+ content += '
Entity type: ' + entity.id.entityType;
+ }
+ if (!isNaN(entity.temperature) && entity.temperature !== '') {
+ content += '
Temperature: ' + entity.temperature + ' °C';
+ }
+ }
+} else {
+ title = 'No entity information available';
+ content = 'No entity information available';
+}
+
+showAlertDialog(title, content);
+
+function showAlertDialog(title, content) {
+ setTimeout(function() {
+ widgetContext.dialogs.alert(title, content).subscribe();
+ }, 100);
+}
+{:copy-code}
+```
+
+* Delete device after confirmation:
+
+```javascript
+var $injector = widgetContext.$scope.$injector;
+var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));
+var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
+
+openDeleteDeviceDialog();
+
+function openDeleteDeviceDialog() {
+ var title = 'Are you sure you want to delete the device ' + entityName + '?';
+ var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!';
+ dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(
+ function(result) {
+ if (result) {
+ deleteDevice();
+ }
+ }
+ );
+}
+
+function deleteDevice() {
+ deviceService.deleteDevice(entityId.id).subscribe(
+ function() {
+ widgetContext.updateAliases();
+ }
+ );
+}
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md
new file mode 100644
index 0000000000..c71e2b63b8
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md
@@ -0,0 +1,46 @@
+ additionalParams: {[key: string]: any} - An optional key/value object holding additional entity parameters depending on widget type and action source:
+
+ - Entities table widget (On row click or Action cell button) - additionalParams:
{ entity: EntityData }:
+
+ - entity:
EntityData - An
+ EntityData object
+ presenting basic entity properties (ex. id, entityName) and
provides access to other entity attributes/timeseries declared in widget datasource configuration.
+
+
+
+ - Alarms table widget (On row click or Action cell button) - additionalParams:
{ alarm: AlarmDataInfo }:
+
+ - alarm:
AlarmDataInfo - An
+ AlarmDataInfo object
+ presenting basic alarm properties (ex. type, severity, originator, etc.) and
provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration.
+
+
+
+ - Timeseries table widget (On row click or Action cell button) - additionalParams:
TimeseriesRow:
+
+ - additionalParams:
TimeseriesRow - A
+ TimeseriesRow object
+ presenting formattedTs (a string value of formatted timestamp) and
timeseries values for each column declared in widget datasource configuration.
+
+
+
+ - Entities hierarchy widget (On node selected) - additionalParams:
{ nodeCtx: HierarchyNodeContext }:
+
+ - nodeCtx:
HierarchyNodeContext - An
+ HierarchyNodeContext object
+ containing entity field holding basic entity properties
(ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration.
+
+
+
+ - Pie - Flot widget (On slice click) - additionalParams:
TbFlotPlotItem:
+
+ - additionalParams:
TbFlotPlotItem - A
+ TbFlotPlotItem object
+ containing series field with information about datasource and
data key of clicked pie slice.
+
+
+
+ - All other widgets - does not provide additionalParams value.
+
+
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md
new file mode 100644
index 0000000000..a9fea185c3
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md
@@ -0,0 +1,74 @@
+#### Custom action (with HTML template) function
+
+
+
+
+*function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void*
+
+A JavaScript function performing custom action with defined HTML template to render dialog.
+
+**Parameters:**
+
+
+ - $event:
MouseEvent - The MouseEvent object. Usually a result of a mouse click event.
+
+ - widgetContext:
WidgetContext - A reference to WidgetContext that has all necessary API
+ and data used by widget instance.
+
+ - entityId:
string - An optional string id of the target entity.
+
+ - entityName:
string - An optional string name of the target entity.
+
+ - htmlTemplate:
string - An optional HTML template string defined in HTML tab.
Used to render custom dialog (see Examples for more details).
+
+ {% include widget/action/custom_additional_params %}
+ - entityLabel:
string - An optional string label of the target entity.
+
+
+
+
+
+##### Examples
+
+###### Display dialog to create a device or an asset
+
+
+
+
+
+
+
+
+
+
+
+###### Display dialog to edit a device or an asset
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md
new file mode 100644
index 0000000000..a604bc3e18
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md
@@ -0,0 +1,160 @@
+#### HTML template of dialog to create a device or an asset
+
+```html
+
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md
new file mode 100644
index 0000000000..9a904bb944
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md
@@ -0,0 +1,132 @@
+#### Function displaying dialog to create a device or an asset
+
+```javascript
+let $injector = widgetContext.$scope.$injector;
+let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
+let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
+let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
+let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
+let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
+
+openAddEntityDialog();
+
+function openAddEntityDialog() {
+ customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();
+}
+
+function AddEntityDialogController(instance) {
+ let vm = instance;
+
+ vm.allowedEntityTypes = ['ASSET', 'DEVICE'];
+ vm.entitySearchDirection = {
+ from: "FROM",
+ to: "TO"
+ }
+
+ vm.addEntityFormGroup = vm.fb.group({
+ entityName: ['', [vm.validators.required]],
+ entityType: ['DEVICE'],
+ entityLabel: [null],
+ type: ['', [vm.validators.required]],
+ attributes: vm.fb.group({
+ latitude: [null],
+ longitude: [null],
+ address: [null],
+ owner: [null],
+ number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
+ booleanValue: [null]
+ }),
+ relations: vm.fb.array([])
+ });
+
+ vm.cancel = function () {
+ vm.dialogRef.close(null);
+ };
+
+ vm.relations = function () {
+ return vm.addEntityFormGroup.get('relations');
+ };
+
+ vm.addRelation = function () {
+ vm.relations().push(vm.fb.group({
+ relatedEntity: [null, [vm.validators.required]],
+ relationType: [null, [vm.validators.required]],
+ direction: [null, [vm.validators.required]]
+ }));
+ };
+
+ vm.removeRelation = function (index) {
+ vm.relations().removeAt(index);
+ vm.relations().markAsDirty();
+ };
+
+ vm.save = function () {
+ vm.addEntityFormGroup.markAsPristine();
+ saveEntityObservable().subscribe(
+ function (entity) {
+ widgetContext.rxjs.forkJoin([
+ saveAttributes(entity.id),
+ saveRelations(entity.id)
+ ]).subscribe(
+ function () {
+ widgetContext.updateAliases();
+ vm.dialogRef.close(null);
+ }
+ );
+ }
+ );
+ };
+
+ function saveEntityObservable() {
+ const formValues = vm.addEntityFormGroup.value;
+ let entity = {
+ name: formValues.entityName,
+ type: formValues.type,
+ label: formValues.entityLabel
+ };
+ if (formValues.entityType == 'ASSET') {
+ return assetService.saveAsset(entity);
+ } else if (formValues.entityType == 'DEVICE') {
+ return deviceService.saveDevice(entity);
+ }
+ }
+
+ function saveAttributes(entityId) {
+ let attributes = vm.addEntityFormGroup.get('attributes').value;
+ let attributesArray = [];
+ for (let key in attributes) {
+ if (attributes[key] !== null) {
+ attributesArray.push({key: key, value: attributes[key]});
+ }
+ }
+ if (attributesArray.length > 0) {
+ return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
+ }
+ return widgetContext.rxjs.of([]);
+ }
+
+ function saveRelations(entityId) {
+ let relations = vm.addEntityFormGroup.get('relations').value;
+ let tasks = [];
+ for (let i = 0; i < relations.length; i++) {
+ let relation = {
+ type: relations[i].relationType,
+ typeGroup: 'COMMON'
+ };
+ if (relations[i].direction == 'FROM') {
+ relation.to = relations[i].relatedEntity;
+ relation.from = entityId;
+ } else {
+ relation.to = entityId;
+ relation.from = relations[i].relatedEntity;
+ }
+ tasks.push(entityRelationService.saveRelation(relation));
+ }
+ if (tasks.length > 0) {
+ return widgetContext.rxjs.forkJoin(tasks);
+ }
+ return widgetContext.rxjs.of([]);
+ }
+}
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md
new file mode 100644
index 0000000000..2737070a26
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md
@@ -0,0 +1,192 @@
+#### HTML template of dialog to edit a device or an asset
+
+```html
+
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md
new file mode 100644
index 0000000000..5a222e9aee
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md
@@ -0,0 +1,220 @@
+#### Function displaying dialog to edit a device or an asset
+
+```javascript
+let $injector = widgetContext.$scope.$injector;
+let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
+let entityService = $injector.get(widgetContext.servicesMap.get('entityService'));
+let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
+let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
+let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
+let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
+
+openEditEntityDialog();
+
+function openEditEntityDialog() {
+ customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();
+}
+
+function EditEntityDialogController(instance) {
+ let vm = instance;
+
+ vm.entityName = entityName;
+ vm.entityType = entityId.entityType;
+ vm.entitySearchDirection = {
+ from: "FROM",
+ to: "TO"
+ };
+ vm.attributes = {};
+ vm.oldRelationsData = [];
+ vm.relationsToDelete = [];
+ vm.entity = {};
+
+ vm.editEntityFormGroup = vm.fb.group({
+ entityName: ['', [vm.validators.required]],
+ entityType: [null],
+ entityLabel: [null],
+ type: ['', [vm.validators.required]],
+ attributes: vm.fb.group({
+ latitude: [null],
+ longitude: [null],
+ address: [null],
+ owner: [null],
+ number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
+ booleanValue: [false]
+ }),
+ oldRelations: vm.fb.array([]),
+ relations: vm.fb.array([])
+ });
+
+ getEntityInfo();
+
+ vm.cancel = function() {
+ vm.dialogRef.close(null);
+ };
+
+ vm.relations = function() {
+ return vm.editEntityFormGroup.get('relations');
+ };
+
+ vm.oldRelations = function() {
+ return vm.editEntityFormGroup.get('oldRelations');
+ };
+
+ vm.addRelation = function() {
+ vm.relations().push(vm.fb.group({
+ relatedEntity: [null, [vm.validators.required]],
+ relationType: [null, [vm.validators.required]],
+ direction: [null, [vm.validators.required]]
+ }));
+ };
+
+ function addOldRelation() {
+ vm.oldRelations().push(vm.fb.group({
+ relatedEntity: [{value: null, disabled: true}, [vm.validators.required]],
+ relationType: [{value: null, disabled: true}, [vm.validators.required]],
+ direction: [{value: null, disabled: true}, [vm.validators.required]]
+ }));
+ }
+
+ vm.removeRelation = function(index) {
+ vm.relations().removeAt(index);
+ vm.relations().markAsDirty();
+ };
+
+ vm.removeOldRelation = function(index) {
+ vm.oldRelations().removeAt(index);
+ vm.relationsToDelete.push(vm.oldRelationsData[index]);
+ vm.oldRelations().markAsDirty();
+ };
+
+ vm.save = function() {
+ vm.editEntityFormGroup.markAsPristine();
+ widgetContext.rxjs.forkJoin([
+ saveAttributes(entityId),
+ saveRelations(entityId),
+ saveEntity()
+ ]).subscribe(
+ function () {
+ widgetContext.updateAliases();
+ vm.dialogRef.close(null);
+ }
+ );
+ };
+
+ function getEntityAttributes(attributes) {
+ for (var i = 0; i < attributes.length; i++) {
+ vm.attributes[attributes[i].key] = attributes[i].value;
+ }
+ }
+
+ function getEntityRelations(relations) {
+ let relationsFrom = relations[0];
+ let relationsTo = relations[1];
+ for (let i=0; i < relationsFrom.length; i++) {
+ let relation = {
+ direction: 'FROM',
+ relationType: relationsFrom[i].type,
+ relatedEntity: relationsFrom[i].to
+ };
+ vm.oldRelationsData.push(relation);
+ addOldRelation();
+ }
+ for (let i=0; i < relationsTo.length; i++) {
+ let relation = {
+ direction: 'TO',
+ relationType: relationsTo[i].type,
+ relatedEntity: relationsTo[i].from
+ };
+ vm.oldRelationsData.push(relation);
+ addOldRelation();
+ }
+ }
+
+ function getEntityInfo() {
+ widgetContext.rxjs.forkJoin([
+ entityRelationService.findInfoByFrom(entityId),
+ entityRelationService.findInfoByTo(entityId),
+ attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'),
+ entityService.getEntity(entityId.entityType, entityId.id)
+ ]).subscribe(
+ function (data) {
+ getEntityRelations(data.slice(0,2));
+ getEntityAttributes(data[2]);
+ vm.entity = data[3];
+ vm.editEntityFormGroup.patchValue({
+ entityName: vm.entity.name,
+ entityType: vm.entityType,
+ entityLabel: vm.entity.label,
+ type: vm.entity.type,
+ attributes: vm.attributes,
+ oldRelations: vm.oldRelationsData
+ }, {emitEvent: false});
+ }
+ );
+ }
+
+ function saveEntity() {
+ const formValues = vm.editEntityFormGroup.value;
+ if (vm.entity.label !== formValues.entityLabel){
+ vm.entity.label = formValues.entityLabel;
+ if (formValues.entityType == 'ASSET') {
+ return assetService.saveAsset(vm.entity);
+ } else if (formValues.entityType == 'DEVICE') {
+ return deviceService.saveDevice(vm.entity);
+ }
+ }
+ return widgetContext.rxjs.of([]);
+ }
+
+ function saveAttributes(entityId) {
+ let attributes = vm.editEntityFormGroup.get('attributes').value;
+ let attributesArray = [];
+ for (let key in attributes) {
+ if (attributes[key] !== vm.attributes[key]) {
+ attributesArray.push({key: key, value: attributes[key]});
+ }
+ }
+ if (attributesArray.length > 0) {
+ return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
+ }
+ return widgetContext.rxjs.of([]);
+ }
+
+ function saveRelations(entityId) {
+ let relations = vm.editEntityFormGroup.get('relations').value;
+ let tasks = [];
+ for(let i=0; i < relations.length; i++) {
+ let relation = {
+ type: relations[i].relationType,
+ typeGroup: 'COMMON'
+ };
+ if (relations[i].direction == 'FROM') {
+ relation.to = relations[i].relatedEntity;
+ relation.from = entityId;
+ } else {
+ relation.to = entityId;
+ relation.from = relations[i].relatedEntity;
+ }
+ tasks.push(entityRelationService.saveRelation(relation));
+ }
+ for (let i=0; i < vm.relationsToDelete.length; i++) {
+ let relation = {
+ type: vm.relationsToDelete[i].relationType
+ };
+ if (vm.relationsToDelete[i].direction == 'FROM') {
+ relation.to = vm.relationsToDelete[i].relatedEntity;
+ relation.from = entityId;
+ } else {
+ relation.to = entityId;
+ relation.from = vm.relationsToDelete[i].relatedEntity;
+ }
+ tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to));
+ }
+ if (tasks.length > 0) {
+ return widgetContext.rxjs.forkJoin(tasks);
+ }
+ return widgetContext.rxjs.of([]);
+ }
+}
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md
index 59a15c953b..459a71e8cc 100644
--- a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md
+++ b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_cell_fn.md
@@ -1,5 +1,48 @@
-#### Show cell button action JavaScript Function
+#### Show cell button action function
+
+
+
+
+*function (widgetContext, data): boolean*
+
+A JavaScript function evaluating whether to display particular table cell action.
+
+**Parameters:**
+
+
+ - widgetContext:
WidgetContext - A reference to WidgetContext that has all necessary API
+ and data used by widget instance.
+
+ - data:
FormattedData - A FormattedData object of specific table row.
+ Represents basic entity properties (ex. entityId, entityName)
and provides access to other entity attributes/timeseries declared in widget datasource configuration.
+
+
+
+**Returns:**
+
+`true` if cell action should be displayed, `false` otherwise.
+
+
+
+##### Examples
+
+* Display action only for customer users:
+
+```javascript
+return widgetContext.currentUser.authority === 'CUSTOMER_USER';
+{:copy-code}
+```
+
+* Display action only if the entity in the row is device and has type `thermostat`:
+
+```javascript
+return data && data.entityType === 'DEVICE' && data.Type === 'thermostat';
+{:copy-code}
+```
+
+* Display action only if the entity in the row has `temperature` latest timeseries or attribute value greater than 25:
```javascript
-return data.entityName === 'Test device'; {:copy-code}
+return data && data.temperature > 25;
+{:copy-code}
```
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_header_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_header_fn.md
index 2f7e66496d..01bcc0ef11 100644
--- a/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_header_fn.md
+++ b/ui-ngx/src/assets/help/en_US/widget/action/show_widget_action_header_fn.md
@@ -5,15 +5,15 @@
*function (widgetContext, data): boolean*
-JavaScript function evaluating whether to display particular widget header action.
+A JavaScript function evaluating whether to display particular widget header action.
**Parameters:**
- - widgetContext - A reference to WidgetContext that has all necessary API
+ - widgetContext:
WidgetContext - A reference to WidgetContext that has all necessary API
and data used by widget instance.
- - data - An array of FormattedData objects.
+ - data:
FormattedData[] - An array of FormattedData objects.
Each object represents basic entity properties (ex. entityId, entityName)
and provides access to other entity attributes/timeseries declared in widget datasource configuration.
diff --git a/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md b/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md
new file mode 100644
index 0000000000..3dbece51c6
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md
@@ -0,0 +1,59 @@
+#### Data generation function
+
+
+
+
+*function (time, prevValue): any*
+
+A JavaScript function generating datapoint values.
+
+**Parameters:**
+
+
+ - time:
number - timestamp in milliseconds of the current datapoint.
+
+ - prevValue:
primitive (number/string/boolean) - A previous datapoint value.
+
+
+
+**Returns:**
+
+A primitive type (number, string or boolean) presenting newly generated datapoint value.
+
+
+
+##### Examples
+
+* Generate data with sine function:
+
+```javascript
+return Math.sin(time/5000);
+{:copy-code}
+```
+
+* Generate true/false sequence:
+
+```javascript
+if (!prevValue) {
+ return true;
+} else {
+ return false;
+}
+{:copy-code}
+```
+
+* Generate repeating sequence of predefined values (for ex. latitude):
+
+```javascript
+var lats = [37.7696499,
+ 37.7699074,
+ 37.7699536,
+ 37.7697242,
+ 37.7695189,
+ 37.7696889];
+
+var index = Math.floor((time/3 % 14000) / 1000);
+
+return lats[index];
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md b/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md
new file mode 100644
index 0000000000..6fd0cf867f
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md
@@ -0,0 +1,56 @@
+#### Data post-processing function
+
+
+
+
+*function (time, value, prevValue, timePrev, prevOrigValue): any*
+
+A JavaScript function doing post-processing on telemetry data.
+
+**Parameters:**
+
+
+ - time:
number - timestamp in milliseconds of the current datapoint.
+
+ - value:
primitive (number/string/boolean) - A value of the current datapoint.
+
+ - prevValue:
primitive (number/string/boolean) - A value of the previous datapoint after applied post-processing.
+
+ - timePrev:
number - timestamp in milliseconds of the previous datapoint value.
+
+ - prevOrigValue:
primitive (number/string/boolean) - An original value of the previous datapoint.
+
+
+
+**Returns:**
+
+A primitive type (number, string or boolean) presenting the new datapoint value.
+
+
+
+##### Examples
+
+* Multiply all datapoint values by 10:
+
+```javascript
+return value * 10;
+{:copy-code}
+```
+
+* Round all datapoint values to whole numbers:
+
+```javascript
+return Math.round(value);
+{:copy-code}
+```
+
+* Get relative difference between data points:
+
+```javascript
+if (prevOrigValue) {
+ return (value - prevOrigValue) / prevOrigValue;
+} else {
+ return 0;
+}
+{:copy-code}
+```
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md
index 10a05f5d4e..c3451cfe16 100644
--- a/ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md
+++ b/ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md
@@ -1,12 +1,37 @@
-#### Tooltip value format JavaScript Function
+#### Tooltip value format function
-##### Input arguments
+
+
-- value - value to format
+*function (value): string*
+
+A JavaScript function used to format datapoint value to be shown on the chart tooltip.
+
+**Parameters:**
+
+
+ - value:
primitive (number/string/boolean) - A value of the datapoint that should be formatted.
+
+
+
+**Returns:**
+
+A string representing the formatted value.
+
+
+
+##### Examples
+
+* Present the datapoint value in tooltip in Celsius (°C) units:
```javascript
-return value;
+return value + ' °C';
{:copy-code}
```
-##### Examples
+* Present the datapoint value in tooltip in Amperage (A) units and two decimal places:
+
+```javascript
+return value.toFixed(2) + ' A';
+{:copy-code}
+```
diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss
index 461b59efdd..1dd50841a3 100644
--- a/ui-ngx/src/styles.scss
+++ b/ui-ngx/src/styles.scss
@@ -516,12 +516,15 @@ mat-label {
}
p, div {
- padding-left: 32px;
- padding-right: 32px;
color: rgba(15, 22, 29, 0.8);
line-height: 1.5em;
}
+ & > p, & > div {
+ padding-right: 32px;
+ padding-left: 32px;
+ }
+
ul {
padding-left: 62px;
padding-right: 32px;
@@ -541,6 +544,12 @@ mat-label {
li {
padding-bottom: .75em;
line-height: 1.5em;
+ ul {
+ margin-bottom: 0;
+ }
+ p {
+ padding-left: 0;
+ }
}
a {
@@ -651,7 +660,7 @@ mat-label {
line-height: 24px;
color: #fff;
background-color: #305680;
- box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.12), 0px 2px 2px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
text-decoration: none;
font-size: 16px;
font-weight: 500;
@@ -777,7 +786,7 @@ mat-label {
button.clipboard-btn {
top: -10px;
- right: 0px;
+ right: 0;
padding: 0 3px;
}
}
diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock
index c34ca170fd..f07fd482e6 100644
--- a/ui-ngx/yarn.lock
+++ b/ui-ngx/yarn.lock
@@ -1824,10 +1824,10 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
-"@types/marked@^1.1.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4"
- integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw==
+"@types/marked@^2.0.0":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@types/marked/-/marked-2.0.5.tgz#453e27f1e97199d45bb25297b0dd2b9bbc1e05ea"
+ integrity sha512-shRZ7XnYFD/8n8zSjKvFdto1QNSf4tONZIlNEZGrJe8GsOE8DL/hG1Hbl8gZlfLnjS7+f5tZGIaTgfpyW38h4w==
"@types/minimatch@*":
version "3.0.3"
@@ -3268,6 +3268,11 @@ commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+commander@^6.0.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+ integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+
commander@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
@@ -6171,12 +6176,12 @@ karma@~6.3.2:
ua-parser-js "^0.7.23"
yargs "^16.1.1"
-katex@^0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
- integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==
+katex@^0.13.0:
+ version "0.13.18"
+ resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931"
+ integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA==
dependencies:
- commander "^2.19.0"
+ commander "^6.0.0"
killable@^1.0.1:
version "1.0.1"
@@ -6484,10 +6489,10 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
-marked@^1.1.0:
- version "1.2.9"
- resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc"
- integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==
+marked@^2.0.0:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753"
+ integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==
material-design-icons@^3.0.1:
version "3.0.1"
@@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1:
hammerjs "^2.0.8"
resize-observer-polyfill "^1.5.1"
-ngx-markdown@^10.1.1:
- version "10.1.1"
- resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-10.1.1.tgz#17840c773db7ced4b18ccbf2e8cb06182e422de3"
- integrity sha512-bUVgN6asb35d5U4xM5CNfo7pSpuwqJSdTgK0PhNZzLiaiyPIK2owtLF6sWGhxTThJu+LngJPjj4MQ+AFe/s8XQ==
+ngx-markdown@^11.1.3:
+ version "11.1.3"
+ resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-11.1.3.tgz#4d01c2dc425e5a46d1b6aa8517d9a1c1feaa1efd"
+ integrity sha512-z32q8l76ubrcP62L03mdvrizwueLBHV10LkT8MEDnFcjmY+8J1PytxFJ9EBTJpvc+CaPolgAoi7felN2XJZTSg==
dependencies:
- "@types/marked" "^1.1.0"
+ "@types/marked" "^2.0.0"
emoji-toolkit "^6.0.1"
- katex "^0.12.0"
- marked "^1.1.0"
- prismjs "^1.20.0"
+ katex "^0.13.0"
+ marked "^2.0.0"
+ prismjs "^1.23.0"
tslib "^2.0.0"
ngx-sharebuttons@^8.0.5:
@@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
-prismjs@^1.20.0:
- version "1.24.1"
- resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"
- integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==
-
prismjs@^1.23.0:
version "1.23.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"