@digital-blueprint/lunchlottery-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/lunchlottery-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/lunchlottery-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/lunchlottery-app/)
1,398 lines • 56 kB
JavaScript
import {css, html} from 'lit';
import {createInstance} from './i18n.js';
import {ref, createRef} from 'lit/directives/ref.js';
import * as commonStyles from '@dbp-toolkit/common/styles';
import {getGrantPermissionDialogCSS} from './styles.js';
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import {
ScopedElementsMixin,
Modal,
Button,
Icon,
IconButton,
LangMixin,
sendNotification,
} from '@dbp-toolkit/common';
import {Notification} from '@dbp-toolkit/notification';
import {PersonSelect} from '@dbp-toolkit/person-select';
import {getDeletionConfirmation, handleDeletionConfirm, handleDeletionCancel} from './utils.js';
import {classMap} from 'lit/directives/class-map.js';
import {repeat} from 'lit/directives/repeat.js';
export class GrantPermissionDialog extends LangMixin(
ScopedElementsMixin(DBPLitElement),
createInstance,
) {
constructor() {
super();
this.auth = {};
this.entryPointUrl = '';
this.modalTitle = '';
this.availableActions = [];
this.userList = new Map();
this.permissionRows = [];
this.usersToAdd = new Map();
this.resourceIdentifier = '';
this.resourceClassIdentifier = '';
/** @type {import('lit/directives/ref.js').Ref<Button>} */
this.addPersonButtonRef = createRef();
/** @type {import('lit/directives/ref.js').Ref<Button>} */
this.savePermissionButtonRef = createRef();
/** @type {import('lit/directives/ref.js').Ref<Modal>} */
this.permissionModalRef = createRef();
this.lastManageCheckbox = null;
this.lastSavedManagerId = null;
}
static get properties() {
return {
...super.properties,
auth: {type: Object},
modalTitle: {type: String, attribute: 'modal-title'},
availableActions: {type: Array},
userList: {type: Map},
usersToAdd: {type: Map},
hasUsersToAdd: {type: Boolean},
permissionRows: {type: Array},
resourceIdentifier: {type: String, attribute: 'resource-identifier'},
resourceClassIdentifier: {type: String, attribute: 'resource-class-identifier'},
entryPointUrl: {type: String, attribute: 'entry-point-url'},
savePermissionButtonIsDisabled: {type: Boolean, attribute: false},
lastManageCheckbox: {type: Object, attribute: false},
lastSavedManagerId: {type: String, attribute: false},
};
}
static get scopedElements() {
return {
'dbp-icon': Icon,
'dbp-button': Button,
'dbp-icon-button': IconButton,
'dbp-person-select': PersonSelect,
'dbp-modal': Modal,
'dbp-notification': Notification,
};
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('dbp-modal-closed', this.modalClosedHandler.bind(this));
}
modalClosedHandler(event) {
this.closeModal(event);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('dbp-modal-closed', this.modalClosedHandler);
}
updated(changedProperties) {
super.updated(changedProperties);
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'lang': {
// Set default title if none provided via attribute
if (!this.modalTitle || !this.hasAttribute('modal-title')) {
this.modalTitle = this._i18n.t('grant-permission-dialog.modal-title');
}
break;
}
case 'userList': {
console.log(`this.userList`, this.userList);
this.checkSavedManagerCount();
break;
}
case 'usersToAdd': {
console.log(`this.usersToAdd`, this.usersToAdd);
break;
}
}
});
}
/**
* Set lastSavedManagerId to prevent deleting the last manager
*/
checkSavedManagerCount() {
let singleManagerId = null;
let multipleManagers = false;
for (const [userId, user] of this.userList) {
const hasManageGrant = user?.permissions?.get('manage')?.identifier;
if (!hasManageGrant) continue;
if (singleManagerId === null) {
singleManagerId = userId;
} else {
multipleManagers = true;
break;
}
}
this.lastSavedManagerId = multipleManagers ? null : singleManagerId;
}
/**
* Disable the last checked "manage" checkbox to prevent unchecking it
*/
disableLastManageCheckbox() {
const manageCheckboxes = this._a('.permission-checkbox[name="manage"]');
const checkedManageCheckboxes = [...manageCheckboxes].filter((checkbox) => {
return checkbox.checked === true;
});
if (checkedManageCheckboxes.length === 1) {
this.lastManageCheckbox = checkedManageCheckboxes[0];
this.lastManageCheckbox.disabled = true;
} else {
// Enable the previously disabled checkbox if it exists and is in edit-mode
if (
this.lastManageCheckbox &&
this.lastManageCheckbox instanceof HTMLInputElement &&
this.lastManageCheckbox.classList.contains('edit-mode')
) {
this.lastManageCheckbox.disabled = false;
}
// Always clear the reference when there are multiple (or zero) checked
this.lastManageCheckbox = null;
}
}
/**
* Returns if a person is set in or not
* @returns {boolean} true or false
*/
isLoggedIn() {
return this.auth && this.auth.token;
}
/**
* Send a fetch to given url with given options
* @param url
* @param options
* @returns {Promise<object>} response (error or result)
*/
async httpGetAsync(url, options) {
let response = await fetch(url, options)
.then((result) => {
if (!result.ok) throw result;
return result;
})
.catch((error) => {
return error;
});
return response;
}
/**
* Gets the actions for our resource class
* @returns {Promise<object>} response
*/
async apiGetAvailableActions() {
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
};
return await this.httpGetAsync(
this.entryPointUrl +
`/authorization/available-resource-class-actions/${this.resourceClassIdentifier}?perPage=9999`,
options,
);
}
async setAvailableActions() {
this.availableActions = [];
let showErrorNotification = false;
const i18n = this._i18n;
try {
let responseBody = null;
const response = await this.apiGetAvailableActions();
if (response.status !== 200 || (responseBody = await response.json()) === undefined) {
showErrorNotification = true;
} else {
this.availableActions = Object.keys(responseBody.itemActions).map((actionKey) => {
return {
[actionKey]: {
[this.lang]:
responseBody.itemActions[actionKey][this.lang] ?? actionKey,
},
};
});
}
} catch {
showErrorNotification = true;
}
if (showErrorNotification) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t(
'grant-permission-dialog.notifications.set-available-actions-error-text',
),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 5,
});
}
}
/**
* Gets the list of Resource Action Grants
* @returns {Promise<object>} response
*/
async apiGetResourceActionGrants() {
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
};
return await this.httpGetAsync(
this.entryPointUrl +
`/authorization/resource-action-grants?resourceClass=${this.resourceClassIdentifier}&resourceIdentifier=${this.resourceIdentifier}&page=1&perPage=9999`,
options,
);
}
async apiGetUserDetails(userIdentifier) {
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
};
return await this.httpGetAsync(
this.entryPointUrl + `/base/people/${userIdentifier}`,
options,
);
}
/**
* Delete user's Resource Action Grant
* @param {string} grantIdentifier - Authorization Resource Action Grant identifier
* @returns {Promise<object>} response
*/
async apiDeleteResourceActionGrant(grantIdentifier) {
if (!grantIdentifier) {
return false;
}
const options = {
method: 'DELETE',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
};
return await this.httpGetAsync(
this.entryPointUrl + `/authorization/resource-action-grants/${grantIdentifier}`,
options,
);
}
/**
* Save user's Resource Action Grant
* @param {string} action
* @param {string} userIdentifier
* @returns {Promise<object>} response
*/
async apiPostResourceActionGrant(action, userIdentifier) {
/* {
"resourceIdentifier": "184c7f86-73b4-4ee7-9f93-09af7d7ff4fd",
"resourceClass": "DbpRelayFormalizeForm",
"action": "manage",
"userIdentifier": "811EC3ACC0ADCA70"
} */
const body = {
resourceIdentifier: this.resourceIdentifier,
resourceClass: this.resourceClassIdentifier,
action: action,
userIdentifier: userIdentifier,
};
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.auth.token,
},
body: JSON.stringify(body),
};
return await this.httpGetAsync(
this.entryPointUrl + `/authorization/resource-action-grants`,
options,
);
}
async deleteUsersAllGrants(userId) {
const i18n = this._i18n;
const userToDelete = this.userList.get(userId);
const grantsToDelete = [];
// Collect grants to delete
userToDelete.permissions.forEach((grant) => {
if (grant.identifier) {
grantsToDelete.push(grant);
}
});
try {
for (const grant of grantsToDelete) {
const deleteResponse = await this.apiDeleteResourceActionGrant(grant.identifier);
if (deleteResponse.status !== 204) {
throw new Error('Failed to delete grant');
}
}
// @TODO handle multiple errors
} catch (e) {
console.log('Error deleting grant', e);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
// Add username and grant name here
body: i18n.t('grant-permission-dialog.notifications.could-not-delete-grant'),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
}
}
async deleteUser(userId) {
const i18n = this._i18n;
try {
await this.deleteUsersAllGrants(userId);
// Set delete row animation
const rowToAnimate = this._(`.user-row[data-user-id="${userId}"]`);
rowToAnimate.classList.add('delete-animation');
// Wait for the animation to complete
// Create a promise that resolves when the animation ends
const animationComplete = new Promise((resolve) => {
rowToAnimate.addEventListener('transitionend', resolve, {once: true});
});
// Wait for animation to complete before updating the Map
await animationComplete;
this.removeUserFromQueue(userId);
const deleted = this.removeUserFromList(userId);
if (deleted) {
rowToAnimate.classList.remove('delete-animation');
this.requestUpdate();
}
} catch (e) {
console.log('delete user error', e);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.could-not-delete-user'),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
}
}
async getUserFullName(userIdentifier) {
const i18n = this._i18n;
try {
const response = await this.apiGetUserDetails(userIdentifier);
let userDetailsResponse = await response.json();
if (userDetailsResponse !== undefined && userDetailsResponse.status !== 403) {
return `${userDetailsResponse['givenName']} ${userDetailsResponse['familyName']}`;
} else {
if (userDetailsResponse.status === 500) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t(
'grant-permission-dialog.notifications.could-not-fetch-user-details',
),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
} else if (userDetailsResponse.status === 403) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.error-not-authorized'),
targetNotificationId: 'permission-modal-notification',
type: 'danger',
timeout: 0,
});
}
}
} catch (error) {
console.log('getUserFullName failed', error);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.failed-get-user-details'),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
}
}
/**
* Retrieves the full name of a user by their user ID
* @param {string} userId - The unique identifier of the user
* @returns {string|undefined} The user's full name or undefined if not found
*/
lookupUserFullName(userId) {
if (!userId) return undefined;
return this.userList.get(userId)?.userFullName;
}
async handleUserEditButton(userId) {
this.setButtonState(userId, 'save');
this.addUserToQueue(userId);
this.enableUsersAllCheckboxes(userId);
}
async handleUserSaveButton(userId) {
try {
await this.saveUserPermissions(userId);
} catch (error) {
console.log(error);
} finally {
this.setButtonState(userId, 'edit');
this.checkSavedManagerCount();
// Remove edit styles & disable checkboxes
this._a(`[data-user-id="${userId}"]`).forEach((checkbox) => {
const checkboxElem = /** @type {HTMLInputElement} */ (checkbox);
checkboxElem.classList.remove('changed');
checkboxElem.removeAttribute('data-changed');
});
this.disableUsersAllCheckboxes(userId);
}
}
async handleUserDeleteButton(userId) {
const i18n = this._i18n;
const userFullName = this.lookupUserFullName(userId);
await this.deleteUser(userId);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.success-title'),
body: i18n.t('grant-permission-dialog.notifications.user-successfully-deleted', {
userFullName: userFullName,
}),
type: 'info',
targetNotificationId: 'permission-modal-notification',
timeout: 5,
});
}
/**
* Get a list of users and granted permissions
* @returns {Promise<void>}
*/
async setListOfUsersAndPermissions() {
const i18n = this._i18n;
try {
let response = await this.apiGetResourceActionGrants();
let responseBody = await response.json();
if (
responseBody !== undefined &&
response.status !== 403 &&
responseBody['hydra:member'].length > 0
) {
// Loop trough all grants
const newUserList = new Map(this.userList);
for (const grant of responseBody['hydra:member']) {
if (grant.userIdentifier) {
// Don't add inherited item-actions that are not available actions
// We can't set such permissions
const isAnAvailableAction = this.availableActions.find((action) => {
return Object.keys(action)[0] === grant.action;
});
if (!isAnAvailableAction) {
continue;
}
const userId = grant.userIdentifier;
const isInherited = grant.grantedActions.length === 0;
if (!newUserList.has(userId)) {
const userFullName = await this.getUserFullName(userId);
// Create user object
const user = {
userIdentifier: userId,
userFullName: userFullName,
permissions: this.createEmptyUserPermission(),
};
// Set current grant
user.permissions.set(grant.action, {
action: grant.action,
identifier: grant.identifier,
inherited: isInherited,
});
// Add to user list
newUserList.set(userId, user);
} else {
// We already have this user in userList
let user = newUserList.get(userId);
// Set current grant
user.permissions.set(grant.action, {
action: grant.action,
identifier: grant.identifier,
inherited: isInherited,
});
// Add to user list
newUserList.set(userId, user);
}
}
}
this.userList = newUserList;
this.setAllButtonState('edit');
} else {
if (responseBody.status === 500) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t(
'grant-permission-dialog.notifications.could-not-fetch-resource-class-actions',
),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
} else if (response.status === 403) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.error-not-authorized'),
targetNotificationId: 'permission-modal-notification',
type: 'danger',
timeout: 0,
});
}
}
} catch (e) {
console.log('setListOfUsersAndPermissions', e);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.unknown-error'),
targetNotificationId: 'permission-modal-notification',
type: 'danger',
timeout: 0,
});
}
}
renderUserPermissionRow() {
if (!this.userList) {
return;
}
const i18n = this._i18n;
return html`
${repeat(
Array.from(this.userList),
([userId]) => userId, // Key function - uses userId as unique identifier
([userId, user]) => html`
<div
class="user-row ${classMap({'edit-mode': this.usersToAdd.has(userId)})}"
data-user-id="${userId}">
<div class="person-select-container">
${user.userFullName
? html`
<span class="user-name">${user.userFullName}</span>
`
: html`
<dbp-person-select
id="permission-person-select"
subscribe="auth"
lang="${this.lang}"
@change="${(event) => {
this.handlePersonSelected(event);
}}"
entry-point-url="${this
.entryPointUrl}"></dbp-person-select>
`}
</div>
${user.userFullName ? this.renderPermissionCheckboxes(user) : ''}
${user.userFullName
? html`
<div class="action-buttons">
${user.buttonState === 'edit'
? html`
<dbp-button
type="is-secondary"
id="user-edit-button-${userId}"
no-spinner-on-click
@click="${() => {
this.handleUserEditButton(userId);
}}">
<dbp-icon name="pencil"></dbp-icon>
${i18n.t(
'grant-permission-dialog.buttons.edit-text',
)}
</dbp-button>
`
: ''}
${user.buttonState === 'save'
? html`
<dbp-button
type="is-primary"
id="user-save-button-${userId}"
no-spinner-on-click
@click="${() => {
this.handleUserSaveButton(userId);
}}">
<dbp-icon name="save"></dbp-icon>
${i18n.t(
'grant-permission-dialog.buttons.save-text',
)}
</dbp-button>
`
: ''}
<dbp-button
type="is-secondary"
id="user-delete-button-${userId}"
no-spinner-on-click
?disabled=${this.lastSavedManagerId === userId}
@click="${async () => {
const confirmed = await getDeletionConfirmation(this);
if (!confirmed) return;
const userFullName = this.lookupUserFullName(userId);
await this.deleteUser(userId);
sendNotification({
summary: i18n.t(
'grant-permission-dialog.notifications.success-title',
),
body: i18n.t(
'grant-permission-dialog.notifications.user-successfully-deleted',
{
userFullName: userFullName,
},
),
type: 'info',
targetNotificationId:
'permission-modal-notification',
timeout: 5,
});
}}">
<dbp-icon name="trash"></dbp-icon>
${i18n.t('grant-permission-dialog.buttons.delete-text')}
</dbp-button>
</div>
`
: ''}
</div>
`,
)}
`;
}
renderPermissionCheckboxes(user) {
const i18n = this._i18n;
if (!this.availableActions) {
return;
}
return html`
<div class="permission-group" role="group" aria-labelledby="permissions-group-label">
<h3 id="permissions-group-label" class="visually-hidden">
${i18n.t('grant-permission-dialog.available-permissions')}
</h3>
${this.availableActions.map((action) => {
const actionValue = Object.keys(action)[0];
const actionName = action[actionValue][this.lang];
let hasThisPermission = false;
let editable = false;
let checkboxTitle = `${actionName}`;
let userPermission = null;
if (user.permissions) {
userPermission = user.permissions.get(actionValue);
// The permission exists if it has an identifier
if (userPermission?.identifier) {
hasThisPermission = true;
}
// Allow editing of newly added permissions
if (userPermission?.editable) {
editable = true;
}
if (userPermission?.inherited) {
editable = false;
checkboxTitle = i18n.t(
'grant-permission-dialog.permissions.inherited-permission-title',
);
}
}
return html`
<div
class="checkbox-container ${classMap({
'inherited-mode': userPermission?.inherited,
})}">
<label
for="${actionValue}-${user.userIdentifier}"
class="visually-hidden">
${actionName}
</label>
<input
id="${actionValue}-${user.userIdentifier}"
name="${actionValue}"
class="permission-checkbox ${classMap({
'edit-mode': this.usersToAdd.has(user.userIdentifier),
'inherited-mode': userPermission?.inherited,
})}"
title="${checkboxTitle}"
data-user-id="${user.userIdentifier}"
type="checkbox"
@input="${this.handleCheckbox}"
?disabled="${!editable}"
?checked="${hasThisPermission}" />
</div>
`;
})}
</div>
`;
}
createEmptyUserPermission(editable = false) {
if (!Array.isArray(this.availableActions) || this.availableActions.length < 1) {
return;
}
const userPermissions = new Map();
this.availableActions.forEach((action) => {
const actionValue = Object.keys(action)[0];
const emptyPermission = {
action: actionValue,
identifier: null,
editable: editable,
};
userPermissions.set(actionValue, emptyPermission);
});
return userPermissions;
}
async handlePersonSelected(event) {
const i18n = this._i18n;
try {
const newUser = JSON.parse(event.target.dataset.object);
// Check if user is already in the list
if (newUser && this.userList.has(newUser.identifier)) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.user-already-added'),
targetNotificationId: 'permission-modal-notification',
type: 'danger',
timeout: 0,
});
return;
}
// Set new user data
let userToAdd = {};
userToAdd.userIdentifier = newUser['identifier'];
userToAdd.userFullName = `${newUser['givenName']} ${newUser['familyName']}`;
userToAdd.permissions = this.createEmptyUserPermission(true);
this.addUserToQueue(userToAdd.userIdentifier, userToAdd);
// Remove person select
this.userList.delete('emptyPerson');
// Update person in this.userList
this.addUserToList(userToAdd.userIdentifier, userToAdd);
// Toggle edit button to save button
this.setButtonState(userToAdd.userIdentifier, 'edit');
await this.updateComplete;
this.handleUserEditButton(userToAdd.userIdentifier);
this.addPersonButtonRef.value.stop();
} catch (error) {
console.log('Failed to get user object', error);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.failed-to-get-user-details'),
targetNotificationId: 'permission-modal-notification',
type: 'danger',
timeout: 0,
});
}
}
handleCheckbox(event) {
const checkbox = event.target;
checkbox.classList.toggle('changed');
if (!checkbox.getAttribute('data-changed')) {
checkbox.setAttribute('data-changed', true);
} else {
checkbox.removeAttribute('data-changed');
}
// Prevent unchecking last manager
this.disableLastManageCheckbox();
this.checkSavedManagerCount();
const userIdentifier = checkbox.getAttribute('data-user-id');
const permissionName = checkbox.getAttribute('name');
// Get users to add
const userToAdd = this.usersToAdd.get(userIdentifier);
// Get clicked permission
const permission = userToAdd.permissions.get(permissionName);
// Set permission to be saved
permission.toSave = checkbox.getAttribute('data-changed') ? true : false;
}
enableUsersAllCheckboxes(userId) {
this.userList.get(userId).permissions.forEach((permission) => {
if (
(permission.action === 'manage' && this.lastSavedManagerId === userId) ||
permission.inherited
) {
permission.editable = false;
} else {
permission.editable = true;
}
});
}
disableUsersAllCheckboxes(userId) {
this.userList.get(userId).permissions.forEach((permission) => {
permission.editable = false;
});
}
disableAllCheckboxes() {
this.userList.forEach((user) => {
user.permissions.forEach((permission) => {
permission.editable = false;
});
});
}
renderPermissionLabels() {
if (!this.availableActions || this.userList.size < 1) {
return;
}
return html`
${this.availableActions.map((action) => {
const actionValue = Object.keys(action)[0];
const actionName = action[actionValue][this.lang] ?? actionValue;
return html`
<div class="checkbox-label-container">
<span>${actionName}</span>
</div>
`;
})}
`;
}
async open() {
const i18n = this._i18n;
if (!this.isLoggedIn()) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.need-login-warning-text'),
type: 'danger',
timeout: 0,
});
} else {
await this.setAvailableActions();
await this.setListOfUsersAndPermissions();
this.permissionModalRef.value.open();
const modalContent = this._('.content-inner');
const resizeObserver = new ResizeObserver((entries) => {
const personSelect = this._('.person-select-container');
const permissionItems = /** @type {HTMLElement[]} */ (
Array.from(
this._a('.user-row:first-child .permission-group .checkbox-container'),
)
);
const actionButtons = this._('.action-buttons');
const userRow = this._('.user-row:first-child');
if (!userRow) return;
const userRowStyle = window.getComputedStyle(userRow);
const userRowPadding =
parseInt(userRowStyle.paddingLeft) + parseInt(userRowStyle.paddingRight);
const permissionGroup = this._('.user-row:first-child .permission-group');
const permissionGroupStyle = window.getComputedStyle(permissionGroup);
const permissionGroupGap = parseInt(permissionGroupStyle.gap);
const modalWidth = modalContent.clientWidth;
const permissionCount = permissionItems.length;
const permissionWidth =
permissionItems[0].clientWidth * permissionCount +
permissionGroupGap * permissionCount -
1;
const rowWidth =
personSelect.clientWidth +
(permissionItems[0].clientWidth * permissionCount +
permissionGroupGap * permissionCount -
1) +
actionButtons.clientWidth;
if (modalWidth < permissionWidth + userRowPadding) {
modalContent.classList.add('mobile');
} else {
modalContent.classList.remove('mobile');
}
if (modalWidth < rowWidth + userRowPadding) {
modalContent.classList.add('collapsed');
} else {
modalContent.classList.remove('collapsed');
}
});
resizeObserver.observe(modalContent);
}
}
closeModal(event) {
/* Reset state */
if (event && event.detail && event.detail.id === 'grant-permission-modal') {
// Remove person select
this.userList = new Map();
// Reset add person button state
this.addPersonButtonRef.value.stop();
this.permissionModalRef.value.close();
}
}
handleAddNewPerson() {
this.addPersonButtonRef.value.start();
this.addUserToList('emptyPerson', {});
}
/**
* Add user to the usersToAdd queue
* Triggers a re-render
* @param {string} userId
*/
addUserToQueue(userId, userToAdd = null) {
const _userToAdd = userToAdd === null ? this.userList.get(userId) : userToAdd;
this.usersToAdd = new Map(this.usersToAdd).set(userId, _userToAdd);
}
/**
* Remove user from the usersToAdd queue
* Triggers a re-render
* @param {string} userId
*/
removeUserFromQueue(userId) {
const newUsersToAdd = new Map(this.usersToAdd);
newUsersToAdd.delete(userId);
this.usersToAdd = newUsersToAdd;
}
/**
* Add user to the userList
* Triggers a re-render
* @param {string} userId
*/
addUserToList(userId, userToAdd) {
this.userList = new Map(this.userList).set(userId, userToAdd);
}
/**
* Remove user from the userList
* Triggers a re-render
* @param {string} userId
*/
removeUserFromList(userId) {
const newUserList = new Map(this.userList);
const deleted = newUserList.delete(userId);
if (deleted) {
this.userList = newUserList;
return true;
} else {
return false;
}
}
/**
*
* @param {string} userId - The user ID
* @param {string} state - edit | save | prepare-delete | delete
*/
setButtonState(userId, state) {
this.userList.set(userId, {
...this.userList.get(userId),
buttonState: state,
});
this.requestUpdate('userList');
}
setAllButtonState(state) {
Array.from(this.userList).forEach(async ([userId, user]) => {
this.setButtonState(userId, state);
});
}
/**
* Saves user permissions for either a single user or all users in usersToAdd
* @param {string} [userId] - Optional user ID. If not provided, saves all pending users
* @returns {Promise<void>}
*/
async saveUserPermissions(userId) {
const i18n = this._i18n;
// If no users to save, show message and return
if (this.usersToAdd.size === 0) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.info-title'),
body: i18n.t('grant-permission-dialog.notifications.there-is-no-user-to-save'),
type: 'info',
targetNotificationId: 'permission-modal-notification',
timeout: 5,
});
return;
}
try {
let usersToProcess;
let errorCount = 0;
let successCount = 0;
if (userId) {
// Process only the specified user
usersToProcess = new Map([[userId, this.usersToAdd.get(userId)]]);
} else {
// Process all users in the queue
usersToProcess = this.usersToAdd;
}
// Process user(s)
for (const userIdentifier of usersToProcess.keys()) {
const userToAdd = usersToProcess.get(userIdentifier);
if (!userToAdd) continue;
const grantsToPost = [];
const grantsToDelete = [];
// Collect grants to create and delete
userToAdd.permissions.forEach((permission) => {
if (!permission.toSave) return;
if (permission.identifier) {
grantsToDelete.push({
action: permission.action,
identifier: permission.identifier,
});
permission.identifier = null;
} else {
grantsToPost.push({
action: permission.action,
userIdentifier: userToAdd.userIdentifier,
});
}
permission.toSave = false;
});
if (grantsToPost.length === 0 && grantsToDelete.length === 0) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.info-title'),
body: i18n.t(
'grant-permission-dialog.notifications.there-is-nothing-to-save',
{userFullName: userToAdd.userFullName},
),
type: 'info',
targetNotificationId: 'permission-modal-notification',
timeout: 5,
});
// Remove user form queue
this.removeUserFromQueue(userIdentifier);
continue;
}
// Add grants
if (grantsToPost.length > 0) {
for (const grant of grantsToPost) {
const postResponse = await this.apiPostResourceActionGrant(
grant.action,
grant.userIdentifier,
);
if (postResponse.status !== 201) {
errorCount++;
continue;
} else {
successCount++;
const responseBody = await postResponse.json();
// Set identifier in user permissions
userToAdd.permissions.set(grant.action, {
action: grant.action,
identifier: responseBody.identifier,
});
}
}
}
// Delete grants
if (grantsToDelete.length > 0) {
for (const grant of grantsToDelete) {
// Don't delete last manage grant
if (grant.action === 'manage' && this.lastSavedManagerId) {
sendNotification({
summary: 'Warning',
body: i18n.t(
'grant-permission-dialog.notifications.cant-remove-last-manager-warning',
),
type: 'warning',
targetNotificationId: 'permission-modal-notification',
timeout: 5,
});
continue;
}
const deleteResponse = await this.apiDeleteResourceActionGrant(
grant.identifier,
);
if (deleteResponse.status !== 204) {
errorCount++;
continue;
} else {
successCount++;
// Remove identifier from user permissions
userToAdd.permissions.set(grant.action, {
action: grant.action,
identifier: null,
});
}
}
}
// Remove edit styles & disable checkboxes
this._a(`[data-user-id="${userToAdd.userIdentifier}"]`).forEach((checkbox) => {
const checkboxElem = /** @type {HTMLInputElement} */ (checkbox);
checkboxElem.classList.remove('changed');
checkboxElem.removeAttribute('data-changed');
});
this.disableUsersAllCheckboxes(userToAdd.userIdentifier);
// Update permissions in userList
this.addUserToList(userToAdd.userIdentifier, {
userIdentifier: userToAdd.userIdentifier,
userFullName: userToAdd.userFullName,
permissions: userToAdd.permissions,
});
// Remove processed user from usersToAdd list
this.removeUserFromQueue(userIdentifier);
}
this.requestUpdate('userList');
// Stop the save button spinner and show success message
this.savePermissionButtonRef.value.stop();
if (successCount > 0) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.success-title'),
body: i18n.t(
'grant-permission-dialog.notifications.permissions-saved-successfully',
{n: successCount},
),
type: 'success',
targetNotificationId: 'permission-modal-notification',
timeout: 5,
});
}
if (errorCount > 0) {
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t('grant-permission-dialog.notifications.save-permissions-error', {
n: errorCount,
}),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
}
} catch (e) {
console.log('Save user permissions error:', e);
sendNotification({
summary: i18n.t('grant-permission-dialog.notifications.error-title'),
body: i18n.t(
'grant-permission-dialog.notifications.save-permissions-unexpected-error',
),
type: 'danger',
targetNotificationId: 'permission-modal-notification',
timeout: 0,
});
return;
}
}
static get styles() {
return [
commonStyles.getGeneralCSS(),
commonStyles.getButtonCSS(),
commonStyles.getModalDialogCSS(),
getGrantPermissionDialogCSS(),
// language=css
css``,
];
}
render() {
const i18n = this._i18n;
console.log('*** MAIN RENDER ***');
return html`
<dbp-modal
id="permission-modal"
sticky-footer
${ref(this.permissionModalRef)}
class="modal modal--permissions"
modal-id="grant-permission-modal"
subscribe="lang">
<div slot="title">
<h2 class="modal-title">${this.modalTitle}</h2>
</div>
<div slot="header" class="header">
<div class="modal-notification">
<dbp-notification
id="permission-modal-notification"
inline
lang="${this.lang}"></dbp-notification>
</div>
</div>
<div slot="content">
<div class="content-container">
<div class="content-inner">
<div
class="header-row ${classMap({
hidden: this.userList.size < 1,
})}">
<div class="person-select-header"></div>
<div class="permissions-header">
${this.renderPermissionLabels()}
</div>
</div>
<!-- END .header-row -->
<div class="body-container">
<div class="button-container">
<dbp-button
type="is-secondary"
${ref(this.addPersonButtonRef)}
id="add-new-person-button"
@click="${() => {
this.handleAddNewPerson();
}}">
<dbp-icon name="plus"></dbp-icon>
<span>
${i18n.t(
'grant-permission-dialog.buttons.add-person-text',
)}
</span>
</dbp-button>
</div>
<div class="user-row-container">
${this.renderUserPermissionRow()}
</div>
</div>
</div>
</div>
</div>
<menu slot="footer" class="footer-menu">
<dbp-button
no-spinner-on-click
type="is-secondary"
@click="${() => {
const customEvent = new CustomEvent('dbp-modal-closed', {
detail: {id: 'grant-permission-modal'},
bubbles: true,
composed: true,
});
this.dispatchEvent(customEvent);
}}">
${i18n.t('grant-permission-dialog.buttons.cancel-text')}
</dbp-button>
<dbp-button
no-spinner-on-click
${ref(this.savePermissionButtonRef)}
id="permission-save-button"
?disabled="${this.usersToAdd.size === 0}"
@click="${async () => {
try {
await this.saveUserPermissions();
} catch (error) {
console.error('Error saving user permissions:', error);
}
// Revert buttons to edit button
this.setAllButtonState('edit');
// Remove edit styles & disable checkboxes
this._a(`input[type="checkbox"][data-user-id]`).forEach((checkbox) => {
const checkboxElem = /** @type {HTMLInputElement} */ (checkbox);
checkboxElem.classList.remove('changed');
checkboxElem.removeAttribute('data-changed');
});
this.disableAllCheckboxes();
}}"
type="is-primary">
${i18n.t('grant-permission-dialog.buttons.save-all-text')}
</dbp-button>
</menu>
</dbp-modal>
<!-- Deletion Confirmation Modal -->
<dbp-modal
id="deletion-confirmation-modal--grant-permission"
class="modal modal--confirmation"
modal-id="deletion-confirmation-modal"
title="${i18n.t('grant-permission-dialog.delete-confirmation.title')}"
subscribe="lang">
<div slot="content">
<p>${i18n.t('grant-permission-dialog.delete-confirmation.message')}</p>
</div>
<menu slot="footer" class="footer-menu">
<dbp-button
type="is-secondary"
no-spinner-on-click
@click="${() => handleDeletionCancel(this)}">
${i18n.t('grant-permission-dialog.delete-confirmation.button-abort-text')}
</dbp-button>
<dbp-button
type="is-danger"
no-spinner-on-click
@click="${() => handleDeletionConfirm(this)}">
${i18n.t('grant-permission-dialog.delete-confirmation.button-delete-text')}
</dbp-button>
</menu>
</dbp-modal>
`;
}
}