ui: add hint on problem_files page

pull/236/head
undefined 3 years ago
parent de375f74c7
commit 8b9ff434e2

@ -28,18 +28,18 @@
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/cross-spawn": "^6.0.2",
"@types/node": "^16.10.3",
"@types/node": "^16.10.9",
"@types/semver": "^7.3.8",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"cac": "^6.7.8",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"cac": "^6.7.11",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.3",
"esbuild": "^0.13.4",
"eslint": "^7.32.0",
"esbuild": "^0.13.6",
"eslint": "^8.0.1",
"eslint-config-airbnb-typescript": "12.3.1",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-simple-import-sort": "^7.0.0",
"fs-extra": "^10.0.0",
"globby": "11.0.4",

@ -10,6 +10,6 @@
"ws": "^8.2.3"
},
"devDependencies": {
"@types/ws": "^7.4.7"
"@types/ws": "^8.2.0"
}
}

@ -67,6 +67,7 @@ module.exports = {
'always',
{ exceptAfterSingleLine: true },
],
'lines-between-class-members': [
'error',
'always',
@ -90,6 +91,7 @@ module.exports = {
'import/extensions': 'off',
'import/no-extraneous-dependencies': 'off',
'max-classes-per-file': 'off',
'newline-per-chained-call': 'off',
'no-empty': ['warn', { allowEmptyCatch: true }],
'no-console': 'off',
'no-continue': 'off',

@ -12,7 +12,7 @@ export default function createHint(message: string, element?: any) {
a.onclick = () => {
new InfoDialog({
cancelByClickingBack: false,
$body: tpl.typoMsg(i18n(message)),
$body: tpl.typoMsg(i18n(message), true),
}).open();
};
$(element).append(a);

@ -110,7 +110,7 @@ class WikiAboutHandler extends Handler {
}
class SetThemeHandler extends Handler {
noCheckPermView = true
noCheckPermView = true;
async get({ theme }) {
await user.setById(this.user._id, { theme });

@ -341,6 +341,7 @@ Highlight discussions: 高亮讨论
Highlight: 高亮
Hint: 提示
Hint::icon::difficulty: Hydro 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,综合计算得出,可能不准确。
Hint::icon::testdata: <a href="https://hydro.js.org/docs/user/testdata/" target="_blank">请参照文档说明</a>
Hint::page::main: 欢迎来到 Hydro 您可在右上角注册或登录。此类提示仅会显示一次。
Hint::page::problem_detail: 这是题目详情页面。您可点击右侧“进入在线编程模式"开始编写您的代码。
Hint::page::problem_files: 您可以将文件或压缩包拖拽至对应区域来上传文件。点击文件名在线编辑或按住 Ctrl 后点击文件名下载单个文件。

@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.25.4",
"version": "4.25.5",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@ -13,7 +13,7 @@
"@blueprintjs/core": "^3.51.0",
"@blueprintjs/icons": "^3.30.2",
"@hydrooj/utils": "workspace:*",
"@types/jquery": "^3.5.6",
"@types/jquery": "^3.5.7",
"@types/json-schema": "^7.0.9",
"@types/katex": "^0.11.1",
"@types/sockjs-client": "^1.5.1",
@ -31,10 +31,10 @@
"emojis-keywords": "2.0.0",
"emojis-list": "2.1.0",
"esbuild-loader": "^2.16.0",
"eslint": "^7.32.0",
"eslint": "^8.0.1",
"eslint-config-airbnb": "^18.2.1",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.26.1",
"fancy-log": "^1.3.3",
@ -53,9 +53,9 @@
"jquery.transit": "^0.9.12",
"mini-css-extract-plugin": "^1.6.2",
"moment": "^2.29.1",
"monaco-editor": "^0.29.0",
"monaco-editor": "^0.29.1",
"monaco-editor-webpack-plugin": "^4.2.0",
"nanoid": "^3.1.29",
"nanoid": "^3.1.30",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pickadate": "^3.6.4",
@ -90,19 +90,19 @@
"wastyle": "^0.0.5",
"web-streams-polyfill": "^3.1.1",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpackbar": "^5.0.0-3"
},
"dependencies": {
"esbuild": "^0.13.4",
"esbuild": "^0.13.6",
"fs-extra": "^10.0.0",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"katex": "^0.13.18",
"lodash": "^4.17.21",
"markdown-it": "^12.2.0",
"markdown-it-anchor": "^8.3.1",
"markdown-it-anchor": "^8.4.1",
"markdown-it-footnote": "^3.0.3",
"markdown-it-imsize": "^2.0.1",
"markdown-it-mark": "^3.0.1",

@ -12,146 +12,146 @@ function onBeforeUnload(e) {
e.returnValue = '';
}
const page = new NamedPage('home_files', () => {
function ensureAndGetSelectedFiles() {
const files = _.map(
$('.home-files tbody [data-checkbox-group="user_files"]:checked'),
(ch) => $(ch).closest('tr').attr('data-filename'),
);
if (files.length === 0) {
Notification.error(i18n('Please select at least one file to perform this operation.'));
return null;
}
return files;
function ensureAndGetSelectedFiles() {
const files = _.map(
$('.home-files tbody [data-checkbox-group="user_files"]:checked'),
(ch) => $(ch).closest('tr').attr('data-filename'),
);
if (files.length === 0) {
Notification.error(i18n('Please select at least one file to perform this operation.'));
return null;
}
return files;
}
async function handleClickUpload(files) {
if (!files) {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.click();
await new Promise((resolve) => { input.onchange = resolve; });
files = input.files;
}
if (!files.length) {
Notification.warn(i18n('No file selected.'));
return;
}
const dialog = new Dialog({
$body: `
<div class="file-label" style="text-align: center; margin-bottom: 5px; color: gray; font-size: small;"></div>
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
<div class="file-progress bp3-progress-meter" style="width: 0"></div>
</div>
<div class="upload-label" style="text-align: center; margin: 5px 0; color: gray; font-size: small;"></div>
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
<div class="upload-progress bp3-progress-meter" style="width: 0"></div>
</div>`,
});
try {
Notification.info(i18n('Uploading files...'));
window.addEventListener('beforeunload', onBeforeUnload);
dialog.open();
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
for (const i in files) {
if (Number.isNaN(+i)) continue;
const file = files[i];
const data = new FormData();
data.append('filename', file.name);
data.append('file', file);
data.append('operation', 'upload_file');
await request.postFile('', data, {
xhr() {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', () => {
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
$uploadProgress.width(0);
});
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
$uploadProgress.width(`${percentComplete}%`);
}
}, false);
return xhr;
},
});
}
window.removeEventListener('beforeunload', onBeforeUnload);
Notification.success(i18n('File uploaded successfully.'));
await pjax.request({ push: false });
} catch (e) {
console.error(e);
Notification.error(i18n('File upload failed: {0}', e.toString()));
} finally {
dialog.close();
}
async function handleClickUpload(files) {
if (!files) {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.click();
await new Promise((resolve) => { input.onchange = resolve; });
files = input.files;
}
async function handleClickRemoveSelected() {
const selectedFiles = ensureAndGetSelectedFiles();
if (selectedFiles === null) return;
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n('Confirm to delete the selected files?')}</p>
</div>`,
}).open();
if (action !== 'yes') return;
try {
await request.post('', {
operation: 'delete_files',
files: selectedFiles,
if (!files.length) {
Notification.warn(i18n('No file selected.'));
return;
}
const dialog = new Dialog({
$body: `
<div class="file-label" style="text-align: center; margin-bottom: 5px; color: gray; font-size: small;"></div>
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
<div class="file-progress bp3-progress-meter" style="width: 0"></div>
</div>
<div class="upload-label" style="text-align: center; margin: 5px 0; color: gray; font-size: small;"></div>
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
<div class="upload-progress bp3-progress-meter" style="width: 0"></div>
</div>`,
});
try {
Notification.info(i18n('Uploading files...'));
window.addEventListener('beforeunload', onBeforeUnload);
dialog.open();
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
for (const i in files) {
if (Number.isNaN(+i)) continue;
const file = files[i];
const data = new FormData();
data.append('filename', file.name);
data.append('file', file);
data.append('operation', 'upload_file');
await request.postFile('', data, {
xhr() {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', () => {
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
$uploadProgress.width(0);
});
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
$uploadProgress.width(`${percentComplete}%`);
}
}, false);
return xhr;
},
});
Notification.success(i18n('Selected files have been deleted.'));
await pjax.request({ push: false });
} catch (error) {
Notification.error(error.message);
}
window.removeEventListener('beforeunload', onBeforeUnload);
Notification.success(i18n('File uploaded successfully.'));
await pjax.request({ push: false });
} catch (e) {
console.error(e);
Notification.error(i18n('File upload failed: {0}', e.toString()));
} finally {
dialog.close();
}
}
/**
* @param {JQuery.DragOverEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
*/
function handleDragOver(ev) {
ev.preventDefault();
// TODO display a drag-drop allowed hint
async function handleClickRemoveSelected() {
const selectedFiles = ensureAndGetSelectedFiles();
if (selectedFiles === null) return;
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n('Confirm to delete the selected files?')}</p>
</div>`,
}).open();
if (action !== 'yes') return;
try {
await request.post('', {
operation: 'delete_files',
files: selectedFiles,
});
Notification.success(i18n('Selected files have been deleted.'));
await pjax.request({ push: false });
} catch (error) {
Notification.error(error.message);
}
}
/**
* @param {JQuery.DropEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
*/
function handleDrop(ev) {
ev.preventDefault();
if (!$('[name="upload_testdata"]').length) {
Notification.error(i18n("You don't have permission to upload file."));
return;
}
ev = ev.originalEvent;
const files = [];
if (ev.dataTransfer.items) {
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
if (ev.dataTransfer.items[i].kind === 'file') {
const file = ev.dataTransfer.items[i].getAsFile();
files.push(file);
}
}
} else {
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
files.push(ev.dataTransfer.files[i]);
/**
* @param {JQuery.DragOverEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
*/
function handleDragOver(ev) {
ev.preventDefault();
// TODO display a drag-drop allowed hint
}
/**
* @param {JQuery.DropEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
*/
function handleDrop(ev) {
ev.preventDefault();
if (!$('[name="upload_testdata"]').length) {
Notification.error(i18n("You don't have permission to upload file."));
return;
}
ev = ev.originalEvent;
const files = [];
if (ev.dataTransfer.items) {
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
if (ev.dataTransfer.items[i].kind === 'file') {
const file = ev.dataTransfer.items[i].getAsFile();
files.push(file);
}
}
handleClickUpload(files);
} else {
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
files.push(ev.dataTransfer.files[i]);
}
}
handleClickUpload(files);
}
const page = new NamedPage('home_files', () => {
const clip = new Clipboard('.home-files .col--name', {
text: (trigger) => {
const filename = trigger.closest('[data-filename]').getAttribute('data-filename');

@ -3,6 +3,7 @@ import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import { ConfirmDialog, ActionDialog, Dialog } from 'vj/components/dialog/index';
import download from 'vj/components/zipDownloader';
import createHint from 'vj/components/hint';
import request from 'vj/utils/request';
import pjax from 'vj/utils/pjax';
import tpl from 'vj/utils/tpl';
@ -210,6 +211,7 @@ const page = new NamedPage('problem_files', () => {
? ev.currentTarget.closest('[data-filename]').getAttribute('data-filename')
// eslint-disable-next-line no-alert
: prompt('Filename');
if (!filename) return;
const filesize = ev
? +ev.currentTarget.closest('[data-size]').getAttribute('data-size')
: 0;
@ -251,6 +253,10 @@ const page = new NamedPage('problem_files', () => {
$(document).on('drop', '.problem-files-additional_file', (ev) => handleDrop('additional_file', ev));
$(document).on('click', '[name="download_selected_testdata"]', () => handleClickDownloadSelected('testdata'));
$(document).on('click', '[name="download_selected_file"]', () => handleClickDownloadSelected('additional_file'));
$(document).on('vjContentNew', (e) => {
createHint('Hint::icon::testdata', $(e.target).find('[name="create_testdata"]').get(0).parentNode.parentNode.children[0]);
});
createHint('Hint::icon::testdata', $(document).find('[name="create_testdata"]').get(0).parentNode.parentNode.children[0]);
});
export default page;

@ -13,10 +13,10 @@ export default function tpl(pieces, ...substitutions) {
return result;
}
tpl.typoMsg = function (msg) {
tpl.typoMsg = function (msg, raw = false) {
return tpl`
<div class="typo">
<p>${msg}</p>
<p>${raw ? { html: msg, templateRaw: true } : msg}</p>
</div>
`;
};

Loading…
Cancel
Save