chore(components): Updated Workbench component to pass data rather than gist to DraftConfigurator
This commit is contained in:
parent
6d122f429f
commit
1375e5ae93
2 changed files with 169 additions and 192 deletions
|
@ -1,43 +1,42 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types'
|
||||||
import Draft from "../../Draft";
|
import Draft from '../../Draft'
|
||||||
import Design from "../Design";
|
import Design from '../Design'
|
||||||
import DraftConfigurator from "../../DraftConfigurator";
|
import DraftConfigurator from '../../DraftConfigurator'
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from 'react-intl'
|
||||||
import Prism from "prismjs";
|
import Prism from 'prismjs'
|
||||||
import fileSaver from "file-saver";
|
import fileSaver from 'file-saver'
|
||||||
import theme from "@freesewing/plugin-theme";
|
import theme from '@freesewing/plugin-theme'
|
||||||
|
|
||||||
const DraftPattern = props => {
|
const DraftPattern = props => {
|
||||||
const [design, setDesign] = useState(true);
|
const [design, setDesign] = useState(true)
|
||||||
const [focus, setFocus] = useState(null);
|
const [focus, setFocus] = useState(null)
|
||||||
|
|
||||||
const raiseEvent = (type, data) => {
|
const raiseEvent = (type, data) => {
|
||||||
if (type === "clearFocusAll") {
|
if (type === 'clearFocusAll') {
|
||||||
props.updateGist(false, "settings", "only");
|
props.updateGist(false, 'settings', 'only')
|
||||||
return setFocus(null);
|
return setFocus(null)
|
||||||
}
|
}
|
||||||
let f = {};
|
let f = {}
|
||||||
if (focus !== null) f = { ...focus };
|
if (focus !== null) f = { ...focus }
|
||||||
if (typeof f[data.part] === "undefined")
|
if (typeof f[data.part] === 'undefined') f[data.part] = { paths: [], points: [], coords: [] }
|
||||||
f[data.part] = { paths: [], points: [], coords: [] };
|
if (type === 'point') f[data.part].points.push(data.name)
|
||||||
if (type === "point") f[data.part].points.push(data.name);
|
else if (type === 'path') f[data.part].paths.push(data.name)
|
||||||
else if (type === "path") f[data.part].paths.push(data.name);
|
else if (type === 'coords') f[data.part].coords.push(data.coords)
|
||||||
else if (type === "coords") f[data.part].coords.push(data.coords);
|
else if (type === 'clearFocus') {
|
||||||
else if (type === "clearFocus") {
|
let i = focus[data.part][data.type].indexOf(data.name)
|
||||||
let i = focus[data.part][data.type].indexOf(data.name);
|
f[data.part][data.type].splice(i, 1)
|
||||||
f[data.part][data.type].splice(i, 1);
|
} else if (type === 'part') props.updateGist(data, 'settings', 'only')
|
||||||
} else if (type === "part") props.updateGist(data, "settings", "only");
|
|
||||||
|
|
||||||
setFocus(f);
|
setFocus(f)
|
||||||
};
|
}
|
||||||
|
|
||||||
const svgToFile = svg => {
|
const svgToFile = svg => {
|
||||||
const blob = new Blob([svg], {
|
const blob = new Blob([svg], {
|
||||||
type: "image/svg+xml;charset=utf-8"
|
type: 'image/svg+xml;charset=utf-8'
|
||||||
});
|
})
|
||||||
fileSaver.saveAs(blob, "freesewing-" + props.config.name + ".svg");
|
fileSaver.saveAs(blob, 'freesewing-' + props.config.name + '.svg')
|
||||||
};
|
}
|
||||||
|
|
||||||
if (props.svgExport) {
|
if (props.svgExport) {
|
||||||
svgToFile(
|
svgToFile(
|
||||||
|
@ -48,32 +47,32 @@ const DraftPattern = props => {
|
||||||
.use(theme)
|
.use(theme)
|
||||||
.draft()
|
.draft()
|
||||||
.render()
|
.render()
|
||||||
);
|
)
|
||||||
props.setSvgExport(false);
|
props.setSvgExport(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
paragraph: {
|
paragraph: {
|
||||||
padding: "0 1rem"
|
padding: '0 1rem'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
let pattern = new props.Pattern(props.gist.settings);
|
let pattern = new props.Pattern(props.gist.settings)
|
||||||
pattern.draft();
|
pattern.draft()
|
||||||
let patternProps = pattern.getRenderProps();
|
let patternProps = pattern.getRenderProps()
|
||||||
let focusCount = 0;
|
let focusCount = 0
|
||||||
if (focus !== null) {
|
if (focus !== null) {
|
||||||
for (let p of Object.keys(focus)) {
|
for (let p of Object.keys(focus)) {
|
||||||
for (let i in focus[p].points) focusCount++;
|
for (let i in focus[p].points) focusCount++
|
||||||
for (let i in focus[p].paths) focusCount++;
|
for (let i in focus[p].paths) focusCount++
|
||||||
for (let i in focus[p].coords) focusCount++;
|
for (let i in focus[p].coords) focusCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let gist = Prism.highlight(
|
let gist = Prism.highlight(
|
||||||
JSON.stringify(props.gist, null, 2),
|
JSON.stringify(props.gist, null, 2),
|
||||||
Prism.languages.javascript,
|
Prism.languages.javascript,
|
||||||
"javascript"
|
'javascript'
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fs-sa">
|
<div className="fs-sa">
|
||||||
|
@ -81,18 +80,10 @@ const DraftPattern = props => {
|
||||||
<h2>
|
<h2>
|
||||||
<FormattedMessage id="app.pattern" />
|
<FormattedMessage id="app.pattern" />
|
||||||
</h2>
|
</h2>
|
||||||
<Draft
|
<Draft {...patternProps} design={design} focus={focus} raiseEvent={raiseEvent} />
|
||||||
{...patternProps}
|
|
||||||
design={design}
|
|
||||||
focus={focus}
|
|
||||||
raiseEvent={raiseEvent}
|
|
||||||
/>
|
|
||||||
<h2>gist</h2>
|
<h2>gist</h2>
|
||||||
<div className="gatsby-highlight">
|
<div className="gatsby-highlight">
|
||||||
<pre
|
<pre className="language-json" dangerouslySetInnerHTML={{ __html: gist }} />
|
||||||
className="language-json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: gist }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -110,10 +101,7 @@ const DraftPattern = props => {
|
||||||
{focusCount > 0 ? (
|
{focusCount > 0 ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
 (
|
 (
|
||||||
<a
|
<a href="#logo" onClick={() => raiseEvent('clearFocusAll', null)}>
|
||||||
href="#logo"
|
|
||||||
onClick={() => raiseEvent("clearFocusAll", null)}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="app.reset" />
|
<FormattedMessage id="app.reset" />
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
@ -140,8 +128,8 @@ const DraftPattern = props => {
|
||||||
<DraftConfigurator
|
<DraftConfigurator
|
||||||
noDocs
|
noDocs
|
||||||
config={props.config}
|
config={props.config}
|
||||||
gist={props.gist}
|
data={props.gist}
|
||||||
updateGist={props.updateGist}
|
updateData={props.updateGist}
|
||||||
raiseEvent={props.raiseEvent}
|
raiseEvent={props.raiseEvent}
|
||||||
freesewing={props.freesewing}
|
freesewing={props.freesewing}
|
||||||
units={props.units}
|
units={props.units}
|
||||||
|
@ -149,8 +137,8 @@ const DraftPattern = props => {
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
DraftPattern.propTypes = {
|
DraftPattern.propTypes = {
|
||||||
gist: PropTypes.object.isRequired,
|
gist: PropTypes.object.isRequired,
|
||||||
|
@ -158,12 +146,12 @@ DraftPattern.propTypes = {
|
||||||
config: PropTypes.object.isRequired,
|
config: PropTypes.object.isRequired,
|
||||||
raiseEvent: PropTypes.func.isRequired,
|
raiseEvent: PropTypes.func.isRequired,
|
||||||
Pattern: PropTypes.func.isRequired,
|
Pattern: PropTypes.func.isRequired,
|
||||||
units: PropTypes.oneOf(["metric", "imperial"])
|
units: PropTypes.oneOf(['metric', 'imperial'])
|
||||||
};
|
}
|
||||||
|
|
||||||
DraftPattern.defaultProps = {
|
DraftPattern.defaultProps = {
|
||||||
units: "metric",
|
units: 'metric',
|
||||||
pointInfo: null
|
pointInfo: null
|
||||||
};
|
}
|
||||||
|
|
||||||
export default DraftPattern;
|
export default DraftPattern
|
||||||
|
|
|
@ -1,147 +1,142 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types'
|
||||||
import withGist from "../withGist";
|
import withGist from '../withGist'
|
||||||
import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles";
|
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
|
||||||
import Navbar from "../Navbar";
|
import Navbar from '../Navbar'
|
||||||
import defaultGist from "@freesewing/utils/defaultGist";
|
import defaultGist from '@freesewing/utils/defaultGist'
|
||||||
import storage from "@freesewing/utils/storage";
|
import storage from '@freesewing/utils/storage'
|
||||||
import { dark, light } from "@freesewing/mui-theme";
|
import { dark, light } from '@freesewing/mui-theme'
|
||||||
import withLanguage from "../withLanguage";
|
import withLanguage from '../withLanguage'
|
||||||
import LanguageIcon from "@material-ui/icons/Translate";
|
import LanguageIcon from '@material-ui/icons/Translate'
|
||||||
import DarkModeIcon from "@material-ui/icons/Brightness3";
|
import DarkModeIcon from '@material-ui/icons/Brightness3'
|
||||||
import LanguageChooser from "./LanguageChooser";
|
import LanguageChooser from './LanguageChooser'
|
||||||
import DraftPattern from "./DraftPattern";
|
import DraftPattern from './DraftPattern'
|
||||||
import SamplePattern from "./SamplePattern";
|
import SamplePattern from './SamplePattern'
|
||||||
import Welcome from "./Welcome";
|
import Welcome from './Welcome'
|
||||||
import Footer from "../Footer";
|
import Footer from '../Footer'
|
||||||
import Measurements from "./Measurements";
|
import Measurements from './Measurements'
|
||||||
|
|
||||||
const Workbench = props => {
|
const Workbench = props => {
|
||||||
const [display, setDisplay] = useState(null);
|
const [display, setDisplay] = useState(null)
|
||||||
const [pattern, setPattern] = useState(false);
|
const [pattern, setPattern] = useState(false)
|
||||||
const [theme, setTheme] = useState("light");
|
const [theme, setTheme] = useState('light')
|
||||||
const [measurements, setMeasurements] = useState(null);
|
const [measurements, setMeasurements] = useState(null)
|
||||||
const [svgExport, setSvgExport] = useState(false);
|
const [svgExport, setSvgExport] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let m = getMeasurements();
|
let m = getMeasurements()
|
||||||
setMeasurements(m);
|
setMeasurements(m)
|
||||||
props.updateGist(m, "settings", "measurements");
|
props.updateGist(m, 'settings', 'measurements')
|
||||||
setDisplay(getDisplay());
|
setDisplay(getDisplay())
|
||||||
props.setLanguage(props.userLanguage || "en");
|
props.setLanguage(props.userLanguage || 'en')
|
||||||
}, []);
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.from) props.importGist(props.from);
|
if (props.from) props.importGist(props.from)
|
||||||
}, [props.from]);
|
}, [props.from])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.language !== props.gist.settings.locale)
|
if (props.language !== props.gist.settings.locale)
|
||||||
props.updateGist(props.language, "settings", "locale");
|
props.updateGist(props.language, 'settings', 'locale')
|
||||||
}, [props.language]);
|
}, [props.language])
|
||||||
|
|
||||||
const getDisplay = () => storage.get(props.config.name + "-display");
|
const getDisplay = () => storage.get(props.config.name + '-display')
|
||||||
const saveDisplay = d => {
|
const saveDisplay = d => {
|
||||||
setDisplay(d);
|
setDisplay(d)
|
||||||
storage.set(props.config.name + "-display", d);
|
storage.set(props.config.name + '-display', d)
|
||||||
};
|
}
|
||||||
const getMeasurements = () =>
|
const getMeasurements = () => storage.get(props.config.name + '-measurements')
|
||||||
storage.get(props.config.name + "-measurements");
|
|
||||||
const saveMeasurements = data => {
|
const saveMeasurements = data => {
|
||||||
storage.set(props.config.name + "-measurements", data);
|
storage.set(props.config.name + '-measurements', data)
|
||||||
props.updateGist(data, "settings", "measurements");
|
props.updateGist(data, 'settings', 'measurements')
|
||||||
};
|
}
|
||||||
const updateMeasurement = (name, val) => {
|
const updateMeasurement = (name, val) => {
|
||||||
let updatedMeasurements = { ...measurements };
|
let updatedMeasurements = { ...measurements }
|
||||||
updatedMeasurements[name] = val;
|
updatedMeasurements[name] = val
|
||||||
setMeasurements(updatedMeasurements);
|
setMeasurements(updatedMeasurements)
|
||||||
saveMeasurements(updatedMeasurements);
|
saveMeasurements(updatedMeasurements)
|
||||||
};
|
}
|
||||||
const preloadMeasurements = model => {
|
const preloadMeasurements = model => {
|
||||||
let updatedMeasurements = {
|
let updatedMeasurements = {
|
||||||
...measurements,
|
...measurements,
|
||||||
...model
|
...model
|
||||||
};
|
}
|
||||||
setMeasurements(updatedMeasurements);
|
setMeasurements(updatedMeasurements)
|
||||||
saveMeasurements(updatedMeasurements);
|
saveMeasurements(updatedMeasurements)
|
||||||
};
|
}
|
||||||
const measurementsMissing = () => {
|
const measurementsMissing = () => {
|
||||||
let required = props.config.measurements;
|
let required = props.config.measurements
|
||||||
if (required.length < 1) return false;
|
if (required.length < 1) return false
|
||||||
if (measurements === null) return true;
|
if (measurements === null) return true
|
||||||
for (let m of required) {
|
for (let m of required) {
|
||||||
if (typeof measurements[m] === "undefined") return true;
|
if (typeof measurements[m] === 'undefined') return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false
|
||||||
};
|
}
|
||||||
const toggleDarkMode = () => {
|
const toggleDarkMode = () => {
|
||||||
if (theme === "light") setTheme("dark");
|
if (theme === 'light') setTheme('dark')
|
||||||
else setTheme("light");
|
else setTheme('light')
|
||||||
};
|
}
|
||||||
const raiseEvent = (type = null, data = null) => {};
|
const raiseEvent = (type = null, data = null) => {}
|
||||||
|
|
||||||
const navs = {
|
const navs = {
|
||||||
left: {
|
left: {
|
||||||
draft: {
|
draft: {
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: () => saveDisplay("draft"),
|
onClick: () => saveDisplay('draft'),
|
||||||
text: "cfp.draftYourPattern",
|
text: 'cfp.draftYourPattern',
|
||||||
active: display === "draft" ? true : false
|
active: display === 'draft' ? true : false
|
||||||
},
|
},
|
||||||
sample: {
|
sample: {
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: () => saveDisplay("sample"),
|
onClick: () => saveDisplay('sample'),
|
||||||
text: "cfp.testYourPattern",
|
text: 'cfp.testYourPattern',
|
||||||
active: display === "sample" ? true : false
|
active: display === 'sample' ? true : false
|
||||||
},
|
},
|
||||||
measurements: {
|
measurements: {
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: () => saveDisplay("measurements"),
|
onClick: () => saveDisplay('measurements'),
|
||||||
text: "app.measurements",
|
text: 'app.measurements',
|
||||||
active: display === "measurements" ? true : false
|
active: display === 'measurements' ? true : false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
right: {
|
right: {
|
||||||
version: {
|
version: {
|
||||||
type: "link",
|
type: 'link',
|
||||||
href: "https://github.com/freesewing/freesewing/releases",
|
href: 'https://github.com/freesewing/freesewing/releases',
|
||||||
text: "v" + props.freesewing.version
|
text: 'v' + props.freesewing.version
|
||||||
},
|
},
|
||||||
language: {
|
language: {
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: () => saveDisplay("languages"),
|
onClick: () => saveDisplay('languages'),
|
||||||
text: <LanguageIcon className="nav-icon" />,
|
text: <LanguageIcon className="nav-icon" />,
|
||||||
title: "Languages",
|
title: 'Languages',
|
||||||
active: display === "languages" ? true : false
|
active: display === 'languages' ? true : false
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: toggleDarkMode,
|
onClick: toggleDarkMode,
|
||||||
text: <DarkModeIcon className="nav-icon moon" />,
|
text: <DarkModeIcon className="nav-icon moon" />,
|
||||||
title: "Toggle dark mode"
|
title: 'Toggle dark mode'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
if (display === "draft" && !measurementsMissing())
|
if (display === 'draft' && !measurementsMissing())
|
||||||
navs.left.svgExport = {
|
navs.left.svgExport = {
|
||||||
type: "button",
|
type: 'button',
|
||||||
onClick: () => setSvgExport(true),
|
onClick: () => setSvgExport(true),
|
||||||
text: "app.export",
|
text: 'app.export',
|
||||||
active: false
|
active: false
|
||||||
};
|
}
|
||||||
// FIXME:
|
// FIXME:
|
||||||
navs.mleft = navs.left;
|
navs.mleft = navs.left
|
||||||
navs.mright = navs.right;
|
navs.mright = navs.right
|
||||||
let main = null;
|
let main = null
|
||||||
switch (display) {
|
switch (display) {
|
||||||
case "languages":
|
case 'languages':
|
||||||
main = (
|
main = <LanguageChooser setLanguage={props.setLanguage} setDisplay={saveDisplay} />
|
||||||
<LanguageChooser
|
break
|
||||||
setLanguage={props.setLanguage}
|
case 'draft':
|
||||||
setDisplay={saveDisplay}
|
if (measurementsMissing()) saveDisplay('measurements')
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "draft":
|
|
||||||
if (measurementsMissing()) saveDisplay("measurements");
|
|
||||||
main = (
|
main = (
|
||||||
<DraftPattern
|
<DraftPattern
|
||||||
freesewing={props.freesewing}
|
freesewing={props.freesewing}
|
||||||
|
@ -154,10 +149,10 @@ const Workbench = props => {
|
||||||
svgExport={svgExport}
|
svgExport={svgExport}
|
||||||
setSvgExport={setSvgExport}
|
setSvgExport={setSvgExport}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
break;
|
break
|
||||||
case "sample":
|
case 'sample':
|
||||||
if (measurementsMissing()) saveDisplay("measurements");
|
if (measurementsMissing()) saveDisplay('measurements')
|
||||||
main = (
|
main = (
|
||||||
<SamplePattern
|
<SamplePattern
|
||||||
freesewing={props.freesewing}
|
freesewing={props.freesewing}
|
||||||
|
@ -168,9 +163,9 @@ const Workbench = props => {
|
||||||
raiseEvent={raiseEvent}
|
raiseEvent={raiseEvent}
|
||||||
units={props.units}
|
units={props.units}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
break;
|
break
|
||||||
case "measurements":
|
case 'measurements':
|
||||||
main = (
|
main = (
|
||||||
<Measurements
|
<Measurements
|
||||||
measurements={measurements}
|
measurements={measurements}
|
||||||
|
@ -180,45 +175,39 @@ const Workbench = props => {
|
||||||
preloadMeasurements={preloadMeasurements}
|
preloadMeasurements={preloadMeasurements}
|
||||||
language={props.language}
|
language={props.language}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
main = <Welcome language={props.language} setDisplay={saveDisplay} />;
|
main = <Welcome language={props.language} setDisplay={saveDisplay} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const themes = { dark, light };
|
const themes = { dark, light }
|
||||||
return (
|
return (
|
||||||
<MuiThemeProvider theme={createMuiTheme(themes[theme])}>
|
<MuiThemeProvider theme={createMuiTheme(themes[theme])}>
|
||||||
<div
|
<div className={theme === 'light' ? 'theme-wrapper light' : 'theme-wrapper dark'}>
|
||||||
className={
|
{display !== 'welcome' ? <Navbar navs={navs} home={() => saveDisplay('welcome')} /> : null}
|
||||||
theme === "light" ? "theme-wrapper light" : "theme-wrapper dark"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{display !== "welcome" ? (
|
|
||||||
<Navbar navs={navs} home={() => saveDisplay("welcome")} />
|
|
||||||
) : null}
|
|
||||||
{main}
|
{main}
|
||||||
{display !== "welcome" ? <Footer language={props.language} /> : null}
|
{display !== 'welcome' ? <Footer language={props.language} /> : null}
|
||||||
</div>
|
</div>
|
||||||
</MuiThemeProvider>
|
</MuiThemeProvider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
Workbench.propTypes = {
|
Workbench.propTypes = {
|
||||||
freesewing: PropTypes.object.isRequired,
|
freesewing: PropTypes.object.isRequired,
|
||||||
Pattern: PropTypes.func.isRequired,
|
Pattern: PropTypes.func.isRequired,
|
||||||
config: PropTypes.object.isRequired,
|
config: PropTypes.object.isRequired,
|
||||||
from: PropTypes.object
|
from: PropTypes.object
|
||||||
};
|
}
|
||||||
|
|
||||||
Workbench.defaultProps = {
|
Workbench.defaultProps = {
|
||||||
from: { settings: { embed: true } }
|
from: { settings: { embed: true } }
|
||||||
};
|
}
|
||||||
|
|
||||||
export default withLanguage(
|
export default withLanguage(
|
||||||
withGist(Workbench, {
|
withGist(Workbench, {
|
||||||
gist: defaultGist,
|
gist: defaultGist,
|
||||||
store: true
|
store: true
|
||||||
}),
|
}),
|
||||||
"en"
|
'en'
|
||||||
);
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue