Browse Source

allow editing of an offer

pull/123/head
Trent Larson 1 month ago
parent
commit
a9b12f4d7c
  1. 45
      src/components/OfferDialog.vue
  2. 23
      src/libs/endorserServer.ts
  3. 7
      src/router/index.ts
  4. 56
      src/views/ClaimView.vue
  5. 9
      src/views/ContactsView.vue
  6. 6
      src/views/GiftedDetailsView.vue
  7. 2
      src/views/NewEditProjectView.vue
  8. 168
      src/views/OfferDetailsView.vue
  9. 6
      src/views/ProjectViewView.vue

45
src/components/OfferDialog.vue

@ -36,18 +36,27 @@
<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"
>
Expiration
<div class="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'offer-details',
query: {
amountInput,
description,
offererDid: activeDid,
projectId,
projectName,
recipientDid,
recipientName,
unitCode: amountUnitCode,
},
}"
class="text-blue-500"
>
Conditions, expiration...
</router-link>
</span>
<input
type="text"
class="w-full border border-slate-400 px-2 py-2 rounded-r"
:placeholder="datePlaceholder()"
v-model="expirationDateInput"
/>
</div>
<p class="text-center mt-6 mb-2 italic">
Sign & Send to publish to the world
@ -71,7 +80,6 @@
</template>
<script lang="ts">
import { DateTime } from "luxon";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
@ -84,7 +92,8 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
export default class OfferDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop projectId? = "";
@Prop projectId?;
@Prop projectName?;
activeDid = "";
apiServer = "";
@ -94,13 +103,15 @@ export default class OfferDialog extends Vue {
description = "";
expirationDateInput = "";
recipientDid? = "";
recipientName? = "";
visible = false;
libsUtil = libsUtil;
async open(recipientDid?: string) {
async open(recipientDid?: string, recipientName?: string) {
try {
this.recipientDid = recipientDid;
this.recipientName = recipientName;
await db.open();
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() {
this.close();
this.eraseValues();

23
src/libs/endorserServer.ts

@ -62,6 +62,7 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
id: string;
issuedAt: string;
issuer: string;
publicUrls?: Record<string, string>; // only for IDs that want to be public
}
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{
@ -734,10 +735,10 @@ export function hydrateOffer(
vcClaimOrig?: OfferVerifiableCredential,
fromDid?: string,
toDid?: string,
conditionDescription?: string,
itemDescription?: string,
amount?: number,
unitCode?: string,
offeringDescription?: string,
conditionDescription?: string,
fulfillsProjectHandleId?: string,
validThrough?: string,
lastClaimId?: string,
@ -766,9 +767,9 @@ export function hydrateOffer(
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
: undefined;
if (offeringDescription || fulfillsProjectHandleId) {
if (itemDescription || fulfillsProjectHandleId) {
vcClaim.itemOffered = vcClaim.itemOffered || {};
vcClaim.itemOffered.description = offeringDescription || undefined;
vcClaim.itemOffered.description = itemDescription || undefined;
if (fulfillsProjectHandleId) {
vcClaim.itemOffered.isPartOf = {
"@type": "PlanAction",
@ -794,9 +795,10 @@ export async function createAndSubmitOffer(
axios: Axios,
apiServer: string,
issuerDid: string,
description?: string,
itemDescription: string,
amount?: number,
unitCode?: string,
conditionDescription?: string,
validThrough?: string,
recipientDid?: string,
fulfillsProjectHandleId?: string,
@ -805,10 +807,10 @@ export async function createAndSubmitOffer(
undefined,
issuerDid,
recipientDid,
description,
itemDescription,
amount,
unitCode,
description,
conditionDescription,
fulfillsProjectHandleId,
validThrough,
undefined,
@ -826,9 +828,10 @@ export async function editAndSubmitOffer(
apiServer: string,
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
issuerDid: string,
description?: string,
itemDescription: string,
amount?: number,
unitCode?: string,
conditionDescription?: string,
validThrough?: string,
recipientDid?: string,
fulfillsProjectHandleId?: string,
@ -837,10 +840,10 @@ export async function editAndSubmitOffer(
fullClaim.claim,
issuerDid,
recipientDid,
description,
itemDescription,
amount,
unitCode,
description,
conditionDescription,
fulfillsProjectHandleId,
validThrough,
fullClaim.id,

7
src/router/index.ts

@ -91,7 +91,7 @@ const routes: Array<RouteRecordRaw> = [
{
path: "/gifted-details",
name: "gifted-details",
component: () => import("../views/GiftedDetails.vue"),
component: () => import("@/views/GiftedDetailsView.vue"),
},
{
path: "/help",
@ -143,6 +143,11 @@ const routes: Array<RouteRecordRaw> = [
name: "new-identifier",
component: () => import("../views/NewIdentifierView.vue"),
},
{
path: "/offer-details/:id?",
name: "offer-details",
component: () => import("../views/OfferDetailsView.vue"),
},
{
path: "/project/:id?",
name: "project",

56
src/views/ClaimView.vue

@ -24,8 +24,9 @@
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
<button
v-if="
veriClaim.claimType === 'GiveAction' &&
veriClaim.issuer === activeDid
['GiveAction', 'Offer'].includes(
veriClaim.claimType as string,
) && veriClaim.issuer === activeDid
"
@click="onClickEditClaim"
title="Edit"
@ -402,7 +403,7 @@
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
<pre
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
>
</div>
@ -425,7 +426,10 @@
</button>
</div>
<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>
<a
@ -843,15 +847,41 @@ export default class ClaimView extends Vue {
}
onClickEditClaim() {
const route = {
name: "gifted-details",
query: {
prevCredToEdit: JSON.stringify(this.veriClaim),
destinationPathAfter:
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
},
};
(this.$router as Router).push(route);
if (this.veriClaim.claimType === "GiveAction") {
const route = {
name: "gifted-details",
query: {
prevCredToEdit: JSON.stringify(this.veriClaim),
destinationPathAfter:
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
},
};
(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>

9
src/views/ContactsView.vue

@ -271,7 +271,7 @@
<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"
@click="openOfferDialog(contact.did)"
@click="openOfferDialog(contact.did, contact.name)"
>
Offer
</button>
@ -1131,8 +1131,11 @@ export default class ContactsView extends Vue {
);
}
openOfferDialog(recipientDid: string) {
(this.$refs.customOfferDialog as OfferDialog).open(recipientDid);
openOfferDialog(recipientDid: string, recipientName: string) {
(this.$refs.customOfferDialog as OfferDialog).open(
recipientDid,
recipientName,
);
}
private async onClickCancelName() {

6
src/views/GiftedDetails.vue → src/views/GiftedDetailsView.vue

@ -269,6 +269,7 @@ export default class GiftedDetails extends Vue {
this.hideBackButton =
(this.$route as Router).query["hideBackButton"] === "true";
this.message = ((this.$route as Router).query["message"] as string) || "";
// find any offer ID
const fulfills = this.prevCredToEdit?.claim?.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.givenToRecipient = !this.givenToProject && !!this.recipientDid;
@ -549,7 +551,7 @@ export default class GiftedDetails extends Vue {
group: "alert",
type: "warning",
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,
);
@ -574,7 +576,7 @@ export default class GiftedDetails extends Vue {
group: "alert",
type: "warning",
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,
);

2
src/views/NewEditProjectView.vue

@ -91,14 +91,12 @@
<div class="flex mb-4 columns-3 w-full">
<input
v-model="startDateInput"
placeholder="Start Date"
type="date"
class="col-span-1 w-full rounded border border-slate-400 px-3 py-2"
/>
<input
:disabled="!startDateInput"
v-model="startTimeInput"
placeholder="Start Time"
type="time"
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
/>

168
src/views/OfferDetails.vue → src/views/OfferDetailsView.vue

@ -21,9 +21,8 @@
<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>From {{ giverName }}</span>
<span>
to
Offer to
{{
offeredToProject
? projectName
@ -36,7 +35,7 @@
<textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was offered"
v-model="description"
v-model="itemDescription"
/>
<div class="flex flex-row justify-center">
<span
@ -64,6 +63,32 @@
</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" }}&nbsp;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"
@ -131,6 +156,12 @@
/>
</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 &amp; 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"
@ -142,6 +173,7 @@
</template>
<script lang="ts">
import { DateTime } from "luxon";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
@ -168,21 +200,20 @@ import { Contact } from "@/db/tables/contacts";
TopMessage,
},
})
export default class OfferDetails extends Vue {
export default class OfferDetailsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
apiServer = "";
amountInput = "0";
description = "";
conditionDescription = "";
itemDescription = "";
destinationPathAfter = "";
offeredToProject = false;
offeredToRecipient = false;
giverDid: string | undefined;
giverName = "";
offererDid: string | undefined;
hideBackButton = false;
isTrade = false;
message = "";
offerId = "";
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
@ -191,7 +222,7 @@ export default class OfferDetails extends Vue {
recipientDid = "";
recipientName = "";
unitCode = "HUR";
validThroughDate = null;
validThroughDateInput = "";
libsUtil = libsUtil;
@ -214,67 +245,54 @@ export default class OfferDetails extends Vue {
);
}
const prevAmount = this.prevCredToEdit?.claim?.object?.amountOfThisGood;
const prevAmount =
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
this.amountInput =
(this.$route as Router).query["amountInput"] ||
(prevAmount ? String(prevAmount) : "") ||
this.amountInput;
this.description =
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?.description ||
this.description;
this.prevCredToEdit?.claim?.itemOffered?.description ||
this.itemDescription;
this.destinationPathAfter = (this.$route as Router).query[
"destinationPathAfter"
];
this.giverDid = ((this.$route as Router).query["giverDid"] ||
this.offererDid = ((this.$route as Router).query["offererDid"] ||
this.prevCredToEdit?.claim?.agent?.identifier ||
this.giverDid) as string;
this.giverName =
((this.$route as Router).query["giverName"] as string) || "";
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 offer ID
const fulfills = this.prevCredToEdit?.claim?.fulfills;
const fulfillsArray = Array.isArray(fulfills)
? fulfills
: fulfills
? [fulfills]
: [];
const offer = fulfillsArray.find((rec) => rec["@type"] === "Offer");
this.offerId = ((this.$route as Router).query["offerId"] ||
offer?.identifier ||
this.offerId) as string;
// find any project ID
const project = fulfillsArray.find((rec) => rec["@type"] === "PlanAction");
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.unitCode = ((this.$route as Router).query["unitCode"] ||
this.prevCredToEdit?.claim?.object?.unitCode ||
this.unitCode) as string;
// this is an endpoint for sharing project info to highlight something given
// https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
if ((this.$route as Router).query["shareTitle"]) {
this.description =
((this.$route as Router).query["shareTitle"] as string) +
(this.description ? "\n" + this.description : "");
}
if ((this.$route as Router).query["shareText"]) {
this.description =
(this.description ? this.description + "\n" : "") +
((this.$route as Router).query["shareText"] as string);
}
if ((this.$route as Router).query["shareUrl"]) {
this.imageUrl = (this.$route as Router).query["shareUrl"] as string;
}
this.validThroughDateInput =
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
try {
await db.open();
@ -284,32 +302,20 @@ export default class OfferDetails extends Vue {
let allContacts: Contact[] = [];
let allMyDids: string[] = [];
if (
(this.giverDid && !this.giverName) ||
(this.recipientDid && !this.recipientName)
) {
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);
if (this.giverDid && !this.giverName) {
this.giverName = didInfo(
this.giverDid,
this.activeDid,
allMyDids,
allContacts,
);
}
if (this.recipientDid && !this.recipientName) {
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
);
}
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;
@ -327,7 +333,7 @@ export default class OfferDetails extends Vue {
);
}
if (this.projectId) {
if (this.projectId && !this.projectName) {
// console.log("Getting project name from cache", this.projectId);
const project = await getPlanFromCache(
this.projectId,
@ -395,7 +401,7 @@ export default class OfferDetails extends Vue {
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
if (!this.itemDescription && !parseFloat(this.amountInput)) {
this.$notify(
{
group: "alert",
@ -431,7 +437,7 @@ export default class OfferDetails extends Vue {
group: "alert",
type: "warning",
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,
);
@ -456,7 +462,7 @@ export default class OfferDetails extends Vue {
group: "alert",
type: "warning",
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,
);
@ -476,7 +482,7 @@ export default class OfferDetails extends Vue {
/**
*
* @param giverDid may be null
* @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"
@ -495,10 +501,11 @@ export default class OfferDetails extends Vue {
this.apiServer,
this.prevCredToEdit,
this.activeDid,
this.description,
this.itemDescription,
parseFloat(this.amountInput),
this.unitCode,
this.validThroughDate,
this.conditionDescription,
this.validThroughDateInput,
recipientDid,
projectId,
);
@ -507,10 +514,11 @@ export default class OfferDetails extends Vue {
this.axios,
this.apiServer,
this.activeDid,
this.description,
this.itemDescription,
parseFloat(this.amountInput),
this.unitCode,
this.validThroughDate,
this.conditionDescription,
this.validThroughDateInput,
recipientDid,
projectId,
);
@ -534,7 +542,7 @@ export default class OfferDetails extends Vue {
group: "alert",
type: "success",
title: "Success",
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
text: `That offer was recorded.`,
},
5000,
);
@ -573,12 +581,12 @@ export default class OfferDetails extends Vue {
this.prevCredToEdit?.claim as OfferVerifiableCredential,
this.activeDid,
recipientDid,
this.description,
this.itemDescription,
parseFloat(this.amountInput),
this.unitCode,
"",
this.conditionDescription,
projectId,
this.validThroughDate,
this.validThroughDateInput,
this.prevCredToEdit?.id as string,
);
const claimStr = JSON.stringify(giveClaim);

6
src/views/ProjectViewView.vue

@ -170,7 +170,11 @@
</button>
</div>
</div>
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
<OfferDialog
ref="customOfferDialog"
:projectId="this.projectId"
:projectName="this.name"
/>
<div v-if="activeDid && isRegistered">
<div class="text-center">

Loading…
Cancel
Save