Update JS code structure

This commit is contained in:
Patrick Jentsch 2024-12-02 09:34:17 +01:00
parent a2904caea2
commit 12a3ac1d5d
21 changed files with 295 additions and 242 deletions

View File

@ -1,11 +1,51 @@
from flask import current_app, Flask
from flask_login import current_user from flask_login import current_user
from flask_socketio import join_room, leave_room from flask_socketio import join_room, leave_room
from app import hashids, socketio from app import db, hashids, socketio
from app.decorators import socketio_login_required from app.decorators import socketio_login_required
from app.models import User from app.models import User
@socketio.on('users.get_user') def _delete_user(app: Flask, user_id: int):
with app.app_context():
user = User.query.get(user_id)
user.delete()
db.session.commit()
@socketio.on('users.delete')
@socketio_login_required
def delete_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int):
return {'status': 400, 'statusText': 'Bad Request'}
user = User.query.get(user_id)
if user is None:
return {'status': 404, 'statusText': 'Not found'}
if not (
user == current_user
or current_user.is_administrator
):
return {'status': 403, 'statusText': 'Forbidden'}
socketio.start_background_task(
_delete_user,
current_app._get_current_object(),
user.id
)
return {
'body': f'User "{user.username}" marked for deletion',
'status': 202,
'statusText': 'Accepted'
}
@socketio.on('users.get')
@socketio_login_required @socketio_login_required
def get_user(user_hashid: str) -> dict: def get_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid) user_id = hashids.decode(user_hashid)
@ -34,7 +74,7 @@ def get_user(user_hashid: str) -> dict:
} }
@socketio.on('users.subscribe_user') @socketio.on('users.subscribe')
@socketio_login_required @socketio_login_required
def subscribe_user(user_hashid: str) -> dict: def subscribe_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid) user_id = hashids.decode(user_hashid)
@ -58,9 +98,9 @@ def subscribe_user(user_hashid: str) -> dict:
return {'status': 200, 'statusText': 'OK'} return {'status': 200, 'statusText': 'OK'}
@socketio.on('users.unsubscribe_user') @socketio.on('users.unsubscribe')
@socketio_login_required @socketio_login_required
def on_unsubscribe_user(user_hashid: str) -> dict: def unsubscribe_user(user_hashid: str) -> dict:
user_id = hashids.decode(user_hashid) user_id = hashids.decode(user_hashid)
if not isinstance(user_id, int): if not isinstance(user_id, int):

View File

@ -43,7 +43,7 @@ def resource_after_delete(mapper, connection, resource):
} }
] ]
room = f'/users/{resource.user_hashid}' room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, room=room) socketio.emit('users.patch', jsonpatch, room=room)
def cfa_after_delete(mapper, connection, cfa): def cfa_after_delete(mapper, connection, cfa):
@ -55,7 +55,7 @@ def cfa_after_delete(mapper, connection, cfa):
} }
] ]
room = f'/users/{cfa.corpus.user.hashid}' room = f'/users/{cfa.corpus.user.hashid}'
socketio.emit('patch_user', jsonpatch, room=room) socketio.emit('users.patch', jsonpatch, room=room)
def resource_after_insert(mapper, connection, resource): def resource_after_insert(mapper, connection, resource):
@ -70,7 +70,7 @@ def resource_after_insert(mapper, connection, resource):
} }
] ]
room = f'/users/{resource.user_hashid}' room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, room=room) socketio.emit('users.patch', jsonpatch, room=room)
def cfa_after_insert(mapper, connection, cfa): def cfa_after_insert(mapper, connection, cfa):
@ -84,7 +84,7 @@ def cfa_after_insert(mapper, connection, cfa):
} }
] ]
room = f'/users/{cfa.corpus.user.hashid}' room = f'/users/{cfa.corpus.user.hashid}'
socketio.emit('patch_user', jsonpatch, room=room) socketio.emit('users.patch', jsonpatch, room=room)
def resource_after_update(mapper, connection, resource): def resource_after_update(mapper, connection, resource):
@ -110,7 +110,7 @@ def resource_after_update(mapper, connection, resource):
) )
if jsonpatch: if jsonpatch:
room = f'/users/{resource.user_hashid}' room = f'/users/{resource.user_hashid}'
socketio.emit('patch_user', jsonpatch, room=room) socketio.emit('users.patch', jsonpatch, room=room)
def job_after_update(mapper, connection, job): def job_after_update(mapper, connection, job):

