UI: Warn player if they are editing and saving files on non-home servers (#1968)

This commit is contained in:
catloversg
2025-02-17 09:26:45 +07:00
committed by GitHub
parent 13990fbe4c
commit 63d7061fd8
4 changed files with 63 additions and 32 deletions

View File

@@ -25,7 +25,7 @@ import { PromptEvent } from "../../ui/React/PromptManager";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { dirty, getServerCode, makeModel } from "./utils"; import { isUnsavedFile, getServerCode, makeModel } from "./utils";
import { OpenScript } from "./OpenScript"; import { OpenScript } from "./OpenScript";
import { Tabs } from "./Tabs"; import { Tabs } from "./Tabs";
import { Toolbar } from "./Toolbar"; import { Toolbar } from "./Toolbar";
@@ -38,6 +38,9 @@ import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
import { hasScriptExtension, isLegacyScript, type ScriptFilePath } from "../../Paths/ScriptFilePath"; import { hasScriptExtension, isLegacyScript, type ScriptFilePath } from "../../Paths/ScriptFilePath";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import type { BaseServer } from "../../Server/BaseServer"; import type { BaseServer } from "../../Server/BaseServer";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { ToastVariant } from "@enums";
// Extend acorn-walk to support TypeScript nodes. // Extend acorn-walk to support TypeScript nodes.
extendAcornWalkForTypeScriptNodes(walk.base); extendAcornWalkForTypeScriptNodes(walk.base);
@@ -206,13 +209,7 @@ function Root(props: IProps): React.ReactElement {
return; return;
} }
saveScript(currentScript);
const server = GetServer(currentScript.hostname);
if (server === null) throw new Error("Server should not be null but it is.");
server.writeToContentFile(currentScript.path, currentScript.code);
if (Settings.SaveGameOnFileSave) {
saveObject.saveGame().catch((error) => exceptionAlert(error));
}
rerender(); rerender();
}, [rerender]); }, [rerender]);
@@ -363,7 +360,13 @@ function Root(props: IProps): React.ReactElement {
function saveScript(scriptToSave: OpenScript): void { function saveScript(scriptToSave: OpenScript): void {
const server = GetServer(scriptToSave.hostname); const server = GetServer(scriptToSave.hostname);
if (!server) throw new Error("Server should not be null but it is."); if (!server) {
throw new Error("Server should not be null but it is.");
}
// Show a warning message if the file is on a non-home server.
if (scriptToSave.hostname !== SpecialServers.Home) {
SnackbarEvents.emit("You saved a file on a non-home server!", ToastVariant.WARNING, 3000);
}
// This server helper already handles overwriting, etc. // This server helper already handles overwriting, etc.
server.writeToContentFile(scriptToSave.path, scriptToSave.code); server.writeToContentFile(scriptToSave.path, scriptToSave.code);
if (Settings.SaveGameOnFileSave) { if (Settings.SaveGameOnFileSave) {
@@ -411,7 +414,7 @@ function Root(props: IProps): React.ReactElement {
const savedScriptCode = closingScript.code; const savedScriptCode = closingScript.code;
const wasCurrentScript = openScripts[index] === currentScript; const wasCurrentScript = openScripts[index] === currentScript;
if (dirty(openScripts, index)) { if (isUnsavedFile(openScripts, index)) {
PromptEvent.emit({ PromptEvent.emit({
txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`, txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`,
resolve: (result: boolean | string) => { resolve: (result: boolean | string) => {

View File

@@ -3,6 +3,7 @@ import { DraggableProvided } from "react-beautiful-dnd";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
@@ -11,9 +12,10 @@ import { Settings } from "../../Settings/Settings";
interface IProps { interface IProps {
provided: DraggableProvided; provided: DraggableProvided;
title: string; fullPath: string;
isActive: boolean; isActive: boolean;
isExternal: boolean; isExternal: boolean;
isUnsaved: boolean;
onClick: () => void; onClick: () => void;
onClose: () => void; onClose: () => void;
@@ -24,7 +26,7 @@ const tabMargin = 5;
const tabIconWidth = 25; const tabIconWidth = 25;
const tabIconHeight = 38.5; const tabIconHeight = 38.5;
export function Tab({ provided, title, isActive, isExternal, onClick, onClose, onUpdate }: IProps) { export function Tab({ provided, fullPath, isActive, isExternal, isUnsaved, onClick, onClose, onUpdate }: IProps) {
const colorProps = isActive const colorProps = isActive
? { ? {
background: Settings.theme.button, background: Settings.theme.button,
@@ -37,8 +39,35 @@ export function Tab({ provided, title, isActive, isExternal, onClick, onClose, o
color: Settings.theme.secondary, color: Settings.theme.secondary,
}; };
let tabTitle;
let tooltipTitle;
if (isUnsaved) {
// Show a blinking "*" character to notify the player that this file is dirtied.
tabTitle = (
<>
<Typography component="span" color={Settings.theme.warning}>
*{" "}
</Typography>
{fullPath}
</>
);
} else {
tabTitle = fullPath;
}
if (isExternal) { if (isExternal) {
colorProps.color = Settings.theme.info; colorProps.color = Settings.theme.warning;
// Show a warning message if this file is on a non-home server.
tooltipTitle = (
<Typography component="span" color={Settings.theme.warning}>
{tabTitle}
<br />
This file is on a non-home server. You will lose all files on non-home servers when they are deleted or
recreated (install augmentations, soft reset, deleted by NS APIs, etc.).
</Typography>
);
} else {
tooltipTitle = tabTitle;
} }
const iconButtonStyle = { const iconButtonStyle = {
maxWidth: tabIconWidth, maxWidth: tabIconWidth,
@@ -71,12 +100,14 @@ export function Tab({ provided, title, isActive, isExternal, onClick, onClose, o
border: "1px solid " + Settings.theme.well, border: "1px solid " + Settings.theme.well,
}} }}
> >
<Tooltip title={title}> <Tooltip title={tooltipTitle}>
<Button <Button
onClick={onClick} onClick={onClick}
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
if (e.button === 1) onClose(); if (e.button === 1) {
onClose();
}
}} }}
style={{ style={{
minHeight: tabIconHeight, minHeight: tabIconHeight,
@@ -84,7 +115,7 @@ export function Tab({ provided, title, isActive, isExternal, onClick, onClose, o
...colorProps, ...colorProps,
}} }}
> >
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>{title}</span> <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{tabTitle}</span>
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title="Overwrite editor content with saved file content"> <Tooltip title="Overwrite editor content with saved file content">

View File

@@ -13,9 +13,10 @@ import SearchIcon from "@mui/icons-material/Search";
import { useBoolean, useRerender } from "../../ui/React/hooks"; import { useBoolean, useRerender } from "../../ui/React/hooks";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { dirty, reorder } from "./utils"; import { isUnsavedFile, reorder } from "./utils";
import { OpenScript } from "./OpenScript"; import { OpenScript } from "./OpenScript";
import { Tab } from "./Tab"; import { Tab } from "./Tab";
import { SpecialServers } from "../../Server/data/SpecialServers";
const tabsMaxWidth = 1640; const tabsMaxWidth = 1640;
const searchWidth = 180; const searchWidth = 180;
@@ -119,22 +120,16 @@ export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpda
{filteredScripts.map(({ script, originalIndex }, index) => { {filteredScripts.map(({ script, originalIndex }, index) => {
const { path: fileName, hostname } = script; const { path: fileName, hostname } = script;
const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname; const isActive = currentScript?.path === script.path && currentScript.hostname === script.hostname;
const fullPath = `${hostname}:/${fileName}`;
const title = `${hostname}:~${fileName.startsWith("/") ? "" : "/"}${fileName} ${dirty(scripts, index)}`;
return ( return (
<Draggable <Draggable key={fullPath} draggableId={fullPath} index={index} disableInteractiveElementBlocking>
key={fileName + hostname}
draggableId={fileName + hostname}
index={index}
disableInteractiveElementBlocking
>
{(provided) => ( {(provided) => (
<Tab <Tab
provided={provided} provided={provided}
title={title} fullPath={fullPath}
isActive={isActive} isActive={isActive}
isExternal={hostname !== "home"} isExternal={hostname !== SpecialServers.Home}
isUnsaved={isUnsavedFile(scripts, index)}
onClick={() => onTabClick(originalIndex)} onClick={() => onTabClick(originalIndex)}
onClose={() => onTabClose(originalIndex)} onClose={() => onTabClose(originalIndex)}
onUpdate={() => onTabUpdate(originalIndex)} onUpdate={() => onTabUpdate(originalIndex)}

View File

@@ -12,11 +12,13 @@ function getServerCode(scripts: OpenScript[], index: number): string | null {
return data; return data;
} }
function dirty(scripts: OpenScript[], index: number): string { function isUnsavedFile(scripts: OpenScript[], index: number): boolean {
const openScript = scripts[index]; const openScript = scripts[index];
const serverData = getServerCode(scripts, index); const serverData = getServerCode(scripts, index);
if (serverData === null) return " *"; if (serverData === null) {
return serverData !== openScript.code ? " *" : ""; return true;
}
return serverData !== openScript.code;
} }
function reorder(list: unknown[], startIndex: number, endIndex: number): void { function reorder(list: unknown[], startIndex: number, endIndex: number): void {
@@ -60,4 +62,4 @@ function makeModel(hostname: string, filename: string, code: string): editor.ITe
return editor.createModel(code, language, uri); return editor.createModel(code, language, uri);
} }
export { getServerCode, dirty, reorder, makeModel }; export { getServerCode, isUnsavedFile, reorder, makeModel };