core&ui: assignment auto complete (#1061)

This commit is contained in:
Tairui Qu authored and GitHub committed 2025-10-10 13:43:38 +01:00
1 parent f32cee2200
commit 564ff1b02d
6 files changed
+184 -8

No files matched your search

+2 -1
View File
@@ -24,13 +24,14 @@ export default load;
export interface EventMap { }
import AutoComplete from './components/autocomplete';
import AssignSelectAutoComplete from './components/autocomplete/AssignSelectAutoComplete';
import CustomSelectAutoComplete from './components/autocomplete/CustomSelectAutoComplete';
import DomainSelectAutoComplete from './components/autocomplete/DomainSelectAutoComplete';
import ProblemSelectAutoComplete from './components/autocomplete/ProblemSelectAutoComplete';
import UserSelectAutoComplete from './components/autocomplete/UserSelectAutoComplete';
export {
AutoComplete, CustomSelectAutoComplete, DomainSelectAutoComplete, ProblemSelectAutoComplete, UserSelectAutoComplete,
AssignSelectAutoComplete, AutoComplete, CustomSelectAutoComplete, DomainSelectAutoComplete, ProblemSelectAutoComplete, UserSelectAutoComplete,
};
export function addPage(page: import('./misc/Page').Page | (() => Promise<void> | void)) {
window.Hydro.extraPages.push(page);
@@ -0,0 +1,24 @@
import AutoComplete, { AutoCompleteOptions } from '.';
import AssignSelectAutoCompleteFC from './components/AssignSelectAutoComplete';
export default class AssignSelectAutoComplete<Multi extends boolean> extends AutoComplete {
static DOMAttachKey = 'ucwAssignSelectAutoCompleteInstance';
constructor($dom, options: AutoCompleteOptions<Multi> = {}) {
super($dom, {
classes: 'assign-select',
component: AssignSelectAutoCompleteFC,
props: {
multi: true,
height: 'auto',
},
...options,
});
}
value(): string {
return this.ref?.getSelectedItemKeys().join(',') ?? this.$dom.val();
}
}
window.Hydro.components.AssignSelectAutoComplete = AssignSelectAutoComplete;
@@ -0,0 +1,118 @@
import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/components';
import type { GDoc, Udoc } from 'hydrooj/src/interface';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';
import { api } from 'vj/utils';
interface AssignItem {
type: 'user' | 'group';
key: string;
name: string;
displayName?: string;
avatarUrl?: string;
uids?: number[];
invalid?: boolean;
}
const toUserItem = (user: Udoc): AssignItem => ({
type: 'user',
key: user._id.toString(),
name: user.uname,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
});
const toGroupItem = (group: GDoc): AssignItem => ({
type: 'group',
key: group.name,
name: group.name,
uids: group.uids,
});
const AssignSelectAutoComplete = forwardRef<AutoCompleteHandle<AssignItem>, AutoCompleteProps<AssignItem>>((props, ref) => (
<AutoComplete<AssignItem>
ref={ref as any}
cacheKey="assign"
queryItems={async (query) => {
const [users, groups] = await Promise.all([
api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl']),
api('groups', { search: query }, ['name', 'uids']),
]);
const userItems: AssignItem[] = users.map((user: Udoc) => toUserItem(user));
const groupItems: AssignItem[] = groups.map((group: GDoc) => toGroupItem(group));
return [...groupItems, ...userItems];
}}
fetchItems={async (keys) => {
const isUserId = (k: string) => /^-?[0-9]+$/.test(k);
const userIds = keys.filter((k) => isUserId(k));
const groupNames = keys.filter((k) => !isUserId(k));
const [users, groups]: [Udoc[], GDoc[]] = await Promise.all([
userIds.length > 0 ? api('users', { auto: userIds }, ['_id', 'uname', 'displayName']) : [],
groupNames.length > 0 ? api('groups', { names: groupNames }, ['name', 'uids']) : [],
]);
const userItems: AssignItem[] = users.map((user: Udoc) => toUserItem(user));
const groupItems: AssignItem[] = keys
.filter((key) => !isUserId(key))
.map((key) => {
const group = groups.find((g) => g.name === key);
return group ? toGroupItem(group) : { type: 'group', key, name: key, invalid: true };
});
return [...groupItems, ...userItems];
}}
itemText={(item) => {
if (item.type === 'group') {
if (item.invalid) return `${item.name} (invalid)`;
return `${item.name} (${item.uids?.length || 0} users)`;
}
return item.name + (item.displayName ? ` (${item.displayName})` : '');
}}
itemKey={(item) => item.key}
renderItem={(item) => (
<div className="media">
{item.type === 'user' && (
<div className="media__left medium">
<img className="small user-profile-avatar" alt="" src={item.avatarUrl} width="30" height="30" />
</div>
)}
<div className="media__body medium">
<div className="assign-select__name">
{item.name}{item.type === 'user' && item.displayName && ` (${item.displayName})`}
</div>
<div className="assign-select__desc">
{item.type === 'group' ? `Group • ${item.uids?.length || 0} users` : `User • UID = ${item.key}`}
</div>
</div>
</div>
)}
{...{
width: '100%',
height: 'auto',
listStyle: {},
multi: true,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
...props,
}}
/>
));
AssignSelectAutoComplete.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,
};
AssignSelectAutoComplete.displayName = 'AssignSelectAutoComplete';
export default AssignSelectAutoComplete;
@@ -1,5 +1,6 @@
import $ from 'jquery';
import moment from 'moment';
import AssignSelectAutoComplete from 'vj/components/autocomplete/AssignSelectAutoComplete';
import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete';
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
@@ -11,6 +12,7 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create',
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
UserSelectAutoComplete.getOrConstruct<true>($('[name="maintainer"]'), { multi: true, clearDefaultValue: false });
LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true });
AssignSelectAutoComplete.getOrConstruct($('[name="assign"]'), { multi: true });
$('[name="rule"]').on('change', () => {
const rule = $('[name="rule"]').val();
$('.contest-rule-settings input').attr('disabled', 'disabled');