UI: Add notification template wizard

This commit is contained in:
Vladyslav_Prykhodko 2023-01-12 18:11:37 +02:00
parent 91a4de7780
commit 9fec151214
10 changed files with 412 additions and 62 deletions

View File

@ -49,6 +49,11 @@
"input": "./node_modules/ace-builds/src-noconflict/",
"output": "/"
},
{
"glob": "**/*",
"input": "node_modules/tinymce",
"output": "/assets/tinymce/"
},
{
"glob": "marker-icon-2x.png",
"input": "node_modules/leaflet/dist/images/",

View File

@ -40,6 +40,7 @@
"@ngrx/store-devtools": "^12.5.1",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@tinymce/tinymce-angular": "~4.2.4",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",
"angular-gridster2": "~12.1.1",
@ -93,6 +94,7 @@
"split.js": "^1.6.4",
"systemjs": "6.11.0",
"tinycolor2": "^1.4.2",
"tinymce": "~5.10.7",
"tooltipster": "^4.2.8",
"ts-transformer-keys": "^0.4.3",
"tslib": "^2.3.1",

View File

@ -63,7 +63,8 @@ export class TemplateTableConfig extends EntityTableConfig<NotificationTemplate>
this.columns.push(
new EntityTableColumn<NotificationTemplate>('notificationType', 'notification.type', '15%'),
new EntityTableColumn<NotificationTemplate>('name', 'notification.template', '25%'),
new EntityTableColumn<NotificationTemplate>('subject', 'notification.subject', '25%'),
new EntityTableColumn<NotificationTemplate>('configuration.notificationSubject', 'notification.subject', '25%',
(template) => template.configuration.notificationSubject, () => ({}), false),
new EntityTableColumn<NotificationTemplate>('configuration.defaultTextTemplate', 'notification.message', '35%',
(template) => template.configuration.defaultTextTemplate, () => ({}), false)
);

View File

@ -27,48 +27,128 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<form mat-dialog-content [formGroup]="templateNotificationForm">
<mat-form-field class="mat-block">
<mat-label translate>notification.target-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="templateNotificationForm.get('name').hasError('required')">
{{ 'notification.target-name-required' | translate }}
</mat-error>
</mat-form-field>
<label style="margin-bottom: 8px">{{ 'notification.type' | translate }}<sup> *</sup></label>
<mat-radio-group formControlName="type">
<mat-radio-button [value]="NotificationType.GENERIC">
<div fxLayout="column">
<div>Generic</div>
<div class="tb-hint">hint</div>
</div>
</mat-radio-button>
<mat-radio-button [value]="NotificationType.ALARM">
<div>Alarm</div>
<div class="tb-hint">hint</div>
</mat-radio-button>
</mat-radio-group>
<section formGroupName="configuration">
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="defaultTextTemplate" required>
<mat-error *ngIf="templateNotificationForm.get('configuration.defaultTextTemplate').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.subject</mat-label>
<input matInput required>
</mat-form-field>
</section>
</form>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async) || templateNotificationForm.invalid || !templateNotificationForm.dirty"
color="primary"
(click)="save()">{{ saveButtonLabel | translate }}</button>
<div mat-dialog-content [formGroup]="templateNotificationForm">
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationTemplate [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="templateNotificationForm">
<ng-template matStepLabel>{{ 'notification.basic-settings' | translate }}</ng-template>
<form [formGroup]="templateNotificationForm" style="padding-bottom: 16px;">
<mat-form-field class="mat-block">
<mat-label translate>notification.target-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="templateNotificationForm.get('name').hasError('required')">
{{ 'notification.target-name-required' | translate }}
</mat-error>
</mat-form-field>
<label style="margin-bottom: 8px">{{ 'notification.type' | translate }}</label>
<mat-radio-group formControlName="notificationType">
<mat-radio-button *ngFor="let notificationType of notificationTypes" [value]="notificationType">
<div fxLayout="column">
<div>{{ notificationTemplateTypeTranslateMap.get(notificationType).name | translate }}</div>
<div class="tb-hint">{{ notificationTemplateTypeTranslateMap.get(notificationType).hint }}</div>
</div>
</mat-radio-button>
</mat-radio-group>
<section formGroupName="configuration">
<mat-form-field class="mat-block">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="notificationSubject" required>
<mat-error *ngIf="templateNotificationForm.get('configuration.notificationSubject').hasError('required')">
{{ 'notification.subject-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="defaultTextTemplate" required>
<mat-error *ngIf="templateNotificationForm.get('configuration.defaultTextTemplate').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<label style="margin-bottom: 8px; display: block;">Delivery methods <sup>*</sup></label>
<section formGroupName="deliveryMethodsTemplates" fxLayout="row warap">
<section *ngFor="let deliveryMethods of notificationDeliveryMethods; even as isEven" class="delivery-method-container"
[formGroupName]="deliveryMethods" fxFlex="50%" [ngClass]={even:isEven}>
<mat-checkbox class="delivery-method" formControlName="enabled">{{ deliveryMethods }}</mat-checkbox>
</section>
</section>
<div class="tb-hint">At least one should be selected</div>
</section>
</form>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.PUSH.enabled').value"
[stepControl]="pushTemplateForm">
<ng-template matStepLabel>{{ 'notification.web-settings' | translate }}</ng-template>
<form [formGroup]="pushTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="subject" [placeholder]="templateNotificationForm.get('configuration.notificationSubject').value">
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="body" [placeholder]="templateNotificationForm.get('configuration.defaultTextTemplate').value">
</mat-form-field>
</form>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.EMAIL.enabled').value"
[stepControl]="emailTemplateForm">
<ng-template matStepLabel>{{ 'notification.email-settings' | translate }}</ng-template>
<ng-template matStepContent>
<form [formGroup]="emailTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="subject" [placeholder]="templateNotificationForm.get('configuration.notificationSubject').value">
</mat-form-field>
<mat-label class="tb-title" translate>notification.message</mat-label>
<editor [init]="tinyMceOptions" formControlName="body"></editor>
</form>
</ng-template>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.SMS.enabled').value"
[stepControl]="smsTemplateForm">
<ng-template matStepLabel>{{ 'notification.sms-settings' | translate }}</ng-template>
<form [formGroup]="smsTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="body" [placeholder]="templateNotificationForm.get('configuration.defaultTextTemplate').value">
</mat-form-field>
</form>
</mat-step>
<mat-step optional *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.SLACK.enabled').value"
[stepControl]="slackTemplateForm">
<ng-template matStepLabel>{{ 'notification.slack-settings' | translate }}</ng-template>
<form [formGroup]="slackTemplateForm">
<mat-form-field class="mat-block" floatLabel="always">
<mat-label translate>notification.message</mat-label>
<input matInput formControlName="body" [placeholder]="templateNotificationForm.get('configuration.defaultTextTemplate').value">
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always">
<mat-label>Conversation type</mat-label>
<input matInput formControlName="conversationType">
</mat-form-field>
<mat-form-field class="mat-block" floatLabel="always">
<mat-label>Conversation id</mat-label>
<input matInput formControlName="conversationId" required>
<mat-error *ngIf="slackTemplateForm.get('conversationId').hasError('required')">
{{ 'notification.subject-required' | translate }}
</mat-error>
</mat-form-field>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row">
<button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span>
<button *ngIf="selectedIndex > 0"
mat-button mat-stroked-button color="primary">
{{ 'notification.send-test-notification' | translate }}
</button>
<button mat-raised-button
color="primary"
(click)="nextStep()">{{ nextStepLabel() | translate }}</button>
</div>

View File

@ -1,12 +1,72 @@
:host {
width: 600px;
@import "../../../../../../scss/constants";
:host-context(.tb-fullscreen-dialog .mat-dialog-container) {
width: 720px;
.tb-hint {
padding: 0;
}
.delivery-method-container {
padding-bottom: 8px;
&.even {
padding-right: 8px;
}
.delivery-method {
padding: 16px 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
width: 100%;
}
}
@media #{$mat-gt-xs} {
.mat-dialog-content {
max-height: 75vh;
}
}
}
:host ::ng-deep {
.mat-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 !important;
.mat-stepper-horizontal {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
@media #{$mat-lt-sm} {
.mat-step-label {
white-space: normal;
overflow: visible;
.mat-step-text-label {
overflow: visible;
}
}
}
.mat-horizontal-content-container {
height: 500px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
scrollbar-gutter: stable;
@media #{$mat-gt-sm} {
min-width: 500px;
}
}
.mat-horizontal-stepper-content[aria-expanded=true] {
height: 100%;
position: relative;
}
}
}
.mat-radio-group {
display: flex;
flex-direction: row;

View File

@ -14,8 +14,14 @@
/// limitations under the License.
///
import { NotificationTemplate, NotificationType } from '@shared/models/notification.models';
import { Component, Inject, OnDestroy } from '@angular/core';
import {
DeliveryMethodNotificationTemplate,
NotificationDeliveryMethod,
NotificationTemplate,
NotificationTemplateType,
NotificationTemplateTypeTranslateMap, SlackChanelType
} from '@shared/models/notification.models';
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -24,7 +30,13 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service';
import { deepTrim, isDefined } from '@core/utils';
import { Subject } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper';
import { MatStepper } from '@angular/material/stepper';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { NotificationType } from '@core/notification/notification.models';
export interface TemplateNotificationDialogData {
template?: NotificationTemplate;
@ -40,11 +52,25 @@ export interface TemplateNotificationDialogData {
export class TemplateNotificationDialogComponent
extends DialogComponent<TemplateNotificationDialogComponent, NotificationTemplate> implements OnDestroy {
@ViewChild('addNotificationTemplate', {static: true}) addNotificationTemplate: MatStepper;
stepperOrientation: Observable<StepperOrientation>;
templateNotificationForm: FormGroup;
pushTemplateForm: FormGroup;
emailTemplateForm: FormGroup;
smsTemplateForm: FormGroup;
slackTemplateForm: FormGroup;
dialogTitle = 'notification.edit-notification-template';
saveButtonLabel = 'action.save';
NotificationType = NotificationType;
notificationTypes = Object.keys(NotificationTemplateType) as NotificationType[];
notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[];
notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap;
selectedIndex = 0;
tinyMceOptions: Record<string, any>;
private readonly destroy$ = new Subject<void>();
@ -52,6 +78,7 @@ export class TemplateNotificationDialogComponent
protected router: Router,
protected dialogRef: MatDialogRef<TemplateNotificationDialogComponent, NotificationTemplate>,
@Inject(MAT_DIALOG_DATA) public data: TemplateNotificationDialogData,
private breakpointObserver: BreakpointObserver,
private fb: FormBuilder,
private notificationService: NotificationService) {
super(store, router, dialogRef);
@ -64,13 +91,57 @@ export class TemplateNotificationDialogComponent
this.dialogTitle = 'notification.copy-notification-template';
}
this.tinyMceOptions = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['link table image imagetools code fullscreen'],
menubar: 'edit insert tools view format table',
toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' +
'| link | table | image | alignleft aligncenter alignright alignjustify ' +
'| numlist bullist outdent indent | removeformat | code | fullscreen',
height: 400,
autofocus: false,
branding: false
};
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
this.templateNotificationForm = this.fb.group({
name: [null, Validators.required],
type: [NotificationType.GENERIC],
name: ['', Validators.required],
notificationType: [NotificationTemplateType.GENERAL],
configuration: this.fb.group({
defaultTextTemplate: [null, Validators.required]
notificationSubject: ['', Validators.required],
defaultTextTemplate: ['', Validators.required],
deliveryMethodsTemplates: this.fb.group({})
})
});
this.notificationDeliveryMethods.forEach(method => {
(this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup)
.addControl(method, this.fb.group({method, enabled: method === NotificationDeliveryMethod.PUSH}), {emitEvent: false});
});
this.pushTemplateForm = this.fb.group({
subject: [''],
body: [''],
icon: [''],
actionButtonConfig: ['']
});
this.emailTemplateForm = this.fb.group({
subject: [''],
body: ['']
});
this.smsTemplateForm = this.fb.group({
body: ['']
});
this.slackTemplateForm = this.fb.group({
body: [''],
conversationId: ['', Validators.required],
conversationType: [SlackChanelType.DIRECT]
});
}
ngOnDestroy() {
@ -92,4 +163,81 @@ export class TemplateNotificationDialogComponent
(target) => this.dialogRef.close(target)
);
}
changeStep($event: StepperSelectionEvent) {
this.selectedIndex = $event.selectedIndex;
}
backStep() {
this.addNotificationTemplate.previous();
}
nextStep() {
if (this.selectedIndex >= this.maxStepperIndex) {
this.add();
} else {
this.addNotificationTemplate.next();
}
}
nextStepLabel(): string {
if (this.selectedIndex === 1 && this.selectedIndex < this.maxStepperIndex && this.pushTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex === 2 && this.selectedIndex < this.maxStepperIndex && this.emailTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.smsTemplateForm.pristine) {
return 'action.skip';
}
if (this.selectedIndex >= this.maxStepperIndex) {
return 'action.add';
}
return 'action.next';
}
private get maxStepperIndex(): number {
return this.addNotificationTemplate?._steps?.length - 1;
}
private add(): void {
if (this.allValid()) {
const formValue: NotificationTemplate = this.templateNotificationForm.value;
if (formValue.configuration.deliveryMethodsTemplates.PUSH.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.PUSH, this.pushTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.PUSH;
}
if (formValue.configuration.deliveryMethodsTemplates.EMAIL.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.EMAIL, this.emailTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.EMAIL;
}
if (formValue.configuration.deliveryMethodsTemplates.SMS.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.SMS, this.smsTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.SMS;
}
if (formValue.configuration.deliveryMethodsTemplates.SLACK.enabled) {
Object.assign(formValue.configuration.deliveryMethodsTemplates.SLACK, this.slackTemplateForm.value);
} else {
delete formValue.configuration.deliveryMethodsTemplates.SLACK;
}
this.notificationService.saveNotificationTemplate(deepTrim(formValue)).subscribe(
(target) => this.dialogRef.close(target)
);
}
}
allValid(): boolean {
return !this.addNotificationTemplate.steps.find((item, index) => {
if (item.stepControl.invalid) {
item.interacted = true;
this.addNotificationTemplate.selectedIndex = index;
return true;
} else {
return false;
}
});
}
}

