From 07b538cadcd136fe1c203a501b7bb8714eb4353c Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 14 Oct 2025 21:21:35 +0800 Subject: [PATCH] feat: implement member visibility dialog with checkbox selection and refresh - Add "Set Visibility" dialog for meeting members who need visibility settings - Filter members to show only those not in contacts or without seesMe set - Implement checkbox selection with "Select All" functionality - Add reactive checkbox behavior with proper state management - Default all checkboxes to checked when dialog opens - Implement "Set Visibility" action to add contacts and set seesMe property - Add success notifications with count of affected members - Disable "Set Visibility" button when no members are selected - Use notification callbacks for data refresh - Hide "Set Visibility" buttons when no members need visibility settings - Add proper dialog state management and cleanup - Ensure dialog closes before triggering data refresh to prevent stale states The implementation provides a smooth user experience for managing member visibility settings with proper state synchronization between components. --- src/App.vue | 312 ++++++++++++++++++++++++++++----- src/components/MembersList.vue | 41 ++++- src/constants/app.ts | 9 +- src/views/NotFoundView.vue | 4 +- 4 files changed, 312 insertions(+), 54 deletions(-) diff --git a/src/App.vue b/src/App.vue index c4638fe0..c4e943a0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -371,49 +371,65 @@

-
+
- + - - - + - + - - - - - -
-
- - - - -
+ +
+ No members need visibility settings
- +
-
- -
-
-
- -
-
@@ -477,6 +488,9 @@ import { Vue, Component } from "vue-facing-decorator"; import { NotificationIface } from "./constants/app"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { logger } from "./utils/logger"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { SOMEONE_UNNAMED } from "@/constants/entities"; +import { setVisibilityUtil } from "./libs/endorserServer"; interface Settings { notifyingNewActivityTime?: string; @@ -491,6 +505,208 @@ export default class App extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; stopAsking = false; + selectedMembers: string[] = []; + selectionInitialized = false; + + get hasSelectedMembers() { + return this.selectedMembers.length > 0; + } + activeDid = ""; + apiServer = ""; + + async created() { + // Initialize settings + const settings = await this.$accountSettings(); + this.apiServer = settings.apiServer || ""; + + // Get activeDid from active_identity table + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const activeIdentity = await (this as any).$getActiveIdentity(); + this.activeDid = activeIdentity.activeDid || ""; + } + + isAllSelected(notification: NotificationIface) { + const membersData = notification?.membersData || []; + if (!membersData || membersData.length === 0) return false; + return membersData.every((member) => + this.selectedMembers.includes(member.did), + ); + } + + isIndeterminate(notification: NotificationIface) { + const membersData = notification?.membersData || []; + if (!membersData || membersData.length === 0) return false; + const selectedCount = membersData.filter((member) => + this.selectedMembers.includes(member.did), + ).length; + return selectedCount > 0 && selectedCount < membersData.length; + } + + toggleSelectAll(notification: NotificationIface) { + const membersData = notification?.membersData || []; + if (!membersData || membersData.length === 0) return; + + if (this.isAllSelected(notification)) { + // Deselect all + this.selectedMembers = []; + } else { + // Select all + this.selectedMembers = membersData.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); + } + + shouldInitializeSelection(notification: NotificationIface) { + // This method will initialize selection when the dialog opens + if ( + notification?.type === "set-visibility-meeting-members" && + !this.selectionInitialized + ) { + this.initializeSelection(notification); + this.selectionInitialized = true; + } + return true; + } + + initializeSelection(notification: NotificationIface) { + // Reset selection when dialog opens + this.selectedMembers = []; + // Select all by default + const membersData = notification?.membersData || []; + this.selectedMembers = membersData.map((member) => member.did); + } + + resetSelection() { + this.selectedMembers = []; + this.selectionInitialized = false; + } + + async setVisibilityForSelectedMembers(notification: NotificationIface) { + try { + const membersData = notification?.membersData || []; + const selectedMembers = membersData.filter((member) => + this.selectedMembers.includes(member.did), + ); + + let successCount = 0; + + for (const member of selectedMembers) { + try { + // If they're not a contact yet, add them as a contact first + if (!member.isContact) { + await this.addAsContact(member); + } + + // Set their seesMe to true + await this.updateContactVisibility(member.did, true); + + successCount++; + } 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: "Visibility Set Successfully", + text: `Visibility set for ${successCount} member${successCount === 1 ? "" : "s"}.`, + }, + 5000, + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error setting visibility:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to set visibility for some members. Please try again.", + }, + 5000, + ); + } + } + + 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; + } + } + + async closeDialog( + notification: NotificationIface, + closeFn: (id: string) => void, + ) { + this.resetSelection(); + + // Close the notification first + closeFn(notification.id); + + // Then call the callback after a short delay to ensure dialog is closed + setTimeout(async () => { + if (notification.callback) { + await notification.callback(); + } + }, 100); + } showContactInfo() { this.$notify( diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index 65da0c0c..ef3106fe 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -67,6 +67,7 @@