mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
utils&ui: move public utils & add upload
This commit is contained in:
1 parent
1f498484d2
commit
4ccb838a06
13 files changed
+703
-397
No files matched your search
@@ -1,7 +1,7 @@
|
||||
.page--problem_config
|
||||
.section--problem-sidebar-testdata
|
||||
.problem-sidebar-testdata__detail
|
||||
height: 270px
|
||||
max-height: 270px
|
||||
overflow-y: auto
|
||||
|
||||
&.collapsed .collapsed--hidden,
|
||||
|
||||
@@ -5,6 +5,10 @@ import loadReactRedux from 'vj/utils/loadReactRedux';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import yaml from 'js-yaml';
|
||||
import Notification from 'vj/components/notification';
|
||||
import Dialog, { ConfirmDialog } from 'vj/components/dialog/index';
|
||||
import download from 'vj/components/zipDownloader';
|
||||
import { size, readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
|
||||
async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any>, type: string) {
|
||||
const $section = $(ev.currentTarget).closest('.section--problem-sidebar-testdata');
|
||||
@@ -21,7 +25,128 @@ async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
function onBeforeUnload(e) {
|
||||
e.returnValue = '';
|
||||
}
|
||||
|
||||
function ensureFile(testdata) {
|
||||
return (file: string) => testdata.filter((i) => i === file)[0];
|
||||
}
|
||||
|
||||
const page = new NamedPage('problem_config', () => {
|
||||
let reduxStore;
|
||||
|
||||
async function handleClickUpload() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.click();
|
||||
await new Promise((resolve) => { input.onchange = resolve; });
|
||||
const { files } = input;
|
||||
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('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;
|
||||
},
|
||||
});
|
||||
reduxStore.dispatch({
|
||||
type: 'CONFIG_ADD_TESTDATA',
|
||||
value: {
|
||||
_id: file.name,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
},
|
||||
});
|
||||
$('.testdata-table tbody').append(
|
||||
$(tpl`<tr data-filename="${file.name}" data-size="${file.size.toString()}">
|
||||
<td class="col--name" title="${file.name}"><a href="./file/${file.name}?type=testdata">${file.name}</a></td>
|
||||
<td class="col--size">${size(file.size)}</td>
|
||||
<td class="col--operation"><a href="javascript:;" name="testdata__delete"><span class="icon icon-delete"></span></a></td>
|
||||
</tr>`),
|
||||
);
|
||||
}
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
Notification.success(i18n('File uploaded successfully.'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notification.error(i18n('File upload failed: {0}', e.toString()));
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickRemove(ev: JQuery.ClickEvent<Document, undefined, any, any>) {
|
||||
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl.typoMsg(i18n('Confirm to delete the file?')),
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('./files', {
|
||||
operation: 'delete_files',
|
||||
files: file,
|
||||
type: 'testdata',
|
||||
});
|
||||
Notification.success(i18n('File have been deleted.'));
|
||||
reduxStore.dispatch({
|
||||
type: 'CONFIG_DELETE_TESTDATA',
|
||||
value: file,
|
||||
});
|
||||
$(ev.currentTarget).parent().parent().remove();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickDownloadAll() {
|
||||
const files = reduxStore.getState().testdata.map((i) => i.name);
|
||||
const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'testdata' });
|
||||
const targets = [];
|
||||
for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] });
|
||||
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
||||
}
|
||||
|
||||
async function uploadConfig(config:object) {
|
||||
const configYaml = yaml.dump(config);
|
||||
Notification.info(i18n('Saving file...'));
|
||||
@@ -44,6 +169,8 @@ const page = new NamedPage('problem_config', () => {
|
||||
React, render, Provider, store,
|
||||
} = await loadReactRedux(ProblemConfigReducer);
|
||||
|
||||
reduxStore = store;
|
||||
|
||||
store.dispatch({
|
||||
type: 'CONFIG_LOAD',
|
||||
payload: request.get(),
|
||||
@@ -55,7 +182,20 @@ const page = new NamedPage('problem_config', () => {
|
||||
<ProblemConfigEditor />
|
||||
</div>
|
||||
<div className="medium-7 columns">
|
||||
<ProblemConfigForm />
|
||||
<ProblemConfigForm
|
||||
onAutoLoad={async () => {
|
||||
const testdata = (reduxStore.getState().testdata || []).map((i) => i.name);
|
||||
const checkFile = ensureFile(testdata);
|
||||
let autocases = await readCasesFromFiles(testdata, checkFile, {});
|
||||
if (!autocases.count) {
|
||||
autocases = await readSubtasksFromFiles(testdata, checkFile, {}, { subtasks: [] });
|
||||
}
|
||||
reduxStore.dispatch({
|
||||
type: 'CONFIG_AUTOCASES_UPDATE',
|
||||
value: autocases,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="rounded primary button" onClick={() => uploadConfig(store.getState().config)}>{i18n('Submit')}</button>
|
||||
@@ -66,6 +206,9 @@ const page = new NamedPage('problem_config', () => {
|
||||
|
||||
mountComponent();
|
||||
|
||||
$(document).on('click', '[name="testdata__upload"]', () => handleClickUpload());
|
||||
$(document).on('click', '[name="testdata__delete"]', (ev) => handleClickRemove(ev));
|
||||
$(document).on('click', '[name="testdata__download__all"]', () => handleClickDownloadAll());
|
||||
$(document).on('click', '[name="testdata__section__expand"]', (ev) => handleSection(ev, 'expand'));
|
||||
$(document).on('click', '[name="testdata__section__collapse"]', (ev) => handleSection(ev, 'collapse'));
|
||||
});
|
||||
|
||||
@@ -3,11 +3,13 @@ import { NamedPage } from 'vj/misc/Page';
|
||||
import request from 'vj/utils/request';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import Dialog, { ConfirmDialog } from 'vj/components/dialog';
|
||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||
import Editor from 'vj/components/editor/index';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { slideDown, slideUp } from 'vj/utils/slide';
|
||||
import download from 'vj/components/zipDownloader';
|
||||
import { size } from '@hydrooj/utils/lib/common';
|
||||
|
||||
const categories = {};
|
||||
const dirtyCategories = [];
|
||||
@@ -19,6 +21,10 @@ function setDomSelected($dom, selected) {
|
||||
else $dom.removeClass('selected');
|
||||
}
|
||||
|
||||
function onBeforeUnload(e) {
|
||||
e.returnValue = '';
|
||||
}
|
||||
|
||||
async function updateSelection() {
|
||||
dirtyCategories.forEach(({ type, category, subcategory }) => {
|
||||
let item = categories[category];
|
||||
@@ -186,6 +192,105 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
buildCategoryFilter();
|
||||
parseCategorySelection();
|
||||
|
||||
async function handleClickUpload() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.click();
|
||||
await new Promise((resolve) => { input.onchange = resolve; });
|
||||
const { files } = input;
|
||||
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('type', 'additional_file');
|
||||
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;
|
||||
},
|
||||
});
|
||||
$('.additionalfile-table tbody').append(
|
||||
$(tpl`<tr data-filename="${file.name}" data-size="${file.size.toString()}">
|
||||
<td class="col--name" title="${file.name}"><a href="./file/${file.name}?type=testdata">${file.name}</a></td>
|
||||
<td class="col--size">${size(file.size)}</td>
|
||||
<td class="col--operation"><a href="javascript:;" name="testdata__delete"><span class="icon icon-delete"></span></a></td>
|
||||
</tr>`),
|
||||
);
|
||||
}
|
||||
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||
Notification.success(i18n('File uploaded successfully.'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Notification.error(i18n('File upload failed: {0}', e.toString()));
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickRemove(ev) {
|
||||
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl.typoMsg(i18n('Confirm to delete the file?')),
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await request.post('./files', {
|
||||
operation: 'delete_files',
|
||||
files: file,
|
||||
type: 'testdata',
|
||||
});
|
||||
Notification.success(i18n('File have been deleted.'));
|
||||
$(ev.currentTarget).parent().parent().remove();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickDownloadAll() {
|
||||
const files = $('.additionalfile-table tr').map(function () { return $(this).attr('data-filename'); }).get();
|
||||
const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'additional_file' });
|
||||
const targets = [];
|
||||
for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] });
|
||||
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
$('img').each(function () {
|
||||
if (this.src.startsWith('file://')) {
|
||||
@@ -248,6 +353,9 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
$(document).on('click', '[name="additional_file__upload"]', () => handleClickUpload());
|
||||
$(document).on('click', '[name="additional_file__delete"]', (ev) => handleClickRemove(ev));
|
||||
$(document).on('click', '[name="additional_file__download"]', () => handleClickDownloadAll());
|
||||
$(document).on('click', '[name="additional_file__section__expand"]', (ev) => handleSection(ev, 'additional_file', 'expand'));
|
||||
$(document).on('click', '[name="additional_file__section__collapse"]', (ev) => handleSection(ev, 'additional_file', 'collapse'));
|
||||
$(document).on('click', '[name="tags__section__expand"]', (ev) => handleSection(ev, 'tags', 'expand'));
|
||||
|
||||
Reference in new issue
Block a user