Skip to content

Commit 6ac734c

Browse files
authored
421 collaborator management component refresh required (#469)
* Sync collaborator state and replace alerts with toasts Update local collaborator cache and improve UI feedback for role/member changes. - api/Project.js: keep this.collaborators in sync by deleting entries on member removal and updating roles after makeLeader, demoteLeader, setToViewer and setRoles calls so the client-side state reflects server changes. - components/project-collaborators/index.js: add refreshCollaborators() to re-render the collaborators list when roles change. - components/roles-handler/index.js: replace alert() calls with TPEN.eventDispatcher 'tpen-toast' notifications, add try/catch error handling, and call refreshCollaborators() after successful operations to update the UI. These changes ensure consistent local state and provide non-blocking toast notifications instead of modal alerts. * buttons hiding correctly now * Refactor role management toggles and handlers Update RolesHandler to improve accessibility, robustness, and control flow for role management UI. - Added aria-expanded and data-role-management-open attributes to manage button markup for accessibility. - Removed unused userId/isOwnerOrLeader logic. - Removed manageRoleButtons setup and consolidated toggle logic into toggleRoleManagementButtons with defensive checks (valid button, memberId, actions container, and collaborator existence). - Use dataset flags and aria-expanded to track open/closed state and cleanly add/remove role management UI. - Replaced dynamic actions map with an explicit switch-based handler to await async actions and directly handle the manage-roles button case. These changes make the role management UI safer against missing DOM/Project state and clearer in behavior. * rehandling buttons * Apply better UX to collaboration management (#471) * major reorganization for better UX * evening out styling * cleanup and accessibility review * Update index.js * Update collaborators.html * addressing Claude's suggestions Send the correct roles payload from ManageRole (use this.group). Update RolesHandler to improve styling, accessibility, and behavior: switch hardcoded colors/shadows to CSS variables; register a cleanup callback to remove injected styles; add aria-labelledby to the modal; prevent duplicate injected manage buttons; track the trigger element and restore focus when the modal closes; set aria-pressed on toggles; disable the Save button and show a loading state while saving; replace alerts with toasts and improve error handling for ownership transfer. These changes fix a payload bug and enhance reliability and accessibility of the roles UI. * Add custom roles UI and selection logic Render a Custom Roles section in the manage modal and wire up toggle buttons for any non-default roles defined on the project. Adds CSS for custom-role toggles, shows the section only when custom roles exist, and toggles aria-pressed/active state on click. Collects active custom-role-toggle values into selected/current roles when saving and viewing, updates the viewer fallback logic to require absence of LEADER/CONTRIBUTOR, and includes custom roles in the originalSelection comparison. Also makes openManageModal async and tweaks manage-button styling to use CSS vars for background and hover color. * static review adjustments Dispatch a member-invited event and refresh collaborators when a new member is invited. Update RolesHandler UI (padding, overflow, z-index) and make openManageModal synchronous. Replace many direct onclick handlers with renderCleanup.onElement bindings to ensure proper cleanup. Add modal state helpers (getCurrentRoles, hasChanges, updateSaveButtonState) to track unsaved changes and enable/disable the Save button accordingly. Tweak default VIEWER logic so VIEWER is added when no Leader/Contributor is present. * Show detailed error messages in roles handler Add getErrorMessage helper to extract user-friendly error text from Error/response objects (handles HTTP status, statusText, response data message/error, and fallbacks). Replace generic toast messages in catch blocks for updating roles, transferring ownership, and removing members to use the new helper so toasts display more informative errors (e.g., 403/404/409/5xx messages or underlying error.message).
1 parent e58fb31 commit 6ac734c

6 files changed

Lines changed: 754 additions & 252 deletions

File tree

api/Project.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export default class Project {
139139
throw new Error(`Error removing member: ${response.status}`)
140140
}
141141

142+
delete this.collaborators[userId]
142143
return await response
143144
} catch (error) {
144145
userMessage(error.message)
@@ -160,6 +161,9 @@ export default class Project {
160161
throw new Error(`Error promoting user to LEADER: ${response.status}`)
161162
}
162163

164+
if (this.collaborators[userId] && !this.collaborators[userId].roles.includes("LEADER")) {
165+
this.collaborators[userId].roles.push("LEADER")
166+
}
163167
return response
164168
} catch (error) {
165169
userMessage(error.message)
@@ -181,6 +185,9 @@ export default class Project {
181185
throw new Error(`Error removing LEADER role: ${response.status}`)
182186
}
183187

188+
if (this.collaborators[userId]) {
189+
this.collaborators[userId].roles = this.collaborators[userId].roles.filter(role => role !== "LEADER")
190+
}
184191
return response
185192
} catch (error) {
186193
userMessage(error.message)
@@ -201,8 +208,9 @@ export default class Project {
201208
if (!response.ok) {
202209
throw new Error(`Error revoking write access: ${response.status}`)
203210
}
204-
205-
return response
211+
if (this.collaborators[userId]) {
212+
this.collaborators[userId].roles = ["VIEWER"]
213+
} return response
206214
} catch (error) {
207215
userMessage(error.message)
208216
}
@@ -223,6 +231,9 @@ export default class Project {
223231
throw new Error(`Error setting user roles: ${response.status}`)
224232
}
225233

234+
if (this.collaborators[userId]) {
235+
this.collaborators[userId].roles = roles
236+
}
226237
return response
227238
} catch (error) {
228239
userMessage(error.message)

components/manage-role/index.js

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class ManageRole extends HTMLElement {
1818

1919
permissions = []
2020
isExistingRole = false
21+
/** @type {Object} Local cache of roles for this component */
22+
group = {}
2123

2224
constructor() {
2325
super()
@@ -55,10 +57,11 @@ class ManageRole extends HTMLElement {
5557
'Authorization': `Bearer ${TPEN.getAuthorization()}`
5658
}
5759
}).then(response => response.json())
58-
this.render(group)
60+
this.group = group || {}
61+
this.render(this.group)
5962
}
6063

61-
render(group) {
64+
render(group = this.group) {
6265
this.shadowRoot.innerHTML = `
6366
<style>
6467
h3 {
@@ -454,17 +457,26 @@ class ManageRole extends HTMLElement {
454457
this.shadowRoot.getElementById('role-name').value = ''
455458
this.permissions = []
456459
const roleId = roleLi.querySelector("#roleID").textContent.toUpperCase()
457-
await fetch(`${TPEN.servicesURL}/project/${TPEN.activeProject._id}/removeCustomRoles`, {
458-
method: 'DELETE',
459-
headers: {
460-
'Content-Type': 'application/json',
461-
'Authorization': `Bearer ${TPEN.getAuthorization()}`
462-
},
463-
body: JSON.stringify({ roles: [roleId] })
464-
}).then(response => {
465-
TPEN.eventDispatcher.dispatch("tpen-toast", response.ok ? { status: "info", message: 'Successfully Removed Role' } : { status: "error", message: 'Error Removing Role' })
466-
})
467-
this.render()
460+
try {
461+
const response = await fetch(`${TPEN.servicesURL}/project/${TPEN.activeProject._id}/removeCustomRoles`, {
462+
method: 'DELETE',
463+
headers: {
464+
'Content-Type': 'application/json',
465+
'Authorization': `Bearer ${TPEN.getAuthorization()}`
466+
},
467+
body: JSON.stringify({ roles: [roleId] })
468+
})
469+
470+
if (response.ok) {
471+
delete this.group[roleId]
472+
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "info", message: 'Successfully Removed Role' })
473+
} else {
474+
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: 'Error Removing Role' })
475+
}
476+
} catch (err) {
477+
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: `Error Removing Role: ${err.message}` })
478+
}
479+
this.render(this.group)
468480
})
469481
})
470482

@@ -474,7 +486,7 @@ class ManageRole extends HTMLElement {
474486
})
475487
}
476488

477-
updateRolePermissions(group, selectedRole) {
489+
updateRolePermissions(group = this.group, selectedRole) {
478490
this.shadowRoot.getElementById('role-name').value = selectedRole.querySelector('#roleID').textContent
479491
this.permissions = []
480492
this.isExistingRole = true
@@ -509,33 +521,34 @@ class ManageRole extends HTMLElement {
509521
const role = this.shadowRoot.getElementById('role-name')
510522
if (!role.value) return TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: 'No role selected for update' })
511523

512-
Object.keys(group || {}).forEach(key => {
524+
Object.keys(this.group || {}).forEach(key => {
513525
if (key.toUpperCase() === role.value.toUpperCase()) {
514-
group[key] = this.permissions
526+
this.group[key] = [...this.permissions]
515527
}
516528
})
517529

518-
await fetch(`${TPEN.servicesURL}/project/${TPEN.activeProject._id}/updateCustomRoles`, {
519-
method: 'PUT',
520-
headers: {
521-
'Content-Type': 'application/json',
522-
'Authorization': `Bearer ${TPEN.getAuthorization()}`
523-
},
524-
body: JSON.stringify({ roles: group })
525-
}).then(response => {
530+
try {
531+
const response = await fetch(`${TPEN.servicesURL}/project/${TPEN.activeProject._id}/updateCustomRoles`, {
532+
method: 'PUT',
533+
headers: {
534+
'Content-Type': 'application/json',
535+
'Authorization': `Bearer ${TPEN.getAuthorization()}`
536+
},
537+
body: JSON.stringify({ roles: this.group })
538+
})
539+
526540
if (response.ok) {
527541
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "info", message: 'Successfully Updated Role' })
528-
this.render()
542+
// Reset internal state before rendering
543+
this.permissions = []
544+
this.isExistingRole = false
545+
this.render(this.group)
529546
} else {
530547
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: 'Error Updating Role' })
531548
}
532-
}).catch(error => {
549+
} catch (error) {
533550
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: `Error updating role: ${error.message}` })
534-
})
535-
536-
this.resetPermissions()
537-
role.value = ''
538-
this.permissions = []
551+
}
539552
}
540553

541554

@@ -644,6 +657,7 @@ class ManageRole extends HTMLElement {
644657
}
645658

646659
addPermissions(group) {
660+
group = group || this.group
647661
let permissionString = this.shadowRoot.getElementById('permission')
648662
const permissionsDiv = this.shadowRoot.getElementById('permissions')
649663
const role = this.shadowRoot.getElementById('role-name')
@@ -751,6 +765,7 @@ class ManageRole extends HTMLElement {
751765

752766
async addRole(group) {
753767
const role = this.shadowRoot.getElementById('role-name')
768+
group = group || this.group
754769

755770
if (!role.value) {
756771
return TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: 'Role name is required' })
@@ -770,29 +785,35 @@ class ManageRole extends HTMLElement {
770785
return TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: 'At least one permission is required' })
771786
}
772787

773-
await fetch(`${TPEN.servicesURL}/project/${TPEN.activeProject._id}/addCustomRoles`, {
774-
method: 'POST',
775-
headers: {
776-
'Content-Type': 'application/json',
777-
'Authorization': `Bearer ${TPEN.getAuthorization()}`
778-
},
779-
body: JSON.stringify({
780-
roles: {
781-
[role.value.toUpperCase()]: this.permissions
782-
}
788+
try {
789+
const response = await fetch(`${TPEN.servicesURL}/project/${TPEN.activeProject._id}/addCustomRoles`, {
790+
method: 'POST',
791+
headers: {
792+
'Content-Type': 'application/json',
793+
'Authorization': `Bearer ${TPEN.getAuthorization()}`
794+
},
795+
body: JSON.stringify({
796+
roles: {
797+
[role.value.toUpperCase()]: this.permissions
798+
}
799+
})
783800
})
784-
})
785-
.then(response => {
801+
786802
if (response.ok) {
787-
return TPEN.eventDispatcher.dispatch("tpen-toast", { status: "info", message: 'Custom role added successfully' })
803+
// update local cache and re-render without refetching
804+
this.group = this.group || {}
805+
this.group[role.value.toUpperCase()] = [...this.permissions]
806+
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "info", message: 'Custom role added successfully' })
807+
// Reset internal state before rendering
808+
this.permissions = []
809+
this.isExistingRole = false
810+
this.render(this.group)
811+
} else {
812+
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: 'Error adding role' })
788813
}
789-
})
790-
.catch(error => {
814+
} catch (error) {
791815
TPEN.eventDispatcher.dispatch("tpen-toast", { status: "error", message: `Error adding role: ${error.message}` })
792-
})
793-
794-
this.resetPermissions()
795-
this.render()
816+
}
796817
}
797818
}
798819

components/member-invitation/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ class InviteMemberElement extends HTMLElement {
123123
const response = await TPEN.activeProject.addMember(this.shadowRoot.querySelector('#invitee-email').value)
124124
if (!response) throw new Error("Invitation failed")
125125

126+
// Dispatch event to notify other components of the new member
127+
TPEN.eventDispatcher.dispatch('tpen-member-invited', { email: email })
128+
126129
this.shadowRoot.querySelector('#submit').textContent = "Submit"
127130
this.shadowRoot.querySelector('#submit').disabled = false
128131
this.shadowRoot.querySelector('#invitee-email').value = ""

components/project-collaborators/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ class ProjectCollaborators extends HTMLElement {
9999
})
100100
.join(" ")
101101
}
102+
103+
/**
104+
* Refreshes the collaborators display by re-rendering the collaborators list.
105+
* This is called when role changes occur to update the UI without a full page refresh.
106+
*/
107+
refreshCollaborators() {
108+
this.renderProjectCollaborators()
109+
}
102110
}
103111

104112
customElements.define('project-collaborators', ProjectCollaborators)

0 commit comments

Comments
 (0)