View File

@ -1,201 +1,33 @@
nopaque.App = class App { nopaque.App = class App {
#promises;
constructor() { constructor() {
this.data = { this.data = {};
users: {}
};
this.#promises = {
getUser: {},
subscribeUser: {}
};
this.socket = io({transports: ['websocket'], upgrade: false}); this.socket = io({transports: ['websocket'], upgrade: false});
this.socket.on('patch_user', (patch) => {this.onPatch(patch);}); this.ui = new nopaque.UIExtension(this);
this.users = new nopaque.UsersExtension(this);
} }
getUser(userId) { // onPatch(patch) {
if (userId in this.#promises.getUser) { // // Filter Patch to only include operations on users that are initialized
return this.#promises.getUser[userId]; // let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
} // let filteredPatch = patch.filter(operation => regExp.test(operation.path));
this.#promises.getUser[userId] = new Promise((resolve, reject) => { // // Handle job status updates
this.socket.emit('users.get_user', userId, (response) => { // let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
if (response.status === 200) { // let subFilteredPatch = filteredPatch
this.data.users[userId] = response.body; // .filter((operation) => {return operation.op === 'replace';})
resolve(this.data.users[userId]); // .filter((operation) => {return subRegExp.test(operation.path);});
} else { // for (let operation of subFilteredPatch) {
reject(`[${response.status}] ${response.statusText}`); // let [match, userId, jobId] = operation.path.match(subRegExp);
} // this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
}); // }
});
return this.#promises.getUser[userId]; // // Apply Patch
} // jsonpatch.applyPatch(this.data, filteredPatch);
// }
subscribeUser(userId) {
if (userId in this.#promises.subscribeUser) {
return this.#promises.subscribeUser[userId];
}
this.#promises.subscribeUser[userId] = new Promise((resolve, reject) => {
this.socket.emit('users.subscribe_user', userId, (response) => {
if (response.status === 200) {
resolve(response);
} else {
reject(response);
}
});
});
return this.#promises.subscribeUser[userId];
}
flash(message, category) {
let iconPrefix = '';
switch (category) {
case 'corpus': {
iconPrefix = '<i class="left material-icons">book</i>';
break;
}
case 'error': {
iconPrefix = '<i class="error-color-text left material-icons">error</i>';
break;
}
case 'job': {
iconPrefix = '<i class="left nopaque-icons">J</i>';
break;
}
case 'settings': {
iconPrefix = '<i class="left material-icons">settings</i>';
break;
}
default: {
iconPrefix = '<i class="left material-icons">notifications</i>';
break;
}
}
let toast = M.toast(
{
html: `
<span>${iconPrefix}${message}</span>
<button class="action-button btn-flat toast-action white-text" data-action="close">
<i class="material-icons">close</i>
</button>
`.trim()
}
);
let toastCloseActionElement = toast.el.querySelector('.action-button[data-action="close"]');
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
}
onPatch(patch) {
// Filter Patch to only include operations on users that are initialized
let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
let filteredPatch = patch.filter(operation => regExp.test(operation.path));
// Handle job status updates
let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
let subFilteredPatch = filteredPatch
.filter((operation) => {return operation.op === 'replace';})
.filter((operation) => {return subRegExp.test(operation.path);});
for (let operation of subFilteredPatch) {
let [match, userId, jobId] = operation.path.match(subRegExp);
this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
}
// Apply Patch
jsonpatch.applyPatch(this.data, filteredPatch);
}
init() { init() {
this.initUi(); this.ui.init();
}
initUi() {
/* Pre-Initialization fixes */
// #region
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
// elements to specify their maximum length (in characters). Unfortunatly
// Materialize won't recognize the maxlength Attribute, instead it uses
// the data-length Attribute. It's conversion time :)
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
elem.dataset.length = elem.getAttribute('maxlength');
elem.removeAttribute('maxlength');
}
// To work around some limitations with the Form setup of Flask-WTF.
// HTML option elements with an empty value are considered as placeholder
// elements. The user should not be able to actively select these options.
// So they get the disabled attribute.
for (let optionElement of document.querySelectorAll('option[value=""]')) {
optionElement.disabled = true;
}
// TODO: Check why we are doing this.
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
for (let c of optgroupElement.children) {
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
}
optgroupElement.remove();
}
// #endregion
/* Initialize Materialize Components */
// #region
// Automatically initialize Materialize Components that do not require
// additional configuration.
M.AutoInit();
// CharacterCounters
// Materialize didn't include the CharacterCounter plugin within the
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
// Header navigation processes and services Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
{
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Header navigation account Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-account-dropdown-trigger'),
{
alignment: 'right',
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Terms of use modal
M.Modal.init(
document.querySelector('#terms-of-use-modal'),
{
dismissible: false,
onCloseEnd: (modalElement) => {
nopaque.requests.users.entity.acceptTermsOfUse();
}
}
);
// #endregion
/* Initialize nopaque Components */
// #region
nopaque.resource_displays.AutoInit();
nopaque.resource_lists.AutoInit();
nopaque.forms.AutoInit();
// #endregion
} }
}; };

