Browse Source

split out group-meeting member list into a separate component, and fix some edit/create mode titles

master
Trent Larson 4 days ago
parent
commit
31d573684a
  1. 107
      src/components/MembersList.vue
  2. 2
      src/views/OnboardMeetingListView.vue
  3. 100
      src/views/OnboardMeetingMembersView.vue
  4. 76
      src/views/OnboardMeetingSetupView.vue

107
src/components/MembersList.vue

@ -0,0 +1,107 @@
<template>
<div class="space-y-4">
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<!-- Members List -->
<div v-else class="space-y-4">
<div v-for="member in decryptedMembers" :key="member.memberId"
class="p-4 bg-gray-50 rounded-lg">
<h3 class="text-lg font-medium">{{ member.name }}</h3>
<p class="text-sm text-gray-600">{{ member.did }}</p>
</div>
<p v-if="members.length === 0" class="text-center text-gray-500 py-4">
No members have joined this meeting yet
</p>
<p v-if="decryptedMembers.length < members.length" class="text-center text-red-600 py-4">
{{ decryptFailureMessage || "Your password failed. Please go back and try again." }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator';
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index';
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer';
import { decryptMessage } from '@/libs/crypto';
interface Member {
memberId: number;
content: string;
}
interface DecryptedMember {
memberId: number;
name: string;
did: string;
}
@Component
export default class MembersList extends Vue {
@Prop({ required: true }) password!: string;
@Prop({ default: 'Your password failed. Please go back and try again.' }) decryptFailureMessage!: string;
decryptedMembers: DecryptedMember[] = [];
missingPassword = false;
isLoading = false;
members: Member[] = [];
activeDid = '';
apiServer = '';
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || '';
this.apiServer = settings.apiServer || '';
await this.fetchMembers();
}
async fetchMembers() {
this.isLoading = true;
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboardMembers/`,
{ headers }
);
if (response.data && response.data.data) {
this.members = response.data.data;
await this.decryptMemberContents();
}
} catch (error) {
logConsoleAndDb('Error fetching members: ' + errorStringForLog(error), true);
this.$emit('error', serverMessageForUser(error) || 'Failed to fetch members.');
} finally {
this.isLoading = false;
}
}
async decryptMemberContents() {
this.decryptedMembers = [];
if (!this.password) {
this.missingPassword = true;
return;
}
for (const member of this.members) {
try {
const decryptedContent = await decryptMessage(member.content, this.password);
const content = JSON.parse(decryptedContent);
this.decryptedMembers.push({
memberId: member.memberId,
name: content.name,
did: content.did,
});
} catch (error) {
// do nothing, relying on the count of members to determine if there was an error
}
}
}
}
</script>

2
src/views/OnboardMeetingListView.vue

@ -138,6 +138,8 @@ export default class OnboardMeetingListView extends Vue {
async submitPassword() { async submitPassword() {
if (!this.selectedMeeting) { if (!this.selectedMeeting) {
// this should never happen
logConsoleAndDb('No meeting selected when prompting for password, which should never happen.', true);
return; return;
} }

100
src/views/OnboardMeetingMembersView.vue

@ -17,13 +17,8 @@
Back to Meetings Back to Meetings
</button> </button>
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<!-- Error State --> <!-- Error State -->
<div v-else-if="errorMessage"> <div v-if="errorMessage">
<div class="text-center text-red-600 py-8"> <div class="text-center text-red-600 py-8">
{{ errorMessage }} {{ errorMessage }}
</div> </div>
@ -33,20 +28,12 @@
</div> </div>
<!-- Members List --> <!-- Members List -->
<div v-else class="space-y-4"> <MembersList
<div v-for="member in decryptedMembers" :key="member.memberId" v-else
class="p-4 bg-white rounded-lg shadow"> :password="password"
<h2 class="text-xl font-medium">{{ member.name }}</h2> :decrypt-failure-message="'That password failed. You may be in the wrong meeting. Go back and try again.'"
<p class="text-sm text-gray-600">DID: {{ member.did }}</p> @error="handleError"
</div> />
<p v-if="decryptedMembers.length === 0 && !decryptFailure" class="text-center text-gray-500 py-8">
No members found in this meeting
</p>
<p v-if="decryptFailure" class="text-center text-red-600 py-8">
That password failed. You may be in the wrong meeting. Go back and try again.
</p>
</div>
</section> </section>
</template> </template>
@ -54,37 +41,17 @@
import { Component, Vue } from 'vue-facing-decorator'; import { Component, Vue } from 'vue-facing-decorator';
import QuickNav from '@/components/QuickNav.vue'; import QuickNav from '@/components/QuickNav.vue';
import TopMessage from '@/components/TopMessage.vue'; import TopMessage from '@/components/TopMessage.vue';
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; import MembersList from '@/components/MembersList.vue';
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer';
import { decryptMessage } from '@/libs/crypto';
interface Member {
memberId: number;
content: string;
}
interface DecryptedMember {
memberId: number;
name: string;
did: string;
}
@Component({ @Component({
components: { components: {
QuickNav, QuickNav,
TopMessage, TopMessage,
MembersList,
}, },
}) })
export default class OnboardMeetingMembersView extends Vue { export default class OnboardMeetingMembersView extends Vue {
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void;
activeDid = '';
apiServer = '';
decryptedMembers: DecryptedMember[] = [];
decryptFailure = false;
errorMessage = ''; errorMessage = '';
isLoading = false;
members: Member[] = [];
get groupId(): string { get groupId(): string {
return this.$route.params.groupId as string; return this.$route.params.groupId as string;
@ -103,55 +70,10 @@ export default class OnboardMeetingMembersView extends Vue {
this.errorMessage = 'The password is missing. Go back and try again.'; this.errorMessage = 'The password is missing. Go back and try again.';
return; return;
} }
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || '';
this.apiServer = settings.apiServer || '';
await this.fetchMembers();
} }
async fetchMembers() { handleError(message: string) {
this.isLoading = true; this.errorMessage = message;
this.errorMessage = '';
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboardMembers/`,
{ headers }
);
if (response.data && response.data.data) {
this.members = response.data.data;
await this.decryptMemberContents();
} else {
throw { response: response };
}
} catch (error) {
logConsoleAndDb('Error fetching members: ' + errorStringForLog(error), true);
this.errorMessage = serverMessageForUser(error) || 'Failed to fetch members.';
} finally {
this.isLoading = false;
}
}
async decryptMemberContents() {
this.decryptedMembers = [];
for (const member of this.members) {
try {
const decryptedContent = await decryptMessage(member.content, this.password);
const content = JSON.parse(decryptedContent);
this.decryptedMembers.push({
memberId: member.memberId,
name: content.name,
did: content.did,
});
} catch (error) {
this.decryptFailure = true;
}
}
} }
} }
</script> </script>

