forked from jsnbuchanan/crowd-funder-for-time-pwa
• Database Migration: Replace databaseUtil with PlatformServiceMixin methods • SQL Abstraction: Replace raw SQL with $getAllContacts() and $accountSettings() • Template Streamlining: Add 5 computed properties for consistent styling • Vue Syntax Fix: Correct vue-facing-decorator mixin and computed property syntax Migration Details: - Removed: databaseUtil imports and PlatformServiceFactory usage - Added: PlatformServiceMixin with $accountSettings(), $getAllContacts(), $updateSettings() - Created: 5 computed properties (primaryButtonClasses, secondaryButtonClasses, etc.) - Fixed: Proper @Component mixin declaration and class getter syntax - Quality: Zero linting errors, full TypeScript compliance Component provides 3-page onboarding flow (Home, Discover, Create) with dynamic content based on user registration and contact status. Ready for human testing across all platforms.
346 lines
11 KiB
Vue
346 lines
11 KiB
Vue
<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>
|