forked from trent_larson/crowd-funder-for-time-pwa
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
This commit is contained in:
@@ -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
|
||||
*/
|
||||
private platformService: PlatformService =
|
||||
PlatformServiceFactory.getInstance();
|
||||
notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
/**
|
||||
* Platform capabilities for the current platform
|
||||
* Computed property to check if we're on web platform
|
||||
*/
|
||||
private get platformCapabilities(): PlatformCapabilities {
|
||||
return this.platformService.getCapabilities();
|
||||
private get isWebPlatform(): boolean {
|
||||
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)
|
||||
*/
|
||||
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
|
||||
? "See your downloads directory for the backup."
|
||||
: "The backup file has been saved.",
|
||||
},
|
||||
3000,
|
||||
this.notify.success(
|
||||
this.isWebPlatform
|
||||
? "See your downloads directory for the backup."
|
||||
: "The backup file has been saved.",
|
||||
);
|
||||
} 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>
|
||||
|
||||
@@ -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>
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
</script>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user