From 5c9cecc1972ecc44bf44bda85d24dac515432f97 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 7 Feb 2025 15:59:26 +0800 Subject: [PATCH] ui: problemconfig: support new CompilableSource --- framework/framework/server.ts | 8 +- packages/common/cases.ts | 183 +----------------- packages/common/index.ts | 2 +- packages/common/subtask.ts | 176 +++++++++++++++++ packages/common/tests/subtask.spec.ts | 2 +- packages/hydrojudge/package.json | 1 + packages/hydrojudge/src/cases.ts | 4 +- packages/hydrojudge/src/hosts/hydro.ts | 2 +- packages/hydrojudge/src/utils.ts | 4 +- packages/hydrooj/package.json | 9 +- packages/hydrooj/src/lib/testdataConfig.ts | 5 +- packages/hydrooj/src/model/builtin.ts | 3 +- packages/onlyoffice/package.json | 3 - .../components/problemconfig/BasicForm.tsx | 46 ++++- .../components/problemconfig/ProblemType.tsx | 29 ++- .../problemconfig/reducer/config.ts | 2 +- packages/ui-default/package.json | 1 + 17 files changed, 252 insertions(+), 228 deletions(-) create mode 100644 packages/common/subtask.ts diff --git a/framework/framework/server.ts b/framework/framework/server.ts index 455a8809d..350a1a314 100644 --- a/framework/framework/server.ts +++ b/framework/framework/server.ts @@ -71,7 +71,6 @@ export interface HydroResponse { etag?: string; attachment: (name: string, stream?: any) => void; addHeader: (name: string, value: string) => void; - serializer: typeof serializer; } export type RendererFunction = (name: string, context: Record) => string | Promise; type HydroContext = { @@ -171,7 +170,7 @@ export class HandlerCommon { renderHTML(templateName: string, args: Record) { const type = templateName.split('.')[1]; - const engine = this.ctx.server.renderers[type] || (() => JSON.stringify(args, this.response.serializer(false, this))); + const engine = this.ctx.server.renderers[type] || (() => JSON.stringify(args, serializer(false, this))); return engine(templateName, { handler: this, UserContext: this.user, @@ -266,7 +265,7 @@ export class ConnectionHandler extends HandlerCommon { } send(data: any) { - let payload = JSON.stringify(data, this.response.serializer(false, this)); + let payload = JSON.stringify(data, serializer(false, this)); if (this.compression) { if (this.counter > 1000) this.resetCompression(); payload = this.compression.deflate(payload); @@ -328,7 +327,6 @@ export interface WebServiceConfig { host?: string; xff?: string; xhost?: string; - serializer?: typeof serializer; } export class WebService extends Service { @@ -443,7 +441,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); func: (t) => this.handleHttp(t, NotFoundHandler, () => true), }, ])); - this.addLayer('base', base(logger, this.config.xff, this.config.xhost, this.config.serializer)); + this.addLayer('base', base(logger, this.config.xff, this.config.xhost)); wsServer.on('connection', async (socket, request) => { socket.on('error', (err) => { logger.warn('Websocket Error: %s', err.message); diff --git a/packages/common/cases.ts b/packages/common/cases.ts index 2ca1e7e5d..da2d7c537 100644 --- a/packages/common/cases.ts +++ b/packages/common/cases.ts @@ -1,7 +1,8 @@ import { findFileSync, parseMemoryMB, parseTimeMS } from '@hydrooj/utils'; +import { CompilableSource, ProblemConfigFile } from './types'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function readYamlCases(cfg: Record = {}, checkFile = (s: string, errMsg: string) => s) { +export async function readYamlCases(cfg: ProblemConfigFile = {}, checkFile = (s: CompilableSource, errMsg: string) => s) { const config: any = { checker_type: cfg.checker_type || 'default', judge_extra_files: [], @@ -13,8 +14,9 @@ export async function readYamlCases(cfg: Record = {}, checkFile = ( } } else { if (cfg.checker) { - if (!cfg.checker.includes('.')) { - config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false); + const name = typeof cfg.checker === 'string' ? cfg.checker : cfg.checker.file; + if (!name.includes('.')) { + config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${name}.cpp`, false); } config.checker ||= checkFile(cfg.checker, 'Cannot find checker {0}.'); } @@ -39,178 +41,3 @@ export async function readYamlCases(cfg: Record = {}, checkFile = ( if (cfg.memory) config.memory = parseMemoryMB(cfg.memory); return Object.assign(cfg, config); } - -export function convertIniConfig(ini: string) { - const f = ini.split('\n'); - const count = parseInt(f[0], 10); - const res = { subtasks: [] }; - for (let i = 1; i <= count; i++) { - if (!f[i]?.trim()) throw new Error('Testdata count incorrect.'); - const [input, output, time, score, memory] = f[i].split('|'); - const cur = { - cases: [{ input, output }], - score: parseInt(score, 10), - time: `${time}s`, - memory: '256m', - }; - if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`; - res.subtasks.push(cur); - } - return res; -} - -interface MatchRule { - regex: RegExp; - output: (a: RegExpExecArray) => string[]; - id: (a: RegExpExecArray) => number; - subtask: (a: RegExpExecArray) => number; - preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum'; -} - -const SubtaskMatcher: MatchRule[] = [ - { - regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/, - output: (a) => ['out', 'ans'] - .flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`]) - .flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]) - .concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null), - id: (a) => +a[4], - subtask: (a) => +(a[3] || 1), - preferredScorerType: (a) => (a[3] ? 'min' : 'sum'), - }, - { - regex: /^([^\d]*)\.(in|IN)(\d+)$/, - output: (a) => [ - `${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`, - `${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`, - ].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]), - id: (a) => +a[2], - subtask: () => 1, - preferredScorerType: () => 'sum', - }, - { - regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/, - output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`), - id: (a) => +a[4], - subtask: (a) => +a[2], - preferredScorerType: () => 'min', - }, - { - regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/, - output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`), - id: (a) => +a[2], - subtask: () => 1, - preferredScorerType: () => 'sum', - }, -]; - -function* getScore(totalScore: number, count: number) { - const base = Math.floor(totalScore / count); - const extra = count - (totalScore % count); - for (let i = 0; i < count; i++) { - if (i >= extra) yield base + 1; - else yield base; - } -} - -interface ParsedCase { - id?: number; - time?: number | string; - memory?: number | string; - score?: number; - input?: string; - output?: string; -} -interface ParsedSubtask { - cases: ParsedCase[]; - type: 'min' | 'max' | 'sum'; - time?: number | string; - memory?: number | string; - score?: number; - id?: number; - if?: number[]; -} - -export function readSubtasksFromFiles(files: string[], config) { - const subtask: Record = {}; - for (const s of config.subtasks || []) if (s.id) subtask[s.id] = s; - for (const file of files) { - let match = false; - 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) }; - const type = rule.preferredScorerType(data); - const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i); - for (const output of outputs) { - if (output === file) continue; - if (output === '/dev/null' || files.includes(output)) { - match = true; - c.output = output; - if (!subtask[sid]) { - subtask[sid] = { - time: config.time, - memory: config.memory, - type, - cases: [c], - id: sid, - }; - } else if (!subtask[sid].cases) subtask[sid].cases = [c]; - else subtask[sid].cases.push(c); - break; - } - } - if (match) break; - } - } - return Object.values(subtask); -} - -export interface NormalizedCase extends Required { - time: number; - memory: number; -} -export interface NormalizedSubtask extends Required { - cases: NormalizedCase[]; - time: number; - memory: number; -} - -export function normalizeSubtasks( - subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string, - time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false, - timeRate = 1, memoryRate = 1, -): NormalizedSubtask[] { - subtasks.sort((a, b) => (a.id - b.id)); - const subtaskScore = getScore( - Math.max(100 - Math.sum(subtasks.map((i) => i.score || 0)), 0), - subtasks.filter((i) => !i.score).length, - ); - return subtasks.map((s, id) => { - s.cases.sort((a, b) => (a.id - b.id)); - const score = s.score || subtaskScore.next().value as number; - const caseScore = getScore( - Math.max(score - Math.sum(s.cases.map((i) => i.score || 0)), 0), - s.cases.filter((i) => !i.score).length, - ); - return { - id: id + 1, - type: 'min', - if: [], - ...s, - score, - time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate, - memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate, - cases: s.cases.map((c, index) => ({ - id: index + 1, - ...c, - score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score), - time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate, - memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate, - input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null', - output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null', - })) as NormalizedCase[], - }; - }); -} diff --git a/packages/common/index.ts b/packages/common/index.ts index 457305d8a..78ec077c1 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -1,4 +1,4 @@ export * from './lang'; export * from './status'; export * from './types'; -export * from './cases'; +export * from './subtask'; diff --git a/packages/common/subtask.ts b/packages/common/subtask.ts new file mode 100644 index 000000000..4e554fbc9 --- /dev/null +++ b/packages/common/subtask.ts @@ -0,0 +1,176 @@ +import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common'; + +export function convertIniConfig(ini: string) { + const f = ini.split('\n'); + const count = parseInt(f[0], 10); + const res = { subtasks: [] }; + for (let i = 1; i <= count; i++) { + if (!f[i]?.trim()) throw new Error('Testdata count incorrect.'); + const [input, output, time, score, memory] = f[i].split('|'); + const cur = { + cases: [{ input, output }], + score: parseInt(score, 10), + time: `${time}s`, + memory: '256m', + }; + if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`; + res.subtasks.push(cur); + } + return res; +} + +interface MatchRule { + regex: RegExp; + output: (a: RegExpExecArray) => string[]; + id: (a: RegExpExecArray) => number; + subtask: (a: RegExpExecArray) => number; + preferredScorerType: (a: RegExpExecArray) => 'min' | 'max' | 'sum'; +} + +const SubtaskMatcher: MatchRule[] = [ + { + regex: /^(([A-Za-z0-9._-]*?)(?:(\d*)[-_])?(\d+))\.(in|IN|txt|TXT|in\.txt|IN\.TXT)$/, + output: (a) => ['out', 'ans'] + .flatMap((i) => [i, i.toUpperCase(), `${i}.txt`, `${i.toUpperCase()}.TXT`]) + .flatMap((i) => [`${a[1]}.${i}`, `${a[1]}.${i}`.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]) + .concat(a[1].includes('input') ? `${a[1]}.txt`.replace(/input/g, 'output') : null), + id: (a) => +a[4], + subtask: (a) => +(a[3] || 1), + preferredScorerType: (a) => (a[3] ? 'min' : 'sum'), + }, + { + regex: /^([^\d]*)\.(in|IN)(\d+)$/, + output: (a) => [ + `${a[1]}.${a[2] === 'in' ? 'ou' : 'OU'}${a[3]}`, + `${a[1]}.${a[2] === 'in' ? 'out' : 'OUT'}${a[3]}`, + ].flatMap((i) => [i, i.replace(/input/g, 'output').replace(/INPUT/g, 'OUTPUT')]), + id: (a) => +a[2], + subtask: () => 1, + preferredScorerType: () => 'sum', + }, + { + regex: /^([^\d]*)([0-9]+)([-_])([0-9]+)\.(in|IN)$/, + output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}${a[2]}${a[3]}${a[4]}.${i}`), + id: (a) => +a[4], + subtask: (a) => +a[2], + preferredScorerType: () => 'min', + }, + { + regex: /^(([0-9]+)[-_](?:.*))\.(in|IN)$/, + output: (a) => ['out', 'ans', 'OUT', 'ANS'].flatMap((i) => `${a[1]}.${i}`), + id: (a) => +a[2], + subtask: () => 1, + preferredScorerType: () => 'sum', + }, +]; + +function* getScore(totalScore: number, count: number) { + const base = Math.floor(totalScore / count); + const extra = count - (totalScore % count); + for (let i = 0; i < count; i++) { + if (i >= extra) yield base + 1; + else yield base; + } +} + +interface ParsedCase { + id?: number; + time?: number | string; + memory?: number | string; + score?: number; + input?: string; + output?: string; +} +interface ParsedSubtask { + cases: ParsedCase[]; + type: 'min' | 'max' | 'sum'; + time?: number | string; + memory?: number | string; + score?: number; + id?: number; + if?: number[]; +} + +export function readSubtasksFromFiles(files: string[], config) { + const subtask: Record = {}; + for (const s of config.subtasks || []) if (s.id) subtask[s.id] = s; + for (const file of files) { + let match = false; + 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) }; + const type = rule.preferredScorerType(data); + const outputs = (config.noOutputFile ? ['/dev/null'] : rule.output(data)).filter((i) => i); + for (const output of outputs) { + if (output === file) continue; + if (output === '/dev/null' || files.includes(output)) { + match = true; + c.output = output; + if (!subtask[sid]) { + subtask[sid] = { + time: config.time, + memory: config.memory, + type, + cases: [c], + id: sid, + }; + } else if (!subtask[sid].cases) subtask[sid].cases = [c]; + else subtask[sid].cases.push(c); + break; + } + } + if (match) break; + } + } + return Object.values(subtask); +} + +export interface NormalizedCase extends Required { + time: number; + memory: number; +} +export interface NormalizedSubtask extends Required { + cases: NormalizedCase[]; + time: number; + memory: number; +} + +export function normalizeSubtasks( + subtasks: ParsedSubtask[], checkFile: (name: string, errMsg: string) => string, + time: number | string = '1000ms', memory: number | string = '256m', ignoreParseError = false, + timeRate = 1, memoryRate = 1, +): NormalizedSubtask[] { + subtasks.sort((a, b) => (a.id - b.id)); + const subtaskScore = getScore( + Math.max(100 - Math.sum(subtasks.map((i) => i.score || 0)), 0), + subtasks.filter((i) => !i.score).length, + ); + return subtasks.map((s, id) => { + s.cases.sort((a, b) => (a.id - b.id)); + const score = s.score || subtaskScore.next().value as number; + const caseScore = getScore( + Math.max(score - Math.sum(s.cases.map((i) => i.score || 0)), 0), + s.cases.filter((i) => !i.score).length, + ); + return { + id: id + 1, + type: 'min', + if: [], + ...s, + score, + time: parseTimeMS(s.time || time, !ignoreParseError) * timeRate, + memory: parseMemoryMB(s.memory || memory, !ignoreParseError) * memoryRate, + cases: s.cases.map((c, index) => ({ + id: index + 1, + ...c, + score: c.score || (s.type === 'sum' ? caseScore.next().value as number : score), + time: parseTimeMS(c.time || s.time || time, !ignoreParseError) * timeRate, + memory: parseMemoryMB(c.memory || s.memory || memory, !ignoreParseError) * memoryRate, + input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null', + output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null', + })) as NormalizedCase[], + }; + }); +} diff --git a/packages/common/tests/subtask.spec.ts b/packages/common/tests/subtask.spec.ts index 73bbd16d0..2c3a05954 100644 --- a/packages/common/tests/subtask.spec.ts +++ b/packages/common/tests/subtask.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe, it } from 'node:test'; -import { readSubtasksFromFiles } from '../cases'; +import { readSubtasksFromFiles } from '../subtask'; interface Info { subtask: number; diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 646c277f7..eed35a3d2 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -6,6 +6,7 @@ "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", "dependencies": { + "@hydrooj/common": "workspace:^", "@hydrooj/utils": "workspace:^", "adm-zip": "0.5.14", "cac": "^6.7.14", diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index 1e29563c9..712aee279 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -1,9 +1,9 @@ /* eslint-disable no-await-in-loop */ import path from 'path'; import { - convertIniConfig, LangConfig, - normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles, readYamlCases, + convertIniConfig, LangConfig, normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles, } from '@hydrooj/common'; +import { readYamlCases } from '@hydrooj/common/cases'; import { changeErrorType, fs, yaml, } from '@hydrooj/utils'; diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index 8187d46b1..335675842 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -3,8 +3,8 @@ import path from 'path'; import PQueue from 'p-queue'; import superagent from 'superagent'; import WebSocket from 'ws'; +import type { LangConfig } from '@hydrooj/common'; import { fs, pipeRequest } from '@hydrooj/utils'; -import { LangConfig } from '@hydrooj/utils/lib/lang'; import * as sysinfo from '@hydrooj/utils/lib/sysinfo'; import type { JudgeResultBody } from 'hydrooj'; import { getConfig } from '../config'; diff --git a/packages/hydrojudge/src/utils.ts b/packages/hydrojudge/src/utils.ts index 2f32e0f5c..d8e659a3d 100644 --- a/packages/hydrojudge/src/utils.ts +++ b/packages/hydrojudge/src/utils.ts @@ -1,5 +1,6 @@ import path from 'path'; import { parse } from 'shell-quote'; +import { CompilableSource } from '@hydrooj/common'; import { fs } from '@hydrooj/utils'; import { FormatError } from './error'; @@ -40,7 +41,8 @@ function restrictFile(p: string) { } export function ensureFile(folder: string) { - return (file: string, message: string) => { + return (src: CompilableSource, message: string) => { + const file = typeof src === 'string' ? src : src?.file; if (file === '/dev/null') return file; // Historical issue if (file.includes('/')) { diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index bd570a875..1db516935 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -12,11 +12,12 @@ }, "preferUnplugged": true, "dependencies": { - "@aws-sdk/client-s3": "3.741.0", - "@aws-sdk/lib-storage": "3.741.0", - "@aws-sdk/s3-presigned-post": "3.741.0", - "@aws-sdk/s3-request-presigner": "3.741.0", + "@aws-sdk/client-s3": "3.726.1", + "@aws-sdk/lib-storage": "3.726.1", + "@aws-sdk/s3-presigned-post": "3.726.1", + "@aws-sdk/s3-request-presigner": "3.726.1", "@graphql-tools/schema": "^10.0.16", + "@hydrooj/common": "workspace:^", "@hydrooj/framework": "workspace:^", "@hydrooj/utils": "workspace:^", "@simplewebauthn/server": "9.0.3", diff --git a/packages/hydrooj/src/lib/testdataConfig.ts b/packages/hydrooj/src/lib/testdataConfig.ts index 4228570e3..c451b0257 100644 --- a/packages/hydrooj/src/lib/testdataConfig.ts +++ b/packages/hydrooj/src/lib/testdataConfig.ts @@ -1,7 +1,6 @@ import { load } from 'js-yaml'; -import { - normalizeSubtasks, ProblemConfigFile, readYamlCases, -} from '@hydrooj/common'; +import { normalizeSubtasks, ProblemConfigFile } from '@hydrooj/common'; +import { readYamlCases } from '@hydrooj/common/cases'; import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils'; import type { ProblemConfig } from '../interface'; diff --git a/packages/hydrooj/src/model/builtin.ts b/packages/hydrooj/src/model/builtin.ts index a32a18522..0abc6efe9 100644 --- a/packages/hydrooj/src/model/builtin.ts +++ b/packages/hydrooj/src/model/builtin.ts @@ -1,5 +1,5 @@ import { - getScoreColor, STATUS, STATUS_CODES, STATUS_SHORT_TEXTS, + getScoreColor, NORMAL_STATUS, STATUS, STATUS_CODES, STATUS_SHORT_TEXTS, STATUS_TEXTS, USER_GENDER_FEMALE, USER_GENDER_ICONS, USER_GENDER_MALE, USER_GENDER_OTHER, USER_GENDER_RANGE, USER_GENDERS, } from '@hydrooj/common'; @@ -475,6 +475,7 @@ global.Hydro.model.builtin = { STATUS_TEXTS, STATUS_SHORT_TEXTS, STATUS_CODES, + NORMAL_STATUS, USER_GENDER_MALE, USER_GENDER_FEMALE, USER_GENDER_OTHER, diff --git a/packages/onlyoffice/package.json b/packages/onlyoffice/package.json index 468710902..481ba11f5 100644 --- a/packages/onlyoffice/package.json +++ b/packages/onlyoffice/package.json @@ -8,8 +8,5 @@ "license": "AGPL-3.0-or-later", "dependencies": { "jsonwebtoken": "^9.0.2" - }, - "devDependencies": { - "@types/jsonwebtoken": "^9.0.8" } } diff --git a/packages/ui-default/components/problemconfig/BasicForm.tsx b/packages/ui-default/components/problemconfig/BasicForm.tsx index 53d2d4edf..381cfc5e4 100644 --- a/packages/ui-default/components/problemconfig/BasicForm.tsx +++ b/packages/ui-default/components/problemconfig/BasicForm.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { i18n } from 'vj/utils'; +import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete'; import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete'; import type { RootState } from './reducer/index'; @@ -21,6 +22,7 @@ export function FormItem({ type KeyType = { [Q in keyof K]: K[Q] extends T ? Q : never; }[keyof K]; +type FileSelectKey = 'checker' | 'interactor' | 'manager'; export function ManagedInput({ placeholder, formKey }: { placeholder?: string, formKey: KeyType }) { const value = useSelector((state: RootState) => state.config[formKey]); @@ -54,16 +56,42 @@ export function ManagedSelect({ options, formKey }: { options: string[], formKey ); } -export function SingleFileSelect({ formKey }: { formKey: KeyType }) { +const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0])); +const data = Object.keys(window.LANGS).filter((i) => !prefixes.has(i)) + .map((i) => ({ name: `${i.includes('.') ? `${window.LANGS[i.split('.')[0]].display || ''}/` : ''}${window.LANGS[i].display}`, _id: i })); +data.unshift({ name: 'Auto', _id: 'auto' }); + +export function SingleFileSelect({ formKey, withLang = false, label = 'Checker' }: { formKey: FileSelectKey, withLang?: boolean, label?: string }) { 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 })} - /> - ); + const selectedFile = typeof value === 'string' ? value : value?.file || ''; + const selectedLang = typeof value === 'string' ? 'auto' : value?.lang || 'auto'; + const update = (file: string, lang: string) => { + if (file === selectedFile && lang === selectedLang) return; + if (withLang) dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: file ? { file, lang } : null }); + else dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: file }); + }; + return withLang ? (<> + + update(val, selectedLang)} + /> + + + update(selectedFile, val)} + /> + + ) : ( update(val, selectedLang)} + />); } diff --git a/packages/ui-default/components/problemconfig/ProblemType.tsx b/packages/ui-default/components/problemconfig/ProblemType.tsx index f830057e2..1113fa467 100644 --- a/packages/ui-default/components/problemconfig/ProblemType.tsx +++ b/packages/ui-default/components/problemconfig/ProblemType.tsx @@ -20,7 +20,8 @@ export default function ProblemType() { const dispatcher = (base) => (value) => dispatch({ ...base, value }); useEffect(() => { if (category || !checker) return; - if (checker.includes('.')) setCategory('custom'); + const name = typeof checker === 'string' ? checker : checker.file; + if (name.includes('.')) setCategory('custom'); else setCategory('preset'); }, [checker]); return ( @@ -75,7 +76,7 @@ export default function ProblemType() { title="testlib" panel={(
- + {category === 'preset' - ? + ? - : - - } + : }
)} /> @@ -111,12 +110,10 @@ export default function ProblemType() { title="other" panel={(
- + - - - +
)} /> @@ -128,9 +125,7 @@ export default function ProblemType() { title={i18n('problem_type.interactive')} panel={(
- - - +
)} /> @@ -139,10 +134,8 @@ export default function ProblemType() { title={i18n('problem_type.communication')} panel={(
- - - - + +