You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
11 KiB
371 lines
11 KiB
<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
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
@click="$router.go(-1)"
|
|
>
|
|
<font-awesome icon="chevron-left" class="fa-fw" />
|
|
</button>
|
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Contact Name -->
|
|
<div class="mt-4 flex" data-testId="contactName">
|
|
<label
|
|
for="contactName"
|
|
class="block text-sm font-medium text-gray-700 mt-2"
|
|
>
|
|
Name
|
|
</label>
|
|
<input
|
|
v-model="contactName"
|
|
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"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Contact Notes -->
|
|
<div class="mt-4">
|
|
<label for="contactNotes" class="block text-sm font-medium text-gray-700">
|
|
Notes
|
|
</label>
|
|
<textarea
|
|
id="contactNotes"
|
|
v-model="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"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Contact Methods -->
|
|
<div class="mt-4">
|
|
<h2 class="text-lg font-medium text-gray-700">Contact Methods</h2>
|
|
<div
|
|
v-for="(method, index) in contactMethods"
|
|
:key="index"
|
|
class="flex mt-2"
|
|
>
|
|
<input
|
|
v-model="method.label"
|
|
type="text"
|
|
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder="Label"
|
|
/>
|
|
<input
|
|
v-model="method.type"
|
|
type="text"
|
|
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder="Type"
|
|
/>
|
|
<div class="relative">
|
|
<button
|
|
class="px-2 py-1 bg-gray-200 rounded-md"
|
|
@click="toggleDropdown(index)"
|
|
>
|
|
<font-awesome icon="caret-down" class="fa-fw" />
|
|
</button>
|
|
<div
|
|
v-if="dropdownIndex === index"
|
|
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
|
>
|
|
<div
|
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
@click="setMethodType(index, 'CELL')"
|
|
>
|
|
CELL
|
|
</div>
|
|
<div
|
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
@click="setMethodType(index, 'EMAIL')"
|
|
>
|
|
EMAIL
|
|
</div>
|
|
<div
|
|
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
@click="setMethodType(index, 'WHATSAPP')"
|
|
>
|
|
WHATSAPP
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input
|
|
v-model="method.value"
|
|
type="text"
|
|
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
placeholder="Number, email, etc."
|
|
/>
|
|
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
|
|
<font-awesome icon="trash-can" class="fa-fw" />
|
|
</button>
|
|
</div>
|
|
<button class="mt-2" @click="addContactMethod">
|
|
<font-awesome
|
|
icon="plus"
|
|
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Save Button -->
|
|
<div class="mt-8 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 * as R from "ramda";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
|
import { db } from "../db/index";
|
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
|
import * as databaseUtil from "../db/databaseUtil";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
|
|
/**
|
|
* Contact Edit View Component
|
|
* @author Matthew Raymer
|
|
*
|
|
* This component provides a full-featured contact editing interface with support for:
|
|
* - Basic contact information (name, notes)
|
|
* - Multiple contact methods with type selection
|
|
* - Data validation and persistence
|
|
*
|
|
* Workflow:
|
|
* 1. Component loads with DID from route params
|
|
* 2. Fetches existing contact data from IndexedDB
|
|
* 3. Presents editable form with current values
|
|
* 4. Validates and saves updates back to database
|
|
*
|
|
* Contact Method Types:
|
|
* - CELL: Mobile phone numbers
|
|
* - EMAIL: Email addresses
|
|
* - WHATSAPP: WhatsApp contact info
|
|
*
|
|
* State Management:
|
|
* - Maintains separate state for form fields to prevent direct mutation
|
|
* - Handles array cloning for contact methods to prevent reference issues
|
|
* - Manages dropdown state for method type selection
|
|
*
|
|
* Navigation:
|
|
* - Back button returns to previous view
|
|
* - Save redirects to contact detail view
|
|
* - Cancel returns to previous view
|
|
* - Invalid DID redirects to contacts list
|
|
*/
|
|
@Component({
|
|
components: {
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class ContactEditView extends Vue {
|
|
/** Notification function injected by Vue */
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
/** Current route instance */
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
/** Router instance for navigation */
|
|
$router!: Router;
|
|
|
|
/** Current contact data */
|
|
contact: Contact | undefined = {
|
|
did: "",
|
|
name: "",
|
|
notes: "",
|
|
};
|
|
/** Editable contact name field */
|
|
contactName = "";
|
|
/** Editable contact notes field */
|
|
contactNotes = "";
|
|
/** Array of editable contact methods */
|
|
contactMethods: Array<ContactMethod> = [];
|
|
/** Currently open dropdown index, null if none open */
|
|
dropdownIndex: number | null = null;
|
|
|
|
/** App string constants */
|
|
AppString = AppString;
|
|
|
|
/**
|
|
* Component lifecycle hook that initializes the contact edit form
|
|
*
|
|
* Workflow:
|
|
* 1. Extracts DID from route parameters
|
|
* 2. Queries database for existing contact
|
|
* 3. Populates form fields with contact data
|
|
* 4. Handles missing contact error case
|
|
*
|
|
* @throws Will not throw but redirects on error
|
|
* @emits Notification on contact not found
|
|
* @emits Router navigation on error
|
|
*/
|
|
async created() {
|
|
const contactDid = this.$route.params.did;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const dbContact = await platformService.dbQuery(
|
|
"SELECT * FROM contacts WHERE did = ?",
|
|
[contactDid],
|
|
);
|
|
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
|
|
dbContact,
|
|
)[0] as unknown as Contact;
|
|
contact.contactMethods = JSON.parse(
|
|
(contact?.contactMethods as unknown as string) || "[]",
|
|
);
|
|
if (USE_DEXIE_DB) {
|
|
await db.open();
|
|
contact = await db.contacts.get(contactDid || "");
|
|
}
|
|
if (contact) {
|
|
this.contact = contact;
|
|
this.contactName = contact.name || "";
|
|
this.contactNotes = contact.notes || "";
|
|
this.contactMethods = contact.contactMethods || [];
|
|
} else {
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Contact Not Found",
|
|
text: "There is no contact with DID " + contactDid,
|
|
});
|
|
(this.$router as Router).push({ path: "/contacts" });
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new empty contact method to the methods array
|
|
*
|
|
* Creates a new method object with empty fields for:
|
|
* - label: Custom label for the method
|
|
* - type: Communication type (CELL, EMAIL, WHATSAPP)
|
|
* - value: The contact information value
|
|
*/
|
|
addContactMethod() {
|
|
this.contactMethods.push({ label: "", type: "", value: "" });
|
|
}
|
|
|
|
/**
|
|
* Removes a contact method at the specified index
|
|
*
|
|
* @param index The array index of the method to remove
|
|
*/
|
|
removeContactMethod(index: number) {
|
|
this.contactMethods.splice(index, 1);
|
|
}
|
|
|
|
/**
|
|
* Toggles the type selection dropdown for a contact method
|
|
*
|
|
* If the clicked dropdown is already open, closes it.
|
|
* If another dropdown is open, closes it and opens the clicked one.
|
|
*
|
|
* @param index The array index of the method whose dropdown to toggle
|
|
*/
|
|
toggleDropdown(index: number) {
|
|
this.dropdownIndex = this.dropdownIndex === index ? null : index;
|
|
}
|
|
|
|
/**
|
|
* Sets the type for a contact method and closes the dropdown
|
|
*
|
|
* @param index The array index of the method to update
|
|
* @param type The new type value (CELL, EMAIL, WHATSAPP)
|
|
*/
|
|
setMethodType(index: number, type: string) {
|
|
this.contactMethods[index].type = type;
|
|
this.dropdownIndex = null;
|
|
}
|
|
|
|
/**
|
|
* Saves the edited contact information to the database
|
|
*
|
|
* Workflow:
|
|
* 1. Clones contact methods array to prevent reference issues
|
|
* 2. Normalizes method types to uppercase
|
|
* 3. Checks for changes in method types
|
|
* 4. Updates database with new values
|
|
* 5. Notifies user of success
|
|
* 6. Redirects to contact detail view
|
|
*
|
|
* @throws Will not throw but notifies on validation errors
|
|
* @emits Notification on type changes or success
|
|
* @emits Router navigation on success
|
|
*/
|
|
async saveEdit() {
|
|
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
|
|
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
|
|
|
|
// Normalize method types to uppercase
|
|
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
|
|
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
|
|
);
|
|
|
|
// Check for type changes
|
|
if (!R.equals(contactMethodsObj, contactMethods)) {
|
|
this.contactMethods = contactMethods;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: "Contact Methods Updated",
|
|
text: "Note that some methods have been updated, such as uppercasing 'email' to 'EMAIL'. Save again if the changes are acceptable.",
|
|
},
|
|
15000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Save to database
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const contactMethodsString = JSON.stringify(contactMethods);
|
|
await platformService.dbExec(
|
|
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?",
|
|
[
|
|
this.contactName,
|
|
this.contactNotes,
|
|
contactMethodsString,
|
|
this.contact?.did || "",
|
|
],
|
|
);
|
|
if (USE_DEXIE_DB) {
|
|
await db.contacts.update(this.contact?.did || "", {
|
|
name: this.contactName,
|
|
notes: this.contactNotes,
|
|
contactMethods: contactMethods,
|
|
});
|
|
}
|
|
// Notify success and redirect
|
|
this.$notify({
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Contact Saved",
|
|
text: "The contact info has been updated successfully.",
|
|
});
|
|
(this.$router as Router).push({
|
|
path: "/did/" + encodeURIComponent(this.contact?.did || ""),
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|