View File

@ -122,18 +122,22 @@ interface CustomerUsersNotificationTargetConfig {
export interface NotificationTemplate extends Omit<BaseData<NotificationTemplateId>, 'label'>{
tenantId: TenantId;
notificationType: NotificationType;
notificationType: NotificationTemplateType;
configuration: NotificationTemplateConfig;
}
interface NotificationTemplateConfig {
defaultTextTemplate: string;
deliveryMethodsTemplates: Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate>;
notificationSubject: string;
deliveryMethodsTemplates: {
[key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate
};
}
interface DeliveryMethodNotificationTemplate extends
export interface DeliveryMethodNotificationTemplate extends
Partial<PushDeliveryMethodNotificationTemplate & EmailDeliveryMethodNotificationTemplate & SlackDeliveryMethodNotificationTemplate>{
body?: string;
enabled: boolean;
method: NotificationDeliveryMethod;
}
@ -176,7 +180,7 @@ export enum NotificationRequestStatus {
}
export enum SlackChanelType {
USER= 'USER',
DIRECT= 'DIRECT',
PUBLIC_CHANNEL = 'PUBLIC_CHANNEL',
PRIVATE_CHANNEL = 'PRIVATE_CHANNEL'
}
@ -193,7 +197,26 @@ export const NotificationTargetConfigTypeTranslateMap = new Map<NotificationTarg
[NotificationTargetConfigType.CUSTOMER_USERS, 'notification.target-type.customer-users'],
]);
export enum NotificationType {
GENERIC = 'GENERIC',
export enum NotificationTemplateType {
GENERAL = 'GENERAL',
ALARM = 'ALARM'
}
interface NotificationTemplateTypeTranslate {
name: string;
hint?: string;
}
export const NotificationTemplateTypeTranslateMap = new Map<NotificationTemplateType, NotificationTemplateTypeTranslate>([
[NotificationTemplateType.GENERAL,
{
name: 'notification.template-type.general',
hint: 'hint'
}
],
[NotificationTemplateType.ALARM,
{
name: 'notification.template-type.alarm',
hint: 'hint'
}]
]);

View File

@ -64,6 +64,7 @@ import { ShareModule as ShareButtonsModule } from 'ngx-sharebuttons';
import { HotkeyModule } from 'angular2-hotkeys';
import { ColorPickerModule } from 'ngx-color-picker';
import { NgxHmCarouselModule } from 'ngx-hm-carousel';
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
import { UserMenuComponent } from '@shared/components/user-menu.component';
import { NospacePipe } from '@shared/pipe/nospace.pipe';
import { TranslateModule } from '@ngx-translate/core';
@ -188,6 +189,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
provide: FlowInjectionToken,
useValue: Flow
},
{
provide: TINYMCE_SCRIPT_SRC,
useValue: 'assets/tinymce/tinymce.min.js'
},
{
provide: MAT_DATE_LOCALE,
useValue: 'en-GB'
@ -457,6 +462,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HotkeyModule,
ColorPickerModule,
NgxHmCarouselModule,
EditorModule,
DndModule,
NgxFlowchartModule,
MarkdownModule,

View File

@ -55,13 +55,15 @@
"continue": "Continue",
"discard-changes": "Discard Changes",
"download": "Download",
"next": "Next",
"next-with-label": "Next: {{label}}",
"read-more": "Read more",
"hide": "Hide",
"done": "Done",
"print": "Print",
"restore": "Restore",
"confirm": "Confirm"
"confirm": "Confirm",
"skip": "Skip"
},
"aggregation": {
"aggregation": "Aggregation",
@ -2697,6 +2699,7 @@
"sent": "Sent",
"settings": "Notification settings",
"subject": "Subject",
"subject-required": "Subject is required",
"target-name": "Name",
"target-name-required": "Name is required",
"target-type": {
@ -2707,9 +2710,19 @@
},
"targets": "Targets",
"template": "Template",
"template-type": {
"general": "General",
"alarm": "Alarm"
},
"templates": "Templates",
"text": "Text",
"type": "Type"
"type": "Type",
"basic-settings": "Basic settings",
"send-test-notification": "Send test notification",
"web-settings": "Web settings",
"email-settings": "Email settings",
"sms-settings": "SMS settings",
"slack-settings": "Slack settings"
},
"ota-update": {
"add": "Add package",

View File

@ -1641,6 +1641,13 @@
"@angular-devkit/schematics" "12.2.17"
jsonc-parser "3.0.0"
"@tinymce/tinymce-angular@~4.2.4":
version "4.2.4"
resolved "https://registry.yarnpkg.com/@tinymce/tinymce-angular/-/tinymce-angular-4.2.4.tgz#45974f39fcdb1af6f30dba6405bfb0b8a3db748f"
integrity sha512-A+FIvhTahHaqDuvcXovuhzvpCR2yjUbPuF+Z/t/MnNaCv/9Y0w7W5Qgmo5wARIW+NM7o8msWcMSxOMK4xkv10Q==
dependencies:
tslib "^1.10.0"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -9582,6 +9589,11 @@ tinycolor2@^1.4.2:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
tinymce@~5.10.7:
version "5.10.7"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.7.tgz#d89d446f1962f2a1df6b2b70018ce475ec7ffb80"
integrity sha512-9UUjaO0R7FxcFo0oxnd1lMs7H+D0Eh+dDVo5hKbVe1a+VB0nit97vOqlinj+YwgoBDt6/DSCUoWqAYlLI8BLYA==
tmp@0.0.30:
version "0.0.30"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed"