utils&ui: move public utils & add upload

This commit is contained in:
panda committed 2022-05-10 01:51:29 +08:00
1 parent 1f498484d2
commit 4ccb838a06
13 files changed
+703 -397

No files matched your search

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import { isEqual } from 'lodash';
import { Switch, Tab, Tabs } from '@blueprintjs/core';
import type { TestCaseConfig } from 'hydrooj/src/interface';
import type { SubtaskConfig, TestCaseConfig } from 'hydrooj/src/interface';
import type { RootState } from './reducer/index';
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
@@ -13,7 +13,8 @@ const SelectValue = {
task_type: ['min', 'max', 'sum'],
};
const eq = (a: TestCaseConfig[], b: TestCaseConfig[]) => isEqual(a, b);
const eq = (a: SubtaskConfig[], b: SubtaskConfig[]) => isEqual(a, b);
const eq1 = (a: TestCaseConfig, b: TestCaseConfig) => isEqual(a, b);
function FormItem({
columns, label, children, helpText = '', disableLabel = false, ...props
@@ -185,14 +186,90 @@ function ExtraFilesConfig() {
);
}
function CasesSubCasesTable({ index, subindex }) {
const subcases = useSelector((state: RootState) => (index === -1
? state.config.cases[subindex] : state.config.subtasks[index].cases[subindex]), eq1);
const Files = useSelector((state: RootState) => state.testdata);
const dispatch = useDispatch();
const dispatcher = (casesKey: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (ev) => {
dispatch({
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-edit',
casesId: subindex,
casesKey,
value: ev.currentTarget.value,
});
};
const dispatcher1 = (casesKey: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (val) => {
dispatch({
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-edit',
casesId: subindex,
casesKey,
value: val,
});
};
return (
<tr>
{
index === -1 && (
<>
<td>
<input
value={subcases.time || ''}
onChange={dispatcher('time')}
className="textbox"
/>
</td>
<td>
<input
value={subcases.memory || ''}
onChange={dispatcher('memory')}
className="textbox"
/>
</td>
</>
)
}
<td>
<CustomSelectAutoComplete
width="100%"
data={Files}
selectedKeys={[subcases.input]}
onChange={dispatcher1('input')}
/>
</td>
<td>
<CustomSelectAutoComplete
width="100%"
data={Files}
selectedKeys={[subcases.output]}
onChange={dispatcher1('output')}
/>
</td>
<td>
<a
onClick={() => dispatch({
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
})}
><span className="icon icon-close"></span>
</a>
</td>
</tr>
);
}
function CasesTable({ index }) {
const [time, setTime] = useState('');
const [memory, setMemory] = useState('');
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const cases = useSelector((state: RootState) => (index === -1 ? state.config.cases : state.config.subtasks[index].cases), eq);
console.log(cases);
const Files = useSelector((state: RootState) => state.testdata);
const inputRef = useRef(null);
const outputRef = useRef(null);
const dispatch = useDispatch();
return (
<table className="data-table">
@@ -223,29 +300,29 @@ function CasesTable({ index }) {
<th>
{i18n('Input')}<br />
<CustomSelectAutoComplete
ref={inputRef}
width="100%"
data={Files}
selectedKeys={[input]}
onChange={setInput}
/>
</th>
<th>
{i18n('Output')}<br />
<CustomSelectAutoComplete
ref={outputRef}
width="100%"
data={Files}
selectedKeys={[output]}
onChange={setOutput}
/>
</th>
<th>
<th className="col--operation">
<span className="icon icon-wrench"></span><br />
<a
onClick={() => {
setTime('');
setMemory('');
setInput('');
setOutput('');
inputRef?.current.setQuery('');
outputRef?.current.setQuery('');
dispatch({
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
id: index,
@@ -255,34 +332,13 @@ function CasesTable({ index }) {
} : { input, output },
});
}}
><span className="icon icon-add">{i18n('Add')}</span>
><span className="icon icon-add" />
</a>
</th>
</tr>
</thead>
<tbody>
{cases && cases.map((k, v) => (
<tr key={JSON.stringify(k)}>
{
index === -1 && (
<>
<td>{k.time}</td>
<td>{k.memory}</td>
</>
)
}
<td>{k.input}</td>
<td>{k.output}</td>
<td>
<a
onClick={() => dispatch({
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: v,
})}
><span className="icon icon-close"></span>
</a>
</td>
</tr>
))}
{cases && cases.map((k, v) => <CasesSubCasesTable index={index} subindex={v} key={JSON.stringify(k)} />)}
</tbody>
</table>
);
@@ -298,7 +354,7 @@ function SubtasksTable({ data, index }) {
};
return (
<div key={index}>
<span>Subtasks #{index} </span>
<span>Subtasks #{index + 1} </span>
<a onClick={() => { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' }); }}><span className="icon icon-add"></span></a>
<a
style={index === 0 ? { display: 'none' } : {}}
@@ -376,7 +432,7 @@ function SubtasksTable({ data, index }) {
);
}
function TaskConfig() {
function TaskConfig({ onAutoLoad }) {
const subtasks = useSelector((state: RootState) => state.config.subtasks, eq);
const cases = useSelector((state: RootState) => state.config.cases, eq);
const dispatch = useDispatch();
@@ -399,9 +455,8 @@ function TaskConfig() {
? subtasks && <ul>{subtasks.map((k, v) => <SubtasksTable data={k} index={v} key={k.id} />)}</ul>
|| cases && (<CasesTable index={-1} />)
: (
<a
onClick={() => dispatch({ type: 'CONFIG_AUTOCASES_UPDATE' })}
><span className="icon icon-settings"> {i18n('Auto Read Tasks')}</span>
<a onClick={() => onAutoLoad()}>
<span className="icon icon-settings"> {i18n('Auto Read Tasks')}</span>
</a>
)
}
@@ -443,11 +498,11 @@ function LangConfig() {
);
}
export default function ProblemConfigForm() {
export default function ProblemConfigForm({ onAutoLoad }) {
return (
<div className="row">
<BasicInfo />
<TaskConfig />
<TaskConfig onAutoLoad={onAutoLoad} />
<ExtraFilesConfig />
<LangConfig />
</div>
@@ -1,13 +1,11 @@
import yaml from 'js-yaml';
import type { ProblemConfigFile } from 'hydrooj/src/interface';
let autocases = { subtasks: [] };
import { cloneDeep } from 'lodash';
export default function reducer(state = { type: 'default' } as ProblemConfigFile, action): ProblemConfigFile {
switch (action.type) {
case 'CONFIG_LOAD_FULFILLED': {
// TODO set yaml schema
autocases = action.payload.autocases;
return { ...state, ...yaml.load(action.payload.config) as object };
}
case 'CONFIG_FORM_UPDATE': {
@@ -30,7 +28,9 @@ export default function reducer(state = { type: 'default' } as ProblemConfigFile
}
case 'CONFIG_AUTOCASES_UPDATE': {
const next = { ...state };
if (autocases.subtasks.length === 1) {
const autocases = action.value;
if (autocases.subtasks.length === 0) next.cases = [];
else if (autocases.subtasks.length === 1) {
next.score = autocases.subtasks[0].score;
next.cases = [];
autocases.subtasks[0].cases.map((i) => next.cases.push({
@@ -50,21 +50,32 @@ export default function reducer(state = { type: 'default' } as ProblemConfigFile
return next;
}
case 'CONFIG_CASES_UPDATE': {
const next = { ...state };
if (action.key === 'cases-add') next.cases.push(action.value);
else if (action.key === 'cases-delete') {
next.cases = next.cases.filter((k, v) => v !== action.value);
if (action.key === 'cases-add') return { ...state, cases: [...state.cases, action.value] };
if (action.key === 'cases-edit') {
const cases = cloneDeep(state.cases);
cases[action.casesId][action.casesKey] = action.value;
return { ...state, cases };
} if (action.key === 'cases-delete') {
return { ...state, cases: state.cases.filter((k, v) => v !== action.value) };
}
return next;
return state;
}
case 'CONFIG_SUBTASK_UPDATE': {
if (action.key.split('-')[0] === 'cases') {
const subtasks = cloneDeep(state.subtasks);
const subsubtasks = cloneDeep(state.subtasks[action.id]);
if (action.key === 'cases-add') subsubtasks.cases.push(action.value);
else if (action.key === 'cases-edit') subsubtasks.cases[action.casesId][action.casesKey] = action.value;
else if (action.key === 'cases-delete') {
subsubtasks.cases = subsubtasks[action.id].cases.filter((k, v) => v !== action.value);
}
subtasks[action.id] = subsubtasks;
return { ...state, subtasks };
}
const next = { ...state };
if (action.key === 'add') next.subtasks.splice(action.id, 0, { id: 0 });
else if (action.key === 'delete') delete next.subtasks[action.key];
else if (action.key === 'cases-add') next.subtasks[action.id].cases.push(action.value);
else if (action.key === 'cases-delete') {
next.subtasks[action.id].cases = next.subtasks[action.id].cases.filter((k, v) => v !== action.value);
} else next.subtasks[action.id][action.key] = action.value;
else next.subtasks[action.id][action.key] = action.value;
return next;
}
default:
@@ -4,11 +4,11 @@ export default function reducer(state = [], action) {
return state.concat(action.payload.testdata);
}
case 'CONFIG_DELETE_TESTDATA': {
return state.filter((i) => i.name !== action.payload.name);
const next = state.filter((i) => i.name !== action.value[0]);
return next;
}
case 'CONFIG_ADD_TESTDATA': {
state.push(action.payload);
return state;
return state.concat([action.value]);
}
default:
return state;
@@ -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'));
});
+109 -1
View File
@@ -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'));
@@ -2,16 +2,16 @@
{% import "components/problem.html" as problem with context %}
<div class="section side section--problem-sidebar-additional_file {{ 'expanded' if page_name == 'problem_edit' else 'collapsed'}}">
<div class="section__header">
<h1 class="section__title">{{ additional_file.length if additional_file.length else 0 }} {{_('Testdata')}}</h1>
<h1 class="section__title">{{ additional_file.length if additional_file.length else 0 }} {{_('Additional File')}}</h1>
</div>
<ol class="menu">
<li class="menu__item flex-row">
<a class="menu__link half" href="javascript:;" name="create_file">
<span class="icon icon-add"></span> {{ _('Create') }}
</a>
<a class="menu__link half" href="javascript:;" name="upload_file">
<a class="menu__link half" href="javascript:;" name="additional_file__upload">
<span class="icon icon-upload"></span> {{ _('Upload') }}
</a>
<a class="menu__link half" href="javascript:;" name="additional_file__download">
<span class="icon icon-download"></span> {{ _('Download All') }}
</a>
</li>
<li class="menu__seperator"></li>
<li class="menu__item">
@@ -25,7 +25,7 @@
</ol>
<div class="section__body no-padding problem-sidebar-additional_file__detail" data-fragment-id="problem-sidebar-additional_file">
{% if additional_file.length %}
<table class="data-table">
<table class="data-table additionalfile-table">
<colgroup>
<col class="col--name">
<col class="col--size">
@@ -45,7 +45,7 @@
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name}) }}">{{ file.name }}</a>
</td>
<td class="col--size">{{ size(file.size) }}</td>
<td class="col--operation"><a href="javascript:;" name="{{ file.name }}"><span class="icon icon-delete"></span></a></td>
<td class="col--operation"><a href="javascript:;"name="additional_file__delete"><span class="icon icon-delete"></span></a></td>
</tr>
{%- endfor -%}
</tbody>
@@ -6,29 +6,26 @@
</div>
<ol class="menu">
<li class="menu__item flex-row">
<a class="menu__link half" href="javascript:;" name="create_file">
<span class="icon icon-add"></span> {{ _('Create') }}
</a>
<a class="menu__link half" href="javascript:;" name="upload_file">
<a class="menu__link half" href="javascript:;" name="testdata__upload">
<span class="icon icon-upload"></span> {{ _('Upload') }}
</a>
<a class="menu__link half" href="javascript:;" name="testdata__download">
<span class="icon icon-download"></span> {{ _('Download All') }}
</a>
</li>
<li class="menu__seperator"></li>
<li class="menu__item flex-row">
<a class="menu__link half" href="javascript:;" name="upload_file">
<span class="icon icon-download"></span> {{ _('Download All') }}
</a>
<a class="menu__link half expanded--hidden" href="javascript:;" name="testdata__section__expand">
<a class="menu__link expanded--hidden" href="javascript:;" name="testdata__section__expand">
<span class="icon icon-expand_more"></span> {{ _('expand') }}
</a>
<a class="menu__link half collapsed--hidden" href="javascript:;" name="testdata__section__collapse">
<a class="menu__link collapsed--hidden" href="javascript:;" name="testdata__section__collapse">
<span class="icon icon-expand_less"></span> {{ _('collapse') }}
</a>
</li>
</ol>
<div class="section__body no-padding problem-sidebar-testdata__detail" data-fragment-id="problem-sidebar-testdata">
{% if testdata.length %}
<table class="data-table">
<table class="data-table testdata-table">
<colgroup>
<col class="col--name">
<col class="col--size">
@@ -46,7 +43,7 @@
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
<td class="col--name" title="{{ file.name }}"><a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name, query:{type:'testdata'} }) }}">{{ file.name }}</a></td>
<td class="col--size">{{ size(file.size) }}</td>
<td class="col--operation"><a href="javascript:;" name="{{ file.name }}"><span class="icon icon-delete"></span></a></td>
<td class="col--operation"><a href="javascript:;" name="testdata__delete"><span class="icon icon-delete"></span></a></td>
</tr>
{%- endfor -%}
</tbody>