mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2025-07-25 20:00:11 +02:00
Compare commits
2 commits
05d7dac34c
...
af43af93e4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
af43af93e4 | ||
![]() |
448b727956 |
9 changed files with 444 additions and 0 deletions
|
@ -506,6 +506,12 @@ async function expireDeletedUsersAfterDuration(req, res, next) {
|
||||||
res.sendStatus(204)
|
res.sendStatus(204)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listAllUsers(req, res, next) {
|
||||||
|
const users = await UserGetter.promises.getAllUsers()
|
||||||
|
|
||||||
|
res.json(users)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
clearSessions: expressify(clearSessions),
|
clearSessions: expressify(clearSessions),
|
||||||
changePassword: expressify(changePassword),
|
changePassword: expressify(changePassword),
|
||||||
|
@ -518,4 +524,5 @@ module.exports = {
|
||||||
expireDeletedUsersAfterDuration: expressify(expireDeletedUsersAfterDuration),
|
expireDeletedUsersAfterDuration: expressify(expireDeletedUsersAfterDuration),
|
||||||
ensureAffiliationMiddleware: expressify(ensureAffiliationMiddleware),
|
ensureAffiliationMiddleware: expressify(ensureAffiliationMiddleware),
|
||||||
ensureAffiliation,
|
ensureAffiliation,
|
||||||
|
listAllUsers: expressify(listAllUsers),
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,6 +150,44 @@ async function getWritefullData(userId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTotalProjectStorageForUser = async function (userId) {
|
||||||
|
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||||
|
const { Project } = require('../../models/Project')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
let totalsize = 0
|
||||||
|
// only owned projects, not shared
|
||||||
|
const ownedProjects = await Project.find(
|
||||||
|
{ owner_ref: userId },
|
||||||
|
"_id"
|
||||||
|
).exec()
|
||||||
|
|
||||||
|
for (let i = 0; i < ownedProjects.length; i++) {
|
||||||
|
const project = ownedProjects[i]
|
||||||
|
const files = await ProjectEntityHandler.promises.getAllFiles(project._id)
|
||||||
|
|
||||||
|
for (const [filePath, file] of Object.entries(files)) {
|
||||||
|
const f = path.join(settings.filestore.stores.user_files, project._id.toString() + '_' + file._id.toString())
|
||||||
|
|
||||||
|
const fstat = await fs.promises.stat(f)
|
||||||
|
const fsize = fstat.size
|
||||||
|
totalsize += fsize
|
||||||
|
}
|
||||||
|
} // foreach Project
|
||||||
|
return { count: ownedProjects.length, total: totalsize } // bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
let i = 0
|
||||||
|
while (bytes >= 1024 && i < units.length - 1) {
|
||||||
|
bytes /= 1024
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return `${bytes.toFixed(2)} ${units[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
const UserGetter = {
|
const UserGetter = {
|
||||||
getSsoUsersAtInstitution: callbackify(getSsoUsersAtInstitution),
|
getSsoUsersAtInstitution: callbackify(getSsoUsersAtInstitution),
|
||||||
|
|
||||||
|
@ -286,6 +324,43 @@ const UserGetter = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getWritefullData: callbackify(getWritefullData),
|
getWritefullData: callbackify(getWritefullData),
|
||||||
|
|
||||||
|
getAllUsers(callback) {
|
||||||
|
const projection = {
|
||||||
|
_id: 1,
|
||||||
|
email: 1,
|
||||||
|
first_name: 1,
|
||||||
|
last_name: 1,
|
||||||
|
lastLoggedIn: 1,
|
||||||
|
signUpDate: 1,
|
||||||
|
loginCount: 1,
|
||||||
|
isAdmin: 1,
|
||||||
|
suspended: 1,
|
||||||
|
institution: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = { $or: [{ 'emails.email': { $exists: true } },], }
|
||||||
|
|
||||||
|
db.users.find(query, {projection: projection}).toArray(async (err, users) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error fetching users:', err)
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
const user = users[i]
|
||||||
|
user.signUpDateformatted = moment(user.signUpDate).format('DD/MM/YYYY')
|
||||||
|
user.lastLoggedInformatted = moment(user.lastLoggedIn).format('DD/MM/YYYY')
|
||||||
|
const ProjectsInfo = await getTotalProjectStorageForUser(user._id)
|
||||||
|
|
||||||
|
user.projectsSize = ProjectsInfo.total
|
||||||
|
user.projectsSizeFormatted = formatBytes(ProjectsInfo.total)
|
||||||
|
user.projectsCount = ProjectsInfo.count
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, users)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const decorateFullEmails = (
|
const decorateFullEmails = (
|
||||||
|
|
|
@ -66,6 +66,7 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(
|
||||||
if canDisplayAdminMenu
|
if canDisplayAdminMenu
|
||||||
+dropdown-menu-link-item(href='/admin') Manage Site
|
+dropdown-menu-link-item(href='/admin') Manage Site
|
||||||
+dropdown-menu-link-item(href='/admin/user') Manage Users
|
+dropdown-menu-link-item(href='/admin/user') Manage Users
|
||||||
|
+dropdown-menu-link-item(href='/admin/users') #{translate('admin_panel')}
|
||||||
+dropdown-menu-link-item(href='/admin/project') Project URL Lookup
|
+dropdown-menu-link-item(href='/admin/project') Project URL Lookup
|
||||||
if canDisplayAdminRedirect
|
if canDisplayAdminRedirect
|
||||||
+dropdown-menu-link-item(href=settings.adminUrl) Switch to Admin
|
+dropdown-menu-link-item(href=settings.adminUrl) Switch to Admin
|
||||||
|
|
|
@ -70,6 +70,8 @@ nav.navbar.navbar-default.navbar-main(
|
||||||
a(href='/admin') Manage Site
|
a(href='/admin') Manage Site
|
||||||
li
|
li
|
||||||
a(href='/admin/user') Manage Users
|
a(href='/admin/user') Manage Users
|
||||||
|
li
|
||||||
|
a(href='/admin/users') #{translate('admin_panel')}
|
||||||
li
|
li
|
||||||
a(href='/admin/project') Project URL Lookup
|
a(href='/admin/project') Project URL Lookup
|
||||||
if canDisplayAdminRedirect
|
if canDisplayAdminRedirect
|
||||||
|
|
|
@ -65,6 +65,8 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
|
||||||
a(href='/admin') Manage Site
|
a(href='/admin') Manage Site
|
||||||
li
|
li
|
||||||
a(href='/admin/user') Manage Users
|
a(href='/admin/user') Manage Users
|
||||||
|
li
|
||||||
|
a(href='/admin/users') #{translate('admin_panel')}
|
||||||
li
|
li
|
||||||
a(href='/admin/project') Project URL Lookup
|
a(href='/admin/project') Project URL Lookup
|
||||||
if canDisplayAdminRedirect
|
if canDisplayAdminRedirect
|
||||||
|
|
|
@ -39,6 +39,9 @@ export default function AdminMenu({
|
||||||
<NavDropdownLinkItem href="/admin/user">
|
<NavDropdownLinkItem href="/admin/user">
|
||||||
Manage Users
|
Manage Users
|
||||||
</NavDropdownLinkItem>
|
</NavDropdownLinkItem>
|
||||||
|
<NavDropdownLinkItem href="/admin/users">
|
||||||
|
Admin Panel
|
||||||
|
</NavDropdownLinkItem>
|
||||||
<NavDropdownLinkItem href="/admin/project">
|
<NavDropdownLinkItem href="/admin/project">
|
||||||
Project URL lookup
|
Project URL lookup
|
||||||
</NavDropdownLinkItem>
|
</NavDropdownLinkItem>
|
||||||
|
|
|
@ -62,8 +62,88 @@ async function activateAccountPage(req, res, next) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
async function listAllUsers(req, res, next) {
|
||||||
|
const users = await UserGetter.promises.getAllUsers()
|
||||||
|
|
||||||
|
res.render(Path.resolve(__dirname, '../views/user/list'), {
|
||||||
|
title: 'Users list',
|
||||||
|
users,
|
||||||
|
currentUserId: req.user._id,
|
||||||
|
_csrf: req.csrfToken(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
import UserUpdater from '../../../../app/src/Features/User/UserUpdater.js'
|
||||||
|
/*
|
||||||
|
* it is a modified copy of /overleaf/services/web/scripts/suspend_users.mjs
|
||||||
|
* @param {request} req
|
||||||
|
* @param {response} res
|
||||||
|
*/
|
||||||
|
async function suspendUser(req, res) {
|
||||||
|
const userId = req.params.userId
|
||||||
|
|
||||||
|
try {
|
||||||
|
await UserUpdater.promises.suspendUser(userId, {
|
||||||
|
initiatorId: userId,
|
||||||
|
ip: req.ip,
|
||||||
|
info: { script: false },
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Failed to suspend ${userId}`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/admin/users')
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* it is a modified copy of UserUpdater.suspendUser
|
||||||
|
* @param {request} req
|
||||||
|
* @param {response} res
|
||||||
|
*/
|
||||||
|
async function unsuspendUser(req, res) {
|
||||||
|
const userId = req.params.userId
|
||||||
|
const upd = await UserUpdater.promises.updateUser(
|
||||||
|
{ _id: userId, suspended: { $ne: false } },
|
||||||
|
{ $set: { suspended: false } }
|
||||||
|
)
|
||||||
|
if (upd.matchedCount !== 1) {
|
||||||
|
console.log('user id not found or already unsuspended')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/admin/users')
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* it is a modified copy of UserUpdater.suspendUser
|
||||||
|
* It is used to update user first and last name
|
||||||
|
* @param {request} req.body.userId
|
||||||
|
* @param {request} req.body.first_name
|
||||||
|
* @param {request} req.body.last_name
|
||||||
|
* @param {response} res
|
||||||
|
*/
|
||||||
|
async function updateUser(req, res) {
|
||||||
|
const { userId, first_name, last_name } = req.body;
|
||||||
|
const upd = await UserUpdater.promises.updateUser(
|
||||||
|
{ _id: userId },
|
||||||
|
{ $set: {
|
||||||
|
first_name: first_name,
|
||||||
|
last_name: last_name,
|
||||||
|
} }
|
||||||
|
)
|
||||||
|
if (upd.matchedCount !== 1) {
|
||||||
|
console.log(`user id not found ${userId}`)
|
||||||
|
} else {
|
||||||
|
res.json({ success: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
registerNewUser,
|
registerNewUser,
|
||||||
register: expressify(register),
|
register: expressify(register),
|
||||||
activateAccountPage: expressify(activateAccountPage),
|
activateAccountPage: expressify(activateAccountPage),
|
||||||
|
listAllUsers: expressify(listAllUsers),
|
||||||
|
suspendUser: expressify(suspendUser),
|
||||||
|
unsuspendUser: expressify(unsuspendUser),
|
||||||
|
updateUser: expressify(updateUser),
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,5 +26,21 @@ export default {
|
||||||
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
UserActivateController.register
|
UserActivateController.register
|
||||||
)
|
)
|
||||||
|
webRouter.get('/admin/users',
|
||||||
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
|
UserActivateController.listAllUsers
|
||||||
|
)
|
||||||
|
webRouter.post('/admin/users/:userId/suspend',
|
||||||
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
|
UserActivateController.suspendUser
|
||||||
|
)
|
||||||
|
webRouter.post('/admin/users/:userId/unsuspend',
|
||||||
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
|
UserActivateController.unsuspendUser
|
||||||
|
)
|
||||||
|
webRouter.post('/admin/users/settings',
|
||||||
|
AuthorizationMiddleware.ensureUserIsSiteAdmin,
|
||||||
|
UserActivateController.updateUser
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
258
services/web/modules/user-activate/app/views/user/list.pug
Normal file
258
services/web/modules/user-activate/app/views/user/list.pug
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
extends ../../../../../app/views/layout-react
|
||||||
|
|
||||||
|
block append meta
|
||||||
|
meta(name="ol-usersBestSubscription" data-type="json" content=usersBestSubscription)
|
||||||
|
meta(name="ol-notifications" data-type="json" content=notifications)
|
||||||
|
meta(name="ol-notificationsInstitution" data-type="json" content=notificationsInstitution)
|
||||||
|
meta(name="ol-userEmails" data-type="json" content=userEmails)
|
||||||
|
meta(name="ol-allInReconfirmNotificationPeriods" data-type="json" content=allInReconfirmNotificationPeriods)
|
||||||
|
meta(name="ol-user" data-type="json" content=user)
|
||||||
|
meta(name="ol-userAffiliations" data-type="json" content=userAffiliations)
|
||||||
|
meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML)
|
||||||
|
meta(name="ol-survey" data-type="json" content=survey)
|
||||||
|
meta(name="ol-tags" data-type="json" content=tags)
|
||||||
|
meta(name="ol-portalTemplates" data-type="json" content=portalTemplates)
|
||||||
|
meta(name="ol-prefetchedProjectsBlob" data-type="json" content=prefetchedProjectsBlob)
|
||||||
|
if (suggestedLanguageSubdomainConfig)
|
||||||
|
meta(name="ol-suggestedLanguage" data-type="json" content=Object.assign(suggestedLanguageSubdomainConfig, {
|
||||||
|
lngName: translate(suggestedLanguageSubdomainConfig.lngCode),
|
||||||
|
imgUrl: buildImgPath("flags/24/" + suggestedLanguageSubdomainConfig.lngCode + ".png")
|
||||||
|
}))
|
||||||
|
meta(name="ol-currentUrl" data-type="string" content=currentUrl)
|
||||||
|
meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner)
|
||||||
|
meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
|
||||||
|
meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner)
|
||||||
|
meta(name="ol-showBrlGeoBanner" data-type="boolean" content=showBrlGeoBanner)
|
||||||
|
meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
|
||||||
|
meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
|
||||||
|
meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
|
||||||
|
meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
|
||||||
|
meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess)
|
||||||
|
meta(name="ol-showUSGovBanner" data-type="boolean" content=showUSGovBanner)
|
||||||
|
meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant)
|
||||||
|
|
||||||
|
block css
|
||||||
|
style.
|
||||||
|
.edit-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.user-name:hover .edit-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.edit-save-icon, .edit-cancel-icon {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.form-control:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); /* Bootstrap-style focus */
|
||||||
|
}
|
||||||
|
/* Ensure consistent width during editing */
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
/* Fixed width for name column */
|
||||||
|
table td:nth-child(3),
|
||||||
|
table th:nth-child(3) {
|
||||||
|
width: 200px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
block append meta
|
||||||
|
link(rel='stylesheet', href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css", id="main-stylesheet")
|
||||||
|
|
||||||
|
block content
|
||||||
|
.content.content-alt#main-content
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-sm-12
|
||||||
|
.card
|
||||||
|
.card-body
|
||||||
|
.page-header
|
||||||
|
h1 #{translate('admin_panel')}
|
||||||
|
if users.length
|
||||||
|
table.table.table-striped
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th #{translate('email')}
|
||||||
|
th #{translate('name')}
|
||||||
|
th #{translate('institution')}
|
||||||
|
th #{translate('sign_up')} #{translate('date')}
|
||||||
|
th #{translate('login_count')}
|
||||||
|
th #{translate('last_logged_in')}
|
||||||
|
th #{translate('projects_count')}
|
||||||
|
th #{translate('file_size')}
|
||||||
|
th #{translate('role')}
|
||||||
|
th #{translate('status')}
|
||||||
|
tbody
|
||||||
|
each user in users
|
||||||
|
tr
|
||||||
|
td #{user.email}
|
||||||
|
td
|
||||||
|
div.user-name(
|
||||||
|
data-user-id=user._id
|
||||||
|
data-original-name=(user.first_name || '') + ', ' + (user.last_name || '')
|
||||||
|
style="width: 200px; min-width: 200px;"
|
||||||
|
)
|
||||||
|
span.name-text #{user.first_name || ''}, #{user.last_name || ''}
|
||||||
|
i.bi.bi-pencil.edit-icon(
|
||||||
|
title=translate('edit'),
|
||||||
|
style="cursor: pointer; margin-left: 5px;"
|
||||||
|
data-user-id=user._id
|
||||||
|
)
|
||||||
|
td #{user.institution || ''}
|
||||||
|
td #{user.signUpDateformatted ? user.signUpDateformatted: ''}
|
||||||
|
td #{user.loginCount || ''}
|
||||||
|
td #{user.lastLoggedInformatted ? user.lastLoggedInformatted: ''}
|
||||||
|
td
|
||||||
|
span.me-2 #{user.projectsCount || 0}
|
||||||
|
td #{user.projectsSizeFormatted || 0}
|
||||||
|
td #{user.isAdmin ? translate('admin') : ''}
|
||||||
|
td
|
||||||
|
span.badge(class=(user.suspended ? 'bg-secondary' : 'bg-success'))
|
||||||
|
#{user.suspended ? translate('suspended') : translate('active')}
|
||||||
|
if user._id.toString() !== currentUserId
|
||||||
|
form.d-inline(method="POST", action=`/admin/users/${user._id}/${user.suspended ? 'unsuspend' : 'suspend'}`)
|
||||||
|
input(type="hidden", name="_csrf", value=csrfToken)
|
||||||
|
button.btn.btn-sm(
|
||||||
|
class=(user.suspended ? 'btn-danger' : 'btn-success'),
|
||||||
|
type="submit",
|
||||||
|
data-bs-toggle="tooltip",
|
||||||
|
title=(user.suspended ? translate('activate') : translate('suspend')),
|
||||||
|
) #{user.suspended ? translate('suspended') : translate('active')}
|
||||||
|
else
|
||||||
|
p There are no registered users.
|
||||||
|
|
||||||
|
|
||||||
|
block head-scripts
|
||||||
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
|
// Enable editing functionality for user names
|
||||||
|
function enableEdit(icon) {
|
||||||
|
// Prevent editing multiple rows
|
||||||
|
if (currentlyEditingRow != null) { return; }
|
||||||
|
|
||||||
|
const container = icon.parentElement;
|
||||||
|
const userId = icon.getAttribute("data-user-id");
|
||||||
|
const originalName = container.getAttribute("data-original-name");
|
||||||
|
[firstName, lastName] = originalName.split(",").map(str => str.trim());
|
||||||
|
if (lastName === undefined) { lastName = ''; }
|
||||||
|
|
||||||
|
// Save reference to current row being edited
|
||||||
|
currentlyEditingRow = container;
|
||||||
|
|
||||||
|
// Replace content with editable inputs and buttons
|
||||||
|
container.innerHTML = `
|
||||||
|
<input type="text" class="form-control form-control-sm d-inline-block w-auto me-1" value="${firstName}" data-first-name>
|
||||||
|
<input type="text" class="form-control form-control-sm d-inline-block w-auto me-1" value="${lastName}" data-last-name>
|
||||||
|
<i class="bi bi-save edit-save-icon" title="#{translate('save')}" style="cursor: pointer;" data-user-id="${userId}"></i>
|
||||||
|
<i class="bi bi-x-circle edit-cancel-icon" title="#{translate('cancel')}" style="cursor: pointer; margin-left: 5px;" data-user-id="${userId}"></i>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add Enter key listener to input fields
|
||||||
|
const firstInput = container.querySelector('[data-first-name]');
|
||||||
|
const lastInput = container.querySelector('[data-last-name]');
|
||||||
|
|
||||||
|
// Function to trigger save on Enter or discard on Escape
|
||||||
|
const handleKeys = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault(); // Prevent form submission
|
||||||
|
saveChanges(container, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault(); // Prevent form submission
|
||||||
|
const container = currentlyEditingRow;
|
||||||
|
const userId = container.getAttribute("data-user-id");
|
||||||
|
discardChanges(container.firstChild, userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach event listener to both input fields
|
||||||
|
firstInput.addEventListener('keydown', handleKeys);
|
||||||
|
lastInput.addEventListener('keydown', handleKeys);
|
||||||
|
|
||||||
|
firstInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
|
// Save changes made to the user name
|
||||||
|
function saveChanges(icon, userId) {
|
||||||
|
const container = icon.parentElement;
|
||||||
|
const firstName = container.querySelector('[data-first-name]').value;
|
||||||
|
const lastName = container.querySelector('[data-last-name]').value;
|
||||||
|
const csrfToken = document.querySelector('input[name="_csrf"]').value;
|
||||||
|
|
||||||
|
fetch('/admin/users/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: userId,
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
_csrf: csrfToken
|
||||||
|
}),
|
||||||
|
credentials: 'same-origin'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Error en la solicitud');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Restore original view
|
||||||
|
icon.innerHTML = `
|
||||||
|
<span class="name-text">${firstName}, ${lastName}</span>
|
||||||
|
<i class="bi bi-pencil edit-icon" style="cursor: pointer; margin-left: 5px;" data-user-id="${userId}"></i>
|
||||||
|
`;
|
||||||
|
icon.setAttribute("data-original-name", `${firstName}, ${lastName}`);
|
||||||
|
|
||||||
|
// Clear editing reference
|
||||||
|
currentlyEditingRow = null;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
|
function discardChanges(icon, userId) {
|
||||||
|
const container = icon.parentElement;
|
||||||
|
const originalName = container.getAttribute("data-original-name");
|
||||||
|
|
||||||
|
// Restore original view
|
||||||
|
container.innerHTML = `
|
||||||
|
<span class="name-text">${originalName}</span>
|
||||||
|
<i class="bi bi-pencil edit-icon" style="cursor: pointer; margin-left: 5px;" data-user-id="${userId}"></i>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Clear editing reference
|
||||||
|
currentlyEditingRow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
currentlyEditingRow = null;
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (event.target.classList.contains("edit-icon")) {
|
||||||
|
enableEdit(event.target);
|
||||||
|
} else if (event.target.classList.contains("edit-save-icon")) {
|
||||||
|
const userId = event.target.getAttribute("data-user-id");
|
||||||
|
saveChanges(event.target, userId);
|
||||||
|
} else if (event.target.classList.contains("edit-cancel-icon")) {
|
||||||
|
const userId = event.target.getAttribute("data-user-id");
|
||||||
|
discardChanges(event.target, userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
|
document.querySelectorAll(".edit-icon").forEach(icon => {
|
||||||
|
icon.addEventListener("click", () => enableEdit(icon));
|
||||||
|
})
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue