mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-12-25 02:44:18 +00:00
Rework list handling
This commit is contained in:
parent
85385ef7e4
commit
1b5b935a28
@ -33,38 +33,24 @@ def disconnect():
|
|||||||
connected_sessions.remove(request.sid)
|
connected_sessions.remove(request.sid)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('user_data_stream_init')
|
@socketio.on('start_user_session')
|
||||||
@socketio_login_required
|
@socketio_login_required
|
||||||
def user_data_stream_init():
|
def start_user_session(user_id):
|
||||||
socketio.start_background_task(user_data_stream,
|
if not (current_user.id == user_id or current_user.is_administrator):
|
||||||
|
return
|
||||||
|
socketio.start_background_task(user_session,
|
||||||
current_app._get_current_object(),
|
current_app._get_current_object(),
|
||||||
current_user.id, request.sid)
|
user_id, request.sid)
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('foreign_user_data_stream_init')
|
def user_session(app, user_id, session_id):
|
||||||
@socketio_login_required
|
|
||||||
@socketio_admin_required
|
|
||||||
def foreign_user_data_stream_init(user_id):
|
|
||||||
socketio.start_background_task(user_data_stream,
|
|
||||||
current_app._get_current_object(),
|
|
||||||
user_id, request.sid, foreign=True)
|
|
||||||
|
|
||||||
|
|
||||||
def user_data_stream(app, user_id, session_id, foreign=False):
|
|
||||||
'''
|
'''
|
||||||
' Sends initial corpus and job lists to the client. Afterwards it checks
|
' Sends initial user data to the client. Afterwards it checks every 3s if
|
||||||
' every 3 seconds if changes to the initial values appeared. If changes are
|
' changes to the initial values appeared. If changes are detected, a
|
||||||
' detected, a RFC 6902 compliant JSON patch gets send.
|
' RFC 6902 compliant JSON patch gets send.
|
||||||
'
|
|
||||||
' NOTE: The initial values are send as a init events.
|
|
||||||
' The JSON patches are send as update events.
|
|
||||||
'''
|
'''
|
||||||
if foreign:
|
init_event = 'user_{}_init'.format(user_id)
|
||||||
init_event = 'foreign_user_data_stream_init'
|
patch_event = 'user_{}_patch'.format(user_id)
|
||||||
update_event = 'foreign_user_data_stream_update'
|
|
||||||
else:
|
|
||||||
init_event = 'user_data_stream_init'
|
|
||||||
update_event = 'user_data_stream_update'
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
# Gather current values from database.
|
# Gather current values from database.
|
||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
@ -80,7 +66,7 @@ def user_data_stream(app, user_id, session_id, foreign=False):
|
|||||||
new_user_dict)
|
new_user_dict)
|
||||||
# In case there are patches, send them to the client.
|
# In case there are patches, send them to the client.
|
||||||
if user_patch:
|
if user_patch:
|
||||||
socketio.emit(update_event, user_patch.to_string(),
|
socketio.emit(patch_event, user_patch.to_string(),
|
||||||
room=session_id)
|
room=session_id)
|
||||||
# Set new values as references for the next iteration.
|
# Set new values as references for the next iteration.
|
||||||
user_dict = new_user_dict
|
user_dict = new_user_dict
|
||||||
|
@ -8,6 +8,10 @@ main {
|
|||||||
margin-top: 48px;
|
margin-top: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.ressource-list tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.parallax-container .parallax {
|
.parallax-container .parallax {
|
||||||
z-index: auto;
|
z-index: auto;
|
||||||
}
|
}
|
||||||
|
@ -1,96 +1,138 @@
|
|||||||
|
class AppClient {
|
||||||
|
constructor(currentUserId) {
|
||||||
|
this.socket = io({transports: ['websocket']});
|
||||||
|
this.users = {};
|
||||||
|
this.users.self = this.loadUser(currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUser(userId) {
|
||||||
|
let user = new User();
|
||||||
|
this.users[userId] = user;
|
||||||
|
this.socket.on(`user_${userId}_init`, msg => user.init(JSON.parse(msg)));
|
||||||
|
this.socket.on(`user_${userId}_patch`, msg => user.patch(JSON.parse(msg)));
|
||||||
|
this.socket.emit('start_user_session', userId);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class User {
|
||||||
|
constructor() {
|
||||||
|
this.data = undefined;
|
||||||
|
this.eventListeners = {
|
||||||
|
corporaInit: [],
|
||||||
|
corporaPatch: [],
|
||||||
|
jobsInit: [],
|
||||||
|
jobsPatch: [],
|
||||||
|
queryResultsInit: [],
|
||||||
|
queryResultsPatch: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init(data) {
|
||||||
|
this.data = data;
|
||||||
|
|
||||||
|
let listener;
|
||||||
|
for (listener of this.eventListeners.corporaInit) {
|
||||||
|
listener(this.data.corpora);
|
||||||
|
}
|
||||||
|
for (listener of this.eventListeners.jobsInit) {
|
||||||
|
listener(this.data.jobs);
|
||||||
|
}
|
||||||
|
for (listener of this.eventListeners.queryResultsInit) {
|
||||||
|
listener(this.data.query_results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(patch) {
|
||||||
|
this.data = jsonpatch.apply_patch(this.data, patch);
|
||||||
|
|
||||||
|
let corporaPatch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
||||||
|
let jobsPatch = patch.filter(operation => operation.path.startsWith("/jobs"));
|
||||||
|
let queryResultsPatch = patch.filter(operation => operation.path.startsWith("/query_results"));
|
||||||
|
|
||||||
|
for (let listener of this.eventListeners.corporaPatch) {
|
||||||
|
if (corporaPatch.length > 0) {listener(corporaPatch);}
|
||||||
|
}
|
||||||
|
for (let listener of this.eventListeners.jobsPatch) {
|
||||||
|
if (jobsPatch.length > 0) {listener(jobsPatch);}
|
||||||
|
}
|
||||||
|
for (let listener of this.eventListeners.queryResultsPatch) {
|
||||||
|
if (queryResultsPatch.length > 0) {listener(queryResultsPatch);}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let operation of jobsPatch) {
|
||||||
|
if (operation.op !== 'replace') {continue;}
|
||||||
|
// Matches the only path that should be handled here: /jobs/{jobId}/status
|
||||||
|
if (/^\/jobs\/(\d+)\/status$/.test(operation.path)) {
|
||||||
|
let [match, jobId] = operation.path.match(/^\/jobs\/(\d+)\/status$/);
|
||||||
|
if (this.data.settings.job_status_site_notifications === "end" && !["complete", "failed"].includes(operation.value)) {continue;}
|
||||||
|
nopaque.flash(`[<a href="/jobs/${jobId}">${this.data.jobs[jobId].title}</a>] New status: ${operation.value}`, "job");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(type, listener) {
|
||||||
|
switch (type) {
|
||||||
|
case 'corporaInit':
|
||||||
|
this.eventListeners.corporaInit.push(listener);
|
||||||
|
if (this.data !== undefined) {listener(this.data.corpora);}
|
||||||
|
break;
|
||||||
|
case 'corporaPatch':
|
||||||
|
this.eventListeners.corporaPatch.push(listener);
|
||||||
|
break;
|
||||||
|
case 'jobsInit':
|
||||||
|
this.eventListeners.jobsInit.push(listener);
|
||||||
|
if (this.data !== undefined) {listener(this.data.jobs);}
|
||||||
|
break;
|
||||||
|
case 'jobsPatch':
|
||||||
|
this.eventListeners.jobsPatch.push(listener);
|
||||||
|
break;
|
||||||
|
case 'queryResultsInit':
|
||||||
|
this.eventListeners.queryResultsInit.push(listener);
|
||||||
|
if (this.data !== undefined) {listener(this.data.query_results);}
|
||||||
|
break;
|
||||||
|
case 'queryResultsPatch':
|
||||||
|
this.eventListeners.queryResultsPatch.push(listener);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Unknown event type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The nopaque object is used as a namespace for nopaque specific functions and
|
* The nopaque object is used as a namespace for nopaque specific functions and
|
||||||
* variables.
|
* variables.
|
||||||
*/
|
*/
|
||||||
var nopaque = {};
|
var nopaque = {};
|
||||||
|
|
||||||
// User data
|
nopaque.flash = function(message, category) {
|
||||||
nopaque.user = {};
|
let toast;
|
||||||
nopaque.user.settings = {};
|
let toastActionElement;
|
||||||
nopaque.user.settings.darkMode = undefined;
|
|
||||||
nopaque.corporaSubscribers = [];
|
|
||||||
nopaque.jobsSubscribers = [];
|
|
||||||
nopaque.queryResultsSubscribers = [];
|
|
||||||
|
|
||||||
// Foreign user (user inspected with admin credentials) data
|
switch (category) {
|
||||||
nopaque.foreignUser = {};
|
case "corpus":
|
||||||
nopaque.foreignUser.isAuthenticated = undefined;
|
message = `<i class="left material-icons">book</i>${message}`;
|
||||||
nopaque.foreignUser.settings = {};
|
break;
|
||||||
nopaque.foreignUser.settings.darkMode = undefined;
|
case "error":
|
||||||
nopaque.foreignCorporaSubscribers = [];
|
message = `<i class="left material-icons red-text">error</i>${message}`;
|
||||||
nopaque.foreignJobsSubscribers = [];
|
break;
|
||||||
nopaque.foreignQueryResultsSubscribers = [];
|
case "job":
|
||||||
|
message = `<i class="left material-icons">work</i>${message}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = `<i class="left material-icons">notifications</i>${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
// nopaque functions
|
toast = M.toast({html: `<span>${message}</span>
|
||||||
nopaque.socket = io({transports: ['websocket']});
|
<button data-action="close" class="btn-flat toast-action white-text">
|
||||||
// Add event handlers
|
<i class="material-icons">close</i>
|
||||||
nopaque.socket.on("user_data_stream_init", function(msg) {
|
</button>`});
|
||||||
nopaque.user = JSON.parse(msg);
|
toastActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
|
||||||
for (let subscriber of nopaque.corporaSubscribers) {
|
toastActionElement.addEventListener('click', () => {toast.dismiss();});
|
||||||
subscriber.init(nopaque.user.corpora);
|
};
|
||||||
}
|
|
||||||
for (let subscriber of nopaque.jobsSubscribers) {
|
|
||||||
subscriber.init(nopaque.user.jobs);
|
|
||||||
}
|
|
||||||
for (let subscriber of nopaque.queryResultsSubscribers) {
|
|
||||||
subscriber.init(nopaque.user.query_results);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nopaque.socket.on("user_data_stream_update", function(msg) {
|
|
||||||
var patch;
|
|
||||||
|
|
||||||
patch = JSON.parse(msg);
|
|
||||||
nopaque.user = jsonpatch.apply_patch(nopaque.user, patch);
|
|
||||||
corpora_patch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
|
||||||
jobs_patch = patch.filter(operation => operation.path.startsWith("/jobs"));
|
|
||||||
query_results_patch = patch.filter(operation => operation.path.startsWith("/query_results"));
|
|
||||||
for (let subscriber of nopaque.corporaSubscribers) {
|
|
||||||
subscriber.update(corpora_patch);
|
|
||||||
}
|
|
||||||
for (let subscriber of nopaque.jobsSubscribers) {
|
|
||||||
subscriber.update(jobs_patch);
|
|
||||||
}
|
|
||||||
for (let subscriber of nopaque.queryResultsSubscribers) {
|
|
||||||
subscriber.update(query_results_patch);
|
|
||||||
}
|
|
||||||
if (["all", "end"].includes(nopaque.user.settings.job_status_site_notifications)) {
|
|
||||||
for (operation of jobs_patch) {
|
|
||||||
/* "/jobs/{jobId}/..." -> ["{jobId}", ...] */
|
|
||||||
pathArray = operation.path.split("/").slice(2);
|
|
||||||
if (operation.op === "replace" && pathArray[1] === "status") {
|
|
||||||
if (nopaque.user.settings.job_status_site_notifications === "end" && !["complete", "failed"].includes(operation.value)) {continue;}
|
|
||||||
nopaque.flash(`[<a href="/jobs/${pathArray[0]}">${nopaque.user.jobs[pathArray[0]].title}</a>] New status: ${operation.value}`, "job");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nopaque.socket.on("foreign_user_data_stream_init", function(msg) {
|
|
||||||
nopaque.foreignUser = JSON.parse(msg);
|
|
||||||
for (let subscriber of nopaque.foreignCorporaSubscribers) {
|
|
||||||
subscriber.init(nopaque.foreignUser.corpora);
|
|
||||||
}
|
|
||||||
for (let subscriber of nopaque.foreignJobsSubscribers) {
|
|
||||||
subscriber.init(nopaque.foreignUser.jobs);
|
|
||||||
}
|
|
||||||
for (let subscriber of nopaque.foreignQueryResultsSubscribers) {
|
|
||||||
subscriber.init(nopaque.foreignUser.query_results);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nopaque.socket.on("foreign_user_data_stream_update", function(msg) {
|
|
||||||
var patch;
|
|
||||||
|
|
||||||
patch = JSON.parse(msg);
|
|
||||||
nopaque.foreignUser = jsonpatch.apply_patch(nopaque.foreignUser, patch);
|
|
||||||
corpora_patch = patch.filter(operation => operation.path.startsWith("/corpora"));
|
|
||||||
jobs_patch = patch.filter(operation => operation.path.startsWith("/jobs"));
|
|
||||||
query_results_patch = patch.filter(operation => operation.path.startsWith("/query_results"));
|
|
||||||
for (let subscriber of nopaque.foreignCorporaSubscribers) {subscriber.update(corpora_patch);}
|
|
||||||
for (let subscriber of nopaque.foreignJobsSubscribers) {subscriber.update(jobs_patch);}
|
|
||||||
for (let subscriber of nopaque.foreignQueryResultsSubscribers) {subscriber.update(query_results_patch);}
|
|
||||||
});
|
|
||||||
|
|
||||||
nopaque.Forms = {};
|
nopaque.Forms = {};
|
||||||
nopaque.Forms.init = function() {
|
nopaque.Forms.init = function() {
|
||||||
@ -163,30 +205,3 @@ nopaque.Forms.init = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
nopaque.flash = function(message, category) {
|
|
||||||
let toast;
|
|
||||||
let toastActionElement;
|
|
||||||
|
|
||||||
switch (category) {
|
|
||||||
case "corpus":
|
|
||||||
message = `<i class="left material-icons">book</i>${message}`;
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
message = `<i class="left material-icons red-text">error</i>${message}`;
|
|
||||||
break;
|
|
||||||
case "job":
|
|
||||||
message = `<i class="left material-icons">work</i>${message}`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
message = `<i class="left material-icons">notifications</i>${message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast = M.toast({html: `<span>${message}</span>
|
|
||||||
<button data-action="close" class="btn-flat toast-action white-text">
|
|
||||||
<i class="material-icons">close</i>
|
|
||||||
</button>`});
|
|
||||||
toastActionElement = toast.el.querySelector('.toast-action[data-action="close"]');
|
|
||||||
toastActionElement.addEventListener('click', () => {toast.dismiss();});
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,33 @@
|
|||||||
class RessourceList {
|
class RessourceList {
|
||||||
constructor(idOrElement, options = {}) {
|
/* A wrapper class for the list.js list.
|
||||||
this.list = new List(idOrElement, {...RessourceList.options, ...options});
|
* This class is not meant to be used directly, instead it should be used as
|
||||||
|
* a template for concrete ressource list implementations.
|
||||||
|
*/
|
||||||
|
constructor(listElement, options = {}) {
|
||||||
|
if (listElement.dataset.userId) {
|
||||||
|
if (listElement.dataset.userId in nopaque.appClient.users) {
|
||||||
|
this.user = nopaque.appClient.users[listElement.dataset.userId];
|
||||||
|
} else {
|
||||||
|
console.error(`User not found: ${listElement.dataset.userId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.user = nopaque.appClient.users.self;
|
||||||
|
}
|
||||||
|
this.list = new List(listElement, {...RessourceList.options, ...options});
|
||||||
|
this.valueNames = ['id'];
|
||||||
|
for (let element of this.list.valueNames) {
|
||||||
|
switch (typeof element) {
|
||||||
|
case 'object':
|
||||||
|
if (element.hasOwnProperty('name')) {this.valueNames.push(element.name);}
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
this.valueNames.push(element);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Unknown value name definition: ${element}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(ressources) {
|
init(ressources) {
|
||||||
@ -9,38 +36,27 @@ class RessourceList {
|
|||||||
this.list.sort('id', {order: 'desc'});
|
this.list.sort('id', {order: 'desc'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patch(patch) {
|
||||||
update(patch) {
|
|
||||||
let item, pathArray;
|
|
||||||
|
|
||||||
for (let operation of patch) {
|
|
||||||
/*
|
/*
|
||||||
* '/{ressourceName}/{ressourceId}/{valueName}' -> ['{ressourceId}', {valueName}]
|
* It's not possible to generalize a patch Handler for all type of
|
||||||
* Example: '/jobs/1/status' -> ['1', 'status']
|
* ressources. So this method is meant to be an interface.
|
||||||
*/
|
*/
|
||||||
let [id, valueName] = operation.path.split("/").slice(2);
|
console.error('patch method not implemented!');
|
||||||
switch(operation.op) {
|
|
||||||
case 'add':
|
|
||||||
this.add(operation.value);
|
|
||||||
break;
|
|
||||||
case 'remove':
|
|
||||||
this.remove(id);
|
|
||||||
break;
|
|
||||||
case 'replace':
|
|
||||||
this.replace(id, valueName, operation.value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(values) {
|
add(values) {
|
||||||
/* WORKAROUND: Set a callback function ('() => {return;}') to force List.js
|
let ressources = Array.isArray(values) ? values : [values];
|
||||||
perform the add method asynchronous.
|
// Discard ressource values, that are not defined to be used in the list.
|
||||||
* https://listjs.com/api/#add
|
ressources = ressources.map(ressource => {
|
||||||
*/
|
let cleanedRessource = {};
|
||||||
this.list.add(values, () => {return;});
|
for (let [valueName, value] of Object.entries(ressource)) {
|
||||||
|
if (this.valueNames.includes(valueName)) {cleanedRessource[valueName] = value;}
|
||||||
|
}
|
||||||
|
return cleanedRessource;
|
||||||
|
});
|
||||||
|
// Set a callback function ('() => {return;}') to force List.js perform the
|
||||||
|
// add method asynchronous: https://listjs.com/api/#add
|
||||||
|
this.list.add(ressources, () => {return;});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id) {
|
remove(id) {
|
||||||
@ -48,35 +64,84 @@ class RessourceList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
replace(id, valueName, newValue) {
|
replace(id, valueName, newValue) {
|
||||||
if (!this.list.valuesNames.includes(valueName)) {return;}
|
if (this.valueNames.includes(valueName)) {
|
||||||
let item = this.list.get('id', id);
|
let item = this.list.get('id', id)[0];
|
||||||
item.values({[valueName]: newValue});
|
item.values({[valueName]: newValue});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]};
|
RessourceList.options = {page: 5, pagination: [{innerWindow: 4, outerWindow: 1}]};
|
||||||
|
|
||||||
|
|
||||||
class CorpusList extends RessourceList {
|
class CorpusList extends RessourceList {
|
||||||
constructor(listElementId, options = {}) {
|
constructor(listElement, options = {}) {
|
||||||
let listElement = document.querySelector(`#${listElementId}`);
|
|
||||||
super(listElement, {...CorpusList.options, ...options});
|
super(listElement, {...CorpusList.options, ...options});
|
||||||
listElement.addEventListener('click', (event) => {
|
this.user.addEventListener('corporaInit', corpora => this.init(corpora));
|
||||||
let actionButtonElement = event.target.closest('.action-button');
|
this.user.addEventListener('corporaPatch', patch => this.patch(patch));
|
||||||
if (actionButtonElement === null) {return;}
|
listElement.addEventListener('click', (event) => {this.onclick(event)});
|
||||||
|
}
|
||||||
|
|
||||||
|
onclick(event) {
|
||||||
let corpusId = event.target.closest('tr').dataset.id;
|
let corpusId = event.target.closest('tr').dataset.id;
|
||||||
let action = actionButtonElement.dataset.action;
|
let actionButtonElement = event.target.closest('.action-button');
|
||||||
|
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'analyse':
|
case 'analyse':
|
||||||
window.location.href = nopaque.user.corpora[corpusId].analysis_url;
|
window.location.href = nopaque.user.corpora[corpusId].analysis_url;
|
||||||
}
|
case 'delete':
|
||||||
});
|
let deleteModalHTML = `<div class="modal">
|
||||||
nopaque.corporaSubscribers.push(this);
|
<div class="modal-content">
|
||||||
|
<h4>Confirm corpus deletion</h4>
|
||||||
|
<p>Do you really want to delete the corpus <b>${nopaque.user.corpora[corpusId].title}</b>? All files will be permanently deleted!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||||
|
<a class="btn modal-close red waves-effect waves-light" href="${nopaque.user.corpora[corpusId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
let deleteModalParentElement = document.querySelector('main');
|
||||||
|
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||||
|
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||||
|
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
|
||||||
|
deleteModal.open();
|
||||||
|
break;
|
||||||
|
case 'view':
|
||||||
|
// TODO: handle unprepared corpora
|
||||||
|
window.location.href = nopaque.user.corpora[corpusId].url;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Unknown action: ${action}`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patch(patch) {
|
||||||
|
for (let operation of patch) {
|
||||||
|
switch(operation.op) {
|
||||||
|
case 'add':
|
||||||
|
// Matches the only paths that should be handled here: /corpora/{corpusId}
|
||||||
|
if (/^\/corpora\/(\d+)$/.test(operation.path)) {this.add(operation.value);}
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
// See case 'add' ;)
|
||||||
|
if (/^\/corpora\/(\d+)$/.test(operation.path)) {
|
||||||
|
let [match, id] = operation.path.match(/^\/corpora\/(\d+)$/);
|
||||||
|
this.remove(corpusId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'replace':
|
||||||
|
// Matches the only paths that should be handled here: /corpora/{corpusId}/{status || description || title}
|
||||||
|
if (/^\/corpora\/(\d+)\/(status|description|title)$/.test(operation.path)) {
|
||||||
|
let [match, id, valueName] = operation.path.match(/^\/corpora\/(\d+)\/(status|description|title)$/);
|
||||||
|
this.replace(id, valueName, operation.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CorpusList.options = {
|
CorpusList.options = {
|
||||||
item: `<tr>
|
item: `<tr>
|
||||||
<td><a class="btn-floating disabled"><i class="material-icons">book</i></a></td>
|
<td><a class="btn-floating disabled"><i class="material-icons">book</i></a></td>
|
||||||
@ -84,23 +149,80 @@ CorpusList.options = {
|
|||||||
<td><span class="badge new status" data-badge-caption=""></span></td>
|
<td><span class="badge new status" data-badge-caption=""></span></td>
|
||||||
<td class="right-align">
|
<td class="right-align">
|
||||||
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
|
||||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="edit" data-position="top" data-tooltip="Edit"><i class="material-icons">edit</i></a>
|
|
||||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="analyse" data-position="top" data-tooltip="Analyse"><i class="material-icons">search</i></a>
|
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="analyse" data-position="top" data-tooltip="Analyse"><i class="material-icons">search</i></a>
|
||||||
|
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>`,
|
</tr>`,
|
||||||
valueNames: [{data: ['id']}, {name: "status", attr: "data-status"}, 'description', 'title']
|
valueNames: [{data: ['id']}, {name: 'status', attr: 'data-status'}, 'description', 'title']
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class JobList extends RessourceList {
|
class JobList extends RessourceList {
|
||||||
constructor(listElementId, options = {}) {
|
constructor(listElement, options = {}) {
|
||||||
let listElement = document.querySelector(`#${listElementId}`);
|
|
||||||
super(listElement, {...JobList.options, ...options});
|
super(listElement, {...JobList.options, ...options});
|
||||||
nopaque.jobsSubscribers.push(this);
|
this.user.addEventListener('jobsInit', jobs => this.init(jobs));
|
||||||
|
this.user.addEventListener('jobsPatch', patch => this.patch(patch));
|
||||||
|
listElement.addEventListener('click', (event) => {this.onclick(event)});
|
||||||
|
}
|
||||||
|
|
||||||
|
onclick(event) {
|
||||||
|
let jobId = event.target.closest('tr').dataset.id;
|
||||||
|
let actionButtonElement = event.target.closest('.action-button');
|
||||||
|
let action = actionButtonElement === null ? 'view' : actionButtonElement.dataset.action;
|
||||||
|
switch (action) {
|
||||||
|
case 'delete':
|
||||||
|
let deleteModalHTML = `<div class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Confirm job deletion</h4>
|
||||||
|
<p>Do you really want to delete the job <b>${this.user.data.jobs[jobId].title}</b>? All files will be permanently deleted!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
|
||||||
|
<a class="btn modal-close red waves-effect waves-light" href="${this.user.data.jobs[jobId].url}/delete"><i class="material-icons left">delete</i>Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
let deleteModalParentElement = document.querySelector('main');
|
||||||
|
deleteModalParentElement.insertAdjacentHTML('beforeend', deleteModalHTML);
|
||||||
|
let deleteModalElement = deleteModalParentElement.lastChild;
|
||||||
|
let deleteModal = M.Modal.init(deleteModalElement, {onCloseEnd: () => {deleteModal.destroy(); deleteModalElement.remove();}});
|
||||||
|
deleteModal.open();
|
||||||
|
break;
|
||||||
|
case 'view':
|
||||||
|
window.location.href = this.user.data.jobs[jobId].url;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Unknown action: "${action}"`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patch(patch) {
|
||||||
|
for (let operation of patch) {
|
||||||
|
switch(operation.op) {
|
||||||
|
case 'add':
|
||||||
|
// Matches the only paths that should be handled here: /jobs/{jobId}
|
||||||
|
if (/^\/jobs\/(\d+)$/.test(operation.path)) {this.add(operation.value);}
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
// See case add ;)
|
||||||
|
if (/^\/jobs\/(\d+)$/.test(operation.path)) {
|
||||||
|
let [match, id] = operation.path.match(/^\/jobs\/(\d+)$/);
|
||||||
|
this.remove(jobId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'replace':
|
||||||
|
// Matches the only paths that should be handled here: /jobs/{jobId}/{service || status || description || title}
|
||||||
|
if (/^\/jobs\/(\d+)\/(service|status|description|title)$/.test(operation.path)) {
|
||||||
|
let [match, id, valueName] = operation.path.match(/^\/jobs\/(\d+)\/(service|status|description|title)$/);
|
||||||
|
this.replace(id, valueName, operation.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
JobList.options = {
|
JobList.options = {
|
||||||
item: `<tr>
|
item: `<tr>
|
||||||
<td><a class="btn-floating disabled"><i class="material-icons service"></i></a></td>
|
<td><a class="btn-floating disabled"><i class="material-icons service"></i></a></td>
|
||||||
@ -111,19 +233,17 @@ JobList.options = {
|
|||||||
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>`,
|
</tr>`,
|
||||||
valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: "status", attr: "data-status"}, 'description', 'title']
|
valueNames: [{data: ['id']}, {name: 'service', attr: 'data-service'}, {name: 'status', attr: 'data-status'}, 'description', 'title']
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class QueryResultList extends RessourceList {
|
class QueryResultList extends RessourceList {
|
||||||
constructor(listElementId, options = {}) {
|
constructor(listElement, options = {}) {
|
||||||
let listElement = document.querySelector(`#${listElementId}`);
|
|
||||||
super(listElement, {...QueryResultList.options, ...options});
|
super(listElement, {...QueryResultList.options, ...options});
|
||||||
nopaque.queryResultsSubscribers.push(this);
|
this.user.addEventListener('queryResultsInit', queryResults => this.init(queryResults));
|
||||||
|
this.user.addEventListener('queryResultsPatch', patch => this.init(patch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
QueryResultList.options = {
|
QueryResultList.options = {
|
||||||
item: `<tr>
|
item: `<tr>
|
||||||
<td><b class="title"></b><br><i class="description"></i><br></td>
|
<td><b class="title"></b><br><i class="description"></i><br></td>
|
||||||
@ -136,5 +256,3 @@ QueryResultList.options = {
|
|||||||
</tr>`,
|
</tr>`,
|
||||||
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
|
valueNames: [{data: ['id']}, 'corpus_title', 'description', 'query', 'title']
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CorpusList, JobList, QueryResultList };
|
|
||||||
|
@ -36,16 +36,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col s12 l6">
|
<div class="col s12 l6" id="corpora" data-user-id="{{ user.id }}">
|
||||||
<h3>Corpora</h3>
|
<h3>Corpora</h3>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content" id="corpora">
|
<div class="card-content">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<i class="material-icons prefix">search</i>
|
<i class="material-icons prefix">search</i>
|
||||||
<input id="search-corpus" class="search" type="search"></input>
|
<input id="search-corpus" class="search" type="search"></input>
|
||||||
<label for="search-corpus">Search corpus</label>
|
<label for="search-corpus">Search corpus</label>
|
||||||
</div>
|
</div>
|
||||||
<table class="highlight">
|
<table class="highlight ressource-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -64,16 +64,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col s12 l6">
|
<div class="col s12 l6" id="jobs" data-user-id="{{ user.id }}">
|
||||||
<h3>Jobs</h3>
|
<h3>Jobs</h3>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content" id="jobs">
|
<div class="card-content">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<i class="material-icons prefix">search</i>
|
<i class="material-icons prefix">search</i>
|
||||||
<input id="search-job" class="search" type="search"></input>
|
<input id="search-job" class="search" type="search"></input>
|
||||||
<label for="search-job">Search job</label>
|
<label for="search-job">Search job</label>
|
||||||
</div>
|
</div>
|
||||||
<table class="highlight">
|
<table class="highlight ressource-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="sort" data-sort="service">Service</span></th>
|
<th><span class="sort" data-sort="service">Service</span></th>
|
||||||
@ -109,10 +109,9 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script type="module">
|
<script>
|
||||||
import {RessourceList} from '{{ url_for('static', filename='js/nopaque.lists.js') }}';
|
nopaque.appClient.loadUser({{ user.id }});
|
||||||
let corpusList = new RessourceList("corpora", nopaque.foreignCorporaSubscribers, "Corpus");
|
let corpusList = new CorpusList(document.querySelector('#corpora'));
|
||||||
let jobList = new RessourceList("jobs", nopaque.foreignJobsSubscribers, "Job");
|
let jobList = new JobList(document.querySelector('#jobs'));
|
||||||
nopaque.socket.emit("foreign_user_data_stream_init", {{ user.id }});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<input id="search-corpus" class="search" type="search"></input>
|
<input id="search-corpus" class="search" type="search"></input>
|
||||||
<label for="search-corpus">Search corpus</label>
|
<label for="search-corpus">Search corpus</label>
|
||||||
</div>
|
</div>
|
||||||
<table class="highlight">
|
<table class="highlight ressource-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -102,7 +102,7 @@
|
|||||||
<input id="search-job" class="search" type="search"></input>
|
<input id="search-job" class="search" type="search"></input>
|
||||||
<label for="search-job">Search job</label>
|
<label for="search-job">Search job</label>
|
||||||
</div>
|
</div>
|
||||||
<table class="highlight">
|
<table class="highlight ressource-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="sort" data-sort="service">Service</span></th>
|
<th><span class="sort" data-sort="service">Service</span></th>
|
||||||
@ -175,10 +175,9 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script type="module">
|
<script>
|
||||||
import {CorpusList, JobList, QueryResultList} from '../../static/js/nopaque.lists.js';
|
let corpusList = new CorpusList(document.querySelector('#corpora'));
|
||||||
let corpusList = new CorpusList("corpora");
|
let jobList = new JobList(document.querySelector('#jobs'));
|
||||||
let jobList = new JobList("jobs");
|
let queryResultList = new QueryResultList(document.querySelector('#query-results'));
|
||||||
let queryResultList = new QueryResultList("query-results");
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
@ -244,28 +244,29 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
{% if current_user.setting_dark_mode %}
|
||||||
<script src="{{ url_for('static', filename='js/darkreader.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/darkreader.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
DarkReader.enable({brightness: 150, contrast: 100, sepia: 0});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/jsonpatch.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/list.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/socket.io.slim.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/socket.io.slim.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/nopaque.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/nopaque.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/nopaque.lists.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
{% if current_user.setting_dark_mode %}
|
|
||||||
DarkReader.enable({brightness: 150, contrast: 100, sepia: 0});
|
|
||||||
{% endif %}
|
|
||||||
// Disable all option elements with no value
|
// Disable all option elements with no value
|
||||||
for (let optionElement of document.querySelectorAll('option[value=""]')) {
|
for (let optionElement of document.querySelectorAll('option[value=""]')) {optionElement.disabled = true;}
|
||||||
optionElement.disabled = true;
|
|
||||||
}
|
|
||||||
M.AutoInit();
|
M.AutoInit();
|
||||||
M.CharacterCounter.init(document.querySelectorAll('input[data-length][type="email"], input[data-length][type="password"], input[data-length][type="text"], textarea[data-length]'));
|
M.CharacterCounter.init(document.querySelectorAll('input[data-length][type="email"], input[data-length][type="password"], input[data-length][type="text"], textarea[data-length]'));
|
||||||
M.Dropdown.init(document.querySelectorAll('#nav-more-dropdown-trigger'), {alignment: 'right', constrainWidth: false, coverTrigger: false});
|
M.Dropdown.init(document.querySelectorAll('#nav-more-dropdown-trigger'), {alignment: 'right', constrainWidth: false, coverTrigger: false});
|
||||||
nopaque.Forms.init();
|
nopaque.Forms.init();
|
||||||
|
for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {nopaque.flash(flashedMessage[1], flashedMessage[0]);}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
nopaque.socket.emit('user_data_stream_init');
|
nopaque.appClient = new AppClient({{ current_user.id }});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
for (let flashedMessage of {{ get_flashed_messages(with_categories=True)|tojson }}) {
|
|
||||||
nopaque.flash(flashedMessage[1], flashedMessage[0]);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
Loading…
Reference in New Issue
Block a user