mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: problem_main: allow multi-page selection
This commit is contained in:
4 files changed
+106
-7
No files matched your search
@@ -6,6 +6,7 @@ __langname: 简体中文
|
|||||||
'{0} is a moderator of this domain.': '{}是这个域的管理者之一。'
|
'{0} is a moderator of this domain.': '{}是这个域的管理者之一。'
|
||||||
'{0} is a superuser.': '{0}是超级管理员。'
|
'{0} is a superuser.': '{0}是超级管理员。'
|
||||||
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。'
|
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。'
|
||||||
|
'{0} problem(s) selected': '{0} 道题目已选中'
|
||||||
'{0} problems': '{0} 道题'
|
'{0} problems': '{0} 道题'
|
||||||
'{0} sections': '{0} 小节'
|
'{0} sections': '{0} 小节'
|
||||||
'{0} solutions': '{0} 条题解'
|
'{0} solutions': '{0} 条题解'
|
||||||
|
|||||||
+103
-6
@@ -1,6 +1,9 @@
|
|||||||
import parser, { SearchParserResult } from '@hydrooj/utils/lib/search';
|
import parser, { SearchParserResult } from '@hydrooj/utils/lib/search';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'lodash';
|
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 DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
|
||||||
import { ActionDialog, ConfirmDialog, Dialog } from 'vj/components/dialog';
|
import { ActionDialog, ConfirmDialog, Dialog } from 'vj/components/dialog';
|
||||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||||
@@ -18,6 +21,9 @@ const pinned: Record<string, string[]> = { category: [], difficulty: [] };
|
|||||||
const selections = { category: {}, difficulty: {} };
|
const selections = { category: {}, difficulty: {} };
|
||||||
const selectedTags: Record<string, string[]> = { category: [], difficulty: [] };
|
const selectedTags: Record<string, string[]> = { category: [], difficulty: [] };
|
||||||
|
|
||||||
|
let selectedPids: string[] = [];
|
||||||
|
let clearSelectionHandler: (() => void) | null = null;
|
||||||
|
|
||||||
function setDomSelected($dom, selected, icon?) {
|
function setDomSelected($dom, selected, icon?) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
$dom.addClass('selected');
|
$dom.addClass('selected');
|
||||||
@@ -163,15 +169,11 @@ function parseCategorySelection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureAndGetSelectedPids() {
|
function ensureAndGetSelectedPids() {
|
||||||
const pids = _.map(
|
if (selectedPids.length === 0) {
|
||||||
$('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.'));
|
Notification.error(i18n('Please select at least one problem to perform this operation.'));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return pids;
|
return selectedPids;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOperation(operation) {
|
async function handleOperation(operation) {
|
||||||
@@ -204,6 +206,7 @@ async function handleOperation(operation) {
|
|||||||
await request.post('', { operation, pids, ...payload });
|
await request.post('', { operation, pids, ...payload });
|
||||||
Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`));
|
Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`));
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
|
clearSelectionHandler?.();
|
||||||
loadQuery();
|
loadQuery();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notification.error(error.message);
|
Notification.error(error.message);
|
||||||
@@ -282,6 +285,94 @@ function processElement(ele) {
|
|||||||
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
|
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProblemSelectionDisplay(props) { // eslint-disable-line
|
||||||
|
const [pids, setPids] = React.useState<string[]>([]);
|
||||||
|
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 (<>
|
||||||
|
<a href="javascript:;" className="menu__link display-mode-hide" onClick={() => setDialogOpen(true)}>
|
||||||
|
<span className="icon icon-stack"></span>
|
||||||
|
{' '}{i18n('{0} problem(s) selected', pids.length)}
|
||||||
|
</a>
|
||||||
|
<div className="dialog withBg" style={{ display: dialogOpen ? 'flex' : 'none', zIndex: 1000, opacity: 1 }} onClick={() => setDialogOpen(false)}>
|
||||||
|
<div className="dialog__content" style={{ transform: 'scale(1, 1)' }} onClick={(ev) => ev.stopPropagation()}>
|
||||||
|
<div className="dialog__body" style={{ height: 'calc(100% - 45px)' }}>
|
||||||
|
<div className="row">
|
||||||
|
<div className="columns">
|
||||||
|
<h1>Select Problems</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="columns">
|
||||||
|
<ProblemSelectAutoComplete
|
||||||
|
multi
|
||||||
|
onChange={(v) => setPids(v.split(',').filter((i) => i.trim()))}
|
||||||
|
selectedKeys={pids}
|
||||||
|
/>
|
||||||
|
<style>{'.autocomplete-wrapper { max-height: 50vh; }'}</style>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="columns clearfix">
|
||||||
|
<div className="float-right dialog__action">
|
||||||
|
<button className="rounded button" onClick={() => setPids([])}>{i18n('Clear')}</button>{' '}
|
||||||
|
<button className="primary rounded button" onClick={() => setDialogOpen(false)}>{i18n('Ok')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
const page = new NamedPage(['problem_main'], () => {
|
const page = new NamedPage(['problem_main'], () => {
|
||||||
const $body = $('body');
|
const $body = $('body');
|
||||||
$body.addClass('display-mode');
|
$body.addClass('display-mode');
|
||||||
@@ -344,6 +435,12 @@ const page = new NamedPage(['problem_main'], () => {
|
|||||||
$(document).on('vjContentNew', (e) => processElement(e.target));
|
$(document).on('vjContentNew', (e) => processElement(e.target));
|
||||||
processElement(document);
|
processElement(document);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('problem_selection'))
|
||||||
|
.render(<ProblemSelectionDisplay
|
||||||
|
onChange={(pids) => { selectedPids = pids; }}
|
||||||
|
onClear={(handler) => { clearSelectionHandler = handler; }}
|
||||||
|
/>);
|
||||||
|
|
||||||
addSpeculationRules({
|
addSpeculationRules({
|
||||||
prerender: [{
|
prerender: [{
|
||||||
'where': {
|
'where': {
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<span class="icon icon-edit"></span>
|
<span class="icon icon-edit"></span>
|
||||||
{{ _('Leave Edit Mode') }}
|
{{ _('Leave Edit Mode') }}
|
||||||
</a>
|
</a>
|
||||||
|
<div id="problem_selection" class="display-mode-hide"></div>
|
||||||
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
|
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
|
||||||
<a href="javascript:;" class="menu__link display-mode-hide" name="hide_selected_problems">
|
<a href="javascript:;" class="menu__link display-mode-hide" name="hide_selected_problems">
|
||||||
<span class="icon icon-close"></span>
|
<span class="icon icon-close"></span>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import DOMServer from 'react-dom/server';
|
|||||||
|
|
||||||
export function substitute(str: string, obj: any) {
|
export function substitute(str: string, obj: any) {
|
||||||
return str.replace(/\{([^{}]+)\}/g, (match, key) => {
|
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}}`;
|
return `{${key}}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user