add missing file for contact-profile-checking & add info button for explanation
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="circle-info"
|
icon="circle-info"
|
||||||
class="text-2xl text-blue-500 ml-2"
|
class="text-2xl text-blue-500 ml-4"
|
||||||
@click="emitShowCopyInfo"
|
@click="emitShowCopyInfo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 -->
|
<!-- Label Filter -->
|
||||||
<div v-if="allLabels.length > 0" class="mt-4 mb-2">
|
<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
|
<button
|
||||||
class="text-sm font-medium text-blue-600 flex items-center gap-1"
|
class="text-sm font-medium text-blue-600 flex items-center gap-1"
|
||||||
@click="showLabelFilter = !showLabelFilter"
|
@click="showLabelFilter = !showLabelFilter"
|
||||||
@@ -133,11 +133,14 @@
|
|||||||
class="my-3 flex justify-center"
|
class="my-3 flex justify-center"
|
||||||
>
|
>
|
||||||
<button
|
<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"
|
@click="checkSelectedProfiles"
|
||||||
>
|
>
|
||||||
<font-awesome icon="magnifying-glass" class="fa-fw mr-1" />
|
<font-awesome icon="circle-user" class="fa-fw mr-1" />
|
||||||
Check for Profile
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -617,6 +620,13 @@ export default class ContactsView extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Navigate to profile check view with selected contacts
|
* 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() {
|
checkSelectedProfiles() {
|
||||||
sessionStorage.setItem("profileCheckFresh", "true");
|
sessionStorage.setItem("profileCheckFresh", "true");
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user