Skip to content

Commit

Permalink
#285 Support importing project from RMG in upload view
Browse files Browse the repository at this point in the history
  • Loading branch information
wongchito committed May 6, 2023
1 parent 4db2aa6 commit 2ba4386
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 13 deletions.
52 changes: 52 additions & 0 deletions src/components/app-clip/rmg-param-app-clip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Modal, ModalContent, ModalOverlay } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import rmgRuntime from '@railmapgen/rmg-runtime';
import { useRootDispatch } from '../../redux';

const CHANNEL_PREFIX = 'rmg-bridge--';

interface RmgAppClipProps {
templateId?: string;
onClose: () => void;
onImport: (param: Record<string, any>) => void;
}

export default function RmgParamAppClip(props: RmgAppClipProps) {
const { templateId, onClose, onImport } = props;

const dispatch = useRootDispatch();

const [appClipId] = useState(crypto.randomUUID());
const frameUrl =
'/rmg/import?' +
new URLSearchParams({
parentComponent: rmgRuntime.getAppName(),
parentId: appClipId,
});

useEffect(() => {
const channel = new BroadcastChannel(CHANNEL_PREFIX + appClipId);
channel.onmessage = ev => {
const { event, data } = ev.data;
console.log('[rmg-templates] Received event from RMG app clip:', event);
if (event === 'CLOSE') {
onClose();
} else if (event === 'IMPORT') {
onImport(data);
}
};

return () => {
channel.close();
};
}, [templateId]);

return (
<Modal isOpen={!!templateId} onClose={onClose}>
<ModalOverlay />
<ModalContent h={500} maxH="70%">
<iframe src={frameUrl} loading="lazy" width="100%" height="100%" />
</ModalContent>
</Modal>
);
}
69 changes: 57 additions & 12 deletions src/components/ticket-view/template-entry-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,41 @@ import { TemplateTicketEntry } from '../../redux/ticket/ticket-slice';
import useTranslatedName from '../hooks/use-translated-name';
import { useTranslation } from 'react-i18next';
import { RmgCard, RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import { IconButton, Input } from '@chakra-ui/react';
import { ChangeEvent } from 'react';
import { Button, HStack, Icon, IconButton, SystemStyleObject, Text, VStack } from '@chakra-ui/react';
import { ChangeEvent, useRef } from 'react';
import { readFileAsText } from '../../util/utils';
import { MdClose } from 'react-icons/md';
import { MdClose, MdInsertDriveFile } from 'react-icons/md';
import { LANGUAGE_NAMES, SUPPORTED_LANGUAGES } from '@railmapgen/rmg-translate';
import useTemplates from '../hooks/use-templates';

const style: SystemStyleObject = {
position: 'relative',

'& > div': {
overflow: 'hidden',
},

'& > div:last-of-type': {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
minW: 120,

'& input': {
display: 'none',
},
},
};

interface TemplateEntryCardProps {
company: string;
templateEntry: TemplateTicketEntry;
onLineChange: (line: string) => void;
onNewLineChange: (newLine: string) => void;
onMajorFlagChange: (majorUpdate: boolean) => void;
onLineNameChange: (lang: string, name: string) => void;
onParamChange: (param: Record<string, any>) => void;
onParamChange: (param?: Record<string, any>) => void;
onParamImport: () => void;
onRemove: () => void;
}

Expand All @@ -29,14 +49,16 @@ export default function TemplateEntryCard(props: TemplateEntryCardProps) {
onMajorFlagChange,
onLineNameChange,
onParamChange,
onParamImport,
onRemove,
} = props;
const { line, newLine, majorUpdate, templateName } = templateEntry;
const { line, newLine, majorUpdate, templateName, param } = templateEntry;

const { t } = useTranslation();
const translateName = useTranslatedName();

const { templates } = useTemplates(company);
const inputRef = useRef<HTMLInputElement>(null);

const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
Expand Down Expand Up @@ -81,12 +103,6 @@ export default function TemplateEntryCard(props: TemplateEntryCardProps) {
onChange: value => onLineChange(value as string),
minW: 150,
},
{
type: 'custom',
label: t('Configuration file'),
component: <Input variant="flushed" size="xs" type="file" accept=".json" onChange={handleFileUpload} />,
minW: 250,
},
{
type: 'input',
label: t('Line code'),
Expand Down Expand Up @@ -116,7 +132,7 @@ export default function TemplateEntryCard(props: TemplateEntryCardProps) {
});

return (
<RmgCard position="relative" direction="column">
<RmgCard sx={style}>
<IconButton
size="sm"
variant="ghost"
Expand All @@ -130,6 +146,35 @@ export default function TemplateEntryCard(props: TemplateEntryCardProps) {
onClick={onRemove}
/>
<RmgFields fields={[...fields, ...languageFields]} minW={110} />

<VStack>
{param ? (
<>
<Icon as={MdInsertDriveFile} boxSize={10} />
<Text as="i" fontSize="xs">
({t('Size')}: {JSON.stringify(param).length} {t('chars')})
</Text>
<Button size="sm" onClick={() => onParamChange(undefined)}>
{t('Remove')}
</Button>
</>
) : (
<>
<Text as="i" fontSize="sm">
{t('Import from')}
</Text>
<HStack spacing={1}>
<Button size="sm" onClick={onParamImport}>
RMG
</Button>
<Button size="sm" onClick={() => inputRef.current?.click()}>
{t('Local')}
</Button>
<input ref={inputRef} type="file" accept=".json" onChange={handleFileUpload} />
</HStack>
</>
)}
</VStack>
</RmgCard>
);
}
18 changes: 18 additions & 0 deletions src/components/ticket-view/templates-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import { MdAdd, MdHelp } from 'react-icons/md';
import useTemplates from '../hooks/use-templates';
import { RmgLoader } from '@railmapgen/rmg-components';
import RmgParamAppClip from '../app-clip/rmg-param-app-clip';
import { useState } from 'react';

export default function TemplatesSection() {
const { t } = useTranslation();
Expand All @@ -22,11 +24,20 @@ export default function TemplatesSection() {
const { company, templates } = useRootSelector(state => state.ticket);
const { templates: templateList, isLoading } = useTemplates(company);

const [templateIdForAppClip, setTemplateIdForAppClip] = useState<string>();

const handleLineChange = (entryId: string, line: string) => {
const existingTemplate = templateList.find(entry => entry.filename === line);
dispatch(setTemplateLineById({ id: entryId, line, name: existingTemplate?.name }));
};

const handleParamImport = (param: Record<string, any>) => {
if (templateIdForAppClip) {
dispatch(setTemplateParamById({ id: templateIdForAppClip, param }));
}
setTemplateIdForAppClip(undefined);
};

return (
<Box as="section" mt={3} position="relative">
{isLoading && <RmgLoader isIndeterminate />}
Expand Down Expand Up @@ -56,6 +67,7 @@ export default function TemplatesSection() {
onMajorFlagChange={majorUpdate => dispatch(setTemplateMajorFlagById({ id: entry.id, majorUpdate }))}
onLineNameChange={(lang, name) => dispatch(setTemplateLineNameById({ id: entry.id, lang, name }))}
onParamChange={param => dispatch(setTemplateParamById({ id: entry.id, param }))}
onParamImport={() => setTemplateIdForAppClip(entry.id)}
onRemove={() => dispatch(removeTemplate(entry.id))}
/>
))}
Expand All @@ -65,6 +77,12 @@ export default function TemplatesSection() {
{t('Add item')}
</Button>
</HStack>

<RmgParamAppClip
templateId={templateIdForAppClip}
onClose={() => setTemplateIdForAppClip(undefined)}
onImport={handleParamImport}
/>
</Box>
);
}
5 changes: 5 additions & 0 deletions src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
"Add item": "添加项目",
"Add or update templates": "添加或更新模本",
"Briefly describe your changes and provide justification": "简洁地描述您的改动并提供理由",
"chars": "字符",
"Click copy button and paste into issue body": "点击复制按钮然后粘贴到 Issue 正文",
"Close": "关闭",
"Company": "公司",
"Follow the instructions below to open an Issue": "按下方的指引打开 Issue",
"Import from": "导入自",
"Justification for major update of": "重大更新的理由:",
"Line": "线路",
"Line code": "线路代码",
"Local": "本地",
"Major update": "重大更新",
"Name": "名称",
"Open": "打开",
"Please provide suitable source and justification.": "请您提供适当的来源和理由。",
"Railway company": "铁路公司",
"Remove": "删除",
"Size": "大小",
"Style": "风格",
"Submit templates": "提交模板",
"Templates": "模板",
Expand Down
5 changes: 5 additions & 0 deletions src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
"Add item": "新增項目",
"Add or update templates": "新增或更新範本",
"Briefly describe your changes and provide justification": "簡短地描述你的變更並提供理由",
"chars": "字元",
"Click copy button and paste into issue body": "點擊複製按鈕並於 Issue 正文內貼上",
"Close": "關閉",
"Company": "公司",
"Follow the instructions below to open an Issue": "按下方的指引開啟 Issue",
"Import from": "讀入自",
"Justification for major update of": "重大更新的理由:",
"Line": "路線",
"Line code": "路線代碼",
"Local": "本機",
"Major update": "重大更新",
"Name": "名稱",
"Open": "開啟",
"Please provide suitable source and justification.": "請你提供適當的來源及理由。",
"Railway company": "鐵路公司",
"Remove": "移除",
"Size": "大小",
"Style": "風格",
"Submit templates": "提交範本",
"Templates": "範本",
Expand Down
2 changes: 1 addition & 1 deletion src/redux/ticket/ticket-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const ticketSlice = createSlice({
);
},

setTemplateParamById: (state, action: PayloadAction<{ id: string; param: Record<string, any> }>) => {
setTemplateParamById: (state, action: PayloadAction<{ id: string; param?: Record<string, any> }>) => {
state.templates = state.templates.map(entry =>
entry.id === action.payload.id ? { ...entry, param: action.payload.param } : entry
);
Expand Down
9 changes: 9 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ export default defineConfig({
}),
splitVendorChunkPlugin(),
],
server: {
proxy: {
'/rmg/': {
target: 'https://uat-railmapgen.github.io',
changeOrigin: true,
secure: false,
},
},
},
test: {
globals: true,
environment: 'jsdom',
Expand Down

0 comments on commit 2ba4386

Please sign in to comment.