Skip to content

Commit 860b463

Browse files
committed
ui: problem_main: allow multi-page selection
1 parent 80f1158 commit 860b463

4 files changed

Lines changed: 106 additions & 7 deletions

File tree

packages/ui-default/locales/zh.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ __langname: 简体中文
66
'{0} is a moderator of this domain.': '{}是这个域的管理者之一。'
77
'{0} is a superuser.': '{0}是超级管理员。'
88
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。'
9+
'{0} problem(s) selected': '{0} 道题目已选中'
910
'{0} problems': '{0} 道题'
1011
'{0} sections': '{0} 小节'
1112
'{0} solutions': '{0} 条题解'

packages/ui-default/pages/problem_main.page.ts renamed to packages/ui-default/pages/problem_main.page.tsx

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import parser, { SearchParserResult } from '@hydrooj/utils/lib/search';
22
import $ from 'jquery';
33
import _ from 'lodash';
4+
import React from 'react';
5+
import ReactDOM from 'react-dom/client';
6+
import ProblemSelectAutoComplete from 'vj/components/autocomplete/components/ProblemSelectAutoComplete';
47
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
58
import { ActionDialog, ConfirmDialog, Dialog } from 'vj/components/dialog';
69
import Dropdown from 'vj/components/dropdown/Dropdown';
@@ -18,6 +21,9 @@ const pinned: Record<string, string[]> = { category: [], difficulty: [] };
1821
const selections = { category: {}, difficulty: {} };
1922
const selectedTags: Record<string, string[]> = { category: [], difficulty: [] };
2023

