Fix offer fulfillment detection + consistencies between ClaimView and ConfirmGiftView #167

Open
jose wants to merge 4 commits from claimview-fullfills-offer into master
  1. 22
      src/libs/endorserServer.ts
  2. 104
      src/views/ClaimView.vue
  3. 89
      src/views/ConfirmGiftView.vue

22
src/libs/endorserServer.ts

@ -1140,6 +1140,28 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}; };
/**
* Formats type string for display by adding spaces before capitals
* and optionally adds an appropriate article prefix (a/an)
*
* @param text - Text to format
* @returns Formatted string with article prefix
*/
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
text: string,
): string => {
const word = capitalizeAndInsertSpacesBeforeCaps(text);
if (word) {
// if the word starts with a vowel, use "an" instead of "a"
const firstLetter = word[0].toLowerCase();
const vowels = ["a", "e", "i", "o", "u"];
const particle = vowels.includes(firstLetter) ? "an" : "a";
return particle + " " + word;
} else {
return "";
}
};
/** /**
return readable summary of claim, or something generic return readable summary of claim, or something generic

104
src/views/ClaimView.vue

@ -24,7 +24,9 @@
<div class="flex columns-3"> <div class="flex columns-3">
<h2 class="text-md font-bold w-full"> <h2 class="text-md font-bold w-full">
{{ {{
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "") serverUtil.capitalizeAndInsertSpacesBeforeCaps(
veriClaim.claimType || "",
)
}} }}
<button <button
v-if="canEditClaim" v-if="canEditClaim"
@ -106,9 +108,9 @@
</div> </div>
<!-- Fullfills Links --> <!-- Fullfills Links -->
<div class="mt-4 empty:hidden">
<!-- fullfills links for a give --> <!-- fullfills links for a give -->
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4"> <div v-if="detailsForGive?.fulfillsPlanHandleId">
<router-link <router-link
:to=" :to="
'/project/' + '/project/' +
@ -116,30 +118,35 @@
" "
class="text-blue-500 mt-2" class="text-blue-500 mt-2"
> >
Fulfills a bigger plan... This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link> </router-link>
</div> </div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div <!-- Show offer fulfillment if this give fulfills an offer -->
v-if=" <div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
detailsForGive?.fulfillsType &&
detailsForGive?.fulfillsType !== 'PlanAction' &&
detailsForGive?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path --> <!-- router-link to /claim/ only changes URL path -->
<a <a
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click=" @click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId) showDifferentClaimPage(
detailsForGiveOfferFulfillment.offerHandleId,
)
" "
> >
Fulfills This fulfills
{{ {{
capitalizeAndInsertSpacesBeforeCaps( serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
detailsForGive.fulfillsType, detailsForGiveOfferFulfillment.offerType || "Offer",
) )
}}... }}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a> </a>
</div> </div>
@ -152,12 +159,16 @@
" "
class="text-blue-500 mt-4" class="text-blue-500 mt-4"
> >
Offered to a bigger plan... Offered to a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link> </router-link>
</div> </div>
<!-- Providers --> <!-- Providers -->
<div v-if="providersForGive?.length > 0" class="mt-4"> <div v-if="providersForGive?.length > 0">
<span>Other assistance provided by:</span> <span>Other assistance provided by:</span>
<ul class="ml-4"> <ul class="ml-4">
<li <li
@ -171,7 +182,11 @@
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click="handleProviderClick(provider)" @click="handleProviderClick(provider)"
> >
an activity... an activity
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a> </a>
</div> </div>
</div> </div>
@ -182,6 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" /> <font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that. {{ issuerName }} posted that.
@ -556,6 +572,17 @@ export default class ClaimView extends Vue {
fulfillsPlanHandleId?: string; fulfillsPlanHandleId?: string;
fulfillsType?: string; fulfillsType?: string;
fulfillsHandleId?: string; fulfillsHandleId?: string;
fullClaim?: {
fulfills?: Array<{
"@type": string;
identifier?: string;
}>;
};
} | null = null;
// Additional offer information extracted from the fulfills array
detailsForGiveOfferFulfillment: {
offerHandleId?: string;
offerType?: string;
} | null = null; } | null = null;
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null; detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
// Project information for fulfillsPlanHandleId // Project information for fulfillsPlanHandleId
@ -689,6 +716,7 @@ export default class ClaimView extends Vue {
this.confsVisibleToIdList = []; this.confsVisibleToIdList = [];
this.detailsForGive = null; this.detailsForGive = null;
this.detailsForOffer = null; this.detailsForOffer = null;
this.detailsForGiveOfferFulfillment = null;
this.projectInfo = null; this.projectInfo = null;
this.fullClaim = null; this.fullClaim = null;
this.fullClaimDump = ""; this.fullClaimDump = "";
@ -701,6 +729,33 @@ export default class ClaimView extends Vue {
this.veriClaimDidsVisible = {}; this.veriClaimDidsVisible = {};
} }
/**
* Extract offer fulfillment information from the fulfills array
*/
extractOfferFulfillment() {
if (!this.detailsForGive?.fullClaim?.fulfills) {
Review

There is one more case here: the fullClaim.fulfills can potentially be a single claim object that is not in an array. (This is the case for most of the schema.org properties: they are defined with a particular type but they could be an array of that type.) So check for fulfills["@type"] of "Offer" to set other fulfills variables.

There is one more case here: the fullClaim.fulfills can potentially be a single claim object that is not in an array. (This is the case for most of the schema.org properties: they are defined with a particular type but they could be an array of that type.) So check for `fulfills["@type"]` of "Offer" to set other `fulfills` variables.
jose commented 4 days ago
Review

What's a good way to recreate a claim object that has this particular structure? Gives that fulfill project offers always have @type: PlanAction, @type: Offer and @type: DonateAction. Gives that fulfill person offers always have @type: Offer and @type: DonateAction.

What's a good way to recreate a claim object that has this particular structure? Gives that fulfill project offers always have `@type: PlanAction`, `@type: Offer` and `@type: DonateAction`. Gives that fulfill person offers always have `@type: Offer` and `@type: DonateAction`.
this.detailsForGiveOfferFulfillment = null;
return;
}
const fulfills = this.detailsForGive.fullClaim.fulfills;
if (!Array.isArray(fulfills)) {
this.detailsForGiveOfferFulfillment = null;
return;
}
// Find the Offer in the fulfills array
const offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
if (offerFulfill) {
this.detailsForGiveOfferFulfillment = {
offerHandleId: offerFulfill.identifier,
offerType: offerFulfill["@type"],
};
} else {
this.detailsForGiveOfferFulfillment = null;
}
}
// ================================================= // =================================================
// UTILITY METHODS // UTILITY METHODS
// ================================================= // =================================================
@ -758,13 +813,6 @@ export default class ClaimView extends Vue {
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
} }
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
if (!text) return "";
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
totalConfirmers() { totalConfirmers() {
return ( return (
this.numConfsNotVisible + this.numConfsNotVisible +
@ -821,6 +869,8 @@ 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];
// Extract offer information from the fulfills array
this.extractOfferFulfillment();
} else { } else {
await this.$logError( await this.$logError(
"Error getting detailed give info: " + JSON.stringify(giveResp), "Error getting detailed give info: " + JSON.stringify(giveResp),

89
src/views/ConfirmGiftView.vue

@ -96,13 +96,15 @@
</div> </div>
<!-- Fullfills Links --> <!-- Fullfills Links -->
<div class="mt-4">
<!-- fullfills links for a give --> <!-- fullfills links for a give -->
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2"> <div v-if="giveDetails?.fulfillsPlanHandleId">
<router-link <router-link
:to=" :to="
'/project/' + '/project/' +
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '') encodeURIComponent(
giveDetails?.fulfillsPlanHandleId || '',
)
" "
class="text-blue-500 mt-2 cursor-pointer" class="text-blue-500 mt-2 cursor-pointer"
> >
@ -113,26 +115,23 @@
/> />
</router-link> </router-link>
</div> </div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div <!-- Show offer fulfillment if this give fulfills an offer -->
v-if=" <div v-if="giveDetailsOfferFulfillment?.offerHandleId">
giveDetails?.fulfillsType &&
giveDetails?.fulfillsType !== 'PlanAction' &&
giveDetails?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path --> <!-- router-link to /claim/ only changes URL path -->
<router-link <router-link
:to=" :to="
'/claim/' + '/claim/' +
encodeURIComponent(giveDetails?.fulfillsHandleId || '') encodeURIComponent(
giveDetailsOfferFulfillment.offerHandleId || '',
)
" "
class="text-blue-500 mt-2 cursor-pointer" class="text-blue-500 mt-2 cursor-pointer"
> >
This fulfills This fulfills
{{ {{
capitalizeAndInsertSpacesBeforeCapsWithAPrefix( serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
giveDetails?.fulfillsType || "", giveDetailsOfferFulfillment.offerType || "Offer",
) )
}} }}
<font-awesome <font-awesome
@ -145,6 +144,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" /> <font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that. {{ issuerName }} posted that.
@ -493,6 +493,11 @@ export default class ConfirmGiftView extends Vue {
confsVisibleErrorMessage = ""; confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
giveDetails?: GiveSummaryRecord; giveDetails?: GiveSummaryRecord;
// Additional offer information extracted from the fulfills array
giveDetailsOfferFulfillment: {
offerHandleId?: string;
offerType?: string;
} | null = null;
giverName = ""; giverName = "";
issuerName = ""; issuerName = "";
isLoading = false; isLoading = false;
@ -648,6 +653,8 @@ export default class ConfirmGiftView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
this.giveDetails = resp.data.data[0]; this.giveDetails = resp.data.data[0];
// Extract offer information from the fulfills array
this.extractOfferFulfillment();
} else { } else {
throw new Error("Error getting detailed give info: " + resp.status); throw new Error("Error getting detailed give info: " + resp.status);
} }
@ -707,6 +714,33 @@ export default class ConfirmGiftView extends Vue {
} }
} }
/**
* Extract offer fulfillment information from the fulfills array
*/
private extractOfferFulfillment() {
if (!this.giveDetails?.fullClaim?.fulfills) {
this.giveDetailsOfferFulfillment = null;
return;
}
const fulfills = this.giveDetails.fullClaim.fulfills;
if (!Array.isArray(fulfills)) {
this.giveDetailsOfferFulfillment = null;
return;
}
// Find the Offer in the fulfills array
const offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
if (offerFulfill) {
this.giveDetailsOfferFulfillment = {
offerHandleId: offerFulfill.identifier,
offerType: offerFulfill["@type"],
};
} else {
this.giveDetailsOfferFulfillment = null;
}
}
/** /**
* Fetches confirmer information for the claim * Fetches confirmer information for the claim
*/ */
@ -849,27 +883,6 @@ export default class ConfirmGiftView extends Vue {
); );
} }
/**
* Formats type string for display by adding spaces before capitals
* Optionally adds a prefix
*
* @param text - Text to format
* @param prefix - Optional prefix to add
* @returns Formatted string
*/
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
if (word) {
// if the word starts with a vowel, use "an" instead of "a"
const firstLetter = word[0].toLowerCase();
const vowels = ["a", "e", "i", "o", "u"];
const particle = vowels.includes(firstLetter) ? "an" : "a";
return particle + " " + word;
} else {
return "";
}
}
/** /**
* Initiates sharing of claim information * Initiates sharing of claim information
* Handles share functionality based on platform capabilities * Handles share functionality based on platform capabilities
@ -894,11 +907,5 @@ export default class ConfirmGiftView extends Vue {
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
this.veriClaimDump = ""; this.veriClaimDump = "";
} }
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
} }
</script> </script>

Loading…
Cancel
Save