Browse Source

UI: Add markdown widget settings form

pull/6545/head
Igor Kulikov 5 years ago
parent
commit
0177904eb2
  1. 5
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 38
      ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html
  3. 76
      ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts
  4. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  5. 3
      ui-ngx/src/app/shared/components/css.component.scss
  6. 8
      ui-ngx/src/app/shared/components/js-func.component.html
  7. 22
      ui-ngx/src/app/shared/components/js-func.component.scss
  8. 47
      ui-ngx/src/app/shared/components/markdown-editor.component.html
  9. 77
      ui-ngx/src/app/shared/components/markdown-editor.component.scss
  10. 154
      ui-ngx/src/app/shared/components/markdown-editor.component.ts
  11. 3
      ui-ngx/src/app/shared/shared.module.ts
  12. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json

5
application/src/main/data/json/system/widget_bundles/cards.json

@ -186,8 +186,9 @@
"templateHtml": "<tb-markdown-widget \n [ctx]=\"ctx\">\n</tb-markdown-widget>",
"templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Markdown/HTML card\",\n \"properties\": {\n \"markdownTextPattern\": {\n \"title\": \"Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.\"\n },\n \"markdownCss\": {\n \"title\": \"Markdown/HTML CSS\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useMarkdownTextFunction\": {\n \"title\": \"Use markdown/HTML value function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markdownTextFunction\": {\n \"title\": \"Markdown/HTML value function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useMarkdownTextFunction\",\n {\n \"key\": \"markdownTextPattern\",\n \"type\": \"markdown\",\n \"condition\": \"model.useMarkdownTextFunction !== true\"\n },\n {\n \"key\": \"markdownTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/markdown/markdown_text_fn\",\n \"condition\": \"model.useMarkdownTextFunction === true\"\n },\n {\n \"key\": \"markdownCss\",\n \"type\": \"css\"\n }\n ]\n}\n",
"dataKeySettingsSchema": "{}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-markdown-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
},

38
ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html

@ -0,0 +1,38 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="markdownWidgetSettingsForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="useMarkdownTextFunction">
{{ 'widgets.markdown.use-markdown-text-function' | translate }}
</mat-slide-toggle>
<section fxLayout="column" [fxShow]="markdownWidgetSettingsForm.get('useMarkdownTextFunction').value">
<tb-js-func formControlName="markdownTextFunction"
[globalVariables]="functionScopeVariables"
[functionArgs]="['data']"
functionTitle="{{ 'widgets.markdown.markdown-text-function' | translate }}"
helpId="widget/lib/markdown/markdown_text_fn">
</tb-js-func>
</section>
<section fxLayout="column" [fxShow]="!markdownWidgetSettingsForm.get('useMarkdownTextFunction').value">
<tb-markdown-editor formControlName="markdownTextPattern"
label="{{ 'widgets.markdown.markdown-text-pattern' | translate }}">
</tb-markdown-editor>
</section>
<tb-css formControlName="markdownCss"
label="{{ 'widgets.markdown.markdown-css' | translate }}">
</tb-css>
</section>

76
ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts

@ -0,0 +1,76 @@
///
/// Copyright © 2016-2022 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-markdown-widget-settings',
templateUrl: './markdown-widget-settings.component.html',
styleUrls: ['./widget-settings.scss']
})
export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent {
markdownWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.markdownWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
useMarkdownTextFunction: false,
markdownTextPattern: '# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.',
markdownTextFunction: 'return \'# Some title\\\\n - Entity name: \' + data[0][\'entityName\'];',
markdownCss: ''
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.markdownWidgetSettingsForm = this.fb.group({
useMarkdownTextFunction: [settings.useMarkdownTextFunction, []],
markdownTextPattern: [settings.markdownTextPattern, []],
markdownTextFunction: [settings.qrCodeTextFunction, []],
markdownCss: [settings.markdownCss, []]
});
}
protected validatorTriggers(): string[] {
return ['useMarkdownTextFunction'];
}
protected updateValidators(emitEvent: boolean) {
const useMarkdownTextFunction: boolean = this.markdownWidgetSettingsForm.get('useMarkdownTextFunction').value;
if (useMarkdownTextFunction) {
this.markdownWidgetSettingsForm.get('markdownTextPattern').disable();
this.markdownWidgetSettingsForm.get('markdownTextFunction').enable();
} else {
this.markdownWidgetSettingsForm.get('markdownTextPattern').enable();
this.markdownWidgetSettingsForm.get('markdownTextFunction').disable();
}
this.markdownWidgetSettingsForm.get('markdownTextPattern').updateValueAndValidity({emitEvent});
this.markdownWidgetSettingsForm.get('markdownTextFunction').updateValueAndValidity({emitEvent});
}
}

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

