mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-22 23:53:48 +01:00
Merge pull request #3976 from Snarling/ScriptEditorResponsiveness
UI: FIX #3975, #3882 Script Editor more responsive on resize, and fix dirty file indicator
This commit is contained in:
commit
46f5640dcd
@ -90,6 +90,7 @@ class OpenScript {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
lastPosition: monaco.Position;
|
lastPosition: monaco.Position;
|
||||||
model: ITextModel;
|
model: ITextModel;
|
||||||
|
isTxt: boolean;
|
||||||
|
|
||||||
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
|
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
@ -97,10 +98,11 @@ class OpenScript {
|
|||||||
this.hostname = hostname;
|
this.hostname = hostname;
|
||||||
this.lastPosition = lastPosition;
|
this.lastPosition = lastPosition;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
|
this.isTxt = fileName.endsWith(".txt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let openScripts: OpenScript[] = [];
|
const openScripts: OpenScript[] = [];
|
||||||
let currentScript: OpenScript | null = null;
|
let currentScript: OpenScript | null = null;
|
||||||
|
|
||||||
// Called every time script editor is opened
|
// Called every time script editor is opened
|
||||||
@ -134,33 +136,14 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
const [ramInfoOpen, setRamInfoOpen] = useState(false);
|
const [ramInfoOpen, setRamInfoOpen] = useState(false);
|
||||||
|
|
||||||
// Prevent Crash if script is open on deleted server
|
// Prevent Crash if script is open on deleted server
|
||||||
openScripts = openScripts.filter((script) => {
|
for (let i = openScripts.length - 1; i >= 0; i--) {
|
||||||
return GetServer(script.hostname) !== null;
|
GetServer(openScripts[i].hostname) === null && openScripts.splice(i, 1);
|
||||||
});
|
}
|
||||||
if (currentScript && GetServer(currentScript.hostname) === null) {
|
if (currentScript && GetServer(currentScript.hostname) === null) {
|
||||||
currentScript = openScripts[0];
|
currentScript = openScripts[0];
|
||||||
if (currentScript === undefined) currentScript = null;
|
if (currentScript === undefined) currentScript = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [dimensions, setDimensions] = useState({
|
|
||||||
height: window.innerHeight,
|
|
||||||
width: window.innerWidth,
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
const debouncedHandleResize = debounce(function handleResize() {
|
|
||||||
setDimensions({
|
|
||||||
height: window.innerHeight,
|
|
||||||
width: window.innerWidth,
|
|
||||||
});
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
window.addEventListener("resize", debouncedHandleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("resize", debouncedHandleResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentScript !== null) {
|
if (currentScript !== null) {
|
||||||
updateRAM(currentScript.code);
|
updateRAM(currentScript.code);
|
||||||
@ -253,10 +236,7 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
// Generates a new model for the script
|
// Generates a new model for the script
|
||||||
function regenerateModel(script: OpenScript): void {
|
function regenerateModel(script: OpenScript): void {
|
||||||
if (monacoRef.current !== null) {
|
if (monacoRef.current !== null) {
|
||||||
script.model = monacoRef.current.editor.createModel(
|
script.model = monacoRef.current.editor.createModel(script.code, script.isTxt ? "plaintext" : "javascript");
|
||||||
script.code,
|
|
||||||
script.fileName.endsWith(".txt") ? "plaintext" : "javascript",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +251,7 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function updateRAM(newCode: string): Promise<void> {
|
async function updateRAM(newCode: string): Promise<void> {
|
||||||
if (currentScript != null && currentScript.fileName.endsWith(".txt")) {
|
if (currentScript != null && currentScript.isTxt) {
|
||||||
debouncedSetRAM("N/A", [["N/A", ""]]);
|
debouncedSetRAM("N/A", [["N/A", ""]]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -424,7 +404,7 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
|
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
|
||||||
);
|
);
|
||||||
openScripts.push(newScript);
|
openScripts.push(newScript);
|
||||||
currentScript = { ...newScript };
|
currentScript = newScript;
|
||||||
editorRef.current.setModel(newScript.model);
|
editorRef.current.setModel(newScript.model);
|
||||||
updateRAM(newScript.code);
|
updateRAM(newScript.code);
|
||||||
}
|
}
|
||||||
@ -471,18 +451,8 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
const newPos = editorRef.current.getPosition();
|
const newPos = editorRef.current.getPosition();
|
||||||
if (newPos === null) return;
|
if (newPos === null) return;
|
||||||
if (currentScript !== null) {
|
if (currentScript !== null) {
|
||||||
currentScript = { ...currentScript, code: newCode, lastPosition: newPos };
|
currentScript.code = newCode;
|
||||||
const curIndex = openScripts.findIndex(
|
currentScript.lastPosition = newPos;
|
||||||
(script) =>
|
|
||||||
currentScript !== null &&
|
|
||||||
script.fileName === currentScript.fileName &&
|
|
||||||
script.hostname === currentScript.hostname,
|
|
||||||
);
|
|
||||||
const newArr = [...openScripts];
|
|
||||||
const tempScript = currentScript;
|
|
||||||
tempScript.code = newCode;
|
|
||||||
newArr[curIndex] = tempScript;
|
|
||||||
openScripts = [...newArr];
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
infLoop(newCode);
|
infLoop(newCode);
|
||||||
@ -519,7 +489,7 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
server.scripts,
|
server.scripts,
|
||||||
);
|
);
|
||||||
server.scripts.push(script);
|
server.scripts.push(script);
|
||||||
} else if (scriptToSave.fileName.endsWith(".txt")) {
|
} else if (scriptToSave.isTxt) {
|
||||||
for (let i = 0; i < server.textFiles.length; ++i) {
|
for (let i = 0; i < server.textFiles.length; ++i) {
|
||||||
if (server.textFiles[i].fn === scriptToSave.fileName) {
|
if (server.textFiles[i].fn === scriptToSave.fileName) {
|
||||||
server.textFiles[i].write(scriptToSave.code);
|
server.textFiles[i].write(scriptToSave.code);
|
||||||
@ -593,6 +563,7 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
server.scripts,
|
server.scripts,
|
||||||
);
|
);
|
||||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||||
|
rerender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -607,11 +578,12 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
server.scripts,
|
server.scripts,
|
||||||
);
|
);
|
||||||
server.scripts.push(script);
|
server.scripts.push(script);
|
||||||
} else if (currentScript.fileName.endsWith(".txt")) {
|
} else if (currentScript.isTxt) {
|
||||||
for (let i = 0; i < server.textFiles.length; ++i) {
|
for (let i = 0; i < server.textFiles.length; ++i) {
|
||||||
if (server.textFiles[i].fn === currentScript.fileName) {
|
if (server.textFiles[i].fn === currentScript.fileName) {
|
||||||
server.textFiles[i].write(currentScript.code);
|
server.textFiles[i].write(currentScript.code);
|
||||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||||
|
rerender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -623,26 +595,18 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||||
|
rerender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorder(list: Array<OpenScript>, startIndex: number, endIndex: number): OpenScript[] {
|
function reorder(list: OpenScript[], startIndex: number, endIndex: number): void {
|
||||||
const result = Array.from(list);
|
const [removed] = list.splice(startIndex, 1);
|
||||||
const [removed] = result.splice(startIndex, 1);
|
list.splice(endIndex, 0, removed);
|
||||||
result.splice(endIndex, 0, removed);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragEnd(result: any): void {
|
function onDragEnd(result: any): void {
|
||||||
// Dropped outside of the list
|
// Dropped outside of the list
|
||||||
if (!result.destination) {
|
if (!result.destination) return;
|
||||||
result;
|
reorder(openScripts, result.source.index, result.destination.index);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = reorder(openScripts, result.source.index, result.destination.index);
|
|
||||||
|
|
||||||
openScripts = items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentTabIndex(): number | undefined {
|
function currentTabIndex(): number | undefined {
|
||||||
@ -667,17 +631,17 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentScript = { ...openScripts[index] };
|
currentScript = openScripts[index];
|
||||||
|
|
||||||
if (editorRef.current !== null && openScripts[index] !== null) {
|
if (editorRef.current !== null && openScripts[index] !== null) {
|
||||||
if (openScripts[index].model === undefined || openScripts[index].model.isDisposed()) {
|
if (currentScript.model === undefined || currentScript.model.isDisposed()) {
|
||||||
regenerateModel(openScripts[index]);
|
regenerateModel(currentScript);
|
||||||
}
|
}
|
||||||
editorRef.current.setModel(openScripts[index].model);
|
editorRef.current.setModel(currentScript.model);
|
||||||
|
|
||||||
editorRef.current.setPosition(openScripts[index].lastPosition);
|
editorRef.current.setPosition(currentScript.lastPosition);
|
||||||
editorRef.current.revealLineInCenter(openScripts[index].lastPosition.lineNumber);
|
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
|
||||||
updateRAM(openScripts[index].code);
|
updateRAM(currentScript.code);
|
||||||
editorRef.current.focus();
|
editorRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -685,18 +649,10 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
function onTabClose(index: number): void {
|
function onTabClose(index: number): void {
|
||||||
// See if the script on the server is up to date
|
// See if the script on the server is up to date
|
||||||
const closingScript = openScripts[index];
|
const closingScript = openScripts[index];
|
||||||
const savedScriptIndex = openScripts.findIndex(
|
const savedScriptCode = closingScript.code;
|
||||||
(script) => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname,
|
const wasCurrentScript = openScripts[index] === currentScript;
|
||||||
);
|
|
||||||
let savedScriptCode = "";
|
|
||||||
if (savedScriptIndex !== -1) {
|
|
||||||
savedScriptCode = openScripts[savedScriptIndex].code;
|
|
||||||
}
|
|
||||||
const server = GetServer(closingScript.hostname);
|
|
||||||
if (server === null) throw new Error(`Server '${closingScript.hostname}' should not be null, but it is.`);
|
|
||||||
|
|
||||||
const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName);
|
if (dirty(index)) {
|
||||||
if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex].code) {
|
|
||||||
PromptEvent.emit({
|
PromptEvent.emit({
|
||||||
txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`,
|
txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`,
|
||||||
resolve: (result: boolean | string) => {
|
resolve: (result: boolean | string) => {
|
||||||
@ -709,40 +665,29 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openScripts.length > 1) {
|
openScripts.splice(index, 1);
|
||||||
openScripts = openScripts.filter((value, i) => i !== index);
|
if (openScripts.length === 0) {
|
||||||
|
currentScript = null;
|
||||||
let indexOffset = -1;
|
props.router.toTerminal();
|
||||||
if (openScripts[index + indexOffset] === undefined) {
|
return;
|
||||||
indexOffset = 1;
|
|
||||||
if (openScripts[index + indexOffset] === undefined) {
|
|
||||||
indexOffset = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change current script if we closed it
|
// Change current script if we closed it
|
||||||
|
if (wasCurrentScript) {
|
||||||
|
//Keep the same index unless we were on the last script
|
||||||
|
const indexOffset = openScripts.length === index ? -1 : 0;
|
||||||
currentScript = openScripts[index + indexOffset];
|
currentScript = openScripts[index + indexOffset];
|
||||||
if (editorRef.current !== null) {
|
if (editorRef.current !== null) {
|
||||||
if (
|
if (currentScript.model.isDisposed() || !currentScript.model) {
|
||||||
openScripts[index + indexOffset].model === undefined ||
|
regenerateModel(currentScript);
|
||||||
openScripts[index + indexOffset].model === null ||
|
|
||||||
openScripts[index + indexOffset].model.isDisposed()
|
|
||||||
) {
|
|
||||||
regenerateModel(openScripts[index + indexOffset]);
|
|
||||||
}
|
}
|
||||||
|
editorRef.current.setModel(currentScript.model);
|
||||||
editorRef.current.setModel(openScripts[index + indexOffset].model);
|
editorRef.current.setPosition(currentScript.lastPosition);
|
||||||
editorRef.current.setPosition(openScripts[index + indexOffset].lastPosition);
|
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
|
||||||
editorRef.current.revealLineInCenter(openScripts[index + indexOffset].lastPosition.lineNumber);
|
|
||||||
editorRef.current.focus();
|
editorRef.current.focus();
|
||||||
}
|
}
|
||||||
rerender();
|
|
||||||
} else {
|
|
||||||
// No more scripts are open
|
|
||||||
openScripts = [];
|
|
||||||
currentScript = null;
|
|
||||||
props.router.toTerminal();
|
|
||||||
}
|
}
|
||||||
|
rerender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTabUpdate(index: number): void {
|
function onTabUpdate(index: number): void {
|
||||||
@ -782,21 +727,20 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
|
|
||||||
function dirty(index: number): string {
|
function dirty(index: number): string {
|
||||||
const openScript = openScripts[index];
|
const openScript = openScripts[index];
|
||||||
const serverScriptCode = getServerCode(index);
|
const serverData = getServerCode(index);
|
||||||
if (serverScriptCode === null) return " *";
|
if (serverData === null) return " *";
|
||||||
|
// For scripts, server code is stored with its starting & trailing whitespace removed
|
||||||
// The server code is stored with its starting & trailing whitespace removed
|
const code = openScript.isTxt ? openScript.code : Script.formatCode(openScript.code);
|
||||||
const openScriptFormatted = Script.formatCode(openScript.code);
|
return serverData !== code ? " *" : "";
|
||||||
return serverScriptCode !== openScriptFormatted ? " *" : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServerCode(index: number): string | null {
|
function getServerCode(index: number): string | null {
|
||||||
const openScript = openScripts[index];
|
const openScript = openScripts[index];
|
||||||
const server = GetServer(openScript.hostname);
|
const server = GetServer(openScript.hostname);
|
||||||
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
|
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
|
||||||
|
const data = openScript.isTxt
|
||||||
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
|
? server.textFiles.find((t) => t.filename === openScript.fileName)?.text
|
||||||
return serverScript?.code ?? null;
|
: server.scripts.find((s) => s.filename === openScript.fileName)?.code;
|
||||||
|
return data ?? null;
|
||||||
}
|
}
|
||||||
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
setFilter(event.target.value);
|
setFilter(event.target.value);
|
||||||
@ -809,13 +753,6 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
(script) => script.hostname.includes(filter) || script.fileName.includes(filter),
|
(script) => script.hostname.includes(filter) || script.fileName.includes(filter),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Toolbars are roughly 112px:
|
|
||||||
// 8px body margin top
|
|
||||||
// 38.5px filename tabs
|
|
||||||
// 5px padding for top of editor
|
|
||||||
// 44px bottom tool bar + 16px margin
|
|
||||||
// + vim bar 34px
|
|
||||||
const editorHeight = dimensions.height - (130 + (options.vim ? 34 : 0));
|
|
||||||
const tabsMaxWidth = 1640;
|
const tabsMaxWidth = 1640;
|
||||||
const tabMargin = 5;
|
const tabMargin = 5;
|
||||||
const tabMaxWidth = filteredOpenScripts.length ? tabsMaxWidth / filteredOpenScripts.length - tabMargin : 0;
|
const tabMaxWidth = filteredOpenScripts.length ? tabsMaxWidth / filteredOpenScripts.length - tabMargin : 0;
|
||||||
@ -951,7 +888,7 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
beforeMount={beforeMount}
|
beforeMount={beforeMount}
|
||||||
onMount={onMount}
|
onMount={onMount}
|
||||||
loading={<Typography>Loading script editor!</Typography>}
|
loading={<Typography>Loading script editor!</Typography>}
|
||||||
height={`${editorHeight}px`}
|
height={`calc(100vh - ${130 + (options.vim ? 34 : 0)}px)`}
|
||||||
defaultLanguage="javascript"
|
defaultLanguage="javascript"
|
||||||
defaultValue={""}
|
defaultValue={""}
|
||||||
onChange={updateCode}
|
onChange={updateCode}
|
||||||
|
@ -76,6 +76,6 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" />
|
<div id="root" style="display:flex" />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -97,10 +97,10 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
"scrollbar-width": "none" /* for Firefox */,
|
"scrollbar-width": "none" /* for Firefox */,
|
||||||
margin: theme.spacing(0),
|
margin: theme.spacing(0),
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
display: "block",
|
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
|
width: "1px",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -2,10 +2,6 @@ import React, { useState, useEffect } from "react";
|
|||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { Theme } from "@mui/material/styles";
|
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
|
||||||
import createStyles from "@mui/styles/createStyles";
|
|
||||||
|
|
||||||
import { Terminal } from "../Terminal";
|
import { Terminal } from "../Terminal";
|
||||||
import { load } from "../db";
|
import { load } from "../db";
|
||||||
@ -18,16 +14,7 @@ import { ActivateRecoveryMode } from "./React/RecoveryRoot";
|
|||||||
import { hash } from "../hash/hash";
|
import { hash } from "../hash/hash";
|
||||||
import { pushGameReady } from "../Electron";
|
import { pushGameReady } from "../Electron";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
backgroundColor: theme.colors.backgroundprimary,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export function LoadingScreen(): React.ReactElement {
|
export function LoadingScreen(): React.ReactElement {
|
||||||
const classes = useStyles();
|
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
@ -69,9 +56,7 @@ export function LoadingScreen(): React.ReactElement {
|
|||||||
doLoad();
|
doLoad();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return loaded ? (
|
||||||
<Box className={classes.root}>
|
|
||||||
{loaded ? (
|
|
||||||
<GameRoot terminal={Terminal} engine={Engine} player={Player} />
|
<GameRoot terminal={Terminal} engine={Engine} player={Player} />
|
||||||
) : (
|
) : (
|
||||||
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
|
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
|
||||||
@ -89,7 +74,5 @@ export function LoadingScreen(): React.ReactElement {
|
|||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user