diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index e1680395b..d1220be3a 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -72,10 +72,13 @@ ScheduleModel.Worker.addHandler('contest', async (doc) => { export class ContestListHandler extends Handler { @param('rule', Types.Range(contest.RULES), true) @param('page', Types.PositiveInt, true) - async get(domainId: string, rule = '', page = 1) { + @param('all', Types.Boolean) + async get(domainId: string, rule = '', page = 1, all = false) { if (rule && contest.RULES[rule].hidden) throw new BadRequestError(); const rules = Object.keys(contest.RULES).filter((i) => !contest.RULES[i].hidden); - const cursor = contest.getMulti(domainId, rule ? { rule } : { rule: { $in: rules } }); + if (all && !this.user.hasPerm(PERM.PERM_MOD_BADGE)) all = false; + const q = { ...all ? { assign: { $in: [...this.user.group, null] } } : {}, ...rule ? { rule } : { rule: { $in: rules } } }; + const cursor = contest.getMulti(domainId, q); const qs = rule ? `rule=${rule}` : ''; const [tdocs, tpcount] = await paginate(cursor, page, system.get('pagination.contest')); const tids = []; diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 2ed9a5f00..943aeca46 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -334,9 +334,14 @@ class DomainJoinHandler extends Handler { } class DomainSearchHandler extends Handler { - @param('q', Types.Content) - async get(domainId: string, q: string) { - const ddocs = await domain.getPrefixSearch(q, 20); + @param('q', Types.Content, true) + async get(domainId: string, q: string = '') { + let ddocs: DomainDoc[] = []; + if (!q) { + const dudict = await domain.getDictUserByDomainId(this.user._id); + const dids = Object.keys(dudict); + ddocs = await domain.getMulti({ _id: { $in: dids } }).toArray(); + } else ddocs = await domain.getPrefixSearch(q, 20); for (let i = 0; i < ddocs.length; i++) { ddocs[i].avatarUrl = ddocs[i].avatar ? avatar(ddocs[i].avatar, 64) : '/img/team_avatar.png'; } diff --git a/packages/hydrooj/src/handler/home.ts b/packages/hydrooj/src/handler/home.ts index 8d2546fff..ba773d86b 100644 --- a/packages/hydrooj/src/handler/home.ts +++ b/packages/hydrooj/src/handler/home.ts @@ -438,6 +438,18 @@ class HomeDomainHandler extends Handler { this.response.template = 'home_domain.html'; this.response.body = { ddocs, dudict, canManage }; } + + @param('id', Types.String) + async postStar(domainId: string, id: string) { + await user.setById(this.user._id, { pinnedDomains: this.user.pinnedDomains.concat(id) }); + this.back({ star: true }); + } + + @param('id', Types.String) + async postUnstar(domainId: string, id: string) { + await user.setById(this.user._id, { pinnedDomains: this.user.pinnedDomains.filter((i) => i !== id) }); + this.back({ star: false }); + } } class HomeDomainCreateHandler extends Handler { @@ -455,8 +467,11 @@ class HomeDomainCreateHandler extends Handler { if (doc) throw new DomainAlreadyExistsError(id); avatar = avatar || this.user.avatar || `gravatar:${this.user.mail}`; const domainId = await domain.add(id, this.user._id, name, bulletin); - await domain.edit(domainId, { avatar }); - await domain.setUserRole(domainId, this.user._id, 'root'); + await Promise.all([ + domain.edit(domainId, { avatar }), + domain.setUserRole(domainId, this.user._id, 'root'), + user.setById(this.user._id, undefined, undefined, { pinnedDomains: [domainId] }), + ]); this.response.redirect = this.url('domain_dashboard', { domainId }); this.response.body = { domainId }; } diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 2296cf42a..eeb7c8a53 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -22,8 +22,10 @@ const convertPenaltyRules = validatePenaltyRules; class HomeworkMainHandler extends Handler { @param('page', Types.PositiveInt, true) - async get(domainId: string, page = 1) { - const cursor = contest.getMulti(domainId, { rule: 'homework' }); + @param('all', Types.Boolean) + async get(domainId: string, page = 1, all = false) { + if (all && !this.user.hasPerm(PERM.PERM_MOD_BADGE)) all = false; + const cursor = contest.getMulti(domainId, { rule: 'homework', ...all ? { assign: { $in: [...this.user.group, null] } } : {} }); const [tdocs, tpcount] = await paginate(cursor, page, system.get('pagination.contest')); const calendar = []; for (const tdoc of tdocs) { diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 4e9f5e986..cc6e4e202 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -200,6 +200,7 @@ AccountSetting( Setting('setting_storage', 'unreadMsg', 0, 'number', 'Unread Message Count', null, FLAG_DISABLED | FLAG_HIDDEN), Setting('setting_storage', 'badge', '', 'text', 'badge info', null, FLAG_DISABLED | FLAG_HIDDEN), Setting('setting_storage', 'banReason', '', 'text', 'ban reason', null, FLAG_DISABLED | FLAG_HIDDEN), + Setting('setting_storage', 'pinnedDomains', [], 'json', 'pinned domains', null, FLAG_DISABLED | FLAG_HIDDEN), ); DomainSetting( diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index 113cc59e3..f32d02096 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -6,6 +6,7 @@ import { Authenticator, BaseUserDict, FileInfo, GDoc, ownerInfo, Udict, Udoc, VUdoc, } from '../interface'; +import avatar from '../lib/avatar'; import pwhash from '../lib/hash.hydro'; import serializer from '../lib/serializer'; import * as bus from '../service/bus'; @@ -96,9 +97,10 @@ export class User { this.perm = dudoc.perm || 0n; // This is a fallback for unknown user this.scope = typeof scope === 'string' ? BigInt(scope) : scope; this.role = dudoc.role || 'default'; + this.domains = udoc.domains || []; this.tfa = !!udoc.tfa; this.authn = (udoc.authenticators || []).length > 0; - if (dudoc.group) this.group = [...dudoc.group, this._id.toString()]; + if (dudoc.group) this.group = dudoc.group; for (const key in setting.SETTINGS_BY_KEY) { this[key] = udoc[key] ?? (setting.SETTINGS_BY_KEY[key].value || system.get(`preference.${key}`)); @@ -154,6 +156,11 @@ export class User { async private() { const user = await new User(this._udoc, this._dudoc, this.scope).init(); + user.avatarUrl = avatar(user.avatar, 128); + if (user.pinnedDomains instanceof Array) { + const result = await Promise.allSettled(user.pinnedDomains.slice(0, 10).map((i) => domain.get(i))); + user.domains = result.map((i) => (i.status === 'fulfilled' ? i.value : null)).filter((i) => i); + } user._isPrivate = true; return user; } @@ -251,11 +258,12 @@ class UserModel { } @ArgMethod - static async setById(uid: number, $set?: Partial, $unset?: Value, ''>) { + static async setById(uid: number, $set?: Partial, $unset?: Value, ''>, $push?: any) { if (uid < -999) return null; const op: any = {}; if ($set && Object.keys($set).length) op.$set = $set; if ($unset && Object.keys($unset).length) op.$unset = $unset; + if ($push && Object.keys($push).length) op.$push = $push; if (op.$set?.loginip) op.$addToSet = { ip: op.$set.loginip }; const res = await coll.findOneAndUpdate({ _id: uid }, op, { returnDocument: 'after' }); deleteUserCache(res.value); diff --git a/packages/hydrooj/src/service/layers/user.ts b/packages/hydrooj/src/service/layers/user.ts index 3559068b9..264720f4d 100644 --- a/packages/hydrooj/src/service/layers/user.ts +++ b/packages/hydrooj/src/service/layers/user.ts @@ -1,5 +1,4 @@ import { NotFoundError } from '../../error'; -import avatar from '../../lib/avatar'; import { PERM } from '../../model/builtin'; import UserModel from '../../model/user'; import type { KoaContext } from '../server'; @@ -15,7 +14,6 @@ export default async (ctx: KoaContext, next) => { user = await UserModel.getById(domainId, ctx.session.uid, ctx.session.scope); } if (user._id === 0) delete user.viewLang; - user.avatarUrl = avatar(user.avatar, 128); ctx.HydroContext.user = await user.private(); if (!domain) { ctx.pendingError = new NotFoundError(args.domainId); diff --git a/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx index bcae93307..48fac31ba 100644 --- a/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx +++ b/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx @@ -44,7 +44,7 @@ DomainSelectAutoComplete.defaultProps = { listStyle: {}, multi: false, selectedKeys: [], - allowEmptyQuery: false, + allowEmptyQuery: true, freeSolo: false, freeSoloConverter: (input) => input, }; diff --git a/packages/ui-default/pages/problem_main.page.js b/packages/ui-default/pages/problem_main.page.js index 4e75046ab..8a0fb4cc2 100644 --- a/packages/ui-default/pages/problem_main.page.js +++ b/packages/ui-default/pages/problem_main.page.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import _ from 'lodash'; +import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete'; import { ActionDialog, ConfirmDialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; import createHint from 'vj/components/hint'; @@ -176,19 +177,35 @@ async function handleOperation(operation) { }).open(); if (action !== 'yes') return; } else if (operation === 'copy') { - const action = await new ActionDialog({ - $body: tpl` -
- + $(tpl` + + `).appendTo(document.body); + const domainSelector = DomainSelectAutoComplete.getOrConstruct($('.dialog__body--problem-copy [name="target"]')); + const copyDialog = await new ActionDialog({ + $body: $('.dialog__body--problem-copy > div'), + onDispatch(action) { + if (action === 'ok' && domainSelector.value() === null) { + domainSelector.focus(); + return false; + } + return true; + }, }).open(); - if (action !== 'ok') return; + if (copyDialog !== 'ok') return; const target = $('[name="target"]').val(); if (!target) return; payload.target = target; diff --git a/packages/ui-default/pages/problem_sidebar.page.ts b/packages/ui-default/pages/problem_sidebar.page.ts index 35021af3a..c564b78cc 100644 --- a/packages/ui-default/pages/problem_sidebar.page.ts +++ b/packages/ui-default/pages/problem_sidebar.page.ts @@ -1,4 +1,5 @@ import $ from 'jquery'; +import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete'; import { ActionDialog, ConfirmDialog } from 'vj/components/dialog'; import Notification from 'vj/components/notification'; import { NamedPage } from 'vj/misc/Page'; @@ -22,17 +23,31 @@ const page = new NamedPage([ }); }); $(document).on('click', '[name="problem-sidebar__copy"]', () => { - new ActionDialog({ - $body: tpl` -
- + $(tpl``).appendTo(document.body); + const domainSelector = DomainSelectAutoComplete.getOrConstruct($('.dialog__body--problem-copy [name="target"]')) as any; + new ActionDialog({ + $body: $('.dialog__body--problem-copy > div'), + onDispatch(action) { + if (action === 'ok' && domainSelector.value() === null) { + domainSelector.focus(); + return false; + } + return true; + }, }).open().then(async (action) => { if (action !== 'ok') return; const target = $('[name="target"]').val(); diff --git a/packages/ui-default/templates/home_domain.html b/packages/ui-default/templates/home_domain.html index 2308241a7..2242f5323 100644 --- a/packages/ui-default/templates/home_domain.html +++ b/packages/ui-default/templates/home_domain.html @@ -33,6 +33,15 @@ +
+ + {% set _show = handler.user.pinnedDomains.includes(ddoc._id) %} + + +
{{ ddoc['name'] }} ({{ ddoc['_id'] }}) diff --git a/packages/ui-default/templates/partials/nav.html b/packages/ui-default/templates/partials/nav.html index 5185bfdc4..5603c715f 100644 --- a/packages/ui-default/templates/partials/nav.html +++ b/packages/ui-default/templates/partials/nav.html @@ -31,6 +31,27 @@ {% else %} +