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.
 
 
 
 
 
 

345 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 { NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import {
NOTIFY_CONTACT_NOT_FOUND,
NOTIFY_CONTACT_METHODS_UPDATED,
NOTIFY_CONTACT_SAVED,
} from "../constants/notifications";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { AppString } from "../constants/app";
/**
* 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 database via PlatformServiceMixin
* 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,
},
mixins: [PlatformServiceMixin],
})
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;
/** Notification helpers */
notify!: ReturnType<typeof createNotifyHelpers>;
/** 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 via PlatformServiceMixin
* 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() {
this.notify = createNotifyHelpers(this.$notify);
const contactDid = this.$route.params.did as string;
const contact = await this.$getContact(contactDid);
if (contact) {
this.contact = contact;
this.contactName = contact.name || "";
this.contactNotes = contact.notes || "";
this.contactMethods = contact.contactMethods || [];
} else {
this.notify.error(
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,
TIMEOUTS.LONG,
);
(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 via PlatformServiceMixin
* 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.warning(
NOTIFY_CONTACT_METHODS_UPDATED.message,
TIMEOUTS.LONG,
);
return;
}
// Save to database via PlatformServiceMixin
await this.$updateContact(this.contact?.did || "", {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
// Notify success and redirect
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);
(this.$router as Router).push({
path: "/did/" + encodeURIComponent(this.contact?.did || ""),
});
}
}
</script>