Browse Source

Refactor ClaimView.vue: remove inline template logic, improve types, and centralize logic

- Move all complex template logic to computed properties and methods
- Replace all `as any` usages with proper TypeScript types (OfferClaim, GiveActionClaim)
- Add computed property for claim image, removing inline image access
- Route all logging through PlatformServiceMixin
- Ensure all icon-only buttons have aria-labels for accessibility
- Remove unused imports and direct logger usage
- Lint clean: no warnings or errors remain
pull/142/head
Matthew Raymer 2 days ago
parent
commit
ddee99cb0b
  1. 1
      src/App.vue
  2. 43
      src/utils/PlatformServiceMixin.ts
  3. 240
      src/views/ClaimView.vue

1
src/App.vue

@ -345,7 +345,6 @@ interface Settings {
}) })
export default class App extends Vue { export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$logAndConsole!: (message: string, isError?: boolean) => Promise<void>;
stopAsking = false; stopAsking = false;

43
src/utils/PlatformServiceMixin.ts

@ -1062,6 +1062,39 @@ export const PlatformServiceMixin = {
return false; return false;
} }
}, },
// =================================================
// LOGGING METHODS (convenience methods for components)
// =================================================
/**
* Log message to database - $log()
* @param message Message to log
* @param level Log level (info, warn, error)
* @returns Promise<void>
*/
async $log(message: string, level?: string): Promise<void> {
return logger.toDb(message, level);
},
/**
* Log error message to database - $logError()
* @param message Error message to log
* @returns Promise<void>
*/
async $logError(message: string): Promise<void> {
return logger.toDb(message, "error");
},
/**
* Log message to console and database - $logAndConsole()
* @param message Message to log
* @param isError Whether this is an error message
* @returns Promise<void>
*/
async $logAndConsole(message: string, isError = false): Promise<void> {
return logger.toConsoleAndDb(message, isError);
},
}, },
}; };
@ -1123,6 +1156,11 @@ export interface IPlatformServiceMixin {
did: string, did: string,
settings: Partial<Settings>, settings: Partial<Settings>,
): Promise<boolean>; ): Promise<boolean>;
// Logging methods
$log(message: string, level?: string): Promise<void>;
$logError(message: string): Promise<void>;
$logAndConsole(message: string, isError?: boolean): Promise<void>;
} }
// TypeScript declaration merging to eliminate (this as any) type assertions // TypeScript declaration merging to eliminate (this as any) type assertions
@ -1217,5 +1255,10 @@ declare module "@vue/runtime-core" {
did: string, did: string,
settings: Partial<Settings>, settings: Partial<Settings>,
): Promise<boolean>; ): Promise<boolean>;
// Logging methods
$log(message: string, level?: string): Promise<void>;
$logError(message: string): Promise<void>;
$logAndConsole(message: string, isError?: boolean): Promise<void>;
} }
} }

240
src/views/ClaimView.vue

