Browse Source

refactor: extract SetVisibilityDialog into standalone component

- 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.
pull/208/head
Jose Olarte III 3 days ago
parent
commit
ea19195850
  1. 328
      src/App.vue
  2. 45
      src/components/MembersList.vue
  3. 333
      src/components/SetVisibilityDialog.vue

328
src/App.vue

@ -352,130 +352,6 @@
</div>
</div>
</div>
<div
v-if="notification.type === 'set-visibility-meeting-members'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-lg mx-auto my-4 overflow-y-scroll bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 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(notification)"
class="mb-4"
>
<table
class="w-full border-collapse border border-slate-300 text-sm text-start"
>
<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="isAllSelected(notification)"
:indeterminate="isIndeterminate(notification)"
@change="toggleSelectAll(notification)"
/>
Select All
</label>
</th>
</tr>
</thead>
<tbody>
<!-- 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="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="
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="
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="closeDialog(notification, close)"
>
{{
notification.membersData &&
notification.membersData.length > 0
? "Maybe Later"
: "Cancel"
}}
</button>
</div>
</div>
</div>
</div>
</div>
</Notification>
</div>
@ -488,9 +364,6 @@ 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;
@ -505,12 +378,6 @@ 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 = "";
@ -525,201 +392,6 @@ export default class App extends Vue {
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(
{
group: "alert",
type: "info",
title: "Contact Info",
text: "This user is already your contact, but your activities are not visible to them yet.",
},
5000,
);
}
async turnOffNotifications(
notification: NotificationIface,
): Promise<boolean> {

45
src/components/MembersList.vue

@ -180,6 +180,15 @@
</p>
</div>
</div>
<!-- Set Visibility Dialog Component -->
<SetVisibilityDialog
:visible="showSetVisibilityDialog"
:members-data="visibilityDialogMembers"
:active-did="activeDid"
:api-server="apiServer"
@close="closeSetVisibilityDialog"
/>
</template>
<script lang="ts">
@ -202,6 +211,7 @@ import {
NOTIFY_CONTINUE_WITHOUT_ADDING,
} from "@/constants/notifications";
import { SOMEONE_UNNAMED } from "@/constants/entities";
import SetVisibilityDialog from "./SetVisibilityDialog.vue";
interface Member {
admitted: boolean;
@ -217,6 +227,9 @@ interface DecryptedMember {
}
@Component({
components: {
SetVisibilityDialog,
},
mixins: [PlatformServiceMixin],
})
export default class MembersList extends Vue {
@ -243,6 +256,15 @@ export default class MembersList extends Vue {
missingMyself = false;
activeDid = "";
apiServer = "";
// Set Visibility Dialog state
showSetVisibilityDialog = false;
visibilityDialogMembers: Array<{
did: string;
name: string;
isContact: boolean;
member: { memberId: string };
}> = [];
contacts: Array<Contact> = [];
// Auto-refresh functionality
@ -573,18 +595,9 @@ export default class MembersList extends Vue {
// Filter members to show only those who need visibility set
const membersForVisibility = this.getMembersForVisibility();
this.$notify(
{
group: "modal",
type: "set-visibility-meeting-members",
membersData: membersForVisibility, // Pass the filtered members data
callback: async () => {
// Refresh data when dialog is closed (regardless of action taken)
await this.refreshData();
},
},
-1,
); // -1 means no auto-dismiss, stays open until user acts
// Open the dialog directly
this.visibilityDialogMembers = membersForVisibility;
this.showSetVisibilityDialog = true;
}
startAutoRefresh() {
@ -629,6 +642,14 @@ export default class MembersList extends Vue {
this.startAutoRefresh();
}
// Set Visibility Dialog methods
closeSetVisibilityDialog() {
this.showSetVisibilityDialog = false;
this.visibilityDialogMembers = [];
// Refresh data when dialog is closed
this.refreshData();
}
beforeDestroy() {
this.stopAutoRefresh();
}

333
src/components/SetVisibilityDialog.vue

@ -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 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>
Loading…
Cancel
Save