24+
let selectedPids: string[] = [];
25+
let clearSelectionHandler: (() => void) | null = null;
26+
2127
function setDomSelected($dom, selected, icon?) {
2228
if (selected) {
2329
$dom.addClass('selected');
@@ -163,15 +169,11 @@ function parseCategorySelection() {
163169
}
164170

165171
function ensureAndGetSelectedPids() {
166-
const pids = _.map(
167-
$('tbody [data-checkbox-group="problem"]:checked'),
168-
(ch) => $(ch).closest('tr').attr('data-pid'),
169-
);
170-
if (pids.length === 0) {
172+
if (selectedPids.length === 0) {
171173
Notification.error(i18n('Please select at least one problem to perform this operation.'));
172174
return null;
173175
}
174-
return pids;
176+
return selectedPids;
175177
}
176178

177179
async function handleOperation(operation) {
@@ -204,6 +206,7 @@ async function handleOperation(operation) {
204206
await request.post('', { operation, pids, ...payload });
205207
Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`));
206208
await delay(2000);
209+
clearSelectionHandler?.();
207210
loadQuery();
208211
} catch (error) {
209212
Notification.error(error.message);
@@ -282,6 +285,94 @@ function processElement(ele) {
282285
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
283286
}
284287

288+
function ProblemSelectionDisplay(props) { // eslint-disable-line
289+
const [pids, setPids] = React.useState<string[]>([]);
290+
const [dialogOpen, setDialogOpen] = React.useState(false);
291+
292+
React.useEffect(() => {
293+
props.onClear?.(() => {
294+
setPids([]);
295+
});
296+
}, [props.onClear]);
297+
298+
React.useEffect(() => {
299+
$(document).on('click', '[data-checkbox-group="problem"]:checked', (ev) => {
300+
const $checkbox = $(ev.currentTarget);
301+
const pid = $checkbox.closest('tr').attr('data-pid');
302+
setPids((o) => Array.from(new Set([...o, pid])));
303+
});
304+
$(document).on('click', '[data-checkbox-group="problem"]:not(:checked)', (ev) => {
305+
const $checkbox = $(ev.currentTarget);
306+
const pid = $checkbox.closest('tr').attr('data-pid');
307+
setPids((o) => o.filter((i) => i !== pid));
308+
});
309+
$(document).on('click', '[data-checkbox-toggle="problem"]:checked', () => {
310+
const all = $('[data-checkbox-group="problem"]').map((_index, i) => $(i).closest('tr').attr('data-pid')).get();
311+
setPids((o) => Array.from(new Set([...o, ...all])));
312+
});
313+
$(document).on('click', '[data-checkbox-toggle="problem"]:not(:checked)', () => {
314+
const all = $('[data-checkbox-group="problem"]').map((_index, i) => $(i).closest('tr').attr('data-pid')).get();
315+
setPids((o) => o.filter((i) => !all.includes(i)));
316+
});
317+
}, []);
318+
319+
const updateCheckboxSelection = React.useCallback(() => {
320+
for (const i of $('[data-checkbox-group="problem"]:checked')) {
321+
if (!pids.includes(i.closest('tr').dataset.pid)) {
322+
$(i).prop('checked', false);
323+
}
324+
}
325+
for (const i of pids) {
326+
$(`[data-pid="${i}"]`).find('[data-checkbox-group="problem"]')?.prop('checked', true);
327+
}
328+
}, [pids]);
329+
330+
React.useEffect(() => {
331+
updateCheckboxSelection();
332+
props.onChange(pids);
333+
$(document).on('vjContentNew', updateCheckboxSelection);
334+
return () => {
335+
$(document).off('vjContentNew', updateCheckboxSelection);
336+
};
337+
}, [pids]);
338+
339+
return (<>
340+
<a href="javascript:;" className="menu__link display-mode-hide" onClick={() => setDialogOpen(true)}>
341+
<span className="icon icon-stack"></span>
342+
{' '}{i18n('{0} problem(s) selected', pids.length)}
343+
</a>
344+
<div className="dialog withBg" style={{ display: dialogOpen ? 'flex' : 'none', zIndex: 1000, opacity: 1 }} onClick={() => setDialogOpen(false)}>
345+
<div className="dialog__content" style={{ transform: 'scale(1, 1)' }} onClick={(ev) => ev.stopPropagation()}>
346+
<div className="dialog__body" style={{ height: 'calc(100% - 45px)' }}>
347+
<div className="row">
348+
<div className="columns">
349+
<h1>Select Problems</h1>
350+
</div>
351+
</div>
352+
<div className="row">
353+
<div className="columns">
354+
<ProblemSelectAutoComplete
355+
multi
356+
onChange={(v) => setPids(v.split(',').filter((i) => i.trim()))}
357+
selectedKeys={pids}
358+
/>
359+
<style>{'.autocomplete-wrapper { max-height: 50vh; }'}</style>
360+
</div>
361+
</div>
362+
</div>
363+
<div className="row">
364+
<div className="columns clearfix">
365+
<div className="float-right dialog__action">
366+
<button className="rounded button" onClick={() => setPids([])}>{i18n('Clear')}</button>{' '}
367+
<button className="primary rounded button" onClick={() => setDialogOpen(false)}>{i18n('Ok')}</button>
368+
</div>
369+
</div>
370+
</div>
371+
</div>
372+
</div>
373+
</>);
374+
}
375+
285376
const page = new NamedPage(['problem_main'], () => {
286377
const $body = $('body');
287378
$body.addClass('display-mode');
@@ -344,6 +435,12 @@ const page = new NamedPage(['problem_main'], () => {
344435
$(document).on('vjContentNew', (e) => processElement(e.target));
345436
processElement(document);
346437

438+
ReactDOM.createRoot(document.getElementById('problem_selection'))
439+
.render(<ProblemSelectionDisplay
440+
onChange={(pids) => { selectedPids = pids; }}
441+
onClear={(handler) => { clearSelectionHandler = handler; }}
442+
/>);
443+
347444
addSpeculationRules({
348445
prerender: [{
349446
'where': {

packages/ui-default/templates/problem_main.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<span class="icon icon-edit"></span>
3434
{{ _('Leave Edit Mode') }}
3535
</a>
36+
<div id="problem_selection" class="display-mode-hide"></div>
3637
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
3738
<a href="javascript:;" class="menu__link display-mode-hide" name="hide_selected_problems">
3839
<span class="icon icon-close"></span>

packages/ui-default/utils/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import DOMServer from 'react-dom/server';
66

77
export function substitute(str: string, obj: any) {
88
return str.replace(/\{([^{}]+)\}/g, (match, key) => {
9-
if (obj[key] !== undefined) return obj[key].toString();
9+
if (obj[key] !== undefined && obj[key] !== null) return obj[key].toString();
1010
return `{${key}}`;
1111
});
1212
}

0 commit comments

Comments
 (0)