From 94bd64900373db23318295ec0126d43432b7222b Mon Sep 17 00:00:00 2001 From: Matthew Raymer <matthew.raymer@anomalistdesign.com> Date: Mon, 7 Apr 2025 07:17:43 +0000 Subject: [PATCH] refactor: improve camera controls and modularize data export - Add detailed error logging for image upload failures in PhotoDialog and SharedPhotoView - Extract DataExportSection into standalone component with proper prop handling - Fix Backup Identifier Seed visibility by passing activeDid prop --- src/components/DataExportSection.vue | 114 +++++++++++++++++++++++++++ src/components/PhotoDialog.vue | 21 ++--- src/views/AccountViewView.vue | 50 +----------- src/views/SharedPhotoView.vue | 21 ++--- 4 files changed, 139 insertions(+), 67 deletions(-) create mode 100644 src/components/DataExportSection.vue diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue new file mode 100644 index 00000000..8a195918 --- /dev/null +++ b/src/components/DataExportSection.vue @@ -0,0 +1,114 @@ +<template> + <div + id="sectionDataExport" + class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" + > + <div class="mb-2 font-bold">Data Export</div> + <router-link + v-if="activeDid" + :to="{ name: 'seed-backup' }" + 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 + </router-link> + + <button + :class="computedStartDownloadLinkClassNames()" + 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()" + > + Download Settings & Contacts + <br /> + (excluding Identifier Data) + </button> + <a + ref="downloadLink" + :class="computedDownloadLinkClassNames()" + class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" + > + If no download happened yet, click again here to download now. + </a> + <div class="mt-4"> + <p> + After the download, you can save the file in your preferred storage + location. + </p> + <ul> + <li class="list-disc list-outside ml-4"> + On iOS: Choose "More..." and select a place in iCloud, or go "Back" + and save to another location. + </li> + <li class="list-disc list-outside ml-4"> + On Android: Choose "Open" and then share + <font-awesome icon="share-nodes" class="fa-fw" /> + to your prefered place. + </li> + </ul> + </div> + </div> +</template> + +<script lang="ts"> +import { Component, Prop, Vue } from "vue-facing-decorator"; +import { NotificationIface } from "../constants/app"; +import { db } from "../db/index"; +import { logger } from "../utils/logger"; + +@Component +export default class DataExportSection extends Vue { + $notify!: (notification: NotificationIface, timeout?: number) => void; + + @Prop({ required: true }) readonly activeDid!: string; + downloadUrl = ""; + + beforeUnmount() { + if (this.downloadUrl) { + URL.revokeObjectURL(this.downloadUrl); + } + } + + public async exportDatabase() { + try { + const blob = await db.export({ prettyJson: true }); + this.downloadUrl = URL.createObjectURL(blob); + const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; + downloadAnchor.href = this.downloadUrl; + downloadAnchor.download = `${db.name}-backup.json`; + downloadAnchor.click(); + this.$notify( + { + group: "alert", + type: "success", + title: "Download Started", + text: "See your downloads directory for the backup. It is in the Dexie format.", + }, + -1, + ); + setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); + } catch (error) { + logger.error("Export Error:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Export Error", + text: "There was an error exporting the data.", + }, + 3000, + ); + } + } + + public computedStartDownloadLinkClassNames() { + return { + hidden: this.downloadUrl, + }; + } + + public computedDownloadLinkClassNames() { + return { + hidden: !this.downloadUrl, + }; + } +} +</script> diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index f0f5b631..a5962f71 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -273,14 +273,14 @@ export default class PhotoDialog extends Vue { } catch (error) { // Log the raw error first logger.error("Raw error object:", JSON.stringify(error, null, 2)); - + let errorMessage = "There was an error saving the picture."; - + if (axios.isAxiosError(error)) { const status = error.response?.status; const statusText = error.response?.statusText; const data = error.response?.data; - + // Log detailed error information logger.error("Upload error details:", { status, @@ -290,16 +290,17 @@ export default class PhotoDialog extends Vue { config: { url: error.config?.url, method: error.config?.method, - headers: error.config?.headers - } + headers: error.config?.headers, + }, }); - + if (status === 401) { errorMessage = "Authentication failed. Please try logging in again."; } else if (status === 413) { errorMessage = "Image file is too large. Please try a smaller image."; } else if (status === 415) { - errorMessage = "Unsupported image format. Please try a different image."; + errorMessage = + "Unsupported image format. Please try a different image."; } else if (status && status >= 500) { errorMessage = "Server error. Please try again later."; } else if (data?.message) { @@ -311,16 +312,16 @@ export default class PhotoDialog extends Vue { name: error.name, message: error.message, stack: error.stack, - error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2) + error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2), }); } else { // Log any other type of error logger.error("Unknown error type:", { error: JSON.stringify(error, null, 2), - type: typeof error + type: typeof error, }); } - + this.$notify( { group: "alert", diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index f9d76cd5..24712a6e 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -420,53 +420,7 @@ </button> </div> - <div - id="sectionDataExport" - class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" - > - <div class="mb-2 font-bold">Data Export</div> - <router-link - v-if="activeDid" - :to="{ name: 'seed-backup' }" - 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 - </router-link> - - <button - :class="computedStartDownloadLinkClassNames()" - 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()" - > - Download Settings & Contacts - <br /> - (excluding Identifier Data) - </button> - <a - ref="downloadLink" - :class="computedDownloadLinkClassNames()" - class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" - > - If no download happened yet, click again here to download now. - </a> - <div class="mt-4"> - <p> - After the download, you can save the file in your preferred storage - location. - </p> - <ul> - <li class="list-disc list-outside ml-4"> - On iOS: Choose "More..." and select a place in iCloud, or go "Back" - and save to another location. - </li> - <li class="list-disc list-outside ml-4"> - On Android: Choose "Open" and then share - <font-awesome icon="share-nodes" class="fa-fw" /> - to your prefered place. - </li> - </ul> - </div> - </div> + <DataExportSection :active-did="activeDid" /> <!-- id used by puppeteer test script --> <h3 @@ -946,6 +900,7 @@ import PushNotificationPermission from "../components/PushNotificationPermission import QuickNav from "../components/QuickNav.vue"; import TopMessage from "../components/TopMessage.vue"; import UserNameDialog from "../components/UserNameDialog.vue"; +import DataExportSection from "../components/DataExportSection.vue"; import { AppString, DEFAULT_IMAGE_API_SERVER, @@ -999,6 +954,7 @@ const inputImportFileNameRef = ref<Blob>(); QuickNav, TopMessage, UserNameDialog, + DataExportSection, }, }) export default class AccountViewView extends Vue { diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue index 815e1b17..26f05672 100644 --- a/src/views/SharedPhotoView.vue +++ b/src/views/SharedPhotoView.vue @@ -223,14 +223,14 @@ export default class SharedPhotoView extends Vue { } catch (error) { // Log the raw error first logger.error("Raw error object:", JSON.stringify(error, null, 2)); - + let errorMessage = "There was an error saving the picture."; - + if (axios.isAxiosError(error)) { const status = error.response?.status; const statusText = error.response?.statusText; const data = error.response?.data; - + // Log detailed error information logger.error("Upload error details:", { status, @@ -240,16 +240,17 @@ export default class SharedPhotoView extends Vue { config: { url: error.config?.url, method: error.config?.method, - headers: error.config?.headers - } + headers: error.config?.headers, + }, }); - + if (status === 401) { errorMessage = "Authentication failed. Please try logging in again."; } else if (status === 413) { errorMessage = "Image file is too large. Please try a smaller image."; } else if (status === 415) { - errorMessage = "Unsupported image format. Please try a different image."; + errorMessage = + "Unsupported image format. Please try a different image."; } else if (status && status >= 500) { errorMessage = "Server error. Please try again later."; } else if (data?.message) { @@ -261,16 +262,16 @@ export default class SharedPhotoView extends Vue { name: error.name, message: error.message, stack: error.stack, - error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2) + error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2), }); } else { // Log any other type of error logger.error("Unknown error type:", { error: JSON.stringify(error, null, 2), - type: typeof error + type: typeof error, }); } - + this.$notify( { group: "alert",