Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/hydrooj/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ contest_scoreboard: 比赛成绩表
contest_user: 参赛者管理
contest: 比赛
Contest: 比赛
Contest/Homework allow submit languages: 比赛/作业允许使用的提交语言
Contests per page: 每页展示的比赛数量
Continue: 继续
Contributed Problems: 贡献的题目
Expand Down
6 changes: 5 additions & 1 deletion packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,15 @@ export class ContestEditHandler extends Handler {
let ts = Date.now();
ts = ts - (ts % (15 * Time.minute)) + 15 * Time.minute;
const beginAt = moment(this.tdoc?.beginAt || new Date(ts)).tz(this.user.timeZone);

this.response.body = {
rules,
tdoc: this.tdoc,
duration: tid ? -beginAt.diff(this.tdoc.endAt, 'hour', true) : 2,
pids: tid ? this.tdoc.pids.join(',') : '',
beginAt,
page_name: tid ? 'contest_edit' : 'contest_create',
limitLangListString: (this?.tdoc?.limitLangList || []).join(','),
};
}

Expand All @@ -305,11 +307,13 @@ export class ContestEditHandler extends Handler {
@param('contestDuration', Types.Float, true)
@param('maintainer', Types.NumericArray, true)
@param('allowViewCode', Types.Boolean)
@param('limitLangList', Types.CommaSeperatedArray, true)
async postUpdate(
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number,
title: string, content: string, rule: string, _pids: string, rated = false,
_code = '', autoHide = false, assign: string[] = [], lock: number = null,
contestDuration: number = null, maintainer: number[] = [], allowViewCode = false,
limitLangList: string[] = [],
) {
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
Expand Down Expand Up @@ -350,7 +354,7 @@ export class ContestEditHandler extends Handler {
});
}
await contest.edit(domainId, tid, {
assign, _code, autoHide, lockAt, maintainer, allowViewCode,
assign, _code, autoHide, lockAt, maintainer, allowViewCode, limitLangList,
});
this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid });
Expand Down
4 changes: 4 additions & 0 deletions packages/hydrooj/src/handler/homework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class HomeworkEditHandler extends Handler {
penaltyRules: tid ? yaml.dump(tdoc.penaltyRules) : null,
pids: tid ? tdoc.pids.join(',') : '',
page_name: tid ? 'homework_edit' : 'homework_create',
limitLangListString: (tdoc?.limitLangList || []).join(','),
};
}

Expand All @@ -191,11 +192,13 @@ class HomeworkEditHandler extends Handler {
@param('rated', Types.Boolean)
@param('maintainer', Types.NumericArray, true)
@param('assign', Types.CommaSeperatedArray, true)
@param('limitLangList', Types.CommaSeperatedArray, true)
async postUpdate(
domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string,
penaltySinceDate: string, penaltySinceTime: string, extensionDays: number,
penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false,
maintainer: number[] = [], assign: string[] = [],
limitLangList: string[] = [],
) {
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
const tdoc = tid ? await contest.get(domainId, tid) : null;
Expand Down Expand Up @@ -226,6 +229,7 @@ class HomeworkEditHandler extends Handler {
rated,
maintainer,
assign,
limitLangList,
});
if (tdoc.beginAt !== beginAt.toDate()
|| tdoc.endAt !== endAt.toDate()
Expand Down
4 changes: 4 additions & 0 deletions packages/hydrooj/src/handler/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
(needHiddenLangs ? !setting.langs[i].remote : !setting.langs[i].remote && !setting.langs[i].hidden));
}
this.pdoc.config.langs = ['objective', 'submit_answer'].includes(this.pdoc.config.type) ? ['_'] : intersection(baseLangs, ...t);
// apply contest language limits
if (this?.tdoc && Array.isArray(this.tdoc?.limitLangList) && this.tdoc?.limitLangList.length > 0) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: after this filter, objective or submit_answer problem will have empty langList.
Will be fixed in next commit.

