|
|
@ -49,12 +49,13 @@ |
|
|
|
|
|
|
|
<div v-if="currentMeeting.password" class="mt-4"> |
|
|
|
<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> |
|
|
|
</div> |
|
|
|
<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. |
|
|
|
You must reenter your password. Edit this meeting, or delete it and |
|
|
|
create a new meeting. |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
@ -92,7 +93,7 @@ |
|
|
|
v-if=" |
|
|
|
!isLoading && |
|
|
|
isInEditOrCreateMode() && |
|
|
|
newOrUpdatedMeeting != null /* duplicate check is for typechecks */ |
|
|
|
newOrUpdatedMeetingInputs != null /* duplicate check is for typechecks */ |
|
|
|
" |
|
|
|
class="mt-8" |
|
|
|
> |
|
|
@ -115,7 +116,7 @@ |
|
|
|
> |
|
|
|
<input |
|
|
|
id="meetingName" |
|
|
|
v-model="newOrUpdatedMeeting.name" |
|
|
|
v-model="newOrUpdatedMeetingInputs.name" |
|
|
|
type="text" |
|
|
|
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" |
|
|
@ -131,7 +132,7 @@ |
|
|
|
> |
|
|
|
<input |
|
|
|
id="expirationTime" |
|
|
|
v-model="newOrUpdatedMeeting.expiresAt" |
|
|
|
v-model="newOrUpdatedMeetingInputs.expiresAt" |
|
|
|
type="datetime-local" |
|
|
|
required |
|
|
|
:min="minDateTime" |
|
|
@ -145,7 +146,7 @@ |
|
|
|
> |
|
|
|
<input |
|
|
|
id="password" |
|
|
|
v-model="newOrUpdatedMeeting.password" |
|
|
|
v-model="newOrUpdatedMeetingInputs.password" |
|
|
|
type="text" |
|
|
|
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" |
|
|
@ -159,7 +160,7 @@ |
|
|
|
> |
|
|
|
<input |
|
|
|
id="userName" |
|
|
|
v-model="newOrUpdatedMeeting.userFullName" |
|
|
|
v-model="newOrUpdatedMeetingInputs.userFullName" |
|
|
|
type="text" |
|
|
|
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" |
|
|
@ -167,6 +168,19 @@ |
|
|
|
/> |
|
|
|
</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 |
|
|
|
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" |
|
|
@ -201,15 +215,25 @@ |
|
|
|
<div class="flex items-center justify-between mb-4"> |
|
|
|
<h2 class="text-2xl">Meeting Members</h2> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="flex items-center gap-2 cursor-pointer text-blue-600" |
|
|
|
@click="copyMembersLinkToClipboard" |
|
|
|
title="Click to copy link for members" |
|
|
|
> |
|
|
|
<span> |
|
|
|
• Page for Members |
|
|
|
<font-awesome icon="link" /> |
|
|
|
</span> |
|
|
|
<router-link |
|
|
|
v-if="!!currentMeeting.password" |
|
|
|
:to="onboardMeetingMembersLink()" |
|
|
|
class="inline-block text-blue-600" |
|
|
|
class="inline-block text-blue-600 ml-4" |
|
|
|
target="_blank" |
|
|
|
@click.stop |
|
|
|
> |
|
|
|
• Open shortcut page for members |
|
|
|
<font-awesome icon="external-link" /> |
|
|
|
</router-link> |
|
|
|
</div> |
|
|
|
|
|
|
|
<MembersList |
|
|
|
:password="currentMeeting.password || ''" |
|
|
@ -219,6 +243,21 @@ |
|
|
|
/> |
|
|
|
</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 class="flex justify-center items-center h-full"> |
|
|
|
<font-awesome icon="spinner" class="fa-spin-pulse" /> |
|
|
@ -229,6 +268,8 @@ |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
import { Component, Vue } from "vue-facing-decorator"; |
|
|
|
import { useClipboard } from "@vueuse/core"; |
|
|
|
|
|
|
|
import QuickNav from "../components/QuickNav.vue"; |
|
|
|
import TopMessage from "../components/TopMessage.vue"; |
|
|
|
import MembersList from "../components/MembersList.vue"; |
|
|
@ -240,19 +281,22 @@ import { |
|
|
|
} from "../libs/endorserServer"; |
|
|
|
import { encryptMessage } from "../libs/crypto"; |
|
|
|
import { logger } from "../utils/logger"; |
|
|
|
|
|
|
|
interface ServerMeeting { |
|
|
|
groupId: number; // from the server |
|
|
|
name: string; // from the server |
|
|
|
expiresAt: string; // from the server |
|
|
|
name: string; // to & from the server |
|
|
|
expiresAt: string; // to & from the server |
|
|
|
userFullName?: 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; |
|
|
|
expiresAt: string; |
|
|
|
userFullName: string; |
|
|
|
password: string; |
|
|
|
projectLink: string; |
|
|
|
} |
|
|
|
|
|
|
|
@Component({ |
|
|
@ -269,7 +313,7 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
) => void; |
|
|
|
|
|
|
|
currentMeeting: ServerMeeting | null = null; |
|
|
|
newOrUpdatedMeeting: MeetingSetupInfo | null = null; |
|
|
|
newOrUpdatedMeetingInputs: MeetingSetupInputs | null = null; |
|
|
|
activeDid = ""; |
|
|
|
apiServer = ""; |
|
|
|
isDeleting = false; |
|
|
@ -295,11 +339,11 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
} |
|
|
|
|
|
|
|
isInCreateMode(): boolean { |
|
|
|
return this.newOrUpdatedMeeting != null && this.currentMeeting == null; |
|
|
|
return this.newOrUpdatedMeetingInputs != null && this.currentMeeting == null; |
|
|
|
} |
|
|
|
|
|
|
|
isInEditOrCreateMode(): boolean { |
|
|
|
return this.newOrUpdatedMeeting != null; |
|
|
|
return this.newOrUpdatedMeetingInputs != null; |
|
|
|
} |
|
|
|
|
|
|
|
getDefaultExpirationTime(): string { |
|
|
@ -324,13 +368,14 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
return `${year}-${month}-${day}T${hours}:${minutes}`; |
|
|
|
} |
|
|
|
|
|
|
|
blankMeeting(): MeetingSetupInfo { |
|
|
|
blankMeeting(): MeetingSetupInputs { |
|
|
|
return { |
|
|
|
// no groupId yet |
|
|
|
name: "", |
|
|
|
expiresAt: this.getDefaultExpirationTime(), |
|
|
|
userFullName: this.fullName, |
|
|
|
password: (this.currentMeeting?.password as string) || "", |
|
|
|
projectLink: (this.currentMeeting?.projectLink as string) || "", |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
@ -342,19 +387,20 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
{ headers }, |
|
|
|
); |
|
|
|
|
|
|
|
const queryPassword = this.$route.query["password"] as string; |
|
|
|
if (response?.data?.data) { |
|
|
|
this.currentMeeting = { |
|
|
|
...response.data.data, |
|
|
|
userFullName: this.fullName, |
|
|
|
password: this.currentMeeting?.password || "", |
|
|
|
password: this.currentMeeting?.password || queryPassword || "", |
|
|
|
}; |
|
|
|
} else { |
|
|
|
// no meeting found |
|
|
|
this.newOrUpdatedMeeting = this.blankMeeting(); |
|
|
|
this.newOrUpdatedMeetingInputs = this.blankMeeting(); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
// no meeting found |
|
|
|
this.newOrUpdatedMeeting = this.blankMeeting(); |
|
|
|
this.newOrUpdatedMeetingInputs = this.blankMeeting(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -362,14 +408,14 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
this.isLoading = true; |
|
|
|
|
|
|
|
try { |
|
|
|
if (!this.newOrUpdatedMeeting) { |
|
|
|
if (!this.newOrUpdatedMeetingInputs) { |
|
|
|
throw Error( |
|
|
|
"There was no meeting data to create. We should never get here.", |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 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(); |
|
|
|
if (localExpiresAt <= now) { |
|
|
|
this.$notify( |
|
|
@ -383,7 +429,7 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.newOrUpdatedMeeting.userFullName) { |
|
|
|
if (!this.newOrUpdatedMeetingInputs.userFullName) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
@ -395,7 +441,7 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.newOrUpdatedMeeting.password) { |
|
|
|
if (!this.newOrUpdatedMeetingInputs.password) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
@ -408,35 +454,36 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// create content with user's name and DID encrypted with password |
|
|
|
// create content with user's name & DID encrypted with password |
|
|
|
const content = { |
|
|
|
name: this.newOrUpdatedMeeting.userFullName, |
|
|
|
name: this.newOrUpdatedMeetingInputs.userFullName, |
|
|
|
did: this.activeDid, |
|
|
|
isRegistered: this.isRegistered, |
|
|
|
}; |
|
|
|
const encryptedContent = await encryptMessage( |
|
|
|
JSON.stringify(content), |
|
|
|
this.newOrUpdatedMeeting.password, |
|
|
|
this.newOrUpdatedMeetingInputs.password, |
|
|
|
); |
|
|
|
|
|
|
|
const headers = await getHeaders(this.activeDid); |
|
|
|
const response = await this.axios.post( |
|
|
|
this.apiServer + "/api/partner/groupOnboard", |
|
|
|
{ |
|
|
|
name: this.newOrUpdatedMeeting.name, |
|
|
|
name: this.newOrUpdatedMeetingInputs.name, |
|
|
|
expiresAt: localExpiresAt.toISOString(), |
|
|
|
content: encryptedContent, |
|
|
|
projectLink: this.newOrUpdatedMeetingInputs.projectLink, |
|
|
|
}, |
|
|
|
{ headers }, |
|
|
|
); |
|
|
|
|
|
|
|
if (response.data && response.data.success) { |
|
|
|
this.currentMeeting = { |
|
|
|
...this.newOrUpdatedMeeting, |
|
|
|
...this.newOrUpdatedMeetingInputs, |
|
|
|
groupId: response.data.success.groupId, |
|
|
|
}; |
|
|
|
|
|
|
|
this.newOrUpdatedMeeting = null; |
|
|
|
this.newOrUpdatedMeetingInputs = null; |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
@ -502,7 +549,7 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
}); |
|
|
|
|
|
|
|
this.currentMeeting = null; |
|
|
|
this.newOrUpdatedMeeting = this.blankMeeting(); |
|
|
|
this.newOrUpdatedMeetingInputs = this.blankMeeting(); |
|
|
|
this.showDeleteConfirm = false; |
|
|
|
|
|
|
|
this.$notify( |
|
|
@ -534,11 +581,12 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
// Populate form with existing meeting data |
|
|
|
if (this.currentMeeting) { |
|
|
|
const localExpiresAt = new Date(this.currentMeeting.expiresAt); |
|
|
|
this.newOrUpdatedMeeting = { |
|
|
|
this.newOrUpdatedMeetingInputs = { |
|
|
|
name: this.currentMeeting.name, |
|
|
|
expiresAt: this.formatDateForInput(localExpiresAt), |
|
|
|
userFullName: this.currentMeeting.userFullName || "", |
|
|
|
password: this.currentMeeting.password || "", |
|
|
|
projectLink: this.currentMeeting.projectLink || "", |
|
|
|
}; |
|
|
|
} else { |
|
|
|
logger.error( |
|
|
@ -549,18 +597,18 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
|
|
|
|
cancelEditing() { |
|
|
|
// Reset form data |
|
|
|
this.newOrUpdatedMeeting = null; |
|
|
|
this.newOrUpdatedMeetingInputs = null; |
|
|
|
} |
|
|
|
|
|
|
|
async updateMeeting() { |
|
|
|
this.isLoading = true; |
|
|
|
if (!this.newOrUpdatedMeeting) { |
|
|
|
if (!this.newOrUpdatedMeetingInputs) { |
|
|
|
throw Error("There was no meeting data to update."); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 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(); |
|
|
|
if (localExpiresAt <= now) { |
|
|
|
this.$notify( |
|
|
@ -574,7 +622,7 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.newOrUpdatedMeeting.userFullName) { |
|
|
|
if (!this.newOrUpdatedMeetingInputs.userFullName) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
@ -586,7 +634,7 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.newOrUpdatedMeeting.password) { |
|
|
|
if (!this.newOrUpdatedMeetingInputs.password) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
@ -598,15 +646,15 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
// create content with user's name and DID encrypted with password |
|
|
|
// create content with user's name & DID encrypted with password |
|
|
|
const content = { |
|
|
|
name: this.newOrUpdatedMeeting.userFullName, |
|
|
|
name: this.newOrUpdatedMeetingInputs.userFullName, |
|
|
|
did: this.activeDid, |
|
|
|
isRegistered: this.isRegistered, |
|
|
|
}; |
|
|
|
const encryptedContent = await encryptMessage( |
|
|
|
JSON.stringify(content), |
|
|
|
this.newOrUpdatedMeeting.password, |
|
|
|
this.newOrUpdatedMeetingInputs.password, |
|
|
|
); |
|
|
|
|
|
|
|
const headers = await getHeaders(this.activeDid); |
|
|
@ -614,9 +662,10 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
this.apiServer + "/api/partner/groupOnboard", |
|
|
|
{ |
|
|
|
// 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(), |
|
|
|
content: encryptedContent, |
|
|
|
projectLink: this.newOrUpdatedMeetingInputs.projectLink, |
|
|
|
}, |
|
|
|
{ headers }, |
|
|
|
); |
|
|
@ -624,10 +673,17 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
if (response.data && response.data.success) { |
|
|
|
// Update the current meeting with only the necessary fields |
|
|
|
this.currentMeeting = { |
|
|
|
...this.newOrUpdatedMeeting, |
|
|
|
...this.newOrUpdatedMeetingInputs, |
|
|
|
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 { |
|
|
|
throw { response: response }; |
|
|
|
} |
|
|
@ -673,5 +729,21 @@ export default class OnboardMeetingView extends Vue { |
|
|
|
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> |
|
|
|