Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/components/page-header/confirm-overwrite-dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { render } from '../../test-utils';
import { CURRENT_VERSION } from '../../util/save';
import ConfirmOverwriteDialog from './confirm-overwrite-dialog';

// Mock the translation hook
vi.mock('react-i18next', async () => {
const actual = await vi.importActual('react-i18next');
return {
...actual,
useTranslation: () => ({
t: (key: string, options?: any) => {
if (key === 'header.open.confirmOverwrite.title') return 'Confirm overwrite';
if (key === 'header.open.confirmOverwrite.body')
return 'This action will overwrite your current project. Any unsaved changes will be lost. Are you sure you want to continue?';
if (key === 'header.open.confirmOverwrite.overwrite') return 'Overwrite';
if (key === 'cancel') return 'Cancel';
if (key === 'header.open.confirmOverwrite.newerVersion')
return `Warning: This save file is from a newer version (${options?.saveVersion}) than the current version (${options?.currentVersion}). Loading it may cause errors or undefined behavior.`;
return key;
},
}),
};
});

describe('ConfirmOverwriteDialog', () => {
it('should not show version warning when saveVersion is 0', () => {
render(<ConfirmOverwriteDialog isOpen={true} onClose={vi.fn()} onConfirm={vi.fn()} saveVersion={0} />);

expect(screen.getByText('Confirm overwrite')).toBeInTheDocument();
expect(
screen.getByText(
'This action will overwrite your current project. Any unsaved changes will be lost. Are you sure you want to continue?'
)
).toBeInTheDocument();
expect(screen.queryByText(/Warning:/)).not.toBeInTheDocument();
});

it('should not show version warning when saveVersion equals CURRENT_VERSION', () => {
render(
<ConfirmOverwriteDialog isOpen={true} onClose={vi.fn()} onConfirm={vi.fn()} saveVersion={CURRENT_VERSION} />
);

expect(screen.getByText('Confirm overwrite')).toBeInTheDocument();
expect(screen.queryByText(/Warning:/)).not.toBeInTheDocument();
});

it('should not show version warning when saveVersion is less than CURRENT_VERSION', () => {
render(
<ConfirmOverwriteDialog
isOpen={true}
onClose={vi.fn()}
onConfirm={vi.fn()}
saveVersion={CURRENT_VERSION - 10}
/>
);

expect(screen.getByText('Confirm overwrite')).toBeInTheDocument();
expect(screen.queryByText(/Warning:/)).not.toBeInTheDocument();
});

it('should show version warning when saveVersion is greater than CURRENT_VERSION', () => {
const newerVersion = CURRENT_VERSION + 10;
render(
<ConfirmOverwriteDialog isOpen={true} onClose={vi.fn()} onConfirm={vi.fn()} saveVersion={newerVersion} />
);

expect(screen.getByText('Confirm overwrite')).toBeInTheDocument();
expect(
screen.getByText(
`Warning: This save file is from a newer version (${newerVersion}) than the current version (${CURRENT_VERSION}). Loading it may cause errors or undefined behavior.`
)
).toBeInTheDocument();
});

it('should render buttons correctly', () => {
render(<ConfirmOverwriteDialog isOpen={true} onClose={vi.fn()} onConfirm={vi.fn()} saveVersion={0} />);

expect(screen.getByText('Cancel')).toBeInTheDocument();
expect(screen.getByText('Overwrite')).toBeInTheDocument();
});

it('should not render when isOpen is false', () => {
render(<ConfirmOverwriteDialog isOpen={false} onClose={vi.fn()} onConfirm={vi.fn()} saveVersion={100} />);

expect(screen.queryByText('Confirm overwrite')).not.toBeInTheDocument();
});
});
19 changes: 17 additions & 2 deletions src/components/page-header/confirm-overwrite-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,48 @@ import {
AlertDialogHeader,
AlertDialogOverlay,
Button,
Text,
} from '@chakra-ui/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CURRENT_VERSION } from '../../util/save';

interface ConfirmOverwriteDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
saveVersion: number;
}