this.pdoc.config.langs = Array.from(Set.intersection(this.pdoc.config.langs, this.tdoc.limitLangList));
}
}
await this.ctx.parallel('problem/get', this.pdoc, this);
[this.psdoc, this.udoc] = await Promise.all([
Expand Down
3 changes: 3 additions & 0 deletions packages/hydrooj/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ export interface Tdoc extends Document {
balloon?: Record<number, string | { color: string, name: string }>;
score?: Record<number, number>;

// 允许使用的语言, undefined or len=0 则允许全部
limitLangList?: string[];

/**
* In hours
* 在比赛有效时间内选择特定的 X 小时参加比赛(从首次打开比赛算起)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import AutoComplete from '.';
import LanguageSelectAutoCompleteFC from './components/LanguageSelectAutoComplete';

const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(props.value ?? '');
return (
<LanguageSelectAutoCompleteFC
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}
allowEmptyQuery={true}
/>
);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve value parsing robustness and error handling.

The current value splitting logic may fail with malformed input and lacks error handling for the onChange callback.

 const Component = React.forwardRef<any, any>((props, ref) => {
-  const [value, setValue] = React.useState(props.value ?? '');
+  const [value, setValue] = React.useState(() => {
+    const initialValue = props.value ?? '';
+    return typeof initialValue === 'string' ? initialValue : String(initialValue);
+  });
+  
+  const parseValue = (val: string) => {
+    if (!val || typeof val !== 'string') return [];
+    return val.split(',').map((i) => i.trim()).filter((i) => i);
+  };
+  
   return (
     <LanguageSelectAutoCompleteFC
       ref={ref as any}
       height="auto"
-      selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
+      selectedKeys={parseValue(value)}
       onChange={(v) => {
-        setValue(v);
-        props.onChange(v);
+        try {
+          setValue(v);
+          props.onChange?.(v);
+        } catch (error) {
+          console.error('Error in LanguageSelectAutoComplete onChange:', error);
+        }
       }}
       multi={props.multi}
       allowEmptyQuery={true}
     />
   );
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(props.value ?? '');
return (
<LanguageSelectAutoCompleteFC
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}
allowEmptyQuery={true}
/>
);
});
const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(() => {
const initialValue = props.value ?? '';
return typeof initialValue === 'string' ? initialValue : String(initialValue);
});
const parseValue = (val: string) => {
if (!val || typeof val !== 'string') return [];
return val.split(',').map((i) => i.trim()).filter((i) => i);
};
return (
<LanguageSelectAutoCompleteFC
ref={ref as any}
height="auto"
selectedKeys={parseValue(value)}
onChange={(v) => {
try {
setValue(v);
props.onChange?.(v);
} catch (error) {
console.error('Error in LanguageSelectAutoComplete onChange:', error);
}
}}
multi={props.multi}
allowEmptyQuery={true}
/>
);
});
🤖 Prompt for AI Agents
In packages/ui-default/components/autocomplete/LanguageSelectAutoComplete.tsx
around lines 5 to 20, improve the robustness of the value parsing by safely
handling cases where props.value might be undefined or not a string, and ensure
the splitting and trimming logic gracefully handles malformed input.
Additionally, wrap the props.onChange call in a try-catch block to handle any
potential errors during the callback execution without breaking the component.


export default class LanguageSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'ucwLanguageSelectAutoCompleteInstance';

constructor($dom, options) {
super($dom, {
classes: 'language-select',
...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.LanguageSelectAutoComplete = LanguageSelectAutoComplete;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/components';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';

interface LanguageFakeDoc {
_id: string
name: string
}

const getLanguagesDocList = (): LanguageFakeDoc[] => {
try {
if (!window.LANGS || typeof window.LANGS !== 'object') {
console.error('window.LANGS is not available or invalid');
return [];
}

const prefixes = new Set(
Object.keys(window.LANGS)
.filter((i) => i.includes('.'))
.map((i) => i.split('.')[0]),
);

return 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,
}));
} catch (error) {
console.error('Error processing languages:', error);
return [];
}
};

const LanguageSelectAutoComplete = forwardRef<AutoCompleteHandle<LanguageFakeDoc>, AutoCompleteProps<LanguageFakeDoc>>((props, ref) => (
<AutoComplete<LanguageFakeDoc>
ref={ref as any}
cacheKey={`language-${UiContext.domainId}`}
queryItems={async (query) => {
const q = query.toLocaleLowerCase();
return getLanguagesDocList().filter((el) =>
el._id.toLocaleLowerCase().includes(q) || el.name.toLocaleLowerCase().includes(q),
);
}}
fetchItems={async (ids) => getLanguagesDocList().filter((el) => ids.includes(el._id))}
itemText={(pdoc) => pdoc.name}
itemKey={(pdoc) => pdoc._id}
renderItem={(pdoc) => (
<div className="media">
<div className="media__body medium">
<div className="language-select__name">{pdoc.name}</div>
<div className="language-select__id">{pdoc._id}</div>
</div>
</div>
)}
{...{
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
...props,
}}
/>
));

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,
};

LanguageSelectAutoComplete.displayName = 'LanguageSelectAutoComplete';

export default LanguageSelectAutoComplete;
3 changes: 3 additions & 0 deletions packages/ui-default/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ Ledo: 乐多
Legacy mode: 兼容模式
Light: 亮色
Limit user to finish contest within N hours: 限制用户在N小时内完成比赛
Limit contest submission language: 限制比赛可提交的语言
Limitations: 限制
Links: 链接
List View: 列表视图
Expand Down Expand Up @@ -821,6 +822,8 @@ Section: 章节
Security: 安全
Select a node to create discussion.: 选择一个节点来发表讨论。
Select a role: 选择一个角色
Select All: 全选
Select None: 全不选
Select Category: 选择标签
Select User: 选择用户
Selected categories: 已选标签
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-default/pages/contest_edit.page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import $ from 'jquery';
import moment from 'moment';
import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete';
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
import { ConfirmDialog } from 'vj/components/dialog';
Expand All @@ -9,6 +10,7 @@ import { i18n, request, tpl } from 'vj/utils';
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
UserSelectAutoComplete.getOrConstruct<true>($('[name="maintainer"]'), { multi: true, clearDefaultValue: false });
LanguageSelectAutoComplete.getOrConstruct($('[name="limitLangList"]'), { multi: true, clearDefaultValue: false });
$('[name="rule"]').on('change', () => {
const rule = $('[name="rule"]').val();
$('.contest-rule-settings input').attr('disabled', 'disabled');
Expand Down
5 changes: 5 additions & 0 deletions packages/ui-default/templates/contest_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ <h2 class="section__title">{{ _('Basic Info') }}</h2>
placeholder:_("Seperated with ','"),
row:true
}) }}
{{ form.form_begin({columns:null,label:_('Contest/Homework allow submit languages')}) }}
<div id="language-select-part">
<input name="limitLangList" value="{{limitLangListString}}">
</div>
{{ form.form_end({}) }}
{{ form.form_textarea({
columns:null,
label:'Description',
Expand Down
5 changes: 5 additions & 0 deletions packages/ui-default/templates/homework_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
name:'pids',
value:pids
}) }}
{{ form.form_begin({columns:null,label:_('Contest/Homework allow submit languages')}) }}
<div id="language-select-part">
<input name="limitLangList" value="{{limitLangListString}}">
</div>
{{ form.form_end({}) }}
{{ form.form_textarea({
columns:null,
label:'Content',
Expand Down