feat: implement member visibility dialog with checkbox selection and refresh #208
Open
jose
wants to merge 9 commits from meeting-members-set-visibility
into master
6 changed files with 650 additions and 114 deletions
@ -0,0 +1,333 @@ |
|||
<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"> |
|||
Set Visibility to Meeting Members |
|||
</h3> |
|||
<p class="text-sm mb-4"> |
|||
Would you like to <b>make your activities visible</b> to the following |
|||
members? (This will also add them as contacts if they aren't already.) |
|||
</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="membersData && 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="isAllSelected" |
|||
:indeterminate="isIndeterminate" |
|||
@change="toggleSelectAll" |
|||
/> |
|||
Select All |
|||
</label> |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<!-- Dynamic data from MembersList --> |
|||
<tr v-if="!membersData || 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 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="isMemberSelected(member.did)" |
|||
@change="toggleMemberSelection(member.did)" |
|||
/> |
|||
{{ member.name || SOMEONE_UNNAMED }} |
|||
</label> |
|||
|
|||
<!-- 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" |
|||
/> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="space-y-2"> |
|||
<button |
|||
v-if="membersData && 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="setVisibilityForSelectedMembers" |
|||
> |
|||
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="cancel" |
|||
> |
|||
{{ |
|||
membersData && membersData.length > 0 ? "Maybe Later" : "Cancel" |
|||
}} |
|||
</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 } from "@/libs/endorserServer"; |
|||
import { createNotifyHelpers } from "@/utils/notify"; |
|||
|
|||
interface MemberData { |
|||
did: string; |
|||
name: string; |
|||
isContact: boolean; |
|||
member: { |
|||
memberId: string; |
|||
}; |
|||
} |
|||
|
|||
@Component({ |
|||
mixins: [PlatformServiceMixin], |
|||
}) |
|||
export default class SetBulkVisibilityDialog extends Vue { |
|||
@Prop({ default: false }) visible!: boolean; |
|||
@Prop({ default: () => [] }) membersData!: MemberData[]; |
|||
@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.membersData || this.membersData.length === 0) return false; |
|||
return this.membersData.every((member) => |
|||
this.selectedMembers.includes(member.did), |
|||
); |
|||
} |
|||
|
|||
get isIndeterminate() { |
|||
if (!this.membersData || this.membersData.length === 0) return false; |
|||
const selectedCount = this.membersData.filter((member) => |
|||
this.selectedMembers.includes(member.did), |
|||
).length; |
|||
return selectedCount > 0 && selectedCount < this.membersData.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.membersData.map((member) => member.did); |
|||
} |
|||
|
|||
resetSelection() { |
|||
this.selectedMembers = []; |
|||
this.selectionInitialized = false; |
|||
} |
|||
|
|||
toggleSelectAll() { |
|||
if (!this.membersData || this.membersData.length === 0) return; |
|||
|
|||
if (this.isAllSelected) { |
|||
// Deselect all |
|||
this.selectedMembers = []; |
|||
} else { |
|||
// Select all |
|||
this.selectedMembers = this.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); |
|||
} |
|||
|
|||
async setVisibilityForSelectedMembers() { |
|||
try { |
|||
const selectedMembers = this.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, |
|||
); |
|||
|
|||
// Emit success event |
|||
this.$emit("success", successCount); |
|||
this.close(); |
|||
} 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; |
|||
} |
|||
} |
|||
|
|||
showContactInfo() { |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "info", |
|||
title: "Contact Info", |
|||
text: "This user is already your contact, but your activities are not visible to them yet.", |
|||
}, |
|||
5000, |
|||
); |
|||
} |
|||
|
|||
close() { |
|||
this.resetSelection(); |
|||
this.$emit("close"); |
|||
} |
|||
|
|||
cancel() { |
|||
this.close(); |
|||
} |
|||
} |
|||
</script> |
Loading…
Reference in new issue