diff --git a/src/components/ProjectIcon.vue b/src/components/ProjectIcon.vue index c07ea0bc4..11ef45af8 100644 --- a/src/components/ProjectIcon.vue +++ b/src/components/ProjectIcon.vue @@ -1,5 +1,5 @@ diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 3fb46edff..735fe25ed 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -118,6 +118,7 @@ export interface PlanSummaryRecord { endTime?: string; fulfillsPlanHandleId: string; handleId: string; + image?: string; issuerDid: string; locLat?: number; locLon?: number; @@ -175,7 +176,7 @@ export interface PlanVerifiableCredential { * Represents data about a project * * @deprecated - * We should use PlanServerRecord instead. + * We should use PlanSummaryRecord instead. **/ export interface PlanData { /** @@ -190,6 +191,7 @@ export interface PlanData { * URL referencing information about the project **/ handleId: string; + image?: string; /** * The DID of the issuer */ diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue index e8b235b38..ae262053e 100644 --- a/src/views/DiscoverView.vue +++ b/src/views/DiscoverView.vue @@ -102,12 +102,13 @@ @click="onClickLoadProject(project.handleId)" class="block py-4 flex gap-4" > -
+
+ :imageUrl="project.image" + class="block border border-slate-300 rounded-md max-h-12 max-w-12" + />
@@ -271,8 +272,15 @@ export default class DiscoverView extends Vue { const plans: PlanData[] = results.data; if (plans) { for (const plan of plans) { - const { name, description, handleId, issuerDid, rowid } = plan; - this.projects.push({ name, description, handleId, issuerDid, rowid }); + const { name, description, handleId, image, issuerDid, rowid } = plan; + this.projects.push({ + name, + description, + handleId, + image, + issuerDid, + rowid, + }); } this.remoteCount = this.projects.length; } else { diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index d1abe2cec..62547b116 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -29,10 +29,31 @@ v-model="fullClaim.name" /> +
+ + + + + + + + + +
+ +
@@ -155,23 +176,31 @@ import "leaflet/dist/leaflet.css"; import { AxiosError } from "axios"; import * as didJwt from "did-jwt"; import { DateTime } from "luxon"; +import { IIdentifier } from "@veramo/core"; import { Component, Vue } from "vue-facing-decorator"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import QuickNav from "@/components/QuickNav.vue"; -import { NotificationIface } from "@/constants/app"; +import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { accessToken, SimpleSigner } from "@/libs/crypto"; +import * as libsUtil from "@/libs/util"; import { useAppStore } from "@/store/app"; -import { IIdentifier } from "@veramo/core"; import { PlanVerifiableCredential } from "@/libs/endorserServer"; +import ImageMethodDialog from "@/components/ImageMethodDialog.vue"; @Component({ - components: { LMap, LMarker, LTileLayer, QuickNav }, + components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav }, }) export default class NewEditProjectView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; + errNote(message) { + this.$notify( + { group: "alert", type: "danger", title: "Error", text: message }, + 5000, + ); + } activeDid = ""; agentDid = ""; @@ -183,6 +212,7 @@ export default class NewEditProjectView extends Vue { name: "", description: "", }; // this default is only to avoid errors before plan is loaded + imageUrl = ""; includeLocation = false; isHiddenSave = false; isHiddenSpinner = true; @@ -197,10 +227,7 @@ export default class NewEditProjectView extends Vue { zoneName = DateTime.local().zoneName; zoom = 2; - async beforeCreate() { - await accountsDB.open(); - this.numAccounts = await accountsDB.accounts.count(); - } + libsUtil = libsUtil; public async getIdentity(activeDid: string) { await accountsDB.open(); @@ -228,6 +255,9 @@ export default class NewEditProjectView extends Vue { } async mounted() { + await accountsDB.open(); + this.numAccounts = await accountsDB.accounts.count(); + await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = (settings?.activeDid as string) || ""; @@ -235,7 +265,7 @@ export default class NewEditProjectView extends Vue { if (this.projectId) { if (this.numAccounts === 0) { - console.error("Error: no account was found."); + this.errNote("There was a problem loading your account info."); } else { const identity = await this.getIdentity(this.activeDid); if (!identity) { @@ -264,6 +294,7 @@ export default class NewEditProjectView extends Vue { if (resp.status === 200) { this.projectIssuerDid = resp.data.issuer; this.fullClaim = resp.data.claim; + this.imageUrl = resp.data.claim.image || ""; this.lastClaimJwtId = resp.data.id; if (this.fullClaim?.location) { this.includeLocation = true; @@ -283,6 +314,84 @@ export default class NewEditProjectView extends Vue { } } catch (error) { console.error("Got error retrieving that project", error); + this.errNote("There was an error retrieving that project."); + } + } + + openImageDialog() { + (this.$refs.imageDialog as ImageMethodDialog).open((imgUrl) => { + this.imageUrl = imgUrl; + }, "PlanAction"); + } + + confirmDeleteImage() { + this.$notify( + { + group: "modal", + type: "confirm", + title: "Are you sure you want to delete the image?", + text: "", + onYes: this.deleteImage, + }, + -1, + ); + } + + async deleteImage() { + if (!this.imageUrl) { + return; + } + try { + const identity = await libsUtil.getIdentity(this.activeDid); + const token = await accessToken(identity); + const response = await this.axios.delete( + DEFAULT_IMAGE_API_SERVER + + "/image/" + + encodeURIComponent(this.imageUrl), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + if (response.status === 204) { + // don't bother with a notification + // (either they'll simply continue or they're canceling and going back) + } else { + console.error("Problem deleting image:", response); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "There was a problem deleting the image.", + }, + 5000, + ); + return; + } + + this.imageUrl = ""; + } catch (error) { + console.error("Error deleting image:", error); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((error as any).response.status === 404) { + console.log("The image was already deleted:", error); + + this.imageUrl = ""; + + // it already doesn't exist so we won't say anything to the user + } else { + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "There was an error deleting the image.", + }, + 5000, + ); + } } } @@ -299,6 +408,11 @@ export default class NewEditProjectView extends Vue { } else { delete vcClaim.agent; } + if (this.imageUrl) { + vcClaim.image = this.imageUrl; + } else { + delete vcClaim.image; + } if (this.includeLocation) { vcClaim.location = { geo: { diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 707bf511e..0361e3d91 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -22,13 +22,14 @@
-
-
+
+
+ :imageUrl="imageUrl" + class="block border border-slate-300 rounded-md max-h-16 max-w-16" + />
@@ -399,6 +400,7 @@ export default class ProjectViewView extends Vue { fulfillersToHitLimit = false; givesToThis: Array = []; givesHitLimit = false; + imageUrl = ""; issuer = ""; latitude = 0; longitude = 0; @@ -427,7 +429,7 @@ export default class ProjectViewView extends Vue { const accountsArr: Account[] = await accounts?.toArray(); this.allMyDids = accountsArr.map((acc) => acc.did); const account = accountsArr.find((acc) => acc.did === this.activeDid); - const identity = JSON.parse(account?.identity || "null"); + const identity = JSON.parse((account?.identity as string) || "null"); const pathParam = window.location.pathname.substring("/project/".length); if (pathParam) { @@ -488,6 +490,7 @@ export default class ProjectViewView extends Vue { startDateTime.toLocaleTimeString(); } this.agentDid = resp.data.claim?.agent?.identifier; + this.imageUrl = resp.data.claim?.image; this.issuer = resp.data.issuer; this.name = resp.data.claim?.name || "(no name)"; this.description = resp.data.claim?.description || "(no description)"; diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index c5878adbb..659445ce7 100644 --- a/src/views/ProjectsView.vue +++ b/src/views/ProjectsView.vue @@ -86,12 +86,12 @@ :key="offer.handleId" >
-
+
+ class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12" + />
-
+
+ :imageUrl="project.image" + class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12" + />
@@ -211,6 +212,7 @@