diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 2ac42fe09..4b18ee1f6 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -6,6 +6,7 @@ __langname: 简体中文 '{0} is a moderator of this domain.': '{}是这个域的管理者之一。' '{0} is a superuser.': '{0}是超级管理员。' '{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。' +'{0} problem(s) selected': '{0} 道题目已选中' '{0} problems': '{0} 道题' '{0} sections': '{0} 小节' '{0} solutions': '{0} 条题解' diff --git a/packages/ui-default/pages/problem_main.page.ts b/packages/ui-default/pages/problem_main.page.tsx similarity index 75% rename from packages/ui-default/pages/problem_main.page.ts rename to packages/ui-default/pages/problem_main.page.tsx index bd326751c..a8b12837f 100644 --- a/packages/ui-default/pages/problem_main.page.ts +++ b/packages/ui-default/pages/problem_main.page.tsx @@ -1,6 +1,9 @@ import parser, { SearchParserResult } from '@hydrooj/utils/lib/search'; import $ from 'jquery'; import _ from 'lodash'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import ProblemSelectAutoComplete from 'vj/components/autocomplete/components/ProblemSelectAutoComplete'; import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete'; import { ActionDialog, ConfirmDialog, Dialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; @@ -18,6 +21,9 @@ const pinned: Record = { category: [], difficulty: [] }; const selections = { category: {}, difficulty: {} }; const selectedTags: Record = { category: [], difficulty: [] }; +let selectedPids: string[] = []; +let clearSelectionHandler: (() => void) | null = null; + function setDomSelected($dom, selected, icon?) { if (selected) { $dom.addClass('selected'); @@ -163,15 +169,11 @@ function parseCategorySelection() { } function ensureAndGetSelectedPids() { - const pids = _.map( - $('tbody [data-checkbox-group="problem"]:checked'), - (ch) => $(ch).closest('tr').attr('data-pid'), - ); - if (pids.length === 0) { + if (selectedPids.length === 0) { Notification.error(i18n('Please select at least one problem to perform this operation.')); return null; } - return pids; + return selectedPids; } async function handleOperation(operation) { @@ -204,6 +206,7 @@ async function handleOperation(operation) { await request.post('', { operation, pids, ...payload }); Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`)); await delay(2000); + clearSelectionHandler?.(); loadQuery(); } catch (error) { Notification.error(error.message); @@ -282,6 +285,94 @@ function processElement(ele) { createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty')); } +function ProblemSelectionDisplay(props) { // eslint-disable-line + const [pids, setPids] = React.useState([]); + const [dialogOpen, setDialogOpen] = React.useState(false); + + React.useEffect(() => { + props.onClear?.(() => { + setPids([]); + }); + }, [props.onClear]); + + React.useEffect(() => { + $(document).on('click', '[data-checkbox-group="problem"]:checked', (ev) => { + const $checkbox = $(ev.currentTarget); + const pid = $checkbox.closest('tr').attr('data-pid'); + setPids((o) => Array.from(new Set([...o, pid]))); + }); + $(document).on('click', '[data-checkbox-group="problem"]:not(:checked)', (ev) => { + const $checkbox = $(ev.currentTarget); + const pid = $checkbox.closest('tr').attr('data-pid'); + setPids((o) => o.filter((i) => i !== pid)); + }); + $(document).on('click', '[data-checkbox-toggle="problem"]:checked', () => { + const all = $('[data-checkbox-group="problem"]').map((_index, i) => $(i).closest('tr').attr('data-pid')).get(); + setPids((o) => Array.from(new Set([...o, ...all]))); + }); + $(document).on('click', '[data-checkbox-toggle="problem"]:not(:checked)', () => { + const all = $('[data-checkbox-group="problem"]').map((_index, i) => $(i).closest('tr').attr('data-pid')).get(); + setPids((o) => o.filter((i) => !all.includes(i))); + }); + }, []); + + const updateCheckboxSelection = React.useCallback(() => { + for (const i of $('[data-checkbox-group="problem"]:checked')) { + if (!pids.includes(i.closest('tr').dataset.pid)) { + $(i).prop('checked', false); + } + } + for (const i of pids) { + $(`[data-pid="${i}"]`).find('[data-checkbox-group="problem"]')?.prop('checked', true); + } + }, [pids]); + + React.useEffect(() => { + updateCheckboxSelection(); + props.onChange(pids); + $(document).on('vjContentNew', updateCheckboxSelection); + return () => { + $(document).off('vjContentNew', updateCheckboxSelection); + }; + }, [pids]); + + return (<> + setDialogOpen(true)}> + + {' '}{i18n('{0} problem(s) selected', pids.length)} + +
setDialogOpen(false)}> +
ev.stopPropagation()}> +
+
+
+

Select Problems

+
+
+
+
+ setPids(v.split(',').filter((i) => i.trim()))} + selectedKeys={pids} + /> + +
+
+
+
+
+
+ {' '} + +
+
+
+
+
+ ); +} + const page = new NamedPage(['problem_main'], () => { const $body = $('body'); $body.addClass('display-mode'); @@ -344,6 +435,12 @@ const page = new NamedPage(['problem_main'], () => { $(document).on('vjContentNew', (e) => processElement(e.target)); processElement(document); + ReactDOM.createRoot(document.getElementById('problem_selection')) + .render( { selectedPids = pids; }} + onClear={(handler) => { clearSelectionHandler = handler; }} + />); + addSpeculationRules({ prerender: [{ 'where': { diff --git a/packages/ui-default/templates/problem_main.html b/packages/ui-default/templates/problem_main.html index 05cbb8d34..9548cbdae 100644 --- a/packages/ui-default/templates/problem_main.html +++ b/packages/ui-default/templates/problem_main.html @@ -33,6 +33,7 @@ {{ _('Leave Edit Mode') }} +
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %} diff --git a/packages/ui-default/utils/base.ts b/packages/ui-default/utils/base.ts index 78f74146d..9ee4667d6 100644 --- a/packages/ui-default/utils/base.ts +++ b/packages/ui-default/utils/base.ts @@ -6,7 +6,7 @@ import DOMServer from 'react-dom/server'; export function substitute(str: string, obj: any) { return str.replace(/\{([^{}]+)\}/g, (match, key) => { - if (obj[key] !== undefined) return obj[key].toString(); + if (obj[key] !== undefined && obj[key] !== null) return obj[key].toString(); return `{${key}}`; }); }