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 23 hours 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 {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$logAndConsole!: (message: string, isError?: boolean) => Promise<void>;
stopAsking = false;

43
src/utils/PlatformServiceMixin.ts

@ -1062,6 +1062,39 @@ export const PlatformServiceMixin = {
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,
settings: Partial<Settings>,
): 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
@ -1217,5 +1255,10 @@ declare module "@vue/runtime-core" {
did: string,
settings: Partial<Settings>,
): 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 -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
aria-label="Go back"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="fa-fw" />
@ -26,14 +27,7 @@
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
}}
<button
v-if="
['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
"
v-if="canEditClaim"
title="Edit"
data-testId="editClaimButton"
@click="onClickEditClaim"
@ -60,6 +54,7 @@
v-if="veriClaim.id"
class="text-blue-500 ml-2 mt-2"
title="Copy Printable Certificate Link"
aria-label="Copy printable certificate link"
@click="
copyToClipboard(
'A link to the certificate page',
@ -74,6 +69,7 @@
<div class="flex justify-end w-full">
<button
title="Copy Link"
aria-label="Copy page link"
@click="copyToClipboard('A link to this page', windowDeepLink)"
>
<font-awesome icon="link" class="text-slate-500" />
@ -83,11 +79,7 @@
<div class="text-sm">
<div data-testId="description">
<font-awesome icon="message" class="fa-fw text-slate-400" />
{{
(veriClaim.claim?.itemOffered as any)?.description ||
(veriClaim.claim as any)?.description ||
""
}}
{{ claimDescription }}
</div>
<div>
<font-awesome icon="user" class="fa-fw text-slate-400" />
@ -96,17 +88,11 @@
<div>
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
Recorded
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
{{ formattedIssueDate }}
</div>
<div
v-if="(veriClaim.claim as any).image"
class="flex justify-center"
>
<a :href="(veriClaim.claim as any).image" target="_blank">
<img
:src="(veriClaim.claim as any).image"
class="h-24 rounded-xl"
/>
<div v-if="claimImage" class="flex justify-center">
<a :href="claimImage" target="_blank">
<img :src="claimImage" class="h-24 rounded-xl" />
</a>
</div>
@ -183,14 +169,7 @@
<div class="grow overflow-hidden">
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
provider.identifier.startsWith('did:')
? $router.push(
'/did/' +
encodeURIComponent(provider.identifier),
)
: showDifferentClaimPage(provider.identifier)
"
@click="handleProviderClick(provider)"
>
an activity...
</a>
@ -266,13 +245,7 @@
</div>
<div class="mt-2">
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
<span v-else-if="totalConfirmers() === 1">
One person has confirmed this.
</span>
<span v-else>
{{ totalConfirmers() }} people have confirmed this.
</span>
{{ confirmationStatusText }}
</div>
<div v-if="totalConfirmers() > 0">
@ -392,13 +365,7 @@
<font-awesome v-else icon="chevron-right" />
</h2>
<div v-if="showVeriClaimDump">
<div
v-if="
serverUtil.containsHiddenDid(veriClaim) &&
R.isEmpty(veriClaimDidsVisible)
"
class="mb-2"
>
<div v-if="isFullyHidden" class="mb-2">
Some of the details are not visible to you; they show as "HIDDEN". They
are not visible to any of your direct contacts, either.
<span v-if="canShare">
@ -425,7 +392,7 @@
</span>
</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
of your contacts.
<span v-if="canShare">
@ -484,11 +451,7 @@
class="text-blue-500"
>
<font-awesome icon="globe" class="fa-fw" />
{{
veriClaim.publicUrls[visDid].substring(
veriClaim.publicUrls[visDid].indexOf("//") + 2,
)
}}
{{ extractDomain(veriClaim.publicUrls[visDid] || "") }}
</a>
</span>
</span>
@ -560,9 +523,13 @@ import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
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 { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({
@ -609,7 +576,102 @@ export default class ClaimView extends Vue {
yaml = yaml;
libsUtil = libsUtil;
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() {
this.confirmerIdList = [];
@ -628,6 +690,28 @@ export default class ClaimView extends Vue {
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() {
const settings = await this.$settings();
@ -640,7 +724,7 @@ export default class ClaimView extends Vue {
try {
this.allMyDids = await libsUtil.retrieveAccountDids();
} catch (error) {
await logger.toConsoleAndDb(
await this.$logAndConsole(
"Error retrieving all account DIDs on home page:" + error,
true,
);
@ -676,10 +760,9 @@ export default class ClaimView extends Vue {
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
if (!text) return "";
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
totalConfirmers() {
@ -691,7 +774,7 @@ export default class ClaimView extends Vue {
}
// Isn't there a better way to make this available to the template?
didInfo(did: string) {
didInfo(did: string): string {
return serverUtil.didInfo(
did,
this.activeDid,
@ -719,7 +802,7 @@ export default class ClaimView extends Vue {
);
} else {
// 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(
{
group: "alert",
@ -747,7 +830,9 @@ export default class ClaimView extends Vue {
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
this.detailsForGive = giveResp.data.data[0];
} else {
logger.error("Error getting detailed give info:", giveResp);
await this.$logError(
"Error getting detailed give info: " + JSON.stringify(giveResp),
);
}
// look for providers
@ -766,7 +851,9 @@ export default class ClaimView extends Vue {
) {
this.providersForGive = providerResp.data.data;
} else {
logger.error("Error getting give providers:", giveResp);
await this.$logError(
"Error getting give providers: " + JSON.stringify(giveResp),
);
this.$notify(
{
group: "alert",
@ -789,7 +876,9 @@ export default class ClaimView extends Vue {
if (offerResp.status === 200) {
this.detailsForOffer = offerResp.data.data[0];
} else {
logger.error("Error getting detailed offer info:", offerResp);
await this.$logError(
"Error getting detailed offer info: " + JSON.stringify(offerResp),
);
this.$notify(
{
group: "alert",
@ -819,7 +908,9 @@ export default class ClaimView extends Vue {
}
} catch (error: unknown) {
const serverError = error as AxiosError;
logger.error("Error retrieving claim:", serverError);
await this.$logError(
"Error retrieving claim: " + JSON.stringify(serverError),
);
this.$notify(
{
group: "alert",
@ -844,7 +935,9 @@ export default class ClaimView extends Vue {
this.fullClaimDump = yaml.dump(this.fullClaim);
} else {
// 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(
{
group: "alert",
@ -856,7 +949,9 @@ export default class ClaimView extends Vue {
);
}
} 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;
if (serverError.response?.status === 403) {
let issuerPhrase = "";
@ -951,7 +1046,9 @@ export default class ClaimView extends Vue {
5000,
);
} else {
logger.error("Got error submitting the confirmation:", result);
await this.$logError(
"Got error submitting the confirmation: " + JSON.stringify(result),
);
this.$notify(
{
group: "alert",
@ -1013,7 +1110,7 @@ export default class ClaimView extends Vue {
});
}
onClickEditClaim() {
async onClickEditClaim() {
if (this.veriClaim.claimType === "GiveAction") {
const route = {
name: "gifted-details",
@ -1041,9 +1138,8 @@ export default class ClaimView extends Vue {
};
(this.$router as Router).push(route);
} else {
logger.error(
"Unrecognized claim type for edit:",
this.veriClaim.claimType,
await this.$logError(
"Unrecognized claim type for edit: " + this.veriClaim.claimType,
);
this.$notify(
{

Loading…
Cancel
Save