Browse Source

add projectLink to onboarding meeting, plus enhancements to setup usability

Trent Larson 7 days ago
parent
commit
41365fab8f
  1. 6
      src/components/MembersList.vue
  2. 9
      src/views/ContactsView.vue
  3. 22
      src/views/OnboardMeetingMembersView.vue
  4. 158
      src/views/OnboardMeetingSetupView.vue

6
src/components/MembersList.vue

@ -296,7 +296,7 @@ export default class MembersList extends Vue {
this.decryptedMembers.length === 0 || this.decryptedMembers.length === 0 ||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId this.decryptedMembers[0].member.memberId !== this.members[0].memberId
) { ) {
return "Your password is not the same as the organizer. Reload or have them check their password."; return "Your password is not the same as the organizer. Retry or have them check their password.";
} else { } else {
// the first (organizer) member was decrypted OK // the first (organizer) member was decrypted OK
return ""; return "";
@ -337,7 +337,7 @@ export default class MembersList extends Vue {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Contact Exists", title: "Contact Exists",
text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.", text: "They are in your contacts. To remove them, use the contacts page.",
}, },
10000, 10000,
); );
@ -347,7 +347,7 @@ export default class MembersList extends Vue {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Contact Available", title: "Contact Available",
text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.", text: "This is to add them to your contacts. To remove them later, use the contacts page.",
}, },
10000, 10000,
); );

9
src/views/ContactsView.vue

@ -54,17 +54,12 @@
/> />
</span> </span>
<span <span
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md" class="flex items-center 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-1.5 py-1 mr-1 rounded-md"
> >
<font-awesome <font-awesome
icon="chair" icon="chair"
class="fa-fw text-2xl" class="fa-fw text-2xl"
@click=" @click="this.$router.push({ name: 'onboard-meeting-list' })"
warning(
'You must get registered before you can initiate an onboarding meeting.',
'Not Registered',
)
"
/> />
</span> </span>
</span> </span>

22
src/views/OnboardMeetingMembersView.vue

