Browse Source

refactor: simplify logic for opening onboarding dialogs

pull/211/head
Trent Larson 1 week ago
parent
commit
7ea6a2ef69
  1. 123
      src/components/AdmitPendingMembersDialog.vue
  2. 311
      src/components/MembersList.vue
  3. 7
      src/components/SetBulkVisibilityDialog.vue
  4. 2
      src/views/OnboardMeetingListView.vue

123
src/components/AdmitPendingMembersDialog.vue

@ -12,7 +12,7 @@
</p> </p>
<!-- Custom table area - you can customize this --> <!-- Custom table area - you can customize this -->
<div v-if="shouldInitializeSelection" class="mb-4"> <div class="mb-4">
<table <table
class="w-full border-collapse border border-slate-300 text-sm text-start" class="w-full border-collapse border border-slate-300 text-sm text-start"
> >
@ -93,22 +93,9 @@
? 'bg-green-600 text-white cursor-pointer' ? 'bg-green-600 text-white cursor-pointer'
: 'bg-slate-400 text-slate-200 cursor-not-allowed', : 'bg-slate-400 text-slate-200 cursor-not-allowed',
]" ]"
@click="admitAndSetVisibility" @click="admitWithVisibility"
> >
Admit + Add Contacts Admit + Add Contact
</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 Only
</button> </button>
<button <button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@ -143,7 +130,6 @@ interface PendingMemberData {
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class AdmitPendingMembersDialog extends Vue { export default class AdmitPendingMembersDialog extends Vue {
@Prop({ default: false }) visible!: boolean;
@Prop({ default: () => [] }) pendingMembersData!: PendingMemberData[]; @Prop({ default: () => [] }) pendingMembersData!: PendingMemberData[];
@Prop({ default: "" }) activeDid!: string; @Prop({ default: "" }) activeDid!: string;
@Prop({ default: "" }) apiServer!: string; @Prop({ default: "" }) apiServer!: string;
@ -159,7 +145,7 @@ export default class AdmitPendingMembersDialog extends Vue {
// Component state // Component state
selectedMembers: string[] = []; selectedMembers: string[] = [];
selectionInitialized = false; visible = false;
// Constants // Constants
// In Vue templates, imported constants need to be explicitly made available to the template // In Vue templates, imported constants need to be explicitly made available to the template
@ -186,29 +172,23 @@ export default class AdmitPendingMembersDialog extends Vue {
return selectedCount > 0 && selectedCount < this.pendingMembersData.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() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
} }
initializeSelection() { open() {
// Reset selection when dialog opens this.visible = true;
this.selectedMembers = [];
// Select all by default // Select all by default
this.selectedMembers = this.pendingMembersData.map((member) => member.did); this.selectedMembers = this.pendingMembersData.map((member) => member.did);
} }
resetSelection() { close(notSelectedMemberDids: string[]) {
this.selectedMembers = []; this.visible = false;
this.selectionInitialized = false; this.$emit("close", { notSelectedMemberDids: notSelectedMemberDids });
}
cancel() {
this.close(this.pendingMembersData.map((member) => member.did));
} }
toggleSelectAll() { toggleSelectAll() {
@ -239,11 +219,14 @@ export default class AdmitPendingMembersDialog extends Vue {
return this.selectedMembers.includes(memberDid); return this.selectedMembers.includes(memberDid);
} }
async admitAndSetVisibility() { async admitWithVisibility() {
try { try {
const selectedMembers = this.pendingMembersData.filter((member) => const selectedMembers = this.pendingMembersData.filter((member) =>
this.selectedMembers.includes(member.did), this.selectedMembers.includes(member.did),
); );
const notSelectedMembers = this.pendingMembersData.filter((member) =>
!this.selectedMembers.includes(member.did),
);
let admittedCount = 0; let admittedCount = 0;
let contactAddedCount = 0; let contactAddedCount = 0;
@ -279,69 +262,10 @@ export default class AdmitPendingMembersDialog extends Vue {
title: "Members Admitted Successfully", 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"}.`, text: `Admitted ${admittedCount} member${admittedCount === 1 ? "" : "s"}, added ${contactAddedCount} as contact${contactAddedCount === 1 ? "" : "s"}, and set visibility for ${visibilitySetCount} member${visibilitySetCount === 1 ? "" : "s"}.`,
}, },
5000, 10000,
); );
// Emit success event this.close(notSelectedMembers.map((member) => member.did));
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) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Error admitting members:", error); console.error("Error admitting members:", error);
@ -433,15 +357,6 @@ export default class AdmitPendingMembersDialog extends Vue {
5000, 5000,
); );
} }
close() {
this.resetSelection();
this.$emit("close");
}
cancel() {
this.close();
}
} }
</script> </script>

