Enabled strict template mode in Angular

Code cleanup
pull/809/head
Tzahi12345 2 years ago
parent e82066b2cd
commit 6010d991fb

@ -2109,8 +2109,6 @@ components:
tag:
type: string
DBInfoResponse:
required:
- db_info
type: object
properties:
using_local_db:
@ -2132,6 +2130,8 @@ components:
$ref: '#/components/schemas/TableInfo'
download_queue:
$ref: '#/components/schemas/TableInfo'
archives:
$ref: '#/components/schemas/TableInfo'
TransferDBResponse:
required:
- success

@ -190,5 +190,8 @@
"@schematics/angular:directive": {
"prefix": "app"
}
},
"cli": {
"analytics": false
}
}

@ -782,7 +782,7 @@ app.post('/api/restartServer', optionalJwt, (req, res) => {
app.get('/api/getDBInfo', optionalJwt, async (req, res) => {
const db_info = await db_api.getDBStats();
res.send({db_info: db_info});
res.send(db_info);
});
app.post('/api/transferDB', optionalJwt, async (req, res) => {
@ -1084,9 +1084,6 @@ app.post('/api/disableSharing', optionalJwt, async function(req, res) {
await db_api.updateRecord('files', {uid: uid}, {sharingEnabled: false})
} else if (is_playlist) {
await db_api.updateRecord(`playlists`, {id: uid}, {sharingEnabled: false});
} else if (type === 'subscription') {
// TODO: Implement. Main blocker right now is subscription videos are not stored in the DB, they are searched for every
// time they are requested from the subscription directory.
} else {
// error
success = false;

@ -4,7 +4,7 @@
<div *ngFor="let permission of available_permissions">
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
<div matListItemLine>
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission, permissions[permission])" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>

@ -5,7 +5,7 @@
<div class="example-header">
<mat-form-field appearance="outline">
<mat-label i18n="Search">Search</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)">
<input matInput (keyup)="applyFilter($event)">
</mat-form-field>
</div>

@ -63,7 +63,8 @@ export class ModifyUsersComponent implements OnInit, AfterViewInit {
this.pageSizeOptions = setPageSizeOptionsInput.split(',').map(str => +str);
}
applyFilter(filterValue: string) {
applyFilter(event: KeyboardEvent) {
let filterValue = (event.target as HTMLInputElement).value; // "as HTMLInputElement" is required: https://angular.io/guide/user-input#type-the-event
filterValue = filterValue.trim(); // Remove whitespace
filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches
this.dataSource.filter = filterValue;

@ -75,7 +75,7 @@
<div class="container">
<div class="row justify-content-center">
<div class="col-10 select-file-title">
<mat-icon class="audio-video-icon">{{(file.type === 'audio' || file.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
<mat-icon class="audio-video-icon">{{file.isAudio ? 'audiotrack' : 'movie'}}</mat-icon>
{{file.title}}
</div>
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
@ -88,7 +88,7 @@
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<mat-selection-list *ngIf="!normal_files_received">
<mat-list-option *ngFor="let file of paged_data">
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" width="250" height="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
<content-loader class="list-ghosts" [primaryColor]="postsService.theme.ghost_primary" [secondaryColor]="postsService.theme.ghost_secondary" [width]="250" [height]="8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option>
</mat-selection-list>
</ng-container>

@ -7,6 +7,7 @@ import { Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MatChipListboxChange } from '@angular/material/chips';
import { MatSelectionListChange } from '@angular/material/list';
@Component({
selector: 'app-recent-videos',
@ -376,8 +377,9 @@ export class RecentVideosComponent implements OnInit {
this.getAllFiles();
}
fileSelectionChanged(event: { option: { _selected: boolean; value: DatabaseFile; } }): void {
const adding = event.option._selected;
fileSelectionChanged(event: MatSelectionListChange): void {
// TODO: make sure below line is possible (_selected is private)
const adding = event.option['_selected'];
const value = event.option.value;
if (adding) {
this.selected_data.push(value.uid);

@ -33,8 +33,8 @@ export class SortPropertyComponent {
@Input() sortProperty = 'registered';
@Input() descendingMode = true;
@Output() sortPropertyChange = new EventEmitter<unknown>();
@Output() descendingModeChange = new EventEmitter<number>();
@Output() sortPropertyChange = new EventEmitter<string>();
@Output() descendingModeChange = new EventEmitter<boolean>();
@Output() sortOptionChanged = new EventEmitter<Sort>();
toggleModeChange(): void {

@ -45,7 +45,7 @@ export class TwitchChatComponent implements OnInit, OnDestroy {
return position > height - threshold;
}
scrollToBottom = (force_scroll) => {
scrollToBottom = (force_scroll = false) => {
if (force_scroll || this.isUserNearBottom()) {
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
}

@ -5,7 +5,7 @@
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
</div>
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="250" [height]="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<!-- The context menu trigger must be kept above the "more info" menu -->
<div style="visibility: hidden; position: fixed"
[style.left]="contextMenuPosition.x"
@ -68,11 +68,11 @@
</div>
<div *ngIf="loading" class="img-div">
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="100" height="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
<content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="100" [height]="55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
</div>
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" width="250" height="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
<span *ngIf="loading" class="title-loading"><content-loader [primaryColor]="theme.ghost_primary" [secondaryColor]="theme.ghost_secondary" [width]="250" [height]="30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
</div>
</mat-card>
</div>

@ -10,7 +10,7 @@
<mat-chip-grid class="example-chip" #chipList aria-label="Args array" cdkDropList cdkDropListDisabled
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)">
<mat-chip-row [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" *ngFor="let arg of args_array; let i = index;" [selectable]="selectable" [removable]="removable" (removed)="remove(i)" cdkDrag>
<mat-chip-row [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" *ngFor="let arg of args_array; let i = index;" [removable]="removable" (removed)="remove(i)" cdkDrag>
{{arg}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip-row>

@ -3,7 +3,7 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { UntypedFormControl } from '@angular/forms';
import { args, args_info } from './youtubedl_args';
import { args, ArgsByCategory, args_info } from './youtubedl_args';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators/map';
import { startWith } from 'rxjs/operators/startWith';
@ -38,7 +38,7 @@ export class ArgModifierDialogComponent implements OnInit, AfterViewInit {
stateCtrl = new UntypedFormControl();
chipCtrl = new UntypedFormControl();
availableArgs = null;
argsByCategory = null;
argsByCategory: ArgsByCategory = null;
argsByKey = null;
argsInfo = null;
filteredOptions: Observable<any>;

@ -72,7 +72,7 @@ const download = [
{'key': '--hls-prefer-ffmpeg', 'description': 'Use ffmpeg instead of the native HLS downloader'},
{'key': '--hls-use-mpegts', 'description': 'Use the mpegts container for HLS videos, allowing to play the video while downloading (some players may not be able to play it)'},
{'key': '--external-downloader', 'description': 'Use the specified external downloader. Currently supports aria2c,avconv,axel,curl,ffmpeg,httpie,wget'},
{'key': '--external-downloader-args'}
{'key': '--external-downloader-args', 'description': 'Give these arguments to the external downloader'}
];
const filesystem = [
@ -195,7 +195,7 @@ const post_processing = [
{'key': '--convert-subs', 'description': 'Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)'}
];
export const args_info = {
export const args_info: ArgsInfo = {
'uncategorized' : {'label': 'Main'},
'network' : {'label': 'Network'},
'geo_restriction': {'label': 'Geo Restriction'},
@ -212,7 +212,7 @@ export const args_info = {
'post_processing': {'label': 'Post Processing'},
};
export const args = {
export const args: ArgsByCategory = {
'uncategorized' : uncategorized,
'network' : network,
'geo_restriction': geo_restriction,
@ -228,3 +228,8 @@ export const args = {
'adobe_pass' : adobe_pass,
'post_processing': post_processing
}
export type ArgInfo = {label: string}
export type ArgsInfo = {[key: string]: ArgInfo}
export type Arg = {key: string, description: string};
export type ArgsByCategory = {[key: string]: Arg[]};

@ -42,7 +42,7 @@
N/A
</mat-option>
<mat-option *ngFor="let available_category of postsService.categories | keyvalue" [value]="available_category.value">
{{available_category.value.name}}
{{available_category.value['name']}}
</mat-option>
</mat-select>
</mat-form-field>

@ -19,7 +19,7 @@
Quality
</ng-container>
</mat-label>
<mat-select [disabled]="url === '' || cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']" [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged($event)">
<mat-select [disabled]="url === '' || cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']" [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged()">
<mat-option i18n="Best" [value]="''">
Best
</mat-option>
@ -67,7 +67,7 @@
</div>
</form>
<br/>
<mat-checkbox [disabled]="autoplay && current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px; margin-left: 4px;">
<mat-checkbox [disabled]="autoplay && !!current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px; margin-left: 4px;">
<ng-container i18n="Only Audio checkbox">
Only Audio
</ng-container>
@ -112,13 +112,13 @@
<div class="container" style="padding-bottom: 20px;">
<div class="row">
<div class="col-12 col-sm-6">
<mat-checkbox color="accent" [disabled]="current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use custom args checkbox">
Use custom args
</ng-container>
</mat-checkbox>
<button class="edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || !!current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Replace args">
Replace args
</ng-container>
@ -134,7 +134,7 @@
</mat-form-field>
</div>
<div class="col-12 col-sm-6">
<mat-checkbox color="accent" [disabled]="current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use custom output checkbox">
Use custom output
</ng-container>
@ -149,7 +149,7 @@
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use authentication checkbox">
Use authentication
</ng-container>
@ -166,7 +166,7 @@
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<mat-checkbox color="accent" [disabled]="!!current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Crop video checkbox">
Crop file
</ng-container>

@ -4,9 +4,9 @@
<mat-drawer-container style="height: 100%" class="example-container" autosize>
<div style="height: fit-content" [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="$any(media)" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
</video>
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" [sponsor_block_cache]="sponsor_block_cache" class="skip-ad-button"></app-skip-ad-button>
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" class="skip-ad-button"></app-skip-ad-button>
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
@ -34,7 +34,7 @@
</ng-container>
<ng-container *ngIf="db_file">
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
<button *ngIf="type !== 'subscription' && (!postsService.isLoggedIn || postsService.permissions.includes('sharing'))" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('sharing')" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
</ng-container>
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
@ -44,7 +44,7 @@
</div>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="true" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>
</div>

@ -110,6 +110,7 @@ import {
} from '../api-types';
import { isoLangs } from './settings/locales_list';
import { Title } from '@angular/platform-browser';
import { MatDrawerMode } from '@angular/material/sidenav';
@Injectable()
export class PostsService implements CanActivate {
@ -119,7 +120,7 @@ export class PostsService implements CanActivate {
THEMES_CONFIG = THEMES_CONFIG;
theme;
card_size = 'medium';
sidepanel_mode = 'over';
sidepanel_mode: MatDrawerMode = 'over';
// auth
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
@ -215,8 +216,8 @@ export class PostsService implements CanActivate {
if (yes_reload) { this.reloadConfig(); }
});
if (localStorage.getItem('sidepanel_mode')) {
this.sidepanel_mode = localStorage.getItem('sidepanel_mode');
if (localStorage.getItem('sidepanel_mode') as MatDrawerMode) {
this.sidepanel_mode = localStorage.getItem('sidepanel_mode') as MatDrawerMode;
}
if (localStorage.getItem('card_size')) {

@ -521,7 +521,7 @@
</div>
<mat-divider></mat-divider>
<mat-form-field style="margin-top: 15px;">
<mat-input i18n="Auth method">Auth method</mat-input>
<mat-label i18n="Auth method">Auth method</mat-label>
<mat-select [(ngModel)]="new_config['Users']['auth_method']">
<mat-option value="internal">
<ng-container i18n="Internal auth method">Internal</ng-container>
@ -534,31 +534,31 @@
<div *ngIf="new_config['Users']['auth_method'] === 'ldap'">
<div>
<mat-form-field>
<mat-input i18n="LDAP URL">LDAP URL</mat-input>
<mat-label i18n="LDAP URL">LDAP URL</mat-label>
<input matInput [(ngModel)]="new_config['Users']['ldap_config']['url']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-input i18n="Bind DN">Bind DN</mat-input>
<mat-label i18n="Bind DN">Bind DN</mat-label>
<input matInput [(ngModel)]="new_config['Users']['ldap_config']['bindDN']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-input i18n="Bind Credentials">Bind Credentials</mat-input>
<mat-label i18n="Bind Credentials">Bind Credentials</mat-label>
<input matInput [(ngModel)]="new_config['Users']['ldap_config']['bindCredentials']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-input i18n="Search Base">Search Base</mat-input>
<mat-label i18n="Search Base">Search Base</mat-label>
<input matInput [(ngModel)]="new_config['Users']['ldap_config']['searchBase']">
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-input i18n="Search Filter">Search Filter</mat-input>
<mat-label i18n="Search Filter">Search Filter</mat-label>
<input matInput [(ngModel)]="new_config['Users']['ldap_config']['searchFilter']">
</mat-form-field>
</div>

@ -13,7 +13,7 @@ import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/edit-category-dialog.component';
import { ActivatedRoute, Router } from '@angular/router';
import { Category } from 'api-types';
import { Category, DBInfoResponse } from 'api-types';
import { GenerateRssUrlComponent } from 'app/dialogs/generate-rss-url/generate-rss-url.component';
@Component({
@ -32,7 +32,7 @@ export class SettingsComponent implements OnInit {
generated_bookmarklet_code = null;
bookmarkletAudioOnly = false;
db_info = null;
db_info: DBInfoResponse = null;
db_transferring = false;
testing_connection_string = false;
@ -315,7 +315,7 @@ export class SettingsComponent implements OnInit {
getDBInfo(): void {
this.postsService.getDBInfo().subscribe(res => {
this.db_info = res['db_info'];
this.db_info = res;
});
}

@ -26,4 +26,7 @@
"exclude": [
"assets/default.json"
],
"angularCompilerOptions": {
"strictTemplates": true,
}
}
Loading…
Cancel
Save