From 3996f6f2ef7f277b2d22ea87be4eabbea39bcb2c Mon Sep 17 00:00:00 2001
From: undefined
Date: Tue, 4 Nov 2025 17:08:20 +0800
Subject: [PATCH] ui: drop blueprintjs 6 (#1073)
---
packages/common/subtask.ts | 7 +-
.../frontend/autocomplete/autocomplete.scss | 18 --
.../components/datepicker/datepicker.styl | 1 -
.../components/discussion/history.page.tsx | 52 ++--
.../components/discussion/reaction.page.tsx | 37 ++-
.../components/notification/index.ts | 86 ++++--
.../notification/notification.page.styl | 4 +
.../components/problem/tag.page.styl | 30 ++
.../problemconfig/ProblemConfigForm.tsx | 18 +-
.../problemconfig/ProblemConfigTree.tsx | 144 +++++-----
.../components/problemconfig/ProblemType.tsx | 271 ++++++++----------
.../components/problemconfig/index.tsx | 107 ++++---
.../problemconfig/reducer/config.ts | 12 +-
.../problemconfig/tree/AddTestcase.tsx | 96 +++----
.../problemconfig/tree/SubtaskSettings.tsx | 200 ++++++-------
.../problemconfig/tree/Testcase.tsx | 79 +++--
packages/ui-default/components/upload.tsx | 58 ++--
packages/ui-default/package.json | 21 +-
.../ui-default/pages/problem_config.page.styl | 45 +--
.../ui-default/pages/problem_config.page.tsx | 13 +-
.../ui-default/pages/problem_main.page.tsx | 2 +-
packages/ui-default/postcss.config.js | 10 +
.../ui-default/templates/contest_detail.html | 14 +-
.../ui-default/templates/problem_detail.html | 22 +-
packages/ui-default/theme/bp6-compat.css | 48 ++++
packages/ui-default/theme/dark.styl | 16 --
packages/ui-default/theme/default.js | 5 +-
27 files changed, 764 insertions(+), 652 deletions(-)
create mode 100644 packages/ui-default/theme/bp6-compat.css
diff --git a/packages/common/subtask.ts b/packages/common/subtask.ts
index 6a1fa5747..f571d7a80 100644
--- a/packages/common/subtask.ts
+++ b/packages/common/subtask.ts
@@ -1,4 +1,4 @@
-import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
+import { parseMemoryMB, parseTimeMS, sortFiles } from '@hydrooj/utils/lib/common';
export function convertIniConfig(ini: string) {
const f = ini.split('\n');
@@ -118,13 +118,16 @@ export function readSubtasksFromFiles(files: string[], config) {
id: sid,
};
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
- else subtask[sid].cases.push(c);
+ else if (!subtask[sid].cases.find((i) => i.input === c.input && i.output === c.output)) {
+ subtask[sid].cases.push(c);
+ }
break;
}
}
if (match) break;
}
}
+ for (const id in subtask) subtask[id].cases = sortFiles(subtask[id].cases, 'input');
return Object.values(subtask);
}
diff --git a/packages/components/frontend/autocomplete/autocomplete.scss b/packages/components/frontend/autocomplete/autocomplete.scss
index 8383271ff..3f391a380 100644
--- a/packages/components/frontend/autocomplete/autocomplete.scss
+++ b/packages/components/frontend/autocomplete/autocomplete.scss
@@ -75,12 +75,6 @@
white-space: nowrap;
text-overflow: ellipsis;
}
-
- &>span.bp6-icon>svg {
- font-size: 12px;
- cursor: pointer;
- padding: 4px;
- }
}
.autocomplete-list {
@@ -104,26 +98,14 @@
flex-grow: 1;
}
- &>span.bp6-icon>svg {
- color: transparent;
- }
-
&[aria-selected='true'] {
background-color: #fafafa;
font-weight: 600;
-
- &>span.bp6-icon>svg {
- color: #1890ff;
- }
}
&[data-focus='true'] {
background-color: #e6f7ff;
cursor: pointer;
-
- &>span.bp6-icon>svg {
- color: currentColor;
- }
}
}
}
\ No newline at end of file
diff --git a/packages/ui-default/components/datepicker/datepicker.styl b/packages/ui-default/components/datepicker/datepicker.styl
index 16e60db05..68e155e70 100644
--- a/packages/ui-default/components/datepicker/datepicker.styl
+++ b/packages/ui-default/components/datepicker/datepicker.styl
@@ -16,7 +16,6 @@
opacity .1s ease-in-out,
max-height 0s,
border-width 0s
- box-shadow: $data-picker-shadow
.picker__list
padding: 0
diff --git a/packages/ui-default/components/discussion/history.page.tsx b/packages/ui-default/components/discussion/history.page.tsx
index b71c6a917..76829e04e 100644
--- a/packages/ui-default/components/discussion/history.page.tsx
+++ b/packages/ui-default/components/discussion/history.page.tsx
@@ -1,8 +1,7 @@
/* eslint-disable react-refresh/only-export-components */
-import { Popover } from '@blueprintjs/core';
+import { MantineProvider, Popover } from '@mantine/core';
import React from 'react';
import ReactDOM from 'react-dom/client';
-import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import TimeAgo from 'timeago-react';
import { InfoDialog } from 'vj/components/dialog';
import { AutoloadPage } from 'vj/misc/Page';
@@ -22,22 +21,31 @@ async function historyDialog(payload, time, uid) {
}).open();
}
-const queryClient = new QueryClient();
-
function History({ payload }) {
const [load, setLoad] = React.useState(false);
- const { isLoading, isError, data } = useQuery(['history', payload], async () => {
- const { history } = await request.get(`${payload}?all=1`);
- return history;
- }, { enabled: !!load });
- return (
- setLoad(true)}
- content={
- {(isLoading || isError) && (
+ const [data, setData] = React.useState([]);
+ const [isLoading, setLoading] = React.useState(true);
+ const [error, setError] = React.useState('');
+
+ React.useEffect(() => {
+ if (!load) return;
+ setLoading(true);
+ request.get(`${payload}?all=1`).then(({ history }) => {
+ setData(history);
+ }).catch((e) => {
+ setError(e.message);
+ }).finally(() => {
+ setLoading(false);
+ });
+ }, [load]);
+
+ return setLoad(true)}>
+
+ {i18n('Edited')}
+
+
+
+ {(isLoading || error) && (
-
{isLoading ? i18n('Loading...') : i18n('Loading failed.')}
@@ -53,11 +61,9 @@ function History({ payload }) {
))}
-
}
- >
- {i18n('Edited')}
-
- );
+
+
+ ;
}
const page = new AutoloadPage('discussionHistoryPage', () => {
@@ -65,9 +71,9 @@ const page = new AutoloadPage('discussionHistoryPage', () => {
const url = $(e).data('raw-url');
if (!url) return;
ReactDOM.createRoot(e).render(
-
+
- ,
+ ,
);
});
});
diff --git a/packages/ui-default/components/discussion/reaction.page.tsx b/packages/ui-default/components/discussion/reaction.page.tsx
index eb30a09e7..c5a41dda2 100644
--- a/packages/ui-default/components/discussion/reaction.page.tsx
+++ b/packages/ui-default/components/discussion/reaction.page.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react-refresh/only-export-components */
import 'jquery.easing';
-import { Popover } from '@blueprintjs/core';
+import { MantineProvider, Popover } from '@mantine/core';
import $ from 'jquery';
import { chunk } from 'lodash';
import * as React from 'react';
@@ -36,21 +36,30 @@ function Reaction({ payload, ele }) {
const emojiList: string[] = (UiContext.emojiList || '👍 👎 😄 😕 ❤️ 🤔 🤣 🌿 🍋 🕊️ 👀 🤡').split(' ');
const elesPerRow = getRow(Math.sqrt(emojiList.length));
const [focus, updateFocus] = React.useState(false);
- const [finish, updateFinish] = React.useState(false);
- if (finish) setTimeout(() => updateFinish(false), 1000);
+ const [open, updateOpen] = React.useState(false);
+ const [trigger, updateTrigger] = React.useState(false);
return (
-
+
+
+ updateTrigger(true)}
+ onMouseLeave={() => {
+ setTimeout(() => updateTrigger(false), 300);
+ }}
+ />
+
+ updateOpen(true)} onMouseLeave={() => updateOpen(false)}>
{chunk(emojiList, elesPerRow).map((line, i) => (
{line.map((emoji) => (
handleEmojiClick(payload, emoji, ele).then(() => updateFinish(true))}
+ onClick={() => handleEmojiClick(payload, emoji, ele).then(() => {
+ updateOpen(false);
+ updateTrigger(false);
+ })}
>
{emoji}
@@ -62,9 +71,7 @@ function Reaction({ payload, ele }) {
updateFocus(true)} onBlur={() => updateFocus(false)}>
- }
- >
-
+
);
}
@@ -72,9 +79,9 @@ function Reaction({ payload, ele }) {
const reactionPage = new AutoloadPage('reactionPage', () => {
const canUseReaction = $('[data-op="react"]').length > 0;
$('[data-op="react"]').each((i, e) => {
- ReactDOM.createRoot(e).render(
- ,
- );
+ ReactDOM.createRoot(e).render(
+
+ );
});
$(document).on('click', '.reaction', async (e) => {
if (!canUseReaction) {
diff --git a/packages/ui-default/components/notification/index.ts b/packages/ui-default/components/notification/index.ts
index a9e878023..5f91e54cb 100644
--- a/packages/ui-default/components/notification/index.ts
+++ b/packages/ui-default/components/notification/index.ts
@@ -1,17 +1,43 @@
-import { Intent, OverlayToaster, Position } from '@blueprintjs/core';
+import { createTheme, MantineProvider, Notification as MantineNotification } from '@mantine/core';
+import { Notifications, notifications } from '@mantine/notifications';
import $ from 'jquery';
+import React from 'react';
import { tpl, zIndexManager } from 'vj/utils/base';
-const ToasterContainer = document.createElement('div');
-ToasterContainer.style.position = 'fixed';
-ToasterContainer.style.bottom = '0px';
-ToasterContainer.style.width = '100%';
-ToasterContainer.style.zIndex = '9999';
-document.body.append(ToasterContainer);
+const colorWhite = {
+ color: 'var(--mantine-color-white)',
+} as const;
-const AppToaster = OverlayToaster.create(
- { position: Position.BOTTOM_LEFT, usePortal: false },
- { container: ToasterContainer },
+const theme = createTheme({
+ components: {
+ Notification: MantineNotification.extend({
+ classNames: {
+ closeButton: 'mantine-notifications-close-button',
+ },
+ styles: {
+ root: {
+ backgroundColor: 'var(--notification-color, var(--mantine-primary-color-filled))',
+ paddingInlineStart: 'var(--mantine-spacing-xs)',
+ },
+ title: {
+ ...colorWhite,
+ fontSize: 'var(--mantine-font-size-md)',
+ },
+ icon: {
+ fontSize: '24px',
+ marginInlineEnd: 'var(--mantine-spacing-xs)',
+ },
+ description: colorWhite,
+ closeButton: colorWhite,
+ },
+ }),
+ },
+});
+
+document.body.append(tpl(
+ React.createElement(MantineProvider, { theme },
+ React.createElement(Notifications, { position: 'bottom-left', zIndex: 99999 }),
+ ), true),
);
interface NotificationOptions {
@@ -66,20 +92,44 @@ export default class Notification {
setTimeout(() => this.$n.remove(), 200);
}
- static async success(message: string, duration?: number) {
- return (await AppToaster).show({ message, timeout: duration, intent: Intent.SUCCESS });
+ static success(message: string, duration?: number) {
+ return notifications.show({
+ title: message,
+ color: '#238551',
+ message: '',
+ icon: React.createElement('i', { className: 'icon icon-check' }),
+ autoClose: duration,
+ });
}
- static async info(message: string, duration?: number) {
- return (await AppToaster).show({ message, timeout: duration, intent: Intent.PRIMARY });
+ static info(message: string, duration?: number) {
+ return notifications.show({
+ title: message,
+ color: '#2d72d2',
+ message: '',
+ icon: React.createElement('i', { className: 'icon icon-info--circle' }),
+ autoClose: duration,
+ });
}
- static async warn(message: string, duration?: number) {
- return (await AppToaster).show({ message, timeout: duration, intent: Intent.WARNING });
+ static warn(message: string, duration?: number) {
+ return notifications.show({
+ title: message,
+ color: '#fbb360',
+ message: '',
+ icon: React.createElement('i', { className: 'icon icon-warning' }),
+ autoClose: duration,
+ });
}
- static async error(message: string, duration?: number) {
- return (await AppToaster).show({ message, timeout: duration, intent: Intent.DANGER });
+ static error(message: string, duration?: number) {
+ return notifications.show({
+ title: message,
+ color: '#cd4246',
+ message: '',
+ icon: React.createElement('i', { className: 'icon icon-close--circle' }),
+ autoClose: duration,
+ });
}
}
diff --git a/packages/ui-default/components/notification/notification.page.styl b/packages/ui-default/components/notification/notification.page.styl
index c7e76e7ca..6be09a441 100644
--- a/packages/ui-default/components/notification/notification.page.styl
+++ b/packages/ui-default/components/notification/notification.page.styl
@@ -77,4 +77,8 @@
.notification {
display: none !important
}
+}
+
+.mantine-notifications-close-button:hover {
+ background-color: rgba(0, 0, 0, 0.1) !important;
}
\ No newline at end of file
diff --git a/packages/ui-default/components/problem/tag.page.styl b/packages/ui-default/components/problem/tag.page.styl
index 2f12b1a15..67e88686c 100644
--- a/packages/ui-default/components/problem/tag.page.styl
+++ b/packages/ui-default/components/problem/tag.page.styl
@@ -19,11 +19,41 @@
margin-left: rem(3px)
margin-bottom: rem(5px)
line-height: 30px !important
+ display: inline-flex
+ flex-direction: row
+ align-items: center
+ border: none
+ border-radius: 2px
+ box-shadow: none
+ font-size: 14px
+ line-height: 18px
+ max-width: 100%
+ min-height: 30px
+ min-width: 30px
+ padding: 6px 8px
+ position: relative
+ background-color: rgba(143, 153, 168, 0.15)
+ color: #1c2127
&:hover
text-decoration: none
> span
display: flex
align-items: center
+ > *
+ flex-grow: 0
+ flex-shrink: 0
+ margin-right: 8px
+ > :last-child
+ margin-right: 0
+ &:focus
+ outline: rgba(33, 93, 176, 0.752) solid 2px
+ outline-offset: 0
+ -moz-outline-radius: 6px
+ &.interactive
+ cursor: pointer
+ &:hover
+ background: rgba(143, 153, 168, 0.3)
+ color: #111418
.problem__tag-link
display: inline-block
diff --git a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
index fc92d4469..cc9e2516c 100644
--- a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
+++ b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
@@ -1,6 +1,4 @@
-import {
- Card, InputGroup, Tag,
-} from '@blueprintjs/core';
+import { Card, Text, TextInput } from '@mantine/core';
import { isEqual } from 'lodash';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -16,16 +14,18 @@ function FileIOConfig() {
const dispatch = useDispatch();
return (
-
+
- .in/.out}
+ .in/.out}
+ rightSectionWidth={100}
+ rightSectionPointerEvents="none"
value={filename || ''}
onChange={(ev) => {
dispatch({ type: 'problemconfig/updateFileIO', filename: ev.currentTarget.value });
}}
- fill
+ style={{ width: '100%' }}
/>
@@ -41,7 +41,7 @@ function ExtraFilesConfig() {
const dispatch = useDispatch();
return (
-
+
-
+
- {subtaskId !== -1 && setExpand((e) => !e)}>
-
-
{i18n('Subtask {0}', subtaskId)}
-
ev.stopPropagation()}>
-
- {i18n('Are you sure you want to delete this subtask?')}
-
-
-
- }
+
+ {subtaskId !== -1 && (
+
setExpand((e) => !e)}>
+
+ {expand ? : }
+
+ {i18n('Subtask {0}', subtaskId)}
+
}
-
- {subtaskId !== -1 && }
+
+
+
+ )}
+
+ {subtaskId !== -1 && expand && (
+
+ )}
{expand
?
- :
setExpand(false)}
- icon="layers"
- label={<> {cases.length} testcases.>}
- path={[0]}
- />}
+ :
+ {cases.length} testcases.
+
}
{!cases.length && (
-
-
-
- {subtaskId === -1
- ? i18n('No testcase here')
- : i18n('Drag and drop testcases here:')}
-
-
+
+ {subtaskId === -1
+ ? i18n('No testcase here')
+ : i18n('Drag and drop testcases here:')}
+
)}
-
-
+
+
);
}
@@ -110,7 +101,7 @@ export function SubtaskConfigTree() {
const ids = useSelector((s: RootState) => Object.values(s.config?.subtasks || []).map((i) => i.id), isEqual);
const dispatch = useDispatch();
const store = useStore();
- function autoConfigure() {
+ const autoConfigure = React.useCallback(() => {
const state = store.getState();
const subtasks = readSubtasksFromFiles(state.testdata.map((t) => t.name), state.config);
const cases = subtasks.reduce((a, b) => a.concat(b.cases), []);
@@ -122,29 +113,34 @@ export function SubtaskConfigTree() {
type: 'problemconfig/delTestcases',
cases,
});
- }
+ }, [dispatch, store]);
+ const rootNodes = React.useMemo(() => [
+ { id: 'auto', label: i18n('Auto configure'), type: 'action' as const },
+ { id: 'global', label: i18n('Global settings'), type: 'global' as const },
+ ...ids.map((id) => ({ id: `sub-${id}`, label: i18n('Subtask {0}', id), type: 'subtask' as const, subtaskId: id })),
+ { id: 'add', label: i18n('Add new subtask'), type: 'add' as const },
+ ], [ids]);
+
return (
-
+ {
+ const n: any = node;
+ if (n.type === 'action') {
+ return
+ {i18n('Auto configure')}
+
;
+ }
+ if (n.type === 'global') return ;
+ if (n.type === 'add') {
+ return dispatch({ type: 'problemconfig/addSubtask' })}>
+ {i18n('Add new subtask')}
+
;
+ }
+ return ;
+ }}
+ />
);
}
@@ -156,11 +152,9 @@ export function ProblemConfigTree() {
diff --git a/packages/ui-default/components/problemconfig/ProblemType.tsx b/packages/ui-default/components/problemconfig/ProblemType.tsx
index 240b04a76..bbf69575d 100644
--- a/packages/ui-default/components/problemconfig/ProblemType.tsx
+++ b/packages/ui-default/components/problemconfig/ProblemType.tsx
@@ -1,6 +1,4 @@
-import {
- Card, Classes, Switch, Tab, Tabs, TabsExpander,
-} from '@blueprintjs/core';
+import { Card, SegmentedControl, Switch } from '@mantine/core';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { i18n } from 'vj/utils';
@@ -26,158 +24,141 @@ export default function ProblemType() {
}, [checker]);
return (
-
-
- {i18n('Problem Type')}
-
-
+
+
{i18n('Problem Type')}
+
+
+
+ {Type === 'default' && (
+ <>
+
+
{i18n('CheckerType')}
+
{
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value });
if (value === 'testlib' && !category) setCategory('custom');
}}
- renderActiveTabPanelOnly
- >
- {i18n('CheckerType')}
-
-
-
- {
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
- }}
- />
-
-
- )}
- />
-
-
-
-
- {category === 'preset'
- ?
-
-
- : }
-
- )}
- />
-
-
-
-
-
-
- )}
- />
-
+ data={[
+ { label: i18n('default'), value: 'default' },
+ { label: 'testlib', value: 'testlib' },
+ { label: 'other', value: 'other' },
+ ]}
+ />
+
+
+ {((['strict', 'default'].includes(checkerType) || !checkerType)) && (
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
+ }}
+ />
)}
- />
-
-
-
- )}
- />
-
-
-
- dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'num_processes', value: +ev.currentTarget.value }))}
- className="textbox"
- />
-
-
- )}
- />
-
-
- {
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'subType', value: subType === 'multi' ? 'single' : 'multi' });
+
+
-
- dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value }))}
- className="textbox"
- />
+ className="select"
+ >
+
+
+
+ {category === 'preset'
+ ?
+
+
+ : }
)}
- />
- {i18n('Unsupported configure this type of problem. Please refer to the documentation.')}
)}
- />
-
+
+ {(!['strict', 'default'].includes(checkerType) && checkerType && checkerType !== 'testlib') && (
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+ {Type === 'interactive' && (
+
+
+
+ )}
+
+ {Type === 'communication' && (
+
+
+
+ dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'num_processes', value: +ev.currentTarget.value }))}
+ className="textbox"
+ />
+
+
+ )}
+
+ {Type === 'submit_answer' && (
+
+
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'subType', value: subType === 'multi' ? 'single' : 'multi' });
+ }}
+ />
+
+
+ dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value }))}
+ className="textbox"
+ />
+
+
+ )}
+
+ {Type === 'objective' && (
+ {i18n('Unsupported configure this type of problem. Please refer to the documentation.')}
+ )}
);
diff --git a/packages/ui-default/components/problemconfig/index.tsx b/packages/ui-default/components/problemconfig/index.tsx
index f95338e9c..ec5e29d37 100644
--- a/packages/ui-default/components/problemconfig/index.tsx
+++ b/packages/ui-default/components/problemconfig/index.tsx
@@ -1,8 +1,9 @@
-import {
- Button, Dialog, DialogBody, DialogFooter, Tab, Tabs,
-} from '@blueprintjs/core';
+import { Button, Tabs } from '@mantine/core';
+import { Allotment } from 'allotment';
+import { ContextMenuProvider } from 'mantine-contextmenu';
import React from 'react';
-import { useSelector } from 'react-redux';
+import { useSelector, useStore } from 'react-redux';
+import { confirm } from 'vj/components/dialog/index';
import { i18n } from 'vj/utils';
import ProblemConfigEditor from './ProblemConfigEditor';
import ProblemConfigForm from './ProblemConfigForm';
@@ -15,40 +16,74 @@ interface Props {
export default function ProblemConfig(props: Props) {
const [selected, setSelected] = React.useState('basic');
- const [open, setOpen] = React.useState(false);
+ const [container, setContainer] = React.useState(null);
+ const [tabs, setTabs] = React.useState(null);
+ const [editor, setEditor] = React.useState(null);
+ const store = useStore();
const valid = useSelector((state: RootState) => state.config.__valid);
const errors = useSelector((state: RootState) => state.config.__errors, (a, b) => JSON.stringify(a) === JSON.stringify(b));
- React.useEffect(() => {
- if (valid) setOpen(false);
- }, [valid]);
- return (<>
-
-
-
-
(t !== 'errors' && setSelected(t.toString()))} selectedTabId={valid ? selected : 'errors'}>
- } />
- } />
- {errors.map((i) => ({i}))}}
- />
-
-
+ const handleSave = React.useCallback(async () => {
+ if (valid) props.onSave();
+ else if (await confirm(i18n('Errors detected in the config. Confirm save?'))) props.onSave();
+ }, [valid, props.onSave]);
+
+ React.useEffect(() => {
+ if (!container) return () => { };
+ container.style.height ||= '600px';
+ const callback = () => {
+ setTimeout(() => {
+ const target = Math.max(container.clientHeight, tabs?.clientHeight || 0);
+ container.style.height = `${target}px`;
+ if (editor) editor.layout();
+ }, 50);
+ };
+ const dispose = store.subscribe(callback);
+ window.addEventListener('resize', callback);
+ return () => {
+ dispose();
+ window.removeEventListener('resize', callback);
+ };
+ }, [container, tabs, store]);
+
+ return
+
+
+
+
+
+
+ (t !== 'errors' && t && setSelected(t.toString()))}
+ >
+
+
+ {i18n('Basic')}
+ {i18n('Subtasks')}
+ {errors.length ? `Errors(${errors.length})` : 'No Errors'}
+
+
+
+
+
+
+
+
+
+
+ {errors.map((i) => (
{i}))}
+
+
+
+
-
-
- >);
+ ;
}
diff --git a/packages/ui-default/components/problemconfig/reducer/config.ts b/packages/ui-default/components/problemconfig/reducer/config.ts
index b054ea561..b3efc9c73 100644
--- a/packages/ui-default/components/problemconfig/reducer/config.ts
+++ b/packages/ui-default/components/problemconfig/reducer/config.ts
@@ -138,17 +138,21 @@ export default function reducer(state = {
case 'problemconfig/moveTestcases': {
const testcases = action.payload.cases;
const subtasks = cloneDeep(state.subtasks);
+ const targetCases = action.payload.target === -1
+ ? state.__cases
+ : (subtasks.find((s) => s.id === action.payload.target)?.cases || []);
+ const toAdd = testcases.filter((tc) => !targetCases.find((j) => j.input === tc.input && j.output === tc.output));
const __cases = action.payload.source === -1
? state.__cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output))
: action.payload.target === -1
- ? sortFiles([...state.__cases, ...testcases], 'input')
+ ? sortFiles([...state.__cases, ...toAdd], 'input')
: state.__cases;
for (const key in subtasks) {
const subtask = subtasks[key];
if (subtask.id === action.payload.source) {
subtask.cases = subtask.cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output));
} else if (subtask.id === action.payload.target) {
- subtask.cases = sortFiles([...subtask.cases, ...testcases], 'input');
+ subtask.cases = sortFiles([...subtask.cases, ...toAdd], 'input');
}
}
return { ...state, subtasks, __cases };
@@ -158,8 +162,10 @@ export default function reducer(state = {
return { ...state, __cases: state.__cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output)) };
}
case 'problemconfig/deleteSubtask': {
+ const currentCases = state.subtasks.find((i) => i.id === action.id)?.cases || [];
const subtasks = state.subtasks.filter((i) => i.id !== action.id);
- return { ...state, subtasks };
+ const add = currentCases.filter((i) => !subtasks.find((j) => j.cases.find((k) => k.input === i.input && k.output === i.output)));
+ return { ...state, subtasks, __cases: sortFiles([...state.__cases, ...add], 'input') };
}
default:
return state;
diff --git a/packages/ui-default/components/problemconfig/tree/AddTestcase.tsx b/packages/ui-default/components/problemconfig/tree/AddTestcase.tsx
index 4dfb5aa74..7a2b6a53f 100644
--- a/packages/ui-default/components/problemconfig/tree/AddTestcase.tsx
+++ b/packages/ui-default/components/problemconfig/tree/AddTestcase.tsx
@@ -1,7 +1,5 @@
import { readSubtasksFromFiles } from '@hydrooj/common';
-import {
- Button, Classes, Dialog, DialogBody, DialogFooter, Icon,
-} from '@blueprintjs/core';
+import { Button, Modal, Text } from '@mantine/core';
import React, { useEffect, useRef } from 'react';
import { useSelector, useStore } from 'react-redux';
import { i18n } from 'vj/utils';
@@ -83,14 +81,14 @@ export function AddTestcase() {
}
return (<>
-
-
-
- {i18n('Auto detect')}
-
-
-
+ {i18n('Auto detect')}
+
+ {
setInput('');
setOutput('');
@@ -99,47 +97,41 @@ export function AddTestcase() {
setOpen(true);
}}
>
-
-
- {i18n('Add testcase')}
-
-
-