126
app/static/js/app.ui.js Normal file
View File

@ -0,0 +1,126 @@
nopaque.UIExtension = class UIExtension {
constructor(app) {
this.app = app;
}
init() {
/* Pre-Initialization fixes */
// #region
// Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
// elements to specify their maximum length (in characters). Unfortunatly
// Materialize won't recognize the maxlength Attribute, instead it uses
// the data-length Attribute. It's conversion time :)
for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
elem.dataset.length = elem.getAttribute('maxlength');
elem.removeAttribute('maxlength');
}
// To work around some limitations with the Form setup of Flask-WTF.
// HTML option elements with an empty value are considered as placeholder
// elements. The user should not be able to actively select these options.
// So they get the disabled attribute.
for (let optionElement of document.querySelectorAll('option[value=""]')) {
optionElement.disabled = true;
}
// TODO: Check why we are doing this.
for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
for (let c of optgroupElement.children) {
optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
}
optgroupElement.remove();
}
// #endregion
/* Initialize Materialize Components */
// #region
// Automatically initialize Materialize Components that do not require
// additional configuration.
M.AutoInit();
// CharacterCounters
// Materialize didn't include the CharacterCounter plugin within the
// AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));
// Header navigation processes and services Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-data-processing-and-analysis-dropdown-trigger'),
{
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Header navigation account Dropdown.
M.Dropdown.init(
document.querySelector('#navbar-account-dropdown-trigger'),
{
alignment: 'right',
constrainWidth: false,
container: document.querySelector('#dropdowns'),
coverTrigger: false
}
);
// Terms of use modal
M.Modal.init(
document.querySelector('#terms-of-use-modal'),
{
dismissible: false,
onCloseEnd: (modalElement) => {
nopaque.requests.users.entity.acceptTermsOfUse();
}
}
);
// #endregion
/* Initialize nopaque Components */
// #region
nopaque.resource_displays.AutoInit();
nopaque.resource_lists.AutoInit();
nopaque.forms.AutoInit();
// #endregion
}
flash(message, category) {
let iconPrefix;
switch (category) {
case 'corpus': {
iconPrefix = '<i class="material-icons left">book</i>';
break;
}
case 'job': {
iconPrefix = '<i class="nopaque-icons left">J</i>';
break;
}
case 'error': {
iconPrefix = '<i class="material-icons left error-color-text">error</i>';
break;
}
default: {
iconPrefix = '<i class="material-icons left">notifications</i>';
break;
}
}
let toast = M.toast(
{
html: `
<span>${iconPrefix}${message}</span>
<button class="btn-flat toast-action white-text" data-toast-action="dismiss">
<i class="material-icons">close</i>
</button>
`.trim()
}
);
let dismissToastElement = toast.el.querySelector('.toast-action[data-toast-action="dismiss"]');
dismissToastElement.addEventListener('click', () => {toast.dismiss();});
}
}

