feat: Update organization member management and bulk invite functionality

- Marked the status of "Invite and Manage Team Members" story as Complete in README.
- Updated the status of ORG-002 to Complete in the corresponding markdown file.
- Modified OrganizationManager to assign roles as 'owner' during organization creation.
- Implemented bulk invitation feature in UserInvitationManager, allowing multiple users to be invited via CSV upload.
- Added IReq_BulkCreateInvitations interface for bulk invitation requests.
- Enhanced CreateOrgForm to update state with new roles upon organization creation.
- Introduced BulkInviteModal for bulk inviting users, including email validation and role assignment.
- Updated UsersView to support ownership transfer and bulk invitation functionality.
- Improved account state management to handle new roles and organizations.
This commit is contained in:
2025-12-05 09:34:19 +00:00
parent 8df44b99b9
commit 833cf3b4b8
11 changed files with 1148 additions and 512 deletions
+124 -19
View File
@@ -12,6 +12,7 @@ import {
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
import * as accountState from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
import { BulkInviteModal } from '../bulk-invite-modal.js';
declare global {
interface HTMLElementTagNameMap {
@@ -64,6 +65,9 @@ export class UsersView extends DeesElement {
@state()
accessor isAdmin: boolean = false;
@state()
accessor isOwner: boolean = false;
@state()
accessor currentUserId: string = '';
@@ -75,6 +79,8 @@ export class UsersView extends DeesElement {
private static readonly AVAILABLE_ROLES = ['owner', 'admin', 'editor', 'viewer', 'guest'];
private emailInputSubscribed: boolean = false;
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
@@ -459,16 +465,28 @@ export class UsersView extends DeesElement {
<span class="role-badge ${role}">${role}</span>
`)}
</div>
${this.isAdmin && member.userId !== this.currentUserId ? html`
${member.userId !== this.currentUserId ? html`
<div class="member-actions">
<button
class="action-button danger"
@click=${() => this.handleRemoveMember(member.userId, member.name)}
?disabled=${this.submitting || member.isOwner}
title=${member.isOwner ? 'Cannot remove owner' : 'Remove member'}
>
Remove
</button>
${this.isOwner && !member.isOwner ? html`
<button
class="action-button"
@click=${() => this.handleTransferOwnership(member.userId, member.name)}
?disabled=${this.submitting}
title="Transfer ownership to this member"
>
Transfer Ownership
</button>
` : ''}
${this.isAdmin ? html`
<button
class="action-button danger"
@click=${() => this.handleRemoveMember(member.userId, member.name)}
?disabled=${this.submitting || member.isOwner}
title=${member.isOwner ? 'Cannot remove owner' : 'Remove member'}
>
Remove
</button>
` : ''}
</div>
` : ''}
</div>
@@ -560,23 +578,40 @@ export class UsersView extends DeesElement {
.status=${this.submitting ? 'pending' : 'normal'}
@click=${() => this.handleSendInvitation()}
></dees-button>
<div style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--border);">
<p style="color: var(--muted-foreground); font-size: 13px; margin: 0 0 12px 0;">
Need to invite multiple people at once?
</p>
<dees-button
.text=${'Import from CSV'}
.type=${'secondary'}
@click=${() => this.handleBulkImport()}
></dees-button>
</div>
</div>
`;
}
public async firstUpdated() {
// Subscribe to email input changes
await this.updateComplete;
const emailInput = this.shadowRoot?.querySelector('dees-input-text') as any;
if (emailInput) {
emailInput.changeSubject?.subscribe((element: any) => {
this.inviteEmail = element.value;
});
}
await this.loadData();
}
public updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
// Subscribe to email input when Invite tab is shown
if (this.activeTab === 'invite' && !this.emailInputSubscribed) {
const emailInput = this.shadowRoot?.querySelector('.invite-form dees-input-text') as any;
if (emailInput?.changeSubject) {
emailInput.changeSubject.subscribe((element: any) => {
this.inviteEmail = element.value;
});
this.emailInputSubscribed = true;
}
}
}
private async loadData() {
this.loading = true;
@@ -598,11 +633,12 @@ export class UsersView extends DeesElement {
this.organizationName = selectedOrg.data.name;
this.currentUserId = currentState.user?.id || '';
// Check if current user is admin
// Check if current user is admin/owner
const currentUserRole = currentState.roles.find(
r => r.data.organizationId === this.organizationId && r.data.userId === this.currentUserId
);
this.isAdmin = currentUserRole?.data?.roles?.some(r => ['owner', 'admin'].includes(r)) ?? false;
this.isOwner = currentUserRole?.data?.roles?.includes('owner') ?? false;
// Get JWT from IdpState
const idpState = await IdpState.getSingletonInstance();
@@ -818,6 +854,75 @@ export class UsersView extends DeesElement {
}
}
private async handleTransferOwnership(newOwnerId: string, name: string) {
const confirmed = await this.showTransferConfirmation(name);
if (!confirmed) return;
this.submitting = true;
this.actionMessage = null;
try {
const idpState = await IdpState.getSingletonInstance();
const jwt = await idpState.idpClient.getJwt();
const request = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_TransferOwnership>(
'transferOwnership'
);
const response = await request.fire({
jwt,
organizationId: this.organizationId,
newOwnerId,
});
if (response.success) {
this.showMessage('success', `Ownership transferred to ${name}. You are now an admin.`);
await this.loadData();
} else {
this.showMessage('error', response.message || 'Failed to transfer ownership.');
}
} catch (error) {
console.error('Error transferring ownership:', error);
this.showMessage('error', 'Failed to transfer ownership. Please try again.');
} finally {
this.submitting = false;
}
}
private async showTransferConfirmation(name: string): Promise<boolean> {
return new Promise((resolve) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Transfer Ownership',
content: html`
<div style="padding: 16px 0;">
<p style="margin: 0 0 12px 0;">Are you sure you want to transfer ownership to <strong>${name}</strong>?</p>
<p style="margin: 0; color: var(--muted-foreground);">
You will be demoted to admin role and will no longer be the owner of this organization.
</p>
</div>
`,
menuOptions: [
{ name: 'Cancel', action: async (modal) => { modal.destroy(); resolve(false); } },
{ name: 'Transfer Ownership', action: async (modal) => { modal.destroy(); resolve(true); } },
],
width: 420,
});
});
}
private async handleBulkImport() {
const result = await BulkInviteModal.show({
organizationId: this.organizationId,
organizationName: this.organizationName,
});
if (result && result.invitedCount > 0) {
this.showMessage('success', `${result.invitedCount} invitation(s) sent successfully.`);
await this.loadData();
this.activeTab = 'pending';
}
}
private showMessage(type: 'success' | 'error', text: string) {
this.actionMessage = { type, text };
// Auto-hide after 5 seconds