mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Update docs
This commit is contained in:
parent
3959f5119e
commit
9b6be14441
14 changed files with 308 additions and 60 deletions
|
|
@ -43,13 +43,15 @@ export default withMermaid({
|
||||||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||||
{ text: "Stateful Components", link: "/tut/crash-course/6-stateful-component" },
|
{ text: "Root Scopes", link: "/tut/crash-course/6-root" },
|
||||||
{ text: "Property Binding", link: "/tut/crash-course/7-property-binding" },
|
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||||
{ text: "Cleanup", link: "/tut/crash-course/8-cleanup" },
|
{ text: "Property Binding", link: "/tut/crash-course/8-property-binding" },
|
||||||
{ text: "Control Flow", link: "/tut/crash-course/9-control-flow" },
|
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||||
{ text: "Property Nesting", link: "/tut/crash-course/10-property-nesting" },
|
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||||
{ text: "Actions", link: "/tut/crash-course/11-actions" },
|
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
||||||
{ text: "Strict Mode", link: "/tut/crash-course/12-strict-mode" },
|
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
|
||||||
|
{ text: "Actions", link: "/tut/crash-course/13-actions" },
|
||||||
|
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -204,22 +204,23 @@ The above code produces a graph like so:
|
||||||
"primaryBorderColor": "#1B1B1F",
|
"primaryBorderColor": "#1B1B1F",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#161618",
|
"tertiaryColor": "#161618",
|
||||||
"tertiaryBorderColor": "#fff"
|
"tertiaryBorderColor": "#1B1B1F"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph root
|
subgraph root
|
||||||
|
direction LR
|
||||||
show
|
show
|
||||||
|
|
||||||
subgraph subroot[show subroot]
|
subgraph subroot["show() subroot"]
|
||||||
p1[prop binding]
|
p1[prop binding]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
visible --> show
|
visible --> show
|
||||||
count --> p1
|
count --> p1
|
||||||
show .-> subroot
|
show -.- subroot
|
||||||
```
|
```
|
||||||
|
|
||||||
This can be recreated without the `show()` control-flow function, with the
|
This can be recreated without the `show()` control-flow function, with the
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@ worrying about manually updating UI instances.
|
||||||
|
|
||||||
Some of the main focuses behind Vide's design choices:
|
Some of the main focuses behind Vide's design choices:
|
||||||
|
|
||||||
- Concise syntax to reduce verbosity as much as possible.
|
- Concise syntax.
|
||||||
- Being completely typecheckable.
|
- Being completely typecheckable.
|
||||||
- Independence from instance lifetimes.
|
- Independence from instance lifetimes.
|
||||||
- A powerful reactive system that can update specific properties as a result of
|
- Real reactivity.
|
||||||
state changes, updates are immediate with no diffing needed.
|
|
||||||
|
|
||||||
## Structure Of A Vide App
|
## Structure Of A Vide App
|
||||||
|
|
||||||
|
|
@ -33,8 +32,10 @@ called *components*.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local function App()
|
local function App()
|
||||||
return create "ScreenGui" {
|
return {
|
||||||
create "TextLabel" { Text = "hi" }
|
PlayerStats(),
|
||||||
|
Inventory(),
|
||||||
|
Settings()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,30 @@ when the timer component is destroyed, whether that is from unmounting the app
|
||||||
or if it is dynamically created by a control-flow function, which will be
|
or if it is dynamically created by a control-flow function, which will be
|
||||||
covered next.
|
covered next.
|
||||||
|
|
||||||
On a related note: the reason why `mount()` is used to create your app, is so
|
This is another reason why `mount()` is used at the top level of your app, so
|
||||||
that any top-level components that need to be cleaned up, can be cleaned up
|
that any registered cleanups created by your app components can be ran when
|
||||||
when the app is later unmounted, since `mount()` runs in a reactive scope to
|
they are destroyed.
|
||||||
track `cleanup()` calls. Vide's entire reactive system is independent from the
|
|
||||||
life-time of instances; instances are just a side-effect of the reactive system.
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#161618"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart
|
||||||
|
|
||||||
|
subgraph root
|
||||||
|
direction LR
|
||||||
|
cleanup([cleanup]) ~~~ count
|
||||||
|
count --> bind[text binding]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
@ -63,6 +63,37 @@ local function JoinMenu()
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart
|
||||||
|
|
||||||
|
subgraph root ["mount() scope"]
|
||||||
|
direction LR
|
||||||
|
joined --> show -.- subroot
|
||||||
|
|
||||||
|
subgraph subroot ["show() scope"]
|
||||||
|
direction LR
|
||||||
|
Button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The dotted line indicates that the new reactive scope isn't actually connected
|
||||||
|
to the `show` on the graph, it is only managed internally through code.
|
||||||
|
|
||||||
## switch()
|
## switch()
|
||||||
|
|
||||||
Similar to `show()`, `switch()`, also condtionally displays one instance at a
|
Similar to `show()`, `switch()`, also condtionally displays one instance at a
|
||||||
|
|
@ -113,6 +144,34 @@ switch(menu) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart
|
||||||
|
|
||||||
|
subgraph root ["mount() scope"]
|
||||||
|
direction LR
|
||||||
|
joined --> show -.- subroot
|
||||||
|
|
||||||
|
subgraph subroot ["switch() scope"]
|
||||||
|
direction LR
|
||||||
|
Button
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
## indexes()
|
## indexes()
|
||||||
|
|
||||||
Often, you will have a table of values that will be displayed in a similar
|
Often, you will have a table of values that will be displayed in a similar
|
||||||
|
|
@ -121,7 +180,7 @@ UI element, `indexes()` allows you to create an instance for each table index,
|
||||||
to display the value at that index.
|
to display the value at that index.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local todoList = {
|
local todoList = source {
|
||||||
"finish the crash course",
|
"finish the crash course",
|
||||||
"star vide's GitHub"
|
"star vide's GitHub"
|
||||||
}
|
}
|
||||||
|
|
@ -153,5 +212,38 @@ given source for that index is updated.
|
||||||
|
|
||||||
An element is only destroyed if the value of an index is set to `nil`.
|
An element is only destroyed if the value of an index is set to `nil`.
|
||||||
|
|
||||||
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#1C1C1F"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart
|
||||||
|
|
||||||
|
subgraph root ["mount() scope"]
|
||||||
|
direction LR
|
||||||
|
todoList --> indexes -.- subroot1 & subroot2
|
||||||
|
|
||||||
|
subgraph subroot1 ["indexes() scope 1"]
|
||||||
|
direction LR
|
||||||
|
value1[todo] --> prop1["prop binding"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph subroot2 ["indexes() scope 2"]
|
||||||
|
direction LR
|
||||||
|
value2[todo] --> prop2[prop binding]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
Together, these control flow functions cover the majority of cases where you
|
Together, these control flow functions cover the majority of cases where you
|
||||||
need to dynamically create and destroy parts of your UI.
|
need to dynamically create and destroy parts of your UI.
|
||||||
|
|
@ -24,6 +24,9 @@ count(count() + 1) -- increment count by 1
|
||||||
Sources can be *derived* by wrapping them in functions. A wrapped source
|
Sources can be *derived* by wrapping them in functions. A wrapped source
|
||||||
effectively becomes a new source.
|
effectively becomes a new source.
|
||||||
|
|
||||||
|
Derived sources should be pure functions. This is where the same output is
|
||||||
|
always produced for the same input no matter how many times it is reran.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local count = source(0)
|
local count = source(0)
|
||||||
|
|
||||||
|
|
@ -36,7 +39,8 @@ count(1)
|
||||||
print(text()) -- "count: 1"
|
print(text()) -- "count: 1"
|
||||||
```
|
```
|
||||||
|
|
||||||
Derived sources should be pure functions. This is where the same output is
|
Sources on their own aren't very special, the above can be achieved with plain
|
||||||
always produced for the same input. As well as making source updates more
|
variables. The real use for sources become apparent when used in combination
|
||||||
predictable, knowing that updates are pure allows Vide to use optimizations such
|
with Vide's *reactive scopes*. When a source is read from within a reactive
|
||||||
as caching, to avoid updating derived sources if their inputs are the same.
|
scope, it can automatically rerun the scope that reads it when the source is
|
||||||
|
updated in the future.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Effects
|
# Effects
|
||||||
|
|
||||||
Effects are functions that are ran in response to source updates. They are
|
Effects are functions that are ran in response to source updates. They are
|
||||||
called effects because they cause *side-effects* when reacting to source updates
|
called effects because they cause *side-effects* when reacting to source
|
||||||
which are pure.
|
updates.
|
||||||
|
|
||||||
Effects are created using `effect()`.
|
Effects are created using `effect()`.
|
||||||
|
|
||||||
|
|
@ -26,10 +26,10 @@ from inside a reactive scope will be tracked, so that if any of those sources
|
||||||
update, the effect will be reran too.
|
update, the effect will be reran too.
|
||||||
|
|
||||||
The callback is first ran immediately inside the `effect()` call to initially
|
The callback is first ran immediately inside the `effect()` call to initially
|
||||||
figure out what sources are being used.
|
track sources used.
|
||||||
|
|
||||||
Effects also work with derived sources, it doesn't matter how deeply nested a
|
Effects also work with derived sources, it doesn't matter how deeply nested
|
||||||
source is.
|
inside a function a source is.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local source = vide.source
|
local source = vide.source
|
||||||
|
|
@ -50,15 +50,7 @@ count(2)
|
||||||
-- "doubled count: 4" printed
|
-- "doubled count: 4" printed
|
||||||
```
|
```
|
||||||
|
|
||||||
Derived sources should be a *pure computation*. A pure computation is one where
|
The reactive graph for the above example:
|
||||||
the same input will always produce the same output.
|
|
||||||
|
|
||||||
All observable changes to the user are considered to be side-effects of pure
|
|
||||||
computations.
|
|
||||||
|
|
||||||
Sources, derived sources, and effects form what is called a *reactive graph*.
|
|
||||||
In the above example, the following graph is formed. Anywhere
|
|
||||||
an update occures, everything further down the graph is updated.
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
%%{init: {
|
%%{init: {
|
||||||
|
|
@ -69,25 +61,12 @@ an update occures, everything further down the graph is updated.
|
||||||
"primaryBorderColor": "#1B1B1F",
|
"primaryBorderColor": "#1B1B1F",
|
||||||
"lineColor": "#79B8FF",
|
"lineColor": "#79B8FF",
|
||||||
"tertiaryColor": "#161618",
|
"tertiaryColor": "#161618",
|
||||||
"tertiaryBorderColor": "#161618"
|
"tertiaryBorderColor": "#fff"
|
||||||
}
|
}
|
||||||
}}%%
|
}}%%
|
||||||
|
|
||||||
flowchart LR
|
flowchart LR
|
||||||
count --> doubled --> effect
|
|
||||||
|
count --> effect
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You should not update other sources using an effect. Improper usage can lead to
|
|
||||||
a cyclic loop in the graph, causing an infinite loop when it tries to update.
|
|
||||||
Sources should be derived instead.
|
|
||||||
|
|
||||||
## Root Reactive Scopes
|
|
||||||
|
|
||||||
Effects must be created within another reactive scope. This is so that the
|
|
||||||
effect itself can be tracked and later freed when the parent reactive scope is
|
|
||||||
destroyed, such as from unmounting an app. The example code above will not
|
|
||||||
actually work unless it is ran inside a root reactive scope, such as one created
|
|
||||||
by `vide.mount(function)`. This generally isn't a concern since you can assume
|
|
||||||
that all your components will be created within a single `mount()` call, which
|
|
||||||
happens only once at the top level, where you put together your UI and parent it
|
|
||||||
to a ScreenGUI.
|
|
||||||
|
|
|
||||||
49
docs/tut/crash-course/6-root.md
Normal file
49
docs/tut/crash-course/6-root.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Root Reactive Scopes
|
||||||
|
|
||||||
|
Any reactive scopes created, such as one from `effect()`, must be done so within
|
||||||
|
a "root" reactive scope. This is the main purpose of `mount()`, which you use
|
||||||
|
once at the top level to create your app as shown in the first introduction.
|
||||||
|
|
||||||
|
This is so that when the app is unmounted, it can clean up any reactive scopes
|
||||||
|
created within it, since reactive scopes track any reactive scopes created
|
||||||
|
within them.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
|
|
||||||
|
local function App()
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
print(count())
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
vide.mount(App) -- works!
|
||||||
|
|
||||||
|
App() -- will error since effect() was not called within a reactive scope
|
||||||
|
```
|
||||||
|
|
||||||
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#161618"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart
|
||||||
|
|
||||||
|
subgraph root
|
||||||
|
direction LR
|
||||||
|
count --> effect
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
@ -30,6 +30,9 @@ end
|
||||||
Above is an example of a counter component, that when clicked, will increment
|
Above is an example of a counter component, that when clicked, will increment
|
||||||
its internal count, and automatically update its text to reflect that count.
|
its internal count, and automatically update its text to reflect that count.
|
||||||
|
|
||||||
|
Making a property update based on a source is also called *property
|
||||||
|
binding*.
|
||||||
|
|
||||||
Each instance of `Counter()` will maintain its own independent count, since the
|
Each instance of `Counter()` will maintain its own independent count, since the
|
||||||
count source is created inside the scope of the component.
|
count source is created inside the scope of the component.
|
||||||
|
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
Explicitly creating effects to update properties can become verbose when there
|
Explicitly creating effects to update properties can become verbose when there
|
||||||
are a lot of properties to update. Vide provides a way to *implicitly* create
|
are a lot of properties to update. Vide provides a way to *implicitly* create
|
||||||
an effect to update properties on source update. This is also known as
|
an effect to update properties on source update.
|
||||||
*property binding*, since changes to a source will automatically update the
|
|
||||||
property.
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local create = vide.create
|
local create = vide.create
|
||||||
|
|
@ -40,7 +38,9 @@ that are updated.
|
||||||
|
|
||||||
## Children Binding
|
## Children Binding
|
||||||
|
|
||||||
Children can also be set in a similar manner.
|
Children can also be set in a similar manner. Sources bound to properties can
|
||||||
|
return an instance or an array of instances. Vide will automatically unparent
|
||||||
|
removed instances and parent new instances.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local items = source {
|
local items = source {
|
||||||
95
docs/tut/crash-course/9-derived-source.md
Normal file
95
docs/tut/crash-course/9-derived-source.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Derived Sources
|
||||||
|
|
||||||
|
We have seen the basic way to derive a source:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
local text = function()
|
||||||
|
return "count: " .. tostring(count())
|
||||||
|
end
|
||||||
|
|
||||||
|
print(text()) -- "count: 0"
|
||||||
|
count(1)
|
||||||
|
print(text()) -- "count: 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
However, in some cases where this source could be used by multiple effects at
|
||||||
|
the same time, the function wrapping the source will needlessly rerun to convert
|
||||||
|
the count into a string for each effect using it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local source = vide.source
|
||||||
|
local effect = vide.effect
|
||||||
|
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
local text = function()
|
||||||
|
print "ran"
|
||||||
|
return "count: " .. tostring(count())
|
||||||
|
end
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
text() -- prints "ran"
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
text() -- prints "ran" again
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
To avoid this, you can use `derive()` to derive a new source instead. This will
|
||||||
|
run a callback in a new reactive scope only when a dependent source has updated.
|
||||||
|
Reading this derived source multiple times will just return a cached result from
|
||||||
|
when it last updated.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local source = vide.source
|
||||||
|
local derive = vide.derive
|
||||||
|
local effect = vide.effect
|
||||||
|
|
||||||
|
local count = source(0)
|
||||||
|
|
||||||
|
local text = derive(function()
|
||||||
|
print "ran"
|
||||||
|
return "count: " .. tostring(count())
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
text() -- prints "ran"
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
text() -- does not print
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Deriving a source in this manner is similar to creating an effect to update
|
||||||
|
another source. You should never manually do this using an effect however,
|
||||||
|
improper usage could accidently create infinite loops in the reactive graph.
|
||||||
|
Always favour deriving when you need one source to update based on another.
|
||||||
|
|
||||||
|
`derive()` must also be used within a root reactive scope, just like `effect()`.
|
||||||
|
|
||||||
|
The reactive graph for the above example:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {
|
||||||
|
"theme": "base",
|
||||||
|
"themeVariables": {
|
||||||
|
"primaryColor": "#1B1B1F",
|
||||||
|
"primaryTextColor": "#fff",
|
||||||
|
"primaryBorderColor": "#1B1B1F",
|
||||||
|
"lineColor": "#79B8FF",
|
||||||
|
"tertiaryColor": "#161618",
|
||||||
|
"tertiaryBorderColor": "#161618"
|
||||||
|
}
|
||||||
|
}}%%
|
||||||
|
|
||||||
|
flowchart
|
||||||
|
|
||||||
|
subgraph root
|
||||||
|
direction LR
|
||||||
|
count --> text --> effect1 & effect2
|
||||||
|
end
|
||||||
|
```
|
||||||
Loading…
Add table
Add a link
Reference in a new issue