@ -8,6 +8,7 @@
<!-- Back --> <!-- Back -->
<button <button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
aria-label="Go back"
@click="$router.go(-1)" @click="$router.go(-1)"
> >
<font-awesome icon="chevron-left" class="fa-fw" /> <font-awesome icon="chevron-left" class="fa-fw" />
@ -26,14 +27,7 @@
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "") capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
}} }}
<button <button
v-if=" v-if="canEditClaim"
['GiveAction', 'Offer', 'PlanAction'].includes(
veriClaim.claimType as string,
) && veriClaim.issuer === activeDid
// a PlanAction agent also could edit one of those,
// but rather than add more Plan-specific logic to detect the agent
// we'll let them click the Project link and edit from there
"
title="Edit" title="Edit"
data-testId="editClaimButton" data-testId="editClaimButton"
@click="onClickEditClaim" @click="onClickEditClaim"
@ -60,6 +54,7 @@
v-if="veriClaim.id" v-if="veriClaim.id"
class="text-blue-500 ml-2 mt-2" class="text-blue-500 ml-2 mt-2"
title="Copy Printable Certificate Link" title="Copy Printable Certificate Link"
aria-label="Copy printable certificate link"
@click=" @click="
copyToClipboard( copyToClipboard(
'A link to the certificate page', 'A link to the certificate page',
@ -74,6 +69,7 @@
<div class="flex justify-end w-full"> <div class="flex justify-end w-full">
<button <button
title="Copy Link" title="Copy Link"
aria-label="Copy page link"
@click="copyToClipboard('A link to this page', windowDeepLink)" @click="copyToClipboard('A link to this page', windowDeepLink)"
> >
<font-awesome icon="link" class="text-slate-500" /> <font-awesome icon="link" class="text-slate-500" />
@ -83,11 +79,7 @@
<div class="text-sm"> <div class="text-sm">
<div data-testId="description"> <div data-testId="description">
<font-awesome icon="message" class="fa-fw text-slate-400" /> <font-awesome icon="message" class="fa-fw text-slate-400" />
{{ {{ claimDescription }}
(veriClaim.claim?.itemOffered as any)?.description ||
(veriClaim.claim as any)?.description ||
""
}}
</div> </div>
<div> <div>
<font-awesome icon="user" class="fa-fw text-slate-400" /> <font-awesome icon="user" class="fa-fw text-slate-400" />
@ -96,17 +88,11 @@
<div> <div>
<font-awesome icon="calendar" class="fa-fw text-slate-400" /> <font-awesome icon="calendar" class="fa-fw text-slate-400" />
Recorded Recorded
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }} {{ formattedIssueDate }}
</div> </div>
<div <div v-if="claimImage" class="flex justify-center">
v-if="(veriClaim.claim as any).image" <a :href="claimImage" target="_blank">
class="flex justify-center" <img :src="claimImage" class="h-24 rounded-xl" />
>
<a :href="(veriClaim.claim as any).image" target="_blank">
<img
:src="(veriClaim.claim as any).image"
class="h-24 rounded-xl"
/>
</a> </a>
</div> </div>
@ -183,14 +169,7 @@
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<a <a
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click=" @click="handleProviderClick(provider)"
provider.identifier.startsWith('did:')
? $router.push(
'/did/' +
encodeURIComponent(provider.identifier),
)
: showDifferentClaimPage(provider.identifier)
"
> >
an activity... an activity...
</a> </a>
@ -266,13 +245,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span> {{ confirmationStatusText }}
<span v-else-if="totalConfirmers() === 1">
One person has confirmed this.
</span>
<span v-else>
{{ totalConfirmers() }} people have confirmed this.
</span>
</div> </div>
<div v-if="totalConfirmers() > 0"> <div v-if="totalConfirmers() > 0">
@ -392,13 +365,7 @@
<font-awesome v-else icon="chevron-right" /> <font-awesome v-else icon="chevron-right" />
</h2> </h2>
<div v-if="showVeriClaimDump"> <div v-if="showVeriClaimDump">
<div <div v-if="isFullyHidden" class="mb-2">
v-if="
serverUtil.containsHiddenDid(veriClaim) &&
R.isEmpty(veriClaimDidsVisible)
"
class="mb-2"
>
Some of the details are not visible to you; they show as "HIDDEN". They Some of the details are not visible to you; they show as "HIDDEN". They
are not visible to any of your direct contacts, either. are not visible to any of your direct contacts, either.
<span v-if="canShare"> <span v-if="canShare">
@ -425,7 +392,7 @@
</span> </span>
</div> </div>
<div v-if="!R.isEmpty(veriClaimDidsVisible)"> <div v-if="hasPartialVisibility">
Some of the details are not visible to you but they are visible to some Some of the details are not visible to you but they are visible to some
of your contacts. of your contacts.
<span v-if="canShare"> <span v-if="canShare">
@ -484,11 +451,7 @@
class="text-blue-500" class="text-blue-500"
> >
<font-awesome icon="globe" class="fa-fw" /> <font-awesome icon="globe" class="fa-fw" />
{{ {{ extractDomain(veriClaim.publicUrls[visDid] || "") }}
veriClaim.publicUrls[visDid].substring(
veriClaim.publicUrls[visDid].indexOf("//") + 2,
)
}}
</a> </a>
</span> </span>
</span> </span>
@ -560,9 +523,13 @@ import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface } from "../constants/app"; import { APP_SERVER, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as serverUtil from "../libs/endorserServer"; import * as serverUtil from "../libs/endorserServer";
import { GenericCredWrapper, OfferClaim, ProviderInfo } from "../interfaces"; import {
GenericCredWrapper,
OfferClaim,
GiveActionClaim,
ProviderInfo,
} from "../interfaces";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({ @Component({
@ -609,7 +576,102 @@ export default class ClaimView extends Vue {
yaml = yaml; yaml = yaml;
libsUtil = libsUtil; libsUtil = libsUtil;
serverUtil = serverUtil; serverUtil = serverUtil;
window = window;
// =================================================
// COMPUTED PROPERTIES
// =================================================
/**
* Whether the current user can edit this claim
*/
get canEditClaim(): boolean {
return (
["GiveAction", "Offer", "PlanAction"].includes(
this.veriClaim.claimType as string,
) && this.veriClaim.issuer === this.activeDid
);
}
/**
* The description to display for this claim
*/
get claimDescription(): string {
const claim = this.veriClaim.claim;
// Handle Offer claims with itemOffered
if (this.veriClaim.claimType === "Offer") {
const offerClaim = claim as OfferClaim;
return (
offerClaim.itemOffered?.description || offerClaim.description || ""
);
}
// Handle GiveAction claims
if (this.veriClaim.claimType === "GiveAction") {
const giveClaim = claim as GiveActionClaim;
return giveClaim.description || "";
}
// Fallback for other claim types
return (claim as { description?: string })?.description || "";
}
/**
* Formatted issue date for display
*/
get formattedIssueDate(): string {
return (
this.veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") || ""
);
}
/**
* Text describing the confirmation status
*/
get confirmationStatusText(): string {
const count = this.totalConfirmers();
if (count === 0) return "Nobody has confirmed this.";
if (count === 1) return "One person has confirmed this.";
return `${count} people have confirmed this.`;
}
/**
* Whether the claim is fully hidden from the user
*/
get isFullyHidden(): boolean {
return (
serverUtil.containsHiddenDid(this.veriClaim) &&
R.isEmpty(this.veriClaimDidsVisible)
);
}
/**
* Whether the claim has some visibility through contacts
*/
get hasPartialVisibility(): boolean {
return !R.isEmpty(this.veriClaimDidsVisible);
}
/**
* The image URL for this claim if available
*/
get claimImage(): string | undefined {
const claim = this.veriClaim.claim;
// Handle different claim types that might have images
if (this.veriClaim.claimType === "Offer") {
const offerClaim = claim as OfferClaim;
return offerClaim.image;
}
if (this.veriClaim.claimType === "GiveAction") {
const giveClaim = claim as GiveActionClaim;
return giveClaim.image;
}
// Fallback for other claim types
return (claim as { image?: string })?.image;
}
resetThisValues() { resetThisValues() {
this.confirmerIdList = []; this.confirmerIdList = [];
@ -628,6 +690,28 @@ export default class ClaimView extends Vue {
this.veriClaimDidsVisible = {}; this.veriClaimDidsVisible = {};
} }
// =================================================
// UTILITY METHODS
// =================================================
/**
* Handle provider click navigation
*/
handleProviderClick(provider: ProviderInfo): void {
if (provider.identifier.startsWith("did:")) {
this.$router.push("/did/" + encodeURIComponent(provider.identifier));
} else {
this.showDifferentClaimPage(provider.identifier);
}
}
/**
* Extract domain from URL for display
*/
extractDomain(url: string): string {
return url.substring(url.indexOf("//") + 2);
}
async created() { async created() {
const settings = await this.$settings(); const settings = await this.$settings();
@ -640,7 +724,7 @@ export default class ClaimView extends Vue {
try { try {
this.allMyDids = await libsUtil.retrieveAccountDids(); this.allMyDids = await libsUtil.retrieveAccountDids();
} catch (error) { } catch (error) {
await logger.toConsoleAndDb( await this.$logAndConsole(
"Error retrieving all account DIDs on home page:" + error, "Error retrieving all account DIDs on home page:" + error,
true, true,
); );
@ -676,10 +760,9 @@ export default class ClaimView extends Vue {
// insert a space before any capital letters except the initial letter // insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case) // (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string) { capitalizeAndInsertSpacesBeforeCaps(text: string): string {
return !text if (!text) return "";
? "" return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
} }
totalConfirmers() { totalConfirmers() {
@ -691,7 +774,7 @@ export default class ClaimView extends Vue {
} }
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string) { didInfo(did: string): string {
return serverUtil.didInfo( return serverUtil.didInfo(
did, did,
this.activeDid, this.activeDid,
@ -719,7 +802,7 @@ export default class ClaimView extends Vue {
); );
} else { } else {
// actually, axios typically throws an error so we never get here // actually, axios typically throws an error so we never get here
logger.error("Error getting claim:", resp); await this.$logError("Error getting claim: " + JSON.stringify(resp));
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -747,7 +830,9 @@ export default class ClaimView extends Vue {
if (giveResp.status === 200 && giveResp.data.data?.length > 0) { if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
this.detailsForGive = giveResp.data.data[0]; this.detailsForGive = giveResp.data.data[0];
} else { } else {
logger.error("Error getting detailed give info:", giveResp); await this.$logError(
"Error getting detailed give info: " + JSON.stringify(giveResp),
);
} }
// look for providers // look for providers
@ -766,7 +851,9 @@ export default class ClaimView extends Vue {
) { ) {
this.providersForGive = providerResp.data.data; this.providersForGive = providerResp.data.data;
} else { } else {
logger.error("Error getting give providers:", giveResp); await this.$logError(
"Error getting give providers: " + JSON.stringify(giveResp),
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -789,7 +876,9 @@ export default class ClaimView extends Vue {
if (offerResp.status === 200) { if (offerResp.status === 200) {
this.detailsForOffer = offerResp.data.data[0]; this.detailsForOffer = offerResp.data.data[0];
} else { } else {
logger.error("Error getting detailed offer info:", offerResp); await this.$logError(
"Error getting detailed offer info: " + JSON.stringify(offerResp),
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -819,7 +908,9 @@ export default class ClaimView extends Vue {
} }
} catch (error: unknown) { } catch (error: unknown) {
const serverError = error as AxiosError; const serverError = error as AxiosError;
logger.error("Error retrieving claim:", serverError); await this.$logError(
"Error retrieving claim: " + JSON.stringify(serverError),
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -844,7 +935,9 @@ export default class ClaimView extends Vue {
this.fullClaimDump = yaml.dump(this.fullClaim); this.fullClaimDump = yaml.dump(this.fullClaim);
} else { } else {
// actually, axios typically throws an error so we never get here // actually, axios typically throws an error so we never get here
logger.error("Error getting full claim:", resp); await this.$logError(
"Error getting full claim: " + JSON.stringify(resp),
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -856,7 +949,9 @@ export default class ClaimView extends Vue {
); );
} }
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error retrieving full claim:", error); await this.$logError(
"Error retrieving full claim: " + JSON.stringify(error),
);
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError.response?.status === 403) { if (serverError.response?.status === 403) {
let issuerPhrase = ""; let issuerPhrase = "";
@ -951,7 +1046,9 @@ export default class ClaimView extends Vue {
5000, 5000,
); );
} else { } else {
logger.error("Got error submitting the confirmation:", result); await this.$logError(
"Got error submitting the confirmation: " + JSON.stringify(result),
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1013,7 +1110,7 @@ export default class ClaimView extends Vue {
}); });
} }
onClickEditClaim() { async onClickEditClaim() {
if (this.veriClaim.claimType === "GiveAction") { if (this.veriClaim.claimType === "GiveAction") {
const route = { const route = {
name: "gifted-details", name: "gifted-details",
@ -1041,9 +1138,8 @@ export default class ClaimView extends Vue {
}; };
(this.$router as Router).push(route); (this.$router as Router).push(route);
} else { } else {
logger.error( await this.$logError(
"Unrecognized claim type for edit:", "Unrecognized claim type for edit: " + this.veriClaim.claimType,
this.veriClaim.claimType,
); );
this.$notify( this.$notify(
{ {

Loading…
Cancel
Save