feat: implement member visibility dialog with checkbox selection and refresh #208
316
src/App.vue
316
src/App.vue
@@ -371,32 +371,65 @@
|
||||
</p>
|
||||
|
||||
<!-- Custom table area - you can customize this -->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
v-if="shouldInitializeSelection(notification)"
|
||||
class="mb-4"
|
||||
>
|
||||
<table
|
||||
class="w-full border-collapse border border-slate-300 text-sm text-start"
|
||||
>
|
||||
<thead>
|
||||
<thead
|
||||
v-if="
|
||||
notification.membersData &&
|
||||
notification.membersData.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 />
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isAllSelected(notification)"
|
||||
:indeterminate="isIndeterminate(notification)"
|
||||
|
|
||||
@change="toggleSelectAll(notification)"
|
||||
/>
|
||||
Select All
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Sample data - replace with your actual data -->
|
||||
<tr>
|
||||
<!-- Dynamic data from MembersList -->
|
||||
<tr
|
||||
v-if="
|
||||
!notification.membersData ||
|
||||
notification.membersData.length === 0
|
||||
"
|
||||
>
|
||||
<td
|
||||
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
|
||||
>
|
||||
No members need visibility settings
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="member in notification.membersData || []"
|
||||
: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 />
|
||||
John Doe
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isMemberSelected(member.did)"
|
||||
@change="toggleMemberSelection(member.did)"
|
||||
/>
|
||||
{{ member.name || SOMEONE_UNNAMED }}
|
||||
</label>
|
||||
|
||||
<!-- Friend indicator -->
|
||||
<!-- Friend 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"
|
||||
@@ -404,62 +437,40 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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 />
|
||||
Jane Smith
|
||||
</label>
|
||||
|
||||
<!-- Friend indicator -->
|
||||
<font-awesome
|
||||
icon="user-circle"
|
||||
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
|
||||
@click="showContactInfo"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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 />
|
||||
Jim Beam
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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 />
|
||||
Susie Q
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md"
|
||||
v-if="
|
||||
notification.membersData &&
|
||||
notification.membersData.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="
|
||||
notification.onYes ? notification.onYes() : null;
|
||||
close(notification.id);
|
||||
setVisibilityForSelectedMembers(notification);
|
||||
closeDialog(notification, close);
|
||||
"
|
||||
>
|
||||
Set Visibility
|
||||
</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="close(notification.id)"
|
||||
@click="closeDialog(notification, close)"
|
||||
>
|
||||
Maybe Later
|
||||
{{
|
||||
notification.membersData &&
|
||||
notification.membersData.length > 0
|
||||
? "Maybe Later"
|
||||
: "Cancel"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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(
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="getMembersForVisibility().length > 0"
|
||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
title="Set visibility for meeting members"
|
||||
@click="showAddMembersNotification"
|
||||
@@ -162,6 +163,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="getMembersForVisibility().length > 0"
|
||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
title="Set visibility for meeting members"
|
||||
@click="showAddMembersNotification"
|
||||
@@ -264,6 +266,12 @@ export default class MembersList extends Vue {
|
||||
await this.loadContacts();
|
||||
}
|
||||
|
||||
async refreshData() {
|
||||
// Force refresh both contacts and members
|
||||
await this.loadContacts();
|
||||
await this.fetchMembers();
|
||||
}
|
||||
|
||||
async fetchMembers() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
@@ -391,6 +399,28 @@ export default class MembersList extends Vue {
|
||||
return this.contacts.find((contact) => contact.did === did);
|
||||
}
|
||||
|
||||
getMembersForVisibility() {
|
||||
return this.decryptedMembers
|
||||
.filter((member) => {
|
||||
// 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) => ({
|
||||
...member,
|
||||
isContact: !!this.getContactFor(member.did),
|
||||
contact: this.getContactFor(member.did),
|
||||
}));
|
||||
}
|
||||
|
||||
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
||||
const contact = this.getContactFor(decrMember.did);
|
||||
if (!decrMember.member.admitted && !contact) {
|
||||
@@ -530,14 +560,17 @@ export default class MembersList extends Vue {
|
||||
}
|
||||
|
||||
showAddMembersNotification() {
|
||||
// Filter members to show only those who need visibility set
|
||||
const membersForVisibility = this.getMembersForVisibility();
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "set-visibility-meeting-members",
|
||||
title: "Set Visibility for Meeting Members",
|
||||
onYes: async () => {
|
||||
// Handle the "Add Selected" action - you can implement the actual logic here
|
||||
console.log("User confirmed adding selected members as contacts");
|
||||
membersData: membersForVisibility, // Pass the filtered members data
|
||||
callback: async () => {
|
||||
// Refresh data when dialog is closed (regardless of action taken)
|
||||
await this.refreshData();
|
||||
},
|
||||
},
|
||||
-1,
|
||||
|
||||
@@ -59,7 +59,7 @@ export const PASSKEYS_ENABLED =
|
||||
export interface NotificationIface {
|
||||
group: string; // "alert" | "modal"
|
||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title: string;
|
||||
title?: string;
|
||||
text?: string;
|
||||
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
||||
noText?: string;
|
||||
@@ -68,4 +68,11 @@ export interface NotificationIface {
|
||||
onYes?: () => Promise<void>;
|
||||
promptToStopAsking?: boolean;
|
||||
yesText?: string;
|
||||
membersData?: Array<{
|
||||
member: { admitted: boolean; content: string; memberId: number };
|
||||
name: string;
|
||||
did: string;
|
||||
isContact: boolean;
|
||||
contact?: { did: string; name?: string; seesMe?: boolean };
|
||||
}>; // For passing member data to visibility dialog
|
||||
}
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2
|
||||
2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1
|
||||
1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
></path>
|
||||
</svg>
|
||||
Go Home
|
||||
|
||||
Reference in New Issue
Block a user
Since these are not used, I don't see any reason to add this section.
This might make these variables accessible in other components and your intent may be to load them in one place, which makes sense. However, I do see a few potential side-effects that I don't love:
There are multiple DB "settings" loads in many of these places (one here and and another in each specific View), so we would want to remove all the other loads to avoid so many DB loads.
If there are errors in the DB accesses, that shuts down the whole app, and I believe some pages like Help should have almost no logic so that people can get there even if errors happen elsewhere. (Maybe there are "catch" statements so that doesn't happen... I'd have to look.)
The variables are a bit magic: when you're in another class, it's hard to know from where these values are loaded. (It may still be worthwhile if it saves user error in all these places. Will ponder.)
So I look forward to your thoughts... if you think it's worthwhile, let me know... and we'd want to work on issue #1 across the codebase, so it might be worthwhile in a separate PR.
Thanks for catching this. It's leftover code from when the rest of the logic was housed in here. I'll clean that up.