forked from trent_larson/crowd-funder-for-time-pwa
fix linting (and change a little wording in onboarding page)
This commit is contained in:
@@ -117,7 +117,7 @@ export default class PromptDialog extends Vue {
|
|||||||
onOption3: this.onOption3,
|
onOption3: this.onOption3,
|
||||||
onCancel: this.onCancel,
|
onCancel: this.onCancel,
|
||||||
} as NotificationIface,
|
} as NotificationIface,
|
||||||
-1
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,4 +149,4 @@ export default class PromptDialog extends Vue {
|
|||||||
close("string that does not matter");
|
close("string that does not matter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -90,7 +90,11 @@
|
|||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "@/constants/app";
|
||||||
import { createAndSubmitGive, didInfo, serverMessageForUser } from "@/libs/endorserServer";
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
didInfo,
|
||||||
|
serverMessageForUser,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
|
|
||||||
<!-- Members List -->
|
<!-- Members List -->
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<div v-for="member in decryptedMembers" :key="member.memberId"
|
<div
|
||||||
class="p-4 bg-gray-50 rounded-lg">
|
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>
|
<h3 class="text-lg font-medium">{{ member.name }}</h3>
|
||||||
<p class="text-sm text-gray-600">{{ member.did }}</p>
|
<p class="text-sm text-gray-600">{{ member.did }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,18 +19,29 @@
|
|||||||
<p v-if="members.length === 0" class="text-center text-gray-500 py-4">
|
<p v-if="members.length === 0" class="text-center text-gray-500 py-4">
|
||||||
No members have joined this meeting yet
|
No members have joined this meeting yet
|
||||||
</p>
|
</p>
|
||||||
<p v-if="decryptedMembers.length < members.length" class="text-center text-red-600 py-4">
|
<p
|
||||||
{{ decryptFailureMessage || "Your password failed. Please go back and try again." }}
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from 'vue-facing-decorator';
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index';
|
|
||||||
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer';
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
import { decryptMessage } from '@/libs/crypto';
|
import {
|
||||||
|
errorStringForLog,
|
||||||
|
getHeaders,
|
||||||
|
serverMessageForUser,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import { decryptMessage } from "@/libs/crypto";
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
memberId: number;
|
memberId: number;
|
||||||
@@ -43,19 +57,20 @@ interface DecryptedMember {
|
|||||||
@Component
|
@Component
|
||||||
export default class MembersList extends Vue {
|
export default class MembersList extends Vue {
|
||||||
@Prop({ required: true }) password!: string;
|
@Prop({ required: true }) password!: string;
|
||||||
@Prop({ default: 'Your password failed. Please go back and try again.' }) decryptFailureMessage!: string;
|
@Prop({ default: "Your password failed. Please go back and try again." })
|
||||||
|
decryptFailureMessage!: string;
|
||||||
|
|
||||||
decryptedMembers: DecryptedMember[] = [];
|
decryptedMembers: DecryptedMember[] = [];
|
||||||
missingPassword = false;
|
missingPassword = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
members: Member[] = [];
|
members: Member[] = [];
|
||||||
activeDid = '';
|
activeDid = "";
|
||||||
apiServer = '';
|
apiServer = "";
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || '';
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || '';
|
this.apiServer = settings.apiServer || "";
|
||||||
await this.fetchMembers();
|
await this.fetchMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,16 +80,22 @@ export default class MembersList extends Vue {
|
|||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
const response = await this.axios.get(
|
const response = await this.axios.get(
|
||||||
`${this.apiServer}/api/partner/groupOnboardMembers/`,
|
`${this.apiServer}/api/partner/groupOnboardMembers/`,
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data && response.data.data) {
|
if (response.data && response.data.data) {
|
||||||
this.members = response.data.data;
|
this.members = response.data.data;
|
||||||
await this.decryptMemberContents();
|
await this.decryptMemberContents();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb('Error fetching members: ' + errorStringForLog(error), true);
|
logConsoleAndDb(
|
||||||
this.$emit('error', serverMessageForUser(error) || 'Failed to fetch members.');
|
"Error fetching members: " + errorStringForLog(error),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.$emit(
|
||||||
|
"error",
|
||||||
|
serverMessageForUser(error) || "Failed to fetch members.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -82,17 +103,20 @@ export default class MembersList extends Vue {
|
|||||||
|
|
||||||
async decryptMemberContents() {
|
async decryptMemberContents() {
|
||||||
this.decryptedMembers = [];
|
this.decryptedMembers = [];
|
||||||
|
|
||||||
if (!this.password) {
|
if (!this.password) {
|
||||||
this.missingPassword = true;
|
this.missingPassword = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const member of this.members) {
|
for (const member of this.members) {
|
||||||
try {
|
try {
|
||||||
const decryptedContent = await decryptMessage(member.content, this.password);
|
const decryptedContent = await decryptMessage(
|
||||||
|
member.content,
|
||||||
|
this.password,
|
||||||
|
);
|
||||||
const content = JSON.parse(decryptedContent);
|
const content = JSON.parse(decryptedContent);
|
||||||
|
|
||||||
this.decryptedMembers.push({
|
this.decryptedMembers.push({
|
||||||
memberId: member.memberId,
|
memberId: member.memberId,
|
||||||
name: content.name,
|
name: content.name,
|
||||||
@@ -104,4 +128,4 @@ export default class MembersList extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -83,7 +83,10 @@
|
|||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "@/constants/app";
|
||||||
import { createAndSubmitOffer, serverMessageForUser } from "@/libs/endorserServer";
|
import {
|
||||||
|
createAndSubmitOffer,
|
||||||
|
serverMessageForUser,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
import { retrieveSettingsForActiveAccount } from "@/db/index";
|
import { retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
|
|
||||||
|
|||||||
@@ -177,43 +177,43 @@ export async function encryptMessage(message: string, password: string) {
|
|||||||
|
|
||||||
// Derive key from password using PBKDF2
|
// Derive key from password using PBKDF2
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
'raw',
|
"raw",
|
||||||
encoder.encode(password),
|
encoder.encode(password),
|
||||||
'PBKDF2',
|
"PBKDF2",
|
||||||
false,
|
false,
|
||||||
['deriveBits', 'deriveKey']
|
["deriveBits", "deriveKey"],
|
||||||
);
|
);
|
||||||
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
const key = await crypto.subtle.deriveKey(
|
||||||
{
|
{
|
||||||
name: 'PBKDF2',
|
name: "PBKDF2",
|
||||||
salt,
|
salt,
|
||||||
iterations: ITERATIONS,
|
iterations: ITERATIONS,
|
||||||
hash: 'SHA-256'
|
hash: "SHA-256",
|
||||||
},
|
},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
{ name: "AES-GCM", length: KEY_LENGTH },
|
||||||
false,
|
false,
|
||||||
['encrypt']
|
["encrypt"],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Encrypt the message
|
// Encrypt the message
|
||||||
const encryptedContent = await crypto.subtle.encrypt(
|
const encryptedContent = await crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: "AES-GCM",
|
||||||
iv
|
iv,
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
encoder.encode(message)
|
encoder.encode(message),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return a JSON structure with base64-encoded components
|
// Return a JSON structure with base64-encoded components
|
||||||
const result = {
|
const result = {
|
||||||
salt: arrayBufferToBase64(salt),
|
salt: arrayBufferToBase64(salt),
|
||||||
iv: arrayBufferToBase64(iv),
|
iv: arrayBufferToBase64(iv),
|
||||||
encrypted: arrayBufferToBase64(encryptedContent)
|
encrypted: arrayBufferToBase64(encryptedContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
return btoa(JSON.stringify(result));
|
return btoa(JSON.stringify(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,34 +229,34 @@ export async function decryptMessage(encryptedJson: string, password: string) {
|
|||||||
|
|
||||||
// Derive the same key using PBKDF2 with the extracted salt
|
// Derive the same key using PBKDF2 with the extracted salt
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
'raw',
|
"raw",
|
||||||
new TextEncoder().encode(password),
|
new TextEncoder().encode(password),
|
||||||
'PBKDF2',
|
"PBKDF2",
|
||||||
false,
|
false,
|
||||||
['deriveBits', 'deriveKey']
|
["deriveBits", "deriveKey"],
|
||||||
);
|
);
|
||||||
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
const key = await crypto.subtle.deriveKey(
|
||||||
{
|
{
|
||||||
name: 'PBKDF2',
|
name: "PBKDF2",
|
||||||
salt: saltArray,
|
salt: saltArray,
|
||||||
iterations: ITERATIONS,
|
iterations: ITERATIONS,
|
||||||
hash: 'SHA-256'
|
hash: "SHA-256",
|
||||||
},
|
},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
{ name: "AES-GCM", length: KEY_LENGTH },
|
||||||
false,
|
false,
|
||||||
['decrypt']
|
["decrypt"],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decrypt the content
|
// Decrypt the content
|
||||||
const decryptedContent = await crypto.subtle.decrypt(
|
const decryptedContent = await crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: "AES-GCM",
|
||||||
iv: ivArray
|
iv: ivArray,
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
encryptedContent
|
encryptedContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert the decrypted content back to a string
|
// Convert the decrypted content back to a string
|
||||||
@@ -268,33 +268,33 @@ export async function testEncryptionDecryption() {
|
|||||||
try {
|
try {
|
||||||
const testMessage = "Hello, this is a test message! 🚀";
|
const testMessage = "Hello, this is a test message! 🚀";
|
||||||
const testPassword = "myTestPassword123";
|
const testPassword = "myTestPassword123";
|
||||||
|
|
||||||
console.log("Original message:", testMessage);
|
console.log("Original message:", testMessage);
|
||||||
|
|
||||||
// Test encryption
|
// Test encryption
|
||||||
console.log("Encrypting...");
|
console.log("Encrypting...");
|
||||||
const encrypted = await encryptMessage(testMessage, testPassword);
|
const encrypted = await encryptMessage(testMessage, testPassword);
|
||||||
console.log("Encrypted result:", encrypted);
|
console.log("Encrypted result:", encrypted);
|
||||||
|
|
||||||
// Test decryption
|
// Test decryption
|
||||||
console.log("Decrypting...");
|
console.log("Decrypting...");
|
||||||
const decrypted = await decryptMessage(encrypted, testPassword);
|
const decrypted = await decryptMessage(encrypted, testPassword);
|
||||||
console.log("Decrypted result:", decrypted);
|
console.log("Decrypted result:", decrypted);
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
const success = testMessage === decrypted;
|
const success = testMessage === decrypted;
|
||||||
console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||||
console.log("Messages match:", success);
|
console.log("Messages match:", success);
|
||||||
|
|
||||||
// Test with wrong password
|
// Test with wrong password
|
||||||
console.log("\nTesting with wrong password...");
|
console.log("\nTesting with wrong password...");
|
||||||
try {
|
try {
|
||||||
const wrongDecrypted = await decryptMessage(encrypted, "wrongPassword");
|
await decryptMessage(encrypted, "wrongPassword");
|
||||||
console.log("Should not reach here");
|
console.log("Should not reach here");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Correctly failed with wrong password ✅");
|
console.log("Correctly failed with wrong password ✅");
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Test failed with error:", error);
|
console.error("Test failed with error:", error);
|
||||||
|
|||||||
@@ -680,6 +680,7 @@ export async function setPlanInCache(
|
|||||||
* @param error that is thrown from an Endorser server call by Axios
|
* @param error that is thrown from an Endorser server call by Axios
|
||||||
* @returns user-friendly message, or undefined if none found
|
* @returns user-friendly message, or undefined if none found
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function serverMessageForUser(error: any) {
|
export function serverMessageForUser(error: any) {
|
||||||
return (
|
return (
|
||||||
// this is how most user messages are returned
|
// this is how most user messages are returned
|
||||||
|
|||||||
@@ -180,19 +180,19 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import("../views/OfferDetailsView.vue"),
|
component: () => import("../views/OfferDetailsView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/onboard-meeting-list',
|
path: "/onboard-meeting-list",
|
||||||
name: 'onboard-meeting-list',
|
name: "onboard-meeting-list",
|
||||||
component: () => import('../views/OnboardMeetingListView.vue'),
|
component: () => import("../views/OnboardMeetingListView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/onboard-meeting-members/:groupId',
|
path: "/onboard-meeting-members/:groupId",
|
||||||
name: 'onboard-meeting-members',
|
name: "onboard-meeting-members",
|
||||||
component: () => import('../views/OnboardMeetingMembersView.vue'),
|
component: () => import("../views/OnboardMeetingMembersView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/onboard-meeting-setup',
|
path: "/onboard-meeting-setup",
|
||||||
name: 'onboard-meeting-setup',
|
name: "onboard-meeting-setup",
|
||||||
component: () => import('../views/OnboardMeetingSetupView.vue'),
|
component: () => import("../views/OnboardMeetingSetupView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/project/:id?",
|
path: "/project/:id?",
|
||||||
|
|||||||
@@ -37,23 +37,24 @@
|
|||||||
<fa icon="chair" class="fa-fw text-2xl" />
|
<fa icon="chair" class="fa-fw text-2xl" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-else class="flex">
|
||||||
v-else
|
<span
|
||||||
class="flex"
|
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"
|
||||||
>
|
>
|
||||||
<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">
|
|
||||||
<fa
|
<fa
|
||||||
icon="envelope-open-text"
|
icon="envelope-open-text"
|
||||||
class="fa-fw text-2xl"
|
class="fa-fw text-2xl"
|
||||||
@click="
|
@click="
|
||||||
warning(
|
warning(
|
||||||
'You must get registered before you can create invites.',
|
'You must get registered before you can create invites.',
|
||||||
'Not Registered',
|
'Not Registered',
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</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">
|
<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"
|
||||||
|
>
|
||||||
<fa
|
<fa
|
||||||
icon="chair"
|
icon="chair"
|
||||||
class="fa-fw text-2xl"
|
class="fa-fw text-2xl"
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
|
|
||||||
<!-- Meeting List -->
|
<!-- Meeting List -->
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<div v-for="meeting in meetings" :key="meeting.groupId"
|
<div
|
||||||
|
v-for="meeting in meetings"
|
||||||
|
:key="meeting.groupId"
|
||||||
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
|
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
|
||||||
@click="promptPassword(meeting)"
|
@click="promptPassword(meeting)"
|
||||||
>
|
>
|
||||||
@@ -47,7 +49,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password Dialog -->
|
<!-- Password Dialog -->
|
||||||
<div v-if="showPasswordDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
<div
|
||||||
|
v-if="showPasswordDialog"
|
||||||
|
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">
|
||||||
<h3 class="text-lg font-medium mb-4">Enter Meeting Password</h3>
|
<h3 class="text-lg font-medium mb-4">Enter Meeting Password</h3>
|
||||||
<input
|
<input
|
||||||
@@ -78,13 +83,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-facing-decorator';
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import QuickNav from '@/components/QuickNav.vue';
|
import { nextTick } from "vue";
|
||||||
import TopMessage from '@/components/TopMessage.vue';
|
import { Router } from "vue-router";
|
||||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index';
|
|
||||||
import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer';
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { encryptMessage } from '@/libs/crypto';
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { nextTick } from 'vue';
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
|
import {
|
||||||
|
errorStringForLog,
|
||||||
|
getHeaders,
|
||||||
|
serverMessageForUser,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import { encryptMessage } from "@/libs/crypto";
|
||||||
|
|
||||||
interface Meeting {
|
interface Meeting {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -98,23 +109,26 @@ interface Meeting {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class OnboardMeetingListView extends Vue {
|
export default class OnboardMeetingListView extends Vue {
|
||||||
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void;
|
$notify!: (
|
||||||
|
notification: { group: string; type: string; title: string; text: string },
|
||||||
|
timeout?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
activeDid = '';
|
activeDid = "";
|
||||||
apiServer = '';
|
apiServer = "";
|
||||||
attendingMeeting: Meeting | null = null;
|
attendingMeeting: Meeting | null = null;
|
||||||
firstName = '';
|
firstName = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
meetings: Meeting[] = [];
|
meetings: Meeting[] = [];
|
||||||
password = '';
|
password = "";
|
||||||
selectedMeeting: Meeting | null = null;
|
selectedMeeting: Meeting | null = null;
|
||||||
showPasswordDialog = false;
|
showPasswordDialog = false;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || '';
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || '';
|
this.apiServer = settings.apiServer || "";
|
||||||
this.firstName = settings.firstName || '';
|
this.firstName = settings.firstName || "";
|
||||||
await this.fetchMeetings();
|
await this.fetchMeetings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +138,8 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
// get the meeting that the user is attending
|
// get the meeting that the user is attending
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
const response = await this.axios.get(
|
const response = await this.axios.get(
|
||||||
this.apiServer + '/api/partner/groupOnboardMember',
|
this.apiServer + "/api/partner/groupOnboardMember",
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data?.data) {
|
if (response.data?.data) {
|
||||||
@@ -134,8 +148,8 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
// retrieve the meeting details
|
// retrieve the meeting details
|
||||||
const headers2 = await getHeaders(this.activeDid);
|
const headers2 = await getHeaders(this.activeDid);
|
||||||
const response2 = await this.axios.get(
|
const response2 = await this.axios.get(
|
||||||
this.apiServer + '/api/partner/groupOnboard/' + attendingMeetingId,
|
this.apiServer + "/api/partner/groupOnboard/" + attendingMeetingId,
|
||||||
{ headers: headers2 }
|
{ headers: headers2 },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response2.data?.data) {
|
if (response2.data?.data) {
|
||||||
@@ -143,29 +157,35 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
logConsoleAndDb('Error fetching meeting for user after saying they are in one.', true);
|
logConsoleAndDb(
|
||||||
|
"Error fetching meeting for user after saying they are in one.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers2 = await getHeaders(this.activeDid);
|
const headers2 = await getHeaders(this.activeDid);
|
||||||
const response2 = await this.axios.get(
|
const response2 = await this.axios.get(
|
||||||
this.apiServer + '/api/partner/groupsOnboarding',
|
this.apiServer + "/api/partner/groupsOnboarding",
|
||||||
{ headers: headers2 }
|
{ headers: headers2 },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response2.data?.data) {
|
if (response2.data?.data) {
|
||||||
this.meetings = response2.data.data;
|
this.meetings = response2.data.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb('Error fetching meetings: ' + errorStringForLog(error), true);
|
logConsoleAndDb(
|
||||||
|
"Error fetching meetings: " + errorStringForLog(error),
|
||||||
|
true,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: serverMessageForUser(error) || 'Failed to fetch meetings.',
|
text: serverMessageForUser(error) || "Failed to fetch meetings.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -173,7 +193,7 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
promptPassword(meeting: Meeting) {
|
promptPassword(meeting: Meeting) {
|
||||||
this.password = '';
|
this.password = "";
|
||||||
this.selectedMeeting = meeting;
|
this.selectedMeeting = meeting;
|
||||||
this.showPasswordDialog = true;
|
this.showPasswordDialog = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -185,7 +205,7 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelPasswordDialog() {
|
cancelPasswordDialog() {
|
||||||
this.password = '';
|
this.password = "";
|
||||||
this.selectedMeeting = null;
|
this.selectedMeeting = null;
|
||||||
this.showPasswordDialog = false;
|
this.showPasswordDialog = false;
|
||||||
}
|
}
|
||||||
@@ -193,7 +213,10 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
async submitPassword() {
|
async submitPassword() {
|
||||||
if (!this.selectedMeeting) {
|
if (!this.selectedMeeting) {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
logConsoleAndDb('No meeting selected when prompting for password, which should never happen.', true);
|
logConsoleAndDb(
|
||||||
|
"No meeting selected when prompting for password, which should never happen.",
|
||||||
|
true,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,51 +224,58 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
// Create member data object
|
// Create member data object
|
||||||
const memberData = {
|
const memberData = {
|
||||||
name: this.firstName,
|
name: this.firstName,
|
||||||
did: this.activeDid
|
did: this.activeDid,
|
||||||
};
|
};
|
||||||
const memberDataString = JSON.stringify(memberData);
|
const memberDataString = JSON.stringify(memberData);
|
||||||
const encryptedMemberData = await encryptMessage(memberDataString, this.password);
|
const encryptedMemberData = await encryptMessage(
|
||||||
|
memberDataString,
|
||||||
|
this.password,
|
||||||
|
);
|
||||||
|
|
||||||
// Get headers for authentication
|
// Get headers for authentication
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
|
||||||
// Encrypt the member data
|
// Encrypt the member data
|
||||||
const postResult = await this.axios.post(
|
const postResult = await this.axios.post(
|
||||||
this.apiServer + '/api/partner/groupOnboardMember',
|
this.apiServer + "/api/partner/groupOnboardMember",
|
||||||
{
|
{
|
||||||
groupId: this.selectedMeeting.groupId,
|
groupId: this.selectedMeeting.groupId,
|
||||||
content: encryptedMemberData
|
content: encryptedMemberData,
|
||||||
},
|
},
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (postResult.data && postResult.data.success) {
|
if (postResult.data && postResult.data.success) {
|
||||||
// Navigate to members view with password and groupId
|
// Navigate to members view with password and groupId
|
||||||
this.$router.push({
|
(this.$router as Router).push({
|
||||||
name: 'onboard-meeting-members',
|
name: "onboard-meeting-members",
|
||||||
params: {
|
params: {
|
||||||
groupId: this.selectedMeeting.groupId.toString()
|
groupId: this.selectedMeeting.groupId.toString(),
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
password: this.password,
|
password: this.password,
|
||||||
memberId: postResult.data.memberId
|
memberId: postResult.data.memberId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.cancelPasswordDialog();
|
this.cancelPasswordDialog();
|
||||||
} else {
|
} else {
|
||||||
throw { response: postResult };
|
throw { response: postResult };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb('Error joining meeting: ' + errorStringForLog(error), true);
|
logConsoleAndDb(
|
||||||
|
"Error joining meeting: " + errorStringForLog(error),
|
||||||
|
true,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: serverMessageForUser(error) || 'Failed to join meeting.',
|
text:
|
||||||
|
serverMessageForUser(error) || "You failed to join the meeting.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,8 +284,8 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
await this.axios.delete(
|
await this.axios.delete(
|
||||||
this.apiServer + '/api/partner/groupOnboardMember',
|
this.apiServer + "/api/partner/groupOnboardMember",
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
this.attendingMeeting = null;
|
this.attendingMeeting = null;
|
||||||
@@ -263,25 +293,29 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'success',
|
type: "success",
|
||||||
title: 'Success',
|
title: "Success",
|
||||||
text: 'Successfully left the meeting.',
|
text: "You left the meeting.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb('Error leaving meeting: ' + errorStringForLog(error), true);
|
logConsoleAndDb(
|
||||||
|
"Error leaving meeting: " + errorStringForLog(error),
|
||||||
|
true,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: serverMessageForUser(error) || 'Failed to leave meeting.',
|
text:
|
||||||
|
serverMessageForUser(error) || "You failed to leave the meeting.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -38,10 +38,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-facing-decorator';
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import QuickNav from '@/components/QuickNav.vue';
|
import { RouteLocation } from "vue-router";
|
||||||
import TopMessage from '@/components/TopMessage.vue';
|
|
||||||
import MembersList from '@/components/MembersList.vue';
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import MembersList from "@/components/MembersList.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -51,23 +53,23 @@ import MembersList from '@/components/MembersList.vue';
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class OnboardMeetingMembersView extends Vue {
|
export default class OnboardMeetingMembersView extends Vue {
|
||||||
errorMessage = '';
|
errorMessage = "";
|
||||||
|
|
||||||
get groupId(): string {
|
get groupId(): string {
|
||||||
return this.$route.params.groupId as string;
|
return (this.$route as RouteLocation).params.groupId as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
get password(): string {
|
get password(): string {
|
||||||
return this.$route.query.password as string;
|
return (this.$route as RouteLocation).query.password as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
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.";
|
||||||
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.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,4 +78,4 @@ export default class OnboardMeetingMembersView extends Vue {
|
|||||||
this.errorMessage = message;
|
this.errorMessage = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
title="Edit Meeting"
|
title="Edit Meeting"
|
||||||
>
|
>
|
||||||
<fa icon="pen" class="fa-fw" />
|
<fa icon="pen" class="fa-fw" />
|
||||||
<span class="sr-only">{{ isInCreateMode() ? 'Create Meeting' : 'Edit Meeting' }}</span>
|
<span class="sr-only">{{
|
||||||
|
isInCreateMode() ? "Create Meeting" : "Edit Meeting"
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -33,28 +35,41 @@
|
|||||||
title="Delete Meeting"
|
title="Delete Meeting"
|
||||||
>
|
>
|
||||||
<fa icon="trash-can" class="fa-fw" />
|
<fa icon="trash-can" class="fa-fw" />
|
||||||
<span class="sr-only">{{ isDeleting ? 'Deleting...' : 'Delete Meeting' }}</span>
|
<span class="sr-only">{{
|
||||||
|
isDeleting ? "Deleting..." : "Delete Meeting"
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p><strong>Name:</strong> {{ currentMeeting.name }}</p>
|
<p><strong>Name:</strong> {{ currentMeeting.name }}</p>
|
||||||
<p><strong>Expires:</strong> {{ formatExpirationTime(currentMeeting.expiresAt) }}</p>
|
<p>
|
||||||
|
<strong>Expires:</strong>
|
||||||
|
{{ formatExpirationTime(currentMeeting.expiresAt) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div v-if="currentMeeting.password" class="mt-4">
|
<div v-if="currentMeeting.password" class="mt-4">
|
||||||
<p class="text-gray-600">Share the password with the people you want to onboard.</p>
|
<p class="text-gray-600">
|
||||||
|
Share the password with the people you want to onboard.
|
||||||
|
</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 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>
|
||||||
</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">
|
||||||
<h3 class="text-lg font-medium mb-4">Delete Meeting?</h3>
|
<h3 class="text-lg font-medium mb-4">Delete Meeting?</h3>
|
||||||
<p class="text-gray-600 mb-6">This action cannot be undone. Are you sure you want to delete this meeting?</p>
|
<p class="text-gray-600 mb-6">
|
||||||
|
This action cannot be undone. Are you sure you want to delete this
|
||||||
|
meeting?
|
||||||
|
</p>
|
||||||
<div class="flex justify-between space-x-4">
|
<div class="flex justify-between space-x-4">
|
||||||
<button
|
<button
|
||||||
@click="showDeleteConfirm = false"
|
@click="showDeleteConfirm = false"
|
||||||
@@ -74,14 +89,27 @@
|
|||||||
|
|
||||||
<!-- Create/Edit Meeting Form -->
|
<!-- Create/Edit Meeting Form -->
|
||||||
<div
|
<div
|
||||||
v-if="!isLoading && isInEditOrCreateMode() && newOrUpdatedMeeting != null /* duplicate check is for typechecks */"
|
v-if="
|
||||||
|
!isLoading &&
|
||||||
|
isInEditOrCreateMode() &&
|
||||||
|
newOrUpdatedMeeting != null /* duplicate check is for typechecks */
|
||||||
|
"
|
||||||
class="mt-8"
|
class="mt-8"
|
||||||
>
|
>
|
||||||
<h2 class="text-2xl mb-4">{{ isInCreateMode() ? 'Create New Meeting' : 'Edit Meeting' }}</h2>
|
<h2 class="text-2xl mb-4">
|
||||||
<!-- This is my first form. Not sure whether I like it or not; gotta see if the browser benefits extend to the native app. -->
|
{{ isInCreateMode() ? "Create New Meeting" : "Edit Meeting" }}
|
||||||
<form @submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()" class="space-y-4">
|
</h2>
|
||||||
|
<!-- This is my first form. Not sure if I like it; will see if the browser benefits extend to the native app. -->
|
||||||
|
<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
|
||||||
id="meetingName"
|
id="meetingName"
|
||||||
v-model="newOrUpdatedMeeting.name"
|
v-model="newOrUpdatedMeeting.name"
|
||||||
@@ -93,7 +121,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="expirationTime" class="block text-sm font-medium text-gray-700">Meeting Expiration Time</label>
|
<label
|
||||||
|
for="expirationTime"
|
||||||
|
class="block text-sm font-medium text-gray-700"
|
||||||
|
>Meeting Expiration Time</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id="expirationTime"
|
id="expirationTime"
|
||||||
v-model="newOrUpdatedMeeting.expiresAt"
|
v-model="newOrUpdatedMeeting.expiresAt"
|
||||||
@@ -105,7 +137,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-medium text-gray-700">Meeting Password</label>
|
<label for="password" class="block text-sm font-medium text-gray-700"
|
||||||
|
>Meeting Password</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
v-model="newOrUpdatedMeeting.password"
|
v-model="newOrUpdatedMeeting.password"
|
||||||
@@ -117,7 +151,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="userName" class="block text-sm font-medium text-gray-700">Your Name</label>
|
<label for="userName" class="block text-sm font-medium text-gray-700"
|
||||||
|
>Your Name</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id="userName"
|
id="userName"
|
||||||
v-model="newOrUpdatedMeeting.userFullName"
|
v-model="newOrUpdatedMeeting.userFullName"
|
||||||
@@ -133,7 +169,15 @@
|
|||||||
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 ? (isInCreateMode() ? 'Creating...' : 'Updating...' ) : (isInCreateMode() ? 'Create Meeting' : 'Update Meeting') }}
|
{{
|
||||||
|
isLoading
|
||||||
|
? isInCreateMode()
|
||||||
|
? "Creating..."
|
||||||
|
: "Updating..."
|
||||||
|
: isInCreateMode()
|
||||||
|
? "Create Meeting"
|
||||||
|
: "Update Meeting"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="isInEditOrCreateMode()"
|
v-if="isInEditOrCreateMode()"
|
||||||
@@ -147,12 +191,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members Section -->
|
<!-- Members Section -->
|
||||||
<div v-if="!isLoading && currentMeeting != null" class="mt-8 p-4 border rounded-lg bg-white shadow">
|
<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">
|
<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>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/onboard-meeting-members/${currentMeeting.groupId}?password=${encodeURIComponent(currentMeeting.password || '')}`"
|
:to="onboardMeetingMembersLink()"
|
||||||
class="inline-block px-4 text-blue-600"
|
class="inline-block px-4 text-blue-600"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
@@ -176,20 +223,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 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 {
|
||||||
import { encryptMessage } from '@/libs/crypto';
|
errorStringForLog,
|
||||||
|
getHeaders,
|
||||||
|
serverMessageForUser,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import { encryptMessage } from "@/libs/crypto";
|
||||||
|
|
||||||
interface ServerMeeting {
|
interface ServerMeeting {
|
||||||
groupId: string; // from the server
|
groupId: number; // from the server
|
||||||
name: string; // from the server
|
name: string; // from the server
|
||||||
expiresAt: string; // from the server
|
expiresAt: string; // 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
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MeetingSetupInfo {
|
interface MeetingSetupInfo {
|
||||||
@@ -207,16 +258,19 @@ interface MeetingSetupInfo {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class OnboardMeetingView extends Vue {
|
export default class OnboardMeetingView extends Vue {
|
||||||
$notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void;
|
$notify!: (
|
||||||
|
notification: { group: string; type: string; title: string; text: string },
|
||||||
|
timeout?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
currentMeeting: ServerMeeting | null = null;
|
currentMeeting: ServerMeeting | null = null;
|
||||||
newOrUpdatedMeeting: MeetingSetupInfo | null = null;
|
newOrUpdatedMeeting: MeetingSetupInfo | null = null;
|
||||||
activeDid = '';
|
activeDid = "";
|
||||||
apiServer = '';
|
apiServer = "";
|
||||||
isDeleting = false;
|
isDeleting = false;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
showDeleteConfirm = false;
|
showDeleteConfirm = false;
|
||||||
fullName = '';
|
fullName = "";
|
||||||
get minDateTime() {
|
get minDateTime() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future
|
now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future
|
||||||
@@ -225,10 +279,10 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || '';
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || '';
|
this.apiServer = settings.apiServer || "";
|
||||||
this.fullName = settings.firstName || '';
|
this.fullName = settings.firstName || "";
|
||||||
|
|
||||||
await this.fetchCurrentMeeting();
|
await this.fetchCurrentMeeting();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -255,21 +309,21 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
// Format a date object to YYYY-MM-DDTHH:mm format for datetime-local input
|
// Format a date object to YYYY-MM-DDTHH:mm format for datetime-local input
|
||||||
private formatDateForInput(date: Date): string {
|
private formatDateForInput(date: Date): string {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
blankMeeting(): MeetingSetupInfo {
|
blankMeeting(): MeetingSetupInfo {
|
||||||
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 || "",
|
password: (this.currentMeeting?.password as string) || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,10 +331,10 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
const response = await this.axios.get(
|
const response = await this.axios.get(
|
||||||
this.apiServer + '/api/partner/groupOnboard',
|
this.apiServer + "/api/partner/groupOnboard",
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response?.data?.data) {
|
if (response?.data?.data) {
|
||||||
this.currentMeeting = {
|
this.currentMeeting = {
|
||||||
...response.data.data,
|
...response.data.data,
|
||||||
@@ -302,7 +356,9 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.newOrUpdatedMeeting) {
|
if (!this.newOrUpdatedMeeting) {
|
||||||
throw Error('There was no meeting data to create. We should never get here.');
|
throw Error(
|
||||||
|
"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
|
||||||
@@ -311,57 +367,59 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
if (localExpiresAt <= now) {
|
if (localExpiresAt <= now) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
title: 'Invalid Time',
|
title: "Invalid Time",
|
||||||
text: 'Select a future time for the meeting expiration.',
|
text: "Select a future time for the meeting expiration.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.newOrUpdatedMeeting.userFullName) {
|
if (!this.newOrUpdatedMeeting.userFullName) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
title: 'Invalid Name',
|
title: "Invalid Name",
|
||||||
text: 'Please enter your name.',
|
text: "Please enter your name.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.newOrUpdatedMeeting.password) {
|
if (!this.newOrUpdatedMeeting.password) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
title: 'Invalid Password',
|
title: "Invalid Password",
|
||||||
text: 'Please enter a password.',
|
text: "Please enter a password.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// create content with user's name and DID encrypted with password
|
// create content with user's name and DID encrypted with password
|
||||||
const content = {
|
const content = {
|
||||||
name: this.newOrUpdatedMeeting.userFullName,
|
name: this.newOrUpdatedMeeting.userFullName,
|
||||||
did: this.activeDid,
|
did: this.activeDid,
|
||||||
};
|
};
|
||||||
const encryptedContent = await encryptMessage(JSON.stringify(content), this.newOrUpdatedMeeting.password);
|
const encryptedContent = await encryptMessage(
|
||||||
|
JSON.stringify(content),
|
||||||
|
this.newOrUpdatedMeeting.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.newOrUpdatedMeeting.name,
|
||||||
expiresAt: localExpiresAt.toISOString(),
|
expiresAt: localExpiresAt.toISOString(),
|
||||||
content: encryptedContent,
|
content: encryptedContent,
|
||||||
},
|
},
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
@@ -373,27 +431,32 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
this.newOrUpdatedMeeting = null;
|
this.newOrUpdatedMeeting = null;
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'success',
|
type: "success",
|
||||||
title: 'Success',
|
title: "Success",
|
||||||
text: 'Meeting created.',
|
text: "Meeting created.",
|
||||||
},
|
},
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw { response: response };
|
throw { response: response };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb('Error creating meeting: ' + errorStringForLog(error), true);
|
logConsoleAndDb(
|
||||||
|
"Error creating meeting: " + errorStringForLog(error),
|
||||||
|
true,
|
||||||
|
);
|
||||||
const errorMessage = serverMessageForUser(error);
|
const errorMessage = serverMessageForUser(error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: errorMessage || 'Failed to create meeting. Try reloading or submitting again.',
|
text:
|
||||||
|
errorMessage ||
|
||||||
|
"Failed to create meeting. Try reloading or submitting again.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -403,14 +466,16 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
formatExpirationTime(expiresAt: string): string {
|
formatExpirationTime(expiresAt: string): string {
|
||||||
const expiration = new Date(expiresAt); // Server time is in UTC
|
const expiration = new Date(expiresAt); // Server time is in UTC
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffHours = Math.round((expiration.getTime() - now.getTime()) / (1000 * 60 * 60));
|
const diffHours = Math.round(
|
||||||
|
(expiration.getTime() - now.getTime()) / (1000 * 60 * 60),
|
||||||
|
);
|
||||||
|
|
||||||
if (diffHours < 0) {
|
if (diffHours < 0) {
|
||||||
return 'Expired';
|
return "Expired";
|
||||||
} else if (diffHours < 1) {
|
} else if (diffHours < 1) {
|
||||||
return 'Less than an hour';
|
return "Less than an hour";
|
||||||
} else if (diffHours === 1) {
|
} else if (diffHours === 1) {
|
||||||
return '1 hour';
|
return "1 hour";
|
||||||
} else {
|
} else {
|
||||||
return `${diffHours} hours`;
|
return `${diffHours} hours`;
|
||||||
}
|
}
|
||||||
@@ -424,34 +489,33 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
this.isDeleting = true;
|
this.isDeleting = true;
|
||||||
try {
|
try {
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
await this.axios.delete(
|
await this.axios.delete(this.apiServer + "/api/partner/groupOnboard", {
|
||||||
this.apiServer + '/api/partner/groupOnboard',
|
headers,
|
||||||
{ headers }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
this.currentMeeting = null;
|
this.currentMeeting = null;
|
||||||
this.newOrUpdatedMeeting = this.blankMeeting();
|
this.newOrUpdatedMeeting = this.blankMeeting();
|
||||||
this.showDeleteConfirm = false;
|
this.showDeleteConfirm = false;
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'success',
|
type: "success",
|
||||||
title: 'Success',
|
title: "Success",
|
||||||
text: 'Meeting deleted successfully.',
|
text: "Meeting deleted successfully.",
|
||||||
},
|
},
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting meeting:', error);
|
console.error("Error deleting meeting:", error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: serverMessageForUser(error) || 'Failed to delete meeting.',
|
text: serverMessageForUser(error) || "Failed to delete meeting.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isDeleting = false;
|
this.isDeleting = false;
|
||||||
@@ -465,11 +529,13 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
this.newOrUpdatedMeeting = {
|
this.newOrUpdatedMeeting = {
|
||||||
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 || "",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.error('There is no current meeting to edit. We should never get here.');
|
console.error(
|
||||||
|
"There is no current meeting to edit. We should never get here.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +547,7 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
async updateMeeting() {
|
async updateMeeting() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
if (!this.newOrUpdatedMeeting) {
|
if (!this.newOrUpdatedMeeting) {
|
||||||
throw Error('There was no meeting data to update.');
|
throw Error("There was no meeting data to update.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -491,36 +557,36 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
if (localExpiresAt <= now) {
|
if (localExpiresAt <= now) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
title: 'Invalid Time',
|
title: "Invalid Time",
|
||||||
text: 'Select a future time for the meeting expiration.',
|
text: "Select a future time for the meeting expiration.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.newOrUpdatedMeeting.userFullName) {
|
if (!this.newOrUpdatedMeeting.userFullName) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
title: 'Invalid Name',
|
title: "Invalid Name",
|
||||||
text: 'Please enter your name.',
|
text: "Please enter your name.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.newOrUpdatedMeeting.password) {
|
if (!this.newOrUpdatedMeeting.password) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
title: 'Invalid Password',
|
title: "Invalid Password",
|
||||||
text: 'Please enter a password.',
|
text: "Please enter a password.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -529,57 +595,74 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
name: this.newOrUpdatedMeeting.userFullName,
|
name: this.newOrUpdatedMeeting.userFullName,
|
||||||
did: this.activeDid,
|
did: this.activeDid,
|
||||||
};
|
};
|
||||||
const encryptedContent = await encryptMessage(JSON.stringify(content), this.newOrUpdatedMeeting.password);
|
const encryptedContent = await encryptMessage(
|
||||||
|
JSON.stringify(content),
|
||||||
|
this.newOrUpdatedMeeting.password,
|
||||||
|
);
|
||||||
|
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
const response = await this.axios.put(
|
const response = await this.axios.put(
|
||||||
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.newOrUpdatedMeeting.name,
|
||||||
expiresAt: localExpiresAt.toISOString(),
|
expiresAt: localExpiresAt.toISOString(),
|
||||||
content: encryptedContent,
|
content: encryptedContent,
|
||||||
},
|
},
|
||||||
{ headers }
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
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.newOrUpdatedMeeting,
|
||||||
groupId: this.currentMeeting?.groupId || "",
|
groupId: (this.currentMeeting?.groupId as number) || -1,
|
||||||
};
|
};
|
||||||
this.newOrUpdatedMeeting = null;
|
this.newOrUpdatedMeeting = null;
|
||||||
} else {
|
} else {
|
||||||
throw { response: response };
|
throw { response: response };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb('Error updating meeting: ' + errorStringForLog(error), true);
|
logConsoleAndDb(
|
||||||
|
"Error updating meeting: " + errorStringForLog(error),
|
||||||
|
true,
|
||||||
|
);
|
||||||
const errorMessage = serverMessageForUser(error);
|
const errorMessage = serverMessageForUser(error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: errorMessage || 'Failed to update meeting. Try reloading or submitting again.',
|
text:
|
||||||
|
errorMessage ||
|
||||||
|
"Failed to update meeting. Try reloading or submitting again.",
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onboardMeetingMembersLink(): string {
|
||||||
|
if (this.currentMeeting) {
|
||||||
|
return `/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
||||||
|
this.currentMeeting?.password || "",
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
handleMembersError(message: string) {
|
handleMembersError(message: string) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: 'alert',
|
group: "alert",
|
||||||
type: 'danger',
|
type: "danger",
|
||||||
title: 'Error',
|
title: "Error",
|
||||||
text: message,
|
text: message,
|
||||||
},
|
},
|
||||||
5000
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user