76
src/views/OnboardMeetingSetupView.vue

@ -9,7 +9,10 @@
</h1> </h1>
<!-- Existing Meeting Section --> <!-- Existing Meeting Section -->
<div v-if="!isLoading && currentMeeting != null && !isEditingOrCreating()" class="mt-8 p-4 border rounded-lg bg-white shadow"> <div
v-if="!isLoading && currentMeeting != null && !isInEditOrCreateMode()"
class="mt-8 p-4 border rounded-lg bg-white shadow"
>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div class="flex items-center"> <div class="flex items-center">
<h2 class="text-2xl">Current Meeting</h2> <h2 class="text-2xl">Current Meeting</h2>
@ -19,7 +22,7 @@
title="Edit Meeting" title="Edit Meeting"
> >
<fa icon="pen" class="fa-fw" /> <fa icon="pen" class="fa-fw" />
<span class="sr-only">Edit Meeting</span> <span class="sr-only">{{ isInCreateMode() ? 'Create Meeting' : 'Edit Meeting' }}</span>
</button> </button>
</div> </div>
<button <button
@ -43,17 +46,10 @@
<div v-else class="text-red-600"> <div v-else class="text-red-600">
Your copy of the password is not saved. Edit the meeting, or delete it and create a new meeting. Your copy of the password is not saved. Edit the meeting, or delete it and create a new meeting.
</div> </div>
<router-link
:to="`/onboard-meeting-members/${currentMeeting.groupId}?password=${encodeURIComponent(currentMeeting.password || '')}`"
class="mt-4 inline-block px-4 py-2 text-blue-600"
target="_blank"
>
Open page that meeting members see
</router-link>
</div> </div>
</div> </div>
<!-- Delete Confirmation Dialog --> <!-- Delete Confirmation Dialog -->
<div v-if="showDeleteConfirm" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"> <div v-if="showDeleteConfirm" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg p-6 max-w-sm w-full"> <div class="bg-white rounded-lg p-6 max-w-sm w-full">
@ -77,10 +73,13 @@
</div> </div>
<!-- Create/Edit Meeting Form --> <!-- Create/Edit Meeting Form -->
<div v-if="!isLoading && newOrUpdatedMeeting != null" class="mt-8"> <div
<h2 class="text-2xl mb-4">{{ isEditingOrCreating() ? 'Edit Meeting' : 'Create New Meeting' }}</h2> v-if="!isLoading && isInEditOrCreateMode() && newOrUpdatedMeeting != null /* duplicate check is for typechecks */"
class="mt-8"
>
<h2 class="text-2xl mb-4">{{ isInCreateMode() ? 'Create New Meeting' : 'Edit Meeting' }}</h2>
<!-- This is my first form. Not sure whether I like it or not; gotta see if the browser benefits extend to the native app. --> <!-- This is my first form. Not sure whether I like it or not; gotta see if the browser benefits extend to the native app. -->
<form @submit.prevent="isEditingOrCreating() ? updateMeeting() : createMeeting()" class="space-y-4"> <form @submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()" class="space-y-4">
<div> <div>
<label for="meetingName" class="block text-sm font-medium text-gray-700">Meeting Name</label> <label for="meetingName" class="block text-sm font-medium text-gray-700">Meeting Name</label>
<input <input
@ -134,10 +133,10 @@
class="w-full bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md hover:from-green-500 hover:to-green-800" class="w-full bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md hover:from-green-500 hover:to-green-800"
:disabled="isLoading" :disabled="isLoading"
> >
{{ isLoading ? (isEditingOrCreating() ? 'Updating...' : 'Creating...') : (isEditingOrCreating() ? 'Update Meeting' : 'Create Meeting') }} {{ isLoading ? (isInCreateMode() ? 'Creating...' : 'Updating...' ) : (isInCreateMode() ? 'Create Meeting' : 'Update Meeting') }}
</button> </button>
<button <button
v-if="isEditingOrCreating()" v-if="isInEditOrCreateMode()"
type="button" type="button"
@click="cancelEditing" @click="cancelEditing"
class="w-full bg-slate-500 text-white px-4 py-2 rounded-md hover:bg-slate-600" class="w-full bg-slate-500 text-white px-4 py-2 rounded-md hover:bg-slate-600"
@ -147,6 +146,27 @@
</form> </form>
</div> </div>
<!-- Members Section -->
<div v-if="!isLoading && currentMeeting != null" class="mt-8 p-4 border rounded-lg bg-white shadow">
<div class="flex items-center justify-between mb-4">
<h2 class="text-2xl">Meeting Members</h2>
</div>
<router-link
:to="`/onboard-meeting-members/${currentMeeting.groupId}?password=${encodeURIComponent(currentMeeting.password || '')}`"
class="inline-block px-4 text-blue-600"
target="_blank"
>
Open page that meeting members see
</router-link>
<MembersList
:password="currentMeeting.password || ''"
decrypt-failure-message="Unable to decrypt some member information. Please check your password."
@error="handleMembersError"
class="mt-8"
/>
</div>
<div v-else-if="isLoading"> <div v-else-if="isLoading">
<div class="flex justify-center items-center h-full"> <div class="flex justify-center items-center h-full">
<fa icon="spinner" class="fa-spin-pulse" /> <fa icon="spinner" class="fa-spin-pulse" />
@ -159,6 +179,7 @@
import { Component, Vue } from 'vue-facing-decorator'; import { Component, Vue } from 'vue-facing-decorator';
import QuickNav from '@/components/QuickNav.vue'; import QuickNav from '@/components/QuickNav.vue';
import TopMessage from '@/components/TopMessage.vue'; import TopMessage from '@/components/TopMessage.vue';
import MembersList from '@/components/MembersList.vue';
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index';
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer';
import { encryptMessage } from '@/libs/crypto'; import { encryptMessage } from '@/libs/crypto';
@ -182,6 +203,7 @@ interface MeetingSetupInfo {
components: { components: {
QuickNav, QuickNav,
TopMessage, TopMessage,
MembersList,
}, },
}) })
export default class OnboardMeetingView extends Vue { export default class OnboardMeetingView extends Vue {
@ -211,7 +233,11 @@ export default class OnboardMeetingView extends Vue {
this.isLoading = false; this.isLoading = false;
} }
isEditingOrCreating(): boolean { isInCreateMode(): boolean {
return this.newOrUpdatedMeeting != null && this.currentMeeting == null;
}
isInEditOrCreateMode(): boolean {
return this.newOrUpdatedMeeting != null; return this.newOrUpdatedMeeting != null;
} }
@ -256,13 +282,11 @@ export default class OnboardMeetingView extends Vue {
); );
if (response?.data?.data) { if (response?.data?.data) {
console.log('Response data', response.data.data);
this.currentMeeting = { this.currentMeeting = {
...response.data.data, ...response.data.data,
userFullName: this.fullName, userFullName: this.fullName,
password: this.currentMeeting?.password || "", password: this.currentMeeting?.password || "",
}; };
console.log('Current meeting', this.currentMeeting);
} else { } else {
// no meeting found // no meeting found
this.newOrUpdatedMeeting = this.blankMeeting(); this.newOrUpdatedMeeting = this.blankMeeting();
@ -345,6 +369,7 @@ export default class OnboardMeetingView extends Vue {
...this.newOrUpdatedMeeting, ...this.newOrUpdatedMeeting,
groupId: response.data.success.groupId, groupId: response.data.success.groupId,
}; };
this.newOrUpdatedMeeting = null; this.newOrUpdatedMeeting = null;
this.$notify( this.$notify(
{ {
@ -436,7 +461,6 @@ export default class OnboardMeetingView extends Vue {
startEditing() { startEditing() {
// Populate form with existing meeting data // Populate form with existing meeting data
if (this.currentMeeting) { if (this.currentMeeting) {
console.log('Current meeting', this.currentMeeting);
const localExpiresAt = new Date(this.currentMeeting.expiresAt); const localExpiresAt = new Date(this.currentMeeting.expiresAt);
this.newOrUpdatedMeeting = { this.newOrUpdatedMeeting = {
name: this.currentMeeting.name, name: this.currentMeeting.name,
@ -520,14 +544,12 @@ export default class OnboardMeetingView extends Vue {
); );
if (response.data && response.data.success) { if (response.data && response.data.success) {
console.log('Updated meeting', response.data);
// Update the current meeting with only the necessary fields // Update the current meeting with only the necessary fields
this.currentMeeting = { this.currentMeeting = {
...this.newOrUpdatedMeeting, ...this.newOrUpdatedMeeting,
groupId: this.currentMeeting?.groupId || "", groupId: this.currentMeeting?.groupId || "",
}; };
this.newOrUpdatedMeeting = null; this.newOrUpdatedMeeting = null;
console.log('Updated meeting now', this.currentMeeting);
} else { } else {
throw { response: response }; throw { response: response };
} }
@ -547,5 +569,17 @@ export default class OnboardMeetingView extends Vue {
this.isLoading = false; this.isLoading = false;
} }
} }
handleMembersError(message: string) {
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Error',
text: message,
},
5000
);
}
} }
</script> </script>
Loading…
Cancel
Save