diff --git a/packages/ui-default/build/config/webpack.ts b/packages/ui-default/build/config/webpack.ts index f9901c360..6edf19584 100644 --- a/packages/ui-default/build/config/webpack.ts +++ b/packages/ui-default/build/config/webpack.ts @@ -89,8 +89,8 @@ export default function (env: { production?: boolean, measure?: boolean } = {}) hashFunction: 'sha1', hashDigest: 'hex', hashDigestLength: 10, - filename: '[name].js?[contenthash]', - chunkFilename: '[name].[chunkhash].chunk.js', + filename: '[name].js?[contenthash:4]', + chunkFilename: '[name].[chunkhash:4].chunk.js', }, resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'], @@ -124,15 +124,15 @@ export default function (env: { production?: boolean, measure?: boolean } = {}) const extra = p.split('node_modules')[1]; const moduleName = extra.split('/')[0]; if (extra.includes('@fontsource')) { - return `fonts/${filename}?[hash]`; + return `fonts/${filename}?[hash:4]`; } if (['katex', 'monaco-editor'].includes(moduleName)) { - return `modules/${moduleName}/${filename}?[hash]`; + return `modules/${moduleName}/${filename}?[hash:4]`; } - return `modules/${extra.substr(1)}?[hash]`; + return `modules/${extra.substr(1)}?[hash:4]`; } - if (p.includes('.iconfont')) return `${filename}?[hash]`; - return `${p.split('ui-default')[1].substring(1)}?[hash]`; + if (p.includes('.iconfont')) return `${filename}?[hash:4]`; + return `${p.split('ui-default')[1].substring(1)}?[hash:4]`; }, }, }, @@ -207,8 +207,8 @@ export default function (env: { production?: boolean, measure?: boolean } = {}) ], exclude: [/mathmaps/, /\.min\.js$/], })], - moduleIds: env.production ? 'size' : 'named', - chunkIds: env.production ? 'size' : 'named', + moduleIds: env.production ? 'deterministic' : 'named', + chunkIds: env.production ? 'deterministic' : 'named', }, plugins: [ new CleanWebpackPlugin(), @@ -221,7 +221,7 @@ export default function (env: { production?: boolean, measure?: boolean } = {}) monaco: 'monaco-editor/esm/vs/editor/editor.api', }), new ExtractCssPlugin({ - filename: '[name].css?[fullhash:10]', + filename: '[name].css?[fullhash:4]', }), new FriendlyErrorsPlugin(), new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs)/ }), @@ -240,7 +240,7 @@ export default function (env: { production?: boolean, measure?: boolean } = {}) }), new webpack.NormalModuleReplacementPlugin(/\/(vscode-)?nls\.js/, require.resolve('../../components/monaco/nls')), new MonacoWebpackPlugin({ - filename: '[name].[hash:10].worker.js', + filename: '[name].[hash:4].worker.js', customLanguages: [{ label: 'yaml', entry: require.resolve('monaco-yaml/index.js'), diff --git a/packages/ui-default/build/main.ts b/packages/ui-default/build/main.ts index a81e9080c..07da427e3 100644 --- a/packages/ui-default/build/main.ts +++ b/packages/ui-default/build/main.ts @@ -56,7 +56,10 @@ async function runWebpack({ for (const file of files) { if (!file.isFile() || file.name.endsWith('.map')) continue; const data = await fs.stat(path.join(root('public'), file.name)); - stats[file.name.replace(/\.[a-f0-9]{10}\./, '.')] = data.size; + const key = file.name + .replace(/\.[a-f0-9]{4}\.chunk\./, '.chunk.') + .replace(/\.[a-f0-9]{4}\.worker\./, '.worker.'); + stats[key] = data.size; } const statsPath = root('__bundleInfo'); if (fs.existsSync(statsPath)) { diff --git a/packages/ui-default/components/upload.ts b/packages/ui-default/components/upload.ts new file mode 100644 index 000000000..f698d812f --- /dev/null +++ b/packages/ui-default/components/upload.ts @@ -0,0 +1,77 @@ +import { Dialog } from 'vj/components/dialog/index'; +import Notification from 'vj/components/notification'; +import delay from 'vj/utils/delay'; +import i18n from 'vj/utils/i18n'; +import pjax from 'vj/utils/pjax'; +import request from 'vj/utils/request'; + +function onBeforeUnload(e) { + e.returnValue = ''; +} + +interface UploadOptions { + type?: string; + pjax?: boolean; + singleFileUploadCallback?: (file: File) => any; +} +export default async function uploadFiles(endpoint = '', files: File[] | FileList, options: UploadOptions = {}) { + 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); + if (options.type) data.append('type', options.type); + data.append('operation', 'upload_file'); + await request.postFile(endpoint, 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; + }, + }); + await options.singleFileUploadCallback?.(file); + } + window.removeEventListener('beforeunload', onBeforeUnload); + Notification.success(i18n('File uploaded successfully.')); + if (options.pjax) await pjax.request({ push: false }); + } catch (e) { + console.error(e); + Notification.error(i18n('File upload failed: {0}', e.toString())); + } finally { + await delay(500); + dialog.close(); + } +} diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 1ab436478..76ec8728c 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.39.11", + "version": "4.39.12", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/files.page.js b/packages/ui-default/pages/files.page.js index 86bcc54a4..dc466b961 100644 --- a/packages/ui-default/pages/files.page.js +++ b/packages/ui-default/pages/files.page.js @@ -1,17 +1,14 @@ import $ from 'jquery'; import _ from 'lodash'; -import { ConfirmDialog, Dialog } from 'vj/components/dialog/index'; +import { ConfirmDialog } from 'vj/components/dialog/index'; import Notification from 'vj/components/notification'; +import uploadFiles from 'vj/components/upload'; import { NamedPage } from 'vj/misc/Page'; import i18n from 'vj/utils/i18n'; import pjax from 'vj/utils/pjax'; import request from 'vj/utils/request'; import tpl from 'vj/utils/tpl'; -function onBeforeUnload(e) { - e.returnValue = ''; -} - function ensureAndGetSelectedFiles() { const files = _.map( $('.files tbody [data-checkbox-group="files"]:checked'), @@ -37,62 +34,7 @@ async function handleClickUpload(files) { 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(); - } + await uploadFiles('', files, { pjax: true }); } async function handleClickRemoveSelected() { diff --git a/packages/ui-default/pages/problem_config.page.tsx b/packages/ui-default/pages/problem_config.page.tsx index ee8ced08d..aa3bb31a2 100644 --- a/packages/ui-default/pages/problem_config.page.tsx +++ b/packages/ui-default/pages/problem_config.page.tsx @@ -4,9 +4,10 @@ import $ from 'jquery'; import yaml from 'js-yaml'; import React from 'react'; import { createRoot } from 'react-dom/client'; -import Dialog, { ConfirmDialog } from 'vj/components/dialog/index'; +import { ConfirmDialog } from 'vj/components/dialog/index'; import Notification from 'vj/components/notification'; import { configYamlFormat } from 'vj/components/problemconfig/ProblemConfigEditor'; +import uploadFiles from 'vj/components/upload'; import download from 'vj/components/zipDownloader'; import { NamedPage } from 'vj/misc/Page'; import i18n from 'vj/utils/i18n'; @@ -14,10 +15,6 @@ import loadReactRedux from 'vj/utils/loadReactRedux'; import request from 'vj/utils/request'; import tpl from 'vj/utils/tpl'; -function onBeforeUnload(e) { - e.returnValue = ''; -} - const page = new NamedPage('problem_config', () => { let reduxStore; @@ -27,54 +24,9 @@ const page = new NamedPage('problem_config', () => { input.multiple = true; input.click(); await new Promise((resolve) => { input.onchange = resolve; }); - const { files } = input; - 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('type', 'testdata'); - data.append('operation', 'upload_file'); - await request.postFile('./files', 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; - }, - }); + await uploadFiles('./files', input.files, { + type: 'testdata', + singleFileUploadCallback: (file) => { reduxStore.dispatch({ type: 'CONFIG_ADD_TESTDATA', value: { @@ -90,15 +42,8 @@ const page = new NamedPage('problem_config', () => {