mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: assignment auto complete (#1061)
This commit is contained in:
1 parent
f32cee2200
commit
564ff1b02d
6 files changed
+177
-1
No files matched your search
@@ -112,10 +112,16 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
||||
setCurrentItem(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
queryCache[query] ||= await queryItems(query);
|
||||
for (const item of queryCache[query]) valueCache[itemKey(item)] = item;
|
||||
setItemList(queryCache[query]);
|
||||
setCurrentItem((!freeSolo && queryCache[query].length) ? 0 : null);
|
||||
} catch (e) {
|
||||
console.error('Failed to query items', e);
|
||||
setItemList([]);
|
||||
setCurrentItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -135,6 +141,8 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
||||
Promise.resolve(props.fetchItems(ids)).then((items) => {
|
||||
for (const item of items) valueCache[itemKey(item)] = item;
|
||||
setRerender(!rerender);
|
||||
}).catch((e) => {
|
||||
console.error('Failed to fetch items', e);
|
||||
});
|
||||
}, [selectedKeys, multi]);
|
||||
|
||||
@@ -287,9 +295,13 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
|
||||
e.preventDefault();
|
||||
const ids = text.replace(/,/g, ',').split(',').filter((v) => v?.trim().length && !selectedKeys.includes(v));
|
||||
if (!ids.length) return;
|
||||
try {
|
||||
const fetched = await props.fetchItems(ids);
|
||||
for (const item of fetched) valueCache[itemKey(item)] = item;
|
||||
setSelectedKeys([...selectedKeys, ...fetched.map((val) => itemKey(val))]);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch items on paste', err);
|
||||
}
|
||||
}}
|
||||
placeholder={props.placeholder}
|
||||
onBlur={() => setFocused(false)}
|
||||
|
||||
@@ -433,6 +433,25 @@ export const DomainApi = {
|
||||
return ddoc;
|
||||
},
|
||||
),
|
||||
groups: Query(
|
||||
Schema.object({
|
||||
search: Schema.string(),
|
||||
names: Schema.array(Schema.string()),
|
||||
domainId: Schema.string().required(),
|
||||
}),
|
||||
async (ctx, args) => {
|
||||
if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
|
||||
const groups = await user.listGroup(args.domainId);
|
||||
if (args.names?.length) {
|
||||
return groups.filter((g) => args.names.includes(g.name));
|
||||
}
|
||||
if (args.search) {
|
||||
const searchLower = args.search.toLowerCase();
|
||||
return groups.filter((g) => g.name.toLowerCase().includes(searchLower));
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
),
|
||||
'domain.group': Mutation(
|
||||
Schema.object({
|
||||
name: Schema.string().required(),
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user