mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: add contest language limit (#1014)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
0699e17ce6
commit
4165e9b153
23 files changed
+175
-232
No files matched your search
+1
-1
@@ -60,7 +60,7 @@ const configFlat = (name) => ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: ['**/*.ts'],
|
include: ['**/*.ts', '**/*.tsx'],
|
||||||
exclude,
|
exclude,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -310,18 +310,20 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
|||||||
)}
|
)}
|
||||||
{focused && itemList.length > 0 && (
|
{focused && itemList.length > 0 && (
|
||||||
<ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}>
|
<ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}>
|
||||||
{itemList.map((item, idx) => (
|
{itemList.map((item, idx) => {
|
||||||
<li
|
const inner = renderItem(item);
|
||||||
|
if (!inner) return null;
|
||||||
|
return <li
|
||||||
key={itemKey(item)}
|
key={itemKey(item)}
|
||||||
onClick={() => toggleItem(item)}
|
onClick={() => toggleItem(item)}
|
||||||
onMouseMove={() => setCurrentItem(idx)}
|
onMouseMove={() => setCurrentItem(idx)}
|
||||||
data-selected={selectedKeys.includes(itemKey(item))}
|
data-selected={selectedKeys.includes(itemKey(item))}
|
||||||
data-focus={idx === currentItem}
|
data-focus={idx === currentItem}
|
||||||
>
|
>
|
||||||
<div>{renderItem(item)}</div>
|
<div>{inner}</div>
|
||||||
{selectedKeys.includes(itemKey(item)) && <Icon name="check" />}
|
{selectedKeys.includes(itemKey(item)) && <Icon name="check" />}
|
||||||
</li>
|
</li>;
|
||||||
))}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -305,11 +305,12 @@ export class ContestEditHandler extends Handler {
|
|||||||
@param('contestDuration', Types.Float, true)
|
@param('contestDuration', Types.Float, true)
|
||||||
@param('maintainer', Types.NumericArray, true)
|
@param('maintainer', Types.NumericArray, true)
|
||||||
@param('allowViewCode', Types.Boolean)
|
@param('allowViewCode', Types.Boolean)
|
||||||
|
@param('langs', Types.CommaSeperatedArray, true)
|
||||||
async postUpdate(
|
async postUpdate(
|
||||||
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number,
|
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number,
|
||||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||||
_code = '', autoHide = false, assign: string[] = [], lock: number = null,
|
_code = '', autoHide = false, assign: string[] = [], lock: number = null,
|
||||||
contestDuration: number = null, maintainer: number[] = [], allowViewCode = false,
|
contestDuration: number = null, maintainer: number[] = [], allowViewCode = false, langs: string[] = [],
|
||||||
) {
|
) {
|
||||||
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||||
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
||||||
@@ -350,7 +351,7 @@ export class ContestEditHandler extends Handler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await contest.edit(domainId, tid, {
|
await contest.edit(domainId, tid, {
|
||||||
assign, _code, autoHide, lockAt, maintainer, allowViewCode,
|
assign, _code, autoHide, lockAt, maintainer, allowViewCode, langs,
|
||||||
});
|
});
|
||||||
this.response.body = { tid };
|
this.response.body = { tid };
|
||||||
this.response.redirect = this.url('contest_detail', { tid });
|
this.response.redirect = this.url('contest_detail', { tid });
|
||||||
|
|||||||
@@ -191,11 +191,12 @@ class HomeworkEditHandler extends Handler {
|
|||||||
@param('rated', Types.Boolean)
|
@param('rated', Types.Boolean)
|
||||||
@param('maintainer', Types.NumericArray, true)
|
@param('maintainer', Types.NumericArray, true)
|
||||||
@param('assign', Types.CommaSeperatedArray, true)
|
@param('assign', Types.CommaSeperatedArray, true)
|
||||||
|
@param('langs', Types.CommaSeperatedArray, true)
|
||||||
async postUpdate(
|
async postUpdate(
|
||||||
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string,
|
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string,
|
||||||
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
|
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
|
||||||
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
|
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
|
||||||
maintainer: number[] = [], assign: string[] = [],
|
maintainer: number[] = [], assign: string[] = [], langs: string[] = [],
|
||||||
) {
|
) {
|
||||||
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
||||||
const tdoc = tid ? await contest.get(domainId, tid) : null;
|
const tdoc = tid ? await contest.get(domainId, tid) : null;
|
||||||
@@ -226,6 +227,7 @@ class HomeworkEditHandler extends Handler {
|
|||||||
rated,
|
rated,
|
||||||
maintainer,
|
maintainer,
|
||||||
assign,
|
assign,
|
||||||
|
langs,
|
||||||
});
|
});
|
||||||
if (tdoc.beginAt !== beginAt.toDate()
|
if (tdoc.beginAt !== beginAt.toDate()
|
||||||
|| tdoc.endAt !== endAt.toDate()
|
|| tdoc.endAt !== endAt.toDate()
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
|
|||||||
if (this.pdoc.config.langs) t.push(this.pdoc.config.langs);
|
if (this.pdoc.config.langs) t.push(this.pdoc.config.langs);
|
||||||
if (ddoc.langs) t.push(ddoc.langs.split(',').map((i) => i.trim()).filter((i) => i));
|
if (ddoc.langs) t.push(ddoc.langs.split(',').map((i) => i.trim()).filter((i) => i));
|
||||||
if (this.domain.langs) t.push(this.domain.langs.split(',').map((i) => i.trim()).filter((i) => i));
|
if (this.domain.langs) t.push(this.domain.langs.split(',').map((i) => i.trim()).filter((i) => i));
|
||||||
|
if (this.tdoc?.langs) t.push(this.tdoc.langs);
|
||||||
if (this.pdoc.config.type === 'remote_judge') {
|
if (this.pdoc.config.type === 'remote_judge') {
|
||||||
const p = this.pdoc.config.subType;
|
const p = this.pdoc.config.subType;
|
||||||
const dl = Object.keys(setting.langs).filter((i) => i.startsWith(`${p}.`) || setting.langs[i].validAs[p]);
|
const dl = Object.keys(setting.langs).filter((i) => i.startsWith(`${p}.`) || setting.langs[i].validAs[p]);
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export interface Tdoc extends Document {
|
|||||||
autoHide?: boolean;
|
autoHide?: boolean;
|
||||||
balloon?: Record<number, string | { color: string, name: string }>;
|
balloon?: Record<number, string | { color: string, name: string }>;
|
||||||
score?: Record<number, number>;
|
score?: Record<number, number>;
|
||||||
|
langs?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In hours
|
* In hours
|
||||||
|
|||||||
@@ -1,55 +1,25 @@
|
|||||||
import { CustomSelectAutoComplete as CustomSelectAutoCompleteFC } from '@hydrooj/components';
|
import { CustomSelectAutoComplete as CustomSelectAutoCompleteFC } from '@hydrooj/components';
|
||||||
import React from 'react';
|
|
||||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
|
|
||||||
interface CustomSelectOptions {
|
interface CustomSelectOptions {
|
||||||
data: any[]
|
data: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component = React.forwardRef<any, any>((props, ref) => {
|
|
||||||
const [value, setValue] = React.useState(props.value);
|
|
||||||
return (
|
|
||||||
<CustomSelectAutoCompleteFC
|
|
||||||
ref={ref as any}
|
|
||||||
height="auto"
|
|
||||||
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
|
||||||
onChange={(v) => {
|
|
||||||
setValue(v);
|
|
||||||
props.onChange(v);
|
|
||||||
}}
|
|
||||||
data={props.data}
|
|
||||||
multi={props.multi}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete<CustomSelectOptions> {
|
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete<CustomSelectOptions> {
|
||||||
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
|
||||||
|
|
||||||
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
classes: 'custom-select',
|
classes: 'custom-select',
|
||||||
|
component: CustomSelectAutoCompleteFC,
|
||||||
|
props: {
|
||||||
|
data: options.data,
|
||||||
|
multi: options.multi,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
value(): Multi extends true ? number[] : string {
|
|
||||||
if (this.options.multi) return this.ref?.getSelectedItemKeys().map((i) => +i) ?? this.$dom.val();
|
|
||||||
return this.ref?.getSelectedItems()[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
attach() {
|
|
||||||
const value = this.$dom.val();
|
|
||||||
this.component.render(
|
|
||||||
<Component
|
|
||||||
ref={(ref) => { this.ref = ref; }}
|
|
||||||
data={this.options.data}
|
|
||||||
value={value}
|
|
||||||
multi={this.options.multi}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Hydro.components.CustomSelectAutoComplete = CustomSelectAutoComplete;
|
window.Hydro.components.CustomSelectAutoComplete = CustomSelectAutoComplete;
|
||||||
@@ -1,42 +1,20 @@
|
|||||||
import React from 'react';
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
import AutoComplete from '.';
|
|
||||||
import DomainSelectAutoCompleteFC from './components/DomainSelectAutoComplete';
|
import DomainSelectAutoCompleteFC from './components/DomainSelectAutoComplete';
|
||||||
|
|
||||||
const Component = React.forwardRef((props: { value: string, multi: boolean, onChange: (v: string) => void }, ref) => {
|
export default class DomainSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||||
const [value, setValue] = React.useState(props.value ?? '');
|
|
||||||
return (
|
|
||||||
<DomainSelectAutoCompleteFC
|
|
||||||
ref={ref as any}
|
|
||||||
height="34px"
|
|
||||||
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
|
||||||
onChange={(v) => {
|
|
||||||
setValue(v);
|
|
||||||
props.onChange(v);
|
|
||||||
}}
|
|
||||||
multi={props.multi}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class DomainSelectAutoComplete extends AutoComplete {
|
|
||||||
static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance';
|
||||||
|
|
||||||
constructor($dom, options) {
|
constructor($dom, options: AutoCompleteOptions<Multi> = {}) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
classes: 'domain-select',
|
classes: 'domain-select',
|
||||||
|
component: DomainSelectAutoCompleteFC,
|
||||||
|
props: {
|
||||||
|
multi: options.multi,
|
||||||
|
height: '34px',
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
|
||||||
const value = this.$dom.val();
|
|
||||||
this.component.render(<Component
|
|
||||||
ref={(ref) => { this.ref = ref; }}
|
|
||||||
value={value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
multi={this.options.multi}
|
|
||||||
/>);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Hydro.components.DomainSelectAutoComplete = DomainSelectAutoComplete;
|
window.Hydro.components.DomainSelectAutoComplete = DomainSelectAutoComplete;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
import FileSelectAutoCompleteFC from './components/FileSelectAutoComplete';
|
import FileSelectAutoCompleteFC from './components/FileSelectAutoComplete';
|
||||||
|
|
||||||
@@ -9,50 +8,21 @@ interface FileSelectOptions {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component = React.forwardRef<any, any>((props, ref) => {
|
|
||||||
const [value, setValue] = React.useState(props.value);
|
|
||||||
return (
|
|
||||||
<FileSelectAutoCompleteFC
|
|
||||||
ref={ref as any}
|
|
||||||
height="auto"
|
|
||||||
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
|
||||||
onChange={(v) => {
|
|
||||||
setValue(v);
|
|
||||||
props.onChange(v);
|
|
||||||
}}
|
|
||||||
data={props.data}
|
|
||||||
multi={props.multi}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete<FileSelectOptions> {
|
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete<FileSelectOptions> {
|
||||||
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
||||||
|
|
||||||
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
classes: 'custom-select',
|
classes: 'file-select',
|
||||||
|
component: FileSelectAutoCompleteFC,
|
||||||
|
props: {
|
||||||
|
data: options.data,
|
||||||
|
multi: options.multi,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
value(): Multi extends true ? number[] : string {
|
|
||||||
if (this.options.multi) return this.ref?.getSelectedItemKeys().map((i) => +i) ?? this.$dom.val();
|
|
||||||
return this.ref?.getSelectedItems()[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
attach() {
|
|
||||||
const value = this.$dom.val();
|
|
||||||
this.component.render(
|
|
||||||
<Component
|
|
||||||
ref={(ref) => { this.ref = ref; }}
|
|
||||||
data={this.options.data}
|
|
||||||
value={value}
|
|
||||||
multi={this.options.multi}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Hydro.components.FileSelectAutoComplete = FileSelectAutoComplete;
|
window.Hydro.components.FileSelectAutoComplete = FileSelectAutoComplete;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
|
import LanguageSelectAutoCompleteFC from './components/LanguageSelectAutoComplete';
|
||||||
|
|
||||||
|
interface LanguageSelectOptions {
|
||||||
|
multi?: boolean;
|
||||||
|
withAuto?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LanguageSelectAutoComplete<Multi extends boolean> extends AutoComplete<LanguageSelectOptions> {
|
||||||
|
static DOMAttachKey = 'ucwLanguageSelectAutoCompleteInstance';
|
||||||
|
|
||||||
|
constructor($dom, options: LanguageSelectOptions & AutoCompleteOptions<Multi>) {
|
||||||
|
super($dom, {
|
||||||
|
classes: 'language-select',
|
||||||
|
component: LanguageSelectAutoCompleteFC,
|
||||||
|
props: {
|
||||||
|
multi: options.multi,
|
||||||
|
withAuto: options.withAuto,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Hydro.components.LanguageSelectAutoComplete = LanguageSelectAutoComplete;
|
||||||
@@ -1,44 +1,20 @@
|
|||||||
import React from 'react';
|
|
||||||
import AutoComplete from '.';
|
import AutoComplete from '.';
|
||||||
import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete';
|
import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete';
|
||||||
|
|
||||||
const Component = React.forwardRef<any, any>((props, ref) => {
|
|
||||||
const [value, setValue] = React.useState(props.value ?? '');
|
|
||||||
return (
|
|
||||||
<ProblemSelectAutoCompleteFC
|
|
||||||
ref={ref as any}
|
|
||||||
height="auto"
|
|
||||||
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
|
||||||
onChange={(v) => {
|
|
||||||
setValue(v);
|
|
||||||
props.onChange(v);
|
|
||||||
}}
|
|
||||||
multi={props.multi}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class ProblemSelectAutoComplete extends AutoComplete {
|
export default class ProblemSelectAutoComplete extends AutoComplete {
|
||||||
static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance';
|
||||||
|
|
||||||
constructor($dom, options) {
|
constructor($dom, options) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
classes: 'problem-select',
|
classes: 'problem-select',
|
||||||
|
component: ProblemSelectAutoCompleteFC,
|
||||||
|
props: {
|
||||||
|
multi: options.multi,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
|
||||||
const value = this.$dom.val();
|
|
||||||
this.component.render(
|
|
||||||
<Component
|
|
||||||
ref={(ref) => { this.ref = ref; }}
|
|
||||||
value={value}
|
|
||||||
multi={this.options.multi}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Hydro.components.ProblemSelectAutoComplete = ProblemSelectAutoComplete;
|
window.Hydro.components.ProblemSelectAutoComplete = ProblemSelectAutoComplete;
|
||||||
@@ -1,29 +1,17 @@
|
|||||||
import React from 'react';
|
|
||||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete';
|
import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete';
|
||||||
|
|
||||||
const Component = React.forwardRef<any, any>((props, ref) => {
|
|
||||||
const [value, setValue] = React.useState(props.value ?? '');
|
|
||||||
return (
|
|
||||||
<UserSelectAutoCompleteFC
|
|
||||||
ref={ref as any}
|
|
||||||
height="auto"
|
|
||||||
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
|
||||||
onChange={(v) => {
|
|
||||||
setValue(v);
|
|
||||||
props.onChange(v);
|
|
||||||
}}
|
|
||||||
multi={props.multi}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class UserSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
export default class UserSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||||
static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance';
|
static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance';
|
||||||
|
|
||||||
constructor($dom, options: AutoCompleteOptions<Multi>) {
|
constructor($dom, options: AutoCompleteOptions<Multi> = {}) {
|
||||||
super($dom, {
|
super($dom, {
|
||||||
classes: 'user-select',
|
classes: 'user-select',
|
||||||
|
component: UserSelectAutoCompleteFC,
|
||||||
|
props: {
|
||||||
|
multi: options.multi,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -32,18 +20,6 @@ export default class UserSelectAutoComplete<Multi extends boolean> extends AutoC
|
|||||||
if (this.options.multi) return this.ref?.getSelectedItemKeys().map((i) => +i) ?? this.$dom.val();
|
if (this.options.multi) return this.ref?.getSelectedItemKeys().map((i) => +i) ?? this.$dom.val();
|
||||||
return this.ref?.getSelectedItems()[0] ?? null;
|
return this.ref?.getSelectedItems()[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
|
||||||
const value = this.$dom.val();
|
|
||||||
this.component.render(
|
|
||||||
<Component
|
|
||||||
ref={(ref) => { this.ref = ref; }}
|
|
||||||
value={value}
|
|
||||||
multi={this.options.multi}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Hydro.components.UserSelectAutoComplete = UserSelectAutoComplete;
|
window.Hydro.components.UserSelectAutoComplete = UserSelectAutoComplete;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
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 }));
|
||||||
|
const withAuto = [...data, { name: 'Auto', _id: 'auto' }];
|
||||||
|
|
||||||
|
const LanguageSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
||||||
|
<CustomSelectAutoComplete
|
||||||
|
ref={ref as any}
|
||||||
|
{...props}
|
||||||
|
data={props.withAuto ? withAuto : data}
|
||||||
|
renderItem={(item) => (prefixes.has(item._id) ? '' : `${item.name}`)}
|
||||||
|
onChange={(val) => {
|
||||||
|
let value = val.split(',');
|
||||||
|
const active = new Set(value.filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
||||||
|
value = value.filter((i) => !prefixes.has(i) || active.has(i));
|
||||||
|
for (const i of active) if (!value.includes(i)) value.push(i);
|
||||||
|
props.onChange(value.join(','));
|
||||||
|
}}
|
||||||
|
multi={!!props.multi}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
LanguageSelectAutoComplete.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),
|
||||||
|
withAuto: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
LanguageSelectAutoComplete.displayName = 'LanguageSelectAutoComplete';
|
||||||
|
|
||||||
|
export default LanguageSelectAutoComplete;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AutoComplete as AutoCompleteFC } from '@hydrooj/components';
|
import { AutoComplete as AutoCompleteFC } from '@hydrooj/components';
|
||||||
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
|
||||||
@@ -18,23 +19,23 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
|||||||
text?: () => string;
|
text?: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AutoComplete<Options extends Record<string, any> = {}> extends DOMAttachedObject {
|
export default class AutoComplete<Options extends Record<string, any> = {}, Multi extends boolean = boolean> extends DOMAttachedObject {
|
||||||
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||||
ref = null;
|
ref = null;
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
options: AutoCompleteOptions & Options;
|
options: AutoCompleteOptions<Multi> & Options;
|
||||||
component = ReactDOM.createRoot(this.container);
|
component = ReactDOM.createRoot(this.container);
|
||||||
changeListener = [
|
changeListener = [
|
||||||
(val) => this.$dom.val(val),
|
(val) => this.$dom.val(val),
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor($dom, options = {} as Options) {
|
constructor($dom, options = {} as Options & { component?: React.ComponentType<any>, props?: Record<string, any> }) {
|
||||||
super($dom);
|
super($dom);
|
||||||
this.options = {
|
this.options = {
|
||||||
items: async () => [],
|
items: async () => [],
|
||||||
render: () => '',
|
render: () => '',
|
||||||
text: () => null,
|
text: () => null,
|
||||||
multi: false,
|
multi: false as Multi,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
this.clear = this.clear.bind(this);
|
this.clear = this.clear.bind(this);
|
||||||
@@ -63,20 +64,21 @@ export default class AutoComplete<Options extends Record<string, any> = {}> exte
|
|||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
const value = this.$dom.val();
|
const Component = this.options.component || AutoCompleteFC;
|
||||||
this.component.render(
|
const Wrapper = (props) => {
|
||||||
<AutoCompleteFC
|
const [value, setValue] = React.useState(props.value);
|
||||||
|
return <Component
|
||||||
ref={(ref) => { this.ref = ref; }}
|
ref={(ref) => { this.ref = ref; }}
|
||||||
|
onChange={(v) => {
|
||||||
|
setValue(v);
|
||||||
|
this.onChange(v);
|
||||||
|
}}
|
||||||
|
selectedKeys={(Array.isArray(value) ? value : value.split(',')).map((i) => i.trim()).filter((i) => i)}
|
||||||
height="34px"
|
height="34px"
|
||||||
queryItems={this.options.items}
|
{...this.options.props}
|
||||||
renderItem={this.options.render}
|
/>;
|
||||||
itemText={this.options.text}
|
};
|
||||||
selectedKeys={value.split(',').map((i) => i.trim())}
|
this.component.render(<Wrapper value={this.$dom.val()} />);
|
||||||
onChange={this.onChange}
|
|
||||||
multi={this.options.multi}
|
|
||||||
freeSolo={this.options.multi}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
@@ -89,8 +91,8 @@ export default class AutoComplete<Options extends Record<string, any> = {}> exte
|
|||||||
this.ref.closeList();
|
this.ref.closeList();
|
||||||
}
|
}
|
||||||
|
|
||||||
value(): any {
|
value(): Multi extends true ? (string | number)[] : string {
|
||||||
if (this.options.multi) return this.$dom.val();
|
if (this.options.multi) return this.ref?.getSelectedItemKeys() ?? this.$dom.val();
|
||||||
return this.ref?.getSelectedItems()[0] ?? null;
|
return this.ref?.getSelectedItems()[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
|
import LanguageSelectAutoComplete from '../autocomplete/components/LanguageSelectAutoComplete';
|
||||||
import type { RootState } from './reducer/index';
|
import type { RootState } from './reducer/index';
|
||||||
|
|
||||||
export function FormItem({
|
export function FormItem({
|
||||||
@@ -56,11 +56,6 @@ export function ManagedSelect({ options, formKey }: { options: string[], formKey
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }) {
|
export function SingleFileSelect({ formKey, withLang = false, label = 'Checker' }: { formKey: FileSelectKey, withLang?: boolean, label?: string }) {
|
||||||
const value = useSelector((state: RootState) => state.config[formKey]);
|
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||||
const Files = useSelector((state: RootState) => state.testdata);
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
@@ -82,10 +77,10 @@ export function SingleFileSelect({ formKey, withLang = false, label = 'Checker'
|
|||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem columns={3} label="Language">
|
<FormItem columns={3} label="Language">
|
||||||
<CustomSelectAutoComplete
|
<LanguageSelectAutoComplete
|
||||||
data={data}
|
|
||||||
selectedKeys={[selectedLang]}
|
selectedKeys={[selectedLang]}
|
||||||
onChange={(val) => update(selectedFile, val)}
|
onChange={(val) => update(selectedFile, val)}
|
||||||
|
withAuto
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</>) : (<FileSelectAutoComplete
|
</>) : (<FileSelectAutoComplete
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
|
||||||
import {
|
import {
|
||||||
Card, InputGroup, Tag,
|
Card, InputGroup, Tag,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
@@ -7,6 +6,7 @@ import React from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
|
import LanguageSelectAutoComplete from '../autocomplete/components/LanguageSelectAutoComplete';
|
||||||
import { FormItem } from './BasicForm';
|
import { FormItem } from './BasicForm';
|
||||||
import ProblemType from './ProblemType';
|
import ProblemType from './ProblemType';
|
||||||
import type { RootState } from './reducer/index';
|
import type { RootState } from './reducer/index';
|
||||||
@@ -67,25 +67,16 @@ function ExtraFilesConfig() {
|
|||||||
|
|
||||||
function LangConfig() {
|
function LangConfig() {
|
||||||
const langs = useSelector((state: RootState) => state.config.langs) || [];
|
const langs = useSelector((state: RootState) => state.config.langs) || [];
|
||||||
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 }));
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const selectedKeys = langs.filter((i) => !prefixes.has(i));
|
|
||||||
return (
|
return (
|
||||||
<FormItem columns={12} label="langs" disableLabel>
|
<FormItem columns={12} label="langs" disableLabel>
|
||||||
<Card style={{ padding: 10 }}>
|
<Card style={{ padding: 10 }}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FormItem columns={12} label="langs">
|
<FormItem columns={12} label="langs">
|
||||||
<CustomSelectAutoComplete
|
<LanguageSelectAutoComplete
|
||||||
data={data}
|
placeholder={!langs.length ? i18n('Unlimited') : i18n('Code language')}
|
||||||
placeholder={!selectedKeys.length ? i18n('Unlimited') : i18n('Code language')}
|
selectedKeys={langs}
|
||||||
selectedKeys={selectedKeys}
|
onChange={(value) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'langs', value })}
|
||||||
onChange={(val) => {
|
|
||||||
const value = val.split(',');
|
|
||||||
value.push(...Array.from(new Set(value.filter((i) => i.includes('.')).map((i) => i.split('.')[0]))));
|
|
||||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'langs', value });
|
|
||||||
}}
|
|
||||||
multi
|
multi
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -821,7 +821,9 @@ Section: 章节
|
|||||||
Security: 安全
|
Security: 安全
|
||||||
Select a node to create discussion.: 选择一个节点来发表讨论。
|
Select a node to create discussion.: 选择一个节点来发表讨论。
|
||||||
Select a role: 选择一个角色
|
Select a role: 选择一个角色
|
||||||
|
Select All: 全选
|
||||||
Select Category: 选择标签
|
Select Category: 选择标签
|
||||||
|
Select None: 全不选
|
||||||
Select User: 选择用户
|
Select User: 选择用户
|
||||||
Selected categories: 已选标签
|
Selected categories: 已选标签
|
||||||
Selected files have been deleted.: 所选文件已被删除。
|
Selected files have been deleted.: 所选文件已被删除。
|
||||||
@@ -884,6 +886,7 @@ Start at: 开始于
|
|||||||
Statistics: 统计
|
Statistics: 统计
|
||||||
Status is hidden: 状态已隐藏
|
Status is hidden: 状态已隐藏
|
||||||
Status: 状态
|
Status: 状态
|
||||||
|
Submission language limit: 提交语言限制
|
||||||
Submission Statistics: 递交统计
|
Submission Statistics: 递交统计
|
||||||
Submission: 递交
|
Submission: 递交
|
||||||
Submissions: 递交
|
Submissions: 递交
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete';
|
||||||
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
|
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
|
||||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||||
import { ConfirmDialog } from 'vj/components/dialog';
|
import { ConfirmDialog } from 'vj/components/dialog';
|
||||||
@@ -9,6 +10,7 @@ import { i18n, request, tpl } from 'vj/utils';
|
|||||||
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
|
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
|
||||||
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
|
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
|
||||||
UserSelectAutoComplete.getOrConstruct<true>($('[name="maintainer"]'), { multi: true, clearDefaultValue: false });
|
UserSelectAutoComplete.getOrConstruct<true>($('[name="maintainer"]'), { multi: true, clearDefaultValue: false });
|
||||||
|
LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true });
|
||||||
$('[name="rule"]').on('change', () => {
|
$('[name="rule"]').on('change', () => {
|
||||||
const rule = $('[name="rule"]').val();
|
const rule = $('[name="rule"]').val();
|
||||||
$('.contest-rule-settings input').attr('disabled', 'disabled');
|
$('.contest-rule-settings input').attr('disabled', 'disabled');
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import CustomSelectAutoComplete from 'vj/components/autocomplete/CustomSelectAutoComplete';
|
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
|
||||||
|
|
||||||
const page = new NamedPage('domain_edit', () => {
|
|
||||||
const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
|
||||||
const langs = 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 }
|
|
||||||
));
|
|
||||||
|
|
||||||
const $langSelect = $('[name=langs]');
|
|
||||||
$langSelect.val(($langSelect.val() as string).split(',').filter((i) => !prefixes.has(i)));
|
|
||||||
|
|
||||||
const select: CustomSelectAutoComplete<true> = CustomSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true, data: langs });
|
|
||||||
select.onChange((val) => {
|
|
||||||
const value = val.split(',');
|
|
||||||
value.push(...new Set(value.filter((i) => i.includes('.')).map((i) => i.split('.')[0])));
|
|
||||||
$langSelect.val(value.join(','));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default page;
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete';
|
||||||
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
|
|
||||||
|
const page = new NamedPage('domain_edit', () => {
|
||||||
|
LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default page;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import React from 'react';
|
||||||
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
|
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
|
||||||
import { ActionDialog } from 'vj/components/dialog';
|
import { ActionDialog } from 'vj/components/dialog';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
|
|||||||
@@ -132,6 +132,14 @@
|
|||||||
<h2 class="section__title">{{ _('Contest Settings') }}</h2>
|
<h2 class="section__title">{{ _('Contest Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
|
{{ form.form_text({
|
||||||
|
columns:12,
|
||||||
|
label:'Submission language limit',
|
||||||
|
name:'langs',
|
||||||
|
value:tdoc.langs|default(''),
|
||||||
|
placeholder:_("Seperated with ','"),
|
||||||
|
row:true
|
||||||
|
}) }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ form.form_checkbox({
|
{{ form.form_checkbox({
|
||||||
columns:2,
|
columns:2,
|
||||||
|
|||||||
@@ -97,6 +97,14 @@
|
|||||||
value:tdoc['content']|default(''),
|
value:tdoc['content']|default(''),
|
||||||
markdown:true
|
markdown:true
|
||||||
}) }}
|
}) }}
|
||||||
|
{{ form.form_text({
|
||||||
|
columns:12,
|
||||||
|
label:'Submission language limit',
|
||||||
|
name:'langs',
|
||||||
|
value:tdoc.langs|default(''),
|
||||||
|
placeholder:_("Seperated with ','"),
|
||||||
|
row:true
|
||||||
|
}) }}
|
||||||
<div class="row"><div class="columns">
|
<div class="row"><div class="columns">
|
||||||
{% if page_name == 'homework_edit' %}
|
{% if page_name == 'homework_edit' %}
|
||||||
<button name="operation" value="update" type="submit" class="rounded primary button">
|
<button name="operation" value="update" type="submit" class="rounded primary button">
|
||||||
|
|||||||
Reference in new issue
Block a user