diff --git a/package.json b/package.json index 3fc90408..36dd4856 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/login-with-qq/package.json b/packages/login-with-qq/package.json index 99673c81..69171765 100644 --- a/packages/login-with-qq/package.json +++ b/packages/login-with-qq/package.json @@ -10,6 +10,6 @@ "ws": "^8.2.3" }, "devDependencies": { - "@types/ws": "^7.4.7" + "@types/ws": "^8.2.0" } } diff --git a/packages/ui-default/.eslintrc.js b/packages/ui-default/.eslintrc.js index 154d8ae4..a79c4a2c 100644 --- a/packages/ui-default/.eslintrc.js +++ b/packages/ui-default/.eslintrc.js @@ -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', diff --git a/packages/ui-default/components/hint.ts b/packages/ui-default/components/hint.ts index 652a9453..becbb43b 100644 --- a/packages/ui-default/components/hint.ts +++ b/packages/ui-default/components/hint.ts @@ -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); diff --git a/packages/ui-default/handler.ts b/packages/ui-default/handler.ts index 32431d84..1aad4135 100644 --- a/packages/ui-default/handler.ts +++ b/packages/ui-default/handler.ts @@ -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 }); diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index bdd3d933..b9217faa 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -341,6 +341,7 @@ Highlight discussions: 高亮讨论 Highlight: 高亮 Hint: 提示 Hint::icon::difficulty: Hydro 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,综合计算得出,可能不准确。 +Hint::icon::testdata: 请参照文档说明 Hint::page::main: 欢迎来到 Hydro! 您可在右上角注册或登录。此类提示仅会显示一次。 Hint::page::problem_detail: 这是题目详情页面。您可点击右侧“进入在线编程模式"开始编写您的代码。 Hint::page::problem_files: 您可以将文件或压缩包拖拽至对应区域来上传文件。点击文件名在线编辑或按住 Ctrl 后点击文件名下载单个文件。 diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index a6f2281d..7426112d 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.25.4", + "version": "4.25.5", "author": "undefined ", "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", diff --git a/packages/ui-default/pages/home_files.page.js b/packages/ui-default/pages/home_files.page.js index 03e59b8b..160017b3 100644 --- a/packages/ui-default/pages/home_files.page.js +++ b/packages/ui-default/pages/home_files.page.js @@ -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: ` -
-
-
-
-
-
-
-
`, - }); - 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` -
-

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

-
`, - }).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: ` +
+
+
+
+
+
+
+
`, + }); + 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} 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` +
+

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

+
`, + }).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} 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} ev + */ +function handleDragOver(ev) { + ev.preventDefault(); + // TODO display a drag-drop allowed hint +} + +/** + * @param {JQuery.DropEvent} 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'); diff --git a/packages/ui-default/pages/problem_files.page.js b/packages/ui-default/pages/problem_files.page.js index 2a599753..205d6f61 100644 --- a/packages/ui-default/pages/problem_files.page.js +++ b/packages/ui-default/pages/problem_files.page.js @@ -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; diff --git a/packages/ui-default/utils/tpl.js b/packages/ui-default/utils/tpl.js index ac3aad5c..d602f1ee 100644 --- a/packages/ui-default/utils/tpl.js +++ b/packages/ui-default/utils/tpl.js @@ -13,10 +13,10 @@ export default function tpl(pieces, ...substitutions) { return result; } -tpl.typoMsg = function (msg) { +tpl.typoMsg = function (msg, raw = false) { return tpl`
-

${msg}

+

${raw ? { html: msg, templateRaw: true } : msg}

`; };