forked from jsnbuchanan/crowd-funder-for-time-pwa
- Extract "Set Visibility to Meeting Members" dialog from App.vue into dedicated SetVisibilityDialog.vue component - Move dialog logic directly to MembersList.vue for better component coupling - Remove unnecessary intermediate state management from App.vue - Clean up redundant style definitions (rely on existing Tailwind CSS classes) - Remove unused logger imports and debug functions - Add explanatory comment for Vue template constant pattern This improves maintainability by isolating dialog functionality and follows established component patterns in the codebase.
334 lines
9.7 KiB
Vue
334 lines
9.7 KiB
Vue
<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 SetVisibilityDialog 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>
|