diff --git a/src/components/modal/template-modal.tsx b/src/components/modal/template-modal.tsx
index 1ddaba9db..f2606c64e 100644
--- a/src/components/modal/template-modal.tsx
+++ b/src/components/modal/template-modal.tsx
@@ -17,7 +17,7 @@ import { useTranslation } from 'react-i18next';
import { translateText } from '../../i18n/config';
import { useRootDispatch } from '../../redux';
import { companyConfig, templateList } from '@railmapgen/rmg-templates-resources';
-import { startLoading } from '../../redux/app/app-slice';
+import { startLoading, stopLoading } from '../../redux/app/app-slice';
import { Events } from '../../constants/constants';
import rmgRuntime from '@railmapgen/rmg-runtime';
import { RmgEnrichedButton } from '@railmapgen/rmg-components';
@@ -41,6 +41,7 @@ export default function TemplateModal(props: TemplateModalProps) {
);
onOpenParam(module.default);
rmgRuntime.event(Events.OPEN_TEMPLATE, { company, filename });
+ dispatch(stopLoading());
};
return (
diff --git a/src/components/page-header/header-actions.tsx b/src/components/page-header/header-actions.tsx
index b9e28be50..937378987 100644
--- a/src/components/page-header/header-actions.tsx
+++ b/src/components/page-header/header-actions.tsx
@@ -1,22 +1,38 @@
import React from 'react';
-import { HStack, Button } from '@chakra-ui/react';
+import { Button, HStack } from '@chakra-ui/react';
import DownloadActions from './download-actions';
-import { MdPalette } from 'react-icons/md';
+import { MdFolder, MdPalette } from 'react-icons/md';
import { useDispatch } from 'react-redux';
import { SidePanelMode } from '../../constants/constants';
-import { setSidePanelMode } from '../../redux/app/app-slice';
+import { setParamConfig, setSidePanelMode } from '../../redux/app/app-slice';
import { useTranslation } from 'react-i18next';
import OpenActions from './open-actions';
+import rmgRuntime, { RmgEnv } from '@railmapgen/rmg-runtime';
+import { useSearchParams } from 'react-router-dom';
export default function HeaderActions() {
const { t } = useTranslation();
const dispatch = useDispatch();
+ const [, setSearchParams] = useSearchParams();
+
+ const handleGoToSelectorView = () => {
+ // reset param config to stop param update trigger
+ dispatch(setParamConfig(undefined));
+ setSearchParams({});
+ };
+
return (
+ {rmgRuntime.getEnv() !== RmgEnv.PRD && (
+ } onClick={handleGoToSelectorView}>
+ {t('All projects')}
+
+ )}
+
-
+ {rmgRuntime.getEnv() === RmgEnv.PRD && }
}
+ onClick={() => setIsTemplateModalOpen(true)}
+ isDisabled={isProjectLimitReached}
+ >
+ {t('Open template')}
+
+ }
+ onClick={() => fileInputRef.current?.click()}
+ isDisabled={isProjectLimitReached}
+ >
+ {t('Import project')}
+
+ }
- onClick={handleOpen}
+ onClick={handleOpenSelected}
isDisabled={selectedParam === undefined}
>
- {t('Open project')}
+ {t('Open selected')}
+
+
+ setIsTemplateModalOpen(false)}
+ onOpenParam={handleOpenTemplate}
+ />
);
}
diff --git a/src/components/param-selector-view/param-selector.tsx b/src/components/param-selector-view/param-selector.tsx
index 660eb6d37..1eaa91bcc 100644
--- a/src/components/param-selector-view/param-selector.tsx
+++ b/src/components/param-selector-view/param-selector.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { ButtonGroup, Flex, IconButton, SystemStyleObject } from '@chakra-ui/react';
+import { Box, ButtonGroup, Flex, IconButton, SystemStyleObject, Text } from '@chakra-ui/react';
import { getRelativeTime } from '../../util/utils';
import { MdDelete } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
@@ -15,19 +15,22 @@ interface ParamSelectorProps {
const styles: SystemStyleObject = {
flex: 1,
- flexDirection: 'column',
- h: 200,
- overflowX: 'hidden',
- overflowY: 'auto',
- borderRadius: 'md',
- borderWidth: 2,
- '& .chakra-button__group': {
- flexShrink: 0,
- h: 10,
+ '& > div': {
+ flexDirection: 'column',
+ h: 200,
+ overflowX: 'hidden',
+ overflowY: 'auto',
+ borderRadius: 'md',
+ borderWidth: 2,
- '& button': {
- h: '100%',
+ '& .chakra-button__group': {
+ flexShrink: 0,
+ h: 10,
+
+ '& button': {
+ h: '100%',
+ },
},
},
};
@@ -37,32 +40,41 @@ export default function ParamSelector(props: ParamSelectorProps) {
const { t } = useTranslation();
return (
-
- {paramRegistry
- .slice()
- .sort((a, b) => {
- return (b.lastModified ?? 0) - (a.lastModified ?? 0);
- })
- .map(({ id, lastModified }) => (
-
- onParamSelect(id)}
- />
- }
- onClick={() => onParamRemove(id)}
- />
-
- ))}
-
+
+
+ {paramRegistry
+ .slice()
+ .sort((a, b) => {
+ return (b.lastModified ?? 0) - (a.lastModified ?? 0);
+ })
+ .map(({ id, lastModified }) => (
+
+ onParamSelect(id)}
+ />
+ }
+ onClick={() => onParamRemove(id)}
+ />
+
+ ))}
+
+ {paramRegistry.length >= 10 && (
+
+ {t('You have reached the maximum number of projects.')}
+
+ )}
+
);
}
diff --git a/src/components/root/app-router.tsx b/src/components/root/app-router.tsx
index 8d4dc12e1..b65da1183 100644
--- a/src/components/root/app-router.tsx
+++ b/src/components/root/app-router.tsx
@@ -49,6 +49,7 @@ export default function AppRouter() {
setSearchParams({ project: Object.keys(paramMap)[0] ?? nanoid() });
} else {
console.log('AppRouter:: No URL param ID provided. Rendering param selector view...');
+ setIsLoaded(false);
}
}
}, [paramId]);
diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json
index 8524c7ef5..e1265cbde 100644
--- a/src/i18n/translations/zh-Hans.json
+++ b/src/i18n/translations/zh-Hans.json
@@ -249,6 +249,7 @@
"desc": "降序"
},
+ "All projects": "所有项目",
"Are you sure to remove station? You cannot undo this action.": "您确定刪除此车站吗?您不能撤销此操作。",
"Blank project": "空白项目",
"Branch left end": "支线左端",
@@ -265,12 +266,22 @@
"Contribution Wiki": "贡献者指南",
"Contributors": "贡献者",
"Core contributors": "核心贡献者",
+ "day ago": "天前",
+ "days ago": "天前",
"Disconnect from main line": "从主线断开",
"Empty template": "空白模板",
"English name": "英文名称",
"Help and support": "帮助和支持",
+ "hour ago": "小時前",
+ "hours ago": "小時前",
+ "Import project": "导入项目",
+ "Just now": "刚刚",
+ "Last modified": "上次编辑",
+ "minute ago": "分钟前",
+ "minutes ago": "分钟前",
"No branches found": "未找到支线",
- "Open project": "打开项目",
+ "Open selected": "打开选中项目",
+ "Open template": "打开模板",
"Paid area": "付费区换乘",
"Please select...": "请选择...",
"Project ID": "项目ID",
@@ -293,6 +304,7 @@
"View": "查看",
"Visit": "访问",
"Visit GitHub": "访问GitHub",
+ "You have reached the maximum number of projects.": "您已到达项目数量上限。",
", open an issue and join us today!": ",创建一个Issue和加入我们!",
"CanvasType": {
diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json
index 0021c0a13..5c103145f 100644
--- a/src/i18n/translations/zh-Hant.json
+++ b/src/i18n/translations/zh-Hant.json
@@ -249,6 +249,7 @@
"desc": "降序"
},
+ "All projects": "所有專案",
"Are you sure to remove station? You cannot undo this action.": "確定移除該車站嗎?此動作無法還原。",
"Blank project": "空白專案",
"Branch left end": "支綫左端",
@@ -265,12 +266,22 @@
"Connect to main line": "連接至主綫",
"Contributors": "貢獻者",
"Core contributors": "核心貢獻者",
+ "day ago": "日前",
+ "days ago": "日前",
"Disconnect from main line": "从主綫斷開",
"Empty template": "空白範本",
"English name": "英文名稱",
"Help and support": "幫助及支援",
+ "hour ago": "小时前",
+ "hours ago": "小时前",
+ "Import project": "讀入專案",
+ "Just now": "剛才",
+ "Last modified": "上次修改",
+ "minute ago": "分鐘前",
+ "minutes ago": "分鐘前",
"No branches found": "未找到支綫",
- "Open project": "開啟專案",
+ "Open selected": "開啟所選專案",
+ "Open template": "開啟範本",
"Paid area": "付費區轉車",
"Please select...": "請選擇...",
"Project ID": "專案ID",
@@ -293,6 +304,7 @@
"View": "檢視",
"Visit": "造訪",
"Visit GitHub": "造訪GitHub",
+ "You have reached the maximum number of projects.": "你已到達專案數量上限。",
", open an issue and join us today!": ",新增一個Issue並且加入我們!",
"CanvasType": {
diff --git a/src/redux/app/action.ts b/src/redux/app/action.ts
index 334269688..51555eae8 100644
--- a/src/redux/app/action.ts
+++ b/src/redux/app/action.ts
@@ -31,6 +31,7 @@ export const readParam = (paramId: string, language: LanguageCode) => {
dispatch(setFullParam(nextParam));
} catch (err) {
console.warn('Failed to parse param.', err);
+ // FIXME: show error preventing from opening app view
console.log('Initiating new param for ID=' + paramId);
const nextParam = initParam(RmgStyle.MTR, language);
diff --git a/src/redux/app/app-slice.ts b/src/redux/app/app-slice.ts
index 86908b599..2b391d022 100644
--- a/src/redux/app/app-slice.ts
+++ b/src/redux/app/app-slice.ts
@@ -38,7 +38,7 @@ const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
- setParamConfig: (state, action: PayloadAction) => {
+ setParamConfig: (state, action: PayloadAction) => {
state.paramConfig = action.payload;
},
diff --git a/src/util/param-manager-utils.test.ts b/src/util/param-manager-utils.test.ts
index e5db502a3..1fb810199 100644
--- a/src/util/param-manager-utils.test.ts
+++ b/src/util/param-manager-utils.test.ts
@@ -84,10 +84,14 @@ describe('ParamMgrUtils', () => {
JSON.stringify({ lastModified: Date.now() })
);
+ // get actual param registry
const result = getParamRegistry();
expect(result).toHaveLength(2);
expect(result).toContainEqual(expect.objectContaining({ id: 'test-01' }));
expect(result).toContainEqual(expect.objectContaining({ id: 'test-03' }));
+
+ // remove invalid param config from localStorage
+ expect(window.localStorage.getItem(LocalStorageKey.PARAM_CONFIG_BY_ID + 'test-02')).toBeNull();
});
});
});
diff --git a/src/util/param-manager-utils.ts b/src/util/param-manager-utils.ts
index 6bc41013a..2fce065b6 100644
--- a/src/util/param-manager-utils.ts
+++ b/src/util/param-manager-utils.ts
@@ -65,9 +65,10 @@ export const upgradeLegacyParam = () => {
};
export const getParamRegistry = (): ParamConfig[] => {
+ // load all paramConfig from localStorage
const loadedParamRegistry = loadParamRegistry();
- // sync paramRegistry with localStorage items
+ // sync paramRegistry with actual localStorage param items
const actualParamRegistry: ParamConfig[] = [];
iterateLocalStorage(
key => key.startsWith(LocalStorageKey.PARAM_BY_ID),
@@ -82,11 +83,16 @@ export const getParamRegistry = (): ParamConfig[] => {
}
}
);
-
console.log(
'getParamRegistry():: Actual param found in localStorage',
actualParamRegistry.map(config => config.id)
);
+
+ // remove invalid paramConfig from localStorage
+ loadedParamRegistry
+ .filter(config => actualParamRegistry.every(c => c.id !== config.id))
+ .forEach(config => window.localStorage.removeItem(LocalStorageKey.PARAM_CONFIG_BY_ID + config.id));
+
return actualParamRegistry;
};
@@ -103,3 +109,13 @@ const iterateLocalStorage = (
count++;
}
};
+
+/**
+ * @param param Accept any param string and save to localStorage. It will not be validated until opening it.
+ */
+export const importParam = (param: string): string => {
+ const id = nanoid();
+ window.localStorage.setItem(LocalStorageKey.PARAM_BY_ID + id, param);
+ window.localStorage.setItem(LocalStorageKey.PARAM_CONFIG_BY_ID + id, JSON.stringify({ lastModified: Date.now() }));
+ return id;
+};
diff --git a/src/util/utils.ts b/src/util/utils.ts
index 49bb573c2..9b7111470 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -28,21 +28,25 @@ export const isSafari = () => {
return navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome');
};
-export const getRelativeTime = (timestamp?: number): string => {
+export const getRelativeTime = (timestamp?: number): string[] => {
if (timestamp) {
const deltaSeconds = new Date().getTime() - timestamp;
if (deltaSeconds < 60 * 1000) {
- return 'Less than 1 minute ago';
+ return ['Just now'];
} else if (deltaSeconds < 2 * 60 * 1000) {
- return '1 minute ago';
+ return ['1', 'minute ago'];
} else if (deltaSeconds < 60 * 60 * 1000) {
- return Math.floor(deltaSeconds / 1000 / 60).toString() + ' minutes ago';
+ return [Math.floor(deltaSeconds / 1000 / 60).toString(), 'minutes ago'];
} else if (deltaSeconds < 2 * 60 * 60 * 1000) {
- return '1 hour ago';
+ return ['1', 'hour ago'];
+ } else if (deltaSeconds < 24 * 60 * 60 * 1000) {
+ return [Math.floor(deltaSeconds / 1000 / 60 / 60).toString(), 'hours ago'];
+ } else if (deltaSeconds < 48 * 60 * 60 * 1000) {
+ return ['1', 'day ago'];
} else {
- return Math.floor(deltaSeconds / 1000 / 60 / 60).toString() + ' hours ago';
+ return [Math.floor(deltaSeconds / 1000 / 60 / 60 / 24).toString(), 'days ago'];
}
} else {
- return 'Unknown';
+ return ['Unknown'];
}
};