diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 983ebfaaa..39f86a6ee 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.23.29", + "version": "2.23.30", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 02fc76257..3d2fcef74 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -55,7 +55,8 @@ export class ProblemHandler extends Handler { export class ProblemMainHandler extends ProblemHandler { @param('page', Types.PositiveInt, true) @param('q', Types.Content, true) - async get(domainId: string, page = 1, q = '') { + @param('category', Types.Name, true, null, parseCategory) + async get(domainId: string, page = 1, q = '', category = []) { this.response.template = 'problem_main.html'; // eslint-disable-next-line @typescript-eslint/no-shadow const query: FilterQuery = {}; @@ -66,9 +67,17 @@ export class ProblemMainHandler extends ProblemHandler { ]; const search = global.Hydro.lib.problemSearch; let sort: number[]; - if (q) { + if (category.length) { + query.$and = []; + for (const tag of category) query.$and.push({ tag }); + } + if (q) category.push(q); + if (category.length) { + this.extraTitleContent = category.join('+'); path[1][1] = 'problem_main'; - path.push([`${this.translate('Keyword')}: ${q}`, null, null, true]); + path.push([category.join('+'), null, null, true]); + } + if (q) { if (search) { const result = await search(domainId, q); query.docId = { $in: result }; @@ -94,7 +103,7 @@ export class ProblemMainHandler extends ProblemHandler { ); } this.response.body = { - path, page, pcount, ppcount, pdocs, psdict, category: q, + path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'), }; } @@ -109,37 +118,41 @@ export class ProblemMainHandler extends ProblemHandler { await problem.setStar(domainId, pid, this.user._id, false); this.back({ star: false }); } -} -export class ProblemCategoryHandler extends ProblemHandler { - @param('page', Types.PositiveInt, true) - @param('category', Types.Name, null, parseCategory) - async get(domainId: string, page = 1, category: string[]) { - this.response.template = 'problem_main.html'; - const q: any = { $and: [] }; - for (const tag of category) q.$and.push({ tag }); - let psdict = {}; - if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; - await bus.serial('problem/list', q, this); - const [pdocs, ppcount, pcount] = await paginate( - problem.getMulti(domainId, q).sort({ pid: 1, docId: 1 }), - page, - system.get('pagination.problem'), - ); - if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { - psdict = await problem.getListStatus( - domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId), - ); + @param('pids', Types.NumericArray) + async postDelete(domainId: string, pids: number[]) { + for (const pid of pids) { + // eslint-disable-next-line no-await-in-loop + const pdoc = await problem.get(domainId, pid); + if (pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM); + // eslint-disable-next-line no-await-in-loop + await problem.del(domainId, pid); } - const path = [ - ['Hydro', 'homepage'], - ['problem_main', 'problem_main'], - [category, null, null, true], - ]; - this.response.body = { - path, page, pcount, ppcount, pdocs, psdict, category: category.join('+'), - }; - this.extraTitleContent = category.join('+'); + this.back(); + } + + @param('pids', Types.NumericArray) + async postHide(domainId: string, pids: number[]) { + for (const pid of pids) { + // eslint-disable-next-line no-await-in-loop + const pdoc = await problem.get(domainId, pid); + if (pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM); + // eslint-disable-next-line no-await-in-loop + await problem.edit(domainId, pid, { hidden: true }); + } + this.back(); + } + + @param('pids', Types.NumericArray) + async postUnhide(domainId: string, pids: number[]) { + for (const pid of pids) { + // eslint-disable-next-line no-await-in-loop + const pdoc = await problem.get(domainId, pid); + if (pdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_PROBLEM); + // eslint-disable-next-line no-await-in-loop + await problem.edit(domainId, pid, { hidden: false }); + } + this.back(); } } @@ -599,7 +612,7 @@ export class ProblemPrefixListHandler extends Handler { export async function apply() { Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM); - Route('problem_category', '/p/category/:category', ProblemCategoryHandler, PERM.PERM_VIEW_PROBLEM); + Route('problem_category', '/p/category/:category', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM); diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index c88ce2da9..ba7ee8983 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -567,7 +567,7 @@ export class Handler extends HandlerCommon { } async putResponse() { - if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition); + if (this.response.disposition) this.ctx.set('Content-Disposition', encodeURIComponent(this.response.disposition)); if (!this.response.body) return; if (this.response.redirect && !this.request.json) { this.ctx.response.type = 'application/octet-stream'; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index aa2686a93..876a4f9b7 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.6.21", + "version": "4.6.22", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/problem_main.page.js b/packages/ui-default/pages/problem_main.page.js index b59a17847..1e6e45d13 100644 --- a/packages/ui-default/pages/problem_main.page.js +++ b/packages/ui-default/pages/problem_main.page.js @@ -1,9 +1,15 @@ import _ from 'lodash'; import { NamedPage } from 'vj/misc/Page'; +import Notification from 'vj/components/notification'; +import { ConfirmDialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; import pjax from 'vj/utils/pjax'; import substitute from 'vj/utils/substitute'; +import request from 'vj/utils/request'; +import tpl from 'vj/utils/tpl'; +import delay from 'vj/utils/delay'; +import i18n from 'vj/utils/i18n'; const categories = {}; const dirtyCategories = []; @@ -56,6 +62,8 @@ async function updateSelection(sendRequest = true) { .join('+'), // build a beautiful URL }); } + const q = $('[name="q"]').val(); + if (q) url += `?q=${q}`; pjax.request({ url }); } } @@ -160,9 +168,58 @@ function parseCategorySelection() { updateSelection(false); } +function ensureAndGetSelectedPids() { + const pids = _.map( + $(`tbody [data-checkbox-group="problem"]:checked`), + (ch) => $(ch).closest('tr').attr('data-pid'), + ); + if (pids.length === 0) { + Notification.error(i18n('Please select at least one problem to perform this operation.')); + return null; + } + return pids; +} + +async function handleOperation(operation) { + const pids = ensureAndGetSelectedPids(); + if (pids === null) return; + if (operation === 'delete') { + const action = await new ConfirmDialog({ + $body: tpl` +
+

${i18n('Confirm to delete the selected problems?')}

+
`, + }).open(); + if (action !== 'yes') return; + } + try { + await request.post('', { operation, pids }); + Notification.success(i18n(`Selected problems have been ${operation}d.`)); + await delay(2000); + window.location.reload(); + } catch (error) { + Notification.error(error.message); + } +} + const page = new NamedPage(['problem_main', 'problem_category'], () => { + const doc = document.documentElement; + doc.className = doc.className + ' display-mode'; buildCategoryFilter(); parseCategorySelection(); + $('[name="leave-edit-mode"]').on('click', () => { + doc.className = doc.className.replace(' edit-mode', ' display-mode'); + }); + $('[name="enter-edit-mode"]').on('click', () => { + doc.className = doc.className.replace(' display-mode', ' edit-mode'); + }); + $('[name="remove_selected_problems"]').on('click', () => handleOperation('delete')); + $('[name="hide_selected_problems"]').on('click', () => handleOperation('hide')); + $('[name="unhide_selected_problems"]').on('click', () => handleOperation('unhide')); + $('#search').on('click', (ev) => { + ev.preventDefault(); + updateSelection(); + }); }); export default page; diff --git a/packages/ui-default/pages/problem_main.page.styl b/packages/ui-default/pages/problem_main.page.styl index 4374d2e22..d90c42999 100644 --- a/packages/ui-default/pages/problem_main.page.styl +++ b/packages/ui-default/pages/problem_main.page.styl @@ -1,8 +1,14 @@ .page--problem_main .page--problem_category .page--problem_settings + .col--checkbox + width: rem(60px) + + .col--pid + width: rem(80px) + .col--status - width: rem(150px) + width: rem(140px) position: relative .col--submit_n, .col--ac_rate, .col--difficulty @@ -97,3 +103,10 @@ margin-left: 0 margin-right: 0 margin-top: rem(floor($menu-drop-triangle-size * -1.414)) + + +.display-mode .display-mode-hide + display: none + +.edit-mode .edit-mode-hide + display: none \ No newline at end of file diff --git a/packages/ui-default/templates/components/record.html b/packages/ui-default/templates/components/record.html index bdd156bab..67fd3596f 100644 --- a/packages/ui-default/templates/components/record.html +++ b/packages/ui-default/templates/components/record.html @@ -1,5 +1,5 @@ -{% macro render_status_td(rdoc, rid_key='_id') %} - +{% macro render_status_td(rdoc, rid_key='_id', class='') %} +
diff --git a/packages/ui-default/templates/partials/problem_list.html b/packages/ui-default/templates/partials/problem_list.html index 38a152bae..78de58d28 100644 --- a/packages/ui-default/templates/partials/problem_list.html +++ b/packages/ui-default/templates/partials/problem_list.html @@ -8,8 +8,12 @@ {% else %} + {% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} + + + {% endif %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - + {% endif %} @@ -18,8 +22,16 @@ + {% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} + + + {% endif %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - + {% endif %} @@ -28,17 +40,24 @@ - {%- for pdoc in pdocs -%} - {% set psdoc = psdict[pdoc['docId']] %} - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - {% if psdoc['rid'] %} - {{ record.render_status_td(psdoc, rid_key='rid') }} - {% else %} - - {% endif %} - {% endif %} + {%- for pdoc in pdocs -%} + {% set psdoc = psdict[pdoc['docId']] %} + + {% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} + + + {% endif %} + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% if psdoc['rid'] %} + {{ record.render_status_td(psdoc, rid_key='rid', class="edit-mode-hide") }} + {% else %} + + {% endif %} + {% endif %} diff --git a/packages/ui-default/templates/problem_files.html b/packages/ui-default/templates/problem_files.html index c69e28776..11281a399 100644 --- a/packages/ui-default/templates/problem_files.html +++ b/packages/ui-default/templates/problem_files.html @@ -60,7 +60,7 @@
-
+ {% endif %} {% endif %} @@ -129,5 +129,4 @@ {% include "partials/problem_sidebar.html" %} - {% endblock %} diff --git a/packages/ui-default/templates/problem_main.html b/packages/ui-default/templates/problem_main.html index 324024286..e5b865332 100644 --- a/packages/ui-default/templates/problem_main.html +++ b/packages/ui-default/templates/problem_main.html @@ -16,14 +16,42 @@
+ {% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} + + {% endif %}
-
+

{{ _('Categories') }}

{% include "partials/category.html" %}
-
+
{% include "partials/problem_lucky.html" %}
@@ -36,7 +64,7 @@ - +
+ + {{ _('ID') }}{{ _('Status') }}{{ _('Status') }}{{ _('Problem') }} {{ _('Submit') }}
-
+ + {{ pdoc.docId }} {{ problem.render_problem_title(pdoc) }}