311
src/components/MembersList.vue

@ -179,21 +179,18 @@
<!-- Admit Pending Members Dialog Component --> <!-- Admit Pending Members Dialog Component -->
<AdmitPendingMembersDialog <AdmitPendingMembersDialog
:visible="showAdmitPendingDialog" ref="admitPendingMembersDialog"
:pending-members-data="pendingMembersData"
:active-did="activeDid" :active-did="activeDid"
:api-server="apiServer" :api-server="apiServer"
:pending-members-data="pendingMembersData"
@close="closeAdmitPendingDialog" @close="closeAdmitPendingDialog"
@success="onAdmitPendingSuccess"
/> />
<!-- Set Visibility Dialog Component -->
<SetBulkVisibilityDialog <SetBulkVisibilityDialog
:visible="showSetVisibilityDialog" :visible="visibleBulkVisibilityDialog"
:members-data="visibilityDialogMembers"
:active-did="activeDid" :active-did="activeDid"
:api-server="apiServer" :api-server="apiServer"
@close="closeSetVisibilityDialog" :members-data="pendingMembersData"
@close="closeSetBulkVisibilityDialog"
/> />
</template> </template>
@ -217,8 +214,8 @@ import {
NOTIFY_CONTINUE_WITHOUT_ADDING, NOTIFY_CONTINUE_WITHOUT_ADDING,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { SOMEONE_UNNAMED } from "@/constants/entities"; import { SOMEONE_UNNAMED } from "@/constants/entities";
import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue";
import AdmitPendingMembersDialog from "./AdmitPendingMembersDialog.vue"; import AdmitPendingMembersDialog from "./AdmitPendingMembersDialog.vue";
import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue";
interface Member { interface Member {
admitted: boolean; admitted: boolean;
@ -235,8 +232,8 @@ interface DecryptedMember {
@Component({ @Component({
components: { components: {
SetBulkVisibilityDialog,
AdmitPendingMembersDialog, AdmitPendingMembersDialog,
SetBulkVisibilityDialog,
}, },
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
@ -255,6 +252,7 @@ export default class MembersList extends Vue {
return message; return message;
} }
contacts: Array<Contact> = [];
decryptedMembers: DecryptedMember[] = []; decryptedMembers: DecryptedMember[] = [];
firstName = ""; firstName = "";
isLoading = true; isLoading = true;
@ -266,32 +264,20 @@ export default class MembersList extends Vue {
apiServer = ""; apiServer = "";
// Admit Pending Members Dialog state // Admit Pending Members Dialog state
showAdmitPendingDialog = false;
pendingMembersData: Array<{ pendingMembersData: Array<{
did: string; did: string;
name: string; name: string;
isContact: boolean; isContact: boolean;
member: { memberId: string }; member: { memberId: string };
}> = []; }> = [];
admitDialogDismissed = false;
isManualRefresh = false;
// Set Visibility Dialog state visibleBulkVisibilityDialog = false;
showSetVisibilityDialog = false;
visibilityDialogMembers: Array<{
did: string;
name: string;
isContact: boolean;
member: { memberId: string };
}> = [];
contacts: Array<Contact> = [];
// Auto-refresh functionality // Auto-refresh functionality
countdownTimer = 10; countdownTimer = 10;
autoRefreshInterval: NodeJS.Timeout | null = null; autoRefreshInterval: NodeJS.Timeout | null = null;
lastRefreshTime = 0;
// Track previous visibility members to detect changes previousMemberDidsIgnored: string[] = [];
previousVisibilityMembers: string[] = [];
/** /**
* Get the unnamed member constant * Get the unnamed member constant
@ -312,33 +298,18 @@ export default class MembersList extends Vue {
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || ""; this.firstName = settings.firstName || "";
await this.fetchMembers();
await this.loadContacts();
// Start auto-refresh this.refreshData();
this.startAutoRefresh();
// Check if we should show the admit pending members dialog first
this.checkAndShowAdmitPendingDialog();
// If no pending members, check for visibility dialog
if (!this.showAdmitPendingDialog) {
this.checkAndShowVisibilityDialog();
}
} }
async refreshData() { async refreshData(showPendingEvenIfAllWereIgnored = false) {
// Force refresh both contacts and members // Force refresh both contacts and members
await this.loadContacts(); this.contacts = await this.$getAllContacts()
await this.fetchMembers(); await this.fetchMembers();
// Check if we should show the admit pending members dialog first // Check if we should show the admit pending members dialog first
this.checkAndShowAdmitPendingDialog(); this.checkAndShowAdmitPendingDialog(showPendingEvenIfAllWereIgnored);
// If no pending members, check for visibility dialog
if (!this.showAdmitPendingDialog) {
this.checkAndShowVisibilityDialog();
}
} }
async fetchMembers() { async fetchMembers() {
@ -487,15 +458,11 @@ export default class MembersList extends Vue {
} }
} }
async loadContacts() {
this.contacts = await this.$getAllContacts();
}
getContactFor(did: string): Contact | undefined { getContactFor(did: string): Contact | undefined {
return this.contacts.find((contact) => contact.did === did); return this.contacts.find((contact) => contact.did === did);
} }
getPendingMembers() { getPendingMembers(): { did: string; name: string; isContact: boolean; member: { memberId: string; }; }[] {
return this.decryptedMembers return this.decryptedMembers
.filter((member) => { .filter((member) => {
// Exclude the current user // Exclude the current user
@ -515,132 +482,64 @@ export default class MembersList extends Vue {
})); }));
} }
getMembersForVisibility() { getNonContactMembers(): { did: string; name: string; isContact: boolean; member: { memberId: string; }; }[] {
const membersForVisibility = this.decryptedMembers return this.decryptedMembers
.filter((member) => { .filter((member) => !this.getContactFor(member.did))
// Exclude the current user
if (member.did === this.activeDid) {
return false;
}
const contact = this.getContactFor(member.did);
// Include members who:
// 1. Haven't been added as contacts yet, OR
// 2. Are contacts but don't have visibility set (seesMe property)
return !contact || !contact.seesMe;
})
.map((member) => ({ .map((member) => ({
did: member.did, did: member.did,
name: member.name, name: member.name,
isContact: !!this.getContactFor(member.did), isContact: false,
member: { member: {
memberId: member.member.memberId.toString(), memberId: member.member.memberId.toString(),
}, },
})); }));
// Deduplicate members by DID to prevent duplicate entries
const uniqueMembers = membersForVisibility.filter(
(member, index, self) =>
index === self.findIndex((m) => m.did === member.did),
);
return uniqueMembers;
} }
/** /**
* Check if we should show the visibility dialog * Show the admit pending members dialog if conditions are met
* Returns true if there are members for visibility and either:
* - This is the first time (no previous members tracked), OR
* - New members have been added since last check (not removed), OR
* - This is a manual refresh (isManualRefresh flag is set)
*/ */
shouldShowVisibilityDialog(): boolean { checkAndShowAdmitPendingDialog(showPendingEvenIfAllWereIgnored = false) {
// Only show for members who can see other members (i.e., they are in the decrypted members list) const pendingMembers =
const currentUserMember = this.decryptedMembers.find( this.isOrganizer ? this.getPendingMembers() : this.getNonContactMembers();
(member) => member.did === this.activeDid, if (pendingMembers.length === 0) {
); this.startAutoRefresh();
return;
// If the current user is not in the decrypted members list, they can't see anyone
if (!currentUserMember) {
return false;
}
const currentMembers = this.getMembersForVisibility();
if (currentMembers.length === 0) {
return false;
} }
if (!showPendingEvenIfAllWereIgnored) {
// If no previous members tracked, show dialog // only show if there are pending members that have not been ignored
if (this.previousVisibilityMembers.length === 0) { const pendingMembersNotIgnored =
return true; pendingMembers.filter(
(member) => !this.previousMemberDidsIgnored.includes(member.did)
);
if (pendingMembersNotIgnored.length === 0) {
this.startAutoRefresh();
// everyone waiting has been ignored
return;
}
} }
this.stopAutoRefresh();
// If this is a manual refresh, always show dialog if there are members this.pendingMembersData = pendingMembers;
if (this.isManualRefresh) { if (this.isOrganizer) {
return true; (this.$refs.admitPendingMembersDialog as AdmitPendingMembersDialog).open();
} else {
this.visibleBulkVisibilityDialog = true;
} }
// Check if new members have been added (not just any change)
const currentMemberIds = currentMembers.map((m) => m.did);
const previousMemberIds = this.previousVisibilityMembers;
// Find new members (members in current but not in previous)
const newMembers = currentMemberIds.filter(
(id) => !previousMemberIds.includes(id),
);
// Only show dialog if there are new members added
return newMembers.length > 0;
} }
/** // Admit Pending Members Dialog methods
* Update the tracking of previous visibility members async closeAdmitPendingDialog(result: { notSelectedMemberDids: string[] } | undefined) {
*/ this.pendingMembersData = [];
updatePreviousVisibilityMembers() { this.previousMemberDidsIgnored = result?.notSelectedMemberDids || [];
const currentMembers = this.getMembersForVisibility();
this.previousVisibilityMembers = currentMembers.map((m) => m.did);
}
/**
* Check if we should show the admit pending members dialog
*/
shouldShowAdmitPendingDialog(): boolean {
// Don't show if already dismissed
if (this.admitDialogDismissed) {
return false;
}
// Only show for the organizer of the meeting
if (!this.isOrganizer) {
return false;
}
const pendingMembers = this.getPendingMembers(); await this.refreshData();
return pendingMembers.length > 0;
} }
/** async closeSetBulkVisibilityDialog(result: { notSelectedMemberDids: string[] } | undefined) {
* Show the admit pending members dialog if conditions are met this.visibleBulkVisibilityDialog = false;
*/ this.pendingMembersData = [];
checkAndShowAdmitPendingDialog() { this.previousMemberDidsIgnored = result?.notSelectedMemberDids || [];
if (this.shouldShowAdmitPendingDialog()) {
this.showAdmitPendingDialogMethod();
} else {
// Ensure dialog state is false when no pending members
this.showAdmitPendingDialog = false;
}
}
/** await this.refreshData();
* Show the visibility dialog if conditions are met
*/
checkAndShowVisibilityDialog() {
if (this.shouldShowVisibilityDialog()) {
this.showSetBulkVisibilityDialog();
}
this.updatePreviousVisibilityMembers();
} }
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) { checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
@ -781,51 +680,19 @@ export default class MembersList extends Vue {
} }
} }
showAdmitPendingDialogMethod() {
// Filter members to show only pending (non-admitted) members
const pendingMembers = this.getPendingMembers();
// Only show dialog if there are pending members
if (pendingMembers.length === 0) {
this.showAdmitPendingDialog = false;
return;
}
// Pause auto-refresh when dialog opens
this.stopAutoRefresh();
// Open the dialog directly
this.pendingMembersData = pendingMembers;
this.showAdmitPendingDialog = true;
}
showSetBulkVisibilityDialog() {
// Filter members to show only those who need visibility set
const membersForVisibility = this.getMembersForVisibility();
// Pause auto-refresh when dialog opens
this.stopAutoRefresh();
// Open the dialog directly
this.visibilityDialogMembers = membersForVisibility;
this.showSetVisibilityDialog = true;
// Reset manual refresh flag after dialog is shown
this.isManualRefresh = false;
}
startAutoRefresh() { startAutoRefresh() {
let lastRefreshTime = Date.now(); this.lastRefreshTime = Date.now();
this.countdownTimer = 10; this.countdownTimer = 10;
this.autoRefreshInterval = setInterval(() => { this.autoRefreshInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
const timeSinceLastRefresh = (now - lastRefreshTime) / 1000; const timeSinceLastRefresh = (now - this.lastRefreshTime) / 1000;
if (timeSinceLastRefresh >= 10) { if (timeSinceLastRefresh >= 10) {
// Time to refresh // Time to refresh
this.refreshData(); this.refreshData();
lastRefreshTime = now; this.lastRefreshTime = now;
this.countdownTimer = 10; this.countdownTimer = 10;
} else { } else {
// Update countdown // Update countdown
@ -851,68 +718,8 @@ export default class MembersList extends Vue {
this.autoRefreshInterval = null; this.autoRefreshInterval = null;
} }
// Set manual refresh flag
this.isManualRefresh = true;
// Reset the dismissed flag on manual refresh
this.admitDialogDismissed = false;
// Trigger immediate refresh // Trigger immediate refresh
await this.refreshData(); await this.refreshData(true);
// Only start auto-refresh if no dialogs are showing
if (!this.showAdmitPendingDialog && !this.showSetVisibilityDialog) {
this.startAutoRefresh();
}
}
// Admit Pending Members Dialog methods
async closeAdmitPendingDialog() {
this.showAdmitPendingDialog = false;
this.pendingMembersData = [];
this.admitDialogDismissed = true;
// Handle manual refresh flow
if (this.isManualRefresh) {
await this.handleManualRefreshFlow();
this.isManualRefresh = false;
} else {
// Normal flow: refresh data and resume auto-refresh
this.refreshData();
this.startAutoRefresh();
}
}
async handleManualRefreshFlow() {
// Refresh data to reflect any changes made in the admit dialog
await this.refreshData();
// Use the same logic as normal flow to check for visibility dialog
this.checkAndShowVisibilityDialog();
// If no visibility dialog was shown, resume auto-refresh
if (!this.showSetVisibilityDialog) {
this.startAutoRefresh();
}
}
async onAdmitPendingSuccess(_result: {
admittedCount: number;
contactAddedCount: number;
visibilitySetCount: number;
}) {
// After admitting pending members, close the admit dialog
// The visibility dialog will be handled by the closeAdmitPendingDialog flow
await this.closeAdmitPendingDialog();
}
// Set Visibility Dialog methods
closeSetVisibilityDialog() {
this.showSetVisibilityDialog = false;
this.visibilityDialogMembers = [];
// Refresh data when dialog is closed to reflect any changes made
this.refreshData();
// Resume auto-refresh when dialog is closed
this.startAutoRefresh();
} }
beforeDestroy() { beforeDestroy() {

7
src/components/SetBulkVisibilityDialog.vue

@ -213,6 +213,9 @@ export default class SetBulkVisibilityDialog extends Vue {
const selectedMembers = this.membersData.filter((member) => const selectedMembers = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did), this.selectedMembers.includes(member.did),
); );
const notSelectedMembers = this.membersData.filter((member) =>
!this.selectedMembers.includes(member.did),
);
let successCount = 0; let successCount = 0;
@ -246,7 +249,7 @@ export default class SetBulkVisibilityDialog extends Vue {
); );
// Emit success event // Emit success event
this.$emit("success", successCount); this.$emit("close", { notSelectedMemberDids: notSelectedMembers.map((member) => member.did) });
this.close(); this.close();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -323,7 +326,7 @@ export default class SetBulkVisibilityDialog extends Vue {
close() { close() {
this.resetSelection(); this.resetSelection();
this.$emit("close"); this.$emit("close", { notSelectedMemberDids: this.membersData.map((member) => member.did) });
} }
cancel() { cancel() {

2
src/views/OnboardMeetingListView.vue

@ -77,7 +77,7 @@
v-if="meetings.length === 0 && !isRegistered" v-if="meetings.length === 0 && !isRegistered"
class="text-center text-gray-500 py-8" class="text-center text-gray-500 py-8"
> >
No onboarding meetings available No onboarding meetings are available
</p> </p>
</div> </div>

Loading…
Cancel
Save