From ef8a5c5c5cb8fe2095aced873bfbd289691f7b3b Mon Sep 17 00:00:00 2001 From: Trent Larson <trent@trentlarson.com> Date: Sat, 4 Jan 2025 16:35:05 -0700 Subject: [PATCH] add a contact-edit page and allow saving of notes --- src/db/tables/contacts.ts | 1 + src/router/index.ts | 5 ++ src/views/ClaimView.vue | 2 +- src/views/ConfirmGiftView.vue | 2 +- src/views/ContactEditView.vue | 130 ++++++++++++++++++++++++++++++++++ src/views/ContactsView.vue | 41 +++++++---- src/views/DIDView.vue | 63 +--------------- 7 files changed, 167 insertions(+), 77 deletions(-) create mode 100644 src/views/ContactEditView.vue diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index e6369b228..9a438e51e 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -2,6 +2,7 @@ export interface Contact { did: string; name?: string; nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key + notes?: string; profileImageUrl?: string; publicKeyBase64?: string; seesMe?: boolean; // cached value of the server setting diff --git a/src/router/index.ts b/src/router/index.ts index ba4b3a244..cf4300a1d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -64,6 +64,11 @@ const routes: Array<RouteRecordRaw> = [ name: "contact-amounts", component: () => import("../views/ContactAmountsView.vue"), }, + { + path: "/contact-edit/:did", + name: "contact-edit", + component: () => import("../views/ContactEditView.vue"), + }, { path: "/contact-gift", name: "contact-gift", diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 95325810b..9e4243789 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -351,7 +351,7 @@ > Details <fa v-if="showVeriClaimDump" icon="chevron-up" /> - <fa v-else icon="chevron-down" /> + <fa v-else icon="chevron-right" /> </h2> <div v-if="showVeriClaimDump"> <div diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index e9df0524e..fb7059b3d 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -261,7 +261,7 @@ > Details <fa v-if="showVeriClaimDump" icon="chevron-up" /> - <fa v-else icon="chevron-down" /> + <fa v-else icon="chevron-right" /> </h2> <div v-if="showVeriClaimDump"> <div diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue new file mode 100644 index 000000000..04e63f2cf --- /dev/null +++ b/src/views/ContactEditView.vue @@ -0,0 +1,130 @@ +<template> + <QuickNav selected="Contacts" /> + <TopMessage /> + + <section id="ContactEdit" class="p-6 max-w-3xl mx-auto"> + <div id="ViewBreadcrumb" class="mb-8"> + <h1 class="text-4xl text-center font-light relative px-7"> + <!-- Back --> + <button + @click="$router.go(-1)" + class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" + > + <fa icon="chevron-left" class="fa-fw" /> + </button> + {{ contact.name || AppString.NO_CONTACT_NAME }} + </h1> + </div> + + <!-- Contact Name --> + <div class="mt-4 flex"> + <label + for="contactName" + class="block text-sm font-medium text-gray-700 mt-2" + > + Name + </label> + <input + type="text" + class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" + v-model="contactName" + /> + </div> + + <!-- Contact Notes --> + <div class="mt-4"> + <label for="contactNotes" class="block text-sm font-medium text-gray-700"> + Notes + </label> + <textarea + id="contactNotes" + rows="4" + class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" + v-model="contactNotes" + ></textarea> + </div> + + <!-- Save Button --> + <div class="mt-4 flex justify-between"> + <button + class="px-4 py-2 bg-blue-500 text-white rounded-md" + @click="saveEdit" + > + Save + </button> + <button + class="ml-4 px-4 py-2 bg-slate-500 text-white rounded-md" + @click="$router.go(-1)" + > + Cancel + </button> + </div> + </section> +</template> + +<script lang="ts"> +import { Component, Vue } from "vue-facing-decorator"; +import { RouteLocation, Router } from "vue-router"; + +import QuickNav from "@/components/QuickNav.vue"; +import TopMessage from "@/components/TopMessage.vue"; +import { AppString, NotificationIface } from "@/constants/app"; +import { db } from "@/db/index"; +import { Contact } from "@/db/tables/contacts"; + +@Component({ + components: { + QuickNav, + TopMessage, + }, +}) +export default class ContactEditView extends Vue { + $notify!: (notification: NotificationIface, timeout?: number) => void; + + contact: Contact = { + did: "", + name: "", + notes: "", + }; + contactName = ""; + contactNotes = ""; + + AppString = AppString; + + async created() { + const contactDid = (this.$route as RouteLocation).params.did; + if (!contactDid) { + this.$notify({ + group: "alert", + type: "error", + title: "Contact Not Found", + text: "There is no contact with that DID.", + }); + (this.$router as Router).push({ path: "/contacts" }); + return; + } + const contact = await db.contacts.get(contactDid); + if (contact) { + this.contact = contact; + this.contactName = contact.name || ""; + this.contactNotes = contact.notes || ""; + } + } + + async saveEdit() { + await db.contacts.update(this.contact.did, { + name: this.contactName, + notes: this.contactNotes, + }); + this.$notify({ + group: "alert", + type: "success", + title: "Notes Saved", + text: "The contact notes have been updated successfully.", + }); + (this.$router as Router).push({ + path: "/did/" + encodeURIComponent(this.contact.did), + }); + } +} +</script> diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 6f9181585..014f32a6b 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -170,27 +170,34 @@ ) : contactsSelected.push(contact.did) " - class="ml-2 h-6 w-6" + class="ml-2 h-6 w-6 flex-shrink-0" data-testId="contactCheckOne" /> - <h2 class="text-base font-semibold ml-2"> - {{ contact.name || AppString.NO_CONTACT_NAME }} + <h2 class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0"> + {{ contactNameNonBreakingSpace(contact.name) }} </h2> - <router-link - :to="{ - path: '/did/' + encodeURIComponent(contact.did), - }" - title="See more about this person" - > - <fa icon="circle-info" class="text-xl text-blue-500 ml-4" /> - </router-link> + <span> + <div class="flex items-center"> + <router-link + :to="{ + path: '/did/' + encodeURIComponent(contact.did), + }" + title="See more about this person" + > + <fa icon="circle-info" class="text-xl text-blue-500 ml-4" /> + </router-link> - <span class="ml-4 text-sm overflow-hidden">{{ - shortDid(contact.did) - }}</span - ><!-- The first 18 characters of did:peer are the same. --> + <span class="ml-4 text-sm overflow-hidden">{{ + shortDid(contact.did) + }}</span + > + </div> + <div class="ml-4 text-sm"> + {{ contact.notes }} + </div> + </span> </div> <div id="ContactActions" class="flex gap-1.5 mt-2"> <div @@ -542,6 +549,10 @@ export default class ContactsView extends Vue { } } + private contactNameNonBreakingSpace(contactName?: string) { + return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0"); + } + private danger(message: string, title: string = "Error", timeout = 5000) { this.$notify( { diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue index 1540daaf4..fde24311b 100644 --- a/src/views/DIDView.vue +++ b/src/views/DIDView.vue @@ -26,15 +26,11 @@ <div> <h2 class="text-xl font-semibold"> {{ contactFromDid?.name || "(no name)" }} - <button - @click=" - contactEdit = true; - contactNewName = (contactFromDid?.name as string) || ''; - " - title="Edit" + <router-link + :to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }" > <fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" /> - </button> + </router-link> </h2> <button @click="showDidDetails = !showDidDetails" @@ -163,34 +159,6 @@ </div> </div> - <!-- Edit Name Dialog, maybe should be replaced by ContactNameDialog --> - <div v-if="contactEdit" class="dialog-overlay"> - <div class="dialog"> - <h1 class="text-xl font-bold text-center mb-4">Edit Name</h1> - <input - type="text" - class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" - placeholder="Name" - v-model="contactNewName" - /> - <div class="flex justify-between"> - <button - class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400" - @click="onClickSaveName(contactNewName)" - > - <fa icon="save" /> - </button> - <span class="inline-block w-2" /> - <button - class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400" - @click="onClickCancelName()" - > - <fa icon="ban" /> - </button> - </div> - </div> - </div> - <!-- Loading Animation --> <div class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full" @@ -290,8 +258,6 @@ export default class DIDView extends Vue { apiServer = ""; claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = []; contactFromDid?: Contact; - contactEdit = false; - contactNewName: string = ""; contactYaml = ""; hitEnd = false; isLoading = false; @@ -559,29 +525,6 @@ export default class DIDView extends Vue { return claim.claim.name || claim.claim.description || ""; } - private async onClickCancelName() { - this.contactEdit = false; - } - - private async onClickSaveName(newName: string) { - if (!this.contactFromDid) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Not A Contact", - text: "First add this on the contact page, then you can edit here.", - }, - 5000, - ); - return; - } - this.contactFromDid.name = newName; - return db.contacts - .update(this.contactFromDid.did, { name: newName }) - .then(() => (this.contactEdit = false)); - } - // note that this is also in ContactView.vue async confirmSetVisibility(contact: Contact, visibility: boolean) { const visibilityPrompt = visibility