Browse Source

feat: Add bulk admit pending members functionality

- Add "Admit Pending" button to admit all pending members at once
- Implement confirmation dialog with options to add members to contacts
- Hide button when no pending members exist
- Pause auto-refresh during dialog interaction for better UX
- Add notification constants for dialog text and actions
- Support both "Admit and Add to Contacts" and "Admit Only" workflows
- Include comprehensive error handling and success feedback

The dialog shows pending member count and allows users to choose whether
to add admitted members to their contacts list, streamlining the
admission process for organizers.
meeting-members-admission-improvements
Jose Olarte III 1 day ago
parent
commit
b6f9533f07
  1. 122
      src/components/MembersList.vue
  2. 8
      src/constants/notifications.ts
  3. 2
      src/libs/fontawesome.ts

122
src/components/MembersList.vue

@ -54,6 +54,15 @@
Refresh Refresh
<span class="text-xs">({{ countdownTimer }}s)</span> <span class="text-xs">({{ countdownTimer }}s)</span>
</button> </button>
<button
v-if="getPendingMembersCount() > 0"
class="text-sm bg-gradient-to-b from-blue-100 to-blue-200 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.2)] text-blue-800 px-3 py-1.5 rounded-md"
@click="showAdmitAllPendingDialog"
>
<font-awesome icon="circle-plus" class="text-blue-500" />
Admit Pending
</button>
</div> </div>
<ul <ul
v-if="membersToShow().length > 0" v-if="membersToShow().length > 0"
@ -86,8 +95,8 @@
/> />
<font-awesome <font-awesome
v-if="!member.member.admitted" v-if="!member.member.admitted"
icon="spinner" icon="hourglass-half"
class="fa-fw fa-spin-pulse text-slate-400" class="fa-fw text-slate-400"
/> />
{{ member.name || unnamedMember }} {{ member.name || unnamedMember }}
</h3> </h3>
@ -205,6 +214,7 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { import {
NOTIFY_ADD_CONTACT_FIRST, NOTIFY_ADD_CONTACT_FIRST,
NOTIFY_CONTINUE_WITHOUT_ADDING, NOTIFY_CONTINUE_WITHOUT_ADDING,
NOTIFY_ADMIT_ALL_PENDING,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { SOMEONE_UNNAMED } from "@/constants/entities"; import { SOMEONE_UNNAMED } from "@/constants/entities";
import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue"; import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue";
@ -744,6 +754,114 @@ export default class MembersList extends Vue {
this.startAutoRefresh(); this.startAutoRefresh();
} }
/**
* Get count of pending (non-admitted) members
*/
getPendingMembersCount(): number {
return this.decryptedMembers.filter(
(member) => !member.member.admitted && member.did !== this.activeDid,
).length;
}
/**
* Get list of pending members
*/
getPendingMembers(): DecryptedMember[] {
return this.decryptedMembers.filter(
(member) => !member.member.admitted && member.did !== this.activeDid,
);
}
/**
* Show the admit all pending members dialog
*/
showAdmitAllPendingDialog() {
const pendingCount = this.getPendingMembersCount();
if (pendingCount === 0) {
this.notify.info(
"There are no pending members to admit.",
TIMEOUTS.STANDARD,
);
return;
}
// Pause auto-refresh when dialog opens
this.stopAutoRefresh();
const dialogText = NOTIFY_ADMIT_ALL_PENDING.text.replace(
"{count}",
pendingCount.toString(),
);
this.$notify(
{
group: "modal",
type: "confirm",
title: NOTIFY_ADMIT_ALL_PENDING.title,
text: dialogText,
yesText: NOTIFY_ADMIT_ALL_PENDING.yesText,
noText: NOTIFY_ADMIT_ALL_PENDING.noText,
onYes: async () => {
await this.admitAllPendingMembers(true); // true = add to contacts
// Resume auto-refresh after action
this.startAutoRefresh();
},
onNo: async () => {
await this.admitAllPendingMembers(false); // false = don't add to contacts
// Resume auto-refresh after action
this.startAutoRefresh();
},
onCancel: async () => {
// Do nothing - user cancelled
// Resume auto-refresh after cancellation
this.startAutoRefresh();
},
},
TIMEOUTS.MODAL,
);
}
/**
* Admit all pending members with optional contact addition
*/
async admitAllPendingMembers(addToContacts: boolean) {
const pendingMembers = this.getPendingMembers();
if (pendingMembers.length === 0) {
return;
}
try {
// Process each pending member
for (const member of pendingMembers) {
// Add to contacts if requested and not already a contact
if (addToContacts && !this.getContactFor(member.did)) {
await this.addAsContact(member);
}
// Admit the member
await this.toggleAdmission(member);
}
// Show success message
const contactMessage = addToContacts ? " and added to your contacts" : "";
this.notify.success(
`All ${pendingMembers.length} pending members have been admitted${contactMessage}.`,
TIMEOUTS.STANDARD,
);
} catch (error) {
this.$logAndConsole(
"Error admitting all pending members: " + errorStringForLog(error),
true,
);
this.notify.error(
"Failed to admit some members. Please try again.",
TIMEOUTS.LONG,
);
}
}
beforeDestroy() { beforeDestroy() {
this.stopAutoRefresh(); this.stopAutoRefresh();
} }

8
src/constants/notifications.ts

@ -471,6 +471,14 @@ export const NOTIFY_CONTINUE_WITHOUT_ADDING = {
noText: "No, Cancel", noText: "No, Cancel",
}; };
// Used in: MembersList.vue (complex modal for admitting all pending members)
export const NOTIFY_ADMIT_ALL_PENDING = {
title: "Admit All Pending Members",
text: "You are about to admit {count} pending member/s. Would you also like to add them to your Contacts list?",
yesText: "Admit and Add to Contacts",
noText: "Admit Only",
};
// HelpNotificationsView.vue specific constants // HelpNotificationsView.vue specific constants
// Used in: HelpNotificationsView.vue (sendTestWebPushMessage method - not subscribed error) // Used in: HelpNotificationsView.vue (sendTestWebPushMessage method - not subscribed error)
export const NOTIFY_PUSH_NOT_SUBSCRIBED = { export const NOTIFY_PUSH_NOT_SUBSCRIBED = {

2
src/libs/fontawesome.ts

@ -60,6 +60,7 @@ import {
faHand, faHand,
faHandHoldingDollar, faHandHoldingDollar,
faHandHoldingHeart, faHandHoldingHeart,
faHourglassHalf,
faHouseChimney, faHouseChimney,
faImage, faImage,
faImagePortrait, faImagePortrait,
@ -156,6 +157,7 @@ library.add(
faHand, faHand,
faHandHoldingDollar, faHandHoldingDollar,
faHandHoldingHeart, faHandHoldingHeart,
faHourglassHalf,
faHouseChimney, faHouseChimney,
faImage, faImage,
faImagePortrait, faImagePortrait,

Loading…
Cancel
Save