forked from trent_larson/crowd-funder-for-time-pwa
import & update selected contacts
This commit is contained in:
@@ -63,6 +63,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "contact-gift",
|
name: "contact-gift",
|
||||||
component: () => import("../views/ContactGiftingView.vue"),
|
component: () => import("../views/ContactGiftingView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/contact-import",
|
||||||
|
name: "contact-import",
|
||||||
|
component: () => import("../views/ContactImportView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-qr",
|
path: "/contact-qr",
|
||||||
name: "contact-qr",
|
name: "contact-qr",
|
||||||
|
|||||||
@@ -216,7 +216,7 @@
|
|||||||
<div class="mb-2 font-bold">Location</div>
|
<div class="mb-2 font-bold">Location</div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'search-area' }"
|
:to="{ name: 'search-area' }"
|
||||||
class="block w-full text-center text-m bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-6"
|
class="block w-full text-center text-m 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-2 mt-6"
|
||||||
>
|
>
|
||||||
Set Search Area…
|
Set Search Area…
|
||||||
<!-- If already set, change button label to "Change Search Area" -->
|
<!-- If already set, change button label to "Change Search Area" -->
|
||||||
@@ -282,14 +282,14 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'seed-backup' }"
|
:to="{ name: 'seed-backup' }"
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
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 mb-2 mt-2"
|
||||||
>
|
>
|
||||||
Backup Identifier Seed
|
Backup Identifier Seed
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-bind:class="computedStartDownloadLinkClassNames()"
|
v-bind:class="computedStartDownloadLinkClassNames()"
|
||||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
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="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Settings & Contacts
|
Download Settings & Contacts
|
||||||
@@ -303,19 +303,20 @@
|
|||||||
>
|
>
|
||||||
If no download happened yet, click again here to download now.
|
If no download happened yet, click again here to download now.
|
||||||
</a>
|
</a>
|
||||||
<div v-if="downloadUrl">
|
<div>
|
||||||
<p>
|
<p>
|
||||||
After the download, you can save the file in your preferred storage
|
After the download, you can save the file in your preferred storage
|
||||||
location.
|
location.
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
On iOS: Choose "More..." and select anyplace in iCloud, or go "Back"
|
On iOS: Choose "More..." and select a place in iCloud, or go "Back"
|
||||||
and save to another location.
|
and save to another location.
|
||||||
</li>
|
</li>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
On Android: Choose "Open" and then share to your prefered place.
|
On Android: Choose "Open" and then share
|
||||||
<fa icon="share-nodes" class="fa-fw" />
|
<fa icon="share-nodes" class="fa-fw" />
|
||||||
|
to your prefered place.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -410,20 +411,27 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h2 class="text-slate-500 text-sm font-bold">
|
<h2 class="text-slate-500 text-sm font-bold">
|
||||||
Contacts & Settings Database
|
Import Contacts & Settings Database
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="ml-4 mt-2">
|
<div class="ml-4 mt-2">
|
||||||
Import
|
|
||||||
<input type="file" @change="uploadImportFile" class="ml-2" />
|
<input type="file" @change="uploadImportFile" class="ml-2" />
|
||||||
<div v-if="showContactImport()">
|
<div v-if="showContactImport()" class="mt-4">
|
||||||
<button
|
<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"
|
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()"
|
@click="confirmSubmitImportFile()"
|
||||||
>
|
>
|
||||||
Import Settings & Contacts
|
Overwrite Settings & Contacts
|
||||||
<br />
|
<br />
|
||||||
(excluding Identifier Data)
|
(which doesn't include Identifier Data)
|
||||||
|
</button>
|
||||||
|
<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
|
||||||
|
<br />
|
||||||
|
after comparing
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -692,9 +700,11 @@ import { Buffer } from "buffer/";
|
|||||||
import Dexie from "dexie";
|
import Dexie from "dexie";
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
import { ImportProgress } from "dexie-export-import/dist/import";
|
import { ImportProgress } from "dexie-export-import/dist/import";
|
||||||
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -710,6 +720,7 @@ import {
|
|||||||
} from "@/constants/app";
|
} from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
@@ -1146,6 +1157,40 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkContactImports() {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const fileContent: string = (event.target?.result as string) || "{}";
|
||||||
|
try {
|
||||||
|
const contents = 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) {
|
||||||
|
console.error("Error checking contact imports:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Importing",
|
||||||
|
text: "There was an error reading that Dexie file.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(inputImportFileNameRef.value as Blob);
|
||||||
|
}
|
||||||
|
|
||||||
private progressCallback(progress: ImportProgress) {
|
private progressCallback(progress: ImportProgress) {
|
||||||
console.log(
|
console.log(
|
||||||
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
||||||
|
|||||||
179
src/views/ContactImportView.vue
Normal file
179
src/views/ContactImportView.vue
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Contact Import
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div v-if="sameCount > 0">
|
||||||
|
{{ sameCount }} contact{{ sameCount == 1 ? "" : "s" }} are the same as
|
||||||
|
existing contacts.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results List -->
|
||||||
|
<ul v-if="contactsImporting.length > 0" class="border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
v-for="(contact, index) in contactsImporting"
|
||||||
|
:key="contact.did"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!contactsExisting[contact.did] ||
|
||||||
|
!R.isEmpty(contactDifferences[contact.did])
|
||||||
|
"
|
||||||
|
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
|
||||||
|
>
|
||||||
|
<h2 class="text-base font-semibold">
|
||||||
|
<input type="checkbox" v-model="contactsSelected[index]" />
|
||||||
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||||
|
-
|
||||||
|
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
|
||||||
|
>Existing</span
|
||||||
|
>
|
||||||
|
<span v-else class="text-green-500">New</span>
|
||||||
|
</h2>
|
||||||
|
<div class="text-sm truncate">
|
||||||
|
{{ contact.did }}
|
||||||
|
</div>
|
||||||
|
<div v-if="contactDifferences[contact.did]">
|
||||||
|
<div>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<div class="font-bold">Field</div>
|
||||||
|
<div class="font-bold">Old Value</div>
|
||||||
|
<div class="font-bold">New Value</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(value, contactField) in contactDifferences[contact.did]"
|
||||||
|
:key="contactField"
|
||||||
|
class="grid grid-cols-3 border"
|
||||||
|
>
|
||||||
|
<div class="border p-1">{{ contactField }}</div>
|
||||||
|
<div class="border p-1">{{ value.old }}</div>
|
||||||
|
<div class="border p-1">{{ value.new }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<fa icon="spinner" v-if="importing" class="animate-spin" />
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="text-sm 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-2 py-1.5 rounded-l-md"
|
||||||
|
@click="importContacts"
|
||||||
|
>
|
||||||
|
Import Selected Contacts
|
||||||
|
</button>
|
||||||
|
</ul>
|
||||||
|
<p v-else>There are no contacts to import.</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import * as R from "ramda";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav },
|
||||||
|
})
|
||||||
|
export default class ContactImportView extends Vue {
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
AppString = AppString;
|
||||||
|
libsUtil = libsUtil;
|
||||||
|
R = R;
|
||||||
|
|
||||||
|
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
|
||||||
|
contactsImporting: Array<Contact> = []; // contacts from the import
|
||||||
|
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
|
||||||
|
contactDifferences: Record<
|
||||||
|
string,
|
||||||
|
Record<string, { new: string; old: string }>
|
||||||
|
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
|
||||||
|
importing = false;
|
||||||
|
sameCount = 0;
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
// Retrieve the imported contacts from the query parameter
|
||||||
|
const importedContacts =
|
||||||
|
((this.$route as Router).query["contacts"] as string) || "[]";
|
||||||
|
this.contactsImporting = JSON.parse(importedContacts);
|
||||||
|
this.contactsSelected = new Array(this.contactsImporting.length).fill(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.open();
|
||||||
|
const baseContacts = await db.contacts.toArray();
|
||||||
|
// set the existing contacts, keyed by DID, if they exist in contactsImporting
|
||||||
|
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||||
|
const contactIn = this.contactsImporting[i];
|
||||||
|
const existingContact = baseContacts.find(
|
||||||
|
(contact) => contact.did === contactIn.did,
|
||||||
|
);
|
||||||
|
if (existingContact) {
|
||||||
|
this.contactsExisting[contactIn.did] = existingContact;
|
||||||
|
|
||||||
|
const differences: Record<string, { new: string; old: string }> = {};
|
||||||
|
Object.keys(contactIn).forEach((key) => {
|
||||||
|
if (contactIn[key] !== existingContact[key]) {
|
||||||
|
differences[key] = {
|
||||||
|
old: existingContact[key],
|
||||||
|
new: contactIn[key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.contactDifferences[contactIn.did] = differences;
|
||||||
|
if (R.isEmpty(differences)) {
|
||||||
|
this.sameCount++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// automatically import new data
|
||||||
|
this.contactsSelected[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importContacts() {
|
||||||
|
this.importing = true;
|
||||||
|
let importedCount = 0,
|
||||||
|
updatedCount = 0;
|
||||||
|
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||||
|
if (this.contactsSelected[i]) {
|
||||||
|
const contact = R.clone(this.contactsImporting[i]); // cloning to avoid the problem with a Proxy object
|
||||||
|
const existingContact = this.contactsExisting[contact.did];
|
||||||
|
if (existingContact) {
|
||||||
|
await db.contacts.update(contact.did, contact);
|
||||||
|
updatedCount++;
|
||||||
|
} else {
|
||||||
|
await db.contacts.add(contact);
|
||||||
|
importedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.importing = false;
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Import Success",
|
||||||
|
text:
|
||||||
|
`${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` +
|
||||||
|
(updatedCount ? ` ${updatedCount} updated.` : ""),
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
(this.$router as Router).push({ name: "contacts" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user