mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
committed by
Aaron Bohy
parent
f07ec21a07
commit
2279dafbec
+1
-1
@@ -441,7 +441,7 @@ of an Owl application has to be created manually:
|
||||
```js
|
||||
class App extends owl.Component { ... }
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES });
|
||||
const env = { qweb: qweb };
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ context, and add it to the environment:
|
||||
```js
|
||||
const deviceContext = new Context({ isMobile: true });
|
||||
const env = {
|
||||
qweb: new QWeb(TEMPLATES),
|
||||
qweb: new QWeb({ templates: TEMPLATES }),
|
||||
deviceContext
|
||||
};
|
||||
```
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ class ClickCounter extends owl.Component {
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadFile("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
qweb: new owl.QWeb({ templates })
|
||||
};
|
||||
const counter = new ClickCounter(env);
|
||||
const target = document.getElementById("main");
|
||||
|
||||
+65
-13
@@ -16,6 +16,7 @@
|
||||
- [Dynamic Attributes](#dynamic-attributes)
|
||||
- [Loops](#loops)
|
||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||
- [Translations](#translations)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## Overview
|
||||
@@ -60,6 +61,7 @@ We present here a list of all standard QWeb directives:
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](#translations) |
|
||||
| `t-name` | [Defining a template (not really a directive)](#qweb-engine) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
@@ -87,11 +89,14 @@ const qweb = new owl.QWeb();
|
||||
|
||||
Its API is quite simple:
|
||||
|
||||
- **`constructor(data)`**: constructor. Takes an optional string to add initial
|
||||
templates (see `addTemplates` for more information on format of the string).
|
||||
- **`constructor(config)`**: constructor. Takes an optional configuration object
|
||||
with an optional `templates` string to add initial
|
||||
templates (see `addTemplates` for more information on format of the string)
|
||||
and an optional `translateFn` translate function (see the section on
|
||||
[translations](#translations)).
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
|
||||
```
|
||||
|
||||
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||
@@ -201,7 +206,7 @@ root nodes.
|
||||
### Expression Evaluation
|
||||
|
||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||
the javascript expression will be replaced by a lookup in the context (so, the
|
||||
the javascript expression will be replaced with a lookup in the context (so, the
|
||||
component). For example, `a + b.c(d)` will be converted into:
|
||||
|
||||
```js
|
||||
@@ -234,14 +239,14 @@ It is useful to explain the various rules that apply on these expressions:
|
||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||
|
||||
| Word | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
| Word | replaced with |
|
||||
| ----- | ------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
@@ -443,7 +448,7 @@ is equivalent to the previous example.
|
||||
or an object (the current item will be the current key).
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced by the name
|
||||
variables for various data points (note: `$as` will be replaced with the name
|
||||
passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
@@ -522,6 +527,53 @@ will result in :
|
||||
</div>
|
||||
```
|
||||
|
||||
### Translations
|
||||
|
||||
If properly setup, Owl QWeb engine can translate all rendered templates. To do
|
||||
so, it needs a translate function, which takes a string and returns a string.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const translations = {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non"
|
||||
};
|
||||
const translateFn = str => translations[str] || str;
|
||||
|
||||
const qweb = new QWeb({ translateFn });
|
||||
```
|
||||
|
||||
Once setup, all rendered templates will be translated using `translateFn`:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
So, with the above `translateFn`, the following templates:
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div t-translation="off">hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="hello" other="yes"/>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<div>bonjour</div>
|
||||
<div>hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="bonjour" other="yes"/>
|
||||
```
|
||||
|
||||
Note that the translation is done during the compilation of the template, not
|
||||
when it is rendered.
|
||||
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function([templates]) {
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
});
|
||||
@@ -62,7 +62,7 @@ initial usecase for this function is to load a template file. For example:
|
||||
```js
|
||||
async function makeEnv() {
|
||||
const templates = await owl.utils.loadFile("templates.xml");
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
return { qweb };
|
||||
}
|
||||
```
|
||||
|
||||
+9
-9
@@ -81,15 +81,15 @@ export function useContextWithCB(ctx: Context, component: Component<any, any>, m
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
const currentCB = __owl__.observer.notifyCB;
|
||||
__owl__.observer.notifyCB = function () {
|
||||
if (ctx.rev > mapping[id]) {
|
||||
// in this case, the context has been updated since we were rendering
|
||||
// last, and we do not need to render here with the observer. A
|
||||
// rendering is coming anyway, with the correct props.
|
||||
return;
|
||||
}
|
||||
currentCB();
|
||||
}
|
||||
__owl__.observer.notifyCB = function() {
|
||||
if (ctx.rev > mapping[id]) {
|
||||
// in this case, the context has been updated since we were rendering
|
||||
// last, and we do not need to render here with the observer. A
|
||||
// rendering is coming anyway, with the correct props.
|
||||
return;
|
||||
}
|
||||
currentCB();
|
||||
};
|
||||
|
||||
mapping[id] = 0;
|
||||
const renderFn = __owl__.renderFn;
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import { QWeb } from "./qweb/index";
|
||||
import * as _store from "./store";
|
||||
import * as _utils from "./utils";
|
||||
import * as _tags from "./tags";
|
||||
import {AsyncRoot} from "./misc/async_root";
|
||||
import { AsyncRoot } from "./misc/async_root";
|
||||
import * as _hooks from "./hooks";
|
||||
import * as _context from "./context";
|
||||
import { Link } from "./router/link";
|
||||
@@ -27,7 +27,7 @@ export const router = { Router, RouteComponent, Link };
|
||||
export const Store = _store.Store;
|
||||
export const utils = _utils;
|
||||
export const tags = _tags;
|
||||
export const misc = { AsyncRoot};
|
||||
export const misc = { AsyncRoot };
|
||||
export const hooks = Object.assign({}, _hooks, {
|
||||
useContext: _context.useContext,
|
||||
useDispatch: _store.useDispatch,
|
||||
|
||||
+27
-5
@@ -57,12 +57,19 @@ export interface Directive {
|
||||
finalize?(info: CompilationInfo): void;
|
||||
}
|
||||
|
||||
interface QWebConfig {
|
||||
templates?: string;
|
||||
translateFn?(text: string): string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Const/global stuff/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DISABLED_TAGS = ["input", "textarea", "button", "select", "option", "optgroup"];
|
||||
|
||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
@@ -134,6 +141,7 @@ function parseXML(xml: string): Document {
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template };
|
||||
static utils = UTILS;
|
||||
@@ -143,7 +151,8 @@ export class QWeb extends EventBus {
|
||||
name: 1,
|
||||
att: 1,
|
||||
attf: 1,
|
||||
key: 1
|
||||
key: 1,
|
||||
translation: 1
|
||||
};
|
||||
static DIRECTIVES: Directive[] = [];
|
||||
|
||||
@@ -166,12 +175,16 @@ export class QWeb extends EventBus {
|
||||
recursiveFns = {};
|
||||
|
||||
isUpdating: boolean = false;
|
||||
translateFn?: QWebConfig["translateFn"];
|
||||
|
||||
constructor(data?: string) {
|
||||
constructor(config: QWebConfig = {}) {
|
||||
super();
|
||||
this.templates = Object.create(QWeb.TEMPLATES);
|
||||
if (data) {
|
||||
this.addTemplates(data);
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
if (config.translateFn) {
|
||||
this.translateFn = config.translateFn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +431,11 @@ export class QWeb extends EventBus {
|
||||
}
|
||||
text = text.replace(whitespaceRE, " ");
|
||||
}
|
||||
if (this.translateFn) {
|
||||
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
|
||||
text = this.translateFn(text);
|
||||
}
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
if (node.nodeType === 3) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
@@ -617,7 +635,11 @@ export class QWeb extends EventBus {
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
let value = attributes[i].textContent!;
|
||||
|
||||
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
|
||||
value = this.translateFn(value);
|
||||
}
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("Context", () => {
|
||||
|
||||
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
||||
testContext.state.key = "y";
|
||||
testContext.state.y = {n: 2};
|
||||
testContext.state.y = { n: 2 };
|
||||
delete testContext.state.x;
|
||||
await nextTick();
|
||||
|
||||
|
||||
@@ -2415,6 +2415,73 @@ exports[`t-set value priority 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support can translate node content 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let sibling = null;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
c1.push({text: \`mot\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support does not translate node content if disabled 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let sibling = null;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2};
|
||||
var vn2 = h('span', p2, c2);
|
||||
c1.push(vn2);
|
||||
c2.push({text: \`mot\`});
|
||||
let c3 = [], p3 = {key:3};
|
||||
var vn3 = h('span', p3, c3);
|
||||
c1.push(vn3);
|
||||
c3.push({text: \`word\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support some attributes are translated 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
let sibling = null;
|
||||
var h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
var vn1 = h('div', p1, c1);
|
||||
var _2 = 'mot';
|
||||
let c3 = [], p3 = {key:3,attrs:{label: _2}};
|
||||
var vn3 = h('p', p3, c3);
|
||||
c1.push(vn3);
|
||||
c3.push({text: \`mot\`});
|
||||
var _4 = 'mot';
|
||||
let c5 = [], p5 = {key:5,attrs:{title: _4}};
|
||||
var vn5 = h('p', p5, c5);
|
||||
c1.push(vn5);
|
||||
c5.push({text: \`mot\`});
|
||||
var _6 = 'mot';
|
||||
let c7 = [], p7 = {key:7,attrs:{placeholder: _6}};
|
||||
var vn7 = h('p', p7, c7);
|
||||
c1.push(vn7);
|
||||
c7.push({text: \`mot\`});
|
||||
var _8 = 'mot';
|
||||
let c9 = [], p9 = {key:9,attrs:{alt: _8}};
|
||||
var vn9 = h('p', p9, c9);
|
||||
c1.push(vn9);
|
||||
c9.push({text: \`mot\`});
|
||||
var _10 = 'word';
|
||||
let c11 = [], p11 = {key:11,attrs:{something: _10}};
|
||||
var vn11 = h('p', p11, c11);
|
||||
c1.push(vn11);
|
||||
c11.push({text: \`mot\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`whitespace handling consecutives whitespaces are condensed into a single space 1`] = `
|
||||
"function anonymous(context,extra
|
||||
) {
|
||||
|
||||
+53
-2
@@ -1305,12 +1305,12 @@ describe("t-ref", () => {
|
||||
|
||||
describe("loading templates", () => {
|
||||
test("can initialize qweb with a string", () => {
|
||||
const data = `
|
||||
const templates = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<div t-name="hey">jupiler</div>
|
||||
</templates>`;
|
||||
const qweb = new QWeb(data);
|
||||
const qweb = new QWeb({ templates });
|
||||
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");
|
||||
});
|
||||
|
||||
@@ -1519,3 +1519,54 @@ describe("properly support svg", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("translation support", () => {
|
||||
test("can translate node content", () => {
|
||||
const translations = {
|
||||
word: "mot"
|
||||
};
|
||||
const translateFn = expr => translations[expr] || expr;
|
||||
const qweb = new QWeb({ translateFn });
|
||||
qweb.addTemplate("test", "<div>word</div>");
|
||||
expect(renderToString(qweb, "test")).toBe("<div>mot</div>");
|
||||
});
|
||||
|
||||
test("does not translate node content if disabled", () => {
|
||||
const translations = {
|
||||
word: "mot"
|
||||
};
|
||||
const translateFn = expr => translations[expr] || expr;
|
||||
const qweb = new QWeb({ translateFn });
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<span>word</span>
|
||||
<span t-translation="off">word</span>
|
||||
</div>`
|
||||
);
|
||||
expect(renderToString(qweb, "test")).toBe("<div><span>mot</span><span>word</span></div>");
|
||||
});
|
||||
|
||||
test("some attributes are translated", () => {
|
||||
const translations = {
|
||||
word: "mot"
|
||||
};
|
||||
const translateFn = expr => translations[expr] || expr;
|
||||
const qweb = new QWeb({ translateFn });
|
||||
qweb.addTemplate(
|
||||
"test",
|
||||
`
|
||||
<div>
|
||||
<p label="word">word</p>
|
||||
<p title="word">word</p>
|
||||
<p placeholder="word">word</p>
|
||||
<p alt="word">word</p>
|
||||
<p something="word">word</p>
|
||||
</div>`
|
||||
);
|
||||
expect(renderToString(qweb, "test")).toBe(
|
||||
'<div><p label="mot">mot</p><p title="mot">mot</p><p placeholder="mot">mot</p><p alt="mot">mot</p><p something="word">mot</p></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ class App extends owl.Component {
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadFile("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
qweb: new owl.QWeb({ templates })
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
|
||||
@@ -435,7 +435,7 @@ async function start() {
|
||||
owl.utils.loadFile("templates.xml"),
|
||||
owl.utils.whenReady()
|
||||
]);
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
+11
-11
@@ -17,7 +17,7 @@ App.components = { Greeter };
|
||||
|
||||
// Application setup
|
||||
// Note that the xml templates are injected into the global TEMPLATES variable.
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
`;
|
||||
@@ -70,7 +70,7 @@ class App extends Component {
|
||||
}
|
||||
App.components = { Counter };
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const app = new App({qweb});
|
||||
app.mount(document.body);
|
||||
`;
|
||||
@@ -228,7 +228,7 @@ class App extends Component {
|
||||
}
|
||||
App.components = { DemoComponent };
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
`;
|
||||
@@ -298,7 +298,7 @@ class App extends owl.Component {
|
||||
}
|
||||
|
||||
// Application setup
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
`;
|
||||
@@ -350,7 +350,7 @@ const themeContext = new Context({
|
||||
foreground: '#fff',
|
||||
});
|
||||
const env = {
|
||||
qweb: new owl.QWeb(TEMPLATES),
|
||||
qweb: new owl.QWeb({ templates: TEMPLATES}),
|
||||
themeContext: themeContext,
|
||||
};
|
||||
const app = new App(env);
|
||||
@@ -548,7 +548,7 @@ function makeEnv() {
|
||||
const state = loadState();
|
||||
const store = new owl.Store({ state, actions });
|
||||
store.on("update", null, () => saveState(store.state));
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
return { qweb, store };
|
||||
}
|
||||
|
||||
@@ -1041,7 +1041,7 @@ function setupResponsivePlugin(env) {
|
||||
// Application Startup
|
||||
//------------------------------------------------------------------------------
|
||||
const env = {
|
||||
qweb: new owl.QWeb(TEMPLATES),
|
||||
qweb: new owl.QWeb({ templates: TEMPLATES}),
|
||||
};
|
||||
setupResponsivePlugin(env);
|
||||
|
||||
@@ -1187,7 +1187,7 @@ class App extends Component {
|
||||
App.components = {Card, Counter};
|
||||
|
||||
// Application setup
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);`;
|
||||
|
||||
@@ -1303,7 +1303,7 @@ class App extends Component {
|
||||
}
|
||||
App.components = {SlowComponent, NotificationList};
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const app = new App({ qweb });
|
||||
app.mount(document.body);
|
||||
`;
|
||||
@@ -1373,7 +1373,7 @@ class Form extends Component {
|
||||
}
|
||||
|
||||
// Application setup
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const form = new Form({ qweb });
|
||||
form.mount(document.body);
|
||||
`;
|
||||
@@ -1540,7 +1540,7 @@ class App extends Component {
|
||||
}
|
||||
App.components = { WindowManager };
|
||||
|
||||
const qweb = new owl.QWeb(TEMPLATES);
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES});
|
||||
const windows = [
|
||||
{
|
||||
name: "Hello",
|
||||
|
||||
Reference in New Issue
Block a user