Move most grid specific logic into a dedicated file to avoid polluting rest of the codebase
This commit is contained in:
parent
dbfad2cb3a
commit
0ba00a5117
|
@ -103,7 +103,7 @@
|
||||||
key: "props.cols",
|
key: "props.cols",
|
||||||
label: "Columns",
|
label: "Columns",
|
||||||
control: Stepper,
|
control: Stepper,
|
||||||
defaultValue: 12,
|
defaultValue: 24,
|
||||||
props: {
|
props: {
|
||||||
min: 2,
|
min: 2,
|
||||||
max: 50,
|
max: 50,
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
key: "props.rows",
|
key: "props.rows",
|
||||||
label: "Rows",
|
label: "Rows",
|
||||||
control: Stepper,
|
control: Stepper,
|
||||||
defaultValue: 12,
|
defaultValue: 24,
|
||||||
props: {
|
props: {
|
||||||
min: 2,
|
min: 2,
|
||||||
max: 50,
|
max: 50,
|
||||||
|
|
|
@ -1391,6 +1391,10 @@
|
||||||
"width": 25,
|
"width": 25,
|
||||||
"height": 25
|
"height": 25
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"hAlign": "center",
|
||||||
|
"vAlign": "center"
|
||||||
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "icon",
|
"type": "icon",
|
||||||
|
@ -1704,6 +1708,10 @@
|
||||||
"width": 260,
|
"width": 260,
|
||||||
"height": 143
|
"height": 143
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"hAlign": "center",
|
||||||
|
"vAlign": "center"
|
||||||
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -1737,6 +1745,10 @@
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 100
|
"height": 100
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"hAlign": "stretch",
|
||||||
|
"vAlign": "stretch"
|
||||||
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
@ -5240,6 +5252,10 @@
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"height": 120
|
"height": 120
|
||||||
},
|
},
|
||||||
|
"grid": {
|
||||||
|
"hAlign": "center",
|
||||||
|
"vAlign": "center"
|
||||||
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
|
@ -284,7 +284,7 @@
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -301,7 +301,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +311,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-root {
|
#app-root {
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -39,8 +39,7 @@
|
||||||
getActionContextKey,
|
getActionContextKey,
|
||||||
getActionDependentContextKeys,
|
getActionDependentContextKeys,
|
||||||
} from "../utils/buttonActions.js"
|
} from "../utils/buttonActions.js"
|
||||||
import { buildStyleString } from "utils/styleable.js"
|
import { gridLayout } from "utils/grid.js"
|
||||||
import { getBaseGridVars } from "utils/grid.js"
|
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
|
@ -198,15 +197,18 @@
|
||||||
$: currentTheme = $context?.device?.theme
|
$: currentTheme = $context?.device?.theme
|
||||||
$: darkMode = !currentTheme?.includes("light")
|
$: darkMode = !currentTheme?.includes("light")
|
||||||
|
|
||||||
// Build up full styles and split them into variables and non-variables
|
$: normalStyles = {
|
||||||
$: baseStyles = getBaseStyles(definition, errorState)
|
|
||||||
$: styles = {
|
|
||||||
...baseStyles,
|
|
||||||
...instance._styles?.normal,
|
...instance._styles?.normal,
|
||||||
...ephemeralStyles,
|
...ephemeralStyles,
|
||||||
}
|
}
|
||||||
$: parsedStyles = parseStyles(styles)
|
$: gridMetadata = {
|
||||||
$: wrapperCSS = buildStyleString(parsedStyles.variables)
|
id,
|
||||||
|
interactive,
|
||||||
|
styles: normalStyles,
|
||||||
|
draggable,
|
||||||
|
definition,
|
||||||
|
errored: errorState,
|
||||||
|
}
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: store.set({
|
$: store.set({
|
||||||
|
@ -214,8 +216,7 @@
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: {
|
styles: {
|
||||||
...instance._styles,
|
...instance._styles,
|
||||||
normal: parsedStyles.nonVariables,
|
normal: normalStyles,
|
||||||
variables: parsedStyles.variables,
|
|
||||||
custom: customCSS,
|
custom: customCSS,
|
||||||
id,
|
id,
|
||||||
empty: emptyState,
|
empty: emptyState,
|
||||||
|
@ -615,31 +616,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWrapperClick = e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
builderStore.actions.selectComponent(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splits component styles into variables and non-variables
|
|
||||||
const parseStyles = styles => {
|
|
||||||
let variables = {}
|
|
||||||
let nonVariables = {}
|
|
||||||
for (let style of Object.keys(styles || {})) {
|
|
||||||
const group = style.startsWith("--") ? variables : nonVariables
|
|
||||||
group[style] = styles[style]
|
|
||||||
}
|
|
||||||
return { variables, nonVariables }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates any required base styles based on the component definition
|
|
||||||
const getBaseStyles = (definition, errored = false) => {
|
|
||||||
return {
|
|
||||||
"--default-width": errored ? 500 : definition.size?.width || 100,
|
|
||||||
"--default-height": errored ? 60 : definition.size?.height || 100,
|
|
||||||
...getBaseGridVars(definition, errored),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Register this component instance for external access
|
// Register this component instance for external access
|
||||||
if ($appStore.isDevApp) {
|
if ($appStore.isDevApp) {
|
||||||
|
@ -683,14 +659,11 @@
|
||||||
class:parent={hasChildren}
|
class:parent={hasChildren}
|
||||||
class:block={isBlock}
|
class:block={isBlock}
|
||||||
class:error={errorState}
|
class:error={errorState}
|
||||||
class:fill={definition.grid?.fill}
|
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-icon={icon}
|
data-icon={icon}
|
||||||
data-parent={$component.id}
|
data-parent={$component.id}
|
||||||
style={wrapperCSS}
|
use:gridLayout={gridMetadata}
|
||||||
{draggable}
|
|
||||||
on:click|self={interactive ? handleWrapperClick : null}
|
|
||||||
>
|
>
|
||||||
{#if errorState}
|
{#if errorState}
|
||||||
<ComponentErrorState
|
<ComponentErrorState
|
||||||
|
|
|
@ -480,7 +480,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
.main:has(> .grid) {
|
.main:has(.screenslot-dom > .component > .grid) {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,16 +68,6 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|
||||||
/* Prevent cross-grid variable inheritance */
|
|
||||||
/* --grid-desktop-col-start: initial;
|
|
||||||
--grid-desktop-col-end: initial;
|
|
||||||
--grid-desktop-row-start: initial;
|
|
||||||
--grid-desktop-row-end: initial;
|
|
||||||
--grid-mobile-col-start: initial;
|
|
||||||
--grid-mobile-col-end: initial;
|
|
||||||
--grid-mobile-row-start: initial;
|
|
||||||
--grid-mobile-row-end: initial;*/
|
|
||||||
}
|
}
|
||||||
.grid,
|
.grid,
|
||||||
.underlay {
|
.underlay {
|
||||||
|
@ -128,12 +118,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Flex vars */
|
/* Flex vars */
|
||||||
--h-align: var(--grid-desktop-h-align, var(--grid-mobile-h-align, stretch));
|
--h-align: var(--grid-desktop-h-align, var(--grid-mobile-h-align));
|
||||||
--v-align: var(--grid-desktop-v-align, var(--grid-mobile-v-align, center));
|
--v-align: var(--grid-desktop-v-align, var(--grid-mobile-v-align));
|
||||||
--child-flex: var(
|
|
||||||
--grid-desktop-child-flex,
|
|
||||||
var(--grid-mobile-child-flex, 0 0 auto)
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Ensure grid metadata falls within limits */
|
/* Ensure grid metadata falls within limits */
|
||||||
grid-column-start: min(max(1, var(--col-start)), var(--cols)) !important;
|
grid-column-start: min(max(1, var(--col-start)), var(--cols)) !important;
|
||||||
|
@ -171,16 +157,20 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Flex vars */
|
/* Flex vars */
|
||||||
--h-align: var(--grid-mobile-h-align, var(--grid-desktop-h-align, stretch));
|
--h-align: var(--grid-mobile-h-align, var(--grid-desktop-h-align));
|
||||||
--v-align: var(--grid-mobile-v-align, var(--grid-desktop-v-align, center));
|
--v-align: var(--grid-mobile-v-align, var(--grid-desktop-v-align));
|
||||||
--child-flex: var(
|
|
||||||
--grid-mobile-child-flex,
|
|
||||||
var(--grid-desktop-child-flex, 0 0 auto)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle grid children which need to fill the outer component wrapper */
|
/* Handle grid children which need to fill the outer component wrapper */
|
||||||
.grid :global(> .component > *) {
|
.grid :global(> .component > *) {
|
||||||
flex: var(--child-flex) !important;
|
flex: 0 0 auto !important;
|
||||||
|
}
|
||||||
|
.grid:not(.mobile) :global(> .component.grid-desktop-grow > *) {
|
||||||
|
flex: 1 1 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
.grid.mobile :global(> .component.grid-mobile-grow > *) {
|
||||||
|
flex: 1 1 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,12 @@
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { builderStore, componentStore } from "stores"
|
import { builderStore, componentStore } from "stores"
|
||||||
import { Utils, memo } from "@budibase/frontend-core"
|
import { Utils, memo } from "@budibase/frontend-core"
|
||||||
import { isGridEvent, getGridParentID, getGridVar } from "utils/grid"
|
import {
|
||||||
|
isGridEvent,
|
||||||
|
getGridParentID,
|
||||||
|
gridCSSVars,
|
||||||
|
GridVars,
|
||||||
|
} from "utils/grid"
|
||||||
|
|
||||||
// Smallest possible 1x1 transparent GIF
|
// Smallest possible 1x1 transparent GIF
|
||||||
const ghost = new Image(1, 1)
|
const ghost = new Image(1, 1)
|
||||||
|
@ -15,10 +20,10 @@
|
||||||
|
|
||||||
// Grid CSS variables
|
// Grid CSS variables
|
||||||
$: vars = {
|
$: vars = {
|
||||||
colStart: $getGridVar("col-start"),
|
colStart: $gridCSSVars[GridVars.ColStart],
|
||||||
colEnd: $getGridVar("col-end"),
|
colEnd: $gridCSSVars[GridVars.ColEnd],
|
||||||
rowStart: $getGridVar("row-start"),
|
rowStart: $gridCSSVars[GridVars.RowStart],
|
||||||
rowEnd: $getGridVar("row-end"),
|
rowEnd: $gridCSSVars[GridVars.RowEnd],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some memoisation of primitive types for performance
|
// Some memoisation of primitive types for performance
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { builderStore, componentStore, dndIsDragging } from "stores"
|
import { builderStore, componentStore, dndIsDragging } from "stores"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { findComponentParent } from "utils/components"
|
import { findComponentParent } from "utils/components"
|
||||||
import { getGridVar } from "utils/grid"
|
import { gridCSSVars, GridVars } from "utils/grid"
|
||||||
|
|
||||||
const verticalOffset = 36
|
const verticalOffset = 36
|
||||||
const horizontalOffset = 2
|
const horizontalOffset = 2
|
||||||
|
@ -44,8 +44,8 @@
|
||||||
insideGrid &&
|
insideGrid &&
|
||||||
(definition?.grid?.hAlign !== "stretch" ||
|
(definition?.grid?.hAlign !== "stretch" ||
|
||||||
definition?.grid?.vAlign !== "stretch")
|
definition?.grid?.vAlign !== "stretch")
|
||||||
$: gridHAlignVar = $getGridVar("h-align")
|
$: gridHAlignVar = $gridCSSVars[GridVars.HAlign]
|
||||||
$: gridVAlignVar = $getGridVar("v-align")
|
$: gridVAlignVar = $gridCSSVars[GridVars.VAlign]
|
||||||
$: gridStyles = $state?.styles
|
$: gridStyles = $state?.styles
|
||||||
|
|
||||||
const getBarSettings = definition => {
|
const getBarSettings = definition => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { builderStore, componentStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
import { derived, get, readable } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
|
import { buildStyleString } from "utils/styleable.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We use CSS variables on components to control positioning and layout of
|
* We use CSS variables on components to control positioning and layout of
|
||||||
|
@ -12,20 +13,42 @@ import { derived, get, readable } from "svelte/store"
|
||||||
* `grid.hAlign` and `grid.vAlign` keys in the manifest.
|
* `grid.hAlign` and `grid.vAlign` keys in the manifest.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Enum representing the different CSS variables we use for grid metadata
|
||||||
|
export const GridVars = {
|
||||||
|
HAlign: "h-align",
|
||||||
|
VAlign: "v-align",
|
||||||
|
ColStart: "col-start",
|
||||||
|
ColEnd: "col-end",
|
||||||
|
RowStart: "row-start",
|
||||||
|
RowEnd: "row-end",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classes used in selectors inside grid containers to control child styles
|
||||||
|
export const GridClasses = {
|
||||||
|
DesktopFill: "grid-desktop-grow",
|
||||||
|
MobileFill: "grid-mobile-grow",
|
||||||
|
}
|
||||||
|
|
||||||
// Enum for device preview type, included in grid CSS variables
|
// Enum for device preview type, included in grid CSS variables
|
||||||
const Devices = {
|
const Devices = {
|
||||||
Desktop: "desktop",
|
Desktop: "desktop",
|
||||||
Mobile: "mobile",
|
Mobile: "mobile",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates the CSS variable for a certain grid param suffix, for the current
|
// A derived map of all CSS variables for the current device
|
||||||
// device
|
|
||||||
const previewDevice = derived(builderStore, $store => $store.previewDevice)
|
const previewDevice = derived(builderStore, $store => $store.previewDevice)
|
||||||
export const getGridVar = derived(previewDevice, device => suffix => {
|
export const gridCSSVars = derived(previewDevice, $device => {
|
||||||
const prefix = device === Devices.Mobile ? Devices.Mobile : Devices.Desktop
|
const device = $device === Devices.Mobile ? Devices.Mobile : Devices.Desktop
|
||||||
return `--grid-${prefix}-${suffix}`
|
let vars = {}
|
||||||
|
for (let type of Object.values(GridVars)) {
|
||||||
|
vars[type] = `--grid-${device}-${type}`
|
||||||
|
}
|
||||||
|
return vars
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Builds a CSS variable name for a certain piece of grid metadata
|
||||||
|
export const getGridCSSVar = (device, type) => `--grid-${device}-${type}`
|
||||||
|
|
||||||
// Generates the CSS variable for a certain grid param suffix, for the other
|
// Generates the CSS variable for a certain grid param suffix, for the other
|
||||||
// device variant than the one included in this variable
|
// device variant than the one included in this variable
|
||||||
export const getOtherDeviceGridVar = cssVar => {
|
export const getOtherDeviceGridVar = cssVar => {
|
||||||
|
@ -69,21 +92,6 @@ export const getGridParentID = node => {
|
||||||
return node?.parentNode?.closest(".grid")?.parentNode.dataset.id
|
return node?.parentNode?.closest(".grid")?.parentNode.dataset.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates the base set of grid CSS vars from a component definition
|
|
||||||
export const getBaseGridVars = (definition, errored = false) => {
|
|
||||||
const hAlign = errored ? "stretch" : definition?.grid?.hAlign || "stretch"
|
|
||||||
const vAlign = errored ? "stretch" : definition?.grid?.vAlign || "center"
|
|
||||||
const flexStyles = vAlign === "stretch" ? "1 1 0" : "0 0 auto"
|
|
||||||
return {
|
|
||||||
"--grid-desktop-h-align": hAlign,
|
|
||||||
"--grid-mobile-h-align": hAlign,
|
|
||||||
"--grid-desktop-v-align": vAlign,
|
|
||||||
"--grid-mobile-v-align": vAlign,
|
|
||||||
"--grid-desktop-child-flex": flexStyles,
|
|
||||||
"--grid-mobile-child-flex": flexStyles,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the current value of a certain grid CSS variable for a component
|
// Gets the current value of a certain grid CSS variable for a component
|
||||||
export const getGridVarValue = (styles, variable) => {
|
export const getGridVarValue = (styles, variable) => {
|
||||||
// Try the desired variable
|
// Try the desired variable
|
||||||
|
@ -97,3 +105,73 @@ export const getGridVarValue = (styles, variable) => {
|
||||||
// Otherwise use the default
|
// Otherwise use the default
|
||||||
return val ? val : getDefaultGridVarValue(variable)
|
return val ? val : getDefaultGridVarValue(variable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Svelte action to apply required class names and styles to our component
|
||||||
|
// wrappers
|
||||||
|
export const gridLayout = (node, metadata) => {
|
||||||
|
let selectComponent
|
||||||
|
|
||||||
|
const applyMetadata = metadata => {
|
||||||
|
const { id, styles, interactive, errored, definition } = metadata
|
||||||
|
consol.log(styles)
|
||||||
|
|
||||||
|
// Callback to select the component when clicking on the wrapper
|
||||||
|
selectComponent = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
builderStore.actions.selectComponent(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate base set of grid CSS vars based for this component
|
||||||
|
const hAlign = errored ? "stretch" : definition?.grid?.hAlign || "stretch"
|
||||||
|
const vAlign = errored ? "stretch" : definition?.grid?.vAlign || "center"
|
||||||
|
const vars = {
|
||||||
|
"--default-width": errored ? 500 : definition.size?.width || 100,
|
||||||
|
"--default-height": errored ? 60 : definition.size?.height || 100,
|
||||||
|
"--grid-desktop-h-align": hAlign,
|
||||||
|
"--grid-mobile-h-align": hAlign,
|
||||||
|
"--grid-desktop-v-align": vAlign,
|
||||||
|
"--grid-mobile-v-align": vAlign,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract any other CSS variables from the saved component styles
|
||||||
|
for (let style of Object.keys(styles)) {
|
||||||
|
if (style.startsWith("--")) {
|
||||||
|
vars[style] = styles[style]
|
||||||
|
delete styles[style]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all CSS variables to the wrapper
|
||||||
|
node.style = buildStyleString(vars)
|
||||||
|
|
||||||
|
// Toggle classes to specify whether our children should fill
|
||||||
|
const desktopVar = getGridCSSVar(Devices.Desktop, GridVars.VAlign)
|
||||||
|
const mobileVar = getGridCSSVar(Devices.Mobile, GridVars.VAlign)
|
||||||
|
node.classList.toggle(
|
||||||
|
GridClasses.DesktopFill,
|
||||||
|
vars[desktopVar] === "stretch"
|
||||||
|
)
|
||||||
|
node.classList.toggle(GridClasses.MobileFill, vars[mobileVar] === "stretch")
|
||||||
|
|
||||||
|
// Add a listener to select this node on click
|
||||||
|
if (interactive) {
|
||||||
|
node.addEventListener("click", selectComponent, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeListeners = () => {
|
||||||
|
node.removeEventListener("click", selectComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMetadata(metadata)
|
||||||
|
|
||||||
|
return {
|
||||||
|
update(newMetadata) {
|
||||||
|
removeListeners()
|
||||||
|
applyMetadata(newMetadata)
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
removeListeners()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue