allow meeting organizer to see info about embeddings, and add match to pages
This commit is contained in:
@@ -27,7 +27,7 @@ Large Components (>500 lines): 5 components (12.5%)
|
||||
├── GiftedDialog.vue (670 lines) ⚠️ HIGH PRIORITY
|
||||
├── PhotoDialog.vue (669 lines) ⚠️ HIGH PRIORITY
|
||||
├── PushNotificationPermission.vue (660 lines) ⚠️ HIGH PRIORITY
|
||||
└── MembersList.vue (550 lines) ⚠️ MODERATE PRIORITY
|
||||
└── MeetingMembersList.vue (550 lines) ⚠️ MODERATE PRIORITY
|
||||
|
||||
Medium Components (200-500 lines): 12 components (30%)
|
||||
├── GiftDetailsStep.vue (450 lines)
|
||||
|
||||
@@ -116,7 +116,7 @@ echo "=============================="
|
||||
|
||||
# Analyze critical files identified in the assessment
|
||||
critical_files=(
|
||||
src/components/MembersList.vue"
|
||||
src/components/MeetingMembersList.vue"
|
||||
"src/views/ContactsView.vue"
|
||||
src/views/OnboardMeetingSetupView.vue"
|
||||
src/db/databaseUtil.ts"
|
||||
|
||||
@@ -93,7 +93,7 @@ echo "=========================="
|
||||
|
||||
# Critical files from our assessment
|
||||
files=(
|
||||
src/components/MembersList.vue"
|
||||
src/components/MeetingMembersList.vue"
|
||||
"src/views/ContactsView.vue"
|
||||
src/views/OnboardMeetingSetupView.vue"
|
||||
src/db/databaseUtil.ts"
|
||||
|
||||
@@ -93,7 +93,7 @@ echo "=========================="
|
||||
|
||||
# Critical files from our assessment
|
||||
files=(
|
||||
src/components/MembersList.vue"
|
||||
src/components/MeetingMembersList.vue"
|
||||
"src/views/ContactsView.vue"
|
||||
src/views/OnboardMeetingSetupView.vue"
|
||||
src/db/databaseUtil.ts"
|
||||
|
||||
245
src/components/MeetingMemberMatch.vue
Normal file
245
src/components/MeetingMemberMatch.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="group-onboard-match-display">
|
||||
<!-- Loading -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center gap-2 py-6 text-slate-600"
|
||||
>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div
|
||||
v-else-if="errorMessage"
|
||||
class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-red-700"
|
||||
>
|
||||
Inform the organizer that there was an error. {{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Matched person -->
|
||||
<div
|
||||
v-else-if="matchedPerson"
|
||||
class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm"
|
||||
>
|
||||
<h2 class="mb-3 font-bold text-slate-700">Your Current Match</h2>
|
||||
<div class="flex items-start gap-3">
|
||||
<EntityIcon
|
||||
:contact="matchedPersonContact"
|
||||
class="!size-14 shrink-0 overflow-hidden rounded-full border border-slate-300 bg-white"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-slate-900">
|
||||
{{ matchedPerson.name || "(No name)" }}
|
||||
</p>
|
||||
<p class="mt-0.5 truncate text-xs text-slate-500">
|
||||
{{ matchedPerson.did }}
|
||||
</p>
|
||||
<p
|
||||
v-if="matchedPerson.description"
|
||||
class="mt-2 line-clamp-3 text-sm text-slate-600"
|
||||
>
|
||||
{{ matchedPerson.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from "vue-facing-decorator";
|
||||
import {
|
||||
errorStringForLog,
|
||||
getHeaders,
|
||||
serverMessageForUser,
|
||||
} from "@/libs/endorserServer";
|
||||
import { decryptMessage } from "@/libs/crypto";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { AxiosErrorResponse } from "@/interfaces";
|
||||
|
||||
/** Participant from GET /api/partner/groupOnboardMatch pair */
|
||||
interface MatchPairParticipant {
|
||||
issuerDid: string;
|
||||
content: string;
|
||||
description?: string;
|
||||
decryptedContentObject?: {
|
||||
name: string;
|
||||
did: string;
|
||||
isRegistered: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface MatchPair {
|
||||
pairNumber: number;
|
||||
similarity: number;
|
||||
participants: MatchPairParticipant[];
|
||||
}
|
||||
|
||||
/** Normalized matched person for display */
|
||||
interface MatchedPersonData {
|
||||
name: string;
|
||||
did: string;
|
||||
isRegistered: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class GroupOnboardMatchDisplay extends Vue {
|
||||
@Prop({ required: true })
|
||||
meetingPassword!: string;
|
||||
|
||||
/** When provided, used to determine this person's match instead of calling groupOnboardMatch */
|
||||
@Prop()
|
||||
matchPairs?: MatchPair[] | null;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
errorMessage = "";
|
||||
isLoading = true;
|
||||
matchedPerson: MatchedPersonData | null = null;
|
||||
/** Pair that contains the current user (for similarity display if needed) */
|
||||
myPair: MatchPair | null = null;
|
||||
|
||||
/** Contact-like object for EntityIcon from matched person */
|
||||
get matchedPersonContact(): Contact | undefined {
|
||||
if (!this.matchedPerson) return undefined;
|
||||
return {
|
||||
did: this.matchedPerson.did,
|
||||
name: this.matchedPerson.name,
|
||||
};
|
||||
}
|
||||
|
||||
async created() {
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity?.activeDid || "";
|
||||
await this.fetchMatches();
|
||||
}
|
||||
|
||||
@Watch("meetingPassword")
|
||||
async onPasswordChange() {
|
||||
if (
|
||||
this.meetingPassword &&
|
||||
(this.matchPairs != null || (this.apiServer && this.activeDid))
|
||||
) {
|
||||
await this.fetchMatches();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("matchPairs")
|
||||
async onMatchPairsChange() {
|
||||
if (this.activeDid && this.meetingPassword) {
|
||||
await this.fetchMatches();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMatches(): Promise<void> {
|
||||
const usePropPairs =
|
||||
this.matchPairs != null &&
|
||||
Array.isArray(this.matchPairs) &&
|
||||
this.matchPairs.length > 0;
|
||||
const needApi = !usePropPairs;
|
||||
|
||||
if (
|
||||
needApi &&
|
||||
(!this.meetingPassword?.trim() || !this.apiServer || !this.activeDid)
|
||||
) {
|
||||
this.isLoading = false;
|
||||
this.matchedPerson = null;
|
||||
this.myPair = null;
|
||||
this.errorMessage = "";
|
||||
return;
|
||||
}
|
||||
if (usePropPairs && (!this.meetingPassword?.trim() || !this.activeDid)) {
|
||||
this.isLoading = false;
|
||||
this.matchedPerson = null;
|
||||
this.myPair = null;
|
||||
this.errorMessage = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = "";
|
||||
this.matchedPerson = null;
|
||||
this.myPair = null;
|
||||
|
||||
try {
|
||||
let pairs: MatchPair[] | null = null;
|
||||
if (usePropPairs) {
|
||||
// Shallow-copy so we can set decryptedContentObject without mutating the prop
|
||||
pairs = (this.matchPairs ?? []).map((p) => ({
|
||||
...p,
|
||||
participants: p.participants.map((part) => ({ ...part })),
|
||||
}));
|
||||
} else {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const response = await this.axios.get(
|
||||
`${this.apiServer}/api/partner/groupOnboardMatch`,
|
||||
{ headers },
|
||||
);
|
||||
pairs = response?.data?.data?.pairs ?? null;
|
||||
}
|
||||
|
||||
if (!Array.isArray(pairs) || pairs.length === 0) {
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt each participant's content and find the pair containing this user
|
||||
for (const pair of pairs) {
|
||||
if (!pair.participants || pair.participants.length !== 2) continue;
|
||||
|
||||
for (const participant of pair.participants) {
|
||||
try {
|
||||
const decrypted = await decryptMessage(
|
||||
participant.content,
|
||||
this.meetingPassword,
|
||||
);
|
||||
participant.decryptedContentObject = JSON.parse(decrypted);
|
||||
} catch {
|
||||
participant.decryptedContentObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
const myIndex = pair.participants.findIndex(
|
||||
(p) => p.issuerDid === this.activeDid,
|
||||
);
|
||||
if (myIndex === -1) continue;
|
||||
|
||||
this.myPair = pair;
|
||||
const other = pair.participants[1 - myIndex];
|
||||
const obj = other.decryptedContentObject;
|
||||
this.matchedPerson = {
|
||||
name: obj?.name ?? "",
|
||||
did: obj?.did ?? other.issuerDid,
|
||||
isRegistered: !!obj?.isRegistered,
|
||||
description: other.description,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error fetching group onboard match: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.errorMessage =
|
||||
serverMessageForUser(error as unknown as AxiosErrorResponse) ||
|
||||
"Failed to load your match.";
|
||||
this.matchedPerson = null;
|
||||
this.myPair = null;
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -49,9 +49,9 @@
|
||||
|
||||
<div class="flex justify-between">
|
||||
<!--
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
<button
|
||||
class="text-sm 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-3 py-1.5 rounded-md"
|
||||
title="Refresh members list now"
|
||||
@@ -198,9 +198,9 @@
|
||||
|
||||
<div v-if="membersToShow().length > 0" class="flex justify-between">
|
||||
<!--
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
<button
|
||||
class="text-sm 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-3 py-1.5 rounded-md"
|
||||
title="Refresh members list now"
|
||||
@@ -273,7 +273,7 @@ interface DecryptedMember {
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class MembersList extends Vue {
|
||||
export default class MeetingMembersList extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
@@ -448,8 +448,8 @@ export const NOTIFY_UNCONFIRMED_HOURS_DYNAMIC = {
|
||||
};
|
||||
|
||||
// Complex modal constants (for raw $notify calls with advanced features)
|
||||
// MembersList.vue complex modals
|
||||
// Used in: MembersList.vue (complex modal for adding contacts)
|
||||
// MeetingMembersList.vue complex modals
|
||||
// Used in: MeetingMembersList.vue (complex modal for adding contacts)
|
||||
export const NOTIFY_ADD_CONTACT_FIRST = {
|
||||
title: "Add as Contact First?",
|
||||
text: "This person is not in your contacts. Would you like to add them as a contact first?",
|
||||
@@ -457,7 +457,7 @@ export const NOTIFY_ADD_CONTACT_FIRST = {
|
||||
noText: "Skip Adding Contact",
|
||||
};
|
||||
|
||||
// Used in: MembersList.vue (complex modal for continuing without adding)
|
||||
// Used in: MeetingMembersList.vue (complex modal for continuing without adding)
|
||||
export const NOTIFY_CONTINUE_WITHOUT_ADDING = {
|
||||
title: "Continue Without Adding?",
|
||||
text: "Are you sure you want to proceed with admission? If they are not a contact, you will not know their name after this meeting.",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export interface UserProfile {
|
||||
description: string;
|
||||
generateEmbedding?: boolean;
|
||||
embeddingIsForEmptyString?: boolean;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
locLat2?: number;
|
||||
|
||||
@@ -164,7 +164,9 @@
|
||||
>
|
||||
{{ userProfileData.description }}
|
||||
</p>
|
||||
<p v-else class="text-slate-500 italic">No description.</p>
|
||||
<p v-else class="text-slate-500 italic">
|
||||
This person has no profile description or it's not visible to you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -319,31 +321,54 @@
|
||||
class="mt-4 pt-4 border-t border-slate-300"
|
||||
data-testid="generateEmbeddingSection"
|
||||
>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Always generate embedding
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2 mt-2">
|
||||
Always generate embedding
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="generateEmbedding"
|
||||
:disabled="generateEmbeddingSaving || generateEmbeddingLoading"
|
||||
:aria-checked="
|
||||
generateEmbedding ?? userProfileData?.generateEmbedding
|
||||
"
|
||||
:disabled="generateEmbeddingSaving || userProfileLoading"
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="generateEmbedding ? 'bg-blue-600' : 'bg-gray-200'"
|
||||
:class="
|
||||
(generateEmbedding ?? userProfileData?.generateEmbedding)
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200'
|
||||
"
|
||||
@click="toggleGenerateEmbedding"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition"
|
||||
:class="generateEmbedding ? 'translate-x-5' : 'translate-x-1'"
|
||||
:class="
|
||||
(generateEmbedding ?? userProfileData?.generateEmbedding)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-1'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ generateEmbedding ? "On" : "Off" }}
|
||||
<span v-if="generateEmbeddingLoading" class="ml-1">(loading…)</span>
|
||||
{{
|
||||
(generateEmbedding ?? userProfileData?.generateEmbedding)
|
||||
? "On"
|
||||
: "Off"
|
||||
}}
|
||||
<span v-if="userProfileLoading" class="ml-1">(loading…)</span>
|
||||
<span v-else-if="generateEmbeddingSaving" class="ml-1"
|
||||
>(saving…)</span
|
||||
>
|
||||
</span>
|
||||
<span class="text-sm text-gray-600">
|
||||
{{
|
||||
userProfileData?.embeddingIsForEmptyString == null
|
||||
? ""
|
||||
: userProfileData?.embeddingIsForEmptyString
|
||||
? "- Embedding is for blank description"
|
||||
: "- Embedding is for non-blank description"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -494,7 +519,7 @@ export default class DIDView extends Vue {
|
||||
contactLabels: string[] = [];
|
||||
|
||||
contactYaml = "";
|
||||
generateEmbedding: boolean | null = null;
|
||||
generateEmbedding: boolean | null = null; // used when there is no profile
|
||||
generateEmbeddingLoading = false;
|
||||
generateEmbeddingSaving = false;
|
||||
hitEnd = false;
|
||||
@@ -509,7 +534,6 @@ export default class DIDView extends Vue {
|
||||
showUserProfile = false;
|
||||
userProfileData: UserProfile | null = null;
|
||||
userProfileError: string | null = null;
|
||||
userProfileFetched = false;
|
||||
userProfileLoading = false;
|
||||
viewingDid?: string;
|
||||
|
||||
@@ -543,7 +567,7 @@ export default class DIDView extends Vue {
|
||||
await this.loadClaimsAbout();
|
||||
await this.checkIfOwnDID();
|
||||
if (this.showGeneralAdvanced && this.activeDid) {
|
||||
await this.loadGenerateEmbeddingState();
|
||||
await this.loadUserProfile();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,48 +647,36 @@ export default class DIDView extends Vue {
|
||||
this.isMyDid = allAccountDids.includes(this.viewingDid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads partner profile generateEmbedding state for the viewed DID (when showGeneralAdvanced).
|
||||
*/
|
||||
private async loadGenerateEmbeddingState() {
|
||||
if (!this.viewingDid || !this.activeDid) return;
|
||||
this.generateEmbeddingLoading = true;
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const url = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${encodeURIComponent(this.viewingDid)}`;
|
||||
const response = await this.axios.get(url, { headers });
|
||||
const data = response.data?.data;
|
||||
this.generateEmbedding =
|
||||
data && typeof data.generateEmbedding === "boolean"
|
||||
? data.generateEmbedding
|
||||
: false;
|
||||
} catch {
|
||||
this.generateEmbedding = false;
|
||||
} finally {
|
||||
this.generateEmbeddingLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the "always generate embedding" flag for the viewed DID on the partner API.
|
||||
* Only permissioned (admin) users can change this; API returns 403 otherwise.
|
||||
*/
|
||||
async toggleGenerateEmbedding() {
|
||||
if (
|
||||
!this.viewingDid ||
|
||||
!this.activeDid ||
|
||||
this.generateEmbeddingSaving ||
|
||||
this.generateEmbeddingLoading
|
||||
) {
|
||||
if (!this.viewingDid || !this.activeDid) {
|
||||
return;
|
||||
}
|
||||
const newValue = !this.generateEmbedding;
|
||||
const newValue = !(this.userProfileData
|
||||
? this.userProfileData.generateEmbedding
|
||||
: this.generateEmbedding);
|
||||
this.generateEmbeddingSaving = true;
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const url = `${this.partnerApiServer}/api/partner/userProfileGenerateEmbedding/${encodeURIComponent(this.viewingDid)}`;
|
||||
await this.axios.put(url, { generateEmbedding: newValue }, { headers });
|
||||
this.generateEmbedding = newValue;
|
||||
if (this.userProfileData) {
|
||||
this.userProfileData.generateEmbedding = newValue;
|
||||
this.userProfileData.embeddingIsForEmptyString = newValue; // the server should have generated it or erased it
|
||||
} else {
|
||||
this.generateEmbedding = newValue;
|
||||
if (newValue) {
|
||||
this.userProfileData = {
|
||||
description: "",
|
||||
issuerDid: this.viewingDid,
|
||||
generateEmbedding: newValue,
|
||||
embeddingIsForEmptyString: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.notify.success(
|
||||
newValue
|
||||
? "Contact tagged to always generate embedding."
|
||||
@@ -719,7 +731,7 @@ export default class DIDView extends Vue {
|
||||
*/
|
||||
toggleUserProfile() {
|
||||
this.showUserProfile = !this.showUserProfile;
|
||||
if (this.showUserProfile && !this.userProfileFetched) {
|
||||
if (this.showUserProfile && !this.userProfileData) {
|
||||
this.loadUserProfile();
|
||||
}
|
||||
}
|
||||
@@ -730,7 +742,6 @@ export default class DIDView extends Vue {
|
||||
*/
|
||||
async loadUserProfile() {
|
||||
if (!this.viewingDid || !this.activeDid) return;
|
||||
this.userProfileFetched = true;
|
||||
this.userProfileLoading = true;
|
||||
this.userProfileError = null;
|
||||
this.userProfileData = null;
|
||||
|
||||
@@ -40,8 +40,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members List -->
|
||||
<MembersList v-else :password="password" @error="handleError" />
|
||||
<div v-else>
|
||||
<MeetingMemberMatch
|
||||
:match-pairs="matchPairs"
|
||||
:meeting-password="password || ''"
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<!-- Members List -->
|
||||
<MeetingMembersList :password="password" @error="handleError" />
|
||||
</div>
|
||||
|
||||
<!-- Project Link Section -->
|
||||
<div v-if="projectLink" class="mt-8 p-4 border rounded-lg bg-white shadow">
|
||||
@@ -67,7 +75,8 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import MembersList from "../components/MembersList.vue";
|
||||
import MeetingMemberMatch from "../components/MeetingMemberMatch.vue";
|
||||
import MeetingMembersList from "../components/MeetingMembersList.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import { encryptMessage } from "../libs/crypto";
|
||||
import {
|
||||
@@ -84,7 +93,8 @@ import { AxiosErrorResponse } from "@/interfaces";
|
||||
components: {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
MembersList,
|
||||
MeetingMemberMatch,
|
||||
MeetingMembersList,
|
||||
UserNameDialog,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
|
||||
@@ -278,6 +278,13 @@
|
||||
@close="handleDialogClose"
|
||||
/>
|
||||
|
||||
<div v-if="!!matchPairs">
|
||||
<MeetingMemberMatch
|
||||
:meeting-password="currentMeeting.password || ''"
|
||||
class="mt-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Members Section -->
|
||||
<div
|
||||
v-if="!isLoading && currentMeeting != null && !!currentMeeting.password"
|
||||
@@ -307,20 +314,19 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<MembersList
|
||||
<MeetingMembersList
|
||||
ref="membersList"
|
||||
:password="currentMeeting.password || ''"
|
||||
:show-organizer-tools="true"
|
||||
class="mt-4"
|
||||
@error="handleMembersError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 border rounded-lg bg-white shadow">
|
||||
<!-- Pairwise matches (organizer only: this page is organizer's meeting) -->
|
||||
<div class="mt-6 pt-4 border-t border-gray-200">
|
||||
<h3 class="font-semibold mb-2">Pairs</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Match members by profile similarity
|
||||
</p>
|
||||
<div class="border-gray-200">
|
||||
<h3 class="font-semibold mb-2">Matching Pairs</h3>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -428,7 +434,8 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import MembersList from "../components/MembersList.vue";
|
||||
import MeetingMembersList from "../components/MeetingMembersList.vue";
|
||||
import MeetingMemberMatch from "../components/MeetingMemberMatch.vue";
|
||||
import MeetingProjectDialog from "../components/MeetingProjectDialog.vue";
|
||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||
import {
|
||||
@@ -473,7 +480,7 @@ interface MeetingSetupInputs {
|
||||
interface MatchPairParticipant {
|
||||
issuerDid: string;
|
||||
content: string;
|
||||
// there's a similar structure in MembersList.vue with extra Member info
|
||||
// there's a similar structure in MeetingMembersList.vue with extra Member info
|
||||
decryptedContentObject: {
|
||||
name: string;
|
||||
did: string;
|
||||
@@ -492,7 +499,8 @@ interface MatchPair {
|
||||
components: {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
MembersList,
|
||||
MeetingMembersList,
|
||||
MeetingMemberMatch,
|
||||
MeetingProjectDialog,
|
||||
ProjectIcon,
|
||||
},
|
||||
@@ -1102,7 +1110,6 @@ export default class OnboardMeetingView extends Vue {
|
||||
);
|
||||
this.matchPairs = null;
|
||||
this.previousMatchedPairs = [];
|
||||
this.notify.success("Matches cleared.", TIMEOUTS.STANDARD);
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error clearing matches: " + errorStringForLog(error),
|
||||
@@ -1187,20 +1194,20 @@ export default class OnboardMeetingView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dialog open event - stop auto-refresh in MembersList
|
||||
* Handle dialog open event - stop auto-refresh in MeetingMembersList
|
||||
*/
|
||||
handleDialogOpen(): void {
|
||||
const membersList = this.$refs.membersList as MembersList;
|
||||
const membersList = this.$refs.membersList as MeetingMembersList;
|
||||
if (membersList) {
|
||||
membersList.stopAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dialog close event - start auto-refresh in MembersList
|
||||
* Handle dialog close event - start auto-refresh in MeetingMembersList
|
||||
*/
|
||||
handleDialogClose(): void {
|
||||
const membersList = this.$refs.membersList as MembersList;
|
||||
const membersList = this.$refs.membersList as MeetingMembersList;
|
||||
if (membersList) {
|
||||
membersList.startAutoRefresh();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export async function createBuildConfig(platform: string): Promise<UserConfig> {
|
||||
server: {
|
||||
port: parseInt(process.env.VITE_PORT || "8080"),
|
||||
fs: { strict: false },
|
||||
//allowedHosts: ['bab3-68-69-173-46.ngrok-free.app'],
|
||||
//allowedHosts: ['*'],
|
||||
|
||||
// CORS headers disabled to allow images from any domain
|
||||
// This means SharedArrayBuffer is unavailable, but absurd-sql
|
||||
// will automatically fall back to IndexedDB mode which still works
|
||||
|
||||
Reference in New Issue
Block a user