View File

@ -0,0 +1,53 @@
nopaque.UsersExtension = class UsersExtension {
#data;
#promises;
constructor(app) {
this.app = app;
this.#data = {};
this.app.data.users = this.#data;
this.#promises = {
get: {},
subscribe: {}
};
}
async #get(userId) {
const response = await this.app.socket.emitWithAck('users.get', userId);
if (response.status != 200) {
throw new Error(`[${response.status}] ${response.statusText}`);
}
this.#data[userId] = response.body;
return this.#data[userId];
}
get(userId) {
if (userId in this.#promises.get) {
return this.#promises.get[userId];
}
this.#promises.get[userId] = this.#get(userId);
return this.#promises.get[userId];
}
async #subscribe(userId) {
const response = await this.app.socket.emitWithAck('users.subscribe', userId);
if (response.status != 200) {
throw new Error(`[${response.status}] ${response.statusText}`);
}
}
subscribe(userId) {
if (userId in this.#promises.subscribe) {
return this.#promises.subscribe[userId];
}
this.#promises.subscribe[userId] = this.#subscribe(userId);
return this.#promises.subscribe[userId];
}
}

View File

@ -66,7 +66,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
errorString += `${error.constructor.name}`; errorString += `${error.constructor.name}`;
this.elements.error.innerText = errorString; this.elements.error.innerText = errorString;
this.elements.error.classList.remove('hide'); this.elements.error.classList.remove('hide');
app.flash(errorString, 'error'); app.ui.flash(errorString, 'error');
this.elements.progress.classList.add('hide'); this.elements.progress.classList.add('hide');
} }
this.app.enableActionElements(); this.app.enableActionElements();
@ -239,7 +239,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
if (subcorpus.selectedItems.size === 0) { if (subcorpus.selectedItems.size === 0) {
this.elements.progress.classList.add('hide'); this.elements.progress.classList.add('hide');
this.app.enableActionElements(); this.app.enableActionElements();
app.flash('No matches selected', 'error'); app.ui.flash('No matches selected', 'error');
return; return;
} }
promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50); promise = subcorpus.o.partialExport([...subcorpus.selectedItems], 50);
@ -298,7 +298,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus]; let subcorpus = this.data.subcorpora[this.settings.selectedSubcorpus];
subcorpus.o.drop().then( subcorpus.o.drop().then(
(cQiStatus) => { (cQiStatus) => {
app.flash(`${subcorpus.o.name} deleted`, 'corpus'); app.ui.flash(`${subcorpus.o.name} deleted`, 'corpus');
delete this.data.subcorpora[subcorpus.o.name]; delete this.data.subcorpora[subcorpus.o.name];
this.settings.selectedSubcorpus = undefined; this.settings.selectedSubcorpus = undefined;
for (let subcorpusName in this.data.subcorpora) { for (let subcorpusName in this.data.subcorpora) {
@ -320,7 +320,7 @@ nopaque.corpus_analysis.ConcordanceExtension = class ConcordanceExtension {
}, },
(cqiError) => { (cqiError) => {
let errorString = `${cqiError.code}: ${cqiError.constructor.name}`; let errorString = `${cqiError.code}: ${cqiError.constructor.name}`;
app.flash(errorString, 'error'); app.ui.flash(errorString, 'error');
} }
); );
}); });

View File

@ -46,7 +46,7 @@ nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
if ('description' in error) {errorString += `: ${error.description}`;} if ('description' in error) {errorString += `: ${error.description}`;}
this.elements.error.innerText = errorString; this.elements.error.innerText = errorString;
this.elements.error.classList.remove('hide'); this.elements.error.classList.remove('hide');
app.flash(errorString, 'error'); app.ui.flash(errorString, 'error');
this.elements.progress.classList.add('hide'); this.elements.progress.classList.add('hide');
} }
this.app.enableActionElements(); this.app.enableActionElements();

