forked from trent_larson/crowd-funder-for-time-pwa
Merge pull request 'offer editing' (#123) from offer-edit into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#123
This commit is contained in:
@@ -55,7 +55,7 @@
|
|||||||
}"
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
Photo & Details ...
|
Photo & more options ...
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,18 +36,27 @@
|
|||||||
<fa icon="chevron-right" />
|
<fa icon="chevron-right" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row mt-2">
|
<div class="mt-4 flex justify-center">
|
||||||
<span
|
<span>
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
<router-link
|
||||||
>
|
:to="{
|
||||||
Expiration
|
name: 'offer-details',
|
||||||
|
query: {
|
||||||
|
amountInput,
|
||||||
|
description,
|
||||||
|
offererDid: activeDid,
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
recipientDid,
|
||||||
|
recipientName,
|
||||||
|
unitCode: amountUnitCode,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
Conditions & more options...
|
||||||
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="w-full border border-slate-400 px-2 py-2 rounded-r"
|
|
||||||
:placeholder="datePlaceholder()"
|
|
||||||
v-model="expirationDateInput"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center mt-6 mb-2 italic">
|
<p class="text-center mt-6 mb-2 italic">
|
||||||
Sign & Send to publish to the world
|
Sign & Send to publish to the world
|
||||||
@@ -71,7 +80,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DateTime } from "luxon";
|
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "@/constants/app";
|
||||||
@@ -84,7 +92,8 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|||||||
export default class OfferDialog extends Vue {
|
export default class OfferDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
@Prop projectId? = "";
|
@Prop projectId?;
|
||||||
|
@Prop projectName?;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -94,13 +103,15 @@ export default class OfferDialog extends Vue {
|
|||||||
description = "";
|
description = "";
|
||||||
expirationDateInput = "";
|
expirationDateInput = "";
|
||||||
recipientDid? = "";
|
recipientDid? = "";
|
||||||
|
recipientName? = "";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
async open(recipientDid?: string) {
|
async open(recipientDid?: string, recipientName?: string) {
|
||||||
try {
|
try {
|
||||||
this.recipientDid = recipientDid;
|
this.recipientDid = recipientDid;
|
||||||
|
this.recipientName = recipientName;
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
@@ -146,12 +157,6 @@ export default class OfferDialog extends Vue {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
datePlaceholder() {
|
|
||||||
return (
|
|
||||||
"Date, eg. " + DateTime.now().plus({ month: 1 }).toISO().slice(0, 10)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.close();
|
this.close();
|
||||||
this.eraseValues();
|
this.eraseValues();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export interface ClaimResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericVerifiableCredential {
|
export interface GenericVerifiableCredential {
|
||||||
"@context"?: string;
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": string;
|
"@type": string;
|
||||||
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,7 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
|||||||
id: string;
|
id: string;
|
||||||
issuedAt: string;
|
issuedAt: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
|
publicUrls?: Record<string, string>; // only for IDs that want to be public
|
||||||
}
|
}
|
||||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||||
{
|
{
|
||||||
@@ -139,13 +140,13 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
|||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id8
|
// https://endorser.ch/doc/html/transactions.html#id8
|
||||||
export interface OfferVerifiableCredential {
|
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer
|
||||||
"@type": "Offer";
|
"@type": "Offer";
|
||||||
description?: string;
|
description?: string; // conditions for the offer
|
||||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||||
itemOffered?: {
|
itemOffered?: {
|
||||||
description?: string;
|
description?: string; // description of the item
|
||||||
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
||||||
};
|
};
|
||||||
offeredBy?: { identifier: string };
|
offeredBy?: { identifier: string };
|
||||||
@@ -155,7 +156,7 @@ export interface OfferVerifiableCredential {
|
|||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id7
|
// https://endorser.ch/doc/html/transactions.html#id7
|
||||||
export interface PlanVerifiableCredential {
|
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context": "https://schema.org";
|
"@context": "https://schema.org";
|
||||||
"@type": "PlanAction";
|
"@type": "PlanAction";
|
||||||
name: string;
|
name: string;
|
||||||
@@ -563,6 +564,8 @@ export async function setPlanInCache(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct GiveAction VC for submission to server
|
* Construct GiveAction VC for submission to server
|
||||||
|
*
|
||||||
|
* @param lastClaimId supplied when editing a previous claim
|
||||||
*/
|
*/
|
||||||
export function hydrateGive(
|
export function hydrateGive(
|
||||||
vcClaimOrig?: GiveVerifiableCredential,
|
vcClaimOrig?: GiveVerifiableCredential,
|
||||||
@@ -587,6 +590,7 @@ export function hydrateGive(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (lastClaimId) {
|
if (lastClaimId) {
|
||||||
|
// this is an edit
|
||||||
vcClaim.lastClaimId = lastClaimId;
|
vcClaim.lastClaimId = lastClaimId;
|
||||||
delete vcClaim.identifier;
|
delete vcClaim.identifier;
|
||||||
}
|
}
|
||||||
@@ -594,16 +598,17 @@ export function hydrateGive(
|
|||||||
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
||||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||||
vcClaim.description = description || undefined;
|
vcClaim.description = description || undefined;
|
||||||
vcClaim.object = amount
|
vcClaim.object =
|
||||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
amount && !isNaN(amount)
|
||||||
: undefined;
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// ensure fulfills is an array
|
// ensure fulfills is an array
|
||||||
if (!Array.isArray(vcClaim.fulfills)) {
|
if (!Array.isArray(vcClaim.fulfills)) {
|
||||||
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
||||||
}
|
}
|
||||||
// ... and replace or add each element, ending with Trade or Donate
|
// ... and replace or add each element, ending with Trade or Donate
|
||||||
// I realize this doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
||||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||||
(elem) => elem["@type"] !== "PlanAction",
|
(elem) => elem["@type"] !== "PlanAction",
|
||||||
);
|
);
|
||||||
@@ -639,8 +644,8 @@ export function hydrateGive(
|
|||||||
*
|
*
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
* @param toDid
|
* @param toDid
|
||||||
* @param description may be null; should have this or amount
|
* @param description may be null
|
||||||
* @param amount may be null; should have this or description
|
* @param amount may be null
|
||||||
*/
|
*/
|
||||||
export async function createAndSubmitGive(
|
export async function createAndSubmitGive(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
@@ -667,6 +672,7 @@ export async function createAndSubmitGive(
|
|||||||
fulfillsOfferHandleId,
|
fulfillsOfferHandleId,
|
||||||
isTrade,
|
isTrade,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as GenericVerifiableCredential,
|
vcClaim as GenericVerifiableCredential,
|
||||||
@@ -680,9 +686,9 @@ export async function createAndSubmitGive(
|
|||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
* @param toDid
|
* @param toDid may be null if project is provided
|
||||||
* @param description may be null; should have this or amount
|
* @param description may be null
|
||||||
* @param amount may be null; should have this or description
|
* @param amount may be null
|
||||||
*/
|
*/
|
||||||
export async function editAndSubmitGive(
|
export async function editAndSubmitGive(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
@@ -720,51 +726,128 @@ export async function editAndSubmitGive(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct Offer VC for submission to server
|
||||||
|
*
|
||||||
|
* @param lastClaimId supplied when editing a previous claim
|
||||||
|
*/
|
||||||
|
export function hydrateOffer(
|
||||||
|
vcClaimOrig?: OfferVerifiableCredential,
|
||||||
|
fromDid?: string,
|
||||||
|
toDid?: string,
|
||||||
|
itemDescription?: string,
|
||||||
|
amount?: number,
|
||||||
|
unitCode?: string,
|
||||||
|
conditionDescription?: string,
|
||||||
|
fulfillsProjectHandleId?: string,
|
||||||
|
validThrough?: string,
|
||||||
|
lastClaimId?: string,
|
||||||
|
): OfferVerifiableCredential {
|
||||||
|
// Remember: replace values or erase if it's null
|
||||||
|
|
||||||
|
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
||||||
|
? R.clone(vcClaimOrig)
|
||||||
|
: {
|
||||||
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
|
"@type": "Offer",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastClaimId) {
|
||||||
|
// this is an edit
|
||||||
|
vcClaim.lastClaimId = lastClaimId;
|
||||||
|
delete vcClaim.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
|
||||||
|
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||||
|
vcClaim.description = conditionDescription || undefined;
|
||||||
|
|
||||||
|
vcClaim.includesObject =
|
||||||
|
amount && !isNaN(amount)
|
||||||
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (itemDescription || fulfillsProjectHandleId) {
|
||||||
|
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
||||||
|
vcClaim.itemOffered.description = itemDescription || undefined;
|
||||||
|
if (fulfillsProjectHandleId) {
|
||||||
|
vcClaim.itemOffered.isPartOf = {
|
||||||
|
"@type": "PlanAction",
|
||||||
|
identifier: fulfillsProjectHandleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vcClaim.validThrough = validThrough || undefined;
|
||||||
|
|
||||||
|
return vcClaim;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param identity
|
* @param identity
|
||||||
* @param description may be null; should have this or amount
|
* @param description may be null
|
||||||
* @param amount may be null; should have this or description
|
* @param amount may be null
|
||||||
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
* @param validThrough ISO 8601 date string YYYY-MM-DD (may be null)
|
||||||
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
||||||
*/
|
*/
|
||||||
export async function createAndSubmitOffer(
|
export async function createAndSubmitOffer(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
apiServer: string,
|
apiServer: string,
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
description?: string,
|
itemDescription: string,
|
||||||
amount?: number,
|
amount?: number,
|
||||||
unitCode?: string,
|
unitCode?: string,
|
||||||
expirationDate?: string,
|
conditionDescription?: string,
|
||||||
|
validThrough?: string,
|
||||||
recipientDid?: string,
|
recipientDid?: string,
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
const vcClaim: OfferVerifiableCredential = {
|
const vcClaim = hydrateOffer(
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
undefined,
|
||||||
"@type": "Offer",
|
issuerDid,
|
||||||
offeredBy: { identifier: issuerDid },
|
recipientDid,
|
||||||
validThrough: expirationDate || undefined,
|
itemDescription,
|
||||||
};
|
amount,
|
||||||
if (amount) {
|
unitCode,
|
||||||
vcClaim.includesObject = {
|
conditionDescription,
|
||||||
amountOfThisGood: amount,
|
fulfillsProjectHandleId,
|
||||||
unitCode: unitCode || "HUR",
|
validThrough,
|
||||||
};
|
undefined,
|
||||||
}
|
);
|
||||||
if (description) {
|
return createAndSubmitClaim(
|
||||||
vcClaim.itemOffered = { description };
|
vcClaim as OfferVerifiableCredential,
|
||||||
}
|
issuerDid,
|
||||||
if (recipientDid) {
|
apiServer,
|
||||||
vcClaim.recipient = { identifier: recipientDid };
|
axios,
|
||||||
}
|
);
|
||||||
if (fulfillsProjectHandleId) {
|
}
|
||||||
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
|
||||||
vcClaim.itemOffered.isPartOf = {
|
export async function editAndSubmitOffer(
|
||||||
"@type": "PlanAction",
|
axios: Axios,
|
||||||
identifier: fulfillsProjectHandleId,
|
apiServer: string,
|
||||||
};
|
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||||
}
|
issuerDid: string,
|
||||||
|
itemDescription: string,
|
||||||
|
amount?: number,
|
||||||
|
unitCode?: string,
|
||||||
|
conditionDescription?: string,
|
||||||
|
validThrough?: string,
|
||||||
|
recipientDid?: string,
|
||||||
|
fulfillsProjectHandleId?: string,
|
||||||
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
|
const vcClaim = hydrateOffer(
|
||||||
|
fullClaim.claim,
|
||||||
|
issuerDid,
|
||||||
|
recipientDid,
|
||||||
|
itemDescription,
|
||||||
|
amount,
|
||||||
|
unitCode,
|
||||||
|
conditionDescription,
|
||||||
|
fulfillsProjectHandleId,
|
||||||
|
validThrough,
|
||||||
|
fullClaim.id,
|
||||||
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as OfferVerifiableCredential,
|
vcClaim as OfferVerifiableCredential,
|
||||||
issuerDid,
|
issuerDid,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: "/gifted-details",
|
path: "/gifted-details",
|
||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
component: () => import("../views/GiftedDetails.vue"),
|
component: () => import("@/views/GiftedDetailsView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/help",
|
path: "/help",
|
||||||
@@ -143,6 +143,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "new-identifier",
|
name: "new-identifier",
|
||||||
component: () => import("../views/NewIdentifierView.vue"),
|
component: () => import("../views/NewIdentifierView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/offer-details/:id?",
|
||||||
|
name: "offer-details",
|
||||||
|
component: () => import("../views/OfferDetailsView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/project/:id?",
|
path: "/project/:id?",
|
||||||
name: "project",
|
name: "project",
|
||||||
|
|||||||
@@ -24,13 +24,15 @@
|
|||||||
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
|
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="
|
||||||
veriClaim.claimType === 'GiveAction' &&
|
['GiveAction', 'Offer'].includes(
|
||||||
veriClaim.issuer === activeDid
|
veriClaim.claimType as string,
|
||||||
|
) && veriClaim.issuer === activeDid
|
||||||
"
|
"
|
||||||
@click="onClickEditClaim"
|
@click="onClickEditClaim"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
|
data-testId="editClaimButton"
|
||||||
>
|
>
|
||||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
|
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
@@ -49,11 +51,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<span v-show="showIdCopy">Copied ID</span>
|
<span v-show="showIdCopy">Copied ID</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div data-testId="description">
|
||||||
<fa icon="message" class="fa-fw text-slate-400" />
|
<fa icon="message" class="fa-fw text-slate-400" />
|
||||||
{{
|
{{
|
||||||
veriClaim.claim?.description ||
|
veriClaim.claim?.itemOffered?.description ||
|
||||||
veriClaim.claim?.itemOffered?.description
|
veriClaim.claim?.description
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -402,7 +404,7 @@
|
|||||||
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
||||||
<pre
|
<pre
|
||||||
v-if="showVeriClaimDump"
|
v-if="showVeriClaimDump"
|
||||||
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
||||||
>{{ veriClaimDump }}</pre
|
>{{ veriClaimDump }}</pre
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -425,7 +427,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<pre>{{ fullClaimDump }}</pre>
|
<pre
|
||||||
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
||||||
|
>{{ fullClaimDump }}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -843,15 +848,41 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickEditClaim() {
|
onClickEditClaim() {
|
||||||
const route = {
|
if (this.veriClaim.claimType === "GiveAction") {
|
||||||
name: "gifted-details",
|
const route = {
|
||||||
query: {
|
name: "gifted-details",
|
||||||
prevCredToEdit: JSON.stringify(this.veriClaim),
|
query: {
|
||||||
destinationPathAfter:
|
prevCredToEdit: JSON.stringify(this.veriClaim),
|
||||||
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
|
destinationPathAfter:
|
||||||
},
|
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
|
||||||
};
|
},
|
||||||
(this.$router as Router).push(route);
|
};
|
||||||
|
(this.$router as Router).push(route);
|
||||||
|
} else if (this.veriClaim.claimType === "Offer") {
|
||||||
|
const route = {
|
||||||
|
name: "offer-details",
|
||||||
|
query: {
|
||||||
|
prevCredToEdit: JSON.stringify(this.veriClaim),
|
||||||
|
destinationPathAfter:
|
||||||
|
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
(this.$router as Router).push(route);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Unrecognized claim type for edit:",
|
||||||
|
this.veriClaim.claimType,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "This is an unrecognized claim type.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -271,7 +271,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="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-2 py-1.5 rounded-md border border-blue-400"
|
class="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-2 py-1.5 rounded-md border border-blue-400"
|
||||||
@click="openOfferDialog(contact.did)"
|
@click="openOfferDialog(contact.did, contact.name)"
|
||||||
>
|
>
|
||||||
Offer
|
Offer
|
||||||
</button>
|
</button>
|
||||||
@@ -1131,8 +1131,11 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openOfferDialog(recipientDid: string) {
|
openOfferDialog(recipientDid: string, recipientName: string) {
|
||||||
(this.$refs.customOfferDialog as OfferDialog).open(recipientDid);
|
(this.$refs.customOfferDialog as OfferDialog).open(
|
||||||
|
recipientDid,
|
||||||
|
recipientName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onClickCancelName() {
|
private async onClickCancelName() {
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
this.hideBackButton =
|
this.hideBackButton =
|
||||||
(this.$route as Router).query["hideBackButton"] === "true";
|
(this.$route as Router).query["hideBackButton"] === "true";
|
||||||
this.message = ((this.$route as Router).query["message"] as string) || "";
|
this.message = ((this.$route as Router).query["message"] as string) || "";
|
||||||
|
|
||||||
// find any offer ID
|
// find any offer ID
|
||||||
const fulfills = this.prevCredToEdit?.claim?.fulfills;
|
const fulfills = this.prevCredToEdit?.claim?.fulfills;
|
||||||
const fulfillsArray = Array.isArray(fulfills)
|
const fulfillsArray = Array.isArray(fulfills)
|
||||||
@@ -351,6 +352,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// these should be functions but something's wrong with the syntax in the <> conditional
|
||||||
this.givenToProject = !!this.projectId;
|
this.givenToProject = !!this.projectId;
|
||||||
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
|
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
|
||||||
|
|
||||||
@@ -549,7 +551,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "To assign to a project, you must open this dialog through a project.",
|
text: "To assign to a project, you must open this page through a project.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
@@ -574,7 +576,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "To assign to a recipient, you must open this dialog from a contact.",
|
text: "To assign to a recipient, you must open this page from a contact.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
@@ -694,7 +696,6 @@ export default class GiftedDetails extends Vue {
|
|||||||
constructGiveParam() {
|
constructGiveParam() {
|
||||||
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
|
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
|
||||||
const projectId = this.givenToProject ? this.projectId : undefined;
|
const projectId = this.givenToProject ? this.projectId : undefined;
|
||||||
// const giveClaim = constructGive(
|
|
||||||
const giveClaim = hydrateGive(
|
const giveClaim = hydrateGive(
|
||||||
this.prevCredToEdit?.claim as GiveVerifiableCredential,
|
this.prevCredToEdit?.claim as GiveVerifiableCredential,
|
||||||
this.giverDid,
|
this.giverDid,
|
||||||
@@ -97,8 +97,8 @@
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
:disabled="!startDateInput"
|
:disabled="!startDateInput"
|
||||||
v-model="startTimeInput"
|
|
||||||
placeholder="Start Time"
|
placeholder="Start Time"
|
||||||
|
v-model="startTimeInput"
|
||||||
type="time"
|
type="time"
|
||||||
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
||||||
/>
|
/>
|
||||||
|
|||||||
633
src/views/OfferDetailsView.vue
Normal file
633
src/views/OfferDetailsView.vue
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Back -->
|
||||||
|
<div
|
||||||
|
v-if="!hideBackButton"
|
||||||
|
class="text-lg text-center font-light relative px-7"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="cancelBack()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Offered</h1>
|
||||||
|
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">
|
||||||
|
<span>
|
||||||
|
Offer to
|
||||||
|
{{
|
||||||
|
offeredToProject
|
||||||
|
? projectName
|
||||||
|
: offeredToRecipient
|
||||||
|
? recipientName
|
||||||
|
: "someone unidentified"
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</h1>
|
||||||
|
<textarea
|
||||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
|
placeholder="What was offered"
|
||||||
|
v-model="itemDescription"
|
||||||
|
data-testId="itemDescription"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
|
||||||
|
@click="changeUnitCode()"
|
||||||
|
>
|
||||||
|
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="amountInput === '0' ? null : decrement()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||||
|
v-model="amountInput"
|
||||||
|
data-testId="inputOfferAmount"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="increment()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row mt-2">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
|
>
|
||||||
|
Conditions
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
class="w-full border border-slate-400 px-3 py-2 rounded-r"
|
||||||
|
placeholder="Prerequisites, other people to include, etc."
|
||||||
|
v-model="conditionDescription"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row mt-2">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
|
>
|
||||||
|
{{ validThroughDateInput ? "" : "No" }} Expiration
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="validThroughDateInput"
|
||||||
|
type="date"
|
||||||
|
class="w-full rounded border border-slate-400 px-3 py-2 rounded-r"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-7 mt-4 flex">
|
||||||
|
<input
|
||||||
|
v-if="projectId && !offeredToRecipient"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-6 w-6 mr-2"
|
||||||
|
v-model="offeredToProject"
|
||||||
|
/>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
|
icon="square"
|
||||||
|
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
||||||
|
@click="notifyUserOfProject()"
|
||||||
|
/>
|
||||||
|
<label class="text-sm mt-1">
|
||||||
|
{{
|
||||||
|
projectId
|
||||||
|
? "This was given to " + projectName
|
||||||
|
: "No project was chosen"
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-7 mt-4 flex">
|
||||||
|
<input
|
||||||
|
v-if="recipientDid && !offeredToProject"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-6 w-6 mr-2"
|
||||||
|
v-model="offeredToRecipient"
|
||||||
|
/>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
|
icon="square"
|
||||||
|
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
||||||
|
@click="notifyUserOfRecipient()"
|
||||||
|
/>
|
||||||
|
<label class="text-sm mt-1">
|
||||||
|
{{
|
||||||
|
recipientDid
|
||||||
|
? "This was given to " + recipientName
|
||||||
|
: "No recipient was chosen."
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'claim-add-raw',
|
||||||
|
query: {
|
||||||
|
claim: constructOfferParam(),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
Edit & Submit Raw
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center mb-2 mt-6 italic">
|
||||||
|
Sign & Send to publish to the world
|
||||||
|
<fa
|
||||||
|
icon="circle-info"
|
||||||
|
class="pl-2 text-blue-500 cursor-pointer"
|
||||||
|
@click="explainData()"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-lg font-bold uppercase 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-2 py-3 rounded-md"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
Sign & Send
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
import { NotificationIface } from "@/constants/app";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
|
import {
|
||||||
|
createAndSubmitOffer,
|
||||||
|
didInfo,
|
||||||
|
editAndSubmitOffer,
|
||||||
|
GenericCredWrapper,
|
||||||
|
getPlanFromCache,
|
||||||
|
hydrateOffer,
|
||||||
|
OfferVerifiableCredential,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
QuickNav,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class OfferDetailsView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
apiServer = "";
|
||||||
|
|
||||||
|
amountInput = "0";
|
||||||
|
conditionDescription = "";
|
||||||
|
itemDescription = "";
|
||||||
|
destinationPathAfter = "";
|
||||||
|
offeredToProject = false;
|
||||||
|
offeredToRecipient = false;
|
||||||
|
offererDid: string | undefined;
|
||||||
|
hideBackButton = false;
|
||||||
|
message = "";
|
||||||
|
offerId = "";
|
||||||
|
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
|
||||||
|
projectId = "";
|
||||||
|
projectName = "a project";
|
||||||
|
recipientDid = "";
|
||||||
|
recipientName = "";
|
||||||
|
unitCode = "HUR";
|
||||||
|
validThroughDateInput = "";
|
||||||
|
|
||||||
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
|
||||||
|
? (JSON.parse(
|
||||||
|
(this.$route as Router).query["prevCredToEdit"],
|
||||||
|
) as GenericCredWrapper<OfferVerifiableCredential>)
|
||||||
|
: undefined;
|
||||||
|
} catch (error) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Retrieval Error",
|
||||||
|
text: "The previous record isn't available for editing. If you submit, you'll create a new record.",
|
||||||
|
},
|
||||||
|
6000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevAmount =
|
||||||
|
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
|
||||||
|
this.amountInput =
|
||||||
|
(this.$route as Router).query["amountInput"] ||
|
||||||
|
(prevAmount ? String(prevAmount) : "") ||
|
||||||
|
this.amountInput;
|
||||||
|
this.unitCode = ((this.$route as Router).query["unitCode"] ||
|
||||||
|
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
|
||||||
|
this.unitCode) as string;
|
||||||
|
|
||||||
|
this.conditionDescription =
|
||||||
|
this.prevCredToEdit?.claim?.description || this.conditionDescription;
|
||||||
|
this.itemDescription =
|
||||||
|
(this.$route as Router).query["description"] ||
|
||||||
|
this.prevCredToEdit?.claim?.itemOffered?.description ||
|
||||||
|
this.itemDescription;
|
||||||
|
this.destinationPathAfter = (this.$route as Router).query[
|
||||||
|
"destinationPathAfter"
|
||||||
|
];
|
||||||
|
this.offererDid = ((this.$route as Router).query["offererDid"] ||
|
||||||
|
this.prevCredToEdit?.claim?.agent?.identifier ||
|
||||||
|
this.offererDid) as string;
|
||||||
|
this.hideBackButton =
|
||||||
|
(this.$route as Router).query["hideBackButton"] === "true";
|
||||||
|
this.message = ((this.$route as Router).query["message"] as string) || "";
|
||||||
|
|
||||||
|
// find any project ID
|
||||||
|
let project;
|
||||||
|
if (
|
||||||
|
this.prevCredToEdit?.claim?.itemOffered?.isPartOf["@type"] ===
|
||||||
|
"PlanAction"
|
||||||
|
) {
|
||||||
|
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
|
||||||
|
}
|
||||||
|
this.projectId = ((this.$route as Router).query["projectId"] ||
|
||||||
|
project?.identifier ||
|
||||||
|
this.projectId) as string;
|
||||||
|
this.projectName = ((this.$route as Router).query["projectName"] ||
|
||||||
|
project?.name ||
|
||||||
|
this.projectName) as string;
|
||||||
|
|
||||||
|
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
|
||||||
|
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
|
||||||
|
this.recipientName =
|
||||||
|
((this.$route as Router).query["recipientName"] as string) || "";
|
||||||
|
|
||||||
|
this.validThroughDateInput =
|
||||||
|
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
|
||||||
|
let allContacts: Contact[] = [];
|
||||||
|
let allMyDids: string[] = [];
|
||||||
|
if (this.recipientDid && !this.recipientName) {
|
||||||
|
allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
this.recipientName = didInfo(
|
||||||
|
this.recipientDid,
|
||||||
|
this.activeDid,
|
||||||
|
allMyDids,
|
||||||
|
allContacts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// these should be functions but something's wrong with the syntax in the <> conditional
|
||||||
|
this.offeredToProject = !!this.projectId;
|
||||||
|
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Error retrieving settings from database:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.projectId && !this.projectName) {
|
||||||
|
// console.log("Getting project name from cache", this.projectId);
|
||||||
|
const project = await getPlanFromCache(
|
||||||
|
this.projectId,
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
);
|
||||||
|
this.projectName = project?.name
|
||||||
|
? "the project: " + project.name
|
||||||
|
: "a project";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeUnitCode() {
|
||||||
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||||
|
const index = units.indexOf(this.unitCode);
|
||||||
|
this.unitCode = units[(index + 1) % units.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.amountInput = `${Math.max(
|
||||||
|
0,
|
||||||
|
(parseFloat(this.amountInput) || 1) - 1,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
if (this.destinationPathAfter) {
|
||||||
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
|
} else {
|
||||||
|
(this.$router as Router).back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelBack() {
|
||||||
|
(this.$router as Router).back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm() {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identifier before you can record a offer.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parseFloat(this.amountInput) < 0) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
text: "You may not send a negative number.",
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.itemDescription && !parseFloat(this.amountInput)) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: `You must enter a description or some number of ${
|
||||||
|
this.libsUtil.UNIT_LONG[this.unitCode]
|
||||||
|
}.`,
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
text: "Recording the give...",
|
||||||
|
title: "",
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
|
await this.recordOffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyUserOfProject() {
|
||||||
|
if (!this.projectId) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "To assign to a project, you must open this page through a project.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// must be because offeredToRecipient is true
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "You cannot assign both to a project and to a recipient.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyUserOfRecipient() {
|
||||||
|
if (!this.recipientDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "To assign to a recipient, you must open this page from a contact.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// must be because offeredToProject is true
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Error",
|
||||||
|
text: "You cannot assign both to a recipient and to a project.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param offererDid may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param amountInput may be 0
|
||||||
|
* @param unitCode may be omitted, defaults to "HUR"
|
||||||
|
*/
|
||||||
|
public async recordOffer() {
|
||||||
|
try {
|
||||||
|
const recipientDid = this.offeredToRecipient
|
||||||
|
? this.recipientDid
|
||||||
|
: undefined;
|
||||||
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
||||||
|
let result;
|
||||||
|
if (this.prevCredToEdit) {
|
||||||
|
// don't create from a blank one in case some properties were set from a different interface
|
||||||
|
result = await editAndSubmitOffer(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.prevCredToEdit,
|
||||||
|
this.activeDid,
|
||||||
|
this.itemDescription,
|
||||||
|
parseFloat(this.amountInput),
|
||||||
|
this.unitCode,
|
||||||
|
this.conditionDescription,
|
||||||
|
this.validThroughDateInput,
|
||||||
|
recipientDid,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await createAndSubmitOffer(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
this.itemDescription,
|
||||||
|
parseFloat(this.amountInput),
|
||||||
|
this.unitCode,
|
||||||
|
this.conditionDescription,
|
||||||
|
this.validThroughDateInput,
|
||||||
|
recipientDid,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type === "error" || this.isCreationError(result.response)) {
|
||||||
|
const errorMessage = this.getCreationErrorMessage(result);
|
||||||
|
console.error("Error with give creation result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage || "There was an error creating the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: `That offer was recorded.`,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
localStorage.removeItem("imageUrl");
|
||||||
|
if (this.destinationPathAfter) {
|
||||||
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
|
} else {
|
||||||
|
(this.$router as Router).back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error with give recordation caught:", error);
|
||||||
|
const errorMessage =
|
||||||
|
error.userMessage ||
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
"There was an error recording the give.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructOfferParam() {
|
||||||
|
const recipientDid = this.offeredToRecipient
|
||||||
|
? this.recipientDid
|
||||||
|
: undefined;
|
||||||
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
||||||
|
const giveClaim = hydrateOffer(
|
||||||
|
this.prevCredToEdit?.claim as OfferVerifiableCredential,
|
||||||
|
this.activeDid,
|
||||||
|
recipientDid,
|
||||||
|
this.itemDescription,
|
||||||
|
parseFloat(this.amountInput),
|
||||||
|
this.unitCode,
|
||||||
|
this.conditionDescription,
|
||||||
|
projectId,
|
||||||
|
this.validThroughDateInput,
|
||||||
|
this.prevCredToEdit?.id as string,
|
||||||
|
);
|
||||||
|
const claimStr = JSON.stringify(giveClaim);
|
||||||
|
return claimStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for readability
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result response "data" from the server
|
||||||
|
* @returns true if the result indicates an error
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
isCreationError(result: any) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
|
* @returns best guess at an error message
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
getCreationErrorMessage(result: any) {
|
||||||
|
return (
|
||||||
|
result.error?.userMessage ||
|
||||||
|
result.error?.error ||
|
||||||
|
result.response?.data?.error?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
explainData() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Data Sharing",
|
||||||
|
text: libsUtil.PRIVACY_MESSAGE,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -170,7 +170,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
|
<OfferDialog
|
||||||
|
ref="customOfferDialog"
|
||||||
|
:projectId="this.projectId"
|
||||||
|
:projectName="this.name"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="activeDid && isRegistered">
|
<div v-if="activeDid && isRegistered">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ test('Record an offer', async ({ page }) => {
|
|||||||
const randomString = Math.random().toString(36).substring(2, 8);
|
const randomString = Math.random().toString(36).substring(2, 8);
|
||||||
// Standard title prefix
|
// Standard title prefix
|
||||||
const finalTitle = `Offering of ${randomString}`;
|
const finalTitle = `Offering of ${randomString}`;
|
||||||
const randomNonZeroNumber = Math.floor(Math.random() * 999) + 1;
|
const randomNonZeroNumber = Math.floor(Math.random() * 998) + 1;
|
||||||
|
|
||||||
// Create new ID for default user
|
// Create new ID for default user
|
||||||
await importUser(page);
|
await importUser(page);
|
||||||
@@ -22,12 +22,41 @@ test('Record an offer', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
await expect(page.getByText('That offer was recorded.')).toBeVisible();
|
await expect(page.getByText('That offer was recorded.')).toBeVisible();
|
||||||
|
|
||||||
// Refresh home view and check gift
|
// go to the offer and check the values
|
||||||
await page.goto('./projects');
|
await page.goto('./projects');
|
||||||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
||||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||||
const page1Promise = page.waitForEvent('popup');
|
const serverPagePromise = page.waitForEvent('popup');
|
||||||
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
await page.getByRole('link', { name: 'View on the Public Server' }).click();
|
||||||
const page1 = await page1Promise;
|
const serverPage = await serverPagePromise;
|
||||||
});
|
await serverPage.getByText(finalTitle);
|
||||||
|
await serverPage.getByText('did:none:HIDDEN');
|
||||||
|
|
||||||
|
// Now update that offer
|
||||||
|
|
||||||
|
// find the edit page and check the old values again
|
||||||
|
await page.goto('./projects');
|
||||||
|
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
||||||
|
await page.getByTestId('editClaimButton').click();
|
||||||
|
await page.locator('heading', { hasText: 'What was offered' }).isVisible();
|
||||||
|
const itemDesc = await page.getByTestId('itemDescription');
|
||||||
|
await expect(itemDesc).toHaveValue(finalTitle);
|
||||||
|
const amount = await page.getByTestId('inputOfferAmount');
|
||||||
|
await expect(amount).toHaveValue(randomNonZeroNumber.toString());
|
||||||
|
// update the values
|
||||||
|
await itemDesc.fill('Updated ' + finalTitle);
|
||||||
|
await amount.fill(String(randomNonZeroNumber + 1));
|
||||||
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
|
||||||
|
// go to the offer claim again and check the updated values
|
||||||
|
await page.goto('./projects');
|
||||||
|
await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click();
|
||||||
|
const newItemDesc = await page.getByTestId('description');
|
||||||
|
await expect(newItemDesc).toHaveText(finalTitle);
|
||||||
|
|
||||||
|
// go to edit page
|
||||||
|
await page.getByTestId('editClaimButton').click();
|
||||||
|
const newAmount = await page.getByTestId('inputOfferAmount');
|
||||||
|
await expect(newAmount).toHaveValue(randomNonZeroNumber.toString());
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user