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>
|
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user