From 643f777d1078c6bd81945dbdb4730dd1f1fb9a0e Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 1 Jul 2023 07:07:46 -0600 Subject: [PATCH 1/4] start linking gives to projects --- src/libs/endorserServer.ts | 12 +++- src/views/ProjectViewView.vue | 120 ++++++++++++++++++++++++++++++++-- src/views/ProjectsView.vue | 12 ++-- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 6766ecd..d220f5d 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -46,6 +46,7 @@ export interface GiveVerifiableCredential { "@type": string; agent?: { identifier: string }; description?: string; + fulfills?: { "@type": string; identifier: string }; identifier?: string; object?: { amountOfThisGood: number; unitCode: string }; recipient: { identifier: string }; @@ -95,7 +96,7 @@ export function didInfo(did, identifiers, contacts) { /** * For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim - + * * @param identity * @param fromDid may be null * @param toDid @@ -109,7 +110,8 @@ export async function createAndSubmitGive( fromDid: string, toDid: string, description: string, - hours: number + hours: number, + fulfillsProjectHandleId: string ): Promise | InternalError> { // Make a claim const vcClaim: GiveVerifiableCredential = { @@ -126,6 +128,12 @@ export async function createAndSubmitGive( if (hours) { vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" }; } + if (fulfillsProjectHandleId) { + vcClaim.fulfills = { + "@type": "PlanAction", + identifier: fulfillsProjectHandleId, + }; + } // Make a payload for the claim const vcPayload = { vc: { diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 37fd059..93d8785 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -9,7 +9,7 @@ > -
  • +
  • -
  • +
  • + + + + + + + @@ -151,15 +169,18 @@ import * as moment from "moment"; import * as R from "ramda"; import { Options, Vue } from "vue-class-component"; +import GiftedDialog from "@/components/GiftedDialog.vue"; import { accountsDB, db } from "@/db"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import { createAndSubmitGive } from "@/libs/endorserServer"; import { accessToken } from "@/libs/crypto"; import { IIdentifier } from "@veramo/core"; @Options({ - components: {}, + components: { GiftedDialog }, }) export default class ProjectViewView extends Vue { + activeDid = ""; apiServer = ""; expanded = false; name = ""; @@ -167,7 +188,7 @@ export default class ProjectViewView extends Vue { truncatedDesc = ""; truncateLength = 40; timeSince = ""; - projectId = localStorage.getItem("projectId") || ""; + projectId = localStorage.getItem("projectId") || ""; // handle ID errorMessage = ""; onEditClick() { @@ -230,7 +251,7 @@ export default class ProjectViewView extends Vue { async created() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); - const activeDid = settings?.activeDid || ""; + this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; await accountsDB.open(); @@ -239,7 +260,7 @@ export default class ProjectViewView extends Vue { console.error("Problem! Should have a profile!"); } else { const accounts = await accountsDB.accounts.toArray(); - const account = R.find((acc) => acc.did === activeDid, accounts); + const account = R.find((acc) => acc.did === this.activeDid, accounts); const identity = JSON.parse(account?.identity || "null"); if (!identity) { throw new Error("No identity found."); @@ -247,5 +268,92 @@ export default class ProjectViewView extends Vue { this.LoadProject(identity); } } + + openDialog(contact) { + this.$refs.customDialog.open(contact); + } + handleDialogResult(result) { + if (result.action === "confirm") { + return new Promise((resolve) => { + this.recordGive(result.contact, result.description, result.hours); + resolve(); + }); + } else { + // action was "cancel" so do nothing + } + } + + /** + * + * @param contact may be null + * @param description may be an empty string + * @param hours may be 0 + */ + async recordGive(contact, description, hours) { + if (this.activeDid == null) { + this.alertTitle = "Error"; + this.alertMessage = + "You must select an identity before you can record a give."; + return; + } + const accounts = await accountsDB.accounts.toArray(); + const account = R.find((acc) => acc.did === this.activeDid, accounts); + const identity = JSON.parse(account?.identity || "null"); + if (!identity) { + throw new Error("No identity found."); + } + createAndSubmitGive( + this.axios, + this.apiServer, + identity, + contact?.did, + this.activeDid, + description, + hours, + this.projectId + ) + .then((result) => { + if (result.status != 201 || result.data?.error) { + console.log("Error with give result:", result); + this.alertTitle = "Error"; + this.alertMessage = + result.data?.message || "There was an error recording the give."; + } else { + this.alertTitle = "Success"; + this.alertMessage = "That gift was recorded."; + //this.updateAllFeed(); // full update is overkill but we should show something + } + }) + .catch((e) => { + console.log("Error with give caught:", e); + this.alertTitle = "Error"; + this.alertMessage = + e.userMessage || "There was an error recording the give."; + }); + } + + // This same popup code is in many files. + alertMessage = ""; + alertTitle = ""; + public onClickClose() { + this.alertTitle = ""; + this.alertMessage = ""; + } + public computedAlertClassNames() { + return { + hidden: !this.alertMessage, + "dismissable-alert": true, + "bg-slate-100": true, + "p-5": true, + rounded: true, + "drop-shadow-lg": true, + fixed: true, + "top-3": true, + "inset-x-3": true, + "transition-transform": true, + "ease-in": true, + "duration-300": true, + }; + } } diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index 107158a..8ef4311 100644 --- a/src/views/ProjectsView.vue +++ b/src/views/ProjectsView.vue @@ -185,9 +185,9 @@ export default class ProjectsView extends Vue { } /** - * Handle clicking on a project entry found in the list - * @param id of the project - **/ + * Handle clicking on a project entry found in the list + * @param id of the project + **/ onClickLoadProject(id: string) { localStorage.setItem("projectId", id); const route = { @@ -197,9 +197,9 @@ export default class ProjectsView extends Vue { } /** - * Load projects initially - * @param identity of the user - **/ + * Load projects initially + * @param identity of the user + **/ async LoadProjects(identity: IIdentifier) { const url = `${this.apiServer}/api/v2/report/plansByIssuer`; const token: string = await accessToken(identity); From a2b3cebdb3d9e6177df37d7290c5f514b03fa5e9 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 1 Jul 2023 15:45:30 -0600 Subject: [PATCH 2/4] finish contact selection for gives --- src/components/GiftedDialog.vue | 20 ++++++++++------- src/libs/endorserServer.ts | 7 +++++- src/views/HomeView.vue | 17 ++++++++------- src/views/ProjectViewView.vue | 38 ++++++++++++++++++++++++++------- 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index c48ce3c..c158cc3 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -2,13 +2,12 @@

    - Received from {{ contact?.name || "nobody in particular" }} + {{ message }} {{ giver?.name || "somebody not specified" }}

    -

    {{ message }}

    @@ -27,9 +26,10 @@
    +

    Sign & Send to publish to the world

     
    @@ -71,7 +72,7 @@ @@ -268,13 +269,13 @@ export default class HomeView extends Vue { return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; } - openDialog(contact) { - this.$refs.customDialog.open(contact); + openDialog(giver) { + this.$refs.customDialog.open(giver); } handleDialogResult(result) { if (result.action === "confirm") { return new Promise((resolve) => { - this.recordGive(result.contact, result.description, result.hours); + this.recordGive(result.contact?.did, result.description, result.hours); resolve(); }); } else { @@ -284,11 +285,11 @@ export default class HomeView extends Vue { /** * - * @param contact may be null + * @param giverDid may be null * @param description may be an empty string * @param hours may be 0 */ - recordGive(contact, description, hours) { + recordGive(giverDid, description, hours) { if (this.activeDid == null) { this.alertTitle = "Error"; this.alertMessage = @@ -308,7 +309,7 @@ export default class HomeView extends Vue { this.axios, this.apiServer, identity, - contact?.did, + giverDid, this.activeDid, description, hours diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 93d8785..d80964f 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -109,16 +109,35 @@ +
    +

    ... or choose a contact who gave:

    + +
    + +  or  + +
    +
    + @@ -171,6 +190,7 @@ import { Options, Vue } from "vue-class-component"; import GiftedDialog from "@/components/GiftedDialog.vue"; import { accountsDB, db } from "@/db"; +import { Contact } from "@/db/tables/contacts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { createAndSubmitGive } from "@/libs/endorserServer"; import { accessToken } from "@/libs/crypto"; @@ -181,6 +201,7 @@ import { IIdentifier } from "@veramo/core"; }) export default class ProjectViewView extends Vue { activeDid = ""; + allContacts: Array = []; apiServer = ""; expanded = false; name = ""; @@ -253,6 +274,7 @@ export default class ProjectViewView extends Vue { const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; + this.allContacts = await db.contacts.toArray(); await accountsDB.open(); const num_accounts = await accountsDB.accounts.count(); @@ -275,21 +297,21 @@ export default class ProjectViewView extends Vue { handleDialogResult(result) { if (result.action === "confirm") { return new Promise((resolve) => { - this.recordGive(result.contact, result.description, result.hours); + this.recordGive(result.contact?.did, result.description, result.hours); resolve(); }); } else { - // action was "cancel" so do nothing + // action was not "confirm" so do nothing } } /** * - * @param contact may be null + * @param giverDid may be null * @param description may be an empty string * @param hours may be 0 */ - async recordGive(contact, description, hours) { + async recordGive(giverDid, description, hours) { if (this.activeDid == null) { this.alertTitle = "Error"; this.alertMessage = @@ -306,7 +328,7 @@ export default class ProjectViewView extends Vue { this.axios, this.apiServer, identity, - contact?.did, + giverDid, this.activeDid, description, hours, From a8f1e2598676576746783231e2d826f527a404c2 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 1 Jul 2023 22:20:10 -0600 Subject: [PATCH 3/4] add error messages for gives --- src/views/HomeView.vue | 14 ++++++++++++-- src/views/ProjectViewView.vue | 26 ++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 29b7bc6..a31d1ec 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -296,6 +296,12 @@ export default class HomeView extends Vue { "You must select an identity before you can record a give."; return; } + if (!description && !hours) { + this.alertTitle = "Error"; + this.alertMessage = + "You must enter a description or some number of hours."; + return; + } const account = R.find( (acc) => acc.did === this.activeDid, this.allAccounts @@ -319,7 +325,8 @@ export default class HomeView extends Vue { console.log("Error with give result:", result); this.alertTitle = "Error"; this.alertMessage = - result.data?.message || "There was an error recording the give."; + result.data?.error?.message || + "There was an error recording the give."; } else { this.alertTitle = "Success"; this.alertMessage = "That gift was recorded."; @@ -327,10 +334,13 @@ export default class HomeView extends Vue { } }) .catch((e) => { + // axios throws errors on 400 responses console.log("Error with give caught:", e); this.alertTitle = "Error"; this.alertMessage = - e.userMessage || "There was an error recording the give."; + e.userMessage || + e.response?.data?.error?.message || + "There was an error recording the give."; }); } diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index d80964f..dc9c5a9 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -179,6 +179,18 @@ --> + + +
    + +

    {{ alertTitle }}

    +

    {{ alertMessage }}

    +
    @@ -318,6 +330,12 @@ export default class ProjectViewView extends Vue { "You must select an identity before you can record a give."; return; } + if (!description && !hours) { + this.alertTitle = "Error"; + this.alertMessage = + "You must enter a description or some number of hours."; + return; + } const accounts = await accountsDB.accounts.toArray(); const account = R.find((acc) => acc.did === this.activeDid, accounts); const identity = JSON.parse(account?.identity || "null"); @@ -339,7 +357,8 @@ export default class ProjectViewView extends Vue { console.log("Error with give result:", result); this.alertTitle = "Error"; this.alertMessage = - result.data?.message || "There was an error recording the give."; + result.data?.error?.message || + "There was an error recording the give."; } else { this.alertTitle = "Success"; this.alertMessage = "That gift was recorded."; @@ -347,10 +366,13 @@ export default class ProjectViewView extends Vue { } }) .catch((e) => { + // axios throws errors on 400 responses console.log("Error with give caught:", e); this.alertTitle = "Error"; this.alertMessage = - e.userMessage || "There was an error recording the give."; + e.userMessage || + e.response?.data?.error?.message || + "There was an error recording the give."; }); } From c2ebaa0a76025cf5c12c39ffb595f6da6767b662 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 2 Jul 2023 06:46:31 -0600 Subject: [PATCH 4/4] mark the give-to-project task as complete --- project.task.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/project.task.yaml b/project.task.yaml index 22abe2d..c0622f9 100644 --- a/project.task.yaml +++ b/project.task.yaml @@ -7,8 +7,6 @@ tasks: - 8 Move to vue-facing-decorator - 01 design ideas for simple gives on the first page -- 01 give time to a particular project - use "provider" attribute - - give example assignee:trent - .1 remove commitments from ProjectView UI - 01 add list of 'give' records for a project on ProjectView UI