View File

@ -101,7 +101,7 @@ nopaque.forms.BaseForm = class BaseForm {
} }
} }
if (request.status === 500) { if (request.status === 500) {
app.flash('Internal Server Error', 'error'); app.ui.flash('Internal Server Error', 'error');
} }
modal.close(); modal.close();
}); });

View File

@ -25,16 +25,16 @@ nopaque.requests.JSONfetch = (input, init={}) => {
let message = json.message; let message = json.message;
let category = json.category || 'message'; let category = json.category || 'message';
if (message) { if (message) {
app.flash(message, category); app.ui.flash(message, category);
} }
}, },
(error) => { (error) => {
app.flash(`[${response.status}]: ${response.statusText}`, 'error'); app.ui.flash(`[${response.status}]: ${response.statusText}`, 'error');
} }
); );
}, },
(response) => { (response) => {
app.flash('Something went wrong', 'error'); app.ui.flash('Something went wrong', 'error');
reject(response); reject(response);
} }
); );

View File

@ -6,13 +6,13 @@ nopaque.resource_displays.ResourceDisplay = class ResourceDisplay {
this.userId = this.displayElement.dataset.userId; this.userId = this.displayElement.dataset.userId;
this.isInitialized = false; this.isInitialized = false;
if (this.userId) { if (this.userId) {
app.subscribeUser(this.userId) app.users.subscribe(this.userId)
.then((response) => { .then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId) app.users.get(this.userId)
.then((user) => { .then((user) => {
this.init(user); this.init(user);
this.isInitialized = true; this.isInitialized = true;

View File

@ -14,12 +14,12 @@ nopaque.resource_lists.CorpusFileList = class CorpusFileList extends nopaque.res
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false; this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false; this.hasPermissionManageFiles = listContainerElement.dataset?.hasPermissionManageFiles == 'true' || false;
if (this.userId === undefined || this.corpusId === undefined) {return;} if (this.userId === undefined || this.corpusId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files)); this.add(Object.values(user.corpora[this.corpusId].files || user.followed_corpora[this.corpusId].files));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

@ -12,12 +12,12 @@ nopaque.resource_lists.CorpusFollowerList = class CorpusFollowerList extends nop
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
this.corpusId = listContainerElement.dataset.corpusId; this.corpusId = listContainerElement.dataset.corpusId;
if (this.userId === undefined || this.corpusId === undefined) {return;} if (this.userId === undefined || this.corpusId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
// let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations); // let corpusFollowerAssociations = Object.values(user.corpora[this.corpusId].corpus_follower_associations);
// let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId); // let filteredList = corpusFollowerAssociations.filter(association => association.follower.id != currentUserId);
// this.add(filteredList); // this.add(filteredList);

View File

@ -11,12 +11,12 @@ nopaque.resource_lists.CorpusList = class CorpusList extends nopaque.resource_li
this.selectedItemIds = new Set(); this.selectedItemIds = new Set();
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;} if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(this.aggregateData(user)); this.add(this.aggregateData(user));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

@ -8,8 +8,8 @@ nopaque.resource_lists.JobInputList = class JobInputList extends nopaque.resourc
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
this.jobId = listContainerElement.dataset.jobId; this.jobId = listContainerElement.dataset.jobId;
if (this.userId === undefined || this.jobId === undefined) {return;} if (this.userId === undefined || this.jobId === undefined) {return;}
app.subscribeUser(this.userId); app.users.subscribe(this.userId);
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(Object.values(user.jobs[this.jobId].inputs)); this.add(Object.values(user.jobs[this.jobId].inputs));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

@ -12,12 +12,12 @@ nopaque.resource_lists.JobList = class JobList extends nopaque.resource_lists.Re
this.selectedItemIds = new Set(); this.selectedItemIds = new Set();
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;} if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(Object.values(user.jobs)); this.add(Object.values(user.jobs));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

@ -8,12 +8,12 @@ nopaque.resource_lists.JobResultList = class JobResultList extends nopaque.resou
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
this.jobId = listContainerElement.dataset.jobId; this.jobId = listContainerElement.dataset.jobId;
if (this.userId === undefined || this.jobId === undefined) {return;} if (this.userId === undefined || this.jobId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(Object.values(user.jobs[this.jobId].results)); this.add(Object.values(user.jobs[this.jobId].results));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

@ -8,12 +8,12 @@ nopaque.resource_lists.SpaCyNLPPipelineModelList = class SpaCyNLPPipelineModelLi
this.isInitialized = false; this.isInitialized = false;
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;} if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(Object.values(user.spacy_nlp_pipeline_models)); this.add(Object.values(user.spacy_nlp_pipeline_models));
this.isInitialized = true; this.isInitialized = true;
}); });

View File

@ -8,12 +8,12 @@ nopaque.resource_lists.TesseractOCRPipelineModelList = class TesseractOCRPipelin
this.isInitialized = false; this.isInitialized = false;
this.userId = listContainerElement.dataset.userId; this.userId = listContainerElement.dataset.userId;
if (this.userId === undefined) {return;} if (this.userId === undefined) {return;}
app.subscribeUser(this.userId).then((response) => { app.users.subscribe(this.userId).then((response) => {
app.socket.on('patch_user', (patch) => { app.socket.on('users.patch', (patch) => {
if (this.isInitialized) {this.onPatch(patch);} if (this.isInitialized) {this.onPatch(patch);}
}); });
}); });
app.getUser(this.userId).then((user) => { app.users.get(this.userId).then((user) => {
this.add(Object.values(user.tesseract_ocr_pipeline_models)); this.add(Object.values(user.tesseract_ocr_pipeline_models));
for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) { for (let uncheckedCheckbox of this.listjs.list.querySelectorAll('input[data-checked="True"]')) {
uncheckedCheckbox.setAttribute('checked', ''); uncheckedCheckbox.setAttribute('checked', '');

View File

@ -9,6 +9,8 @@
output='gen/nopaque.%(version)s.js', output='gen/nopaque.%(version)s.js',
'js/index.js', 'js/index.js',
'js/app.js', 'js/app.js',
'js/app.ui.js',
'js/app.users.js',
'js/utils.js', 'js/utils.js',
'js/forms/index.js', 'js/forms/index.js',
@ -82,11 +84,11 @@
const currentUserId = {{ current_user.hashid|tojson }}; const currentUserId = {{ current_user.hashid|tojson }};
// Subscribe to the current user's data events // Subscribe to the current user's data events
app.subscribeUser(currentUserId) app.users.subscribe(currentUserId)
.catch((error) => {throw JSON.stringify(error);}); .catch((error) => {throw JSON.stringify(error);});
// Get the current user's data // Get the current user's data
app.getUser(currentUserId, true, true) app.users.get(currentUserId, true, true)
.catch((error) => {throw JSON.stringify(error);}); .catch((error) => {throw JSON.stringify(error);});
{% if not current_user.terms_of_use_accepted -%} {% if not current_user.terms_of_use_accepted -%}
@ -96,7 +98,7 @@
// Display flashed messages // Display flashed messages
for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) { for (let [category, message] of {{ get_flashed_messages(with_categories=True)|tojson }}) {
app.flash(message, message); app.ui.flash(message, message);
} }
</script> </script>

View File

@ -364,8 +364,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => { shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value) navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then( .then(
() => {app.flash('Copied!');}, () => {app.ui.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');} () => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
); );
}); });

View File

@ -396,8 +396,8 @@ shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => { shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value) navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then( .then(
() => {app.flash('Copied!');}, () => {app.ui.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');} () => {app.ui.flash('Could not copy to clipboard. Please copy manually.', 'error');}
); );
}); });