mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: drop blueprintjs 6 (#1073)
This commit is contained in:
1 parent
40114323e4
commit
3996f6f2ef
27 files changed
+626
-514
No files matched your search
@@ -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) {
|
export function convertIniConfig(ini: string) {
|
||||||
const f = ini.split('\n');
|
const f = ini.split('\n');
|
||||||
@@ -118,13 +118,16 @@ export function readSubtasksFromFiles(files: string[], config) {
|
|||||||
id: sid,
|
id: sid,
|
||||||
};
|
};
|
||||||
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
|
} 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (match) break;
|
if (match) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const id in subtask) subtask[id].cases = sortFiles(subtask[id].cases, 'input');
|
||||||
return Object.values(subtask);
|
return Object.values(subtask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,12 +75,6 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&>span.bp6-icon>svg {
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-list {
|
.autocomplete-list {
|
||||||
@@ -104,26 +98,14 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&>span.bp6-icon>svg {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-selected='true'] {
|
&[aria-selected='true'] {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
&>span.bp6-icon>svg {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-focus='true'] {
|
&[data-focus='true'] {
|
||||||
background-color: #e6f7ff;
|
background-color: #e6f7ff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&>span.bp6-icon>svg {
|
|
||||||
color: currentColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
opacity .1s ease-in-out,
|
opacity .1s ease-in-out,
|
||||||
max-height 0s,
|
max-height 0s,
|
||||||
border-width 0s
|
border-width 0s
|
||||||
box-shadow: $data-picker-shadow
|
|
||||||
|
|
||||||
.picker__list
|
.picker__list
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
import { Popover } from '@blueprintjs/core';
|
import { MantineProvider, Popover } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
|
||||||
import TimeAgo from 'timeago-react';
|
import TimeAgo from 'timeago-react';
|
||||||
import { InfoDialog } from 'vj/components/dialog';
|
import { InfoDialog } from 'vj/components/dialog';
|
||||||
import { AutoloadPage } from 'vj/misc/Page';
|
import { AutoloadPage } from 'vj/misc/Page';
|
||||||
@@ -22,22 +21,31 @@ async function historyDialog(payload, time, uid) {
|
|||||||
}).open();
|
}).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
function History({ payload }) {
|
function History({ payload }) {
|
||||||
const [load, setLoad] = React.useState(false);
|
const [load, setLoad] = React.useState(false);
|
||||||
const { isLoading, isError, data } = useQuery(['history', payload], async () => {
|
const [data, setData] = React.useState([]);
|
||||||
const { history } = await request.get(`${payload}?all=1`);
|
const [isLoading, setLoading] = React.useState(true);
|
||||||
return history;
|
const [error, setError] = React.useState('');
|
||||||
}, { enabled: !!load });
|
|
||||||
return (
|
React.useEffect(() => {
|
||||||
<Popover
|
if (!load) return;
|
||||||
usePortal
|
setLoading(true);
|
||||||
interactionKind="hover"
|
request.get(`${payload}?all=1`).then(({ history }) => {
|
||||||
position="bottom"
|
setData(history);
|
||||||
onOpening={() => setLoad(true)}
|
}).catch((e) => {
|
||||||
content={<ol className="menu">
|
setError(e.message);
|
||||||
{(isLoading || isError) && (
|
}).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
return <Popover onOpen={() => setLoad(true)}>
|
||||||
|
<Popover.Target>
|
||||||
|
<span>{i18n('Edited')} <span className="icon icon-expand_more"></span></span>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<ol className="menu">
|
||||||
|
{(isLoading || error) && (
|
||||||
<li className="menu__item">
|
<li className="menu__item">
|
||||||
<a className="menu__link">
|
<a className="menu__link">
|
||||||
{isLoading ? i18n('Loading...') : i18n('Loading failed.')}
|
{isLoading ? i18n('Loading...') : i18n('Loading failed.')}
|
||||||
@@ -53,11 +61,9 @@ function History({ payload }) {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>}
|
</ol>
|
||||||
>
|
</Popover.Dropdown>
|
||||||
<span>{i18n('Edited')} <span className="icon icon-expand_more"></span></span>
|
</Popover>;
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = new AutoloadPage('discussionHistoryPage', () => {
|
const page = new AutoloadPage('discussionHistoryPage', () => {
|
||||||
@@ -65,9 +71,9 @@ const page = new AutoloadPage('discussionHistoryPage', () => {
|
|||||||
const url = $(e).data('raw-url');
|
const url = $(e).data('raw-url');
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
ReactDOM.createRoot(e).render(
|
ReactDOM.createRoot(e).render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<MantineProvider>
|
||||||
<History payload={url} />
|
<History payload={url} />
|
||||||
</QueryClientProvider>,
|
</MantineProvider>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
/* eslint-disable react-refresh/only-export-components */
|
||||||
import 'jquery.easing';
|
import 'jquery.easing';
|
||||||
|
|
||||||
import { Popover } from '@blueprintjs/core';
|
import { MantineProvider, Popover } from '@mantine/core';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { chunk } from 'lodash';
|
import { chunk } from 'lodash';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
@@ -36,21 +36,30 @@ function Reaction({ payload, ele }) {
|
|||||||
const emojiList: string[] = (UiContext.emojiList || '👍 👎 😄 😕 ❤️ 🤔 🤣 🌿 🍋 🕊️ 👀 🤡').split(' ');
|
const emojiList: string[] = (UiContext.emojiList || '👍 👎 😄 😕 ❤️ 🤔 🤣 🌿 🍋 🕊️ 👀 🤡').split(' ');
|
||||||
const elesPerRow = getRow(Math.sqrt(emojiList.length));
|
const elesPerRow = getRow(Math.sqrt(emojiList.length));
|
||||||
const [focus, updateFocus] = React.useState(false);
|
const [focus, updateFocus] = React.useState(false);
|
||||||
const [finish, updateFinish] = React.useState(false);
|
const [open, updateOpen] = React.useState(false);
|
||||||
if (finish) setTimeout(() => updateFinish(false), 1000);
|
const [trigger, updateTrigger] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover opened={trigger || open || focus}>
|
||||||
usePortal
|
<Popover.Target>
|
||||||
interactionKind="hover"
|
<span
|
||||||
isOpen={finish ? false : (focus ? true : undefined)}
|
className="icon icon-emoji"
|
||||||
content={<div>
|
onMouseEnter={() => updateTrigger(true)}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setTimeout(() => updateTrigger(false), 300);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown onMouseEnter={() => updateOpen(true)} onMouseLeave={() => updateOpen(false)}>
|
||||||
{chunk(emojiList, elesPerRow).map((line, i) => (
|
{chunk(emojiList, elesPerRow).map((line, i) => (
|
||||||
<div className="row" key={+i} style={{ paddingBottom: 4, paddingTop: 4 }}>
|
<div className="row" key={+i} style={{ paddingBottom: 4, paddingTop: 4 }}>
|
||||||
{line.map((emoji) => (
|
{line.map((emoji) => (
|
||||||
<div
|
<div
|
||||||
key={emoji}
|
key={emoji}
|
||||||
className={`medium-${12 / elesPerRow} small-${12 / elesPerRow} columns popover-reaction-item`}
|
className={`medium-${12 / elesPerRow} small-${12 / elesPerRow} columns popover-reaction-item`}
|
||||||
onClick={() => handleEmojiClick(payload, emoji, ele).then(() => updateFinish(true))}
|
onClick={() => handleEmojiClick(payload, emoji, ele).then(() => {
|
||||||
|
updateOpen(false);
|
||||||
|
updateTrigger(false);
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{emoji}
|
{emoji}
|
||||||
</div>
|
</div>
|
||||||
@@ -62,9 +71,7 @@ function Reaction({ payload, ele }) {
|
|||||||
<input name="emojiSuggest" onFocus={() => updateFocus(true)} onBlur={() => updateFocus(false)}></input>
|
<input name="emojiSuggest" onFocus={() => updateFocus(true)} onBlur={() => updateFocus(false)}></input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</Popover.Dropdown>
|
||||||
>
|
|
||||||
<span className="icon icon-emoji"></span>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,9 +79,9 @@ function Reaction({ payload, ele }) {
|
|||||||
const reactionPage = new AutoloadPage('reactionPage', () => {
|
const reactionPage = new AutoloadPage('reactionPage', () => {
|
||||||
const canUseReaction = $('[data-op="react"]').length > 0;
|
const canUseReaction = $('[data-op="react"]').length > 0;
|
||||||
$('[data-op="react"]').each((i, e) => {
|
$('[data-op="react"]').each((i, e) => {
|
||||||
ReactDOM.createRoot(e).render(
|
ReactDOM.createRoot(e).render(<MantineProvider>
|
||||||
<Reaction payload={$(e).data('form')} ele={$(`.reactions[data-${$(e).data('form').nodeType}='${$(e).data('form').id}']`)} />,
|
<Reaction payload={$(e).data('form')} ele={$(`.reactions[data-${$(e).data('form').nodeType}='${$(e).data('form').id}']`)} />
|
||||||
);
|
</MantineProvider>);
|
||||||
});
|
});
|
||||||
$(document).on('click', '.reaction', async (e) => {
|
$(document).on('click', '.reaction', async (e) => {
|
||||||
if (!canUseReaction) {
|
if (!canUseReaction) {
|
||||||
|
|||||||
@@ -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 $ from 'jquery';
|
||||||
|
import React from 'react';
|
||||||
import { tpl, zIndexManager } from 'vj/utils/base';
|
import { tpl, zIndexManager } from 'vj/utils/base';
|
||||||
|
|
||||||
const ToasterContainer = document.createElement('div');
|
const colorWhite = {
|
||||||
ToasterContainer.style.position = 'fixed';
|
color: 'var(--mantine-color-white)',
|
||||||
ToasterContainer.style.bottom = '0px';
|
} as const;
|
||||||
ToasterContainer.style.width = '100%';
|
|
||||||
ToasterContainer.style.zIndex = '9999';
|
|
||||||
document.body.append(ToasterContainer);
|
|
||||||
|
|
||||||
const AppToaster = OverlayToaster.create(
|
const theme = createTheme({
|
||||||
{ position: Position.BOTTOM_LEFT, usePortal: false },
|
components: {
|
||||||
{ container: ToasterContainer },
|
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 {
|
interface NotificationOptions {
|
||||||
@@ -66,20 +92,44 @@ export default class Notification {
|
|||||||
setTimeout(() => this.$n.remove(), 200);
|
setTimeout(() => this.$n.remove(), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async success(message: string, duration?: number) {
|
static success(message: string, duration?: number) {
|
||||||
return (await AppToaster).show({ message, timeout: duration, intent: Intent.SUCCESS });
|
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) {
|
static info(message: string, duration?: number) {
|
||||||
return (await AppToaster).show({ message, timeout: duration, intent: Intent.PRIMARY });
|
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) {
|
static warn(message: string, duration?: number) {
|
||||||
return (await AppToaster).show({ message, timeout: duration, intent: Intent.WARNING });
|
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) {
|
static error(message: string, duration?: number) {
|
||||||
return (await AppToaster).show({ message, timeout: duration, intent: Intent.DANGER });
|
return notifications.show({
|
||||||
|
title: message,
|
||||||
|
color: '#cd4246',
|
||||||
|
message: '',
|
||||||
|
icon: React.createElement('i', { className: 'icon icon-close--circle' }),
|
||||||
|
autoClose: duration,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,3 +78,7 @@
|
|||||||
display: none !important
|
display: none !important
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mantine-notifications-close-button:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
@@ -19,11 +19,41 @@
|
|||||||
margin-left: rem(3px)
|
margin-left: rem(3px)
|
||||||
margin-bottom: rem(5px)
|
margin-bottom: rem(5px)
|
||||||
line-height: 30px !important
|
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
|
&:hover
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
> span
|
> span
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
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
|
.problem__tag-link
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { Card, Text, TextInput } from '@mantine/core';
|
||||||
Card, InputGroup, Tag,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -16,16 +14,18 @@ function FileIOConfig() {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="FileIOConfig" disableLabel>
|
<FormItem columns={12} label="FileIOConfig" disableLabel>
|
||||||
<Card style={{ padding: 10 }}>
|
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="FileIO">
|
<FormItem columns={6} label="FileIO">
|
||||||
<InputGroup
|
<TextInput
|
||||||
rightElement={<Tag minimal>.in/.out</Tag>}
|
rightSection={<Text size="sm">.in/.out</Text>}
|
||||||
|
rightSectionWidth={100}
|
||||||
|
rightSectionPointerEvents="none"
|
||||||
value={filename || ''}
|
value={filename || ''}
|
||||||
onChange={(ev) => {
|
onChange={(ev) => {
|
||||||
dispatch({ type: 'problemconfig/updateFileIO', filename: ev.currentTarget.value });
|
dispatch({ type: 'problemconfig/updateFileIO', filename: ev.currentTarget.value });
|
||||||
}}
|
}}
|
||||||
fill
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,7 +41,7 @@ function ExtraFilesConfig() {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="ExtraFilesConfig" disableLabel>
|
<FormItem columns={12} label="ExtraFilesConfig" disableLabel>
|
||||||
<Card style={{ padding: 10 }}>
|
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={12} label={i18n('user_extra_files')}>
|
<FormItem columns={12} label={i18n('user_extra_files')}>
|
||||||
<FileSelectAutoComplete
|
<FileSelectAutoComplete
|
||||||
@@ -70,7 +70,7 @@ function LangConfig() {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="langs" disableLabel>
|
<FormItem columns={12} label="langs" disableLabel>
|
||||||
<Card style={{ padding: 10 }}>
|
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={12} label="langs">
|
<FormItem columns={12} label="langs">
|
||||||
<LanguageSelectAutoComplete
|
<LanguageSelectAutoComplete
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/common';
|
import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/common';
|
||||||
import { TestCaseConfig } from 'hydrooj';
|
import { TestCaseConfig } from 'hydrooj';
|
||||||
import {
|
import { Button, Text, Tree } from '@mantine/core';
|
||||||
Button, Classes, Icon, Popover, TreeNode,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DndProvider, useDrop } from 'react-dnd';
|
import { DndProvider, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { useDispatch, useSelector, useStore } from 'react-redux';
|
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||||
|
import { confirm } from 'vj/components/dialog/index';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import { RootState } from './reducer';
|
import { RootState } from './reducer';
|
||||||
import { AddTestcase } from './tree/AddTestcase';
|
import { AddTestcase } from './tree/AddTestcase';
|
||||||
@@ -47,14 +46,6 @@ export function SubtaskNode(props: { subtaskId: number }) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function deleteSubtask() {
|
function deleteSubtask() {
|
||||||
dispatch({
|
|
||||||
type: 'problemconfig/moveTestcases',
|
|
||||||
payload: {
|
|
||||||
target: -1,
|
|
||||||
source: subtaskId,
|
|
||||||
cases,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'problemconfig/deleteSubtask',
|
type: 'problemconfig/deleteSubtask',
|
||||||
id: subtaskId,
|
id: subtaskId,
|
||||||
@@ -62,47 +53,47 @@ export function SubtaskNode(props: { subtaskId: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={`${Classes.TREE_NODE} ${Classes.TREE_NODE_EXPANDED}`}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
{subtaskId !== -1 && <div className={Classes.TREE_NODE_CONTENT} onClick={() => setExpand((e) => !e)}>
|
{subtaskId !== -1 && (
|
||||||
<Icon icon={expand ? 'folder-open' : 'folder-close'} />
|
<div style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={() => setExpand((e) => !e)}>
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Subtask {0}', subtaskId)}</span>
|
<Text fw={600} style={{ marginRight: 8 }}>
|
||||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL} onClick={(ev) => ev.stopPropagation()}>
|
{expand ? <i className="icon icon-expand_more" /> : <i className="icon icon-expand_less" />}
|
||||||
<Popover
|
</Text>
|
||||||
content={<div style={{ padding: 20 }}>
|
<Text>{i18n('Subtask {0}', subtaskId)}</Text>
|
||||||
<b>{i18n('Are you sure you want to delete this subtask?')}</b>
|
<Button
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 15 }}>
|
variant="subtle"
|
||||||
<Button intent="danger" onClick={deleteSubtask}>{i18n('Delete')}</Button>
|
size="sm"
|
||||||
</div>
|
color="black"
|
||||||
</div>}
|
style={{ marginLeft: 'auto' }}
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
confirm(i18n('Are you sure you want to delete this subtask?')).then((yes) => {
|
||||||
|
if (yes) deleteSubtask();
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="trash" />
|
<i className="icon icon-delete" />
|
||||||
</Popover>
|
</Button>
|
||||||
</span>
|
</div>
|
||||||
</div>}
|
)}
|
||||||
<ul className={Classes.TREE_NODE_LIST} ref={drop}>
|
<div ref={drop}>
|
||||||
{subtaskId !== -1 && <SubtaskSettings subtaskId={subtaskId} subtaskIds={subtaskIds} time={time} memory={memory} />}
|
{subtaskId !== -1 && expand && (
|
||||||
|
<SubtaskSettings subtaskId={subtaskId} subtaskIds={subtaskIds} time={time} memory={memory} />
|
||||||
|
)}
|
||||||
{expand
|
{expand
|
||||||
? <SelectionManager subtaskId={subtaskId} subtaskIds={subtaskIds} />
|
? <SelectionManager subtaskId={subtaskId} subtaskIds={subtaskIds} />
|
||||||
: <TreeNode
|
: <div style={{ paddingLeft: 22 }}>
|
||||||
depth={0}
|
<Text>{cases.length} testcases.</Text>
|
||||||
id={`s${subtaskId}`}
|
</div>}
|
||||||
onClick={() => setExpand(false)}
|
|
||||||
icon="layers"
|
|
||||||
label={<> {cases.length} testcases.</>}
|
|
||||||
path={[0]}
|
|
||||||
/>}
|
|
||||||
{!cases.length && (
|
{!cases.length && (
|
||||||
<li className={Classes.TREE_NODE}>
|
<div style={{ paddingLeft: 22 }}>
|
||||||
<div className={Classes.TREE_NODE_CONTENT}>
|
<Text c="dimmed">{subtaskId === -1
|
||||||
<span className={`${Classes.TREE_NODE_CARET_NONE} ${Classes.ICON_STANDARD}`}></span>
|
|
||||||
<span className={`${Classes.TREE_NODE_LABEL} text-gray`}>{subtaskId === -1
|
|
||||||
? i18n('No testcase here')
|
? i18n('No testcase here')
|
||||||
: i18n('Drag and drop testcases here:')}</span>
|
: i18n('Drag and drop testcases here:')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
)}
|
)}
|
||||||
</ul>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +101,7 @@ export function SubtaskConfigTree() {
|
|||||||
const ids = useSelector((s: RootState) => Object.values(s.config?.subtasks || []).map((i) => i.id), isEqual);
|
const ids = useSelector((s: RootState) => Object.values(s.config?.subtasks || []).map((i) => i.id), isEqual);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const store = useStore<RootState>();
|
const store = useStore<RootState>();
|
||||||
function autoConfigure() {
|
const autoConfigure = React.useCallback(() => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const subtasks = readSubtasksFromFiles(state.testdata.map((t) => t.name), state.config);
|
const subtasks = readSubtasksFromFiles(state.testdata.map((t) => t.name), state.config);
|
||||||
const cases = subtasks.reduce((a, b) => a.concat(b.cases), []);
|
const cases = subtasks.reduce((a, b) => a.concat(b.cases), []);
|
||||||
@@ -122,29 +113,34 @@ export function SubtaskConfigTree() {
|
|||||||
type: 'problemconfig/delTestcases',
|
type: 'problemconfig/delTestcases',
|
||||||
cases,
|
cases,
|
||||||
});
|
});
|
||||||
}
|
}, [dispatch, store]);
|
||||||
|
const rootNodes = React.useMemo<any[]>(() => [
|
||||||
|
{ 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 (
|
return (
|
||||||
<div className={Classes.TREE}>
|
<Tree
|
||||||
<ul className={`${Classes.TREE_NODE_LIST} ${Classes.TREE_ROOT}`}>
|
data={rootNodes}
|
||||||
<li className={Classes.TREE_NODE} onClick={autoConfigure}>
|
levelOffset={12}
|
||||||
<div className={`${Classes.TREE_NODE_CONTENT} ${Classes.TREE_NODE_CONTENT}-0`}>
|
renderNode={({ node }) => {
|
||||||
<Icon icon="clean" />
|
const n: any = node;
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Auto configure')}</span>
|
if (n.type === 'action') {
|
||||||
</div>
|
return <div style={{ padding: '6px 0' }} onClick={autoConfigure}>
|
||||||
</li>
|
<Text><i className="icon icon-settings" /> {i18n('Auto configure')}</Text>
|
||||||
<GlobalSettings />
|
</div>;
|
||||||
{ids.map((id) => <SubtaskNode key={id} subtaskId={id} />)}
|
}
|
||||||
<li
|
if (n.type === 'global') return <GlobalSettings />;
|
||||||
className={Classes.TREE_NODE}
|
if (n.type === 'add') {
|
||||||
onClick={() => dispatch({ type: 'problemconfig/addSubtask' })}
|
return <div style={{ padding: '6px 0' }} onClick={() => dispatch({ type: 'problemconfig/addSubtask' })}>
|
||||||
>
|
<Text><i className="icon icon-add" /> {i18n('Add new subtask')}</Text>
|
||||||
<div className={`${Classes.TREE_NODE_CONTENT} ${Classes.TREE_NODE_CONTENT}-0`}>
|
</div>;
|
||||||
<Icon icon="folder-new" />
|
}
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Add new subtask')}</span>
|
return <SubtaskNode subtaskId={n.subtaskId} />;
|
||||||
</div>
|
}}
|
||||||
</li>
|
/>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,11 +152,9 @@ export function ProblemConfigTree() {
|
|||||||
<SubtaskConfigTree />
|
<SubtaskConfigTree />
|
||||||
</div>
|
</div>
|
||||||
<div className="medium-6 columns">
|
<div className="medium-6 columns">
|
||||||
<div className={Classes.TREE}>
|
<div>
|
||||||
<ul className={`${Classes.TREE_ROOT} ${Classes.TREE_NODE_LIST}`}>
|
|
||||||
<AddTestcase />
|
<AddTestcase />
|
||||||
<SubtaskNode subtaskId={-1} />
|
<SubtaskNode subtaskId={-1} />
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { Card, SegmentedControl, Switch } from '@mantine/core';
|
||||||
Card, Classes, Switch, Tab, Tabs, TabsExpander,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
@@ -26,55 +24,53 @@ export default function ProblemType() {
|
|||||||
}, [checker]);
|
}, [checker]);
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="" disableLabel>
|
<FormItem columns={12} label="" disableLabel>
|
||||||
<Card style={{ padding: 10 }}>
|
<Card withBorder style={{ padding: 10, overflow: 'visible' }}>
|
||||||
<Tabs
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||||
id="ProblemTypeTabs"
|
<div>{i18n('Problem Type')}</div>
|
||||||
selectedTabId={Type}
|
<SegmentedControl
|
||||||
|
value={Type}
|
||||||
onChange={dispatcher({ type: 'CONFIG_FORM_UPDATE', key: 'type' })}
|
onChange={dispatcher({ type: 'CONFIG_FORM_UPDATE', key: 'type' })}
|
||||||
defaultSelectedTabId="default"
|
data={[
|
||||||
renderActiveTabPanelOnly
|
{ label: i18n('problem_type.default'), value: 'default' },
|
||||||
>
|
{ label: i18n('problem_type.interactive'), value: 'interactive' },
|
||||||
<span className={Classes.TAB}>{i18n('Problem Type')}</span>
|
{ label: i18n('problem_type.communication'), value: 'communication' },
|
||||||
<TabsExpander />
|
{ label: i18n('problem_type.submit_answer'), value: 'submit_answer' },
|
||||||
<Tab
|
{ label: i18n('problem_type.objective'), value: 'objective' },
|
||||||
id="default"
|
]}
|
||||||
title={i18n('problem_type.default')}
|
/>
|
||||||
panel={(
|
</div>
|
||||||
<Tabs
|
|
||||||
id="CheckerTypeTabs"
|
{Type === 'default' && (
|
||||||
selectedTabId={
|
<>
|
||||||
['strict', 'default'].includes(checkerType) || !checkerType
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 3 }}>
|
||||||
? 'default' : (checkerType !== 'testlib' ? 'other' : 'testlib')
|
<div>{i18n('CheckerType')}</div>
|
||||||
}
|
<SegmentedControl
|
||||||
|
value={(['strict', 'default'].includes(checkerType) || !checkerType)
|
||||||
|
? 'default' : (checkerType !== 'testlib' ? 'other' : 'testlib')}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value });
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value });
|
||||||
if (value === 'testlib' && !category) setCategory('custom');
|
if (value === 'testlib' && !category) setCategory('custom');
|
||||||
}}
|
}}
|
||||||
renderActiveTabPanelOnly
|
data={[
|
||||||
>
|
{ label: i18n('default'), value: 'default' },
|
||||||
<span className={Classes.TAB}>{i18n('CheckerType')}</span>
|
{ label: 'testlib', value: 'testlib' },
|
||||||
<TabsExpander />
|
{ label: 'other', value: 'other' },
|
||||||
<Tab
|
]}
|
||||||
id="default"
|
/>
|
||||||
title={i18n('default')}
|
</div>
|
||||||
panel={(
|
|
||||||
<div className="row">
|
{((['strict', 'default'].includes(checkerType) || !checkerType)) && (
|
||||||
<FormItem columns={12} label="Config">
|
|
||||||
<Switch
|
<Switch
|
||||||
|
styles={{ body: { display: 'flex' } }}
|
||||||
checked={checkerType !== 'strict'}
|
checked={checkerType !== 'strict'}
|
||||||
label={i18n('Ignore trailing space and enter.')}
|
label={i18n('Ignore trailing space and enter.')}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Tab
|
{(!['strict', 'default'].includes(checkerType) && checkerType && checkerType === 'testlib') && (
|
||||||
id="testlib"
|
|
||||||
title="testlib"
|
|
||||||
panel={(
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={4} label="Type">
|
<FormItem columns={4} label="Type">
|
||||||
<select
|
<select
|
||||||
@@ -104,11 +100,8 @@ export default function ProblemType() {
|
|||||||
: <SingleFileSelect formKey="checker" label="Checker" withLang />}
|
: <SingleFileSelect formKey="checker" label="Checker" withLang />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Tab
|
{(!['strict', 'default'].includes(checkerType) && checkerType && checkerType !== 'testlib') && (
|
||||||
id="other"
|
|
||||||
title="other"
|
|
||||||
panel={(
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={4} label="Interface">
|
<FormItem columns={4} label="Interface">
|
||||||
<ManagedSelect options={['syzoj', 'hustoj', 'qduoj', 'lemon', 'kattis']} formKey="checker_type" />
|
<ManagedSelect options={['syzoj', 'hustoj', 'qduoj', 'lemon', 'kattis']} formKey="checker_type" />
|
||||||
@@ -116,23 +109,16 @@ export default function ProblemType() {
|
|||||||
<SingleFileSelect formKey="checker" label="Checker" withLang />
|
<SingleFileSelect formKey="checker" label="Checker" withLang />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
</>
|
||||||
</Tabs>
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Tab
|
{Type === 'interactive' && (
|
||||||
id="interactive"
|
|
||||||
title={i18n('problem_type.interactive')}
|
|
||||||
panel={(
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<SingleFileSelect formKey="interactor" label="Interactor" withLang />
|
<SingleFileSelect formKey="interactor" label="Interactor" withLang />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Tab
|
{Type === 'communication' && (
|
||||||
id="communication"
|
|
||||||
title={i18n('problem_type.communication')}
|
|
||||||
panel={(
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<SingleFileSelect formKey="manager" label="Manager" withLang />
|
<SingleFileSelect formKey="manager" label="Manager" withLang />
|
||||||
<FormItem columns={4} label="Number of Processes">
|
<FormItem columns={4} label="Number of Processes">
|
||||||
@@ -145,15 +131,13 @@ export default function ProblemType() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Tab
|
{Type === 'submit_answer' && (
|
||||||
id="submit_answer"
|
|
||||||
title={i18n('problem_type.submit_answer')}
|
|
||||||
panel={(
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={6} label="Config" disableLabel>
|
<FormItem columns={6} label="Config" disableLabel>
|
||||||
<Switch
|
<Switch
|
||||||
checked={subType === 'multi'}
|
checked={subType === 'multi'}
|
||||||
|
styles={{ body: { display: 'flex' } }}
|
||||||
label={i18n('Multi-file')}
|
label={i18n('Multi-file')}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'subType', value: subType === 'multi' ? 'single' : 'multi' });
|
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'subType', value: subType === 'multi' ? 'single' : 'multi' });
|
||||||
@@ -171,13 +155,10 @@ export default function ProblemType() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
<Tab
|
{Type === 'objective' && (
|
||||||
id="objective"
|
<p>{i18n('Unsupported configure this type of problem. Please refer to the documentation.')}</p>
|
||||||
title={i18n('problem_type.objective')}
|
)}
|
||||||
panel={(<p>{i18n('Unsupported configure this type of problem. Please refer to the documentation.')}</p>)}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
</Card>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {
|
import { Button, Tabs } from '@mantine/core';
|
||||||
Button, Dialog, DialogBody, DialogFooter, Tab, Tabs,
|
import { Allotment } from 'allotment';
|
||||||
} from '@blueprintjs/core';
|
import { ContextMenuProvider } from 'mantine-contextmenu';
|
||||||
import React from 'react';
|
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 { i18n } from 'vj/utils';
|
||||||
import ProblemConfigEditor from './ProblemConfigEditor';
|
import ProblemConfigEditor from './ProblemConfigEditor';
|
||||||
import ProblemConfigForm from './ProblemConfigForm';
|
import ProblemConfigForm from './ProblemConfigForm';
|
||||||
@@ -15,40 +16,74 @@ interface Props {
|
|||||||
|
|
||||||
export default function ProblemConfig(props: Props) {
|
export default function ProblemConfig(props: Props) {
|
||||||
const [selected, setSelected] = React.useState('basic');
|
const [selected, setSelected] = React.useState('basic');
|
||||||
const [open, setOpen] = React.useState(false);
|
const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
|
||||||
|
const [tabs, setTabs] = React.useState<HTMLDivElement | null>(null);
|
||||||
|
const [editor, setEditor] = React.useState<any>(null);
|
||||||
|
const store = useStore<RootState>();
|
||||||
const valid = useSelector((state: RootState) => state.config.__valid);
|
const valid = useSelector((state: RootState) => state.config.__valid);
|
||||||
const errors = useSelector((state: RootState) => state.config.__errors, (a, b) => JSON.stringify(a) === JSON.stringify(b));
|
const errors = useSelector((state: RootState) => state.config.__errors, (a, b) => JSON.stringify(a) === JSON.stringify(b));
|
||||||
React.useEffect(() => {
|
|
||||||
if (valid) setOpen(false);
|
|
||||||
}, [valid]);
|
|
||||||
|
|
||||||
return (<>
|
const handleSave = React.useCallback(async () => {
|
||||||
<div className="row">
|
if (valid) props.onSave();
|
||||||
<div className="medium-4 columns">
|
else if (await confirm(i18n('Errors detected in the config. Confirm save?'))) props.onSave();
|
||||||
<ProblemConfigEditor />
|
}, [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 <ContextMenuProvider>
|
||||||
|
<div ref={setContainer}>
|
||||||
|
<Allotment defaultSizes={[2, 3]}>
|
||||||
|
<Allotment.Pane>
|
||||||
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
||||||
|
<ProblemConfigEditor ref={setEditor} />
|
||||||
</div>
|
</div>
|
||||||
<div className="medium-8 columns">
|
</Allotment.Pane>
|
||||||
<Tabs onChange={(t) => (t !== 'errors' && setSelected(t.toString()))} selectedTabId={valid ? selected : 'errors'}>
|
<Allotment.Pane>
|
||||||
<Tab id="basic" disabled={!valid} title={i18n('Basic')} panel={<ProblemConfigForm />} />
|
<Tabs
|
||||||
<Tab id="subtasks" disabled={!valid} title={i18n('Subtasks')} panel={<ProblemConfigTree />} />
|
ref={setTabs}
|
||||||
<Tab
|
value={valid ? selected : 'errors'}
|
||||||
id="errors"
|
keepMounted={false}
|
||||||
disabled={valid}
|
style={{ paddingLeft: 20 }}
|
||||||
title={errors.length ? `Errors(${errors.length})` : 'No Errors'}
|
onChange={(t) => (t !== 'errors' && t && setSelected(t.toString()))}
|
||||||
panel={<div>{errors.map((i) => (<pre key={i}>{i}</pre>))}</div>}
|
>
|
||||||
/>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="basic" disabled={!valid}>{i18n('Basic')}</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="subtasks" disabled={!valid}>{i18n('Subtasks')}</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="errors" disabled={valid}>{errors.length ? `Errors(${errors.length})` : 'No Errors'}</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
{i18n('Save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Tabs.Panel value="basic">
|
||||||
|
<ProblemConfigForm />
|
||||||
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="subtasks">
|
||||||
|
<ProblemConfigTree />
|
||||||
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="errors">
|
||||||
|
<div>{errors.map((i) => (<pre key={i}>{i}</pre>))}</div>
|
||||||
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</Allotment.Pane>
|
||||||
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ContextMenuProvider>;
|
||||||
<Dialog isOpen={open && !valid} onClose={() => setOpen(false)}>
|
|
||||||
<DialogBody>
|
|
||||||
<p>{i18n('Errors detected in the config. Confirm save?')}</p>
|
|
||||||
</DialogBody>
|
|
||||||
<DialogFooter actions={<>
|
|
||||||
<Button onClick={() => setOpen(false)}>{i18n('Cancel')}</Button>
|
|
||||||
<Button intent="warning" onClick={props.onSave}>{i18n('Save')}</Button>
|
|
||||||
</>} />
|
|
||||||
</Dialog>
|
|
||||||
<button className="rounded primary button" onClick={valid ? props.onSave : () => setOpen(true)}>{i18n('Save')}</button>
|
|
||||||
</>);
|
|
||||||
}
|
}
|
||||||
@@ -138,17 +138,21 @@ export default function reducer(state = {
|
|||||||
case 'problemconfig/moveTestcases': {
|
case 'problemconfig/moveTestcases': {
|
||||||
const testcases = action.payload.cases;
|
const testcases = action.payload.cases;
|
||||||
const subtasks = cloneDeep(state.subtasks);
|
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
|
const __cases = action.payload.source === -1
|
||||||
? state.__cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output))
|
? state.__cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output))
|
||||||
: action.payload.target === -1
|
: action.payload.target === -1
|
||||||
? sortFiles([...state.__cases, ...testcases], 'input')
|
? sortFiles([...state.__cases, ...toAdd], 'input')
|
||||||
: state.__cases;
|
: state.__cases;
|
||||||
for (const key in subtasks) {
|
for (const key in subtasks) {
|
||||||
const subtask = subtasks[key];
|
const subtask = subtasks[key];
|
||||||
if (subtask.id === action.payload.source) {
|
if (subtask.id === action.payload.source) {
|
||||||
subtask.cases = subtask.cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output));
|
subtask.cases = subtask.cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output));
|
||||||
} else if (subtask.id === action.payload.target) {
|
} 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 };
|
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)) };
|
return { ...state, __cases: state.__cases.filter((i) => !testcases.find((j) => i.input === j.input && i.output === j.output)) };
|
||||||
}
|
}
|
||||||
case 'problemconfig/deleteSubtask': {
|
case 'problemconfig/deleteSubtask': {
|
||||||
|
const currentCases = state.subtasks.find((i) => i.id === action.id)?.cases || [];
|
||||||
const subtasks = state.subtasks.filter((i) => i.id !== action.id);
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { readSubtasksFromFiles } from '@hydrooj/common';
|
import { readSubtasksFromFiles } from '@hydrooj/common';
|
||||||
import {
|
import { Button, Modal, Text } from '@mantine/core';
|
||||||
Button, Classes, Dialog, DialogBody, DialogFooter, Icon,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useSelector, useStore } from 'react-redux';
|
import { useSelector, useStore } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
@@ -83,14 +81,14 @@ export function AddTestcase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<li className={Classes.TREE_NODE} onClick={auto}>
|
<div
|
||||||
<div className={`${Classes.TREE_NODE_CONTENT} ${Classes.TREE_NODE_CONTENT}-0`}>
|
style={{ cursor: 'pointer', padding: '6px 0' }}
|
||||||
<Icon icon="clean" />
|
onClick={auto}
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Auto detect')}</span>
|
>
|
||||||
|
<Text><i className="icon icon-wrench" /> {i18n('Auto detect')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
<div
|
||||||
<li
|
style={{ cursor: 'pointer', padding: '6px 0' }}
|
||||||
className={Classes.TREE_NODE}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInput('');
|
setInput('');
|
||||||
setOutput('');
|
setOutput('');
|
||||||
@@ -99,15 +97,16 @@ export function AddTestcase() {
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`${Classes.TREE_NODE_CONTENT} ${Classes.TREE_NODE_CONTENT}-0`}>
|
<Text><i className="icon icon-add" /> {i18n('Add testcase')}</Text>
|
||||||
<Icon icon="clean" />
|
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Add testcase')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
<Modal
|
||||||
<Dialog title="Add testcase" icon="cog" isOpen={open} onClose={() => setOpen(false)}>
|
opened={open}
|
||||||
<DialogBody>
|
onClose={() => setOpen(false)}
|
||||||
<div className="row">
|
title={i18n('Add testcase')}
|
||||||
<div className="columns medium-6">
|
styles={{ body: { overflow: 'visible' }, content: { overflow: 'visible' } }}
|
||||||
|
>
|
||||||
|
<div className="row" style={{ overflow: 'visible' }}>
|
||||||
|
<div className="columns medium-6" style={{ overflow: 'visible' }}>
|
||||||
<FileSelectAutoComplete
|
<FileSelectAutoComplete
|
||||||
ref={refInput}
|
ref={refInput}
|
||||||
data={testdata}
|
data={testdata}
|
||||||
@@ -118,7 +117,7 @@ export function AddTestcase() {
|
|||||||
value={input || ''}
|
value={input || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="columns medium-6">
|
<div className="columns medium-6" style={{ overflow: 'visible' }}>
|
||||||
<FileSelectAutoComplete
|
<FileSelectAutoComplete
|
||||||
ref={refOutput}
|
ref={refOutput}
|
||||||
data={testdata}
|
data={testdata}
|
||||||
@@ -126,20 +125,13 @@ export function AddTestcase() {
|
|||||||
width="100%"
|
width="100%"
|
||||||
onChange={(e) => setOutput(e)}
|
onChange={(e) => setOutput(e)}
|
||||||
placeholder={i18n('Output')}
|
placeholder={i18n('Output')}
|
||||||
value={input || ''}
|
value={output || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
|
||||||
<DialogFooter
|
<Button onClick={onConfirm} disabled={!valid}> {i18n('Save')}</Button>
|
||||||
actions={<Button
|
</div>
|
||||||
className={`primary rounded button${valid ? '' : ' disabled'}`}
|
</Modal>
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={!valid}
|
|
||||||
intent="primary"
|
|
||||||
text="Save"
|
|
||||||
/>}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
||||||
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
||||||
import {
|
import { Button, Group, Modal, Text, TextInput } from '@mantine/core';
|
||||||
Button, Classes, ControlGroup,
|
|
||||||
Dialog, DialogBody, DialogFooter,
|
|
||||||
Icon, InputGroup, Tag,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -61,36 +57,37 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Dialog title={i18n('Set time and memory limits')} icon="cog" isOpen={open} onClose={() => setOpen(false)}>
|
<Modal opened={open} onClose={() => setOpen(false)} title={i18n('Set time and memory limits')}>
|
||||||
<DialogBody>
|
<Group grow>
|
||||||
<ControlGroup fill={true} vertical={false}>
|
<TextInput
|
||||||
<InputGroup
|
leftSection={<i className="icon icon-stopwatch" />}
|
||||||
leftElement={<Icon icon="time" />}
|
rightSection={<Text size="sm">ms</Text>}
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
rightSectionWidth={60}
|
||||||
onChange={(ev) => setTime(ev.currentTarget.value ? `${ev.currentTarget.value}ms` : '')}
|
onChange={(ev) => setTime(ev.currentTarget.value ? `${ev.currentTarget.value}ms` : '')}
|
||||||
placeholder={parseTimeMS(props.time, false).toString() || '1000'}
|
placeholder={parseTimeMS(props.time, false).toString() || '1000'}
|
||||||
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<TextInput
|
||||||
leftElement={<Icon icon="comparison" />}
|
leftSection={<i className="icon icon-comparison" />}
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
rightSection={<Text size="sm">MB</Text>}
|
||||||
|
rightSectionWidth={60}
|
||||||
onChange={(ev) => setMemory(ev.currentTarget.value ? `${ev.currentTarget.value}MB` : '')}
|
onChange={(ev) => setMemory(ev.currentTarget.value ? `${ev.currentTarget.value}MB` : '')}
|
||||||
placeholder={parseMemoryMB(props.memory, false).toString() || '256'}
|
placeholder={parseMemoryMB(props.memory, false).toString() || '256'}
|
||||||
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<TextInput
|
||||||
leftElement={<Icon icon="star" />}
|
leftSection={<i className="icon icon-star" />}
|
||||||
onChange={(ev) => setScore(+ev.target.value || 0)}
|
onChange={(ev) => setScore(+ev.target.value || 0)}
|
||||||
placeholder="Score"
|
placeholder="Score"
|
||||||
type="number"
|
type="number"
|
||||||
value={cscore.toString()}
|
value={cscore.toString()}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</Group>
|
||||||
</DialogBody>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
|
||||||
<DialogFooter actions={<Button className="primary rounded button" onClick={onConfirm} intent="primary" text="Save" />} />
|
<Button color="blue" onClick={onConfirm}>{i18n('Save')}</Button>
|
||||||
</Dialog>
|
</div>
|
||||||
<Dialog title={i18n('Set dependencies')} icon="cog" isOpen={depsOpen} onClose={() => setDepsOpen(false)}>
|
</Modal>
|
||||||
<DialogBody>
|
<Modal opened={depsOpen} onClose={() => setDepsOpen(false)} title={i18n('Set dependencies')}>
|
||||||
<CustomSelectAutoComplete
|
<CustomSelectAutoComplete
|
||||||
data={subtaskIds.map((i) => ({ _id: i, name: `${i18n('Subtask {0}', i)}` }))}
|
data={subtaskIds.map((i) => ({ _id: i, name: `${i18n('Subtask {0}', i)}` }))}
|
||||||
setSelectItems={cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i)}
|
setSelectItems={cdeps.split(',').map((i) => i.trim()).filter((i) => +i).map((i) => +i)}
|
||||||
@@ -98,38 +95,26 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
|
|||||||
placeholder="dependencies"
|
placeholder="dependencies"
|
||||||
multi
|
multi
|
||||||
/>
|
/>
|
||||||
</DialogBody>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
|
||||||
<DialogFooter actions={<Button className="primary rounded button" onClick={onConfirm} intent="primary" text="Save" />} />
|
<Button color="blue" onClick={onConfirm}>{i18n('Save')}</Button>
|
||||||
</Dialog>
|
|
||||||
<li className={Classes.TREE_NODE} onClick={() => setOpen(true)}>
|
|
||||||
<div className={Classes.TREE_NODE_CONTENT}>
|
|
||||||
<span className={`${Classes.TREE_NODE_CARET_NONE} ${Classes.ICON_STANDARD}`}></span>
|
|
||||||
<Icon icon="time" />
|
|
||||||
|
|
||||||
<span className={`${Classes.TREE_NODE_LABEL}${time ? '' : ' text-gray'}`}>{time || props.time || '1s'}</span>
|
|
||||||
<Icon icon="comparison" />
|
|
||||||
|
|
||||||
<span className={`${Classes.TREE_NODE_LABEL}${memory ? '' : ' text-gray'}`}>{memory || props.memory || '256m'}</span>
|
|
||||||
<Icon icon="star" />
|
|
||||||
{' '}
|
|
||||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>{score || 0}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</Modal>
|
||||||
<li className={Classes.TREE_NODE} onClick={() => setDepsOpen(true)}>
|
<div style={{ paddingLeft: 22, display: 'flex', cursor: 'pointer', gap: 8 }} onClick={() => setOpen(true)}>
|
||||||
<div className={Classes.TREE_NODE_CONTENT}>
|
<Text><i className="icon icon-stopwatch" /></Text>
|
||||||
<span className={`${Classes.TREE_NODE_CARET_NONE} ${Classes.ICON_STANDARD}`}></span>
|
<Text className={time ? '' : 'text-gray'}>{time || props.time || '1s'} </Text>
|
||||||
<Icon icon="diagram-tree" />
|
<Text><i className="icon icon-comparison" /></Text>
|
||||||
|
<Text className={memory ? '' : 'text-gray'}>{memory || props.memory || '256m'} </Text>
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Dependencies')}: {deps.length ? deps.join(', ') : i18n('(None)')}</span>
|
<Text><i className="icon icon-star" /></Text>
|
||||||
|
<Text>{score || 0}</Text>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', cursor: 'pointer', gap: 8 }} onClick={() => setDepsOpen(true)}>
|
||||||
<li className={Classes.TREE_NODE}>
|
<Text><i className="icon icon-diagram-tree" /></Text>
|
||||||
<div className={Classes.TREE_NODE_CONTENT}>
|
<Text>{i18n('Dependencies')}: {deps.length ? deps.join(', ') : i18n('(None)')}</Text>
|
||||||
<span className={`${Classes.TREE_NODE_CARET_NONE} ${Classes.ICON_STANDARD}`}></span>
|
</div>
|
||||||
<Icon icon="asterisk" />
|
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<i className="icon icon-asterisk" />
|
||||||
<span className={Classes.TREE_NODE_LABEL}>{i18n('Scoring method')}</span>
|
<Text>{i18n('Scoring method')}</Text>
|
||||||
<span className={Classes.TREE_NODE_SECONDARY_LABEL}>
|
<span style={{ marginLeft: 'auto' }}>
|
||||||
<select className="compact select" value={ctype} onChange={(e) => setType(e.target.value)}>
|
<select className="compact select" value={ctype} onChange={(e) => setType(e.target.value)}>
|
||||||
<option value="min">Min</option>
|
<option value="min">Min</option>
|
||||||
<option value="max">Max</option>
|
<option value="max">Max</option>
|
||||||
@@ -137,7 +122,6 @@ export function SubtaskSettings(props: SubtaskSettingsProps) {
|
|||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,36 +147,34 @@ export function GlobalSettings() {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
return (<>
|
return (<>
|
||||||
<Dialog title={i18n('Set time and memory limits')} icon="cog" isOpen={open} onClose={() => setOpen(false)}>
|
<Modal opened={open} onClose={() => setOpen(false)} title={i18n('Set time and memory limits')}>
|
||||||
<DialogBody>
|
<Group grow>
|
||||||
<ControlGroup fill={true} vertical={false}>
|
<TextInput
|
||||||
<InputGroup
|
leftSection={<i className="icon icon-stopwatch" />}
|
||||||
leftElement={<Icon icon="time" />}
|
rightSection={<Text size="sm">ms</Text>}
|
||||||
rightElement={<Tag minimal>ms</Tag>}
|
rightSectionWidth={60}
|
||||||
onChange={(ev) => setTime(ev.currentTarget.value ? `${ev.currentTarget.value}ms` : '')}
|
onChange={(ev) => setTime(ev.currentTarget.value ? `${ev.currentTarget.value}ms` : '')}
|
||||||
placeholder="1000"
|
placeholder="1000"
|
||||||
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
value={ctime ? parseTimeMS(ctime, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
<InputGroup
|
<TextInput
|
||||||
leftElement={<Icon icon="comparison" />}
|
leftSection={<i className="icon icon-comparison" />}
|
||||||
rightElement={<Tag minimal>MB</Tag>}
|
rightSection={<Text size="sm">MB</Text>}
|
||||||
|
rightSectionWidth={60}
|
||||||
onChange={(ev) => setMemory(ev.currentTarget.value ? `${ev.currentTarget.value}MB` : '')}
|
onChange={(ev) => setMemory(ev.currentTarget.value ? `${ev.currentTarget.value}MB` : '')}
|
||||||
placeholder="256"
|
placeholder="256"
|
||||||
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
value={cmemory ? parseMemoryMB(cmemory, false).toString() || '' : ''}
|
||||||
/>
|
/>
|
||||||
</ControlGroup>
|
</Group>
|
||||||
</DialogBody>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
|
||||||
<DialogFooter actions={<Button className="primary rounded button" onClick={onConfirm} intent="primary" text="Save" />} />
|
<Button color="blue" onClick={onConfirm}>{i18n('Save')}</Button>
|
||||||
</Dialog>
|
</div>
|
||||||
<li className="bp6-tree-node" onClick={() => setOpen(true)}>
|
</Modal>
|
||||||
<div className="bp6-tree-node-content">
|
<div style={{ paddingLeft: 22, display: 'flex', alignItems: 'center', cursor: 'pointer', gap: 8 }} onClick={() => setOpen(true)}>
|
||||||
<Icon icon="time" />
|
<i className="icon icon-stopwatch" />
|
||||||
|
<Text className={time ? '' : 'text-gray'}>{time || '1s'}</Text>
|
||||||
<span className={`bp6-tree-node-label${time ? '' : ' text-gray'}`}>{time || '1s'}</span>
|
<i className="icon icon-comparison" />
|
||||||
<Icon icon="comparison" />
|
<Text className={memory ? '' : 'text-gray'}>{memory || '256MB'}</Text>
|
||||||
{' '}
|
|
||||||
<span className={`bp6-tree-node-secondary-label${memory ? '' : ' text-gray'}`}>{memory || '256MB'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { TestCaseConfig } from 'hydrooj';
|
import { TestCaseConfig } from 'hydrooj';
|
||||||
import {
|
import { Text } from '@mantine/core';
|
||||||
ContextMenu, Menu, MenuItem, TreeNode,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
import { useContextMenu } from 'mantine-contextmenu';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDrag } from 'react-dnd';
|
import { useDrag } from 'react-dnd';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
|
|
||||||
interface TestcaseNodeProps {
|
interface TestcaseNodeProps {
|
||||||
@@ -20,37 +20,35 @@ interface TestcaseNodeProps {
|
|||||||
|
|
||||||
export function TestcaseNode(props: TestcaseNodeProps) {
|
export function TestcaseNode(props: TestcaseNodeProps) {
|
||||||
const {
|
const {
|
||||||
c, selected, onClick, subtaskIds, subtaskId,
|
c, selected, onClick, subtaskId,
|
||||||
} = props;
|
} = props;
|
||||||
|
let display = `${c.input} / ${c.output}`;
|
||||||
|
const minlength = Math.min(c.input.length, c.output.length);
|
||||||
|
for (let i = minlength; i >= 0; i--) {
|
||||||
|
const prefix = c.input.slice(0, i);
|
||||||
|
if (c.input.startsWith(prefix) && c.output.startsWith(prefix)) {
|
||||||
|
display = `${prefix}(${c.input.substring(i)}/${c.output.substring(i)})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<div
|
||||||
onContextMenu={onClick}
|
onClick={onClick}
|
||||||
data-subtaskid={subtaskId}
|
data-subtaskid={subtaskId}
|
||||||
data-index={c.input}
|
data-index={c.input}
|
||||||
data-selected={selected}
|
data-selected={selected}
|
||||||
content={
|
style={{
|
||||||
<Menu>
|
paddingLeft: 22,
|
||||||
<MenuItem icon="drawer-left" text={i18n('Move to subtask')}>
|
display: 'flex',
|
||||||
{subtaskIds.filter((i) => i !== subtaskId).map((i) => (
|
alignItems: 'center',
|
||||||
<MenuItem key={i} text={i18n('Subtask {0}', i)} />
|
cursor: 'pointer',
|
||||||
))}
|
background: selected ? 'var(--mantine-color-blue-light, #ebf3ff)' : 'transparent',
|
||||||
{subtaskIds.length <= 1 && (
|
borderRadius: 4,
|
||||||
<MenuItem icon="disable" disabled text={i18n('No target available')} />
|
fontFamily: 'var(--mantine-font-family-monospace)',
|
||||||
)}
|
}}
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<TreeNode
|
<Text ml="xs"><i className="icon icon-file" /> {display}</Text>
|
||||||
depth={0}
|
</div>
|
||||||
id={c.input}
|
|
||||||
isSelected={selected}
|
|
||||||
onClick={onClick}
|
|
||||||
icon="document"
|
|
||||||
label={<> {c.input} / {c.output}</>}
|
|
||||||
path={[0]}
|
|
||||||
/>
|
|
||||||
</ContextMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +59,11 @@ interface TestcaseGroupProps extends Omit<TestcaseNodeProps, 'c'> {
|
|||||||
|
|
||||||
export function TestcaseGroup(props: TestcaseGroupProps) {
|
export function TestcaseGroup(props: TestcaseGroupProps) {
|
||||||
const {
|
const {
|
||||||
cases, subtaskId, onClick, index,
|
cases, subtaskId, subtaskIds, onClick, index,
|
||||||
} = props;
|
} = props;
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { showContextMenu } = useContextMenu();
|
||||||
|
const moveTargets = subtaskIds.filter((i) => i !== subtaskId);
|
||||||
const [collected, drag] = useDrag(() => ({
|
const [collected, drag] = useDrag(() => ({
|
||||||
type: 'cases',
|
type: 'cases',
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
@@ -71,7 +72,25 @@ export function TestcaseGroup(props: TestcaseGroupProps) {
|
|||||||
canDrag: props.selected,
|
canDrag: props.selected,
|
||||||
item: { cases, subtaskId },
|
item: { cases, subtaskId },
|
||||||
}), [JSON.stringify(cases), subtaskId]);
|
}), [JSON.stringify(cases), subtaskId]);
|
||||||
return <div ref={drag} onClick={onClick} onMouseDown={props.onMouseDown} style={{ opacity: collected.isDragging ? 0.5 : 1 }}>
|
return <div
|
||||||
|
ref={drag}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseDown={props.onMouseDown}
|
||||||
|
onContextMenu={showContextMenu(
|
||||||
|
moveTargets.length
|
||||||
|
? moveTargets.map((id) => ({
|
||||||
|
key: `move-${id}`,
|
||||||
|
icon: <i className="icon icon-send" />,
|
||||||
|
title: `${i18n('Move to')} ${i18n('Subtask {0}', id)}`,
|
||||||
|
onClick: () => dispatch({
|
||||||
|
type: 'problemconfig/moveTestcases',
|
||||||
|
payload: { target: id, source: subtaskId, cases },
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
: [{ key: 'no-target', title: i18n('No target available'), onClick: () => { }, disabled: true }],
|
||||||
|
)}
|
||||||
|
style={{ opacity: collected.isDragging ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
{cases.map((c, id) => (
|
{cases.map((c, id) => (
|
||||||
<TestcaseNode c={c} key={`${c.input}@${index + id}`} {...omit(props, 'onClick')} index={index + id} />
|
<TestcaseNode c={c} key={`${c.input}@${index + id}`} {...omit(props, 'onClick')} index={index + id} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Classes } from '@blueprintjs/core';
|
import { MantineProvider, Progress } from '@mantine/core';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Dialog } from 'vj/components/dialog/index';
|
import { Dialog } from 'vj/components/dialog/index';
|
||||||
@@ -19,36 +19,40 @@ interface UploadOptions {
|
|||||||
filenameCallback?: (file: File) => string;
|
filenameCallback?: (file: File) => string;
|
||||||
}
|
}
|
||||||
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
||||||
const dialog = new Dialog({
|
let fileLabel = '';
|
||||||
$body: $(tpl(<>
|
let uploadLabel = '';
|
||||||
|
let fileProgress = 0;
|
||||||
|
let uploadProgress = 0;
|
||||||
|
let render = () => { };
|
||||||
|
|
||||||
|
function ProgressDialog() {
|
||||||
|
const [, setRender] = React.useState(0);
|
||||||
|
React.useEffect(() => {
|
||||||
|
render = () => setRender((r) => r + 1);
|
||||||
|
}, []);
|
||||||
|
return <MantineProvider>
|
||||||
<div
|
<div
|
||||||
className="file-label"
|
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center', marginBottom: '5px', color: 'gray', fontSize: 'small',
|
textAlign: 'center', marginBottom: '5px', color: 'gray', fontSize: 'small',
|
||||||
}}
|
}}
|
||||||
></div>
|
>{uploadLabel}</div>
|
||||||
<div className={`${Classes.PROGRESS_BAR} ${Classes.INTENT_PRIMARY} bp5-no-stripes`}>
|
<Progress value={uploadProgress} />
|
||||||
<div className={`file-progress ${Classes.PROGRESS_METER}`} style={{ width: 0 }}></div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="upload-label"
|
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center', margin: '5px 0', color: 'gray', fontSize: 'small',
|
textAlign: 'center', margin: '5px 0', color: 'gray', fontSize: 'small',
|
||||||
}}
|
}}
|
||||||
></div>
|
>{fileLabel}</div>
|
||||||
<div className={`${Classes.PROGRESS_BAR} ${Classes.INTENT_PRIMARY} bp5-no-stripes`}>
|
<Progress value={fileProgress} />
|
||||||
<div className={`upload-progress ${Classes.PROGRESS_METER}`} style={{ width: 0 }}></div>
|
</MantineProvider>;
|
||||||
</div>
|
}
|
||||||
</>)),
|
|
||||||
|
const dialog = new Dialog({
|
||||||
|
$body: $(tpl(<ProgressDialog />, true)),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
Notification.info(i18n('Uploading files...'));
|
Notification.info(i18n('Uploading files...'));
|
||||||
window.addEventListener('beforeunload', onBeforeUnload);
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
dialog.open();
|
dialog.open();
|
||||||
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
|
|
||||||
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
|
|
||||||
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
|
|
||||||
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
|
|
||||||
for (const i in files) {
|
for (const i in files) {
|
||||||
if (Number.isNaN(+i)) continue;
|
if (Number.isNaN(+i)) continue;
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
@@ -58,20 +62,22 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
|
|||||||
if (options.type) data.append('type', options.type);
|
if (options.type) data.append('type', options.type);
|
||||||
data.append('operation', 'upload_file');
|
data.append('operation', 'upload_file');
|
||||||
await request.postFile(endpoint, data, {
|
await request.postFile(endpoint, data, {
|
||||||
xhr() {
|
xhr() { // eslint-disable-line
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.upload.addEventListener('loadstart', () => {
|
xhr.upload.addEventListener('loadstart', () => {
|
||||||
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name} `);
|
uploadLabel = `[${+i + 1}/${files.length}] ${file.name} `;
|
||||||
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}% `);
|
uploadProgress = Math.round((+i + 1) / files.length * 100);
|
||||||
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
|
fileLabel = i18n('Uploading... ({0}%)', 0);
|
||||||
$uploadProgress.width(0);
|
fileProgress = 0;
|
||||||
|
render();
|
||||||
});
|
});
|
||||||
xhr.upload.addEventListener('progress', (e) => {
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||||
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
|
if (percentComplete === 100) fileLabel = i18n('Processing...');
|
||||||
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
|
else fileLabel = i18n('Uploading... ({0}%)', percentComplete);
|
||||||
$uploadProgress.width(`${percentComplete}% `);
|
fileProgress = percentComplete;
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
return xhr;
|
return xhr;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"cli": false
|
"cli": false
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/core": "^6.3.2",
|
|
||||||
"@fontsource/dm-mono": "^5.2.7",
|
"@fontsource/dm-mono": "^5.2.7",
|
||||||
"@fontsource/fira-code": "^5.2.7",
|
"@fontsource/fira-code": "^5.2.7",
|
||||||
"@fontsource/inconsolata": "^5.2.8",
|
"@fontsource/inconsolata": "^5.2.8",
|
||||||
@@ -21,8 +20,11 @@
|
|||||||
"@fontsource/source-code-pro": "^5.2.7",
|
"@fontsource/source-code-pro": "^5.2.7",
|
||||||
"@fontsource/ubuntu-mono": "^5.2.8",
|
"@fontsource/ubuntu-mono": "^5.2.8",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
|
"@mantine/core": "^8.3.6",
|
||||||
|
"@mantine/hooks": "^8.3.6",
|
||||||
|
"@mantine/notifications": "^8.3.6",
|
||||||
"@sentry/browser": "^10.22.0",
|
"@sentry/browser": "^10.22.0",
|
||||||
"@sentry/webpack-plugin": "^4.5.0",
|
"@sentry/webpack-plugin": "^4.6.0",
|
||||||
"@simplewebauthn/browser": "13.2.2",
|
"@simplewebauthn/browser": "13.2.2",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "^9.0.4",
|
||||||
@@ -34,8 +36,8 @@
|
|||||||
"@types/nunjucks": "^3.2.6",
|
"@types/nunjucks": "^3.2.6",
|
||||||
"@types/pickadate": "^3.5.35",
|
"@types/pickadate": "^3.5.35",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^18.3.26",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/redux-logger": "^3.0.13",
|
"@types/redux-logger": "^3.0.13",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"@types/webpack-env": "^1.18.8",
|
"@types/webpack-env": "^1.18.8",
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
"jquery-scroll-lock": "^3.1.3",
|
"jquery-scroll-lock": "^3.1.3",
|
||||||
"jquery.easing": "^1.4.1",
|
"jquery.easing": "^1.4.1",
|
||||||
"jquery.transit": "^0.9.12",
|
"jquery.transit": "^0.9.12",
|
||||||
|
"mantine-contextmenu": "^8.2.0",
|
||||||
"matchmedia-polyfill": "^0.3.2",
|
"matchmedia-polyfill": "^0.3.2",
|
||||||
"md-editor-rt": "^6.1.0",
|
"md-editor-rt": "^6.1.0",
|
||||||
"mini-css-extract-plugin": "^2.9.4",
|
"mini-css-extract-plugin": "^2.9.4",
|
||||||
@@ -80,17 +83,17 @@
|
|||||||
"p-queue": "^9.0.0",
|
"p-queue": "^9.0.0",
|
||||||
"pickadate": "^3.6.4",
|
"pickadate": "^3.6.4",
|
||||||
"postcss-loader": "7.3.4",
|
"postcss-loader": "7.3.4",
|
||||||
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"prism-themes": "^1.9.0",
|
"prism-themes": "^1.9.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"qface": "^1.4.1",
|
"qface": "^1.4.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-query": "^3.39.3",
|
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
@@ -98,7 +101,7 @@
|
|||||||
"redux-promise-middleware": "^6.2.0",
|
"redux-promise-middleware": "^6.2.0",
|
||||||
"redux-thunk": "^3.1.0",
|
"redux-thunk": "^3.1.0",
|
||||||
"rupture": "^0.7.1",
|
"rupture": "^0.7.1",
|
||||||
"sass": "^1.93.2",
|
"sass": "^1.93.3",
|
||||||
"sass-loader": "^16.0.6",
|
"sass-loader": "^16.0.6",
|
||||||
"schemastery": "^3.17.1",
|
"schemastery": "^3.17.1",
|
||||||
"shorty.js": "^1.0.1",
|
"shorty.js": "^1.0.1",
|
||||||
@@ -117,7 +120,7 @@
|
|||||||
"webpack": "^5.102.1",
|
"webpack": "^5.102.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"webpack-dev-server": "^5.2.2",
|
"webpack-dev-server": "^5.2.2",
|
||||||
"webpack-manifest-plugin": "^5.0.1",
|
"webpack-manifest-plugin": "^6.0.1",
|
||||||
"webpackbar": "^7.0.0"
|
"webpackbar": "^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,50 +1,14 @@
|
|||||||
.page--problem_config
|
.page--problem_config
|
||||||
|
.main
|
||||||
|
.row
|
||||||
|
max-width: 100vw !important;
|
||||||
|
|
||||||
.textbox,.select
|
.textbox,.select
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
.bp6-tab-panel
|
|
||||||
margin-top: 5px
|
|
||||||
|
|
||||||
.bp6-dialog
|
|
||||||
font-family: var(--code-font-family)
|
|
||||||
background: #fff
|
|
||||||
|
|
||||||
.autocomplete-container
|
|
||||||
font-size: 0.875rem
|
|
||||||
color: #666
|
|
||||||
|
|
||||||
.bp6-dialog-footer-fixed
|
|
||||||
border-top: 0
|
|
||||||
|
|
||||||
.button
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
input::placeholder
|
input::placeholder
|
||||||
color: rgba(95,107,124,.6)!important
|
color: rgba(95,107,124,.6)!important
|
||||||
|
|
||||||
.bp6-input-group
|
|
||||||
input
|
|
||||||
appearance: none
|
|
||||||
display: block
|
|
||||||
width: 100%
|
|
||||||
font-size: 0.875rem
|
|
||||||
height: 2.1875rem
|
|
||||||
line-height: 1.2
|
|
||||||
padding: 0.3125rem;
|
|
||||||
border: 1px solid #ddd
|
|
||||||
color: inherit
|
|
||||||
border-radius: 0
|
|
||||||
outline: 2px solid transparent
|
|
||||||
transition: outline-color 0.2s, border-color 0.2s
|
|
||||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1)
|
|
||||||
box-shadow: none
|
|
||||||
|
|
||||||
.bp6-tag.bp6-minimal
|
|
||||||
background: transparent
|
|
||||||
|
|
||||||
.bp6-input-action
|
|
||||||
pointer-events: none
|
|
||||||
|
|
||||||
.problem-config-form
|
.problem-config-form
|
||||||
font-family: var(--code-font-family)
|
font-family: var(--code-font-family)
|
||||||
|
|
||||||
@@ -55,4 +19,3 @@
|
|||||||
|
|
||||||
.hover-row.status-bar
|
.hover-row.status-bar
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { normalizeSubtasks, readSubtasksFromFiles, SubtaskType } from '@hydrooj/common';
|
import { normalizeSubtasks, readSubtasksFromFiles, SubtaskType } from '@hydrooj/common';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -114,6 +115,7 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
type: 'CONFIG_LOAD',
|
type: 'CONFIG_LOAD',
|
||||||
payload: request.get(''),
|
payload: request.get(''),
|
||||||
});
|
});
|
||||||
|
let autoConfigTriggered = false;
|
||||||
const unsubscribe = store.subscribe(() => {
|
const unsubscribe = store.subscribe(() => {
|
||||||
// TODO set yaml schema
|
// TODO set yaml schema
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
@@ -129,7 +131,10 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
delete state.config.cases;
|
delete state.config.cases;
|
||||||
delete state.config.score;
|
delete state.config.score;
|
||||||
}
|
}
|
||||||
if (state.config.subtasks?.length) return;
|
if (state.config.subtasks?.length || autoConfigTriggered) {
|
||||||
|
autoConfigTriggered = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const testdata = (state.testdata || []).map((i) => i.name);
|
const testdata = (state.testdata || []).map((i) => i.name);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
const subtasks = readSubtasksFromFiles(testdata, state.config);
|
const subtasks = readSubtasksFromFiles(testdata, state.config);
|
||||||
@@ -138,11 +143,11 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
subtasks: normalizeSubtasks(subtasks, (i) => i, state.config.time, state.config.memory, true),
|
subtasks: normalizeSubtasks(subtasks, (i) => i, state.config.time, state.config.memory, true),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
createRoot(document.getElementById('ProblemConfig')!).render(
|
createRoot(document.getElementById('ProblemConfig')!).render(<MantineProvider>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ProblemConfig onSave={() => uploadConfig(store.getState().config)} />
|
<ProblemConfig onSave={() => uploadConfig(store.getState().config)} />
|
||||||
</Provider>,
|
</Provider>
|
||||||
);
|
</MantineProvider>);
|
||||||
}
|
}
|
||||||
|
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ function ProblemSelectionDisplay(props) { // eslint-disable-line
|
|||||||
}, [pids]);
|
}, [pids]);
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<a href="javascript:;" className="menu__link display-mode-hide" onClick={() => setDialogOpen(true)}>
|
<a className="menu__link display-mode-hide" onClick={() => setDialogOpen(true)}>
|
||||||
<span className="icon icon-stack"></span>
|
<span className="icon icon-stack"></span>
|
||||||
{' '}{i18n('{0} problem(s) selected', pids.length)}
|
{' '}{i18n('{0} problem(s) selected', pids.length)}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
|
'postcss-simple-vars': {
|
||||||
|
variables: {
|
||||||
|
'mantine-breakpoint-xs': '36em',
|
||||||
|
'mantine-breakpoint-sm': '48em',
|
||||||
|
'mantine-breakpoint-md': '62em',
|
||||||
|
'mantine-breakpoint-lg': '75em',
|
||||||
|
'mantine-breakpoint-xl': '88em',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -12,14 +12,14 @@
|
|||||||
<h1 class="section__title">{{ tdoc.title }}</h1>
|
<h1 class="section__title">{{ tdoc.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item">{{ _(model.contest.statusText(tdoc, tsdoc)) }}</span>
|
<span class="problem__tag-item">{{ _(model.contest.statusText(tdoc, tsdoc)) }}</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-award">{{ _(model.contest.RULES[tdoc.rule].TEXT) }}</span>
|
<span class="problem__tag-item icon icon-award">{{ _(model.contest.RULES[tdoc.rule].TEXT) }}</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item">{{ _('Start at') }}: {{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}</span>
|
<span class="problem__tag-item">{{ _('Start at') }}: {{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-schedule">{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}</span>
|
<span class="problem__tag-item icon icon-schedule">{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item">{{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}</span>
|
<span class="problem__tag-item">{{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-user--multiple">{{ tdoc['attend']|default(0) }}</span>
|
<span class="problem__tag-item icon icon-user--multiple">{{ tdoc['attend']|default(0) }}</span>
|
||||||
{% if tsdoc.attend %}
|
{% if tsdoc.attend %}
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-check">{{ _('Attended') }}</span>
|
<span class="problem__tag-item icon icon-check">{{ _('Attended') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,26 +90,26 @@
|
|||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<ul class="problem__tags">
|
<ul class="problem__tags">
|
||||||
{% if not tdoc %}
|
{% if not tdoc %}
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
<span class="problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
<span class="problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
||||||
{% if pdoc.config.type == 'default' and pdoc.config.subType %}
|
{% if pdoc.config.type == 'default' and pdoc.config.subType %}
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-book">{{ _('File IO: {0}').format(pdoc.config.subType) }}</span>
|
<span class="problem__tag-item icon icon-book">{{ _('File IO: {0}').format(pdoc.config.subType) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
<span class="problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item icon icon-comparison">{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</span>
|
<span class="problem__tag-item icon icon-comparison">{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not tdoc %}
|
{% if not tdoc %}
|
||||||
<a class="bp6-tag bp6-large bp6-minimal bp6-interactive problem__tag-item" href="{{ url('record_main', query={pid:pdoc.docId}) }}">{{ _('Tried') }}: {{ pdoc.nSubmit }}</a>
|
<a class="problem__tag-item interactive" href="{{ url('record_main', query={pid:pdoc.docId}) }}">{{ _('Tried') }}: {{ pdoc.nSubmit }}</a>
|
||||||
<a class="bp6-tag bp6-large bp6-minimal bp6-interactive problem__tag-item" href="{{ url('record_main', query={pid:pdoc.docId, status:1}) }}">{{ _('Accepted') }}: {{ pdoc.nAccept if ((not tdoc) or showAccept) else '?' }}</a>
|
<a class="problem__tag-item interactive" href="{{ url('record_main', query={pid:pdoc.docId, status:1}) }}">{{ _('Accepted') }}: {{ pdoc.nAccept if ((not tdoc) or showAccept) else '?' }}</a>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item">{{ _('Difficulty') }}: {{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}</span>
|
<span class="problem__tag-item">{{ _('Difficulty') }}: {{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}</span>
|
||||||
<span class="bp6-tag bp6-large bp6-minimal bp6-interactive problem__tag-item">{{ _('Uploaded By') }}: {{ user.render_inline(udoc, badge=false) }}</span>
|
<span class="problem__tag-item interactive">{{ _('Uploaded By') }}: {{ user.render_inline(udoc, badge=false) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pdoc.tag.length %}
|
{% if pdoc.tag.length %}
|
||||||
<span name="show_tags" class="bp6-tag bp6-large bp6-minimal bp6-interactive problem__tag-item nojs--hide icon icon-tag">{{ _('Tags') + '>' }}</span>
|
<span name="show_tags" class="problem__tag-item interactive nojs--hide icon icon-tag">{{ _('Tags') + '>' }}</span>
|
||||||
{%- for tag in pdoc.tag -%}
|
{%- for tag in pdoc.tag -%}
|
||||||
<span class="bp6-tag bp6-large bp6-minimal problem__tag-item tags hasjs--hide">{{ tag }}</span>
|
<span class="problem__tag-item tags hasjs--hide">{{ tag }}</span>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
.bp6-progress-bar {
|
||||||
|
background: rgba(95, 107, 124, 0.2);
|
||||||
|
border-radius: 40px;
|
||||||
|
display: block;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-progress-bar .bp6-progress-meter {
|
||||||
|
background: linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%);
|
||||||
|
background-color: rgba(95, 107, 124, 0.8);
|
||||||
|
background-size: 30px 30px;
|
||||||
|
border-radius: 40px;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
transition: width 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-progress-bar.bp6-no-stripes .bp6-progress-meter {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-dark .bp6-progress-bar {
|
||||||
|
background: rgba(17, 20, 24, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-dark .bp6-progress-bar .bp6-progress-meter {
|
||||||
|
background-color: #8f99a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-progress-bar.bp6-intent-primary .bp6-progress-meter {
|
||||||
|
background-color: #2d72d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-progress-bar.bp6-intent-success .bp6-progress-meter {
|
||||||
|
background-color: #238551;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-progress-bar.bp6-intent-warning .bp6-progress-meter {
|
||||||
|
background-color: #c87619;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp6-progress-bar.bp6-intent-danger .bp6-progress-meter {
|
||||||
|
background-color: #cd4246;
|
||||||
|
}
|
||||||
@@ -212,10 +212,6 @@ $hover-background-color = #424242
|
|||||||
.menu__link.highlight
|
.menu__link.highlight
|
||||||
background: rgb(40, 11, 20)
|
background: rgb(40, 11, 20)
|
||||||
|
|
||||||
.bp6-tag.bp6-minimal:not([class*="bp6-intent-"])
|
|
||||||
background-color: rgba(87, 100, 117, .2)
|
|
||||||
color: #d9dfe7
|
|
||||||
|
|
||||||
.typo table thead th
|
.typo table thead th
|
||||||
background: #1e1e1e
|
background: #1e1e1e
|
||||||
background-color: rgb(30, 30, 30)
|
background-color: rgb(30, 30, 30)
|
||||||
@@ -391,18 +387,6 @@ $hover-background-color = #424242
|
|||||||
background-color: $main-background-color !important
|
background-color: $main-background-color !important
|
||||||
border-color: #3d3d3d !important
|
border-color: #3d3d3d !important
|
||||||
|
|
||||||
.bp6-tab
|
|
||||||
color: #aaa
|
|
||||||
|
|
||||||
&[aria-disabled=true]
|
|
||||||
color: #555
|
|
||||||
|
|
||||||
.bp6-card,.bp6-dialog,.bp6-dialog-header,.bp6-dialog-footer
|
|
||||||
background-color: #1f1f1f !important
|
|
||||||
|
|
||||||
.bp6-input-group input
|
|
||||||
border-color: #3d3d3d!important
|
|
||||||
|
|
||||||
&.hasjs .collapsed #status:not([data-status="7"]) .compiler-text-mask
|
&.hasjs .collapsed #status:not([data-status="7"]) .compiler-text-mask
|
||||||
background-image: linear-gradient(to bottom, transparent, #000000 25px)
|
background-image: linear-gradient(to bottom, transparent, #000000 25px)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import 'pickadate/lib/themes/classic.css';
|
|||||||
import 'pickadate/lib/themes/classic.date.css';
|
import 'pickadate/lib/themes/classic.date.css';
|
||||||
import 'pickadate/lib/themes/classic.time.css';
|
import 'pickadate/lib/themes/classic.time.css';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
import '@blueprintjs/core/lib/css/blueprint.css';
|
import '@mantine/core/styles.css';
|
||||||
|
import '@mantine/notifications/styles.css';
|
||||||
|
import 'mantine-contextmenu/styles.css';
|
||||||
import '@fontsource/fira-code';
|
import '@fontsource/fira-code';
|
||||||
import '@fontsource/source-code-pro';
|
import '@fontsource/source-code-pro';
|
||||||
import '@fontsource/roboto-mono';
|
import '@fontsource/roboto-mono';
|
||||||
@@ -26,6 +28,7 @@ import 'vj/misc/nothing.styl';
|
|||||||
import 'vj/components/editor/cmeditor.styl';
|
import 'vj/components/editor/cmeditor.styl';
|
||||||
import 'vj/components/datepicker/datepicker.styl';
|
import 'vj/components/datepicker/datepicker.styl';
|
||||||
import './dark.styl';
|
import './dark.styl';
|
||||||
|
import './bp6-compat.css';
|
||||||
|
|
||||||
// load all page stylesheets
|
// load all page stylesheets
|
||||||
const pageStyleReq = require.context('../', true, /\.page\.styl$/i);
|
const pageStyleReq = require.context('../', true, /\.page\.styl$/i);
|
||||||
|
|||||||
Reference in new issue
Block a user