mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-15 01:05:42 +00:00
Merge branch 'query-builder' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into query-builder
This commit is contained in:
commit
c046fbfb1e
@ -104,7 +104,7 @@ class CorpusAnalysisStaticVisualization {
|
|||||||
renderTextInfoList() {
|
renderTextInfoList() {
|
||||||
let corpusData = this.data.corpus.o.staticData;
|
let corpusData = this.data.corpus.o.staticData;
|
||||||
let corpusTextInfoListElement = document.querySelector('.corpus-text-info-list');
|
let corpusTextInfoListElement = document.querySelector('.corpus-text-info-list');
|
||||||
let corpusTextInfoList = new CorpusTextInfoList(corpusTextInfoListElement);
|
let corpusTextInfoList = new ResourceLists.CorpusTextInfoList(corpusTextInfoListElement);
|
||||||
let texts = corpusData.s_attrs.text.lexicon;
|
let texts = corpusData.s_attrs.text.lexicon;
|
||||||
let textData = [];
|
let textData = [];
|
||||||
for (let i = 0; i < Object.entries(texts).length; i++) {
|
for (let i = 0; i < Object.entries(texts).length; i++) {
|
||||||
@ -213,7 +213,7 @@ class CorpusAnalysisStaticVisualization {
|
|||||||
|
|
||||||
async renderTokenList() {
|
async renderTokenList() {
|
||||||
let corpusTokenListElement = document.querySelector('.corpus-token-list');
|
let corpusTokenListElement = document.querySelector('.corpus-token-list');
|
||||||
let corpusTokenList = new CorpusTokenList(corpusTokenListElement);
|
let corpusTokenList = new ResourceLists.CorpusTokenList(corpusTokenListElement);
|
||||||
let filteredData = this.filterData();
|
let filteredData = this.filterData();
|
||||||
let stopwords = this.data.stopwords;
|
let stopwords = this.data.stopwords;
|
||||||
if (this.data.stopwords === undefined) {
|
if (this.data.stopwords === undefined) {
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
class Utils {
|
|
||||||
static escape(text) {
|
|
||||||
// https://codereview.stackexchange.com/a/126722
|
|
||||||
var table = {
|
|
||||||
'<': 'lt',
|
|
||||||
'>': 'gt',
|
|
||||||
'"': 'quot',
|
|
||||||
'\'': 'apos',
|
|
||||||
'&': 'amp',
|
|
||||||
'\r': '#10',
|
|
||||||
'\n': '#13'
|
|
||||||
};
|
|
||||||
|
|
||||||
return text.toString().replace(/[<>"'\r\n&]/g, (chr) => {
|
|
||||||
return '&' + table[chr] + ';';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
static unescape(escapedText) {
|
|
||||||
var table = {
|
|
||||||
'lt': '<',
|
|
||||||
'gt': '>',
|
|
||||||
'quot': '"',
|
|
||||||
'apos': "'",
|
|
||||||
'amp': '&',
|
|
||||||
'#10': '\r',
|
|
||||||
'#13': '\n'
|
|
||||||
};
|
|
||||||
|
|
||||||
return escapedText.replace(/&(#?\w+);/g, (match, entity) => {
|
|
||||||
if (table.hasOwnProperty(entity)) {
|
|
||||||
return table[entity];
|
|
||||||
}
|
|
||||||
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static HTMLToElement(HTMLString) {
|
|
||||||
let templateElement = document.createElement('template');
|
|
||||||
templateElement.innerHTML = HTMLString.trim();
|
|
||||||
return templateElement.content.firstChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
static generateElementId(prefix='', suffix='') {
|
|
||||||
for (let i = 0; true; i++) {
|
|
||||||
if (document.querySelector(`#${prefix}${i}${suffix}`) !== null) {continue;}
|
|
||||||
return `${prefix}${i}${suffix}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static isObject(object) {
|
|
||||||
return object !== null && typeof object === 'object' && !Array.isArray(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
static mergeObjectsDeep(...objects) {
|
|
||||||
let mergedObject = {};
|
|
||||||
if (objects.length === 0) {
|
|
||||||
return mergedObject;
|
|
||||||
}
|
|
||||||
if (!Utils.isObject(objects[0])) {throw 'Cannot merge non-object';}
|
|
||||||
if (objects.length === 1) {
|
|
||||||
return Utils.mergeObjectsDeep(mergedObject, objects[0]);
|
|
||||||
}
|
|
||||||
if (!Utils.isObject(objects[1])) {throw 'Cannot merge non-object';}
|
|
||||||
for (let key in objects[0]) {
|
|
||||||
if (objects[0].hasOwnProperty(key)) {
|
|
||||||
if (objects[1].hasOwnProperty(key)) {
|
|
||||||
if (Utils.isObject(objects[0][key]) && Utils.isObject(objects[1][key])) {
|
|
||||||
mergedObject[key] = Utils.mergeObjectsDeep(objects[0][key], objects[1][key]);
|
|
||||||
} else {
|
|
||||||
mergedObject[key] = objects[1][key];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mergedObject[key] = objects[0][key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let key in objects[1]) {
|
|
||||||
if (objects[1].hasOwnProperty(key)) {
|
|
||||||
if (!objects[0].hasOwnProperty(key)) {
|
|
||||||
mergedObject[key] = objects[1][key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (objects.length === 2) {
|
|
||||||
return mergedObject;
|
|
||||||
}
|
|
||||||
return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
class App {
|
App.App = class App {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.data = {
|
this.data = {
|
||||||
promises: {getUser: {}, subscribeUser: {}},
|
promises: {getUser: {}, subscribeUser: {}},
|
||||||
@ -101,4 +101,4 @@ class App {
|
|||||||
// Apply Patch
|
// Apply Patch
|
||||||
jsonpatch.applyPatch(this.data, filteredPatch);
|
jsonpatch.applyPatch(this.data, filteredPatch);
|
||||||
}
|
}
|
||||||
}
|
};
|
1
app/static/js/app/index.js
Normal file
1
app/static/js/app/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
App = {};
|
@ -524,7 +524,7 @@ cqi.api.APIClient = class APIClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dump the values of <field> for match ranges <first> .. <last>
|
* Dump the values of <field> for match ranges <first> .. <last>
|
||||||
* in <subcorpus>. <field> is one of the CQI_CONST_FIELD_* constants.
|
* in <subcorpus>. <field> is one of the cqi.constants.FIELD_* constants.
|
||||||
*
|
*
|
||||||
* @param {string} subcorpus
|
* @param {string} subcorpus
|
||||||
* @param {number} field
|
* @param {number} field
|
||||||
@ -561,9 +561,9 @@ cqi.api.APIClient = class APIClient {
|
|||||||
*
|
*
|
||||||
* returns <n> (id, frequency) pairs flattened into a list of size 2*<n>
|
* returns <n> (id, frequency) pairs flattened into a list of size 2*<n>
|
||||||
* field is one of
|
* field is one of
|
||||||
* - CQI_CONST_FIELD_MATCH
|
* - cqi.constants.FIELD_MATCH
|
||||||
* - CQI_CONST_FIELD_TARGET
|
* - cqi.constants.FIELD_TARGET
|
||||||
* - CQI_CONST_FIELD_KEYWORD
|
* - cqi.constants.FIELD_KEYWORD
|
||||||
*
|
*
|
||||||
* NB: pairs are sorted by frequency desc.
|
* NB: pairs are sorted by frequency desc.
|
||||||
*
|
*
|
||||||
|
43
app/static/js/cqi/constants.js
Normal file
43
app/static/js/cqi/constants.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
cqi.constants = {};
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_KEYWORD = 9;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_MATCH = 16;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_MATCHEND = 17;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET = 0;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_0 = 0;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_1 = 1;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_2 = 2;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_3 = 3;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_4 = 4;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_5 = 5;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_6 = 6;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_7 = 7;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_8 = 8;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
cqi.constants.FIELD_TARGET_9 = 9;
|
@ -1,6 +1 @@
|
|||||||
var cqi = {};
|
var cqi = {};
|
||||||
|
|
||||||
cqi.CONST_FIELD_KEYWORD = 9;
|
|
||||||
cqi.CONST_FIELD_MATCH = 16;
|
|
||||||
cqi.CONST_FIELD_MATCHEND = 17;
|
|
||||||
cqi.CONST_FIELD_TARGET = 0;
|
|
||||||
|
@ -145,17 +145,17 @@ cqi.models.subcorpora.SubcorpusCollection = class SubcorpusCollection extends cq
|
|||||||
let apiName = `${this.corpus.apiName}:${subcorpusName}`;
|
let apiName = `${this.corpus.apiName}:${subcorpusName}`;
|
||||||
/** @type {object} */
|
/** @type {object} */
|
||||||
let fields = {};
|
let fields = {};
|
||||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_MATCH)) {
|
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_MATCH)) {
|
||||||
fields.match = cqi.CONST_FIELD_MATCH;
|
fields.match = cqi.constants.FIELD_MATCH;
|
||||||
}
|
}
|
||||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_MATCHEND)) {
|
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_MATCHEND)) {
|
||||||
fields.matchend = cqi.CONST_FIELD_MATCHEND
|
fields.matchend = cqi.constants.FIELD_MATCHEND
|
||||||
}
|
}
|
||||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_TARGET)) {
|
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_TARGET)) {
|
||||||
fields.target = cqi.CONST_FIELD_TARGET
|
fields.target = cqi.constants.FIELD_TARGET
|
||||||
}
|
}
|
||||||
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.CONST_FIELD_KEYWORD)) {
|
if (await this.client.api.cqp_subcorpus_has_field(apiName, cqi.constants.FIELD_KEYWORD)) {
|
||||||
fields.keyword = cqi.CONST_FIELD_KEYWORD
|
fields.keyword = cqi.constants.FIELD_KEYWORD
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
api_name: apiName,
|
api_name: apiName,
|
||||||
|
138
app/static/js/forms/base-form.js
Normal file
138
app/static/js/forms/base-form.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
Forms.BaseForm = class BaseForm {
|
||||||
|
static htmlClass;
|
||||||
|
|
||||||
|
constructor(formElement) {
|
||||||
|
this.formElement = formElement;
|
||||||
|
this.eventListeners = {
|
||||||
|
'requestLoad': []
|
||||||
|
};
|
||||||
|
this.afterRequestListeners = [];
|
||||||
|
|
||||||
|
for (let selectElement of this.formElement.querySelectorAll('select')) {
|
||||||
|
selectElement.removeAttribute('required');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formElement.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.submit(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(eventType, listener) {
|
||||||
|
if (eventType in this.eventListeners) {
|
||||||
|
this.eventListeners[eventType].push(listener);
|
||||||
|
} else {
|
||||||
|
throw `Unknown event type ${eventType}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(event) {
|
||||||
|
let request = new XMLHttpRequest();
|
||||||
|
let modalElement = Utils.HTMLToElement(
|
||||||
|
`
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4><i class="material-icons left">file_upload</i>Submitting...</h4>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="determinate" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a class="action-button btn red waves-effect waves-light modal-close" data-action="cancel">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
document.querySelector('#modals').appendChild(modalElement);
|
||||||
|
let modal = M.Modal.init(
|
||||||
|
modalElement,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
onCloseEnd: () => {
|
||||||
|
modal.destroy();
|
||||||
|
modalElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
modal.open();
|
||||||
|
|
||||||
|
// Remove all previous helper text elements that indicate errors
|
||||||
|
let errorHelperTextElements = this.formElement
|
||||||
|
.querySelectorAll('.helper-text[data-helper-text-type="error"]');
|
||||||
|
for (let errorHelperTextElement of errorHelperTextElements) {
|
||||||
|
errorHelperTextElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if select elements are filled out properly
|
||||||
|
for (let selectElement of this.formElement.querySelectorAll('select')) {
|
||||||
|
if (selectElement.value === '') {
|
||||||
|
let inputFieldElement = selectElement.closest('.input-field');
|
||||||
|
let errorHelperTextElement = Utils.HTMLToElement(
|
||||||
|
'<span class="helper-text error-color-text" data-helper-text-type="error">Please select an option.</span>'
|
||||||
|
);
|
||||||
|
inputFieldElement.appendChild(errorHelperTextElement);
|
||||||
|
inputFieldElement.querySelector('.select-dropdown').classList.add('invalid');
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup abort handling
|
||||||
|
let cancelElement = modalElement.querySelector('.action-button[data-action="cancel"]');
|
||||||
|
cancelElement.addEventListener('click', (event) => {request.abort();});
|
||||||
|
|
||||||
|
// Setup load handling (after the request completed)
|
||||||
|
request.addEventListener('load', (event) => {
|
||||||
|
for (let listener of this.eventListeners['requestLoad']) {
|
||||||
|
listener(event);
|
||||||
|
}
|
||||||
|
if (request.status === 400) {
|
||||||
|
let responseJson = JSON.parse(request.responseText);
|
||||||
|
for (let [inputName, inputErrors] of Object.entries(responseJson.errors)) {
|
||||||
|
let inputFieldElement = this.formElement
|
||||||
|
.querySelector(`input[name$="${inputName}"], select[name$="${inputName}"]`)
|
||||||
|
.closest('.input-field');
|
||||||
|
for (let inputError of inputErrors) {
|
||||||
|
let errorHelperTextElement = Utils.HTMLToElement(
|
||||||
|
`<span class="helper-text error-color-text" data-helper-type="error">${inputError}</span>`
|
||||||
|
);
|
||||||
|
inputFieldElement.appendChild(errorHelperTextElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.status === 500) {
|
||||||
|
app.flash('Internal Server Error', 'error');
|
||||||
|
}
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup progress handling
|
||||||
|
let progressBarElement = modalElement.querySelector('.progress > .determinate');
|
||||||
|
request.upload.addEventListener('progress', (event) => {
|
||||||
|
let progress = Math.floor(100 * event.loaded / event.total);
|
||||||
|
progressBarElement.style.width = `${progress}%`;
|
||||||
|
});
|
||||||
|
|
||||||
|
request.open(this.formElement.method, this.formElement.action);
|
||||||
|
request.setRequestHeader('Accept', 'application/json');
|
||||||
|
let formData = new FormData(this.formElement);
|
||||||
|
switch (this.formElement.enctype) {
|
||||||
|
case 'application/x-www-form-urlencoded': {
|
||||||
|
let urlSearchParams = new URLSearchParams(formData);
|
||||||
|
request.send(urlSearchParams);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'multipart/form-data': {
|
||||||
|
request.send(formData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'text/plain': {
|
||||||
|
throw 'enctype "text/plain" is not supported';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -16,142 +16,3 @@ Forms.autoInit = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Forms.BaseForm = class BaseForm {
|
|
||||||
static htmlClass;
|
|
||||||
|
|
||||||
constructor(formElement) {
|
|
||||||
this.formElement = formElement;
|
|
||||||
this.eventListeners = {
|
|
||||||
'requestLoad': []
|
|
||||||
};
|
|
||||||
this.afterRequestListeners = [];
|
|
||||||
|
|
||||||
for (let selectElement of this.formElement.querySelectorAll('select')) {
|
|
||||||
selectElement.removeAttribute('required');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.formElement.addEventListener('submit', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.submit(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener(eventType, listener) {
|
|
||||||
if (eventType in this.eventListeners) {
|
|
||||||
this.eventListeners[eventType].push(listener);
|
|
||||||
} else {
|
|
||||||
throw `Unknown event type ${eventType}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(event) {
|
|
||||||
let request = new XMLHttpRequest();
|
|
||||||
let modalElement = Utils.HTMLToElement(
|
|
||||||
`
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h4><i class="material-icons left">file_upload</i>Submitting...</h4>
|
|
||||||
<div class="progress">
|
|
||||||
<div class="determinate" style="width: 0%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<a class="action-button btn red waves-effect waves-light modal-close" data-action="cancel">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
document.querySelector('#modals').appendChild(modalElement);
|
|
||||||
let modal = M.Modal.init(
|
|
||||||
modalElement,
|
|
||||||
{
|
|
||||||
dismissible: false,
|
|
||||||
onCloseEnd: () => {
|
|
||||||
modal.destroy();
|
|
||||||
modalElement.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
modal.open();
|
|
||||||
|
|
||||||
// Remove all previous helper text elements that indicate errors
|
|
||||||
let errorHelperTextElements = this.formElement
|
|
||||||
.querySelectorAll('.helper-text[data-helper-text-type="error"]');
|
|
||||||
for (let errorHelperTextElement of errorHelperTextElements) {
|
|
||||||
errorHelperTextElement.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if select elements are filled out properly
|
|
||||||
for (let selectElement of this.formElement.querySelectorAll('select')) {
|
|
||||||
if (selectElement.value === '') {
|
|
||||||
let inputFieldElement = selectElement.closest('.input-field');
|
|
||||||
let errorHelperTextElement = Utils.HTMLToElement(
|
|
||||||
'<span class="helper-text error-color-text" data-helper-text-type="error">Please select an option.</span>'
|
|
||||||
);
|
|
||||||
inputFieldElement.appendChild(errorHelperTextElement);
|
|
||||||
inputFieldElement.querySelector('.select-dropdown').classList.add('invalid');
|
|
||||||
modal.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup abort handling
|
|
||||||
let cancelElement = modalElement.querySelector('.action-button[data-action="cancel"]');
|
|
||||||
cancelElement.addEventListener('click', (event) => {request.abort();});
|
|
||||||
|
|
||||||
// Setup load handling (after the request completed)
|
|
||||||
request.addEventListener('load', (event) => {
|
|
||||||
for (let listener of this.eventListeners['requestLoad']) {
|
|
||||||
listener(event);
|
|
||||||
}
|
|
||||||
if (request.status === 400) {
|
|
||||||
let responseJson = JSON.parse(request.responseText);
|
|
||||||
for (let [inputName, inputErrors] of Object.entries(responseJson.errors)) {
|
|
||||||
let inputFieldElement = this.formElement
|
|
||||||
.querySelector(`input[name$="${inputName}"], select[name$="${inputName}"]`)
|
|
||||||
.closest('.input-field');
|
|
||||||
for (let inputError of inputErrors) {
|
|
||||||
let errorHelperTextElement = Utils.HTMLToElement(
|
|
||||||
`<span class="helper-text error-color-text" data-helper-type="error">${inputError}</span>`
|
|
||||||
);
|
|
||||||
inputFieldElement.appendChild(errorHelperTextElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (request.status === 500) {
|
|
||||||
app.flash('Internal Server Error', 'error');
|
|
||||||
}
|
|
||||||
modal.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup progress handling
|
|
||||||
let progressBarElement = modalElement.querySelector('.progress > .determinate');
|
|
||||||
request.upload.addEventListener('progress', (event) => {
|
|
||||||
let progress = Math.floor(100 * event.loaded / event.total);
|
|
||||||
progressBarElement.style.width = `${progress}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
request.open(this.formElement.method, this.formElement.action);
|
|
||||||
request.setRequestHeader('Accept', 'application/json');
|
|
||||||
let formData = new FormData(this.formElement);
|
|
||||||
switch (this.formElement.enctype) {
|
|
||||||
case 'application/x-www-form-urlencoded': {
|
|
||||||
let urlSearchParams = new URLSearchParams(formData);
|
|
||||||
request.send(urlSearchParams);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'multipart/form-data': {
|
|
||||||
request.send(formData);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'text/plain': {
|
|
||||||
throw 'enctype "text/plain" is not supported';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ResourceDisplays.CorpusDisplay = class CorpusDisplay extends ResourceDisplays.BaseDisplay {
|
ResourceDisplays.CorpusDisplay = class CorpusDisplay extends ResourceDisplays.ResourceDisplay {
|
||||||
static htmlClass = 'corpus-display';
|
static htmlClass = 'corpus-display';
|
||||||
|
|
||||||
constructor(displayElement) {
|
constructor(displayElement) {
|
||||||
|
@ -3,9 +3,9 @@ var ResourceDisplays = {};
|
|||||||
ResourceDisplays.autoInit = () => {
|
ResourceDisplays.autoInit = () => {
|
||||||
for (let propertyName in ResourceDisplays) {
|
for (let propertyName in ResourceDisplays) {
|
||||||
let property = ResourceDisplays[propertyName];
|
let property = ResourceDisplays[propertyName];
|
||||||
// Call autoInit of all properties that are subclasses of `ResourceDisplays.BaseDisplay`.
|
// Call autoInit of all properties that are subclasses of `ResourceDisplays.ResourceDisplay`.
|
||||||
// This does not include `ResourceDisplays.BaseDisplay` itself.
|
// This does not include `ResourceDisplays.ResourceDisplay` itself.
|
||||||
if (property.prototype instanceof ResourceDisplays.BaseDisplay) {
|
if (property.prototype instanceof ResourceDisplays.ResourceDisplay) {
|
||||||
// Check if the static `htmlClass` property is defined.
|
// Check if the static `htmlClass` property is defined.
|
||||||
if (property.htmlClass === undefined) {return;}
|
if (property.htmlClass === undefined) {return;}
|
||||||
// Gather all HTML elements that have the `this.htmlClass` class
|
// Gather all HTML elements that have the `this.htmlClass` class
|
||||||
@ -15,51 +15,4 @@ ResourceDisplays.autoInit = () => {
|
|||||||
for (let displayElement of displayElements) {new property(displayElement);}
|
for (let displayElement of displayElements) {new property(displayElement);}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ResourceDisplays.BaseDisplay = class BaseDisplay {
|
|
||||||
static htmlClass;
|
|
||||||
|
|
||||||
constructor(displayElement) {
|
|
||||||
this.displayElement = displayElement;
|
|
||||||
this.userId = this.displayElement.dataset.userId;
|
|
||||||
this.isInitialized = false;
|
|
||||||
if (this.userId) {
|
|
||||||
app.subscribeUser(this.userId)
|
|
||||||
.then((response) => {
|
|
||||||
app.socket.on('PATCH', (patch) => {
|
|
||||||
if (this.isInitialized) {this.onPatch(patch);}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
app.getUser(this.userId)
|
|
||||||
.then((user) => {
|
|
||||||
this.init(user);
|
|
||||||
this.isInitialized = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(user) {throw 'Not implemented';}
|
|
||||||
|
|
||||||
onPatch(patch) {throw 'Not implemented';}
|
|
||||||
|
|
||||||
setElement(element, value) {
|
|
||||||
switch (element.tagName) {
|
|
||||||
case 'INPUT': {
|
|
||||||
element.value = value;
|
|
||||||
M.updateTextFields();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
element.innerText = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setElements(elements, value) {
|
|
||||||
for (let element of elements) {
|
|
||||||
this.setElement(element, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ResourceDisplays.JobDisplay = class JobDisplay extends ResourceDisplays.BaseDisplay {
|
ResourceDisplays.JobDisplay = class JobDisplay extends ResourceDisplays.ResourceDisplay {
|
||||||
static htmlClass = 'job-display';
|
static htmlClass = 'job-display';
|
||||||
|
|
||||||
constructor(displayElement) {
|
constructor(displayElement) {
|
||||||
|
46
app/static/js/resource-displays/resource-display.js
Normal file
46
app/static/js/resource-displays/resource-display.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
ResourceDisplays.ResourceDisplay = class ResourceDisplay {
|
||||||
|
static htmlClass;
|
||||||
|
|
||||||
|
constructor(displayElement) {
|
||||||
|
this.displayElement = displayElement;
|
||||||
|
this.userId = this.displayElement.dataset.userId;
|
||||||
|
this.isInitialized = false;
|
||||||
|
if (this.userId) {
|
||||||
|
app.subscribeUser(this.userId)
|
||||||
|
.then((response) => {
|
||||||
|
app.socket.on('PATCH', (patch) => {
|
||||||
|
if (this.isInitialized) {this.onPatch(patch);}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.getUser(this.userId)
|
||||||
|
.then((user) => {
|
||||||
|
this.init(user);
|
||||||
|
this.isInitialized = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(user) {throw 'Not implemented';}
|
||||||
|
|
||||||
|
onPatch(patch) {throw 'Not implemented';}
|
||||||
|
|
||||||
|
setElement(element, value) {
|
||||||
|
switch (element.tagName) {
|
||||||
|
case 'INPUT': {
|
||||||
|
element.value = value;
|
||||||
|
M.updateTextFields();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
element.innerText = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setElements(elements, value) {
|
||||||
|
for (let element of elements) {
|
||||||
|
this.setElement(element, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class AdminUserList extends ResourceList {
|
ResourceLists.AdminUserList = class AdminUserList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'admin-user-list';
|
||||||
for (let adminUserListElement of document.querySelectorAll('.admin-user-list:not(.no-autoinit)')) {
|
|
||||||
new AdminUserList(adminUserListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -108,4 +104,4 @@ class AdminUserList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class CorpusFileList extends ResourceList {
|
ResourceLists.CorpusFileList = class CorpusFileList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'corpus-file-list';
|
||||||
for (let corpusFileListElement of document.querySelectorAll('.corpus-file-list:not(.no-autoinit)')) {
|
|
||||||
new CorpusFileList(corpusFileListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -369,4 +365,4 @@ class CorpusFileList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class CorpusFollowerList extends ResourceList {
|
ResourceLists.CorpusFollowerList = class CorpusFollowerList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'corpus-follower-list';
|
||||||
for (let corpusFollowerListElement of document.querySelectorAll('.corpus-follower-list:not(.no-autoinit)')) {
|
|
||||||
new CorpusFollowerList(corpusFollowerListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -196,4 +192,4 @@ class CorpusFollowerList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class CorpusList extends ResourceList {
|
ResourceLists.CorpusList = class CorpusList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'corpus-list';
|
||||||
for (let corpusListElement of document.querySelectorAll('.corpus-list:not(.no-autoinit)')) {
|
|
||||||
new CorpusList(corpusListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -370,4 +366,4 @@ class CorpusList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,10 +1,5 @@
|
|||||||
class CorpusTextInfoList extends ResourceList {
|
ResourceLists.CorpusTextInfoList = class CorpusTextInfoList extends ResourceLists.ResourceList {
|
||||||
|
static htmlClass = 'corpus-text-info-list';
|
||||||
static autoInit() {
|
|
||||||
for (let corpusTextInfoListElement of document.querySelectorAll('.corpus-text-info-list:not(.no-autoinit)')) {
|
|
||||||
new CorpusTextInfoList(corpusTextInfoListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultOptions = {
|
static defaultOptions = {
|
||||||
page: 5
|
page: 5
|
||||||
@ -12,7 +7,7 @@ class CorpusTextInfoList extends ResourceList {
|
|||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
let _options = Utils.mergeObjectsDeep(
|
let _options = Utils.mergeObjectsDeep(
|
||||||
CorpusTextInfoList.defaultOptions,
|
ResourceLists.CorpusTextInfoList.defaultOptions,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
super(listContainerElement, _options);
|
super(listContainerElement, _options);
|
||||||
@ -26,7 +21,7 @@ class CorpusTextInfoList extends ResourceList {
|
|||||||
get item() {
|
get item() {
|
||||||
return (values) => {
|
return (values) => {
|
||||||
return `
|
return `
|
||||||
<tr class="list-item clickable hoverable">
|
<tr class="list-item hoverable">
|
||||||
<td><span class="title"></span> (<span class="publishing_year"></span>)</td>
|
<td><span class="title"></span> (<span class="publishing_year"></span>)</td>
|
||||||
<td><span class="num_tokens"></span></td>
|
<td><span class="num_tokens"></span></td>
|
||||||
<td><span class="num_sentences"></span></td>
|
<td><span class="num_sentences"></span></td>
|
||||||
@ -109,4 +104,4 @@ class CorpusTextInfoList extends ResourceList {
|
|||||||
clickedSortElement.style.color = '#aa9cc9';
|
clickedSortElement.style.color = '#aa9cc9';
|
||||||
clickedSortElement.innerHTML = clickedSortElement.classList.contains('asc') ? 'arrow_drop_down' : 'arrow_drop_up';
|
clickedSortElement.innerHTML = clickedSortElement.classList.contains('asc') ? 'arrow_drop_down' : 'arrow_drop_up';
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class CorpusTokenList extends ResourceList {
|
ResourceLists.CorpusTokenList = class CorpusTokenList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'corpus-token-list';
|
||||||
for (let corpusTokenListElement of document.querySelectorAll('.corpus-token-list:not(.no-autoinit)')) {
|
|
||||||
new CorpusTokenList(corpusTokenListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultOptions = {
|
static defaultOptions = {
|
||||||
page: 7
|
page: 7
|
||||||
@ -11,7 +7,7 @@ class CorpusTokenList extends ResourceList {
|
|||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
let _options = Utils.mergeObjectsDeep(
|
let _options = Utils.mergeObjectsDeep(
|
||||||
CorpusTokenList.defaultOptions,
|
ResourceLists.CorpusTokenList.defaultOptions,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
super(listContainerElement, _options);
|
super(listContainerElement, _options);
|
||||||
@ -138,4 +134,4 @@ class CorpusTokenList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
};
|
@ -1,4 +1,6 @@
|
|||||||
class DetailledPublicCorpusList extends CorpusList {
|
ResourceLists.DetailedPublicCorpusList = class DetailedPublicCorpusList extends ResourceLists.ResourceList {
|
||||||
|
static htmlClass = 'detailed-public-corpus-list';
|
||||||
|
|
||||||
get item() {
|
get item() {
|
||||||
return (values) => {
|
return (values) => {
|
||||||
return `
|
return `
|
||||||
@ -68,4 +70,4 @@ class DetailledPublicCorpusList extends CorpusList {
|
|||||||
'current-user-is-following': Object.values(corpus.corpus_follower_associations).some(association => association.follower.id === currentUserId)
|
'current-user-is-following': Object.values(corpus.corpus_follower_associations).some(association => association.follower.id === currentUserId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
18
app/static/js/resource-lists/index.js
Normal file
18
app/static/js/resource-lists/index.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
var ResourceLists = {};
|
||||||
|
|
||||||
|
ResourceLists.autoInit = () => {
|
||||||
|
for (let propertyName in ResourceLists) {
|
||||||
|
let property = ResourceLists[propertyName];
|
||||||
|
// Call autoInit of all properties that are subclasses of `ResourceLists.ResourceList`.
|
||||||
|
// This does not include `ResourceLists.ResourceList` itself.
|
||||||
|
if (property.prototype instanceof ResourceLists.ResourceList) {
|
||||||
|
// Check if the static `htmlClass` property is defined.
|
||||||
|
if (property.htmlClass === undefined) {return;}
|
||||||
|
// Gather all HTML elements that have the `this.htmlClass` class
|
||||||
|
// and do not have the `no-autoinit` class.
|
||||||
|
let listElements = document.querySelectorAll(`.${property.htmlClass}:not(.no-autoinit)`);
|
||||||
|
// Create an instance of this class for each display element.
|
||||||
|
for (let listElement of listElements) {new property(listElement);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class JobInputList extends ResourceList {
|
ResourceLists.JobInputList = class JobInputList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'job-input-list';
|
||||||
for (let jobInputListElement of document.querySelectorAll('.job-input-list:not(.no-autoinit)')) {
|
|
||||||
new JobInputList(jobInputListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -90,4 +86,4 @@ class JobInputList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class JobList extends ResourceList {
|
ResourceLists.JobList = class JobList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'job-list';
|
||||||
for (let jobListElement of document.querySelectorAll('.job-list:not(.no-autoinit)')) {
|
|
||||||
new JobList(jobListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -323,4 +319,4 @@ class JobList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class JobResultList extends ResourceList {
|
ResourceLists.JobResultList = class JobResultList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'job-result-list';
|
||||||
for (let jobResultListElement of document.querySelectorAll('.job-result-list:not(.no-autoinit)')) {
|
|
||||||
new JobResultList(jobResultListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -115,4 +111,4 @@ class JobResultList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,4 +1,6 @@
|
|||||||
class PublicCorpusList extends CorpusList {
|
ResourceLists.PublicCorpusList = class PublicCorpusList extends ResourceLists.ResourceList {
|
||||||
|
static htmlClass = 'public-corpus-list';
|
||||||
|
|
||||||
get item() {
|
get item() {
|
||||||
return (values) => {
|
return (values) => {
|
||||||
return `
|
return `
|
||||||
@ -52,4 +54,4 @@ class PublicCorpusList extends CorpusList {
|
|||||||
<ul class="pagination"></ul>
|
<ul class="pagination"></ul>
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,23 +1,10 @@
|
|||||||
class ResourceList {
|
ResourceLists.ResourceList = class ResourceList {
|
||||||
/* A wrapper class for the list.js list.
|
/* A wrapper class for the list.js list.
|
||||||
* This class is not meant to be used directly, instead it should be used as
|
* This class is not meant to be used directly, instead it should be used as
|
||||||
* a base class for concrete resource list implementations.
|
* a base class for concrete resource list implementations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static autoInit() {
|
static htmlClass;
|
||||||
CorpusList.autoInit();
|
|
||||||
CorpusFileList.autoInit();
|
|
||||||
JobList.autoInit();
|
|
||||||
JobInputList.autoInit();
|
|
||||||
JobResultList.autoInit();
|
|
||||||
SpaCyNLPPipelineModelList.autoInit();
|
|
||||||
TesseractOCRPipelineModelList.autoInit();
|
|
||||||
UserList.autoInit();
|
|
||||||
AdminUserList.autoInit();
|
|
||||||
CorpusFollowerList.autoInit();
|
|
||||||
CorpusTextInfoList.autoInit();
|
|
||||||
CorpusTokenList.autoInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultOptions = {
|
static defaultOptions = {
|
||||||
page: 5,
|
page: 5,
|
||||||
@ -36,7 +23,7 @@ class ResourceList {
|
|||||||
}
|
}
|
||||||
let _options = Utils.mergeObjectsDeep(
|
let _options = Utils.mergeObjectsDeep(
|
||||||
{item: this.item, valueNames: this.valueNames},
|
{item: this.item, valueNames: this.valueNames},
|
||||||
ResourceList.defaultOptions,
|
ResourceLists.ResourceList.defaultOptions,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
this.listContainerElement = listContainerElement;
|
this.listContainerElement = listContainerElement;
|
@ -1,9 +1,5 @@
|
|||||||
class SpaCyNLPPipelineModelList extends ResourceList {
|
ResourceLists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'spacy-nlp-pipeline-model-list';
|
||||||
for (let spaCyNLPPipelineModelListElement of document.querySelectorAll('.spacy-nlp-pipeline-model-list:not(.no-autoinit)')) {
|
|
||||||
new SpaCyNLPPipelineModelList(spaCyNLPPipelineModelListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -220,4 +216,4 @@ class SpaCyNLPPipelineModelList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class TesseractOCRPipelineModelList extends ResourceList {
|
ResourceLists.TesseractOCRPipelineModelList = class TesseractOCRPipelineModelList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'tesseract-ocr-pipeline-model-list';
|
||||||
for (let tesseractOCRPipelineModelListElement of document.querySelectorAll('.tesseract-ocr-pipeline-model-list:not(.no-autoinit)')) {
|
|
||||||
new TesseractOCRPipelineModelList(tesseractOCRPipelineModelListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -229,4 +225,4 @@ class TesseractOCRPipelineModelList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
@ -1,9 +1,5 @@
|
|||||||
class UserList extends ResourceList {
|
ResourceLists.UserList = class UserList extends ResourceLists.ResourceList {
|
||||||
static autoInit() {
|
static htmlClass = 'user-list';
|
||||||
for (let userListElement of document.querySelectorAll('.user-list:not(.no-autoinit)')) {
|
|
||||||
new UserList(userListElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(listContainerElement, options = {}) {
|
constructor(listContainerElement, options = {}) {
|
||||||
super(listContainerElement, options);
|
super(listContainerElement, options);
|
||||||
@ -101,4 +97,4 @@ class UserList extends ResourceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
1
app/static/js/utils/index.js
Normal file
1
app/static/js/utils/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
Utils = {};
|
89
app/static/js/utils/utils.js
Normal file
89
app/static/js/utils/utils.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
Utils.escape = (text) => {
|
||||||
|
// https://codereview.stackexchange.com/a/126722
|
||||||
|
var table = {
|
||||||
|
'<': 'lt',
|
||||||
|
'>': 'gt',
|
||||||
|
'"': 'quot',
|
||||||
|
'\'': 'apos',
|
||||||
|
'&': 'amp',
|
||||||
|
'\r': '#10',
|
||||||
|
'\n': '#13'
|
||||||
|
};
|
||||||
|
|
||||||
|
return text.toString().replace(/[<>"'\r\n&]/g, (chr) => {
|
||||||
|
return '&' + table[chr] + ';';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.unescape = (escapedText) => {
|
||||||
|
var table = {
|
||||||
|
'lt': '<',
|
||||||
|
'gt': '>',
|
||||||
|
'quot': '"',
|
||||||
|
'apos': "'",
|
||||||
|
'amp': '&',
|
||||||
|
'#10': '\r',
|
||||||
|
'#13': '\n'
|
||||||
|
};
|
||||||
|
|
||||||
|
return escapedText.replace(/&(#?\w+);/g, (match, entity) => {
|
||||||
|
if (table.hasOwnProperty(entity)) {
|
||||||
|
return table[entity];
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.HTMLToElement = (HTMLString) => {
|
||||||
|
let templateElement = document.createElement('template');
|
||||||
|
templateElement.innerHTML = HTMLString.trim();
|
||||||
|
return templateElement.content.firstChild;
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.generateElementId = (prefix='', suffix='') => {
|
||||||
|
for (let i = 0; true; i++) {
|
||||||
|
if (document.querySelector(`#${prefix}${i}${suffix}`) !== null) {continue;}
|
||||||
|
return `${prefix}${i}${suffix}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.isObject = (object) => {
|
||||||
|
return object !== null && typeof object === 'object' && !Array.isArray(object);
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.mergeObjectsDeep = (...objects) => {
|
||||||
|
let mergedObject = {};
|
||||||
|
if (objects.length === 0) {
|
||||||
|
return mergedObject;
|
||||||
|
}
|
||||||
|
if (!Utils.isObject(objects[0])) {throw 'Cannot merge non-object';}
|
||||||
|
if (objects.length === 1) {
|
||||||
|
return Utils.mergeObjectsDeep(mergedObject, objects[0]);
|
||||||
|
}
|
||||||
|
if (!Utils.isObject(objects[1])) {throw 'Cannot merge non-object';}
|
||||||
|
for (let key in objects[0]) {
|
||||||
|
if (objects[0].hasOwnProperty(key)) {
|
||||||
|
if (objects[1].hasOwnProperty(key)) {
|
||||||
|
if (Utils.isObject(objects[0][key]) && Utils.isObject(objects[1][key])) {
|
||||||
|
mergedObject[key] = Utils.mergeObjectsDeep(objects[0][key], objects[1][key]);
|
||||||
|
} else {
|
||||||
|
mergedObject[key] = objects[1][key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mergedObject[key] = objects[0][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key in objects[1]) {
|
||||||
|
if (objects[1].hasOwnProperty(key)) {
|
||||||
|
if (!objects[0].hasOwnProperty(key)) {
|
||||||
|
mergedObject[key] = objects[1][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (objects.length === 2) {
|
||||||
|
return mergedObject;
|
||||||
|
}
|
||||||
|
return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
|
||||||
|
};
|
@ -7,9 +7,17 @@
|
|||||||
{%- assets
|
{%- assets
|
||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/app.%(version)s.js',
|
output='gen/app.%(version)s.js',
|
||||||
'js/App.js',
|
'js/app/index.js',
|
||||||
'js/Utils.js',
|
'js/app/app.js'
|
||||||
'js/XMLtoObject.js'
|
%}
|
||||||
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
|
||||||
|
{%- assets
|
||||||
|
filters='rjsmin',
|
||||||
|
output='gen/utils.%(version)s.js',
|
||||||
|
'js/utils/index.js',
|
||||||
|
'js/utils/utils.js'
|
||||||
%}
|
%}
|
||||||
<script src="{{ ASSET_URL }}"></script>
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
{%- endassets %}
|
{%- endassets %}
|
||||||
@ -18,6 +26,7 @@
|
|||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/cqi.%(version)s.js',
|
output='gen/cqi.%(version)s.js',
|
||||||
'js/cqi/index.js',
|
'js/cqi/index.js',
|
||||||
|
'js/cqi/constants.js',
|
||||||
'js/cqi/errors.js',
|
'js/cqi/errors.js',
|
||||||
'js/cqi/status.js',
|
'js/cqi/status.js',
|
||||||
'js/cqi/api/index.js',
|
'js/cqi/api/index.js',
|
||||||
@ -36,6 +45,7 @@
|
|||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/Forms.%(version)s.js',
|
output='gen/Forms.%(version)s.js',
|
||||||
'js/forms/index.js',
|
'js/forms/index.js',
|
||||||
|
'js/forms/base-form.js',
|
||||||
'js/forms/create-contribution-form.js',
|
'js/forms/create-contribution-form.js',
|
||||||
'js/forms/create-corpus-file-form.js',
|
'js/forms/create-corpus-file-form.js',
|
||||||
'js/forms/create-job-form.js'
|
'js/forms/create-job-form.js'
|
||||||
@ -47,6 +57,7 @@
|
|||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/resource-displays.%(version)s.js',
|
output='gen/resource-displays.%(version)s.js',
|
||||||
'js/resource-displays/index.js',
|
'js/resource-displays/index.js',
|
||||||
|
'js/resource-displays/resource-display.js',
|
||||||
'js/resource-displays/corpus-display.js',
|
'js/resource-displays/corpus-display.js',
|
||||||
'js/resource-displays/job-display.js'
|
'js/resource-displays/job-display.js'
|
||||||
%}
|
%}
|
||||||
@ -55,22 +66,23 @@
|
|||||||
|
|
||||||
{%- assets
|
{%- assets
|
||||||
filters='rjsmin',
|
filters='rjsmin',
|
||||||
output='gen/ResourceLists.%(version)s.js',
|
output='gen/resource-lists.%(version)s.js',
|
||||||
'js/ResourceLists/ResourceList.js',
|
'js/resource-lists/index.js',
|
||||||
'js/ResourceLists/CorpusFileList.js',
|
'js/resource-lists/resource-list.js',
|
||||||
'js/ResourceLists/CorpusList.js',
|
'js/resource-lists/admin-user-list.js',
|
||||||
'js/ResourceLists/PublicCorpusList.js',
|
'js/resource-lists/corpus-file-list.js',
|
||||||
'js/ResourceLists/JobList.js',
|
'js/resource-lists/corpus-follower-list.js',
|
||||||
'js/ResourceLists/JobInputList.js',
|
'js/resource-lists/corpus-list.js',
|
||||||
'js/ResourceLists/JobResultList.js',
|
'js/resource-lists/corpus-text-info-list.js',
|
||||||
'js/ResourceLists/SpacyNLPPipelineModelList.js',
|
'js/resource-lists/corpus-token-list.js',
|
||||||
'js/ResourceLists/TesseractOCRPipelineModelList.js',
|
'js/resource-lists/detailed-public-corpus-list.js',
|
||||||
'js/ResourceLists/UserList.js',
|
'js/resource-lists/job-input-list.js',
|
||||||
'js/ResourceLists/AdminUserList.js',
|
'js/resource-lists/job-list.js',
|
||||||
'js/ResourceLists/CorpusFollowerList.js',
|
'js/resource-lists/job-result-list.js',
|
||||||
'js/ResourceLists/CorpusTextInfoList.js',
|
'js/resource-lists/public-corpus-list.js',
|
||||||
'js/ResourceLists/DetailledPublicCorpusList.js',
|
'js/resource-lists/spacy-nlp-pipeline-model-list.js',
|
||||||
'js/ResourceLists/CorpusTokenList.js'
|
'js/resource-lists/tesseract-ocr-pipeline-model-list.js',
|
||||||
|
'js/resource-lists/user-list.js'
|
||||||
%}
|
%}
|
||||||
<script src="{{ ASSET_URL }}"></script>
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
{%- endassets %}
|
{%- endassets %}
|
||||||
@ -106,7 +118,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// TODO: Implement an app.run method and use this for all of the following
|
// TODO: Implement an app.run method and use this for all of the following
|
||||||
const app = new App();
|
const app = new App.App();
|
||||||
{%- if current_user.is_authenticated %}
|
{%- if current_user.is_authenticated %}
|
||||||
const currentUserId = {{ current_user.hashid|tojson }};
|
const currentUserId = {{ current_user.hashid|tojson }};
|
||||||
|
|
||||||
@ -141,7 +153,7 @@
|
|||||||
{alignment: 'right', constrainWidth: false, coverTrigger: false}
|
{alignment: 'right', constrainWidth: false, coverTrigger: false}
|
||||||
);
|
);
|
||||||
ResourceDisplays.autoInit();
|
ResourceDisplays.autoInit();
|
||||||
ResourceList.autoInit();
|
ResourceLists.autoInit();
|
||||||
Forms.autoInit();
|
Forms.autoInit();
|
||||||
|
|
||||||
// Display flashed messages
|
// Display flashed messages
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
let corpusListElement = document.querySelector('#corpus-list');
|
let corpusListElement = document.querySelector('#corpus-list');
|
||||||
let corpusList = new CorpusList(corpusListElement);
|
let corpusList = new ResourceLists.CorpusList(corpusListElement);
|
||||||
corpusList.add(
|
corpusList.add(
|
||||||
[
|
[
|
||||||
{% for corpus in corpora %}
|
{% for corpus in corpora %}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
let adminUserListElement = document.querySelector('#admin-user-list');
|
let adminUserListElement = document.querySelector('#admin-user-list');
|
||||||
let adminUserList = new AdminUserList(adminUserListElement);
|
let adminUserList = new ResourceLists.AdminUserList(adminUserListElement);
|
||||||
adminUserList.add(
|
adminUserList.add(
|
||||||
[
|
[
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
|
@ -242,7 +242,7 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
let publicCorpusFileList = new CorpusFileList(document.querySelector('#corpus-file-list'));
|
let publicCorpusFileList = new ResourceLists.CorpusFileList(document.querySelector('#corpus-file-list'));
|
||||||
publicCorpusFileList.add(
|
publicCorpusFileList.add(
|
||||||
[
|
[
|
||||||
{% for corpus_file in corpus.files %}
|
{% for corpus_file in corpus.files %}
|
||||||
@ -259,7 +259,7 @@ refreshButton.addEventListener('click', () => {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
|
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
|
||||||
let publicCorpusFollowerList = new CorpusFollowerList(document.querySelector('.corpus-follower-list'));
|
let publicCorpusFollowerList = new ResourceLists.CorpusFollowerList(document.querySelector('.corpus-follower-list'));
|
||||||
publicCorpusFollowerList.add(
|
publicCorpusFollowerList.add(
|
||||||
[
|
[
|
||||||
{% for cfa in cfas %}
|
{% for cfa in cfas %}
|
||||||
|
@ -148,7 +148,8 @@
|
|||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.then((responseText) => {return new DOMParser().parseFromString(responseText, 'application/xml');})
|
.then((responseText) => {return new DOMParser().parseFromString(responseText, 'application/xml');})
|
||||||
.then((xmlDocument) => {return xmlDocument.toObject();})
|
// .then((xmlDocument) => {return xmlDocument.toObject();})
|
||||||
|
.then((xmlDocument) => {return {};})
|
||||||
.then((feed) => {resolve(feed);});
|
.then((feed) => {resolve(feed);});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
let userList = new UserList(document.querySelector('.user-list'));
|
let userList = new ResourceLists.UserList(document.querySelector('.user-list'));
|
||||||
userList.add(
|
userList.add(
|
||||||
[
|
[
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
@ -70,7 +70,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let publicCorpusList = new PublicCorpusList(document.querySelector('.public-corpus-list'));
|
let publicCorpusList = new ResourceLists.PublicCorpusList(document.querySelector('.public-corpus-list'));
|
||||||
publicCorpusList.add(
|
publicCorpusList.add(
|
||||||
[
|
[
|
||||||
{% for corpus in corpora %}
|
{% for corpus in corpora %}
|
||||||
|
@ -122,7 +122,7 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
let followedCorpusList = new PublicCorpusList(document.querySelector('.followed-corpus-list'));
|
let followedCorpusList = new ResourceLists.PublicCorpusList(document.querySelector('.followed-corpus-list'));
|
||||||
followedCorpusList.add(
|
followedCorpusList.add(
|
||||||
[
|
[
|
||||||
{% for corpus in user.followed_corpora %}
|
{% for corpus in user.followed_corpora %}
|
||||||
@ -132,7 +132,7 @@ followedCorpusList.add(
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let publicCorpusList = new PublicCorpusList(document.querySelector('.public-corpus-list'));
|
let publicCorpusList = new ResourceLists.PublicCorpusList(document.querySelector('.public-corpus-list'));
|
||||||
publicCorpusList.add(
|
publicCorpusList.add(
|
||||||
[
|
[
|
||||||
{% for corpus in user.corpora %}
|
{% for corpus in user.corpora %}
|
||||||
|
@ -6,16 +6,24 @@ NO_COLOR="\033[0m"
|
|||||||
CHECK_MARK="\xE2\x9C\x93"
|
CHECK_MARK="\xE2\x9C\x93"
|
||||||
CROSS_MARK="\xE2\x9D\x8C"
|
CROSS_MARK="\xE2\x9D\x8C"
|
||||||
|
|
||||||
echo -n "Set container UID and GIDs to match the host system..."
|
|
||||||
|
|
||||||
if [[ "${NOPAQUE_UID}" == 0 ]]; then
|
if [[ "${NOPAQUE_UID}" == "0" ]]; then
|
||||||
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
||||||
echo "Running as root is not allowed"
|
echo "Running as root is not allowed"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo "Set container UID and GIDs to match the host system..."
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# docker GID #
|
||||||
|
##############################################################################
|
||||||
|
if [[ "${DOCKER_GID}" == "$(getent group docker | cut -d: -f3)" ]]; then
|
||||||
|
echo -n "- docker GID is already matching..."
|
||||||
|
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
||||||
|
else
|
||||||
echo -n "- Updating docker GID ($(getent group docker | cut -d: -f3) -> ${DOCKER_GID})... "
|
echo -n "- Updating docker GID ($(getent group docker | cut -d: -f3) -> ${DOCKER_GID})... "
|
||||||
groupmod --gid "${DOCKER_GID}" docker > /dev/null
|
groupmod --gid "${DOCKER_GID}" docker > /dev/null
|
||||||
if [[ "${?}" == "0" ]]; then
|
if [[ "${?}" == "0" ]]; then
|
||||||
@ -24,16 +32,44 @@ else
|
|||||||
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# nopaque GID #
|
||||||
|
##############################################################################
|
||||||
|
if [[ "${NOPAQUE_GID}" == "$(id -g nopaque)" ]]; then
|
||||||
|
echo -n "- nopaque GID is already matching..."
|
||||||
|
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
||||||
|
else
|
||||||
echo -n "- Updating nopaque GID ($(id -g nopaque) -> ${NOPAQUE_GID})... "
|
echo -n "- Updating nopaque GID ($(id -g nopaque) -> ${NOPAQUE_GID})... "
|
||||||
groupmod --gid "${NOPAQUE_GID}" nopaque > /dev/null
|
groupmod --gid "${NOPAQUE_GID}" nopaque > /dev/null
|
||||||
if [[ "${?}" == "0" ]]; then
|
if [[ "${?}" == "0" ]]; then
|
||||||
|
HAS_NOPAQUE_GID_CHANGED=true
|
||||||
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
||||||
else
|
else
|
||||||
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo -n "- Updating nopaque directory group... "
|
||||||
|
chown -R :nopaque /home/nopaque
|
||||||
|
if [[ "${?}" == "0" ]]; then
|
||||||
|
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
||||||
|
else
|
||||||
|
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# nopaque UID #
|
||||||
|
##############################################################################
|
||||||
|
if [[ "${NOPAQUE_UID}" == "$(id -u nopaque)" ]]; then
|
||||||
|
echo -n "- nopaque UID is already matching..."
|
||||||
|
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
||||||
|
else
|
||||||
echo -n "- Updating nopaque UID ($(id -u nopaque) -> ${NOPAQUE_UID})... "
|
echo -n "- Updating nopaque UID ($(id -u nopaque) -> ${NOPAQUE_UID})... "
|
||||||
usermod --uid "${NOPAQUE_UID}" nopaque > /dev/null
|
usermod --uid "${NOPAQUE_UID}" nopaque > /dev/null
|
||||||
if [[ "${?}" == "0" ]]; then
|
if [[ "${?}" == "0" ]]; then
|
||||||
@ -43,13 +79,15 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n "- Updating nopaque directory owner and group... "
|
echo -n "- Updating nopaque directory owner... "
|
||||||
chown -R nopaque:nopaque /home/nopaque
|
chown -R nopaque /home/nopaque
|
||||||
if [[ "${?}" == "0" ]]; then
|
if [[ "${?}" == "0" ]]; then
|
||||||
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
echo -e "${GREEN_COLOR}${CHECK_MARK}${NO_COLOR}"
|
||||||
else
|
else
|
||||||
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
echo -e "${RED_COLOR}${CROSS_MARK}${NO_COLOR}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
exec gosu nopaque ./boot.sh ${@}
|
exec gosu nopaque ./boot.sh ${@}
|
||||||
|
Loading…
Reference in New Issue
Block a user