2023-10-30 10:36:28 +00:00
|
|
|
nopaque.App = class App {
|
2021-11-30 15:22:16 +00:00
|
|
|
constructor() {
|
2022-07-08 09:46:47 +00:00
|
|
|
this.data = {
|
|
|
|
promises: {getUser: {}, subscribeUser: {}},
|
|
|
|
users: {},
|
|
|
|
};
|
2021-11-30 15:22:16 +00:00
|
|
|
this.socket = io({transports: ['websocket'], upgrade: false});
|
2022-09-02 11:07:30 +00:00
|
|
|
this.socket.on('PATCH', (patch) => {this.onPatch(patch);});
|
2021-11-30 15:22:16 +00:00
|
|
|
}
|
|
|
|
|
2023-07-24 08:02:35 +00:00
|
|
|
getUser(userId) {
|
2022-07-08 09:46:47 +00:00
|
|
|
if (userId in this.data.promises.getUser) {
|
|
|
|
return this.data.promises.getUser[userId];
|
|
|
|
}
|
|
|
|
|
|
|
|
this.data.promises.getUser[userId] = new Promise((resolve, reject) => {
|
2023-07-24 08:02:35 +00:00
|
|
|
this.socket.emit('GET /users/<user_id>', userId, (response) => {
|
|
|
|
if (response.status === 200) {
|
|
|
|
this.data.users[userId] = response.body;
|
|
|
|
resolve(this.data.users[userId]);
|
|
|
|
} else {
|
|
|
|
reject(`[${response.status}] ${response.statusText}`);
|
2022-12-22 12:57:55 +00:00
|
|
|
}
|
|
|
|
});
|
2022-07-08 09:46:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return this.data.promises.getUser[userId];
|
2021-12-01 13:15:20 +00:00
|
|
|
}
|
2021-11-30 15:22:16 +00:00
|
|
|
|
2022-07-04 12:09:17 +00:00
|
|
|
subscribeUser(userId) {
|
2022-07-08 09:46:47 +00:00
|
|
|
if (userId in this.data.promises.subscribeUser) {
|
|
|
|
return this.data.promises.subscribeUser[userId];
|
2021-12-01 13:15:20 +00:00
|
|
|
}
|
2022-07-08 09:46:47 +00:00
|
|
|
|
|
|
|
this.data.promises.subscribeUser[userId] = new Promise((resolve, reject) => {
|
|
|
|
this.socket.emit('SUBSCRIBE /users/<user_id>', userId, (response) => {
|
2022-12-22 12:57:55 +00:00
|
|
|
if (response.status !== 200) {
|
2022-07-04 12:09:17 +00:00
|
|
|
reject(response);
|
2022-12-22 12:57:55 +00:00
|
|
|
return;
|
2022-07-04 12:09:17 +00:00
|
|
|
}
|
2022-12-22 12:57:55 +00:00
|
|
|
resolve(response);
|
2022-07-04 12:09:17 +00:00
|
|
|
});
|
|
|
|
});
|
2022-07-08 09:46:47 +00:00
|
|
|
|
|
|
|
return this.data.promises.subscribeUser[userId];
|
2021-11-30 15:22:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
flash(message, category) {
|
2022-09-02 11:07:30 +00:00
|
|
|
let iconPrefix = '';
|
2021-11-30 15:22:16 +00:00
|
|
|
switch (category) {
|
2022-09-02 11:07:30 +00:00
|
|
|
case 'corpus': {
|
2021-12-01 13:15:20 +00:00
|
|
|
iconPrefix = '<i class="left material-icons">book</i>';
|
2021-11-30 15:22:16 +00:00
|
|
|
break;
|
2022-09-02 11:07:30 +00:00
|
|
|
}
|
|
|
|
case 'error': {
|
2021-12-01 13:15:20 +00:00
|
|
|
iconPrefix = '<i class="error-color-text left material-icons">error</i>';
|
2021-11-30 15:22:16 +00:00
|
|
|
break;
|
2022-09-02 11:07:30 +00:00
|
|
|
}
|
|
|
|
case 'job': {
|
2022-02-15 08:25:34 +00:00
|
|
|
iconPrefix = '<i class="left nopaque-icons">J</i>';
|
2021-11-30 15:22:16 +00:00
|
|
|
break;
|
2022-09-02 11:07:30 +00:00
|
|
|
}
|
2023-04-11 13:03:12 +00:00
|
|
|
case 'settings': {
|
|
|
|
iconPrefix = '<i class="left material-icons">settings</i>';
|
|
|
|
break;
|
|
|
|
}
|
2022-09-02 11:07:30 +00:00
|
|
|
default: {
|
2021-12-01 13:15:20 +00:00
|
|
|
iconPrefix = '<i class="left material-icons">notifications</i>';
|
|
|
|
break;
|
2022-09-02 11:07:30 +00:00
|
|
|
}
|
2021-11-30 15:22:16 +00:00
|
|
|
}
|
2022-09-02 11:07:30 +00:00
|
|
|
let toast = M.toast(
|
2021-12-01 13:15:20 +00:00
|
|
|
{
|
|
|
|
html: `
|
|
|
|
<span>${iconPrefix}${message}</span>
|
2022-09-02 11:07:30 +00:00
|
|
|
<button class="action-button btn-flat toast-action white-text" data-action="close">
|
2021-12-01 13:15:20 +00:00
|
|
|
<i class="material-icons">close</i>
|
|
|
|
</button>
|
|
|
|
`.trim()
|
|
|
|
}
|
|
|
|
);
|
2022-09-02 11:07:30 +00:00
|
|
|
let toastCloseActionElement = toast.el.querySelector('.action-button[data-action="close"]');
|
2021-11-30 15:22:16 +00:00
|
|
|
toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
|
|
|
|
}
|
2022-09-02 11:07:30 +00:00
|
|
|
|
|
|
|
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);
|
2023-03-31 07:25:13 +00:00
|
|
|
this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-status="${operation.value}"></span>`, 'job');
|
2022-09-02 11:07:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Apply Patch
|
|
|
|
jsonpatch.applyPatch(this.data, filteredPatch);
|
|
|
|
}
|
2023-10-24 13:09:20 +00:00
|
|
|
|
|
|
|
init() {
|
|
|
|
this.initUi();
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-10-30 10:36:28 +00:00
|
|
|
|
2023-10-24 13:09:20 +00:00
|
|
|
/* 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 "more" Dropdown.
|
|
|
|
M.Dropdown.init(
|
|
|
|
document.querySelector('#nav-more-dropdown-trigger'),
|
|
|
|
{
|
|
|
|
alignment: 'right',
|
|
|
|
constrainWidth: false,
|
|
|
|
coverTrigger: false
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// Manual modal
|
|
|
|
M.Modal.init(
|
|
|
|
document.querySelector('#manual-modal'),
|
|
|
|
{
|
|
|
|
onOpenStart: (modalElement, modalTriggerElement) => {
|
|
|
|
if ('manualModalChapter' in modalTriggerElement.dataset) {
|
|
|
|
let manualModalTocElement = document.querySelector('#manual-modal-toc');
|
|
|
|
let manualModalToc = M.Tabs.getInstance(manualModalTocElement);
|
|
|
|
manualModalToc.select(modalTriggerElement.dataset.manualModalChapter);
|
2023-12-07 11:46:37 +00:00
|
|
|
// TODO: Make this work.
|
|
|
|
// if ('manualModalChapterAnchor' in modalTriggerElement.dataset) {
|
|
|
|
// let manualModalChapterAnchor = document.querySelector(`#${modalTriggerElement.dataset.manualModalChapterAnchor}`);
|
|
|
|
// let xCoord = manualModalChapterAnchor.getBoundingClientRect().left;
|
|
|
|
// let yCoord = manualModalChapterAnchor.getBoundingClientRect().top;
|
|
|
|
// let modalContentElement = modalElement.querySelector('.modal-content');
|
|
|
|
// modalContentElement.scroll(xCoord, yCoord);
|
|
|
|
// }
|
2023-10-24 13:09:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// Terms of use modal
|
|
|
|
M.Modal.init(
|
|
|
|
document.querySelector('#terms-of-use-modal'),
|
|
|
|
{
|
|
|
|
dismissible: false,
|
|
|
|
onCloseEnd: (modalElement) => {
|
2023-11-09 14:51:00 +00:00
|
|
|
nopaque.requests.users.entity.acceptTermsOfUse();
|
2023-10-24 13:09:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
// #endregion
|
|
|
|
|
2023-10-30 10:36:28 +00:00
|
|
|
|
|
|
|
/* Initialize nopaque Components */
|
|
|
|
// #region
|
2023-11-09 13:29:01 +00:00
|
|
|
nopaque.resource_displays.AutoInit();
|
|
|
|
nopaque.resource_lists.AutoInit();
|
|
|
|
nopaque.forms.AutoInit();
|
2023-10-30 10:36:28 +00:00
|
|
|
// #endregion
|
2023-10-24 13:09:20 +00:00
|
|
|
}
|
2023-10-12 12:13:47 +00:00
|
|
|
};
|