-
-
Notifications
You must be signed in to change notification settings - Fork 470
core&ui: contest can limit submission language #1007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
f21c6ea
51f7a66
2617b47
102bbac
eff7801
da38fa5
33281bf
2ad088f
bb011cc
b461eb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| 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,77 @@ | ||
| import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/components'; | ||
| import PropTypes from 'prop-types'; | ||
| import React, { forwardRef } from 'react'; | ||
|
|
||
| interface LanguageFakeDoc { | ||
| _id: string | ||
| name: string | ||
| } | ||
| const LanguageSelectAutoComplete = forwardRef<AutoCompleteHandle<LanguageFakeDoc>, AutoCompleteProps<LanguageFakeDoc>>((props, ref) => ( | ||
| <AutoComplete<LanguageFakeDoc> | ||
| ref={ref as any} | ||
| cacheKey={`language-${UiContext.domainId}`} | ||
| queryItems={async (query) => { | ||
| console.log('query', query); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0])); | ||
| const listAll = 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 q = query.toLocaleLowerCase(); | ||
| return listAll.filter((el) => | ||
| el._id.toLocaleLowerCase().includes(q) || el.name.toLocaleLowerCase().includes(q), | ||
| ); | ||
| }} | ||
| fetchItems={async (ids) => { | ||
| // api('problems', { ids: ids.map((i) => +i) }, ['docId', 'pid', 'title']) | ||
| console.log('ids', ids); | ||
| const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0])); | ||
| const listAll = 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, | ||
| })); | ||
| return listAll; | ||
| }} | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -79,6 +79,22 @@ <h2 class="section__title">{{ _('Basic Info') }}</h2> | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| markdown:true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="section__body"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="row"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ form.form_checkbox({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| columns:9, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label:'Limit language', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name:'limitLang', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder:_('Limit contest submission language'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value:isLimitLang, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| row:false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled:true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="language-select-part" {% if not isLimitLang %}style="display:none;"{% endif %}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input name="limitLangList" value="{{limitLangListString}}"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve form accessibility and structure. The language limitation section has several accessibility and usability issues:
<div class="section__body">
<div class="row">
{{ form.form_checkbox({
columns:9,
label:'Limit language',
name:'limitLang',
placeholder:_('Limit contest submission language'),
value:isLimitLang,
row:false,
- disabled:true
+ disabled:true,
+ help_text:_('(Read-only)')
}) }}
</div>
<div id="language-select-part"{% if not isLimitLang %} style="display:none;"{% endif %}>
- <input name="limitLangList" value="{{limitLangListString}}">
+ <input type="hidden" name="limitLangList" value="{{limitLangListString}}" aria-label="Selected languages list">
+ {% if isLimitLang and limitLangListString %}
+ <div class="form-item">
+ <label class="form-item__label">{{ _('Selected Languages') }}</label>
+ <div class="form-item__body">
+ <span class="text-muted">{{ limitLangListString }}</span>
+ </div>
+ </div>
+ {% endif %}
</div>
</div>📝 Committable suggestion
Suggested change
🧰 Tools🪛 HTMLHint (1.5.0)[error] 94-94: Special characters must be escaped : [ < ]. (spec-char-escape) [error] 94-94: Special characters must be escaped : [ > ]. (spec-char-escape) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div style="padding-top: 0" class="section__header"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2 class="section__title">{{ _('Permission Control') }}</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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
🤖 Prompt for AI Agents