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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user