@ -28,6 +28,16 @@
<!-- Members List --> <!-- Members List -->
<MembersList v-else :password="password" @error="handleError" /> <MembersList v-else :password="password" @error="handleError" />
<!-- Project Link Section -->
<div v-if="projectLink" class="mt-8 p-4 border rounded-lg bg-white shadow">
<router-link
:to="'/project/' + encodeURIComponent(projectLink)"
class="text-blue-600 hover:text-blue-800 transition-colors duration-200"
>
Go To Project Page
</router-link>
</div>
</section> </section>
<UserNameDialog <UserNameDialog
@ -69,6 +79,7 @@ export default class OnboardMeetingMembersView extends Vue {
firstName = ""; firstName = "";
isRegistered = false; isRegistered = false;
isLoading = true; isLoading = true;
projectLink = "";
$route!: RouteLocationNormalizedLoaded; $route!: RouteLocationNormalizedLoaded;
$router!: Router; $router!: Router;
@ -85,10 +96,12 @@ export default class OnboardMeetingMembersView extends Vue {
async created() { async created() {
if (!this.groupId) { if (!this.groupId) {
this.errorMessage = "The group info is missing. Go back and try again."; this.errorMessage = "The group info is missing. Go back and try again.";
this.isLoading = false;
return; return;
} }
if (!this.password) { if (!this.password) {
this.errorMessage = "The password is missing. Go back and try again."; this.errorMessage = "The password is missing. Go back and try again.";
this.isLoading = false;
return; return;
} }
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
@ -129,6 +142,15 @@ export default class OnboardMeetingMembersView extends Vue {
// updateMemberInMeeting sets isLoading to false // updateMemberInMeeting sets isLoading to false
} }
} }
// Fetch the meeting details to get the project link
const meetingResponse = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboard/${this.groupId}`,
{ headers }
);
if (meetingResponse.data?.data?.projectLink) {
this.projectLink = meetingResponse.data.data.projectLink;
}
} catch (error) { } catch (error) {
this.errorMessage = this.errorMessage =
serverMessageForUser(error) || serverMessageForUser(error) ||

158
src/views/OnboardMeetingSetupView.vue

@ -49,12 +49,13 @@
<div v-if="currentMeeting.password" class="mt-4"> <div v-if="currentMeeting.password" class="mt-4">
<p class="text-gray-600"> <p class="text-gray-600">
Share the password with the people you want to onboard. Share the password with the members. You can also send them the
"shortcut page for members" link below.
</p> </p>
</div> </div>
<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 You must reenter your password. Edit this meeting, or delete it and
and create a new meeting. create a new meeting.
</div> </div>
</div> </div>
</div> </div>
@ -92,7 +93,7 @@
v-if=" v-if="
!isLoading && !isLoading &&
isInEditOrCreateMode() && isInEditOrCreateMode() &&
newOrUpdatedMeeting != null /* duplicate check is for typechecks */ newOrUpdatedMeetingInputs != null /* duplicate check is for typechecks */
" "
class="mt-8" class="mt-8"
> >
@ -115,7 +116,7 @@
> >
<input <input
id="meetingName" id="meetingName"
v-model="newOrUpdatedMeeting.name" v-model="newOrUpdatedMeetingInputs.name"
type="text" type="text"
required required
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
@ -131,7 +132,7 @@
> >
<input <input
id="expirationTime" id="expirationTime"
v-model="newOrUpdatedMeeting.expiresAt" v-model="newOrUpdatedMeetingInputs.expiresAt"
type="datetime-local" type="datetime-local"
required required
:min="minDateTime" :min="minDateTime"
@ -145,7 +146,7 @@
> >
<input <input
id="password" id="password"
v-model="newOrUpdatedMeeting.password" v-model="newOrUpdatedMeetingInputs.password"
type="text" type="text"
required required
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
@ -159,7 +160,7 @@
> >
<input <input
id="userName" id="userName"
v-model="newOrUpdatedMeeting.userFullName" v-model="newOrUpdatedMeetingInputs.userFullName"
type="text" type="text"
required required
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none" class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
@ -167,6 +168,19 @@
/> />
</div> </div>
<div>
<label for="projectLink" class="block text-sm font-medium text-gray-700"
>Project Link</label
>
<input
id="projectLink"
v-model="newOrUpdatedMeetingInputs.projectLink"
type="text"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none"
placeholder="Project ID"
/>
</div>
<button <button
type="submit" type="submit"
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"
@ -201,15 +215,25 @@
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-2xl">Meeting Members</h2> <h2 class="text-2xl">Meeting Members</h2>
</div> </div>
<div
class="flex items-center gap-2 cursor-pointer text-blue-600"
@click="copyMembersLinkToClipboard"
title="Click to copy link for members"
>
<span>
&bull; Page for Members
<font-awesome icon="link" />
</span>
<router-link <router-link
v-if="!!currentMeeting.password" v-if="!!currentMeeting.password"
:to="onboardMeetingMembersLink()" :to="onboardMeetingMembersLink()"
class="inline-block text-blue-600" class="inline-block text-blue-600 ml-4"
target="_blank" target="_blank"
@click.stop
> >
&bull; Open shortcut page for members
<font-awesome icon="external-link" /> <font-awesome icon="external-link" />
</router-link> </router-link>
</div>
<MembersList <MembersList
:password="currentMeeting.password || ''" :password="currentMeeting.password || ''"
@ -219,6 +243,21 @@
/> />
</div> </div>
<div
v-if="currentMeeting?.projectLink"
class="mt-8 p-4 border rounded-lg bg-white shadow"
>
<!-- Project Link Section -->
<div>
<router-link
:to="'/project/' + encodeURIComponent(currentMeeting.projectLink)"
class="text-blue-600 hover:text-blue-800 transition-colors duration-200"
>
Go To Project Page
</router-link>
</div>
</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">
<font-awesome icon="spinner" class="fa-spin-pulse" /> <font-awesome icon="spinner" class="fa-spin-pulse" />
@ -229,6 +268,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
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 MembersList from "../components/MembersList.vue";
@ -240,19 +281,22 @@ import {
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { encryptMessage } from "../libs/crypto"; import { encryptMessage } from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
interface ServerMeeting { interface ServerMeeting {
groupId: number; // from the server groupId: number; // from the server
name: string; // from the server name: string; // to & from the server
expiresAt: string; // from the server expiresAt: string; // to & from the server
userFullName?: string; // from the user's session userFullName?: string; // from the user's session
password?: string; // from the user's session password?: string; // from the user's session
projectLink?: string; // to & from the server
} }
interface MeetingSetupInfo { interface MeetingSetupInputs {
name: string; name: string;
expiresAt: string; expiresAt: string;
userFullName: string; userFullName: string;
password: string; password: string;
projectLink: string;
} }
@Component({ @Component({
@ -269,7 +313,7 @@ export default class OnboardMeetingView extends Vue {
) => void; ) => void;
currentMeeting: ServerMeeting | null = null; currentMeeting: ServerMeeting | null = null;
newOrUpdatedMeeting: MeetingSetupInfo | null = null; newOrUpdatedMeetingInputs: MeetingSetupInputs | null = null;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
isDeleting = false; isDeleting = false;
@ -295,11 +339,11 @@ export default class OnboardMeetingView extends Vue {
} }
isInCreateMode(): boolean { isInCreateMode(): boolean {
return this.newOrUpdatedMeeting != null && this.currentMeeting == null; return this.newOrUpdatedMeetingInputs != null && this.currentMeeting == null;
} }
isInEditOrCreateMode(): boolean { isInEditOrCreateMode(): boolean {
return this.newOrUpdatedMeeting != null; return this.newOrUpdatedMeetingInputs != null;
} }
getDefaultExpirationTime(): string { getDefaultExpirationTime(): string {
@ -324,13 +368,14 @@ export default class OnboardMeetingView extends Vue {
return `${year}-${month}-${day}T${hours}:${minutes}`; return `${year}-${month}-${day}T${hours}:${minutes}`;
} }
blankMeeting(): MeetingSetupInfo { blankMeeting(): MeetingSetupInputs {
return { return {
// no groupId yet // no groupId yet
name: "", name: "",
expiresAt: this.getDefaultExpirationTime(), expiresAt: this.getDefaultExpirationTime(),
userFullName: this.fullName, userFullName: this.fullName,
password: (this.currentMeeting?.password as string) || "", password: (this.currentMeeting?.password as string) || "",
projectLink: (this.currentMeeting?.projectLink as string) || "",
}; };
} }
@ -342,19 +387,20 @@ export default class OnboardMeetingView extends Vue {
{ headers }, { headers },
); );
const queryPassword = this.$route.query["password"] as string;
if (response?.data?.data) { if (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 || queryPassword || "",
}; };
} else { } else {
// no meeting found // no meeting found
this.newOrUpdatedMeeting = this.blankMeeting(); this.newOrUpdatedMeetingInputs = this.blankMeeting();
} }
} catch (error) { } catch (error) {
// no meeting found // no meeting found
this.newOrUpdatedMeeting = this.blankMeeting(); this.newOrUpdatedMeetingInputs = this.blankMeeting();
} }
} }
@ -362,14 +408,14 @@ export default class OnboardMeetingView extends Vue {
this.isLoading = true; this.isLoading = true;
try { try {
if (!this.newOrUpdatedMeeting) { if (!this.newOrUpdatedMeetingInputs) {
throw Error( throw Error(
"There was no meeting data to create. We should never get here.", "There was no meeting data to create. We should never get here.",
); );
} }
// Convert local time to UTC for comparison and server submission // Convert local time to UTC for comparison and server submission
const localExpiresAt = new Date(this.newOrUpdatedMeeting.expiresAt); const localExpiresAt = new Date(this.newOrUpdatedMeetingInputs.expiresAt);
const now = new Date(); const now = new Date();
if (localExpiresAt <= now) { if (localExpiresAt <= now) {
this.$notify( this.$notify(
@ -383,7 +429,7 @@ export default class OnboardMeetingView extends Vue {
); );
return; return;
} }
if (!this.newOrUpdatedMeeting.userFullName) { if (!this.newOrUpdatedMeetingInputs.userFullName) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -395,7 +441,7 @@ export default class OnboardMeetingView extends Vue {
); );
return; return;
} }
if (!this.newOrUpdatedMeeting.password) { if (!this.newOrUpdatedMeetingInputs.password) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -408,35 +454,36 @@ export default class OnboardMeetingView extends Vue {
return; return;
} }
// create content with user's name and DID encrypted with password // create content with user's name & DID encrypted with password
const content = { const content = {
name: this.newOrUpdatedMeeting.userFullName, name: this.newOrUpdatedMeetingInputs.userFullName,
did: this.activeDid, did: this.activeDid,
isRegistered: this.isRegistered, isRegistered: this.isRegistered,
}; };
const encryptedContent = await encryptMessage( const encryptedContent = await encryptMessage(
JSON.stringify(content), JSON.stringify(content),
this.newOrUpdatedMeeting.password, this.newOrUpdatedMeetingInputs.password,
); );
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
const response = await this.axios.post( const response = await this.axios.post(
this.apiServer + "/api/partner/groupOnboard", this.apiServer + "/api/partner/groupOnboard",
{ {
name: this.newOrUpdatedMeeting.name, name: this.newOrUpdatedMeetingInputs.name,
expiresAt: localExpiresAt.toISOString(), expiresAt: localExpiresAt.toISOString(),
content: encryptedContent, content: encryptedContent,
projectLink: this.newOrUpdatedMeetingInputs.projectLink,
}, },
{ headers }, { headers },
); );
if (response.data && response.data.success) { if (response.data && response.data.success) {
this.currentMeeting = { this.currentMeeting = {
...this.newOrUpdatedMeeting, ...this.newOrUpdatedMeetingInputs,
groupId: response.data.success.groupId, groupId: response.data.success.groupId,
}; };
this.newOrUpdatedMeeting = null; this.newOrUpdatedMeetingInputs = null;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -502,7 +549,7 @@ export default class OnboardMeetingView extends Vue {
}); });
this.currentMeeting = null; this.currentMeeting = null;
this.newOrUpdatedMeeting = this.blankMeeting(); this.newOrUpdatedMeetingInputs = this.blankMeeting();
this.showDeleteConfirm = false; this.showDeleteConfirm = false;
this.$notify( this.$notify(
@ -534,11 +581,12 @@ export default class OnboardMeetingView extends Vue {
// Populate form with existing meeting data // Populate form with existing meeting data
if (this.currentMeeting) { if (this.currentMeeting) {
const localExpiresAt = new Date(this.currentMeeting.expiresAt); const localExpiresAt = new Date(this.currentMeeting.expiresAt);
this.newOrUpdatedMeeting = { this.newOrUpdatedMeetingInputs = {
name: this.currentMeeting.name, name: this.currentMeeting.name,
expiresAt: this.formatDateForInput(localExpiresAt), expiresAt: this.formatDateForInput(localExpiresAt),
userFullName: this.currentMeeting.userFullName || "", userFullName: this.currentMeeting.userFullName || "",
password: this.currentMeeting.password || "", password: this.currentMeeting.password || "",
projectLink: this.currentMeeting.projectLink || "",
}; };
} else { } else {
logger.error( logger.error(
@ -549,18 +597,18 @@ export default class OnboardMeetingView extends Vue {
cancelEditing() { cancelEditing() {
// Reset form data // Reset form data
this.newOrUpdatedMeeting = null; this.newOrUpdatedMeetingInputs = null;
} }
async updateMeeting() { async updateMeeting() {
this.isLoading = true; this.isLoading = true;
if (!this.newOrUpdatedMeeting) { if (!this.newOrUpdatedMeetingInputs) {
throw Error("There was no meeting data to update."); throw Error("There was no meeting data to update.");
} }
try { try {
// Convert local time to UTC for comparison and server submission // Convert local time to UTC for comparison and server submission
const localExpiresAt = new Date(this.newOrUpdatedMeeting.expiresAt); const localExpiresAt = new Date(this.newOrUpdatedMeetingInputs.expiresAt);
const now = new Date(); const now = new Date();
if (localExpiresAt <= now) { if (localExpiresAt <= now) {
this.$notify( this.$notify(
@ -574,7 +622,7 @@ export default class OnboardMeetingView extends Vue {
); );
return; return;
} }
if (!this.newOrUpdatedMeeting.userFullName) { if (!this.newOrUpdatedMeetingInputs.userFullName) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -586,7 +634,7 @@ export default class OnboardMeetingView extends Vue {
); );
return; return;
} }
if (!this.newOrUpdatedMeeting.password) { if (!this.newOrUpdatedMeetingInputs.password) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -598,15 +646,15 @@ export default class OnboardMeetingView extends Vue {
); );
return; return;
} }
// create content with user's name and DID encrypted with password // create content with user's name & DID encrypted with password
const content = { const content = {
name: this.newOrUpdatedMeeting.userFullName, name: this.newOrUpdatedMeetingInputs.userFullName,
did: this.activeDid, did: this.activeDid,
isRegistered: this.isRegistered, isRegistered: this.isRegistered,
}; };
const encryptedContent = await encryptMessage( const encryptedContent = await encryptMessage(
JSON.stringify(content), JSON.stringify(content),
this.newOrUpdatedMeeting.password, this.newOrUpdatedMeetingInputs.password,
); );
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
@ -614,9 +662,10 @@ export default class OnboardMeetingView extends Vue {
this.apiServer + "/api/partner/groupOnboard", this.apiServer + "/api/partner/groupOnboard",
{ {
// the groupId is in the currentMeeting but it's not necessary while users only have one meeting // the groupId is in the currentMeeting but it's not necessary while users only have one meeting
name: this.newOrUpdatedMeeting.name, name: this.newOrUpdatedMeetingInputs.name,
expiresAt: localExpiresAt.toISOString(), expiresAt: localExpiresAt.toISOString(),
content: encryptedContent, content: encryptedContent,
projectLink: this.newOrUpdatedMeetingInputs.projectLink,
}, },
{ headers }, { headers },
); );
@ -624,10 +673,17 @@ export default class OnboardMeetingView extends Vue {
if (response.data && response.data.success) { if (response.data && response.data.success) {
// 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.newOrUpdatedMeetingInputs,
groupId: (this.currentMeeting?.groupId as number) || -1, groupId: (this.currentMeeting?.groupId as number) || -1,
}; };
this.newOrUpdatedMeeting = null; this.newOrUpdatedMeetingInputs = null;
if (this.currentMeeting?.password) {
this.$router.push({
name: "onboard-meeting-setup",
query: { password: this.currentMeeting?.password },
});
}
} else { } else {
throw { response: response }; throw { response: response };
} }
@ -673,5 +729,21 @@ export default class OnboardMeetingView extends Vue {
5000, 5000,
); );
} }
copyMembersLinkToClipboard() {
useClipboard()
.copy(this.onboardMeetingMembersLink())
.then(() => {
this.$notify(
{
group: "alert",
type: "info",
title: "Copied",
text: "The member link is copied to the clipboard.",
},
5000,
);
});
}
} }
</script> </script>

Loading…
Cancel
Save