Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ec2002cb0 | |||
|
|
36eb9a16b0 | ||
| 7d295dd062 | |||
| 5f1b4dcc21 | |||
|
|
203cf6b078 | ||
|
|
9b84b28a78 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "6.0.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4-beta",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="sectionDataExport" :class="containerClasses">
|
<div id="sectionDataExport" :class="containerClasses">
|
||||||
<div :class="titleClasses">Data Export</div>
|
<div :class="titleClasses">Data Management</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
:to="{ name: 'seed-backup' }"
|
:to="{ name: 'seed-backup' }"
|
||||||
@@ -30,7 +30,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
|||||||
:class="exportButtonClasses"
|
:class="exportButtonClasses"
|
||||||
@click="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
{{ isExporting ? "Exporting..." : "Download Contacts" }}
|
{{ isExporting ? "Exporting..." : "Export Contacts" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -55,11 +55,54 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Contacts -->
|
||||||
|
<div id="sectionImportContactsSettings" class="mt-4">
|
||||||
|
<h2 class="text-slate-500 text-sm font-bold">Import Contacts</h2>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="w-full bg-white rounded-md pe-2 file:border-0 file:bg-gradient-to-b file:from-blue-400 file:to-blue-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:me-2 file:rounded-s-md"
|
||||||
|
@change="uploadImportFile"
|
||||||
|
/>
|
||||||
|
<transition
|
||||||
|
enter-active-class="transform ease-out duration-300 transition"
|
||||||
|
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
|
||||||
|
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
|
||||||
|
leave-active-class="transition ease-in duration-500"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div v-if="showContactImport()" class="mt-2">
|
||||||
|
<!-- Bulk import has an error
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button
|
||||||
|
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
|
@click="confirmSubmitImportFile()"
|
||||||
|
>
|
||||||
|
Overwrite Settings & Contacts
|
||||||
|
<br />
|
||||||
|
(which doesn't include Identifier Data)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="checkContactImports()"
|
||||||
|
>
|
||||||
|
Import Contacts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
@@ -67,8 +110,10 @@ import { Contact } from "../db/tables/contacts";
|
|||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { contactsToExportJson } from "../libs/util";
|
import { contactsToExportJson } from "../libs/util";
|
||||||
import { createNotifyHelpers } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
|
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||||
|
import { ImportContent } from "@/interfaces/accountView";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
@@ -91,6 +136,12 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router instance injected by Vue
|
||||||
|
* Used for navigation
|
||||||
|
*/
|
||||||
|
$router!: Router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Active DID (Decentralized Identifier) of the user
|
* Active DID (Decentralized Identifier) of the user
|
||||||
* Controls visibility of seed backup option
|
* Controls visibility of seed backup option
|
||||||
@@ -110,6 +161,12 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
showRedNotificationDot = false;
|
showRedNotificationDot = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the selected import file
|
||||||
|
* Used to store the file selected by the user for import
|
||||||
|
*/
|
||||||
|
private inputImportFileName: Blob | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification helper for consistent notification patterns
|
* Notification helper for consistent notification patterns
|
||||||
* Created as a getter to ensure $notify is available when called
|
* Created as a getter to ensure $notify is available when called
|
||||||
@@ -200,12 +257,30 @@ export default class DataExportSection extends Vue {
|
|||||||
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
|
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
|
||||||
const exContact: Contact = R.omit(["contactMethods"], contact);
|
const exContact: Contact = R.omit(["contactMethods"], contact);
|
||||||
// now add contactMethods as a true array of ContactMethod objects
|
// now add contactMethods as a true array of ContactMethod objects
|
||||||
exContact.contactMethods = contact.contactMethods
|
// $contacts() returns normalized contacts where contactMethods is already an array,
|
||||||
? typeof contact.contactMethods === "string" &&
|
// but we handle both array and string cases for robustness
|
||||||
contact.contactMethods.trim() !== ""
|
if (contact.contactMethods) {
|
||||||
? JSON.parse(contact.contactMethods)
|
if (Array.isArray(contact.contactMethods)) {
|
||||||
: []
|
// Already an array, use it directly
|
||||||
: [];
|
exContact.contactMethods = contact.contactMethods;
|
||||||
|
} else {
|
||||||
|
// Check if it's a string that needs parsing (shouldn't happen with normalized contacts, but handle for robustness)
|
||||||
|
const contactMethodsValue = contact.contactMethods as unknown;
|
||||||
|
if (
|
||||||
|
typeof contactMethodsValue === "string" &&
|
||||||
|
contactMethodsValue.trim() !== ""
|
||||||
|
) {
|
||||||
|
// String that needs parsing
|
||||||
|
exContact.contactMethods = JSON.parse(contactMethodsValue);
|
||||||
|
} else {
|
||||||
|
// Invalid data, use empty array
|
||||||
|
exContact.contactMethods = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No contactMethods, use empty array
|
||||||
|
exContact.contactMethods = [];
|
||||||
|
}
|
||||||
return exContact;
|
return exContact;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,5 +323,58 @@ export default class DataExportSection extends Vue {
|
|||||||
this.showRedNotificationDot = false;
|
this.showRedNotificationDot = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles file selection for contact import
|
||||||
|
* Stores the selected file for later processing
|
||||||
|
*/
|
||||||
|
async uploadImportFile(event: Event): Promise<void> {
|
||||||
|
this.inputImportFileName = (event.target as HTMLInputElement).files?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a contact import file has been selected
|
||||||
|
* Used to conditionally show the import button
|
||||||
|
*/
|
||||||
|
showContactImport(): boolean {
|
||||||
|
return !!this.inputImportFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the selected import file and navigates to the contact import view
|
||||||
|
* Parses the JSON file and extracts contact data for import
|
||||||
|
*/
|
||||||
|
async checkContactImports(): Promise<void> {
|
||||||
|
if (!this.inputImportFileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const fileContent: string = (event.target?.result as string) || "{}";
|
||||||
|
try {
|
||||||
|
const contents: ImportContent = JSON.parse(fileContent);
|
||||||
|
const contactTableRows: Array<Contact> = (
|
||||||
|
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
|
||||||
|
)?.find((table) => table.tableName === "contacts")
|
||||||
|
?.rows as Array<Contact>;
|
||||||
|
const contactRows = contactTableRows.map(
|
||||||
|
// @ts-expect-error for omitting this field that is found in the Dexie format
|
||||||
|
(contact) => R.omit(["$types"], contact) as Contact,
|
||||||
|
);
|
||||||
|
this.$router.push({
|
||||||
|
name: "contact-import",
|
||||||
|
query: { contacts: JSON.stringify(contactRows) },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error checking contact imports:", error);
|
||||||
|
this.notify.error(
|
||||||
|
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMPORT_ERROR,
|
||||||
|
TIMEOUTS.STANDARD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(this.inputImportFileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelope,
|
||||||
faEnvelopeOpenText,
|
faEnvelopeOpenText,
|
||||||
faEraser,
|
faEraser,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -101,6 +102,9 @@ import {
|
|||||||
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
||||||
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
|
// Brand icons
|
||||||
|
import { faWhatsapp } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
// Initialize Font Awesome library with all required icons
|
// Initialize Font Awesome library with all required icons
|
||||||
library.add(
|
library.add(
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
@@ -140,6 +144,7 @@ library.add(
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelope,
|
||||||
faEnvelopeOpenText,
|
faEnvelopeOpenText,
|
||||||
faEraser,
|
faEraser,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -193,6 +198,7 @@ library.add(
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
|
faWhatsapp,
|
||||||
faXmark,
|
faXmark,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1367,6 +1367,9 @@ export const PlatformServiceMixin = {
|
|||||||
contact.profileImageUrl !== undefined
|
contact.profileImageUrl !== undefined
|
||||||
? contact.profileImageUrl
|
? contact.profileImageUrl
|
||||||
: null,
|
: null,
|
||||||
|
notes: contact.notes !== undefined ? contact.notes : null,
|
||||||
|
iViewContent:
|
||||||
|
contact.iViewContent !== undefined ? contact.iViewContent : null,
|
||||||
contactMethods:
|
contactMethods:
|
||||||
contact.contactMethods !== undefined
|
contact.contactMethods !== undefined
|
||||||
? Array.isArray(contact.contactMethods)
|
? Array.isArray(contact.contactMethods)
|
||||||
@@ -1377,8 +1380,8 @@ export const PlatformServiceMixin = {
|
|||||||
|
|
||||||
await this.$dbExec(
|
await this.$dbExec(
|
||||||
`INSERT OR REPLACE INTO contacts
|
`INSERT OR REPLACE INTO contacts
|
||||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, contactMethods)
|
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, notes, iViewContent, contactMethods)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
safeContact.did,
|
safeContact.did,
|
||||||
safeContact.name,
|
safeContact.name,
|
||||||
@@ -1387,6 +1390,8 @@ export const PlatformServiceMixin = {
|
|||||||
safeContact.registered,
|
safeContact.registered,
|
||||||
safeContact.nextPubKeyHashB64,
|
safeContact.nextPubKeyHashB64,
|
||||||
safeContact.profileImageUrl,
|
safeContact.profileImageUrl,
|
||||||
|
safeContact.notes,
|
||||||
|
safeContact.iViewContent,
|
||||||
safeContact.contactMethods,
|
safeContact.contactMethods,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -375,45 +375,6 @@
|
|||||||
Switch Identifier
|
Switch Identifier
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div id="sectionImportContactsSettings" class="mt-4">
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold">Import Contacts</h2>
|
|
||||||
|
|
||||||
<div class="ml-4 mt-2">
|
|
||||||
<input type="file" class="ml-2" @change="uploadImportFile" />
|
|
||||||
<transition
|
|
||||||
enter-active-class="transform ease-out duration-300 transition"
|
|
||||||
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
|
|
||||||
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
|
|
||||||
leave-active-class="transition ease-in duration-500"
|
|
||||||
leave-from-class="opacity-100"
|
|
||||||
leave-to-class="opacity-0"
|
|
||||||
>
|
|
||||||
<div v-if="showContactImport()" class="mt-4">
|
|
||||||
<!-- Bulk import has an error
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<button
|
|
||||||
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
@click="confirmSubmitImportFile()"
|
|
||||||
>
|
|
||||||
Overwrite Settings & Contacts
|
|
||||||
<br />
|
|
||||||
(which doesn't include Identifier Data)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<button
|
|
||||||
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
@click="checkContactImports()"
|
|
||||||
>
|
|
||||||
Import Contacts
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label
|
<label
|
||||||
for="toggleShowAmounts"
|
for="toggleShowAmounts"
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
class="flex items-center justify-between cursor-pointer my-4"
|
||||||
@@ -770,9 +731,7 @@ import "dexie-export-import";
|
|||||||
import { ImportProgress } from "dexie-export-import";
|
import { ImportProgress } from "dexie-export-import";
|
||||||
import { LeafletMouseEvent } from "leaflet";
|
import { LeafletMouseEvent } from "leaflet";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
import * as R from "ramda";
|
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ref } from "vue";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { copyToClipboard } from "../services/ClipboardService";
|
||||||
@@ -799,7 +758,6 @@ import {
|
|||||||
NotificationIface,
|
NotificationIface,
|
||||||
PASSKEYS_ENABLED,
|
PASSKEYS_ENABLED,
|
||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||||
BoundingBox,
|
BoundingBox,
|
||||||
@@ -823,11 +781,7 @@ import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
|||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||||
import {
|
import { AccountSettings, isApiError } from "@/interfaces/accountView";
|
||||||
AccountSettings,
|
|
||||||
isApiError,
|
|
||||||
ImportContent,
|
|
||||||
} from "@/interfaces/accountView";
|
|
||||||
// Profile data interface (inlined from ProfileService)
|
// Profile data interface (inlined from ProfileService)
|
||||||
interface ProfileData {
|
interface ProfileData {
|
||||||
description: string;
|
description: string;
|
||||||
@@ -836,8 +790,6 @@ interface ProfileData {
|
|||||||
includeLocation: boolean;
|
includeLocation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputImportFileNameRef = ref<Blob>();
|
|
||||||
|
|
||||||
interface UserNameDialogRef {
|
interface UserNameDialogRef {
|
||||||
open: (cb: (name?: string) => void) => void;
|
open: (cb: (name?: string) => void) => void;
|
||||||
}
|
}
|
||||||
@@ -1369,65 +1321,6 @@ export default class AccountViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImportFile(event: Event): Promise<void> {
|
|
||||||
inputImportFileNameRef.value = (
|
|
||||||
event.target as HTMLInputElement
|
|
||||||
).files?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
showContactImport(): boolean {
|
|
||||||
return !!inputImportFileNameRef.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmSubmitImportFile(): void {
|
|
||||||
if (inputImportFileNameRef.value != null) {
|
|
||||||
this.notify.confirm(
|
|
||||||
ACCOUNT_VIEW_CONSTANTS.WARNINGS.IMPORT_REPLACE_WARNING,
|
|
||||||
this.submitImportFile,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously imports the database from a downloadable JSON file.
|
|
||||||
*
|
|
||||||
* @throws Will notify the user if there is an export error.
|
|
||||||
*/
|
|
||||||
async submitImportFile(): Promise<void> {
|
|
||||||
if (inputImportFileNameRef.value != null) {
|
|
||||||
// TODO: implement this for SQLite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkContactImports(): Promise<void> {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
const fileContent: string = (event.target?.result as string) || "{}";
|
|
||||||
try {
|
|
||||||
const contents: ImportContent = JSON.parse(fileContent);
|
|
||||||
const contactTableRows: Array<Contact> = (
|
|
||||||
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
|
|
||||||
)?.find((table) => table.tableName === "contacts")
|
|
||||||
?.rows as Array<Contact>;
|
|
||||||
const contactRows = contactTableRows.map(
|
|
||||||
// @ts-expect-error for omitting this field that is found in the Dexie format
|
|
||||||
(contact) => R.omit(["$types"], contact) as Contact,
|
|
||||||
);
|
|
||||||
(this.$router as Router).push({
|
|
||||||
name: "contact-import",
|
|
||||||
query: { contacts: JSON.stringify(contactRows) },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking contact imports:", error);
|
|
||||||
this.notify.error(
|
|
||||||
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMPORT_ERROR,
|
|
||||||
TIMEOUTS.STANDARD,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(inputImportFileNameRef.value as Blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
private progressCallback(progress: ImportProgress): boolean {
|
private progressCallback(progress: ImportProgress): boolean {
|
||||||
logger.log(
|
logger.log(
|
||||||
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
||||||
|
|||||||
@@ -55,56 +55,70 @@
|
|||||||
|
|
||||||
<!-- Contact Methods -->
|
<!-- Contact Methods -->
|
||||||
<div class="mt-4">
|
<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="mt-4">
|
||||||
<div
|
<!-- Type and Value Row -->
|
||||||
v-for="(method, index) in contactMethods"
|
<div class="flex gap-2">
|
||||||
:key="index"
|
<div class="flex-none w-32">
|
||||||
class="flex mt-2"
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
>
|
Type
|
||||||
<input
|
</label>
|
||||||
v-model="method.label"
|
<select
|
||||||
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"
|
v-model="method.type"
|
||||||
type="text"
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
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" />
|
<option value="">—</option>
|
||||||
</button>
|
<option
|
||||||
<div
|
|
||||||
v-if="dropdownIndex === index"
|
|
||||||
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="methodType in contactMethodTypes"
|
v-for="methodType in contactMethodTypes"
|
||||||
:key="methodType.value"
|
:key="methodType.value"
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
:value="methodType.value"
|
||||||
@click="setMethodType(index, methodType.value)"
|
|
||||||
>
|
>
|
||||||
{{ methodType.label }}
|
{{ methodType.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex-1">
|
||||||
</div>
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Value
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model="method.value"
|
v-model="method.value"
|
||||||
type="text"
|
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"
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
placeholder="Number, email, etc."
|
placeholder="Number, email, etc."
|
||||||
/>
|
/>
|
||||||
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-end pb-0.5 text-red-500"
|
||||||
|
@click="removeContactMethod(index)"
|
||||||
|
>
|
||||||
<font-awesome icon="trash-can" class="fa-fw" />
|
<font-awesome icon="trash-can" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="mt-2" @click="addContactMethod">
|
|
||||||
|
<!-- WhatsApp Help Text -->
|
||||||
|
<div
|
||||||
|
v-if="method.type === 'WHATSAPP'"
|
||||||
|
class="mt-1 ml-[calc(8rem+0.5rem)] text-xs text-gray-600 italic"
|
||||||
|
>
|
||||||
|
Must include country code and only numbers (e.g., 12225551234)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label Row -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<div class="flex-1 ml-[calc(8rem+0.5rem)]">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="method.label"
|
||||||
|
type="text"
|
||||||
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Label / Note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-[2.5rem]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="mt-4" @click="addContactMethod">
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="plus"
|
icon="plus"
|
||||||
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
||||||
@@ -210,12 +224,10 @@ export default class ContactEditView extends Vue {
|
|||||||
contactNotes = "";
|
contactNotes = "";
|
||||||
/** Array of editable contact methods */
|
/** Array of editable contact methods */
|
||||||
contactMethods: Array<ContactMethod> = [];
|
contactMethods: Array<ContactMethod> = [];
|
||||||
/** Currently open dropdown index, null if none open */
|
|
||||||
dropdownIndex: number | null = null;
|
|
||||||
|
|
||||||
/** App string constants */
|
/** App string constants */
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
/** Contact method types for dropdown */
|
/** Contact method types for datalist suggestions */
|
||||||
contactMethodTypes = CONTACT_METHOD_TYPES;
|
contactMethodTypes = CONTACT_METHOD_TYPES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,29 +285,6 @@ export default class ContactEditView extends Vue {
|
|||||||
this.contactMethods.splice(index, 1);
|
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
|
* Saves the edited contact information to the database
|
||||||
*
|
*
|
||||||
@@ -331,9 +320,10 @@ export default class ContactEditView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to database via PlatformServiceMixin
|
// Save to database via PlatformServiceMixin
|
||||||
|
// Normalize empty strings to null to preserve database consistency
|
||||||
await this.$updateContact(this.contact?.did || "", {
|
await this.$updateContact(this.contact?.did || "", {
|
||||||
name: this.contactName,
|
name: this.contactName?.trim() || null,
|
||||||
notes: this.contactNotes,
|
notes: this.contactNotes?.trim() || null,
|
||||||
contactMethods: contactMethods,
|
contactMethods: contactMethods,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,14 +52,16 @@
|
|||||||
|
|
||||||
<!-- Contact Methods -->
|
<!-- Contact Methods -->
|
||||||
<div v-if="contactFromDid.contactMethods?.length" class="mt-3">
|
<div v-if="contactFromDid.contactMethods?.length" class="mt-3">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div
|
<div
|
||||||
v-for="(method, index) in contactFromDid.contactMethods"
|
v-for="(method, index) in contactFromDid.contactMethods"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="inline-flex items-center gap-2 text-sm"
|
class="flex items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-slate-600"
|
<span class="font-semibold text-slate-600"
|
||||||
>{{ getContactMethodLabel(method.type) }}:</span
|
>{{
|
||||||
|
getContactMethodLabel(method.type) || "(unspecified)"
|
||||||
|
}}:</span
|
||||||
>
|
>
|
||||||
<span class="text-slate-700">{{ method.label }}</span>
|
<span class="text-slate-700">{{ method.label }}</span>
|
||||||
<span class="text-slate-600">{{ method.value }}</span>
|
<span class="text-slate-600">{{ method.value }}</span>
|
||||||
@@ -71,6 +73,23 @@
|
|||||||
>
|
>
|
||||||
<font-awesome icon="message" class="text-base" />
|
<font-awesome icon="message" class="text-base" />
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'EMAIL'"
|
||||||
|
:href="`mailto:${method.value}`"
|
||||||
|
class="ml-2 text-blue-500 hover:text-blue-700"
|
||||||
|
title="Send email"
|
||||||
|
>
|
||||||
|
<font-awesome icon="envelope" class="text-base" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'WHATSAPP'"
|
||||||
|
:href="`https://wa.me/${method.value.replace(/\D/g, '')}`"
|
||||||
|
target="_blank"
|
||||||
|
class="ml-2 text-blue-700"
|
||||||
|
title="Send WhatsApp message"
|
||||||
|
>
|
||||||
|
<font-awesome :icon="['fab', 'whatsapp']" class="text-base" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user