add missing file for contact-profile-checking & add info button for explanation
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
</button>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-2xl text-blue-500 ml-2"
|
||||
class="text-2xl text-blue-500 ml-4"
|
||||
@click="emitShowCopyInfo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
340
src/views/ContactProfileCheckView.vue
Normal file
340
src/views/ContactProfileCheckView.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<QuickNav selected="Contacts" />
|
||||
|
||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||
<TopMessage />
|
||||
|
||||
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
|
||||
<h1 class="grow text-xl text-center font-semibold leading-tight">
|
||||
Profile Check
|
||||
</h1>
|
||||
|
||||
<button
|
||||
class="order-first text-lg text-center leading-none p-1"
|
||||
@click="goBack()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="contactResults.length === 0"
|
||||
class="text-center text-slate-500 mt-8"
|
||||
>
|
||||
No contacts to check. Go back and select contacts first.
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-4 text-sm text-slate-600">
|
||||
Checked {{ completedCount }} of {{ contactResults.length }}
|
||||
{{ contactResults.length === 1 ? "contact" : "contacts" }}
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 mb-6">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: progressPercent + '%' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3">
|
||||
<li
|
||||
v-for="result in contactResults"
|
||||
:key="result.did"
|
||||
class="bg-slate-50 rounded-lg p-4 border border-slate-200"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="result.status === 'pending'" class="text-slate-400">
|
||||
<font-awesome icon="clock" class="fa-fw" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'checking'"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse fa-fw" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'has-profile'"
|
||||
class="text-green-600"
|
||||
>
|
||||
<font-awesome icon="circle-check" class="fa-fw" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'no-profile'"
|
||||
class="text-amber-500"
|
||||
>
|
||||
<font-awesome icon="circle-xmark" class="fa-fw" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'error'"
|
||||
class="text-red-500"
|
||||
>
|
||||
<font-awesome icon="triangle-exclamation" class="fa-fw" />
|
||||
</span>
|
||||
|
||||
<span class="font-medium text-slate-800 truncate">
|
||||
{{ result.name || "(no name)" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-sm">
|
||||
<span v-if="result.status === 'pending'" class="text-slate-400">
|
||||
Waiting…
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'checking'"
|
||||
class="text-blue-500"
|
||||
>
|
||||
Checking profile…
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'has-profile'"
|
||||
class="text-green-700"
|
||||
>
|
||||
Has profile
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'no-profile'"
|
||||
class="text-amber-600"
|
||||
>
|
||||
No profile
|
||||
</span>
|
||||
<span
|
||||
v-else-if="result.status === 'error'"
|
||||
class="text-red-600"
|
||||
>
|
||||
Error: {{ result.error }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
result.status === 'has-profile' && result.profileDescription
|
||||
"
|
||||
class="mt-2 text-sm text-slate-600 line-clamp-2"
|
||||
>
|
||||
{{ result.profileDescription }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'did', params: { did: result.did } }"
|
||||
class="flex-shrink-0 text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
>
|
||||
Contact
|
||||
</router-link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="isComplete" class="mt-6 p-4 bg-slate-100 rounded-lg">
|
||||
<h3 class="font-semibold text-slate-800 mb-2">Summary</h3>
|
||||
<div class="text-sm text-slate-600 space-y-1">
|
||||
<div>
|
||||
<span class="text-green-600 font-medium">{{ profileCount }}</span>
|
||||
{{ profileCount === 1 ? "contact has" : "contacts have" }} a profile
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-amber-600 font-medium">{{ noProfileCount }}</span>
|
||||
{{ noProfileCount === 1 ? "contact has" : "contacts have" }} no
|
||||
profile or it's not visible to you
|
||||
</div>
|
||||
<div v-if="errorCount > 0">
|
||||
<span class="text-red-600 font-medium">{{ errorCount }}</span>
|
||||
{{ errorCount === 1 ? "check" : "checks" }} failed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import {
|
||||
DEFAULT_PARTNER_API_SERVER,
|
||||
NotificationIface,
|
||||
} from "../constants/app";
|
||||
import { getHeaders } from "../libs/endorserServer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
|
||||
interface ContactCheckResult {
|
||||
did: string;
|
||||
name: string;
|
||||
status: "pending" | "checking" | "has-profile" | "no-profile" | "error";
|
||||
profileDescription?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
name: "ContactProfileCheckView",
|
||||
components: {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class ContactProfileCheckView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
activeDid = "";
|
||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||
contactResults: ContactCheckResult[] = [];
|
||||
|
||||
created() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
}
|
||||
|
||||
async mounted() {
|
||||
const settings = await this.$accountSettings();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||
this.activeDid = activeIdentity.activeDid || "";
|
||||
this.partnerApiServer =
|
||||
(settings.partnerApiServer as string) || DEFAULT_PARTNER_API_SERVER;
|
||||
|
||||
const didsParam = this.$route.query.dids as string;
|
||||
if (!didsParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFresh = sessionStorage.getItem("profileCheckFresh") === "true";
|
||||
sessionStorage.removeItem("profileCheckFresh");
|
||||
|
||||
if (!isFresh) {
|
||||
const cachedDids = sessionStorage.getItem("profileCheckDids");
|
||||
const cachedResults = sessionStorage.getItem("profileCheckResults");
|
||||
if (cachedDids === didsParam && cachedResults) {
|
||||
try {
|
||||
this.contactResults = JSON.parse(cachedResults);
|
||||
return;
|
||||
} catch {
|
||||
// fall through to fresh load
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionStorage.removeItem("profileCheckResults");
|
||||
sessionStorage.removeItem("profileCheckDids");
|
||||
|
||||
let dids: string[];
|
||||
try {
|
||||
dids = JSON.parse(didsParam);
|
||||
} catch {
|
||||
this.notify.error("Invalid contact data.", TIMEOUTS.LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const did of dids) {
|
||||
const contact = await this.$getContact(did);
|
||||
this.contactResults.push({
|
||||
did,
|
||||
name: contact?.name || "",
|
||||
status: "pending",
|
||||
});
|
||||
}
|
||||
|
||||
await this.runChecks();
|
||||
}
|
||||
|
||||
get completedCount(): number {
|
||||
return this.contactResults.filter(
|
||||
(r) => r.status !== "pending" && r.status !== "checking",
|
||||
).length;
|
||||
}
|
||||
|
||||
get progressPercent(): number {
|
||||
if (this.contactResults.length === 0) return 0;
|
||||
return Math.round((this.completedCount / this.contactResults.length) * 100);
|
||||
}
|
||||
|
||||
get isComplete(): boolean {
|
||||
return (
|
||||
this.contactResults.length > 0 &&
|
||||
this.completedCount === this.contactResults.length
|
||||
);
|
||||
}
|
||||
|
||||
get profileCount(): number {
|
||||
return this.contactResults.filter((r) => r.status === "has-profile").length;
|
||||
}
|
||||
|
||||
get noProfileCount(): number {
|
||||
return this.contactResults.filter((r) => r.status === "no-profile").length;
|
||||
}
|
||||
|
||||
get errorCount(): number {
|
||||
return this.contactResults.filter((r) => r.status === "error").length;
|
||||
}
|
||||
|
||||
private async runChecks() {
|
||||
if (!this.activeDid) {
|
||||
this.notify.error("No active identity.", TIMEOUTS.LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.contactResults.length; i++) {
|
||||
this.contactResults[i].status = "checking";
|
||||
this.contactResults = [...this.contactResults];
|
||||
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const url =
|
||||
`${this.partnerApiServer}/api/partner/userProfileForIssuer/` +
|
||||
encodeURIComponent(this.contactResults[i].did);
|
||||
const response = await this.axios.get(url, { headers });
|
||||
const data = response.data?.data;
|
||||
|
||||
if (data && data.description) {
|
||||
this.contactResults[i].status = "has-profile";
|
||||
this.contactResults[i].profileDescription = data.description;
|
||||
} else {
|
||||
this.contactResults[i].status = "no-profile";
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const axiosErr = err as {
|
||||
response?: { status?: number; data?: { error?: string } };
|
||||
};
|
||||
if (axiosErr.response?.status === 404) {
|
||||
this.contactResults[i].status = "no-profile";
|
||||
} else {
|
||||
this.contactResults[i].status = "error";
|
||||
this.contactResults[i].error =
|
||||
axiosErr.response?.data?.error || "Could not check profile";
|
||||
logger.error(
|
||||
`Failed to check profile for ${this.contactResults[i].did}:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.contactResults = [...this.contactResults];
|
||||
}
|
||||
|
||||
const didsParam = this.$route.query.dids as string;
|
||||
if (didsParam) {
|
||||
sessionStorage.setItem("profileCheckDids", didsParam);
|
||||
sessionStorage.setItem(
|
||||
"profileCheckResults",
|
||||
JSON.stringify(this.contactResults),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.$router.back();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
<!-- Label Filter -->
|
||||
<div v-if="allLabels.length > 0" class="mt-4 mb-2">
|
||||
<div class="flex items-center justify-between pl-[16.666%] pr-[16.666%]">
|
||||
<div class="flex items-center justify-between pl-[10%] pr-[10%]">
|
||||
<button
|
||||
class="text-sm font-medium text-blue-600 flex items-center gap-1"
|
||||
@click="showLabelFilter = !showLabelFilter"
|
||||
@@ -133,11 +133,14 @@
|
||||
class="my-3 flex justify-center"
|
||||
>
|
||||
<button
|
||||
class="text-sm uppercase 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"
|
||||
class="text-sm 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"
|
||||
@click="checkSelectedProfiles"
|
||||
>
|
||||
<font-awesome icon="magnifying-glass" class="fa-fw mr-1" />
|
||||
Check for Profile
|
||||
<font-awesome icon="circle-user" class="fa-fw mr-1" />
|
||||
Check for Public Profile
|
||||
</button>
|
||||
<button class="ml-4 text-2xl text-blue-500" @click="showProfileCheckInfo">
|
||||
<font-awesome icon="circle-info" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -617,6 +620,13 @@ export default class ContactsView extends Vue {
|
||||
/**
|
||||
* Navigate to profile check view with selected contacts
|
||||
*/
|
||||
showProfileCheckInfo() {
|
||||
this.notify.info(
|
||||
"This User Profile visibility check is a useful report for meeting organizers when running meetings that match people based on common interests.",
|
||||
TIMEOUTS.VERY_LONG,
|
||||
);
|
||||
}
|
||||
|
||||
checkSelectedProfiles() {
|
||||
sessionStorage.setItem("profileCheckFresh", "true");
|
||||
this.$router.push({
|
||||
|
||||
Reference in New Issue
Block a user