Browse Source

refactor: migrate DataExportSection to PlatformServiceMixin

- Use PlatformServiceMixin for platform and database access
- Replace manual PlatformService instantiation with mixin methods/properties
- Use $contacts() for contact export
- Use capabilities for platform checks in template and logic
- Remove unused imports and redundant code
- Lint clean
pull/142/head
Matthew Raymer 1 day ago
parent
commit
d8b078d372
  1. 134
      src/components/DataExportSection.vue
  2. 8
      src/components/LocationSearchSection.vue
  3. 9
      src/components/RegistrationNotice.vue
  4. 10
      src/components/UsageLimitsSection.vue
  5. 14
      src/views/AccountViewView.vue

134
src/components/DataExportSection.vue

@ -20,34 +20,33 @@ backup and database export, with platform-specific download instructions. * *
</router-link> </router-link>
<button <button
:class="computedStartDownloadLinkClassNames()" :class="{ hidden: isDownloadInProgress }"
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" 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 Contacts Download Contacts
</button> </button>
<a <a
v-if="isWebPlatform && downloadUrl"
ref="downloadLink" ref="downloadLink"
:class="computedDownloadLinkClassNames()" :href="downloadUrl"
:download="fileName"
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" 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. If no download happened yet, click again here to download now.
</a> </a>
<div v-if="platformCapabilities.needsFileHandlingInstructions" class="mt-4"> <div v-if="capabilities.needsFileHandlingInstructions" class="mt-4">
<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 <li v-if="capabilities.isIOS" class="list-disc list-outside ml-4">
v-if="platformCapabilities.isIOS"
class="list-disc list-outside ml-4"
>
On iOS: You will be prompted to choose a location to save your backup On iOS: You will be prompted to choose a location to save your backup
file. file.
</li> </li>
<li <li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS" v-if="capabilities.isMobile && !capabilities.isIOS"
class="list-disc list-outside ml-4" class="list-disc list-outside ml-4"
> >
On Android: You will be prompted to choose a location to save your On Android: You will be prompted to choose a location to save your
@ -62,23 +61,19 @@ backup and database export, with platform-specific download instructions. * *
import { Component, Prop, Vue } from "vue-facing-decorator"; import { Component, Prop, Vue } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import {
PlatformService,
PlatformCapabilities,
} from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util"; import { contactsToExportJson } from "../libs/util";
import { createNotifyHelpers } from "@/utils/notify";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
/** /**
* @vue-component * @vue-component
* Data Export Section Component * Data Export Section Component
* Handles database export and seed backup functionality with platform-specific behavior * Handles database export and seed backup functionality with platform-specific behavior
*/ */
@Component @Component({
mixins: [PlatformServiceMixin],
})
export default class DataExportSection extends Vue { export default class DataExportSection extends Vue {
/** /**
* Notification function injected by Vue * Notification function injected by Vue
@ -101,16 +96,29 @@ export default class DataExportSection extends Vue {
downloadUrl = ""; downloadUrl = "";
/** /**
* Platform service instance for platform-specific operations * Notification helper for consistent notification patterns
*/ */
private platformService: PlatformService = notify = createNotifyHelpers(this.$notify);
PlatformServiceFactory.getInstance();
/** /**
* Platform capabilities for the current platform * Computed property to check if we're on web platform
*/ */
private get platformCapabilities(): PlatformCapabilities { private get isWebPlatform(): boolean {
return this.platformService.getCapabilities(); return this.capabilities.hasFileDownload;
}
/**
* Computed property to check if download is in progress
*/
private get isDownloadInProgress(): boolean {
return Boolean(this.downloadUrl && this.isWebPlatform);
}
/**
* Computed property for the export file name
*/
private get fileName(): string {
return `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
} }
/** /**
@ -118,7 +126,7 @@ export default class DataExportSection extends Vue {
* Revokes object URL when component is unmounted (web platform only) * Revokes object URL when component is unmounted (web platform only)
*/ */
beforeUnmount() { beforeUnmount() {
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) { if (this.downloadUrl && this.isWebPlatform) {
URL.revokeObjectURL(this.downloadUrl); URL.revokeObjectURL(this.downloadUrl);
} }
} }
@ -129,84 +137,56 @@ export default class DataExportSection extends Vue {
* Shows success/error notifications to user * Shows success/error notifications to user
* *
* @throws {Error} If export fails * @throws {Error} If export fails
* @emits {Notification} Success or error notification
*/ */
public async exportDatabase() { public async exportDatabase(): Promise<void> {
try { try {
let allContacts: Contact[] = []; // Fetch contacts from database using mixin's cached method
const platformService = PlatformServiceFactory.getInstance(); const allContacts = await this.$contacts();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
// Convert contacts to export format // Convert contacts to export format
const exportData = contactsToExportJson(allContacts); const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2); const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" }); const blob = new Blob([jsonStr], { type: "application/json" });
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`; // Handle export based on platform capabilities
if (this.isWebPlatform) {
if (this.platformCapabilities.hasFileDownload) { await this.handleWebExport(blob);
// Web platform: Use download link } else if (this.capabilities.hasFileSystem) {
this.downloadUrl = URL.createObjectURL(blob); await this.handleNativeExport(jsonStr);
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = this.downloadUrl;
downloadAnchor.download = fileName;
downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory
await this.platformService.writeAndShareFile(fileName, jsonStr);
} else { } else {
throw new Error("This platform does not support file downloads."); throw new Error("This platform does not support file downloads.");
} }
this.$notify( this.notify.success(
{ this.isWebPlatform
group: "alert", ? "See your downloads directory for the backup."
type: "success", : "The backup file has been saved.",
title: "Export Successful",
text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup."
: "The backup file has been saved.",
},
3000,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Export Error:", error);
this.$notify( this.notify.error(
{ `There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
group: "alert",
type: "danger",
title: "Export Error",
text: "There was an error exporting the data.",
},
3000,
); );
} }
} }
/** /**
* Computes class names for the initial download button * Handles export for web platform using download link
* @returns Object with 'hidden' class when download is in progress (web platform only) * @param blob The blob to download
*/ */
public computedStartDownloadLinkClassNames() { private async handleWebExport(blob: Blob): Promise<void> {
return { this.downloadUrl = URL.createObjectURL(blob);
hidden: this.downloadUrl && this.platformCapabilities.hasFileDownload, const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
}; downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} }
/** /**
* Computes class names for the secondary download link * Handles export for native platforms using file system
* @returns Object with 'hidden' class when no download is available or not on web platform * @param jsonStr The JSON string to save
*/ */
public computedDownloadLinkClassNames() { private async handleNativeExport(jsonStr: string): Promise<void> {
return { await this.platformService.writeAndShareFile(this.fileName, jsonStr);
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
};
} }
} }
</script> </script>

8
src/components/LocationSearchSection.vue

@ -1,7 +1,7 @@
<template> <template>
<section v-if="isRegistered" class="mt-4"> <section v-if="isRegistered" class="mt-4">
<h2 class="text-lg font-semibold mb-2">Location for Searches</h2> <h2 class="text-lg font-semibold mb-2">Location for Searches</h2>
<div class="mb-2" v-if="searchAreaLabel"> <div v-if="searchAreaLabel" class="mb-2">
<span class="text-slate-700">Current Area: </span> <span class="text-slate-700">Current Area: </span>
<span class="font-mono">{{ searchAreaLabel }}</span> <span class="font-mono">{{ searchAreaLabel }}</span>
</div> </div>
@ -15,14 +15,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator'; import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
@Component({ name: 'LocationSearchSection' }) @Component({ name: "LocationSearchSection" })
export default class LocationSearchSection extends Vue { export default class LocationSearchSection extends Vue {
@Prop({ required: true }) isRegistered!: boolean; @Prop({ required: true }) isRegistered!: boolean;
@Prop({ required: false }) searchAreaLabel?: string; @Prop({ required: false }) searchAreaLabel?: string;
@Emit('set-search-area') @Emit("set-search-area")
setSearchArea() {} setSearchArea() {}
} }
</script> </script>

9
src/components/RegistrationNotice.vue

@ -7,7 +7,8 @@
aria-live="polite" aria-live="polite"
> >
<p class="mb-4"> <p class="mb-4">
Before you can publicly announce a new project or time commitment, a friend needs to register you. Before you can publicly announce a new project or time commitment, a
friend needs to register you.
</p> </p>
<button <button
class="inline-block 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-4 py-2 rounded-md" class="inline-block 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-4 py-2 rounded-md"
@ -19,14 +20,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator'; import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
@Component({ name: 'RegistrationNotice' }) @Component({ name: "RegistrationNotice" })
export default class RegistrationNotice extends Vue { export default class RegistrationNotice extends Vue {
@Prop({ required: true }) isRegistered!: boolean; @Prop({ required: true }) isRegistered!: boolean;
@Prop({ required: true }) show!: boolean; @Prop({ required: true }) show!: boolean;
@Emit('share-info') @Emit("share-info")
shareInfo() {} shareInfo() {}
} }
</script> </script>

10
src/components/UsageLimitsSection.vue

@ -2,7 +2,9 @@
<section class="mt-4"> <section class="mt-4">
<h2 class="text-lg font-semibold mb-2">Usage Limits</h2> <h2 class="text-lg font-semibold mb-2">Usage Limits</h2>
<div class="mb-2"> <div class="mb-2">
<span v-if="loadingLimits" class="text-slate-700">Checking... <span class="animate-spin"></span></span> <span v-if="loadingLimits" class="text-slate-700"
>Checking... <span class="animate-spin"></span></span
>
<span v-else class="text-slate-700">{{ limitsMessage }}</span> <span v-else class="text-slate-700">{{ limitsMessage }}</span>
</div> </div>
<button <button
@ -15,14 +17,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop, Emit } from 'vue-facing-decorator'; import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
@Component({ name: 'UsageLimitsSection' }) @Component({ name: "UsageLimitsSection" })
export default class UsageLimitsSection extends Vue { export default class UsageLimitsSection extends Vue {
@Prop({ required: true }) loadingLimits!: boolean; @Prop({ required: true }) loadingLimits!: boolean;
@Prop({ required: true }) limitsMessage!: string; @Prop({ required: true }) limitsMessage!: string;
@Emit('recheck-limits') @Emit("recheck-limits")
recheckLimits() {} recheckLimits() {}
} }
</script> </script>

14
src/views/AccountViewView.vue

@ -766,10 +766,10 @@ import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue"; import UserNameDialog from "../components/UserNameDialog.vue";
import DataExportSection from "../components/DataExportSection.vue"; import DataExportSection from "../components/DataExportSection.vue";
import IdentitySection from '@/components/IdentitySection.vue'; import IdentitySection from "@/components/IdentitySection.vue";
import RegistrationNotice from '@/components/RegistrationNotice.vue'; import RegistrationNotice from "@/components/RegistrationNotice.vue";
import LocationSearchSection from '@/components/LocationSearchSection.vue'; import LocationSearchSection from "@/components/LocationSearchSection.vue";
import UsageLimitsSection from '@/components/UsageLimitsSection.vue'; import UsageLimitsSection from "@/components/UsageLimitsSection.vue";
import { import {
AppString, AppString,
DEFAULT_IMAGE_API_SERVER, DEFAULT_IMAGE_API_SERVER,
@ -1635,13 +1635,13 @@ export default class AccountViewView extends Vue {
// Placeholder for share dialog logic // Placeholder for share dialog logic
openShareDialog() { openShareDialog() {
// TODO: Implement share dialog logic // TODO: Implement share dialog logic
this.notify.info('Share dialog not yet implemented.'); this.notify.info("Share dialog not yet implemented.");
} }
get searchAreaLabel(): string { get searchAreaLabel(): string {
// Return a string representing the current search area, or blank if not set // Return a string representing the current search area, or blank if not set
// Example: return this.searchAreaName || ''; // Example: return this.searchAreaName || '';
return this.isSearchAreasSet ? 'Custom Area Set' : ''; return this.isSearchAreasSet ? "Custom Area Set" : "";
} }
onSetSearchArea() { onSetSearchArea() {
@ -1651,7 +1651,7 @@ export default class AccountViewView extends Vue {
openSearchAreaDialog() { openSearchAreaDialog() {
// TODO: Implement search area dialog logic // TODO: Implement search area dialog logic
this.notify.info('Search area dialog not yet implemented.'); this.notify.info("Search area dialog not yet implemented.");
} }
onRecheckLimits() { onRecheckLimits() {

Loading…
Cancel
Save