Skip to content

Commit 708bd47

Browse files
authored
feat: support cases add and delete (#22)
* fix: markdown code style * feat: support cases add and delete * feat: add reset cases button
1 parent a04a5e0 commit 708bd47

File tree

4 files changed

+195
-63
lines changed

4 files changed

+195
-63
lines changed

config/i18n.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,25 @@
114114
"request_error": {
115115
"en": "Network error, please check your network or refresh the page",
116116
"zhCN": "网络请求失败,请检查网络状况后刷新重试"
117+
},
118+
"confirm_title": {
119+
"en": "Confirm",
120+
"zhCN": "确认"
121+
},
122+
"confirm_btn": {
123+
"en": "confirm",
124+
"zhCN": "确认"
125+
},
126+
"cancel_btn": {
127+
"en": "cancel",
128+
"zhCN": "取消"
129+
},
130+
"confirm_reset_code": {
131+
"en": "Are you sure to reset code?",
132+
"zhCN": "确认重置代码?"
133+
},
134+
"confirm_reset_cases": {
135+
"en": "Are you sure to reset cases?",
136+
"zhCN": "确认重置示例?"
117137
}
118138
}

src/components/Markdown/index.module.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ blockquote {
1111
margin-bottom: 4px;
1212
}
1313
}
14-
code {
14+
p > code {
1515
margin: 0 2px;
1616
padding: 2px 4px;
1717
vertical-align: middle;

src/modules/Editor/index.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Modal, Skeleton } from '@arco-design/web-react';
1+
import { Modal, Skeleton, Tooltip } from '@arco-design/web-react';
22
import { IconCode, IconUndo } from '@arco-design/web-react/icon';
33
import debounce from 'lodash.debounce';
44
import { useCallback, useContext, useEffect, useState } from 'react';
@@ -44,10 +44,10 @@ function Editor() {
4444

4545
function resetCode() {
4646
const modal = Modal.confirm({
47-
title: 'Confirm',
48-
content: 'Are you sure to reset code?',
49-
okText: 'confirm',
50-
cancelText: 'cancel',
47+
title: i18nJson['confirm_title'][setting.language],
48+
content: i18nJson['confirm_reset_code'][setting.language],
49+
okText: i18nJson['confirm_btn'][setting.language],
50+
cancelText: i18nJson['cancel_btn'][setting.language],
5151
onOk: async function () {
5252
setLoading(true);
5353
localCache.setProblemCache(currentProblem.key, {
@@ -75,7 +75,9 @@ function Editor() {
7575
{i18nJson['code'][setting.language]}
7676
</span>
7777
<a onClick={resetCode} className={styles.reset}>
78-
<IconUndo />
78+
<Tooltip mini={true} content={'reset'}>
79+
<IconUndo />
80+
</Tooltip>
7981
</a>
8082
</div>
8183
<Skeleton

src/modules/Results/index.tsx

Lines changed: 166 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import type { editor } from 'monaco-editor';
22
import dayjs from 'dayjs';
33
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
4-
import { Button, Input, Skeleton } from '@arco-design/web-react';
4+
import {
5+
Button,
6+
Input,
7+
Modal,
8+
Skeleton,
9+
TabsProps,
10+
Tooltip,
11+
} from '@arco-design/web-react';
512
import debounce from 'lodash.debounce';
13+
import { IconUndo } from '@arco-design/web-react/icon';
614
import { CustomTabs } from '@src/components/CustomTabs';
715
import localCache, { PROBLEM_STATUS } from '@src/utils/local-cache';
816
import emitter from '@src/utils/emit';
@@ -20,6 +28,7 @@ import {
2028
monacoInstance,
2129
validateMonacoModel,
2230
} from '@src/utils/monaco';
31+
import { Setting } from '@src/utils/setting';
2332
import styles from './index.module.less';
2433

2534
const enum MainTab {
@@ -35,8 +44,65 @@ function formatErrorFromMarkers(markers: editor.IMarker[]) {
3544
});
3645
}
3746

47+
function createResultError(status: string[]) {
48+
return (
49+
<div className={styles['result-errors']}>
50+
<div className={styles['result-error-title']}>Compilation Error</div>
51+
<div className={styles['result-error-info']}>
52+
{status.map(function (error) {
53+
return (
54+
<div key={error} className={styles['result-error-item']}>
55+
{error}
56+
</div>
57+
);
58+
})}
59+
</div>
60+
</div>
61+
);
62+
}
63+
64+
function createCasesError(
65+
cases: NonNullable<Problem['cases']>,
66+
casesErrors: string[][],
67+
language: Setting['language'],
68+
) {
69+
return (
70+
<CustomTabs className={styles['case-tabs']}>
71+
{cases.map(function (_, index) {
72+
const result = casesErrors[index];
73+
return (
74+
<CustomTabs.TabPane
75+
key={index}
76+
title={`${i18nJson['case'][language]} ${index + 1}`}
77+
>
78+
{result.length > 0 && (
79+
<div className={styles['result-error-info']}>
80+
{result.map(function (error) {
81+
return (
82+
<div key={error} className={styles['result-error-item']}>
83+
{error}
84+
</div>
85+
);
86+
})}
87+
</div>
88+
)}
89+
{result.length === 0 && (
90+
<div className={styles['result-pass']}>Pass!</div>
91+
)}
92+
</CustomTabs.TabPane>
93+
);
94+
})}
95+
</CustomTabs>
96+
);
97+
}
98+
3899
const Results = function () {
39-
const [{ currentProblem, setting }] = useContext(Context);
100+
const [
101+
{
102+
currentProblem,
103+
setting: { language },
104+
},
105+
] = useContext(Context);
40106
const [loading, setLoading] = useState(true);
41107
const [activeMainTab, setActiveMainTab] = useState<string>(MainTab.cases);
42108
const [status, setStatus] = useState<string[] | 'Accept!'>([]);
@@ -46,6 +112,7 @@ const Results = function () {
46112
const [testRaw, setTestRaw] = useState<string | undefined>(undefined);
47113
const [casesErrors, setCasesErrors] = useState<string[][]>([]);
48114
const [btnDisabled, setBtnDisabled] = useState(true);
115+
const [activeCase, setActiveCase] = useState<string>('0');
49116

50117
const monacoEditorStatusListener = useCallback(
51118
() => setBtnDisabled(false),
@@ -58,6 +125,7 @@ const Results = function () {
58125
setTestRaw(raw);
59126
setStatus([]);
60127
setCasesErrors([]);
128+
setActiveCase('0');
61129
setCases(problem.cases || [NULL_CASE]);
62130
setActiveMainTab(MainTab.cases);
63131
setLoading(false);
@@ -170,65 +238,93 @@ const Results = function () {
170238
if (typeof status === 'string') {
171239
return <div className={styles['result-accept']}>Accepted!</div>;
172240
} else if (Array.isArray(status) && status.length > 0) {
173-
return (
174-
<div className={styles['result-errors']}>
175-
<div className={styles['result-error-title']}>
176-
Compilation Error
177-
</div>
178-
<div className={styles['result-error-info']}>
179-
{status.map(function (error) {
180-
return (
181-
<div key={error} className={styles['result-error-item']}>
182-
{error}
183-
</div>
184-
);
185-
})}
186-
</div>
187-
</div>
188-
);
241+
return createResultError(status);
189242
} else if (casesErrors.length > 0) {
190-
return (
191-
<CustomTabs className={styles['case-tabs']}>
192-
{cases.map(function (_, index) {
193-
const result = casesErrors[index];
194-
return (
195-
<CustomTabs.TabPane
196-
key={index}
197-
title={`${i18nJson['case'][setting.language]} ${index + 1}`}
198-
>
199-
{result.length > 0 && (
200-
<div className={styles['result-error-info']}>
201-
{result.map(function (error) {
202-
return (
203-
<div
204-
key={error}
205-
className={styles['result-error-item']}
206-
>
207-
{error}
208-
</div>
209-
);
210-
})}
211-
</div>
212-
)}
213-
{result.length === 0 && (
214-
<div className={styles['result-pass']}>Pass!</div>
215-
)}
216-
</CustomTabs.TabPane>
217-
);
218-
})}
219-
</CustomTabs>
220-
);
243+
return createCasesError(cases, casesErrors, language);
221244
} else {
222245
return (
223246
<div className={styles['result-empty']}>
224-
{i18nJson['please_run_or_submit_first'][setting.language]}
247+
{i18nJson['please_run_or_submit_first'][language]}
225248
</div>
226249
);
227250
}
228251
},
229252
[cases, casesErrors, status],
230253
);
231254

255+
function onAddCase() {
256+
if (cases.length >= 5) return;
257+
setActiveCase(String(cases.length));
258+
setCases([...cases, NULL_CASE]);
259+
}
260+
261+
function onDeleteCase(key: string) {
262+
if (cases.length <= 1) return;
263+
if (String(cases.length - 1) === activeCase) {
264+
setActiveCase(String(cases.length - 2));
265+
}
266+
setCases(cases.filter((_, index) => String(index) !== key));
267+
}
268+
269+
function onChangeCase(
270+
key: number,
271+
newCase: Partial<NonNullable<Problem['cases']>[number]>,
272+
) {
273+
setCases(
274+
cases.map(function (originCase, index) {
275+
if (key === index) {
276+
return {
277+
...originCase,
278+
...newCase,
279+
};
280+
}
281+
return originCase;
282+
}),
283+
);
284+
}
285+
286+
function resetCases() {
287+
const modal = Modal.confirm({
288+
title: i18nJson['confirm_title'][language],
289+
content: i18nJson['confirm_reset_cases'][language],
290+
okText: i18nJson['confirm_btn'][language],
291+
cancelText: i18nJson['cancel_btn'][language],
292+
onOk: async function () {
293+
setActiveCase('0');
294+
setCases(originCases.length == 0 ? [NULL_CASE] : originCases);
295+
modal.close();
296+
},
297+
});
298+
}
299+
300+
const renderTabHeader: TabsProps['renderTabHeader'] = function (
301+
tabProps,
302+
DefaultTabHeader,
303+
) {
304+
if (noCases) {
305+
return <DefaultTabHeader {...tabProps} />;
306+
}
307+
return (
308+
<div
309+
style={{
310+
width: '100%',
311+
display: 'flex',
312+
alignItems: 'center',
313+
justifyContent: 'space-between',
314+
}}
315+
>
316+
<div style={{ flex: 1 }}>
317+
<DefaultTabHeader {...tabProps} />
318+
</div>
319+
<a onClick={resetCases}>
320+
<Tooltip mini={true} content={'reset'}>
321+
<IconUndo />
322+
</Tooltip>
323+
</a>
324+
</div>
325+
);
326+
};
327+
232328
return (
233329
<Skeleton
234330
loading={loading}
@@ -244,28 +340,42 @@ const Results = function () {
244340
>
245341
<CustomTabs.TabPane
246342
key={MainTab.cases}
247-
title={i18nJson[MainTab.cases][setting.language]}
343+
title={i18nJson[MainTab.cases][language]}
248344
>
249-
<CustomTabs className={styles['case-tabs']}>
345+
<CustomTabs
346+
editable={!noCases}
347+
onAddTab={onAddCase}
348+
onDeleteTab={onDeleteCase}
349+
activeTab={activeCase}
350+
onChange={setActiveCase}
351+
className={styles['case-tabs']}
352+
renderTabHeader={renderTabHeader}
353+
>
250354
{cases.map(function ({ source, target }, index) {
251355
return (
252356
<CustomTabs.TabPane
253357
key={index}
254-
title={`${i18nJson['case'][setting.language]} ${index + 1}`}
358+
title={`${i18nJson['case'][language]} ${index + 1}`}
255359
>
256360
<div className={styles['case-header']}>Source</div>
257361
<Input.TextArea
258362
value={source}
259363
autoSize={true}
260364
readOnly={noCases}
261365
className={styles['case-input']}
366+
onChange={newSource =>
367+
onChangeCase(index, { source: newSource })
368+
}
262369
/>
263370
<div className={styles['case-header']}>Target</div>
264371
<Input.TextArea
265372
value={target}
266373
autoSize={true}
267374
readOnly={noCases}
268375
className={styles['case-input']}
376+
onChange={newTarget =>
377+
onChangeCase(index, { target: newTarget })
378+
}
269379
/>
270380
</CustomTabs.TabPane>
271381
);
@@ -274,7 +384,7 @@ const Results = function () {
274384
</CustomTabs.TabPane>
275385
<CustomTabs.TabPane
276386
key={MainTab.result}
277-
title={i18nJson[MainTab.result][setting.language]}
387+
title={i18nJson[MainTab.result][language]}
278388
>
279389
{resultContent}
280390
</CustomTabs.TabPane>
@@ -286,7 +396,7 @@ const Results = function () {
286396
disabled={btnDisabled}
287397
className={styles.btn}
288398
>
289-
{i18nJson['run_code'][setting.language]}
399+
{i18nJson['run_code'][language]}
290400
</Button>
291401
<Button
292402
type={'primary'}
@@ -295,7 +405,7 @@ const Results = function () {
295405
disabled={btnDisabled}
296406
className={styles.btn}
297407
>
298-
{i18nJson['submit_code'][setting.language]}
408+
{i18nJson['submit_code'][language]}
299409
</Button>
300410
</div>
301411
</div>

0 commit comments

Comments
 (0)