Skip to content
Merged
19 changes: 19 additions & 0 deletions packages/hydrooj/src/handler/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,25 @@ export const DomainApi = {
return ddoc;
},
),
groups: Query(
Schema.object({
search: Schema.string(),
names: Schema.array(Schema.string()),
Comment thread
TaiRuiQu marked this conversation as resolved.
domainId: Schema.string(),
}),
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);
Comment thread
TaiRuiQu marked this conversation as resolved.
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;
},
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'domain.group': Mutation(
Schema.object({
name: Schema.string().required(),
Expand Down
3 changes: 2 additions & 1 deletion packages/ui-default/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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];
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fetchItems={async (keys) => {
const isUserId = (k: string) => /^-?[0-9]+$/.test(k);
const userIds: string[] = keys.filter((k) => isUserId(k));
const groupNames: string[] = 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']) : [],
Comment thread
TaiRuiQu marked this conversation as resolved.
]);

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];
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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>
) : null}
Comment thread
TaiRuiQu marked this conversation as resolved.
Outdated
<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}</>}
Comment thread
TaiRuiQu marked this conversation as resolved.
Outdated
</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,
};
Comment thread
TaiRuiQu marked this conversation as resolved.

AssignSelectAutoComplete.displayName = 'AssignSelectAutoComplete';

export default AssignSelectAutoComplete;
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 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';
Expand All @@ -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');
Expand Down