From 310189bc4ddb62e8b236778dd567107f32be03b2 Mon Sep 17 00:00:00 2001
From: undefined
Date: Tue, 26 Apr 2022 23:49:24 +0800
Subject: [PATCH] ui: UI&UX enhance, bug fixes
Co-authored-by: panda
---
package.json | 5 +-
packages/hydrojudge/src/cases.ts | 19 +-
packages/hydrooj/src/entry/common.ts | 1 +
packages/hydrooj/src/interface.ts | 9 +-
packages/ui-default/.eslintrc.js | 3 +-
.../autocomplete/autocomplete.page.styl | 8 +-
.../components/CustomSelectAutoComplete.tsx | 2 +-
.../components/FileSelectAutoComplete.tsx | 51 ++
.../components/hotkey/hotkey.page.js | 18 +-
.../messagepad/reducers/activeId.js | 18 +-
.../messagepad/reducers/dialogues.js | 94 ++--
.../components/messagepad/reducers/inputs.js | 66 +--
.../messagepad/reducers/isPosting.js | 84 ++--
.../components/monaco/languages/yaml.ts | 7 +-
.../components/problemconfig/BasicForm.tsx | 69 +++
.../problemconfig/ProblemConfigEditor.tsx | 40 +-
.../problemconfig/ProblemConfigForm.tsx | 444 ++----------------
.../components/problemconfig/ProblemType.tsx | 129 +++++
.../components/problemconfig/SubtaskTable.tsx | 139 ++++++
.../problemconfig/TestCasesTable.tsx | 123 +++++
.../problemconfig/reducer/config.ts | 133 +++---
.../problemconfig/reducer/testdata.ts | 24 +-
.../components/scratchpad/reducers/records.ts | 46 +-
.../components/scratchpad/reducers/ui.ts | 174 +++----
packages/ui-default/locales/zh.yaml | 12 +-
packages/ui-default/package.json | 3 +-
packages/ui-default/pages/home_files.page.js | 8 +-
.../ui-default/pages/homework_main.page.js | 20 +-
.../ui-default/pages/problem_config.page.styl | 52 +-
.../ui-default/pages/problem_config.page.tsx | 61 +--
.../ui-default/pages/problem_edit.page.js | 8 +-
.../ui-default/pages/problem_files.page.js | 8 +-
.../ui-default/templates/contest_detail.html | 14 +-
.../ui-default/templates/problem_detail.html | 20 +-
packages/ui-default/theme/dark.styl | 2 +-
packages/ui-default/utils/i18n.js | 1 +
packages/utils/lib/common.ts | 137 ++----
37 files changed, 1104 insertions(+), 948 deletions(-)
create mode 100644 packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx
create mode 100644 packages/ui-default/components/problemconfig/BasicForm.tsx
create mode 100644 packages/ui-default/components/problemconfig/ProblemType.tsx
create mode 100644 packages/ui-default/components/problemconfig/SubtaskTable.tsx
create mode 100644 packages/ui-default/components/problemconfig/TestCasesTable.tsx
diff --git a/package.json b/package.json
index d00626301..c42cf827a 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"debug": "node --trace-warnings --async-stack-traces --trace-deprecation node_modules/hydrooj/bin/hydrooj --debug --template",
"start:all": "node build/start",
"start": "node_modules/.bin/hydrooj",
- "postinstall": "node build/hack && node build/prepare.js && rm -rf node_modules/esbuild-loader/node_modules/esbuild"
+ "postinstall": "node build/hack && node build/prepare.js"
},
"version": "1.0.0",
"license": "AGPL-3.0-only",
@@ -64,5 +64,8 @@
"supertest": "^6.2.2",
"typedoc": "^0.22.13",
"typescript": "4.6.2"
+ },
+ "resolutions": {
+ "esbuild": "0.14.3"
}
}
diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts
index 6edf22aa3..d81ab2e17 100644
--- a/packages/hydrojudge/src/cases.ts
+++ b/packages/hydrojudge/src/cases.ts
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import yaml from 'js-yaml';
import { max, sum } from 'lodash';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
-import { readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
+import { readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import { changeErrorType } from '@hydrooj/utils/lib/utils';
import { getConfig } from './config';
import { FormatError, SystemError } from './error';
@@ -28,8 +28,7 @@ async function readAutoCases(folder: string, { next }, cfg, rst) {
const outputs = await fs.readdir(path.resolve(folder, 'output'));
files.push(...outputs.map((i) => `output/${i}`));
}
- let result = await readCasesFromFiles(files, checkFile, cfg);
- if (!result.count) result = await readSubtasksFromFiles(files, checkFile, cfg, rst);
+ const result = readSubtasksFromFiles(files, checkFile, cfg, rst);
Object.assign(config, result);
if (cfg.isSelfSubmission) next({ message: { message: 'Found {0} testcases.', params: [config.count] } });
} catch (e) {
@@ -42,11 +41,11 @@ function isValidConfig(config) {
if (config.count > (getConfig('testcases_max') || 100)) {
throw new FormatError('Too many testcases. Cancelled.');
}
- const time = sum(config.subtasks.map((subtask) => subtask.time * subtask.cases.length));
+ const time = sum(config.subtasks.map((subtask) => sum(subtask.cases.map((c) => c.time))));
if (time > (getConfig('total_time_limit') || 60) * 1000) {
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
}
- const memMax = max(config.subtasks.map((subtask) => subtask.memory));
+ const memMax = max(config.subtasks.map((subtask) => max(subtask.cases.map((c) => c.memory))));
if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max');
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
throw new FormatError('You did not specify a checker.');
@@ -57,18 +56,18 @@ export default async function readCases(folder: string, cfg: Record
const iniConfig = path.resolve(folder, 'config.ini');
const yamlConfig = path.resolve(folder, 'config.yaml');
const ymlConfig = path.resolve(folder, 'config.yml');
- let config;
+ const config = { ...cfg };
if (fs.existsSync(yamlConfig)) {
- config = { ...cfg, ...yaml.load(fs.readFileSync(yamlConfig).toString()) as object };
+ Object.assign(config, yaml.load(fs.readFileSync(yamlConfig).toString()));
} else if (fs.existsSync(ymlConfig)) {
- config = { ...cfg, ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object };
+ Object.assign(config, yaml.load(fs.readFileSync(ymlConfig).toString()));
} else if (fs.existsSync(iniConfig)) {
try {
- config = { ...cfg, ...convertIniConfig(fs.readFileSync(iniConfig).toString()) };
+ Object.assign(config, convertIniConfig(fs.readFileSync(iniConfig).toString()));
} catch (e) {
throw changeErrorType(e, FormatError);
}
- } else config = cfg;
+ }
let result;
try {
result = await readYamlCases(config, ensureFile(folder));
diff --git a/packages/hydrooj/src/entry/common.ts b/packages/hydrooj/src/entry/common.ts
index 5a15bb04a..4cfb71851 100644
--- a/packages/hydrooj/src/entry/common.ts
+++ b/packages/hydrooj/src/entry/common.ts
@@ -178,6 +178,7 @@ export async function service(pending: string[], fail: string[]) {
}
}
for (const key in global.Hydro.service) {
+ if (key === 'server') continue;
const srv = global.Hydro.service[key];
if (!srv.started && srv.start) await srv.start();
}
diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts
index f6ea50295..f2772bf57 100644
--- a/packages/hydrooj/src/interface.ts
+++ b/packages/hydrooj/src/interface.ts
@@ -127,10 +127,11 @@ export interface FileInfo {
}
export interface TestCaseConfig {
- input: string,
- output: string,
- time?: number,
- memory?: number,
+ input: string;
+ output: string;
+ time?: number;
+ memory?: number;
+ score?: number;
}
export enum ProblemType {
diff --git a/packages/ui-default/.eslintrc.js b/packages/ui-default/.eslintrc.js
index 09bf98b73..20983ca61 100644
--- a/packages/ui-default/.eslintrc.js
+++ b/packages/ui-default/.eslintrc.js
@@ -81,7 +81,7 @@ module.exports = {
indent: [
'error',
2,
- { SwitchCase: 0 },
+ { SwitchCase: 1 },
],
'max-len': ['error', 150],
quotes: 'warn',
@@ -115,6 +115,7 @@ module.exports = {
'no-underscore-dangle': 'off',
'no-await-in-loop': 'off',
'no-lonely-if': 'off',
+ 'no-nested-ternary': 'off',
'no-param-reassign': 'off',
'no-script-url': 'off',
'no-bitwise': 'off',
diff --git a/packages/ui-default/components/autocomplete/autocomplete.page.styl b/packages/ui-default/components/autocomplete/autocomplete.page.styl
index dd4fc3e28..ff150f4fe 100644
--- a/packages/ui-default/components/autocomplete/autocomplete.page.styl
+++ b/packages/ui-default/components/autocomplete/autocomplete.page.styl
@@ -53,7 +53,7 @@
white-space: nowrap
text-overflow: ellipsis
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
font-size: 12px
cursor: pointer
padding: 4px
@@ -78,19 +78,19 @@
&>div
flex-grow: 1
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
color: transparent
&[aria-selected='true']
background-color: #fafafa
font-weight: 600
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
color: #1890ff
&[data-focus='true']
background-color: #e6f7ff
cursor: pointer
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
color: currentColor
diff --git a/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx
index 70d185a00..f8cd5acd8 100644
--- a/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx
+++ b/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx
@@ -33,7 +33,7 @@ CustomSelectAutoComplete.propTypes = {
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
- data: PropTypes.any,
+ data: PropTypes.arrayOf(PropTypes.any),
};
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
diff --git a/packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx
new file mode 100644
index 000000000..4ff950c0c
--- /dev/null
+++ b/packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import AutoComplete from './AutoComplete';
+
+type DefaultProps = {
+ _id: string;
+ name: string;
+} & string;
+
+const FileSelectAutoComplete = React.forwardRef((props, ref) => (
+
+ ref={ref as any}
+ fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
+ queryItems={(query) => props.data.filter((i) => (i.name
+ ? i.name.toLowerCase().match(query.toLowerCase())
+ : i.toString().toLowerCase().match(query.toLowerCase())
+ ))}
+ itemText={(item) => `${item.name || item}`}
+ itemKey={(item) => `${item._id?.toString() || item.name || item}`}
+ renderItem={(item) => item.name || item._id || item} // TODO icon
+ allowEmptyQuery
+ {...props}
+ />
+));
+
+FileSelectAutoComplete.propTypes = {
+ width: PropTypes.string,
+ height: PropTypes.string,
+ listStyle: PropTypes.object,
+ onChange: PropTypes.func.isRequired,
+ multi: PropTypes.bool,
+ selectedKeys: PropTypes.arrayOf(PropTypes.string),
+ allowEmptyQuery: PropTypes.bool,
+ freeSolo: PropTypes.bool,
+ freeSoloConverter: PropTypes.func,
+ data: PropTypes.arrayOf(PropTypes.any),
+};
+
+FileSelectAutoComplete.displayName = 'FileSelectAutoComplete';
+
+FileSelectAutoComplete.defaultProps = {
+ width: '100%',
+ height: 'auto',
+ listStyle: {},
+ multi: false,
+ selectedKeys: [],
+ freeSolo: false,
+ freeSoloConverter: (input) => input,
+};
+
+export default FileSelectAutoComplete;
diff --git a/packages/ui-default/components/hotkey/hotkey.page.js b/packages/ui-default/components/hotkey/hotkey.page.js
index ab174d564..e4537a6df 100644
--- a/packages/ui-default/components/hotkey/hotkey.page.js
+++ b/packages/ui-default/components/hotkey/hotkey.page.js
@@ -38,15 +38,15 @@ function testElementHotkey(hotkey, $element, attr) {
const [defStr, trigger] = singleDef.split(':');
if (isHotkeyMatch(hotkey, defStr)) {
switch (trigger) {
- case 'submit':
- $element.closest('form').trigger('submit');
- break;
- case undefined:
- $element.trigger('click');
- break;
- default:
- $element.trigger(trigger);
- break;
+ case 'submit':
+ $element.closest('form').trigger('submit');
+ break;
+ case undefined:
+ $element.trigger('click');
+ break;
+ default:
+ $element.trigger(trigger);
+ break;
}
}
});
diff --git a/packages/ui-default/components/messagepad/reducers/activeId.js b/packages/ui-default/components/messagepad/reducers/activeId.js
index 036642c14..8af2782c8 100644
--- a/packages/ui-default/components/messagepad/reducers/activeId.js
+++ b/packages/ui-default/components/messagepad/reducers/activeId.js
@@ -21,14 +21,14 @@ function scrollToViewport() {
export default function reducer(state = null, action) {
switch (action.type) {
- case 'DIALOGUES_SWITCH_TO': {
- scrollToViewport();
- return action.payload;
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- return action.meta.dialogueId;
- }
- default:
- return state;
+ case 'DIALOGUES_SWITCH_TO': {
+ scrollToViewport();
+ return action.payload;
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ return action.meta.dialogueId;
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/messagepad/reducers/dialogues.js b/packages/ui-default/components/messagepad/reducers/dialogues.js
index e56009a5a..d6e55d7bf 100644
--- a/packages/ui-default/components/messagepad/reducers/dialogues.js
+++ b/packages/ui-default/components/messagepad/reducers/dialogues.js
@@ -2,52 +2,52 @@ import _ from 'lodash';
export default function reducer(state = {}, action) {
switch (action.type) {
- case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
- const { messages } = action.payload;
- return { ...state, ..._.keyBy(messages, 'udoc._id') };
- }
- case 'DIALOGUES_CREATE': {
- const { user } = action.payload;
- if (state[user._id]) return state;
- return {
- ...state,
- [user._id]: {
- _id: user._id,
- udoc: { ...user },
- messages: [],
- },
- };
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- const id = action.meta.dialogueId;
- const { mdoc } = action.payload;
- return {
- ...state,
- [id]: {
- ...state[id],
- messages: [
- ...state[id].messages,
- mdoc,
- ],
- },
- };
- }
- case 'DIALOGUES_MESSAGE_PUSH': {
- const { mdoc, udoc } = action.payload;
- const to = mdoc.from === UserContext._id ? mdoc.to : mdoc.from;
- return {
- ...state,
- [to]: {
- ...state[to] || {},
- udoc,
- messages: [
- ...state[to]?.messages || [],
- mdoc,
- ],
- },
- };
- }
- default:
- return state;
+ case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
+ const { messages } = action.payload;
+ return { ...state, ..._.keyBy(messages, 'udoc._id') };
+ }
+ case 'DIALOGUES_CREATE': {
+ const { user } = action.payload;
+ if (state[user._id]) return state;
+ return {
+ ...state,
+ [user._id]: {
+ _id: user._id,
+ udoc: { ...user },
+ messages: [],
+ },
+ };
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ const id = action.meta.dialogueId;
+ const { mdoc } = action.payload;
+ return {
+ ...state,
+ [id]: {
+ ...state[id],
+ messages: [
+ ...state[id].messages,
+ mdoc,
+ ],
+ },
+ };
+ }
+ case 'DIALOGUES_MESSAGE_PUSH': {
+ const { mdoc, udoc } = action.payload;
+ const to = mdoc.from === UserContext._id ? mdoc.to : mdoc.from;
+ return {
+ ...state,
+ [to]: {
+ ...state[to] || {},
+ udoc,
+ messages: [
+ ...state[to]?.messages || [],
+ mdoc,
+ ],
+ },
+ };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/messagepad/reducers/inputs.js b/packages/ui-default/components/messagepad/reducers/inputs.js
index a0856a741..a188073e6 100644
--- a/packages/ui-default/components/messagepad/reducers/inputs.js
+++ b/packages/ui-default/components/messagepad/reducers/inputs.js
@@ -2,42 +2,42 @@ import _ from 'lodash';
export default function reducer(state = {}, action) {
switch (action.type) {
- case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
- const dialogues = action.payload.messages;
- return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, ''])) };
- }
- case 'DIALOGUES_CREATE': {
- const { user } = action.payload;
- return {
- ...state,
- [user._id]: '',
- };
- }
- case 'DIALOGUES_INPUT_CHANGED': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: action.payload,
- };
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: '',
- };
- }
- case 'DIALOGUES_MESSAGE_PUSH': {
- const { udoc } = action.payload;
- if (!state[udoc._id]) {
+ case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
+ const dialogues = action.payload.messages;
+ return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, ''])) };
+ }
+ case 'DIALOGUES_CREATE': {
+ const { user } = action.payload;
return {
...state,
- [udoc._id]: '',
+ [user._id]: '',
};
}
- return state;
- }
- default:
- return state;
+ case 'DIALOGUES_INPUT_CHANGED': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: action.payload,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: '',
+ };
+ }
+ case 'DIALOGUES_MESSAGE_PUSH': {
+ const { udoc } = action.payload;
+ if (!state[udoc._id]) {
+ return {
+ ...state,
+ [udoc._id]: '',
+ };
+ }
+ return state;
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/messagepad/reducers/isPosting.js b/packages/ui-default/components/messagepad/reducers/isPosting.js
index 4bb841122..ecd04cbff 100644
--- a/packages/ui-default/components/messagepad/reducers/isPosting.js
+++ b/packages/ui-default/components/messagepad/reducers/isPosting.js
@@ -3,47 +3,47 @@ import Notification from 'vj/components/notification';
export default function reducer(state = {}, action) {
switch (action.type) {
- case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
- const dialogues = action.payload.messages;
- return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, false])) };
- }
- case 'DIALOGUES_CREATE': {
- const { user } = action.payload;
- return {
- ...state,
- [user._id]: false,
- };
- }
- case 'DIALOGUES_POST_SEND_PENDING': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: true,
- };
- }
- case 'DIALOGUES_POST_SEND_REJECTED': {
- Notification.error(action.payload.message);
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: false,
- };
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: false,
- };
- }
- case 'DIALOGUES_MESSAGE_PUSH': {
- const { udoc } = action.payload;
- return {
- ...state,
- [udoc._id]: false,
- };
- }
- default:
- return state;
+ case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
+ const dialogues = action.payload.messages;
+ return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, false])) };
+ }
+ case 'DIALOGUES_CREATE': {
+ const { user } = action.payload;
+ return {
+ ...state,
+ [user._id]: false,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_PENDING': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: true,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_REJECTED': {
+ Notification.error(action.payload.message);
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: false,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: false,
+ };
+ }
+ case 'DIALOGUES_MESSAGE_PUSH': {
+ const { udoc } = action.payload;
+ return {
+ ...state,
+ [udoc._id]: false,
+ };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/monaco/languages/yaml.ts b/packages/ui-default/components/monaco/languages/yaml.ts
index 544d0fd85..783524435 100644
--- a/packages/ui-default/components/monaco/languages/yaml.ts
+++ b/packages/ui-default/components/monaco/languages/yaml.ts
@@ -8,6 +8,9 @@ const problemConfigSchemaDef = {
properties: {
input: { type: 'string' },
output: { type: 'string' },
+ time: { $ref: '#/def/time' },
+ memory: { $ref: '#/def/memory' },
+ score: { $ref: '#/def/score', description: 'score' },
},
required: ['input'],
additionalProperties: false,
@@ -28,7 +31,7 @@ const problemConfigSchemaDef = {
additionalProperties: false,
},
time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' },
- memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kmg])b?$' },
+ memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kKmMgG])[bB]?$' },
score: { type: 'integer', maximum: 100, minimum: 1 },
};
@@ -37,7 +40,7 @@ const problemConfigSchema = {
type: 'object',
def: problemConfigSchemaDef,
properties: {
- redirect: { type: 'string', pattern: '[0-9a-z_-]+\\/[0-9]+' },
+ redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' },
key: { type: 'string', pattern: '[0-9a-f]{32}' },
type: { enum: ['default', 'interactive', 'submit_answer', 'objective', 'remote_judge'] },
subType: { type: 'string' },
diff --git a/packages/ui-default/components/problemconfig/BasicForm.tsx b/packages/ui-default/components/problemconfig/BasicForm.tsx
new file mode 100644
index 000000000..0d34abcaf
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/BasicForm.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import type { RootState } from './reducer/index';
+import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
+
+export function FormItem({
+ columns, label, children, helpText = '', disableLabel = false, ...props
+}) {
+ return (
+
+
+
+ );
+}
+
+type KeyType = {
+ [Q in keyof K]: K[Q] extends T ? Q : never;
+}[keyof K];
+
+export function ManagedInput({ placeholder, formKey }: { placeholder?: string, formKey: KeyType }) {
+ const value = useSelector((state: RootState) => state.config[formKey]);
+ const dispatch = useDispatch();
+ return (
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
+ }}
+ className="textbox"
+ />
+ );
+}
+
+export function ManagedSelect({ options, formKey }: { options: string[], formKey: KeyType }) {
+ const value = useSelector((state: RootState) => state.config[formKey]);
+ const dispatch = useDispatch();
+ return (
+
+ );
+}
+
+export function SingleFileSelect({ formKey }: { formKey: KeyType }) {
+ const value = useSelector((state: RootState) => state.config[formKey]);
+ const Files = useSelector((state: RootState) => state.testdata);
+ const dispatch = useDispatch();
+ return (
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: val })}
+ />
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx
index e3debd546..68774fc7c 100644
--- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx
+++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx
@@ -3,6 +3,7 @@ import type { editor } from 'monaco-editor';
import { connect } from 'react-redux';
import { load } from 'vj/components/monaco/loader';
import Editor from 'vj/components/editor';
+import { diffLines } from 'diff';
import yaml from 'js-yaml';
import type { ProblemConfigFile, TestCaseConfig } from 'hydrooj/src/interface';
@@ -41,9 +42,10 @@ function configYamlFormat(config: ProblemConfigFile) {
const formatConfig: ProblemConfigFile = {};
configKey.forEach((key) => {
if (config[key] !== undefined) {
+ if (key === 'subType' && ['single', 'multi'].includes(config[key]) && config.type !== 'submit_answer') return;
if (key === 'checker_type' && config.type !== 'default') return;
if (key === 'checker'
- && (['default', 'strict'].includes(formatConfig.checker_type) || formatConfig.checker_type === undefined)) return;
+ && (['default', 'strict'].includes(formatConfig.checker_type) || formatConfig.checker_type === undefined)) return;
if (key === 'interactor' && config.type !== 'interactive') return;
if (key === 'subtasks') {
formatConfig[key] = [];
@@ -71,6 +73,11 @@ function configYamlFormat(config: ProblemConfigFile) {
} else formatConfig[key] = config[key];
}
});
+ if (formatConfig.type === 'objective') {
+ Object.keys(formatConfig).filter((i) => !['type', 'outputs'].includes(i)).forEach((i) => delete formatConfig[i]);
+ formatConfig.outputs = formatConfig.outputs || [];
+ }
+ Object.keys(formatConfig).filter((i) => i.startsWith('__')).forEach((i) => delete formatConfig[i]);
return formatConfig;
}
@@ -87,7 +94,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e
async componentDidMount() {
const { monaco } = await load(['yaml']);
const uri = monaco.Uri.parse('hydro://problem/file/config.yaml');
- this.model = monaco.editor.createModel(yaml.dump(this.props.config), 'yaml', uri);
+ this.model = monaco.editor.createModel(yaml.dump(configYamlFormat(this.props.config)), 'yaml', uri);
this.vjEditor = Editor.getOrConstruct($(this.containerElement), {
language: 'yaml',
model: this.model,
@@ -101,17 +108,28 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e
}
componentDidUpdate(prevProps) {
- if (this.__preventUpdate) return;
+ if (this.__preventUpdate || !this.model) return;
if (yaml.dump(prevProps.config) !== yaml.dump(this.props.config)) {
this.__preventFormat = true;
- this.model?.pushEditOperations(
- [],
- [{
- range: this.model.getFullModelRange(),
- text: yaml.dump(configYamlFormat(this.props.config)),
- }],
- undefined,
- );
+ const curValue = this.model.getValue();
+ const diff = diffLines(curValue, yaml.dump(configYamlFormat(this.props.config)));
+ const ops = [];
+ let cursor = 1;
+ for (const line of diff) {
+ if (line.added) {
+ let range = this.model.getFullModelRange();
+ range = range.setStartPosition(cursor, 0);
+ range = range.setEndPosition(cursor, 0);
+ ops.push({ range, text: line.value });
+ } else if (line.removed) {
+ let range = this.model.getFullModelRange();
+ range = range.setStartPosition(cursor, 0);
+ cursor += line.count;
+ range = range.setEndPosition(cursor, 0);
+ ops.push({ range, text: '' });
+ } else cursor += line.count;
+ }
+ this.model.pushEditOperations([], ops, undefined);
this.__preventFormat = false;
}
}
diff --git a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
index f70d4b410..afab7fac0 100644
--- a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
+++ b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
@@ -1,153 +1,36 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
-import { isEqual } from 'lodash';
-import { Switch, Tab, Tabs } from '@blueprintjs/core';
-import type { SubtaskConfig, TestCaseConfig } from 'hydrooj/src/interface';
+import {
+ Card, Switch, Tab, Tabs, InputGroup, Tag,
+} from '@blueprintjs/core';
import type { RootState } from './reducer/index';
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
+import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
+import { FormItem } from './BasicForm';
+import ProblemType from './ProblemType';
+import { TaskConfig } from './SubtaskTable';
-const SelectValue = {
- type: ['default', 'interactive', 'submit_answer', 'objective'],
- checker_type: ['default', 'lemon', 'syzoj', 'hustoj', 'testlib', 'qduoj'],
- task_type: ['min', 'max', 'sum'],
-};
-
-const eq = (a: TestCaseConfig, b: TestCaseConfig) => isEqual(a, b);
-const eqId = (a: SubtaskConfig[], b: SubtaskConfig[]) => {
- if (a.length !== b.length) return true;
- for (let i = 0; i < a.length; i++) {
- if (a[i].id !== b[i].id) return true;
- }
- return false;
-};
-
-function FormItem({
- columns, label, children, helpText = '', disableLabel = false, ...props
-}) {
- return (
-
-
-
- );
-}
-
-type KeyType = {
- [Q in keyof K]: K[Q] extends T ? Q : never;
-}[keyof K];
-
-function ManagedInput({ placeholder, formKey }: { placeholder: string, formKey: KeyType }) {
- const value = useSelector((state: RootState) => state.config[formKey]);
+function FileIOConfig() {
+ const filename = useSelector((state: RootState) => state.config.filename);
const dispatch = useDispatch();
return (
- {
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
- }}
- className="textbox"
- />
- );
-}
-
-function ManagedSelect({ placeholder, formKey }: { placeholder: string, formKey: KeyType }) {
- const value = useSelector((state: RootState) => state.config[formKey]);
- const dispatch = useDispatch();
- return (
-
- );
-}
-
-function SingleFileSelect({ formKey }: { formKey: KeyType }) {
- const value = useSelector((state: RootState) => state.config[formKey]);
- const Files = useSelector((state: RootState) => state.testdata);
- const dispatch = useDispatch();
- return (
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: val })}
- />
- );
-}
-
-function BasicInfo() {
- const Type = useSelector((state: RootState) => state.config.type);
- const checkerType = useSelector((state: RootState) => state.config.checker_type);
- const dispatch = useDispatch();
- return (
- <>
-
-
-
-
-
-
-
- {
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value });
- }}
- renderActiveTabPanelOnly
- >
- {i18n('CheckerType')}
-
- {
- SelectValue.checker_type.map((i) => (
-
- {['default', 'strict'].includes(i)
- ? (
- {
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
- }
- }
- />
- )
- : (
-
- )}
-
- )}
- />
- ))
- }
-
-
-
-
-
- >
+
+
+
+
+ .in/.out}
+ value={filename || ''}
+ onChange={(ev) => {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value });
+ }}
+ fill
+ />
+
+
+
+
);
}
@@ -166,7 +49,7 @@ function ExtraFilesConfig() {
id="user_extra_files"
title={i18n('user_extra_files')}
panel={(
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'user_extra_files', value: val.split(',') })}
@@ -178,7 +61,7 @@ function ExtraFilesConfig() {
id="judge_extra_files"
title={i18n('judge_extra_files')}
panel={(
- dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'judge_extra_files', value: val.split(',') })}
@@ -192,261 +75,6 @@ function ExtraFilesConfig() {
);
}
-function CasesSubCasesTable({ index, subindex }) {
- const subcases = useSelector((state: RootState) => (index === -1
- ? state.config.cases[subindex] : state.config.subtasks[index].cases[subindex]), eq);
- const Files = useSelector((state: RootState) => state.testdata);
- const dispatch = useDispatch();
- const dispatcher = (casesKey: string): React.ChangeEventHandler => (ev) => {
- dispatch({
- type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
- id: index,
- key: 'cases-edit',
- casesId: subindex,
- casesKey,
- value: ev.currentTarget.value,
- });
- };
- const dispatcher1 = (casesKey: string): React.ChangeEventHandler => (val) => {
- dispatch({
- type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
- id: index,
- key: 'cases-edit',
- casesId: subindex,
- casesKey,
- value: val,
- });
- };
- return (
-
- {
- index === -1 && (
- <>
- |
-
- |
-
-
- |
- >
- )
- }
-
-
- |
-
-
- |
-
- dispatch({
- type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
- })}
- >
-
- |
-
- );
-}
-
-function CasesTable({ index }) {
- const casesLength = useSelector((state: RootState) => (index === -1 ? state.config.cases?.length : state.config.subtasks[index].cases?.length));
- const dispatch = useDispatch();
- return (
-
- );
-}
-
-function SubtasksIds({ index }) {
- const subtasks = useSelector((state: RootState) => state.config.subtasks, eqId);
- const subtaskIf = useSelector((state: RootState) => state.config.subtasks[index].if);
- const dispatch = useDispatch();
- const subtasksIds = subtasks.map((i) => i.id).filter((i) => i !== undefined);
- return (
-
- | if |
-
- `${item}`}
- onChange={(val) => dispatch({
- type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
- })}
- multi
- />
- |
-
- );
-}
-
-function SubtasksTable({ index }) {
- const subtask = (key: string) => useSelector((state: RootState) => state.config.subtasks[index][key]);
- const dispatch = useDispatch();
- const dispatcher = (key: string): React.ChangeEventHandler => (ev) => {
- dispatch({
- type: 'CONFIG_SUBTASK_UPDATE', id: index, key, value: ev.currentTarget.value,
- });
- };
- return (
- <>
- Subtasks #{index + 1}
- { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' }); }}>
- { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'delete' }); }}
- >
-
-
-
- >
- );
-}
-
-function TaskConfig({ onAutoLoad }) {
- const subtasksLength = useSelector((state: RootState) => state.config.subtasks?.length);
- const casesLength = useSelector((state: RootState) => state.config.cases?.length);
- const dispatch = useDispatch();
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {casesLength && dispatch({ type: 'CONFIG_SUBTASKS_SWITCH', value: true })} />}
- {
- subtasksLength || casesLength
- ? subtasksLength && [...Array(subtasksLength).keys()].map((i) => )
- || casesLength && ()
- : (
- onAutoLoad()}>
- {i18n('Auto Read Tasks')}
-
- )
- }
-
-
-
- );
-}
-
function LangConfig() {
const [showTab, setshowTab] = useState(false);
const langs = useSelector((state: RootState) => state.config.langs) || [];
@@ -479,13 +107,19 @@ function LangConfig() {
);
}
-export default function ProblemConfigForm({ onAutoLoad }) {
+export default function ProblemConfigForm() {
+ const Type = useSelector((state: RootState) => state.config.type);
return (
-
-
-
-
-
+
+
+ {Type === 'default' &&
}
+ {!['submit_answer', 'objective'].includes(Type) && (
+ <>
+
+
+
+ >
+ )}
);
}
diff --git a/packages/ui-default/components/problemconfig/ProblemType.tsx b/packages/ui-default/components/problemconfig/ProblemType.tsx
new file mode 100644
index 000000000..f81c88fe1
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/ProblemType.tsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import {
+ Card, Switch, Tab, Tabs,
+} from '@blueprintjs/core';
+import type { RootState } from './reducer/index';
+import { FormItem, ManagedSelect, SingleFileSelect } from './BasicForm';
+
+export default function ProblemType() {
+ const Type = useSelector((state: RootState) => state.config.type);
+ const checkerType = useSelector((state: RootState) => state.config.checker_type);
+ const filename = useSelector((state: RootState) => state.config.filename);
+ const subType = useSelector((state: RootState) => state.config.subType);
+ const dispatch = useDispatch();
+ const dispatcher = (base) => (value) => dispatch({ ...base, value });
+ return (
+
+
+
+ {i18n('Problem Type')}
+
+
+ {i18n('CheckerType')}
+
+
+
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
+ }}
+ />
+
+
+ )}
+ />
+ {['testlib', 'other'].map((i) => (
+
+ {i === 'other' && (
+
+
+
+ )}
+
+
+
+
+ )}
+ />
+ ))}
+
+ )}
+ />
+
+
+
+
+
+ )}
+ />
+
+
+ {
+ 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"
+ />
+
+
+ )}
+ />
+ 暂不支持快捷配置此类题目。请参照文档操作。
)}
+ />
+
+
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/SubtaskTable.tsx b/packages/ui-default/components/problemconfig/SubtaskTable.tsx
new file mode 100644
index 000000000..0f3e68148
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/SubtaskTable.tsx
@@ -0,0 +1,139 @@
+import { Card, NumericInput, Tag } from '@blueprintjs/core';
+import { pick, isEqual } from 'lodash';
+import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { SubtaskConfig } from 'hydrooj/src/interface';
+import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
+import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
+import { FormItem, ManagedInput } from './BasicForm';
+import { RootState } from './reducer';
+import { CasesTable } from './TestCasesTable';
+
+const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
+
+export function SubtasksIds({ index }) {
+ const ids = useSelector((state: RootState) => state.config.subtasks.map((i) => i.id).filter((i) => i !== undefined).join(','));
+ const subtaskIf = useSelector((state: RootState) => state.config.subtasks[index].if);
+ const dispatch = useDispatch();
+ return (
+
+ | if |
+
+ i.length)}
+ selectedKeys={subtaskIf?.map(String) || []}
+ onChange={(val) => dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
+ })}
+ multi
+ />
+ |
+
+ );
+}
+
+export function SubtasksTable({ index }) {
+ const keys = ['id', 'type', 'score', 'time', 'memory'];
+ const subtask = useSelector((state: RootState) => pick(state.config.subtasks[index], keys), eq);
+ const defaultTime = useSelector((state: RootState) => state.config.time);
+ const defaultMemory = useSelector((state: RootState) => state.config.memory);
+ const dispatch = useDispatch();
+ const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent | number) => {
+ let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
+ if (value === 0) value = '';
+ if (value && suffix) value += suffix;
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE', id: index, key, value,
+ });
+ };
+ return (
+
+ Subtasks #{index + 1}
+ dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' })}>
+ dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'delete' })}
+ >
+
+
+
+
+
+ );
+}
+
+export function TaskConfig() {
+ const len = useSelector((state: RootState) => state.config.subtasks?.length);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {len && [...Array(len).keys()].map((i) => )}
+
+
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/TestCasesTable.tsx b/packages/ui-default/components/problemconfig/TestCasesTable.tsx
new file mode 100644
index 000000000..23c2ff287
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/TestCasesTable.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import { isEqual } from 'lodash';
+import { Tag, NumericInput } from '@blueprintjs/core';
+import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
+import type { SubtaskConfig } from 'hydrooj/src/interface';
+import type { RootState } from './reducer/index';
+import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
+
+const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
+const eqArr = (a: any[], b: any[]) => isEqual(a, b);
+
+export function TestCaseEntry({ index, subindex }) {
+ const testcase = useSelector((state: RootState) => state.config.subtasks[index].cases[subindex], eq);
+ const Files = useSelector((state: RootState) => state.testdata, eqArr);
+ const defaultTime = useSelector((state: RootState) => state.config.subtasks[index].time || state.config.time);
+ const defaultMemory = useSelector((state: RootState) => state.config.subtasks[index].memory || state.config.memory);
+ const dispatch = useDispatch();
+ const dispatcher = (casesKey: string, valueSuffix = '') => (ev: React.ChangeEvent | number) => {
+ let value = typeof ev !== 'object' ? ev : ev.currentTarget.value;
+ if (value === 0) value = '';
+ if (valueSuffix && value) value += valueSuffix;
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE',
+ id: index,
+ key: 'cases-edit',
+ casesId: subindex,
+ casesKey,
+ value,
+ });
+ };
+ return (
+
+ |
+ ms}
+ value={testcase.time ? parseTimeMS(testcase.time).toString() : ''}
+ placeholder={parseTimeMS(defaultTime || '1000ms').toString()}
+ onValueChange={dispatcher('time', 'ms')}
+ buttonPosition="none"
+ fill
+ />
+ |
+
+ MB}
+ value={testcase.memory ? parseMemoryMB(testcase.memory).toString() : ''}
+ placeholder={parseMemoryMB(defaultMemory || '256m').toString()}
+ onValueChange={dispatcher('memory', 'MB')}
+ buttonPosition="none"
+ fill
+ />
+ |
+ {['input', 'output'].map((t) => (
+
+
+ |
+ ))}
+
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
+ })}
+ >
+
+ |
+
+ );
+}
+
+export function CasesTable({ index }) {
+ const len = useSelector((state: RootState) => state.config.subtasks[index].cases?.length);
+ const dispatch = useDispatch();
+ return (
+
+
+
+ | {i18n('Time')} |
+ {i18n('Memory')} |
+ {i18n('Input')} |
+ {i18n('Output')} |
+
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE',
+ id: index,
+ key: 'cases-add',
+ value: { input: '', output: '' },
+ })}
+ >
+
+ |
+
+
+
+
+ | {i18n('Time')} |
+ {i18n('Memory')} |
+ {i18n('Input')} |
+ {i18n('Output')} |
+
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE',
+ id: index,
+ key: 'cases-add',
+ value: { input: '', output: '' },
+ })}
+ >
+
+ |
+
+ {[...Array(len).keys()].map((i) => )}
+
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/reducer/config.ts b/packages/ui-default/components/problemconfig/reducer/config.ts
index 011bd0ff4..eec223d6a 100644
--- a/packages/ui-default/components/problemconfig/reducer/config.ts
+++ b/packages/ui-default/components/problemconfig/reducer/config.ts
@@ -2,84 +2,69 @@ import yaml from 'js-yaml';
import type { ProblemConfigFile } from 'hydrooj/src/interface';
import { cloneDeep } from 'lodash';
-export default function reducer(state = { type: 'default' } as ProblemConfigFile, action): ProblemConfigFile {
+type State = ProblemConfigFile & { __loaded: boolean };
+
+export default function reducer(state = { type: 'default', __loaded: false } as State, action): State {
switch (action.type) {
- case 'CONFIG_LOAD_FULFILLED': {
- // TODO set yaml schema
- return { ...state, ...yaml.load(action.payload.config) as object };
- }
- case 'CONFIG_FORM_UPDATE': {
- const next = { ...state, [action.key]: action.value };
- if (action.key === 'score' && action.value) next.score = +next.score;
- if (!action.value || (typeof action.value === 'object' && !action.value.join(''))) delete next[action.key];
- return next;
- }
- case 'CONFIG_CODE_UPDATE': {
- try {
- return { ...state, ...yaml.load(action.payload) as object };
- } catch {
- return state;
+ case 'CONFIG_LOAD_FULFILLED': {
+ return { ...state, ...yaml.load(action.payload.config) as object, __loaded: true };
}
- }
- case 'CONFIG_AUTOCASES_UPDATE': {
- const next = { ...state };
- const autocases = action.value;
- if (autocases.subtasks.length === 0) next.cases = [];
- else if (autocases.subtasks.length === 1) {
- next.score = autocases.subtasks[0].score;
- next.cases = [];
- autocases.subtasks[0].cases.map((i) => next.cases.push({
- time: autocases.subtasks[0].time, memory: autocases.subtasks[0].memory, input: i.input, output: i.output,
- }));
- } else {
- next.subtasks = autocases.subtasks.map((subtask) => (
- { ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
+ case 'CONFIG_FORM_UPDATE': {
+ const next = { ...state, [action.key]: action.value };
+ if (action.key === 'score' && action.value) next.score = +next.score;
+ if (action.key === 'checker_type' && action.value === 'other') next.checker_type = 'syzoj';
+ if (!action.value || (typeof action.value === 'object' && !action.value.join(''))) delete next[action.key];
+ return next;
}
- return next;
- }
- case 'CONFIG_SUBTASKS_SWITCH': {
- const next = { ...state };
- next.subtasks = next.cases.map((k, v) => ({
- id: v, time: k.time, memory: k.memory, cases: [{ input: k.input, output: k.output }],
- }));
- if (!next.subtasks.length) next.subtasks.push({ id: 0 });
- delete next.cases;
- return next;
- }
- case 'CONFIG_CASES_UPDATE': {
- if (['time', 'memory'].includes(action.casesKey) && action.value !== '') action.value = +action.value;
- if (action.key === 'cases-add') return { ...state, cases: [...state.cases, action.value] };
- if (action.key === 'cases-delete') {
- return { ...state, cases: state.cases.filter((k, v) => v !== action.value) };
- }
- const cases = cloneDeep(state.cases);
- cases[action.casesId][action.casesKey] = action.value;
- if (action.value === '') delete cases[action.casesId][action.casesKey];
- return { ...state, cases };
- }
- case 'CONFIG_SUBTASK_UPDATE': {
- const subtasks = cloneDeep(state.subtasks);
- const subsubtasks = cloneDeep(state.subtasks[action.id]);
- if (action.value !== ''
- && (['time', 'memory'].includes(action.casesKey) || ['time', 'memory', 'score', 'id'].includes(action.key))
- ) action.value = +action.value;
- if (action.key === 'if' && action.value.join('') !== '') action.value = action.value.map((i) => +i);
- if (action.key.split('-')[0] === 'cases') {
- if (action.key === 'cases-add') subsubtasks.cases.push(action.value);
- else if (action.key === 'cases-edit') subsubtasks.cases[action.casesId][action.casesKey] = action.value;
- else if (action.key === 'cases-delete') {
- subsubtasks.cases = subsubtasks.cases.filter((k, v) => v !== action.value);
+ case 'CONFIG_CODE_UPDATE': {
+ try {
+ return { ...state, ...yaml.load(action.payload) as object };
+ } catch {
+ return state;
}
- } else if (action.key === 'add') subtasks.push({ time: 1000, memory: 256, cases: [] });
- else if (action.key === 'delete') delete subtasks[action.key];
- else {
- if (action.value === '' || (action.key === 'if' && action.value.join('') === '')) delete subsubtasks[action.key];
- else subsubtasks[action.key] = action.value;
}
- if (action.key !== 'delete') subtasks[action.id] = subsubtasks;
- return { ...state, subtasks };
- }
- default:
- return state;
+ case 'CONFIG_AUTOCASES_UPDATE': {
+ const next = { ...state };
+ const autocases = action.value;
+ if (autocases.subtasks.length === 0) next.subtasks = [];
+ else {
+ next.subtasks = autocases.subtasks.map((subtask) => (
+ { ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
+ }
+ return next;
+ }
+ case 'CONFIG_SUBTASKS_SWITCH': {
+ const next = { ...state };
+ next.subtasks = next.cases.map((k, v) => ({
+ id: v, time: k.time, memory: k.memory, cases: [{ input: k.input, output: k.output }],
+ }));
+ if (!next.subtasks.length) next.subtasks.push({ id: 0 });
+ delete next.cases;
+ return next;
+ }
+ case 'CONFIG_SUBTASK_UPDATE': {
+ const subtasks = cloneDeep(state.subtasks);
+ const subsubtasks = cloneDeep(state.subtasks[action.id]);
+ if (action.value !== '' && ['score', 'id'].includes(action.key)) action.value = +action.value;
+ if (action.key === 'if' && action.value.join('') !== '') action.value = action.value.map((i) => +i);
+ if (action.key.split('-')[0] === 'cases') {
+ if (action.key === 'cases-add') subsubtasks.cases.push(action.value);
+ else if (action.key === 'cases-edit') {
+ if (action.value === '') delete subsubtasks.cases[action.casesId][action.casesKey];
+ else subsubtasks.cases[action.casesId][action.casesKey] = action.value;
+ } else if (action.key === 'cases-delete') {
+ subsubtasks.cases = subsubtasks.cases.filter((k, v) => v !== action.value);
+ }
+ } else if (action.key === 'add') subtasks.push({ cases: [] });
+ else if (action.key === 'delete') return { ...state, subtasks: subtasks.filter((k, v) => v !== action.id) };
+ else {
+ if (action.value === '' || (action.key === 'if' && action.value.join('') === '')) delete subsubtasks[action.key];
+ else subsubtasks[action.key] = action.value;
+ }
+ subtasks[action.id] = subsubtasks;
+ return { ...state, subtasks };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/problemconfig/reducer/testdata.ts b/packages/ui-default/components/problemconfig/reducer/testdata.ts
index 924766e41..34fb0fb88 100644
--- a/packages/ui-default/components/problemconfig/reducer/testdata.ts
+++ b/packages/ui-default/components/problemconfig/reducer/testdata.ts
@@ -1,16 +1,16 @@
export default function reducer(state = [], action) {
switch (action.type) {
- case 'CONFIG_LOAD_FULFILLED': {
- return state.concat(action.payload.testdata);
- }
- case 'CONFIG_DELETE_TESTDATA': {
- const next = state.filter((i) => i.name !== action.value[0]);
- return next;
- }
- case 'CONFIG_ADD_TESTDATA': {
- return state.concat([action.value]);
- }
- default:
- return state;
+ case 'CONFIG_LOAD_FULFILLED': {
+ return state.concat(action.payload.testdata);
+ }
+ case 'CONFIG_DELETE_TESTDATA': {
+ const next = state.filter((i) => i.name !== action.value[0]);
+ return next;
+ }
+ case 'CONFIG_ADD_TESTDATA': {
+ return state.concat([action.value]);
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/scratchpad/reducers/records.ts b/packages/ui-default/components/scratchpad/reducers/records.ts
index 4c44b09e6..3b8b0d655 100644
--- a/packages/ui-default/components/scratchpad/reducers/records.ts
+++ b/packages/ui-default/components/scratchpad/reducers/records.ts
@@ -5,36 +5,36 @@ export default function reducer(state = {
items: {},
}, action) {
switch (action.type) {
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
- const { rdocs } = action.payload;
- return {
- ...state,
- rows: _.map(rdocs, '_id'),
- items: _.keyBy(rdocs, '_id'),
- };
- }
- case 'SCRATCHPAD_RECORDS_PUSH': {
- const { rdoc } = action.payload;
- const rows = [...state.rows];
- if (!rows.includes(rdoc._id)) {
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
+ const { rdocs } = action.payload;
+ return {
+ ...state,
+ rows: _.map(rdocs, '_id'),
+ items: _.keyBy(rdocs, '_id'),
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_PUSH': {
+ const { rdoc } = action.payload;
+ const rows = [...state.rows];
+ if (!rows.includes(rdoc._id)) {
+ return {
+ ...state,
+ rows: [rdoc._id, ...state.rows],
+ items: {
+ ...state.items,
+ [rdoc._id]: rdoc,
+ },
+ };
+ }
return {
...state,
- rows: [rdoc._id, ...state.rows],
items: {
...state.items,
[rdoc._id]: rdoc,
},
};
}
- return {
- ...state,
- items: {
- ...state.items,
- [rdoc._id]: rdoc,
- },
- };
- }
- default:
- return state;
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/scratchpad/reducers/ui.ts b/packages/ui-default/components/scratchpad/reducers/ui.ts
index 257868f14..ca374c480 100644
--- a/packages/ui-default/components/scratchpad/reducers/ui.ts
+++ b/packages/ui-default/components/scratchpad/reducers/ui.ts
@@ -19,93 +19,93 @@ export default function reducer(state = {
isPosting: false,
}, action) {
switch (action.type) {
- case 'SCRATCHPAD_UI_CHANGE_SIZE': {
- const { uiElement, size } = action.payload;
- return {
- ...state,
- [uiElement]: {
- ...state[uiElement],
- size,
- },
- };
- }
- case 'SCRATCHPAD_UI_SET_VISIBILITY': {
- const { uiElement, visibility } = action.payload;
- localStorage.setItem(`scratchpad/${uiElement}`, visibility.toString());
- return {
- ...state,
- [uiElement]: {
- ...state[uiElement],
- visible: visibility,
- },
- };
- }
- case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
- const { uiElement } = action.payload;
- localStorage.setItem(`scratchpad/${uiElement}`, (!state[uiElement].visible).toString());
- return {
- ...state,
- [uiElement]: {
- ...state[uiElement],
- visible: !state[uiElement].visible,
- },
- };
- }
- case 'SCRATCHPAD_POST_PRETEST_PENDING':
- case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
- return {
- ...state,
- isPosting: true,
- };
- }
- case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
- case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
- Notification.success(i18n('Submitted.'));
- if (action.type === 'SCRATCHPAD_POST_SUBMIT_FULFILLED' && UiContext.canViewRecord) {
- state.records.visible = true;
+ case 'SCRATCHPAD_UI_CHANGE_SIZE': {
+ const { uiElement, size } = action.payload;
+ return {
+ ...state,
+ [uiElement]: {
+ ...state[uiElement],
+ size,
+ },
+ };
}
- return {
- ...state,
- isPosting: false,
- };
- }
- case 'SCRATCHPAD_POST_PRETEST_REJECTED':
- case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
- Notification.error(action.payload.message);
- return {
- ...state,
- isPosting: false,
- };
- }
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
- return {
- ...state,
- records: {
- ...state.records,
- isLoading: true,
- },
- };
- }
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
- Notification.error(action.payload.message);
- return {
- ...state,
- records: {
- ...state.records,
- isLoading: false,
- },
- };
- }
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
- return {
- ...state,
- records: {
- ...state.records,
- isLoading: false,
- },
- };
- }
- default:
- return state;
+ case 'SCRATCHPAD_UI_SET_VISIBILITY': {
+ const { uiElement, visibility } = action.payload;
+ localStorage.setItem(`scratchpad/${uiElement}`, visibility.toString());
+ return {
+ ...state,
+ [uiElement]: {
+ ...state[uiElement],
+ visible: visibility,
+ },
+ };
+ }
+ case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
+ const { uiElement } = action.payload;
+ localStorage.setItem(`scratchpad/${uiElement}`, (!state[uiElement].visible).toString());
+ return {
+ ...state,
+ [uiElement]: {
+ ...state[uiElement],
+ visible: !state[uiElement].visible,
+ },
+ };
+ }
+ case 'SCRATCHPAD_POST_PRETEST_PENDING':
+ case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
+ return {
+ ...state,
+ isPosting: true,
+ };
+ }
+ case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
+ case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
+ Notification.success(i18n('Submitted.'));
+ if (action.type === 'SCRATCHPAD_POST_SUBMIT_FULFILLED' && UiContext.canViewRecord) {
+ state.records.visible = true;
+ }
+ return {
+ ...state,
+ isPosting: false,
+ };
+ }
+ case 'SCRATCHPAD_POST_PRETEST_REJECTED':
+ case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
+ Notification.error(action.payload.message);
+ return {
+ ...state,
+ isPosting: false,
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
+ return {
+ ...state,
+ records: {
+ ...state.records,
+ isLoading: true,
+ },
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
+ Notification.error(action.payload.message);
+ return {
+ ...state,
+ records: {
+ ...state.records,
+ isLoading: false,
+ },
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
+ return {
+ ...state,
+ records: {
+ ...state.records,
+ isLoading: false,
+ },
+ };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml
index 73a82ef28..7aaa46934 100644
--- a/packages/ui-default/locales/zh.yaml
+++ b/packages/ui-default/locales/zh.yaml
@@ -118,8 +118,8 @@ Change Email: 修改电子邮件
Change Password: 修改密码
Change Theme: 切换主题
Check In: 签到
-CheckerType: 比较器类型
Checker: 比较器
+CheckerType: 比较器类型
Chinese: 中文
Choose the background image in your profile page.: 选择您资料页面的背景图片。
Claim homework: 认领作业
@@ -270,7 +270,6 @@ domain_user: 管理用户
domain: 域
Domain: 域
Don't have an account?: 还没有账户?
-Don't ignore space and enter.: 不忽略行末回车与空格。
Done: 已结束
Download All: 下载全部
Download Dataset: 下载数据集
@@ -388,6 +387,7 @@ Host: 主持人
hour(s): 小时
If enabled, source code will be emailed to you after the submission is accepted.: 如果启用,在您通过题目后,源代码会以 Email 的形式发送给您。
If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。
+Ignore trailing space and enter.: 忽略行末空格与文件尾回车。
Images With Size: 指定大小的图片
Images: 图片
Import from: 导入自
@@ -423,15 +423,15 @@ Italic: 斜体
Join: 加入
Judge Config: 评测设置
Judge Status: 评测状态
-judge_playground: 评测游乐场
judge_extra_files: 评测额外文件
+judge_playground: 评测游乐场
Judged At: 评测时间
Judged By: 评测机
Judging Queue: 评测队列
Keep current expiration: 保持当前过期设置
+Langs Config: 语言设置
langs: 语言
Language: 语言
-Langs Config: 语言设置
last active at: 最后活动于
last login at: 最后登录于
Last Submit At: 最后递交于
@@ -594,8 +594,8 @@ Problem Solution List: 题解列表
Problem Solution: 题解列表
Problem Tags Visibility: 题目标签可见性
Problem Tags: 题目标签
-problem_create: 创建题目
problem_config: 评测设置
+problem_create: 创建题目
problem_edit: 编辑题目
problem_import: 导入题目
problem_main: 题库
@@ -768,8 +768,8 @@ SuperUser: 超级管理员
Support: 支持
Switch account: 切换账户
Tags: 标签
-tasks_list: 任务列表
Task Settings: 任务设置
+tasks_list: 任务列表
Technical Information: 技术信息
Temporary session: 临时会话
Terms of Service: 服务条款
diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json
index 5a3abccdd..9df5f5240 100644
--- a/packages/ui-default/package.json
+++ b/packages/ui-default/package.json
@@ -10,7 +10,7 @@
"lint": "eslint ."
},
"devDependencies": {
- "@blueprintjs/core": "^3.54.0",
+ "@blueprintjs/core": "^4.2.1",
"@fontsource/dm-mono": "^4.5.7",
"@fontsource/fira-code": "^4.5.8",
"@fontsource/inconsolata": "^4.5.4",
@@ -37,6 +37,7 @@
"clipboard": "^2.0.10",
"copy-webpack-plugin": "^6.4.1",
"css-loader": "^4.3.0",
+ "diff": "^5.0.0",
"diff-dom": "^4.2.3",
"echarts": "^5.3.2",
"emojis-keywords": "2.0.0",
diff --git a/packages/ui-default/pages/home_files.page.js b/packages/ui-default/pages/home_files.page.js
index 1aca098df..e2f3ee0c2 100644
--- a/packages/ui-default/pages/home_files.page.js
+++ b/packages/ui-default/pages/home_files.page.js
@@ -40,12 +40,12 @@ async function handleClickUpload(files) {
const dialog = new Dialog({
$body: `
-
-
+
-
-
+
`,
});
try {
diff --git a/packages/ui-default/pages/homework_main.page.js b/packages/ui-default/pages/homework_main.page.js
index 4dc9a6d58..842e223b2 100644
--- a/packages/ui-default/pages/homework_main.page.js
+++ b/packages/ui-default/pages/homework_main.page.js
@@ -19,16 +19,16 @@ const page = new NamedPage('homework_main', () => {
calendar.getDom().appendTo('[name="calendar_entry"]');
$('[name="homework_display"]').change((ev) => {
switch (ev.currentTarget.value) {
- case 'calendar':
- $('.homework__list').hide();
- $('[name="calendar_entry"]').show();
- break;
- case 'list':
- $('.homework__list').show();
- $('[name="calendar_entry"]').hide();
- break;
- default:
- throw new Error('Unexpected display parameter');
+ case 'calendar':
+ $('.homework__list').hide();
+ $('[name="calendar_entry"]').show();
+ break;
+ case 'list':
+ $('.homework__list').show();
+ $('[name="calendar_entry"]').hide();
+ break;
+ default:
+ throw new Error('Unexpected display parameter');
}
});
}
diff --git a/packages/ui-default/pages/problem_config.page.styl b/packages/ui-default/pages/problem_config.page.styl
index 304845f7e..0166572f8 100644
--- a/packages/ui-default/pages/problem_config.page.styl
+++ b/packages/ui-default/pages/problem_config.page.styl
@@ -15,9 +15,57 @@
width: 80px
.col--operation
width: 40px
+ text-align: center
.textbox,.select
margin: 0
- .bp3-tab-panel
- margin-top: 5px
\ No newline at end of file
+ .bp4-tab-panel
+ margin-top: 5px
+
+ .problem-config-form
+ font-family: var(--code-font-family)
+
+ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button
+ -webkit-appearance: none !important
+ input[type="number"]
+ -moz-appearance: textfield
+
+ .bp4-tag.bp4-minimal
+ background: transparent
+ .bp4-input-action
+ pointer-events: none
+ .bp4-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
+ background-color: transparent
+ 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
+ input::placeholder
+ color: inherit
+
+ .data-table
+ tr
+ border-top: 0
+ border-bottom: 0
+ height: auto
+ td
+ padding: 0
+ .thead td
+ text-align: center
+ td input, td div
+ background: transparent
+
+ .hover-row.status-bar
+ display: none
\ No newline at end of file
diff --git a/packages/ui-default/pages/problem_config.page.tsx b/packages/ui-default/pages/problem_config.page.tsx
index 51334c9f8..06eeeef47 100644
--- a/packages/ui-default/pages/problem_config.page.tsx
+++ b/packages/ui-default/pages/problem_config.page.tsx
@@ -7,7 +7,9 @@ import yaml from 'js-yaml';
import Notification from 'vj/components/notification';
import Dialog, { ConfirmDialog } from 'vj/components/dialog/index';
import download from 'vj/components/zipDownloader';
-import { size, readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
+import {
+ size, readSubtasksFromFiles, parseTimeMS, parseMemoryMB,
+} from '@hydrooj/utils/lib/common';
import tpl from 'vj/utils/tpl';
async function handleSection(ev: JQuery.ClickEvent
, type: string) {
@@ -29,10 +31,6 @@ function onBeforeUnload(e) {
e.returnValue = '';
}
-function ensureFile(testdata) {
- return (file: string) => testdata.filter((i) => i === file)[0];
-}
-
const page = new NamedPage('problem_config', () => {
let reduxStore;
@@ -46,12 +44,12 @@ const page = new NamedPage('problem_config', () => {
const dialog = new Dialog({
$body: `
-
-
+
-
-
+
`,
});
try {
@@ -147,7 +145,7 @@ const page = new NamedPage('problem_config', () => {
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
}
- async function uploadConfig(config:object) {
+ async function uploadConfig(config: object) {
const configYaml = yaml.dump(config);
Notification.info(i18n('Saving file...'));
const data = new FormData();
@@ -161,9 +159,11 @@ const page = new NamedPage('problem_config', () => {
}
async function mountComponent() {
- const { default: ProblemConfigEditor } = await import('vj/components/problemconfig/ProblemConfigEditor');
- const { default: ProblemConfigForm } = await import('vj/components/problemconfig/ProblemConfigForm');
- const { default: ProblemConfigReducer } = await import('vj/components/problemconfig/reducer');
+ const [{ default: ProblemConfigEditor }, { default: ProblemConfigForm }, { default: ProblemConfigReducer }] = await Promise.all([
+ import('vj/components/problemconfig/ProblemConfigEditor'),
+ import('vj/components/problemconfig/ProblemConfigForm'),
+ import('vj/components/problemconfig/reducer'),
+ ]);
const {
React, render, Provider, store,
@@ -175,6 +175,26 @@ const page = new NamedPage('problem_config', () => {
type: 'CONFIG_LOAD',
payload: request.get(),
});
+ const unsubscribe = store.subscribe(() => {
+ // TODO set yaml schema
+ const state = store.getState();
+ if (!state.config.__loaded) return;
+ if (state.config.subtasks || state.config.cases) return;
+ const testdata = (state.testdata || []).map((i) => i.name);
+ const checkFile = (file: string) => (testdata.includes(file) ? file : null);
+ unsubscribe();
+ const value = readSubtasksFromFiles(testdata, checkFile, state.config, {});
+ for (const subtask of value.subtasks) {
+ if (subtask.time === parseTimeMS(state.config.time || '1s')) delete subtask.time;
+ if (subtask.memory === parseMemoryMB(state.config.memory || '256m')) delete subtask.memory;
+ if (subtask.time) subtask.time += 'ms';
+ if (subtask.memory) subtask.memory += 'MB';
+ }
+ store.dispatch({
+ type: 'CONFIG_AUTOCASES_UPDATE',
+ value,
+ });
+ });
render(
@@ -182,20 +202,7 @@ const page = new NamedPage('problem_config', () => {
-
{
- const testdata = (reduxStore.getState().testdata || []).map((i) => i.name);
- const checkFile = ensureFile(testdata);
- let autocases = await readCasesFromFiles(testdata, checkFile, {});
- if (!autocases.count) {
- autocases = await readSubtasksFromFiles(testdata, checkFile, {}, { subtasks: [] });
- }
- reduxStore.dispatch({
- type: 'CONFIG_AUTOCASES_UPDATE',
- value: autocases,
- });
- }}
- />
+
diff --git a/packages/ui-default/pages/problem_edit.page.js b/packages/ui-default/pages/problem_edit.page.js
index 3e5c4f8e0..759ccec00 100644
--- a/packages/ui-default/pages/problem_edit.page.js
+++ b/packages/ui-default/pages/problem_edit.page.js
@@ -202,12 +202,12 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
const dialog = new Dialog({
$body: `
-
-
+
-
-
+
`,
});
try {
diff --git a/packages/ui-default/pages/problem_files.page.js b/packages/ui-default/pages/problem_files.page.js
index 1b4e08311..1f633665f 100644
--- a/packages/ui-default/pages/problem_files.page.js
+++ b/packages/ui-default/pages/problem_files.page.js
@@ -51,12 +51,12 @@ const page = new NamedPage('problem_files', () => {
const dialog = new Dialog({
$body: `
-
-
+
-
-
+
`,
});
try {
diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html
index f22c726fa..64b521654 100644
--- a/packages/ui-default/templates/contest_detail.html
+++ b/packages/ui-default/templates/contest_detail.html
@@ -11,14 +11,14 @@
{{ tdoc.title }}
- {{ _(model.contest.statusText(tdoc)) }}
- {{ _(model.contest.RULES[tdoc.rule].TEXT) }}
- {{ _('Start at') }}: {{ contest.render_time(tdoc.beginAt) }}
- {{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
- {{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}
- {{ tdoc['attend']|default(0) }}
+ {{ _(model.contest.statusText(tdoc)) }}
+ {{ _(model.contest.RULES[tdoc.rule].TEXT) }}
+ {{ _('Start at') }}: {{ contest.render_time(tdoc.beginAt) }}
+ {{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
+ {{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}
+ {{ tdoc['attend']|default(0) }}
{% if tsdoc.attend %}
- {{ _('Attended') }}
+ {{ _('Attended') }}
{% endif %}
diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html
index 69b0fdb26..5929c7849 100644
--- a/packages/ui-default/templates/problem_detail.html
+++ b/packages/ui-default/templates/problem_detail.html
@@ -49,22 +49,22 @@
diff --git a/packages/ui-default/theme/dark.styl b/packages/ui-default/theme/dark.styl
index 93f58cdf1..f0f03f2b9 100644
--- a/packages/ui-default/theme/dark.styl
+++ b/packages/ui-default/theme/dark.styl
@@ -212,7 +212,7 @@ $hover-background-color = #424242
.menu__link.highlight
background: rgb(40, 11, 20)
- .bp3-tag.bp3-minimal:not([class*="bp3-intent-"])
+ .bp4-tag.bp4-minimal:not([class*="bp4-intent-"])
background-color: rgba(87, 100, 117, .2)
color: #d9dfe7
diff --git a/packages/ui-default/utils/i18n.js b/packages/ui-default/utils/i18n.js
index bc5b6464d..7cb5aab10 100644
--- a/packages/ui-default/utils/i18n.js
+++ b/packages/ui-default/utils/i18n.js
@@ -1,6 +1,7 @@
import substitute from './substitute';
export default function i18n(str, ...params) {
+ if (!str) return '';
return substitute(window.LOCALES?.[str] || str, params);
}
diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts
index 9469bb70b..1edd95a88 100644
--- a/packages/utils/lib/common.ts
+++ b/packages/utils/lib/common.ts
@@ -137,14 +137,14 @@ const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 };
export function parseTimeMS(str: string | number) {
- if (typeof str === 'number') return str;
+ if (typeof str === 'number' || Number.isSafeInteger(+str)) return +str;
const match = TIME_RE.exec(str);
if (!match) throw new Error(`${str} error parsing time`);
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2].toLowerCase()]);
}
export function parseMemoryMB(str: string | number) {
- if (typeof str === 'number') return str;
+ if (typeof str === 'number' || Number.isSafeInteger(+str)) return +str;
const match = MEMORY_RE.exec(str);
if (!match) throw new Error(`${str} error parsing memory`);
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]);
@@ -161,19 +161,17 @@ export function size(s: number, base = 1) {
return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`;
}
-interface Re0 {
- reg: RegExp,
- output: ((a: RegExpExecArray) => string)[],
- id: (a: RegExpExecArray) => number,
+interface MatchRule {
+ regex: RegExp;
+ output: ((a: RegExpExecArray) => string)[];
+ id: (a: RegExpExecArray) => number;
+ subtask: (a: RegExpExecArray) => number;
+ preferredScorerType: 'min' | 'max' | 'sum';
}
-interface Re1 extends Re0 {
- subtask: (a: RegExpExecArray) => number,
-}
-
-const RE0: Re0[] = [
+const SubtaskMatcher: MatchRule[] = [
{
- reg: /^([^\d]*)(\d+).(in|txt)$/,
+ regex: /^([^\d]*)(\d+).(in|txt)$/,
output: [
(a) => `${a[1] + a[2]}.out`,
(a) => `${a[1] + a[2]}.ans`,
@@ -181,108 +179,52 @@ const RE0: Re0[] = [
(a) => (a[1].includes('input') ? `${a[1] + a[2]}.txt`.replace(/input/g, 'output') : null),
],
id: (a) => +a[2],
+ subtask: () => 0,
+ preferredScorerType: 'sum',
},
{
- reg: /^([^\d]*)\.in(\d+)$/,
+ regex: /^([^\d]*)\.in(\d+)$/,
output: [
(a) => `${a[1]}.ou${a[2]}`,
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
],
id: (a) => +a[2],
+ subtask: () => 0,
+ preferredScorerType: 'sum',
},
-];
-const RE1: Re1[] = [
{
- reg: /^([^\d]*)([0-9]+)([-_])([0-9]+).in$/,
+ regex: /^([^\d]*)([0-9]+)([-_])([0-9]+).in$/,
output: [(a) => `${a[1] + a[2]}${a[3]}${a[4]}.out`],
- subtask: (a) => +a[2],
id: (a) => +a[4],
+ subtask: (a) => +a[2],
+ preferredScorerType: 'min',
},
];
-export async function readCasesFromFiles(files: string[], checkFile, cfg) {
- const cases = [];
- for (const file of files) {
- for (const REG of RE0) {
- if (REG.reg.test(file)) {
- const data = REG.reg.exec(file);
- const c = { input: file, output: '', id: REG.id(data) };
- for (const func of REG.output) {
- if (cfg.noOutputFile) c.output = '/dev/null';
- else c.output = func(data);
- if (c.output && (c.output === '/dev/null' || checkFile(c.output))) {
- cases.push(c);
- break;
- }
- }
- }
- }
- }
- cases.sort((a, b) => (a.id - b.id));
- const extra = cases.length - (100 % cases.length);
- const config = {
- count: 0,
- subtasks: [{
- time: parseTimeMS(cfg.time || '1s'),
- memory: parseMemoryMB(cfg.memory || '256m'),
- type: 'sum',
- cases: [],
- score: Math.floor(100 / cases.length),
- }],
- };
- for (let i = 0; i < extra; i++) {
- config.count++;
- config.subtasks[0].cases.push({
- id: config.count,
- input: checkFile(cases[i].input),
- output: checkFile(cases[i].output),
- });
- }
- if (extra < cases.length) {
- config.subtasks.push({
- time: parseTimeMS(cfg.time || '1s'),
- memory: parseMemoryMB(cfg.memory || '256m'),
- type: 'sum',
- cases: [],
- score: Math.floor(100 / cases.length) + 1,
- });
- for (let i = extra; i < cases.length; i++) {
- config.count++;
- config.subtasks[1].cases.push({
- id: config.count,
- input: checkFile(cases[i].input),
- output: checkFile(cases[i].output),
- });
- }
- }
- return config;
-}
-
-export async function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
+export function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
const subtask = {};
- for (const s of rst.subtasks) if (s.id) subtask[s.id] = s;
+ for (const s of rst.subtasks || []) if (s.id) subtask[s.id] = s;
const subtasks = [];
for (const file of files) {
- for (const REG of RE1) {
- if (REG.reg.test(file)) {
- const data = REG.reg.exec(file);
- const sid = REG.subtask(data);
- const c = { input: file, output: '', id: REG.id(data) };
- for (const func of REG.output) {
- if (cfg.noOutputFile) c.output = '/dev/null';
- else c.output = func(data);
- if (c.output === '/dev/null' || checkFile(c.output)) {
- if (!subtask[sid]) {
- subtask[sid] = {
- time: parseTimeMS(cfg.time || '1s'),
- memory: parseMemoryMB(cfg.memory || '256m'),
- type: 'min',
- cases: [c],
- };
- } else if (!subtask[sid].cases) subtask[sid].cases = [c];
- else subtask[sid].cases.push(c);
- break;
- }
+ for (const rule of SubtaskMatcher) {
+ const data = rule.regex.exec(file);
+ if (!data) continue;
+ const sid = rule.subtask(data);
+ const c = { input: file, output: '', id: rule.id(data) };
+ for (const func of rule.output) {
+ if (cfg.noOutputFile) c.output = '/dev/null';
+ else c.output = func(data);
+ if (c.output === '/dev/null' || checkFile(c.output)) {
+ if (!subtask[sid]) {
+ subtask[sid] = {
+ time: parseTimeMS(cfg.time || '1s'),
+ memory: parseMemoryMB(cfg.memory || '256m'),
+ type: rule.preferredScorerType,
+ cases: [c],
+ };
+ } else if (!subtask[sid].cases) subtask[sid].cases = [c];
+ else subtask[sid].cases.push(c);
+ break;
}
}
}
@@ -291,6 +233,7 @@ export async function readSubtasksFromFiles(files: string[], checkFile, cfg, rst
subtask[i].cases.sort((a, b) => (a.id - b.id));
subtasks.push(subtask[i]);
}
+ subtasks.sort((a, b) => (a.id - b.id));
const base = Math.floor(100 / subtasks.length);
const extra = subtasks.length - (100 % subtasks.length);
const config = { count: 0, subtasks };