Browse Source
- Add new AdmitPendingMembersDialog component with checkbox selection - Support two action modes: "Admit + Add Contacts" and "Admit Only" - Integrate dialog into MembersList with proper sequencing - Show admit dialog before visibility dialog when pending members exist - Fix auto-refresh pause/resume logic for both dialogs - Ensure consistent dialog behavior between initial load and manual refresh - Add proper async/await handling for data refresh operations - Optimize dialog state management and remove redundant code - Maintain proper flag timing to prevent race conditions The admit dialog now shows automatically when there are pending members, allowing organizers to efficiently admit multiple members at once while optionally adding them as contacts and setting visibility preferences.pull/211/head
2 changed files with 620 additions and 13 deletions
@ -0,0 +1,458 @@ |
|||
<template> |
|||
<div v-if="visible" class="dialog-overlay"> |
|||
<div class="dialog"> |
|||
<div class="text-slate-900 text-center"> |
|||
<h3 class="text-lg font-semibold leading-[1.25] mb-2"> |
|||
Admit Pending Members |
|||
</h3> |
|||
<p class="text-sm mb-4"> |
|||
The following members are waiting to be admitted to the meeting. You |
|||
can choose to admit them and optionally add them as contacts with |
|||
visibility settings. |
|||
</p> |
|||
|
|||
<!-- Custom table area - you can customize this --> |
|||
<div v-if="shouldInitializeSelection" class="mb-4"> |
|||
<table |
|||
class="w-full border-collapse border border-slate-300 text-sm text-start" |
|||
> |
|||
<thead v-if="pendingMembersData && pendingMembersData.length > 0"> |
|||
<tr class="bg-slate-100 font-medium"> |
|||
<th class="border border-slate-300 px-3 py-2"> |
|||
<label class="flex items-center gap-2"> |
|||
<input |
|||
type="checkbox" |
|||
:checked="isAllSelected" |
|||
:indeterminate="isIndeterminate" |
|||
@change="toggleSelectAll" |
|||
/> |
|||
Select All |
|||
</label> |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<!-- Dynamic data from MembersList --> |
|||
<tr v-if="!pendingMembersData || pendingMembersData.length === 0"> |
|||
<td |
|||
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500" |
|||
> |
|||
No pending members to admit |
|||
</td> |
|||
</tr> |
|||
<tr |
|||
v-for="member in pendingMembersData || []" |
|||
:key="member.member.memberId" |
|||
> |
|||
<td class="border border-slate-300 px-3 py-2"> |
|||
<div class="flex items-center justify-between gap-2"> |
|||
<label class="flex items-center gap-2"> |
|||
<input |
|||
type="checkbox" |
|||
:checked="isMemberSelected(member.did)" |
|||
@change="toggleMemberSelection(member.did)" |
|||
/> |
|||
{{ member.name || SOMEONE_UNNAMED }} |
|||
</label> |
|||
|
|||
<!-- Contact indicator - only show if they are already a contact --> |
|||
<font-awesome |
|||
v-if="member.isContact" |
|||
icon="user-circle" |
|||
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600" |
|||
@click="showContactInfo" |
|||
/> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="space-y-2"> |
|||
<button |
|||
v-if="pendingMembersData && pendingMembersData.length > 0" |
|||
:disabled="!hasSelectedMembers" |
|||
:class="[ |
|||
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md', |
|||
hasSelectedMembers |
|||
? 'bg-green-600 text-white cursor-pointer' |
|||
: 'bg-slate-400 text-slate-200 cursor-not-allowed', |
|||
]" |
|||
@click="admitAndSetVisibility" |
|||
> |
|||
Admit Pending + Add Contacts |
|||
</button> |
|||
<button |
|||
v-if="pendingMembersData && pendingMembersData.length > 0" |
|||
:disabled="!hasSelectedMembers" |
|||
:class="[ |
|||
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md', |
|||
hasSelectedMembers |
|||
? 'bg-blue-600 text-white cursor-pointer' |
|||
: 'bg-slate-400 text-slate-200 cursor-not-allowed', |
|||
]" |
|||
@click="admitOnly" |
|||
> |
|||
Admit Pending Only |
|||
</button> |
|||
<button |
|||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" |
|||
@click="cancel" |
|||
> |
|||
Maybe Later |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Vue, Component, Prop } from "vue-facing-decorator"; |
|||
|
|||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; |
|||
import { SOMEONE_UNNAMED } from "@/constants/entities"; |
|||
import { setVisibilityUtil, getHeaders } from "@/libs/endorserServer"; |
|||
import { createNotifyHelpers } from "@/utils/notify"; |
|||
|
|||
interface PendingMemberData { |
|||
did: string; |
|||
name: string; |
|||
isContact: boolean; |
|||
member: { |
|||
memberId: string; |
|||
}; |
|||
} |
|||
|
|||
@Component({ |
|||
mixins: [PlatformServiceMixin], |
|||
}) |
|||
export default class AdmitPendingMembersDialog extends Vue { |
|||
@Prop({ default: false }) visible!: boolean; |
|||
@Prop({ default: () => [] }) pendingMembersData!: PendingMemberData[]; |
|||
@Prop({ default: "" }) activeDid!: string; |
|||
@Prop({ default: "" }) apiServer!: string; |
|||
|
|||
// Vue notification system |
|||
$notify!: ( |
|||
notification: { group: string; type: string; title: string; text: string }, |
|||
timeout?: number, |
|||
) => void; |
|||
|
|||
// Notification system |
|||
notify!: ReturnType<typeof createNotifyHelpers>; |
|||
|
|||
// Component state |
|||
selectedMembers: string[] = []; |
|||
selectionInitialized = false; |
|||
|
|||
// Constants |
|||
// In Vue templates, imported constants need to be explicitly made available to the template |
|||
readonly SOMEONE_UNNAMED = SOMEONE_UNNAMED; |
|||
|
|||
get hasSelectedMembers() { |
|||
return this.selectedMembers.length > 0; |
|||
} |
|||
|
|||
get isAllSelected() { |
|||
if (!this.pendingMembersData || this.pendingMembersData.length === 0) |
|||
return false; |
|||
return this.pendingMembersData.every((member) => |
|||
this.selectedMembers.includes(member.did), |
|||
); |
|||
} |
|||
|
|||
get isIndeterminate() { |
|||
if (!this.pendingMembersData || this.pendingMembersData.length === 0) |
|||
return false; |
|||
const selectedCount = this.pendingMembersData.filter((member) => |
|||
this.selectedMembers.includes(member.did), |
|||
).length; |
|||
return selectedCount > 0 && selectedCount < this.pendingMembersData.length; |
|||
} |
|||
|
|||
get shouldInitializeSelection() { |
|||
// This method will initialize selection when the dialog opens |
|||
if (!this.selectionInitialized) { |
|||
this.initializeSelection(); |
|||
this.selectionInitialized = true; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
created() { |
|||
this.notify = createNotifyHelpers(this.$notify); |
|||
} |
|||
|
|||
initializeSelection() { |
|||
// Reset selection when dialog opens |
|||
this.selectedMembers = []; |
|||
// Select all by default |
|||
this.selectedMembers = this.pendingMembersData.map((member) => member.did); |
|||
} |
|||
|
|||
resetSelection() { |
|||
this.selectedMembers = []; |
|||
this.selectionInitialized = false; |
|||
} |
|||
|
|||
toggleSelectAll() { |
|||
if (!this.pendingMembersData || this.pendingMembersData.length === 0) |
|||
return; |
|||
|
|||
if (this.isAllSelected) { |
|||
// Deselect all |
|||
this.selectedMembers = []; |
|||
} else { |
|||
// Select all |
|||
this.selectedMembers = this.pendingMembersData.map( |
|||
(member) => member.did, |
|||
); |
|||
} |
|||
} |
|||
|
|||
toggleMemberSelection(memberDid: string) { |
|||
const index = this.selectedMembers.indexOf(memberDid); |
|||
if (index > -1) { |
|||
this.selectedMembers.splice(index, 1); |
|||
} else { |
|||
this.selectedMembers.push(memberDid); |
|||
} |
|||
} |
|||
|
|||
isMemberSelected(memberDid: string) { |
|||
return this.selectedMembers.includes(memberDid); |
|||
} |
|||
|
|||
async admitAndSetVisibility() { |
|||
try { |
|||
const selectedMembers = this.pendingMembersData.filter((member) => |
|||
this.selectedMembers.includes(member.did), |
|||
); |
|||
|
|||
let admittedCount = 0; |
|||
let contactAddedCount = 0; |
|||
let visibilitySetCount = 0; |
|||
|
|||
for (const member of selectedMembers) { |
|||
try { |
|||
// First, admit the member |
|||
await this.admitMember(member); |
|||
admittedCount++; |
|||
|
|||
// If they're not a contact yet, add them as a contact |
|||
if (!member.isContact) { |
|||
await this.addAsContact(member); |
|||
contactAddedCount++; |
|||
} |
|||
|
|||
// Set their seesMe to true |
|||
await this.updateContactVisibility(member.did, true); |
|||
visibilitySetCount++; |
|||
} catch (error) { |
|||
// eslint-disable-next-line no-console |
|||
console.error(`Error processing member ${member.did}:`, error); |
|||
// Continue with other members even if one fails |
|||
} |
|||
} |
|||
|
|||
// Show success notification |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Members Admitted Successfully", |
|||
text: `Admitted ${admittedCount} member${admittedCount === 1 ? "" : "s"}, added ${contactAddedCount} as contact${contactAddedCount === 1 ? "" : "s"}, and set visibility for ${visibilitySetCount} member${visibilitySetCount === 1 ? "" : "s"}.`, |
|||
}, |
|||
5000, |
|||
); |
|||
|
|||
// Emit success event |
|||
this.$emit("success", { |
|||
admittedCount, |
|||
contactAddedCount, |
|||
visibilitySetCount, |
|||
}); |
|||
this.close(); |
|||
} catch (error) { |
|||
// eslint-disable-next-line no-console |
|||
console.error("Error admitting members:", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: "Failed to admit some members. Please try again.", |
|||
}, |
|||
5000, |
|||
); |
|||
} |
|||
} |
|||
|
|||
async admitOnly() { |
|||
try { |
|||
const selectedMembers = this.pendingMembersData.filter((member) => |
|||
this.selectedMembers.includes(member.did), |
|||
); |
|||
|
|||
let admittedCount = 0; |
|||
|
|||
for (const member of selectedMembers) { |
|||
try { |
|||
// Just admit the member |
|||
await this.admitMember(member); |
|||
admittedCount++; |
|||
} catch (error) { |
|||
// eslint-disable-next-line no-console |
|||
console.error(`Error admitting member ${member.did}:`, error); |
|||
// Continue with other members even if one fails |
|||
} |
|||
} |
|||
|
|||
// Show success notification |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Members Admitted Successfully", |
|||
text: `Admitted ${admittedCount} member${admittedCount === 1 ? "" : "s"} to the meeting.`, |
|||
}, |
|||
5000, |
|||
); |
|||
|
|||
// Emit success event |
|||
this.$emit("success", { |
|||
admittedCount, |
|||
contactAddedCount: 0, |
|||
visibilitySetCount: 0, |
|||
}); |
|||
this.close(); |
|||
} catch (error) { |
|||
// eslint-disable-next-line no-console |
|||
console.error("Error admitting members:", error); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: "Failed to admit some members. Please try again.", |
|||
}, |
|||
5000, |
|||
); |
|||
} |
|||
} |
|||
|
|||
async admitMember(member: { |
|||
did: string; |
|||
name: string; |
|||
member: { memberId: string }; |
|||
}) { |
|||
try { |
|||
const headers = await getHeaders(this.activeDid); |
|||
await this.axios.put( |
|||
`${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`, |
|||
{ admitted: true }, |
|||
{ headers }, |
|||
); |
|||
} catch (err) { |
|||
// eslint-disable-next-line no-console |
|||
console.error("Error admitting member:", err); |
|||
throw err; |
|||
} |
|||
} |
|||
|
|||
async addAsContact(member: { did: string; name: string }) { |
|||
try { |
|||
const newContact = { |
|||
did: member.did, |
|||
name: member.name, |
|||
}; |
|||
|
|||
await this.$insertContact(newContact); |
|||
} catch (err) { |
|||
// eslint-disable-next-line no-console |
|||
console.error("Error adding contact:", err); |
|||
if (err instanceof Error && err.message?.indexOf("already exists") > -1) { |
|||
// Contact already exists, continue |
|||
} else { |
|||
throw err; // Re-throw if it's not a duplicate error |
|||
} |
|||
} |
|||
} |
|||
|
|||
async updateContactVisibility(did: string, seesMe: boolean) { |
|||
try { |
|||
// Get the contact object |
|||
const contact = await this.$getContact(did); |
|||
if (!contact) { |
|||
throw new Error(`Contact not found for DID: ${did}`); |
|||
} |
|||
|
|||
// Use the proper API to set visibility on the server |
|||
const result = await setVisibilityUtil( |
|||
this.activeDid, |
|||
this.apiServer, |
|||
this.axios, |
|||
contact, |
|||
seesMe, |
|||
); |
|||
|
|||
if (!result.success) { |
|||
throw new Error(result.error || "Failed to set visibility"); |
|||
} |
|||
} catch (err) { |
|||
// eslint-disable-next-line no-console |
|||
console.error("Error updating contact visibility:", err); |
|||
throw err; |
|||
} |
|||
} |
|||
|
|||
showContactInfo() { |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "info", |
|||
title: "Contact Info", |
|||
text: "This user is already your contact, but they are not yet admitted to the meeting.", |
|||
}, |
|||
5000, |
|||
); |
|||
} |
|||
|
|||
close() { |
|||
this.resetSelection(); |
|||
this.$emit("close"); |
|||
} |
|||
|
|||
cancel() { |
|||
this.close(); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.dialog-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 1000; |
|||
} |
|||
|
|||
.dialog { |
|||
background: white; |
|||
border-radius: 8px; |
|||
padding: 24px; |
|||
max-width: 500px; |
|||
width: 90%; |
|||
max-height: 80vh; |
|||
overflow-y: auto; |
|||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue