UI: Added support js module resource in widget/action

This commit is contained in:
Vladyslav_Prykhodko 2023-06-07 15:37:47 +03:00
parent 87786ef72d
commit 1bde8d3dff
19 changed files with 419 additions and 64 deletions

View File

@ -96,7 +96,7 @@
"schema-inspector": "^2.0.2",
"screenfull": "^6.0.2",
"split.js": "^1.6.5",
"systemjs": "6.11.0",
"systemjs": "6.14.1",
"tinycolor2": "^1.6.0",
"tinymce": "~5.10.7",
"tooltipster": "^4.2.8",
@ -137,7 +137,7 @@
"@types/raphael": "^2.3.2",
"@types/react": "17.0.37",
"@types/react-dom": "17.0.11",
"@types/systemjs": "6.1.1",
"@types/systemjs": "6.13.1",
"@types/tinycolor2": "^1.4.3",
"@types/tooltipster": "^0.0.31",
"@typescript-eslint/eslint-plugin": "5.57.0",

View File

@ -20,8 +20,9 @@ import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo } from '@shared/models/resource.models';
import { Resource, ResourceInfo, ResourceType } from '@shared/models/resource.models';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils';
@Injectable({
providedIn: 'root'
@ -33,15 +34,22 @@ export class ResourceService {
}
public getResources(pageLink: PageLink, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
return this.http.get<PageData<ResourceInfo>>(`/api/resource${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
let url = `/api/resource${pageLink.toQuery()}`;
if (isNotEmptyStr(resourceType)) {
url += `&resourceType=${resourceType}`;
}
return this.http.get<PageData<ResourceInfo>>(url, defaultHttpOptionsFromConfig(config));
}
public getResource(resourceId: string, config?: RequestConfig): Observable<Resource> {
return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public getResourceInfo(resourceId: string, config?: RequestConfig): Observable<ResourceInfo> {
return this.http.get<Resource>(`/api/resource/info/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public downloadResource(resourceId: string): Observable<any> {
return this.http.get(`/api/resource/${resourceId}/download`, {
responseType: 'arraybuffer',

View File

@ -27,6 +27,9 @@ import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { IModulesMap } from '@modules/common/modules-map.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { isObject } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
declare const System;
@ -69,16 +72,18 @@ export class ResourcesService {
return this.loadResourceByType(fileType, url);
}
public loadFactories(url: string, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
public loadFactories(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModulesAndFactories[url]) {
return this.loadedModulesAndFactories[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<ModulesWithFactories>();
this.loadedModulesAndFactories[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url).then(
System.import(url, undefined, meta).then(
(module) => {
const modules = this.extractNgModules(module);
if (modules.length) {
@ -123,16 +128,18 @@ export class ResourcesService {
return subject.asObservable();
}
public loadModules(url: string, modulesMap: IModulesMap): Observable<Type<any>[]> {
public loadModules(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<Type<any>[]> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<Type<any>[]>();
this.loadedModules[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url).then(
System.import(url, undefined, meta).then(
(module) => {
try {
let modules;
@ -246,4 +253,21 @@ export class ResourcesService {
this.anchor.appendChild(el);
return subject.asObservable();
}
private getDownloadUrl(resourceId: string | TbResourceId): string {
if (isObject(resourceId)) {
return `/api/resource/js/${(resourceId as TbResourceId).id}/download`;
}
return resourceId as string;
}
private getMetaInfo(resourceId: string | TbResourceId): object {
if (isObject(resourceId)) {
return {
additionalHeaders: {
'X-Authorization': `Bearer ${AuthService.getJwtToken()}`
}
};
}
}
}

View File

@ -614,6 +614,13 @@ class ModulesMap implements IModulesMap {
for (const moduleId of Object.keys(this.modulesMap)) {
System.set('app:' + moduleId, this.modulesMap[moduleId]);
}
System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download');
System.constructor.prototype.fetch = (url, options: RequestInit & {meta?: any}) => {
if (options?.meta?.additionalHeaders) {
options.headers = { ...options.headers, ...options.meta.additionalHeaders };
}
return fetch(url, options);
};
this.initialized = true;
}
}

View File

@ -22,11 +22,14 @@
<div fxFlex fxLayout="row"
fxLayoutAlign="start center"
*ngFor="let resource of action.customResources; let i = index" style="padding-top: 10px;">
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<tb-resource-autocomplete fxFlex class="mat-block"
subscriptSizing="dynamic"
hideRequiredMarker required
[allowAutocomplete]="resource.isModule"
[(ngModel)]="resource.url"
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="notifyActionUpdated()">
{{ 'widget.resource-is-module' | translate }}
@ -40,16 +43,15 @@
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 18px;">
<button mat-button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
type="button"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
<button mat-button mat-raised-button color="primary"
style="margin-top: 18px;"
[disabled]="isLoading$ | async"
type="button"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>

View File

@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
import { QueueComponent } from '@home/pages/admin/queue/queue.component';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@ -44,6 +45,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
OAuth2SettingsComponent,
HomeSettingsComponent,
ResourcesLibraryComponent,
ResourcesTableHeaderComponent,
QueueComponent,
RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent,

View File

@ -36,6 +36,7 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-
import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
@Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
@ -53,6 +54,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.entityComponent = ResourcesLibraryComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.headerComponent = ResourcesTableHeaderComponent;
this.config.entityTitle = (resource) => resource ?
resource.title : '';
@ -81,7 +83,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink);
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType);
this.config.loadEntity = id => this.resourceService.getResource(id.id);
this.config.saveEntity = resource => this.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
@ -110,6 +112,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
resolve(): EntityTableConfig<Resource, PageLink, ResourceInfo> {
this.config.tableTitle = this.translate.instant('resource.resources-library');
this.config.componentsData = {
resourceType: ''
};
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);

View File

@ -57,7 +57,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() {
super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe(
startWith(ResourceType.LWM2M_MODEL),
startWith(ResourceType.JS_MODULE),
filter(() => this.isAdd),
takeUntil(this.destroy$)
).subscribe((type) => {
@ -91,7 +91,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
buildForm(entity: Resource): FormGroup {
return this.fb.group({
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required],
fileName: [entity ? entity.fileName : null, Validators.required],
data: [entity ? entity.data : null, Validators.required]
});

View File

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>resource.resource-type</mat-label>
<mat-select [ngModel]="entitiesTableConfig.componentsData.resourceType"
(ngModelChange)="resourceTypeChanged($event)"
placeholder="{{ 'resource.resource-type' | translate }}">
<mat-option value="">
{{ "resource.all-types" | translate }}
</mat-option>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>

View File

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { PageLink } from '@shared/models/page/page-link';
@Component({
selector: 'tb-resources-table-header',
templateUrl: './resources-table-header.component.html',
styleUrls: []
})
export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent<Resource, PageLink, ResourceInfo> {
readonly resourceTypes: ResourceType[] = Object.values(ResourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
constructor(protected store: Store<AppState>) {
super(store);
}
resourceTypeChanged(resourceType: ResourceType) {
this.entitiesTableConfig.componentsData.resourceType = resourceType;
this.entitiesTableConfig.getTable().resetSortAndFilter(true);
}
}

View File

@ -122,12 +122,14 @@
<div fxFlex fxLayout="row" style="max-height: 40px;"
fxLayoutAlign="start center"
*ngFor="let resource of widget.resources; let i = index" >
<mat-form-field fxFlex class="mat-block resource-field" hideRequiredMarker="false"
style="margin: 10px 0 0 0; max-height: 40px;">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<tb-resource-autocomplete fxFlex class="mat-block resource-field"
hideRequiredMarker required
subscriptSizing="dynamic"
[allowAutocomplete]="resource.isModule"
[(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="isDirty = true">
{{ 'widget.resource-is-module' | translate }}
@ -140,15 +142,14 @@
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 6px;">
<button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
<button mat-raised-button color="primary"
style="margin-top: 8px;"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>

View File

@ -31,18 +31,21 @@ tb-widget-editor {
overflow-y: auto;
}
mat-form-field.resource-field {
max-height: 40px;
margin: 10px 0 0;
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
.mat-mdc-form-field-flex {
max-height: 40px;
.mat-mdc-form-field-infix {
border: 0;
padding-top: 7px;
padding-bottom: 7px;
min-height: 32px;
.resource-field {
mat-form-field {
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
height: 40px;
.mat-mdc-form-field-flex {
max-height: 40px;
.mat-mdc-form-field-infix {
border: 0;
padding-top: 7px;
padding-bottom: 7px;
min-height: 32px;
}
}
}
}

View File

@ -22,4 +22,5 @@ export * from './js-func.component';
export * from './script-lang.component';
export * from './slack-conversation-autocomplete.component';
export * from './notification/template-autocomplete.component';
export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component';

View File

@ -0,0 +1,46 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="resourceFormGroup" class="mat-block"
[appearance]="appearance"
[hideRequiredMarker]="hideRequiredMarker"
[subscriptSizing]="subscriptSizing">
<input matInput type="text"
#resourceInput
formControlName="resource"
(focusin)="onFocus()"
[placeholder]="placeholder"
[required]="required"
[matAutocomplete]="entityAutocomplete"
[matAutocompleteDisabled]="!allowAutocomplete">
<button *ngIf="resourceFormGroup.get('resource').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#entityAutocomplete="matAutocomplete"
[displayWith]="displayResourceFn">
<mat-option *ngFor="let resource of filteredResources$ | async" [value]="resource">
<span [innerHTML]="resource.title | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="searchText">
{{ searchText }}
</mat-option>
</mat-autocomplete>
</mat-form-field>

View File

@ -0,0 +1,179 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { isDefinedAndNotNull, isEmptyStr, isEqual, isObject } from '@core/utils';
import { ResourceInfo, ResourceType } from '@shared/models/resource.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { ResourceService } from '@core/http/resource.service';
import { PageLink } from '@shared/models/page/page-link';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-resource-autocomplete',
templateUrl: './resource-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ResourceAutocompleteComponent),
multi: true
}]
})
export class ResourceAutocompleteComponent implements ControlValueAccessor, OnInit {
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
@coerceBoolean()
required: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
placeholder: string;
@Input()
@coerceBoolean()
hideRequiredMarker = false;
@Input()
@coerceBoolean()
allowAutocomplete = false;
resourceFormGroup = this.fb.group({
resource: [null]
});
filteredResources$: Observable<Array<ResourceInfo>>;
searchText = '';
@ViewChild('resourceInput', {static: true}) resourceInput: ElementRef;
private modelValue: string | TbResourceId;
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder,
private resourceService: ResourceService) {
}
ngOnInit(): void {
if(this.required) {
this.resourceFormGroup.get('resource').setValidators(Validators.required);
this.resourceFormGroup.get('resource').updateValueAndValidity({emitEvent: false});
}
this.filteredResources$ = this.resourceFormGroup.get('resource').valueChanges
.pipe(
debounceTime(150),
tap(value => {
let modelValue;
if (isObject(value)) {
modelValue = value.id;
} else if (isEmptyStr(value)) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.title) : ''),
switchMap(name => this.fetchResources(name) ),
share()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.resourceFormGroup.disable({emitEvent: false});
} else {
this.resourceFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | TbResourceId) {
if (isDefinedAndNotNull(value)) {
this.searchText = '';
if (isObject(value) && typeof value !== 'string' && (value as TbResourceId).id) {
this.resourceService.getResourceInfo(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe(resource => {
this.modelValue = resource.id;
this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
});
} else {
this.modelValue = value;
this.resourceFormGroup.get('resource').patchValue(value, {emitEvent: false});
}
this.dirty = true;
}
}
displayResourceFn(resource?: ResourceInfo | string): string {
return isObject(resource) ? (resource as ResourceInfo).title : resource as string;
}
clear() {
this.resourceFormGroup.get('resource').patchValue('', {emitEvent: true});
setTimeout(() => {
this.resourceInput.nativeElement.blur();
this.resourceInput.nativeElement.focus();
}, 0);
}
onFocus() {
if (this.dirty) {
this.resourceFormGroup.get('resource').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
private updateView(value: string | TbResourceId ) {
if (!isEqual(this.modelValue, value)) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
private fetchResources(searchText?: string): Observable<Array<ResourceInfo>> {
this.searchText = searchText;
return this.resourceService.getResources(new PageLink(50, 0, searchText), ResourceType.JS_MODULE, {ignoreLoading: true}).pipe(
catchError(() => of(null)),
map(data => data.data)
);
}
}

View File

@ -52,7 +52,7 @@ export const ResourceTypeTranslationMap = new Map<ResourceType, string>(
]
);
export interface ResourceInfo extends BaseData<TbResourceId> {
export interface ResourceInfo extends Omit<BaseData<TbResourceId>, 'name' | 'label'> {
tenantId?: TenantId;
resourceKey?: string;
title?: string;
@ -62,6 +62,7 @@ export interface ResourceInfo extends BaseData<TbResourceId> {
export interface Resource extends ResourceInfo {
data: string;
fileName: string;
name?: string;
}
export interface Resources extends ResourceInfo {

View File

@ -189,6 +189,7 @@ import {
GtMdLgShowHideDirective
} from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
@ -360,6 +361,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent
],
imports: [
@ -586,6 +588,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent
]
})

View File

@ -3252,6 +3252,7 @@
},
"resource": {
"add": "Add Resource",
"all-types": "All",
"copyId": "Copy resource Id",
"delete": "Delete resource",
"delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",

View File

@ -3286,10 +3286,10 @@
dependencies:
"@types/react" "*"
"@types/systemjs@6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.1.1.tgz#eae17f2a080e867d01a2dd614f524ab227cf5a41"
integrity sha512-d1M6eDKBGWx7RbYy295VEFoOF9YDJkPI959QYnmzcmeaV+SP4D0xV7dEh3sN5XF3GvO3PhGzm+17Z598nvHQuQ==
"@types/systemjs@6.13.1":
version "6.13.1"
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.13.1.tgz#fccf8049fdf328bca4cfbad3a9cc7bf088b45048"
integrity sha512-Jxo2/uif1WpkabfyvWpFmPWFPDdwKUmyL7xWzjtxNALEu2pgce+eISjbf0Vr+SsK/D9savO5kTRcf+COLK5eiQ==
"@types/tinycolor2@^1.4.3":
version "1.4.3"
@ -10101,10 +10101,10 @@ symbol-observable@4.0.0:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==
systemjs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.11.0.tgz#8df8e74fc05822e6c40170aa409b9ca64833315f"
integrity sha512-7YPIY44j+BoY+E6cGBSw0oCU8SNTTIHKZgftcBdwWkDzs/M86Fdlr21FrzAyph7Zo8r3CFGscyFe4rrBtixrBg==
systemjs@6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.1.tgz#95a580b91b50d0d69ff178ed4816f0ddbcea23c1"
integrity sha512-8ftwWd+XnQtZ/aGbatrN4QFNGrKJzmbtixW+ODpci7pyoTajg4sonPP8aFLESAcuVxaC1FyDESt+SpfFCH9rZQ==
table-layout@^1.0.2:
version "1.0.2"