@ -29,13 +29,17 @@ import {
import {
TimeseriesTableLatestKeySettingsComponent
} from '@home/components/widget/lib/settings/timeseries-table-latest-key-settings.component';
import {
MarkdownWidgetSettingsComponent
} from '@home/components/widget/lib/settings/markdown-widget-settings.component';
@NgModule({
declarations: [
QrCodeWidgetSettingsComponent,
TimeseriesTableWidgetSettingsComponent,
TimeseriesTableKeySettingsComponent,
TimeseriesTableLatestKeySettingsComponent
TimeseriesTableLatestKeySettingsComponent,
MarkdownWidgetSettingsComponent
],
imports: [
CommonModule,
@ -46,7 +50,8 @@ import {
QrCodeWidgetSettingsComponent,
TimeseriesTableWidgetSettingsComponent,
TimeseriesTableKeySettingsComponent,
TimeseriesTableLatestKeySettingsComponent
TimeseriesTableLatestKeySettingsComponent,
MarkdownWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -56,5 +61,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent,
'tb-timeseries-table-widget-settings': TimeseriesTableWidgetSettingsComponent,
'tb-timeseries-table-key-settings': TimeseriesTableKeySettingsComponent,
'tb-timeseries-table-latest-key-settings': TimeseriesTableLatestKeySettingsComponent
'tb-timeseries-table-latest-key-settings': TimeseriesTableLatestKeySettingsComponent,
'tb-markdown-widget-settings': MarkdownWidgetSettingsComponent
};

3
ui-ngx/src/app/shared/components/css.component.scss

@ -25,8 +25,7 @@
}
.tb-css-content-panel {
height: calc(100% - 80px);
margin-left: 15px;
height: calc(100% - 40px);
border: 1px solid #c0c0c0;
#tb-css-input {

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}"
<div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight, 'tb-js-func-title': functionTitle}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar">
@ -37,10 +37,10 @@
</div>
</fieldset>
</div>
<div id="tb-javascript-panel" class="tb-js-func-panel" [ngClass]="{'tb-js-func-title': functionTitle}" fxLayout="column" tb-toast toastTarget="{{toastTargetId}}">
<div id="tb-javascript-panel" class="tb-js-func-panel" fxLayout="column" tb-toast toastTarget="{{toastTargetId}}">
<div #javascriptEditor id="tb-javascript-input" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<label *ngIf="!functionTitle" class="tb-title no-padding">}</label>
<div *ngIf="!functionTitle" fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<label class="tb-title no-padding">}</label>
</div>
</div>

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

@ -24,14 +24,22 @@
height: 100%;
}
&:not(.tb-js-func-title) {
.tb-js-func-panel {
margin-left: 15px;
}
}
&.tb-js-func-title {
.tb-js-func-panel {
height: calc(100% - 40px);
}
}
.tb-js-func-panel {
height: calc(100% - 80px);
border: 1px solid #c0c0c0;
&:not(.tb-js-func-title) {
margin-left: 15px;
}
#tb-javascript-input {
width: 100%;
min-width: 200px;
@ -43,6 +51,12 @@
}
}
&:not(.tb-fullscreen) {
&.tb-js-func-title {
padding-bottom: 15px;
}
}
.tb-js-func-toolbar {
& > * {
&:not(:last-child) {

47
ui-ngx/src/app/shared/components/markdown-editor.component.html

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="markdown-content" [ngClass]="{'tb-edit-mode': !readonly}"
tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()">
<div *ngIf="label" fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<label class="tb-title no-padding" [ngClass]="{'tb-required': required}">{{ label }}</label>
</div>
<div [fxShow]="!readonly && !disabled" class="markdown-content-editor">
<div class="buttons-panel">
<button [fxShow]="!editorMode"
class="edit-toggle"
type="button"
mat-button (click)="toggleEditMode()">{{ 'markdown.edit' | translate }}</button>
<button [fxShow]="editorMode"
class="edit-toggle"
type="button"
mat-button (click)="toggleEditMode()">{{ 'markdown.preview' | translate }}</button>
<button mat-button mat-icon-button (click)="fullscreen = !fullscreen"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div [fxShow]="editorMode" #markdownEditor class="tb-markdown-editor"></div>
<div [fxShow]="!editorMode" class="tb-markdown-view-container">
<tb-markdown [data]="renderValue" lineNumbers fallbackToPlainMarkdown></tb-markdown>
</div>
</div>
<div *ngIf="readonly || disabled" class="tb-markdown-view-container">
<tb-markdown [data]="renderValue" lineNumbers fallbackToPlainMarkdown></tb-markdown>
</div>
</div>

77
ui-ngx/src/app/shared/components/markdown-editor.component.scss

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2022 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.
*/
.markdown-content {
min-width: 400px;
&.tb-edit-mode {
.tb-markdown-view-container {
border: 1px solid #c0c0c0;
}
.tb-markdown-view {
padding: 20px;
}
&:not(.tb-fullscreen) {
padding-bottom: 15px;
.markdown-content-editor {
min-height: 200px;
max-height: 200px;
height: 200px;
}
.tb-markdown-view-container {
min-height: 200px;
max-height: 200px;
height: 200px;
}
}
}
&.tb-fullscreen {
background: #fff;
.markdown-content-editor {
height: calc(100% - 40px);
}
}
.markdown-content-editor {
position: relative;
height: 100%;
}
.tb-markdown-editor {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #c0c0c0;
}
.tb-markdown-view-container {
overflow: auto;
height: 100%;
}
.buttons-panel {
position: absolute;
top: 5px;
right: 24px;
z-index: 1;
button.edit-toggle {
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
}
}
}

154
ui-ngx/src/app/shared/components/markdown-editor.component.ts

@ -0,0 +1,154 @@
///
/// Copyright © 2016-2022 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, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getAce } from '@shared/models/ace/ace.models';
@Component({
selector: 'tb-markdown-editor',
templateUrl: './markdown-editor.component.html',
styleUrls: ['./markdown-editor.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MarkdownEditorComponent),
multi: true
}
]
})
export class MarkdownEditorComponent implements OnInit, ControlValueAccessor {
@Input() label: string;
@Input() disabled: boolean;
@Input() readonly: boolean;
@ViewChild('markdownEditor', {static: true})
markdownEditorElmRef: ElementRef;
private markdownEditor: Ace.Editor;
editorMode = true;
fullscreen = false;
markdownValue: string;
renderValue: string;
ignoreChange = false;
private propagateChange = null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
constructor() {
}
ngOnInit(): void {
if (!this.readonly) {
const editorElement = this.markdownEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/markdown',
showGutter: true,
showPrintMargin: false,
readOnly: false
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
getAce().subscribe(
(ace) => {
this.markdownEditor = ace.edit(editorElement, editorOptions);
this.markdownEditor.session.setUseWrapMode(false);
this.markdownEditor.setValue(this.markdownValue ? this.markdownValue : '', -1);
this.markdownEditor.on('change', () => {
if (!this.ignoreChange) {
this.updateView();
}
});
}
);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string): void {
this.editorMode = true;
this.markdownValue = value;
this.renderValue = this.markdownValue ? this.markdownValue : ' ';
if (this.markdownEditor) {
this.ignoreChange = true;
this.markdownEditor.setValue(this.markdownValue ? this.markdownValue : '', -1);
this.ignoreChange = false;
}
}
updateView() {
const editorValue = this.markdownEditor.getValue();
if (this.markdownValue !== editorValue) {
this.markdownValue = editorValue;
this.renderValue = this.markdownValue ? this.markdownValue : ' ';
this.propagateChange(this.markdownValue);
}
}
onFullscreen() {
if (this.markdownEditor) {
setTimeout(() => {
this.markdownEditor.resize();
}, 0);
}
}
toggleEditMode() {
this.editorMode = !this.editorMode;
if (this.editorMode && this.markdownEditor) {
setTimeout(() => {
this.markdownEditor.resize();
}, 0);
}
}
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -79,6 +79,7 @@ import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
import { ClipboardModule } from 'ngx-clipboard';
import { ValueInputComponent } from '@shared/components/value-input.component';
import { MarkdownModule, MarkedOptions } from 'ngx-markdown';
import { MarkdownEditorComponent } from '@shared/components/markdown-editor.component';
import { FullscreenDirective } from '@shared/components/fullscreen.directive';
import { HighlightPipe } from '@shared/pipe/highlight.pipe';
import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
@ -258,6 +259,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
KeyValMapComponent,
NavTreeComponent,
LedLightComponent,
MarkdownEditorComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -452,6 +454,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
KeyValMapComponent,
NavTreeComponent,
LedLightComponent,
MarkdownEditorComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

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

@ -2390,6 +2390,8 @@
"error": "Login error"
},
"markdown": {
"edit": "Edit",
"preview": "Preview",
"copy-code": "Click to copy",
"copied": "Copied!"
},
@ -3368,6 +3370,12 @@
"rotateButton": "Rotate polygon"
}
},
"markdown": {
"use-markdown-text-function": "Use markdown/HTML value function",
"markdown-text-function": "Markdown/HTML value function",
"markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')",
"markdown-css": "Markdown/HTML CSS"
},
"table": {
"common-table-settings": "Common Table Settings",
"enable-search": "Enable search",

Loading…
Cancel
Save