/**
* A shared dialog component for confirming overwrite of current project data.
* Used when importing data from various sources that would replace the current graph.
*/
export default function ConfirmOverwriteDialog(props: ConfirmOverwriteDialogProps) {
const { isOpen, onClose, onConfirm } = props;
const { isOpen, onClose, onConfirm, saveVersion } = props;
const { t } = useTranslation();
const cancelRef = React.useRef<HTMLButtonElement | null>(null);

const isNewerVersion = saveVersion > CURRENT_VERSION;

return (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('header.open.confirmOverwrite.title')}
</AlertDialogHeader>
<AlertDialogBody>{t('header.open.confirmOverwrite.body')}</AlertDialogBody>
<AlertDialogBody>
{t('header.open.confirmOverwrite.body')}
{isNewerVersion && (
<Text mt={4} color="orange.500" fontWeight="semibold">
{t('header.open.confirmOverwrite.newerVersion', {
saveVersion,
currentVersion: CURRENT_VERSION,
})}
</Text>
)}
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
{t('cancel')}
Expand Down
13 changes: 11 additions & 2 deletions src/components/page-header/open-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { getCanvasSize } from '../../util/helpers';
import { useWindowSize } from '../../util/hooks';
import { pullServerImages, saveImagesFromParam } from '../../util/image';
import { saveManagerChannel, SaveManagerEvent, SaveManagerEventType } from '../../util/rmt-save';
import { getInitialParam, RMPSave, upgrade } from '../../util/save';
import { getInitialParam, parseVersionFromSave, RMPSave, upgrade } from '../../util/save';
import ConfirmOverwriteDialog from './confirm-overwrite-dialog';
import RmgParamAppClip from './rmg-param-app-clip';
import RmpGalleryAppClip from './rmp-gallery-app-clip';
Expand All @@ -21,6 +21,7 @@ export default function OpenActions() {
const { t } = useTranslation();
const { isOpen: isConfirmOpen, onOpen: onConfirmOpen, onClose: onConfirmClose } = useDisclosure();
const [paramToLoad, setParamToLoad] = React.useState<string | null>(null);
const [versionToLoad, setVersionToLoad] = React.useState<number>(0);

const size = useWindowSize();
const { height } = getCanvasSize(size);
Expand Down Expand Up @@ -103,6 +104,8 @@ export default function OpenActions() {
try {
const paramStr = await readFileAsText(file);
setParamToLoad(paramStr);
const version = parseVersionFromSave(paramStr);
setVersionToLoad(version);
onConfirmOpen();
} catch (err) {
dispatch(setGlobalAlert({ status: 'error', message: t('header.open.unknownError') }));
Expand All @@ -120,6 +123,7 @@ export default function OpenActions() {
const handleLoadTutorial = async () => {
const initialParam = await getInitialParam();
setParamToLoad(initialParam);
setVersionToLoad(parseVersionFromSave(initialParam));
onConfirmOpen();
};

Expand Down Expand Up @@ -185,7 +189,12 @@ export default function OpenActions() {
<RmpGalleryAppClip isOpen={isOpenGallery} onClose={() => setIsOpenGallery(false)} />
</Menu>

<ConfirmOverwriteDialog isOpen={isConfirmOpen} onClose={onConfirmClose} onConfirm={handleConfirmLoad} />
<ConfirmOverwriteDialog
isOpen={isConfirmOpen}
onClose={onConfirmClose}
onConfirm={handleConfirmLoad}
saveVersion={versionToLoad}
/>
</>
);
}
Expand Down
34 changes: 21 additions & 13 deletions src/components/page-header/rmp-gallery-app-clip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
const dispatch = useRootDispatch();
const { isOpen: isConfirmOpen, onOpen: onConfirmOpen, onClose: onConfirmClose } = useDisclosure();
const [workToLoad, setWorkToLoad] = React.useState<RMPSave | null>(null);
const [versionToLoad, setVersionToLoad] = React.useState<number>(0);

const {
telemetry: { project: isAllowProjectTelemetry },
Expand All @@ -56,8 +57,8 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
dispatch(refreshEdgesThunk());
}, [dispatch, refreshNodesThunk, refreshEdgesThunk, saveGraph, graph]);

const handleOpenTemplate = async (rmpSave: RMPSave) => {
// templates may be obsolete and require upgrades
const handleOpenWork = async (rmpSave: RMPSave) => {
// works may be obsolete and require upgrades
const { version, images, ...save } = JSON.parse(await upgrade(JSON.stringify(rmpSave))) as RMPSave;

// details panel will complain about unknown nodes or edges if the last selected is not cleared
Expand Down Expand Up @@ -86,15 +87,15 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {

const handleConfirmOpen = async () => {
if (workToLoad) {
await handleOpenTemplate(workToLoad);
await handleOpenWork(workToLoad);
}
onConfirmClose();
setWorkToLoad(null);
};

const fetchAndApplyTemplate = async (id: string, host?: string) => {
const fetchAndApplyWork = async (id: string, host?: string) => {
const urlPrefix = host ? `https://${host}` : '';
const template = (await (
const work = (await (
(
await Promise.allSettled([
fetch(`${urlPrefix}/rmp-gallery/resources/real_world/${id}.json`),
Expand All @@ -104,8 +105,9 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
)
.find(rep => rep.value.status === 200)
?.value.json()) as RMPSave | undefined;
if (template) {
setWorkToLoad(template);
if (work) {
setWorkToLoad(work);
setVersionToLoad(work.version);
onConfirmOpen();

if (isAllowAppTelemetry) {
Expand Down Expand Up @@ -135,8 +137,9 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
if (rep.status !== 200) {
throw new Error(t('header.open.importFailContent'));
}
const work = await rep.json();
setWorkToLoad(work as RMPSave);
const work = (await rep.json()) as RMPSave;
setWorkToLoad(work);
setVersionToLoad(work.version);
onConfirmOpen();

if (isAllowAppTelemetry) {
Expand All @@ -162,7 +165,7 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
//
// Since rmt will pass all params in `searchParams` here,
// e.g. https://railmapgen.github.io/?app=rmp&searchParams=id.hostname
// we will split id and host name from it and `fetchAndApplyTemplate`.
// we will split id and host name from it and `fetchAndApplyWork`.
//
// It's really ugly to have multiple search params in searchParams after `encodeURIComponent`,
// so we are joining id and host by '.'.
Expand All @@ -176,7 +179,7 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
let host: string | undefined = undefined;
if (firstDotIndex !== -1) host = params.substring(firstDotIndex + 1);
if (host === 'org') fetchAndApplyShare(id);
else fetchAndApplyTemplate(id, host);
else fetchAndApplyWork(id, host);
// clear the search params in rmt, otherwise it will be preserved and re-imported every time
rmgRuntime.updateAppMetadata({ search: '' });
}
Expand All @@ -186,7 +189,7 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
CHN.onmessage = e => {
const { event, data: id } = e.data;
if (event === RMP_GALLERY_CHANNEL_EVENT) {
fetchAndApplyTemplate(id);
fetchAndApplyWork(id);
onClose();
}
};
Expand All @@ -199,7 +202,12 @@ export default function RmpGalleryAppClip(props: RmpGalleryAppClipProps) {
<iframe src="/rmp-gallery/" loading="lazy" />
<CloseButton onClick={onClose} position="fixed" top="5px" right="15px" />
</RmgAppClip>
<ConfirmOverwriteDialog isOpen={isConfirmOpen} onClose={onConfirmClose} onConfirm={handleConfirmOpen} />
<ConfirmOverwriteDialog
isOpen={isConfirmOpen}
onClose={onConfirmClose}
onConfirm={handleConfirmOpen}
saveVersion={versionToLoad}
/>
</>
);
}
3 changes: 2 additions & 1 deletion src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,8 @@
"confirmOverwrite": {
"title": "Confirm overwrite",
"body": "This action will overwrite your current project. Any unsaved changes will be lost. Are you sure you want to continue?",
"overwrite": "Overwrite"
"overwrite": "Overwrite",
"newerVersion": "Warning: This save file is from a newer version ({{saveVersion}}) than the current version ({{currentVersion}}). Loading it may cause errors or undefined behavior."
}
},
"download": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,8 @@
"confirmOverwrite": {
"title": "上書きの確認",
"body": "この操作により現在のプロジェクトが上書きされます。未保存の変更はすべて失われます。続行してもよろしいですか?",
"overwrite": "上書き"
"overwrite": "上書き",
"newerVersion": "警告:この作品は新しいバージョン({{saveVersion}})からのもので、現在のバージョン({{currentVersion}})より新しいです。読み込むと失敗や未定義の動作が発生する可能性があります。"
}
},
"download": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,8 @@
"confirmOverwrite": {
"title": "확인 덮어쓰기",
"body": "이 작업은 현재 프로젝트를 덮어씁니다. 저장하지 않은 모든 변경 사항이 손실됩니다. 계속하시겠습니까?",
"overwrite": "덮어쓰기"
"overwrite": "덮어쓰기",
"newerVersion": "경고: 이 저장 파일은 현재 버전({{currentVersion}})보다 최신 버전({{saveVersion}})에서 가져온 것입니다. 로드하면 오류나 정의되지 않은 동작이 발생할 수 있습니다."
}
},
"download": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,8 @@
"confirmOverwrite": {
"title": "确认覆盖",
"body": "此操作将覆盖当前项目。所有未保存的更改将丢失。确定要继续吗?",
"overwrite": "覆盖"
"overwrite": "覆盖",
"newerVersion": "警告:此存档文件来自更新的版本({{saveVersion}}),高于当前版本({{currentVersion}})。加载它可能会导致错误或未定义的行为。"
}
},
"download": {
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,8 @@
"confirmOverwrite": {
"title": "確認覆蓋",
"body": "此操作將覆蓋當前項目。所有未保存的更改將丟失。確定要繼續嗎?",
"overwrite": "覆蓋"
"overwrite": "覆蓋",
"newerVersion": "警告:此存檔檔案來自更新的版本({{saveVersion}}),高於當前版本({{currentVersion}})。載入它可能會導致錯誤或未定義的行為。"
}
},
"download": {
Expand Down
Loading