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. 130
      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

130
src/components/DataExportSection.vue

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

8
src/components/LocationSearchSection.vue

@ -1,7 +1,7 @@
<template>
<section v-if="isRegistered" class="mt-4">
<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="font-mono">{{ searchAreaLabel }}</span>
</div>
@ -15,14 +15,14 @@
</template>
<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 {
@Prop({ required: true }) isRegistered!: boolean;
@Prop({ required: false }) searchAreaLabel?: string;
@Emit('set-search-area')
@Emit("set-search-area")
setSearchArea() {}
}
</script>

9
src/components/RegistrationNotice.vue

@ -7,7 +7,8 @@
aria-live="polite"
>
<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>
<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"
@ -19,14 +20,14 @@
</template>
<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 {
@Prop({ required: true }) isRegistered!: boolean;
@Prop({ required: true }) show!: boolean;
@Emit('share-info')
@Emit("share-info")
shareInfo() {}
}
</script>

10
src/components/UsageLimitsSection.vue

@ -2,7 +2,9 @@
<section class="mt-4">
<h2 class="text-lg font-semibold mb-2">Usage Limits</h2>
<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>
</div>
<button
@ -15,14 +17,14 @@
</template>
<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 {
@Prop({ required: true }) loadingLimits!: boolean;
@Prop({ required: true }) limitsMessage!: string;
@Emit('recheck-limits')
@Emit("recheck-limits")
recheckLimits() {}
}
</script>

14
src/views/AccountViewView.vue

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

Loading…
Cancel
Save