Browse Source
- Resolves merge conflicts from master branch integration - Includes latest features and bug fixes from master - Maintains clean-db-disconnects branch functionality Files affected: Multiple components, views, and utilities Timestamp: Wed Oct 22 07:26:21 AM UTC 2025pull/204/head
78 changed files with 3002 additions and 942 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> |
@ -0,0 +1,88 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
class="min-h-screen bg-gray-50 flex flex-col justify-start pt-2 sm:px-6 lg:px-8" |
||||
|
> |
||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-md"> |
||||
|
<div class="text-center"> |
||||
|
<div class="mx-auto h-24 w-24 text-gray-400"> |
||||
|
<svg |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
> |
||||
|
<circle cx="12" cy="12" r="10" stroke-width="1.5" /> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M12 8v4m0 4h.01" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<h1 class="mt-4 text-3xl font-extrabold text-gray-900">Not Found</h1> |
||||
|
<p class="text-sm text-gray-600"> |
||||
|
The page you're looking for doesn't exist. |
||||
|
</p> |
||||
|
<div class="mt-1"> |
||||
|
<button |
||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" |
||||
|
@click="goBack" |
||||
|
> |
||||
|
<svg |
||||
|
class="-ml-1 mr-2 h-5 w-5" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M10 19l-7-7m0 0l7-7m-7 7h18" |
||||
|
/> |
||||
|
</svg> |
||||
|
Go Back |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="mt-16"> |
||||
|
<button |
||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" |
||||
|
@click="goHome" |
||||
|
> |
||||
|
<svg |
||||
|
class="-ml-1 mr-2 h-5 w-5" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
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" |
||||
|
></path> |
||||
|
</svg> |
||||
|
Go Home |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { useRouter } from "vue-router"; |
||||
|
|
||||
|
const router = useRouter(); |
||||
|
|
||||
|
const goHome = () => { |
||||
|
router.push("/"); |
||||
|
}; |
||||
|
|
||||
|
const goBack = () => { |
||||
|
router.go(-1); |
||||
|
}; |
||||
|
</script> |
Loading…
Reference in new issue