diff --git a/src/controllers/api/admin/banController.js b/src/controllers/api/admin/banController.js
index 63fc1a2..a54a79d 100644
--- a/src/controllers/api/admin/banController.js
+++ b/src/controllers/api/admin/banController.js
@@ -36,6 +36,31 @@ module.exports.get = async function(req, res){
}
module.exports.post = async function(req, res){
+ try{
+ const validResult = validationResult(req);
+ if(validResult.isEmpty()){
+ const {user, permanent, expirationDays} = matchedData(req);
+ const userDB = await userModel.findOne({user});
+
+ if(userDB == null){
+ res.status(400);
+ return res.send({errors:[{type: "Bad Query", msg: "User not found.", date: new Date()}]});
+ }
+
+ await banModel.banByUserDoc(userDB, permanent, expirationDays);
+
+ res.status(200);
+ return res.send(await banModel.getBans());
+ }else{
+ res.status(400);
+ return res.send({errors: validResult.array()})
+ }
+ }catch(err){
+ return exceptionHandler(res, err);
+ }
+}
+
+module.exports.delete = async function(req, res){
try{
const validResult = validationResult(req);
if(validResult.isEmpty()){
@@ -44,11 +69,13 @@ module.exports.post = async function(req, res){
if(userDB == null){
res.status(400);
- res.send({errors:[{type: "Bad Query", msg: "User not found.", date: new Date()}]});
+ return res.send({errors:[{type: "Bad Query", msg: "User not found.", date: new Date()}]});
}
- await banModel.banByUserDoc(userDB);
+ await banModel.unbanByUserDoc(userDB);
+ res.status(200);
+ return res.send(await banModel.getBans());
}else{
res.status(400);
return res.send({errors: validResult.array()})
diff --git a/src/controllers/popup/placeholderController.js b/src/controllers/popup/placeholderController.js
new file mode 100644
index 0000000..4ed74b6
--- /dev/null
+++ b/src/controllers/popup/placeholderController.js
@@ -0,0 +1,20 @@
+/*Canopy - The next generation of stoner streaming software
+Copyright (C) 2024 Rainbownapkin and the TTN Community
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .*/
+
+//root index functions
+module.exports.get = async function(req, res){
+ res.render('partial/popup/placeholder', {});
+}
\ No newline at end of file
diff --git a/src/controllers/popup/userBanController.js b/src/controllers/popup/userBanController.js
new file mode 100644
index 0000000..911168d
--- /dev/null
+++ b/src/controllers/popup/userBanController.js
@@ -0,0 +1,20 @@
+/*Canopy - The next generation of stoner streaming software
+Copyright (C) 2024 Rainbownapkin and the TTN Community
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .*/
+
+//root index functions
+module.exports.get = async function(req, res){
+ res.render('partial/popup/userBan');
+}
\ No newline at end of file
diff --git a/src/routers/api/adminRouter.js b/src/routers/api/adminRouter.js
index aa96d7a..74a92a9 100644
--- a/src/routers/api/adminRouter.js
+++ b/src/routers/api/adminRouter.js
@@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .*/
//npm imports
-const { checkExact } = require('express-validator');
+const { body, checkExact} = require('express-validator');
const { Router } = require('express');
@@ -42,6 +42,8 @@ router.get('/permissions', permissionsController.get);
router.post('/permissions', checkExact([permissionsValidator.permissionsMap(), channelPermissionValidator.channelPermissionsMap()]), permissionsController.post);
router.post('/changeRank', accountValidator.user(), accountValidator.rank(), changeRankController.post);
router.get('/ban', banController.get);
-router.post('/ban', accountValidator.user(), banController.post);
+//Sometimes they're so simple you don't need to put your validators in their own special place :P
+router.post('/ban', accountValidator.user(), body("permanent").isBoolean(), body("expirationDays").isInt(), banController.post);
+router.delete('/ban', accountValidator.user(), banController.delete);
module.exports = router;
diff --git a/src/routers/popupRouter.js b/src/routers/popupRouter.js
new file mode 100644
index 0000000..5d92616
--- /dev/null
+++ b/src/routers/popupRouter.js
@@ -0,0 +1,32 @@
+/*Canopy - The next generation of stoner streaming software
+Copyright (C) 2024 Rainbownapkin and the TTN Community
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .*/
+
+//npm imports
+const { Router } = require('express');
+
+
+//local imports
+const placeholderController = require("../controllers/popup/placeholderController");
+const userBanController = require("../controllers/popup/userBanController");
+
+//globals
+const router = Router();
+
+//routing functions
+router.get('/placeholder', placeholderController.get);
+router.get('/userBan', userBanController.get);
+
+module.exports = router;
diff --git a/src/schemas/userBanSchema.js b/src/schemas/userBanSchema.js
index 0068f95..950dea1 100644
--- a/src/schemas/userBanSchema.js
+++ b/src/schemas/userBanSchema.js
@@ -48,7 +48,7 @@ const userBanSchema = new mongoose.Schema({
default: 30
},
//If true, then expiration date deletes associated accounts instead of deleting the ban record
- deleteAccountOnExpire: {
+ permanent: {
type: mongoose.SchemaTypes.Boolean,
required: true,
default: false
@@ -73,17 +73,58 @@ userBanSchema.statics.checkBan = async function(user){
return this.checkBanByUserDoc(userDB);
}
-userBanSchema.statics.banByUserDoc = async function(userDB){
+userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays){
+ //Prevent missing users
+ if(userDB == null){
+ throw new Error("User not found")
+ }
+
+ //Ensure the user isn't already banned
if(await this.checkBanByUserDoc(userDB) != null){
throw new Error("User already banned");
}
- return await this.create({user: userDB._id});
+ if(expirationDays < 0){
+ throw new Error("Expiration Days must be a positive integer!");
+ }else if(expirationDays < 30 && permanent){
+ throw new Error("Permanent bans must be given at least 30 days before automatic account deletion!");
+ }else if(expirationDays > 185){
+ throw new Error("Expiration/Deletion date cannot be longer than half a year out from the original ban date.");
+ }
+
+ //Log the user out
+ await userDB.killAllSessions();
+
+ //Add the ban to the database
+ return await this.create({user: userDB._id, permanent, expirationDays});
}
-userBanSchema.statics.ban = async function(user){
+userBanSchema.statics.ban = async function(user, permanent, expirationDays){
const userDB = await userModel.findOne({user: user.user});
- return this.banByUserDoc(userDB);
+ return this.banByUserDoc(userDB, permanent, expirationDays);
+}
+
+userBanSchema.statics.unbanByUserDoc = async function(userDB){
+
+ //Prevent missing users
+ if(userDB == null){
+ throw new Error("User not found")
+ }
+
+ const ban = await this.checkBanByUserDoc(userDB);
+
+ if(!ban){
+ throw new Error("User already un-banned");
+ }
+
+ //Use _id in-case mongoose wants to be a cunt
+ var oldBan = await this.deleteOne({_id: ban._id});
+ return oldBan;
+}
+
+userBanSchema.statics.unban = async function(user){
+ const userDB = await userModel.findOne({user: user.user});
+ return this.unbanByUserDoc(userDB);
}
userBanSchema.statics.getBans = async function(){
@@ -109,7 +150,7 @@ userBanSchema.statics.getBans = async function(){
user: userObj,
ips: ban.ips,
alts: ban.alts,
- deleteAccountOnExpire: ban.deleteAccountOnExpire
+ permanent: ban.permanent
}
bans.push(banObj);
diff --git a/src/schemas/userSchema.js b/src/schemas/userSchema.js
index c18c578..b820a9f 100644
--- a/src/schemas/userSchema.js
+++ b/src/schemas/userSchema.js
@@ -211,7 +211,7 @@ userSchema.statics.getUserList = async function(fullList = false){
//create a user object with limited properties (safe for public consumption)
var userObj = {
id: user.id,
- name: user.user,
+ user: user.user,
img: user.img,
date: user.date
}
@@ -270,7 +270,7 @@ userSchema.methods.nuke = async function(pass){
if(this.checkPass(pass)){
//Annoyingly there isnt a good way to do this from 'this'
- var oldUser = await module.exports.deleteOne(this);
+ var oldUser = await module.exports.userModel.deleteOne(this);
if(oldUser){
await this.killAllSessions();
diff --git a/src/server.js b/src/server.js
index 1bc641e..c7a10f2 100644
--- a/src/server.js
+++ b/src/server.js
@@ -34,6 +34,7 @@ const adminPanelRouter = require('./routers/adminPanelRouter');
const channelRouter = require('./routers/channelRouter');
const newChannelRouter = require('./routers/newChannelRouter');
const panelRouter = require('./routers/panelRouter');
+const popupRouter = require('./routers/popupRouter');
const apiRouter = require('./routers/apiRouter');
//Define Config
@@ -92,6 +93,8 @@ app.use('/c', channelRouter);
app.use('/newChannel', newChannelRouter);
//Panel
app.use('/panel', panelRouter);
+//Popup
+app.use('/popup', popupRouter);
//Bot-Ready
app.use('/api', apiRouter);
diff --git a/src/views/partial/adminPanel/userBanList.ejs b/src/views/partial/adminPanel/userBanList.ejs
index 4e9293c..423f60c 100644
--- a/src/views/partial/adminPanel/userBanList.ejs
+++ b/src/views/partial/adminPanel/userBanList.ejs
@@ -38,6 +38,9 @@ along with this program. If not, see .-->
Expiration Action
+
+
Actions
+
\ No newline at end of file
diff --git a/src/views/partial/adminPanel/userList.ejs b/src/views/partial/adminPanel/userList.ejs
index 7da532f..bd87c8b 100644
--- a/src/views/partial/adminPanel/userList.ejs
+++ b/src/views/partial/adminPanel/userList.ejs
@@ -29,33 +29,36 @@ along with this program. If not, see .-->
\ No newline at end of file
diff --git a/src/views/partial/popup/userBan.ejs b/src/views/partial/popup/userBan.ejs
new file mode 100644
index 0000000..c9cb60d
--- /dev/null
+++ b/src/views/partial/popup/userBan.ejs
@@ -0,0 +1,33 @@
+
+
+
Ban NULL
+
\ No newline at end of file
diff --git a/www/css/adminPanel.css b/www/css/adminPanel.css
index 39e58ed..db755dd 100644
--- a/www/css/adminPanel.css
+++ b/www/css/adminPanel.css
@@ -108,4 +108,9 @@ img.admin-list-entry-item{
#new-rank-select{
margin: auto;
height: 1.3em;
+}
+
+.admin-user-list-icon{
+ cursor: pointer;
+ margin: 0 0.2em;
}
\ No newline at end of file
diff --git a/www/css/global.css b/www/css/global.css
index ac818ff..86f8b18 100644
--- a/www/css/global.css
+++ b/www/css/global.css
@@ -48,4 +48,43 @@ p.navbar-item, input.navbar-item{
.navbar-item input{
padding: 0.2em;
+}
+
+.popup-backer{
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
+
+.popup-div{
+ position: fixed;
+ display: flex;
+ flex-direction: column;
+ margin: auto;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ height: fit-content;
+ width: fit-content;
+}
+
+.popup-close-icon{
+ text-align: right;
+ position:absolute;
+ right: 0;
+ margin: 0.5em;
+ margin-bottom: 0;
+ cursor: pointer;
+}
+
+.popup-content-div{
+ margin: 1em;
+ padding-top: 0.2em;
+}
+
+.popup-title{
+ margin-top: 0;
}
\ No newline at end of file
diff --git a/www/css/popup/userBan.css b/www/css/popup/userBan.css
new file mode 100644
index 0000000..b8ccd7f
--- /dev/null
+++ b/www/css/popup/userBan.css
@@ -0,0 +1,13 @@
+.ban-popup-content{
+ display: flex;
+ flex-direction: column;
+}
+
+#ban-popup-button-span{
+ display: flex;
+ flex-direction: row;
+}
+
+#ban-popup-button-span-spacer{
+ flex: 1;
+}
\ No newline at end of file
diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css
index 7515d40..f9e2ba8 100644
--- a/www/css/theme/movie-night.css
+++ b/www/css/theme/movie-night.css
@@ -281,4 +281,22 @@ select.panel-head-element{
#new-rank-input:focus{
border: none;
outline: none;
+}
+
+
+.popup-backer{
+ background-color: var(--bg0-alpha1);
+ backdrop-filter: var(--background-panel-effect-fast);
+}
+
+.popup-div{
+ background-color: var(--bg1);
+ color: var(--accent1);
+ box-shadow: 3px 3px 1px var(--bg1-alt0) inset;
+ border-radius: 1em;
+}
+
+#ban-popup-ban-button{
+ background-color: var(--accent0-warning);
+ color: var(--accent1);
}
\ No newline at end of file
diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js
index 131a029..7e75eda 100644
--- a/www/js/adminPanel.js
+++ b/www/js/adminPanel.js
@@ -19,6 +19,59 @@ class canopyAdminUtils{
}
+ //Statics
+ static banUserPopup = class{
+ constructor(target){
+ this.target = target;
+ this.popup = new canopyUXUtils.popup("userBan", true, this.asyncConstruction.bind(this));
+ }
+
+ asyncConstruction(){
+ this.title = document.querySelector(".popup-title");
+ //Setup title text real quick-like :P
+ this.title.innerHTML = `Ban ${this.target}`;
+
+ this.permBan = document.querySelector("#ban-popup-perm");
+ this.expiration = document.querySelector("#ban-popup-expiration");
+ this.expirationPrefix = document.querySelector("#ban-popup-expiration-prefix");
+ this.banButton = document.querySelector("#ban-popup-ban-button");
+ this.cancelButton = document.querySelector("#ban-popup-cancel-button");
+
+ this.setupInput();
+ }
+
+ setupInput(){
+ this.permBan.addEventListener("change", this.permaBanLabel.bind(this));
+ this.cancelButton.addEventListener("click", this.popup.closePopup.bind(this.popup));
+ this.banButton.addEventListener("click",this.ban.bind(this));
+ }
+
+ permaBanLabel(event){
+ if(event.target.checked){
+ this.expirationPrefix.innerHTML = "Account Deletion In: "
+ this.expiration.value = 30;
+ }else{
+ this.expirationPrefix.innerHTML = "Ban Expires In: "
+ this.expiration.value = 14;
+ }
+ }
+
+ async ban(event){
+ //Close out the popup
+ this.popup.closePopup();
+
+ //Submit the user ban based on input
+ const bans = await adminUtil.banUser(this.target, this.permBan.checked, this.expiration.value);
+
+ //For some reason comparing this against undefined or null wasnt working in and of itself...
+ if(typeof userBanList != "undefined" && bans != null){
+ //Why add an extra get request when we already have the data? :P
+ await userBanList.renderBanList(bans);
+ }
+ }
+ }
+
+ //Methods
async setUserRank(user, rank){
var response = await fetch(`/api/admin/changeRank`,{
method: "POST",
@@ -82,13 +135,30 @@ class canopyAdminUtils{
}
}
- async banUser(user){
+ async banUser(user, permanent, expirationDays){
var response = await fetch(`/api/admin/ban`,{
method: "POST",
headers: {
"Content-Type": "application/json"
},
//Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
+ body: JSON.stringify({user, permanent, expirationDays})
+ });
+
+ if(response.status == 200){
+ return await response.json();
+ }else{
+ utils.ux.displayResponseError(await response.json());
+ }
+ }
+
+ async unbanUser(user){
+ var response = await fetch(`/api/admin/ban`,{
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ //Unfortunately JSON doesn't natively handle ES6 maps, and god forbid someone update the standard in a way that's backwards compatible...
body: JSON.stringify({user})
});
@@ -103,6 +173,7 @@ class canopyAdminUtils{
class adminUserList{
constructor(){
this.rankSelectors = document.querySelectorAll(".admin-user-list-rank-select");
+ this.banIcons = document.querySelectorAll(".admin-user-list-ban-icon");
this.setupInput();
}
@@ -111,6 +182,10 @@ class adminUserList{
this.rankSelectors.forEach((rankSelector)=>{
rankSelector.addEventListener("change", this.setRank.bind(this))
});
+
+ this.banIcons.forEach((banIcon) => {
+ banIcon.addEventListener("click", this.banPopup.bind(this));
+ })
}
async setRank(event){
@@ -120,6 +195,12 @@ class adminUserList{
this.updateSelect(await adminUtil.setUserRank(user, rank), event.target);
}
+ banPopup(event){
+ const user = event.target.id.replace("admin-user-list-ban-icon-","");
+
+ new canopyAdminUtils.banUserPopup(user);
+ }
+
updateSelect(update, select){
if(update != null){
select.value = update.rank;
@@ -186,7 +267,22 @@ class adminUserBanList{
this.renderBanList(await adminUtil.getBans());
}
+ clearBanList(){
+ const oldRows = this.table.querySelectorAll('tr.admin-list-entry');
+ oldRows.forEach((row) => {
+ row.remove();
+ });
+ }
+
+ async unban(event){
+ //Get username from target id
+ const user = event.target.id.replace("admin-user-list-unban-icon-","");
+ //Send unban command to server and display the resulting banlist
+ this.renderBanList(await adminUtil.unbanUser(user));
+ }
+
renderBanList(banList){
+ this.clearBanList();
banList.forEach((ban) => {
//Create entry row
const entryRow = document.createElement('tr');
@@ -197,26 +293,40 @@ class adminUserBanList{
imgNode.classList.add("admin-list-entry","admin-list-entry-item");
imgNode.src = ban.user.img;
- console.log(new Date(ban.user.date).toDateString());
-
+ //Calculate expiration date and expiration days
const expirationDate = new Date(ban.expirationDate);
+ const expirationDays = ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1);
- const expirationDays = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
+ //Create unban icon
+ const unbanIcon = document.createElement('i');
+ unbanIcon.classList.add("bi-emoji-smile-fill","admin-user-list-icon","admin-user-list-unban-icon");
+ unbanIcon.id = `admin-user-list-unban-icon-${ban.user.user}`;
+ unbanIcon.title = `Unban ${ban.user.user}`;
+ unbanIcon.addEventListener("click", this.unban.bind(this));
+
+ //Create nuke account icon
+ const nukeAccount = document.createElement('i');
+ nukeAccount.classList.add("bi-radioactive","admin-user-list-icon","admin-user-list-unban-icon");
+ nukeAccount.id = `admin-user-list-unban-icon-${ban.user.user}`;
+ nukeAccount.title = `Nuke accounts`;
+ nukeAccount.addEventListener("click",console.log);
//Append cells to row
- entryRow.appendChild(newCell(imgNode, true, true));
+ entryRow.appendChild(newCell(imgNode, true));
entryRow.appendChild(newCell(ban.user.id));
entryRow.appendChild(newCell(ban.user.user));
entryRow.appendChild(newCell(new Date(ban.user.date).toDateString()));
entryRow.appendChild(newCell(new Date(ban.banDate).toDateString()));
entryRow.appendChild(newCell(`${expirationDate.toDateString()} (${expirationDays} days left)`));
- entryRow.appendChild(newCell(ban.deleteAccountOnExpire ? "Delete" : "Un-Ban"));
+ entryRow.appendChild(newCell(ban.permanent ? "Account Deletion" : "Un-Ban"));
+ entryRow.appendChild(newCell([unbanIcon, nukeAccount]));
//Append row to table
this.table.appendChild(entryRow);
});
- function newCell(content, addAsNode = false, firstCol = false){
+ //We should really move this over to uxutils along with newrow & newtable functions
+ function newCell(content, firstCol = false){
//Create a new 'td' element
const cell = document.createElement('td');
cell.classList.add("admin-list-entry","admin-list-entry-item");
@@ -226,17 +336,33 @@ class adminUserBanList{
cell.classList.add("admin-list-entry-not-first-col");
}
- //If we're adding as node
- if(addAsNode){
- //append it like it's a node
- cell.appendChild(content);
+ //check for arrays
+ if(content.forEach == null){
+ //add single items
+ addContent(content);
}else{
- //otherwise use it as innerHTML
- cell.innerHTML = content;
+ //Crawl through content array
+ content.forEach((item)=>{
+ //add each item
+ addContent(item);
+ });
}
//return the resulting cell
return cell;
+
+
+ function addContent(ct){
+ //If we're adding as node
+ if(ct.cloneNode != null){
+ //append it like it's a node
+ cell.appendChild(ct);
+ }else{
+ //otherwise use it as innerHTML
+ cell.innerHTML = ct;
+ }
+ }
+
}
}
}
diff --git a/www/js/utils.js b/www/js/utils.js
index 412b4ad..41509a2 100644
--- a/www/js/utils.js
+++ b/www/js/utils.js
@@ -25,13 +25,68 @@ class canopyUXUtils{
constructor(){
}
- async displayResponseError(body){
+ displayResponseError(body){
const errors = body.errors;
errors.forEach((err)=>{
window.alert(`ERROR: ${err.msg} \nTYPE: ${err.type} \nDATE: ${err.date}`);
});
}
+ static popup = class{
+ constructor(content, ajaxPopup = false, cb){
+ //Define non-popup node values
+ this.content = content;
+ this.ajaxPopup = ajaxPopup;
+ this.cb = cb;
+ //define popup nodes
+ this.createPopup();
+
+ //fill popup nodes
+ this.fillPopupContent();
+ }
+
+ createPopup(){
+ this.popupBacker = document.createElement('div');
+ this.popupBacker.classList.add('popup-backer');
+
+ this.popupDiv = document.createElement('div');
+ this.popupDiv.classList.add('popup-div');
+
+ this.closeIcon = document.createElement('i');
+ this.closeIcon.classList.add('bi-x','popup-close-icon');
+ this.closeIcon.addEventListener("click", this.closePopup.bind(this));
+
+ this.contentDiv = document.createElement('div');
+ this.contentDiv.classList.add('popup-content-div');
+
+ this.popupDiv.appendChild(this.closeIcon);
+ this.popupDiv.appendChild(this.contentDiv);
+ }
+
+ async fillPopupContent(){
+ if(this.ajaxPopup){
+ this.contentDiv.innerHTML = await utils.ajax.getPopup(this.content);
+ }else{
+ this.contentDiv.innerHTML = this.content;
+ }
+
+ //display popup nodes
+ this.displayPopup();
+
+ //Callbacks are kinda out of vogue, but there really isn't a good way to handle asynchronously constructed objects/classes
+ this.cb();
+ }
+
+ displayPopup(){
+ document.body.prepend(this.popupDiv);
+ document.body.prepend(this.popupBacker);
+ }
+
+ closePopup(){
+ this.popupDiv.remove();
+ this.popupBacker.remove();
+ }
+ }
static clickDragger = class{
constructor(handle, element, leftHandle = true){
@@ -309,6 +364,18 @@ class canopyAjaxUtils{
utils.ux.displayResponseError(await response.json());
}
}
+
+ async getPopup(popup){
+ var response = await fetch(`/popup/${popup}`,{
+ method: "GET"
+ });
+
+ if(response.status == 200){
+ return (await response.text())
+ }else{
+ utils.ux.displayResponseError(await response.json());
+ }
+ }
}
const utils = new canopyUtils()
\ No newline at end of file