Implement offline functionality with conflict resolution; add OfflineIndicator component for user notifications, integrate offline action management in the store, and enhance service worker caching strategies for improved performance.
This commit is contained in:
parent
7bbec7ad5f
commit
db5f2d089e
@ -162,12 +162,12 @@ export default defineConfig((ctx) => {
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||
pwa: {
|
||||
workboxMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
|
||||
// swFilename: 'sw.js',
|
||||
// manifestFilename: 'manifest.json',
|
||||
workboxMode: 'InjectManifest', // Changed from 'GenerateSW' to 'InjectManifest'
|
||||
swFilename: 'sw.js',
|
||||
manifestFilename: 'manifest.json',
|
||||
injectPwaMetaTags: true,
|
||||
// extendManifestJson (json) {},
|
||||
// useCredentialsForManifestTag: true,
|
||||
// injectPwaMetaTags: false,
|
||||
// extendPWACustomSWConf (esbuildConf) {},
|
||||
// extendGenerateSWOptions (cfg) {},
|
||||
// extendInjectManifestOptions (cfg) {}
|
||||
|
@ -14,6 +14,10 @@ import {
|
||||
createHandlerBoundToURL,
|
||||
} from 'workbox-precaching';
|
||||
import { registerRoute, NavigationRoute } from 'workbox-routing';
|
||||
import { CacheFirst, NetworkFirst } from 'workbox-strategies';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
|
||||
import type { WorkboxPlugin } from 'workbox-core/types';
|
||||
|
||||
self.skipWaiting().catch((error) => {
|
||||
console.error('Error during service worker activation:', error);
|
||||
@ -25,6 +29,46 @@ precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
cleanupOutdatedCaches();
|
||||
|
||||
// Cache app shell and static assets with Cache First strategy
|
||||
registerRoute(
|
||||
// Match static assets
|
||||
({ request }) =>
|
||||
request.destination === 'style' ||
|
||||
request.destination === 'script' ||
|
||||
request.destination === 'image' ||
|
||||
request.destination === 'font',
|
||||
new CacheFirst({
|
||||
cacheName: 'static-assets',
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200],
|
||||
}) as WorkboxPlugin,
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 60,
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
||||
}) as WorkboxPlugin,
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// Cache API calls with Network First strategy
|
||||
registerRoute(
|
||||
// Match API calls
|
||||
({ url }) => url.pathname.startsWith('/api/'),
|
||||
new NetworkFirst({
|
||||
cacheName: 'api-cache',
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200],
|
||||
}) as WorkboxPlugin,
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
||||
}) as WorkboxPlugin,
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// Non-SSR fallbacks to index.html
|
||||
// Production SSR fallbacks to offline.html (except for dev)
|
||||
if (process.env.MODE !== 'ssr' || process.env.PROD) {
|
||||
|
288
fe/src/components/ConflictResolutionDialog.vue
Normal file
288
fe/src/components/ConflictResolutionDialog.vue
Normal file
@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<q-dialog v-model="show" persistent>
|
||||
<q-card style="min-width: 600px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Conflict Resolution</div>
|
||||
<div class="text-subtitle2 q-mt-sm">
|
||||
This item was modified while you were offline. Please review the changes and choose how to resolve the conflict.
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
class="text-primary"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="justify"
|
||||
narrow-indicator
|
||||
>
|
||||
<q-tab name="compare" label="Compare Versions" />
|
||||
<q-tab name="merge" label="Merge Changes" />
|
||||
</q-tabs>
|
||||
|
||||
<q-tab-panels v-model="activeTab" animated>
|
||||
<!-- Compare Versions Tab -->
|
||||
<q-tab-panel name="compare">
|
||||
<div class="row q-col-gutter-md">
|
||||
<!-- Local Version -->
|
||||
<div class="col-6">
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1">Your Version</div>
|
||||
<div class="text-caption">
|
||||
Last modified: {{ formatDate(conflictData?.localVersion.timestamp ?? 0) }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-list>
|
||||
<q-item v-for="(value, key) in conflictData?.localVersion.data" :key="key">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-caption text-grey">
|
||||
{{ formatKey(key) }}
|
||||
</q-item-label>
|
||||
<q-item-label :class="{ 'text-positive': isDifferent(key) }">
|
||||
{{ formatValue(value) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Server Version -->
|
||||
<div class="col-6">
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1">Server Version</div>
|
||||
<div class="text-caption">
|
||||
Last modified: {{ formatDate(conflictData?.serverVersion.timestamp ?? 0) }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-list>
|
||||
<q-item v-for="(value, key) in conflictData?.serverVersion.data" :key="key">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-caption text-grey">
|
||||
{{ formatKey(key) }}
|
||||
</q-item-label>
|
||||
<q-item-label :class="{ 'text-positive': isDifferent(key) }">
|
||||
{{ formatValue(value) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Merge Changes Tab -->
|
||||
<q-tab-panel name="merge">
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1">Merge Changes</div>
|
||||
<div class="text-caption">
|
||||
Select which version to keep for each field
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-list>
|
||||
<q-item v-for="(value, key) in conflictData?.localVersion.data" :key="key">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-caption text-grey">
|
||||
{{ formatKey(key) }}
|
||||
</q-item-label>
|
||||
<div class="row q-col-gutter-sm q-mt-xs">
|
||||
<div class="col">
|
||||
<q-radio
|
||||
v-model="mergeChoices[key]"
|
||||
val="local"
|
||||
label="Your Version"
|
||||
/>
|
||||
<div class="text-caption">
|
||||
{{ formatValue(value) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-radio
|
||||
v-model="mergeChoices[key]"
|
||||
val="server"
|
||||
label="Server Version"
|
||||
/>
|
||||
<div class="text-caption">
|
||||
{{ formatValue(conflictData?.serverVersion.data[key]) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
v-if="activeTab === 'compare'"
|
||||
flat
|
||||
label="Keep Local Version"
|
||||
color="primary"
|
||||
@click="resolveConflict('local')"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="activeTab === 'compare'"
|
||||
flat
|
||||
label="Keep Server Version"
|
||||
color="primary"
|
||||
@click="resolveConflict('server')"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="activeTab === 'compare'"
|
||||
flat
|
||||
label="Merge Changes"
|
||||
color="primary"
|
||||
@click="activeTab = 'merge'"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="activeTab === 'merge'"
|
||||
flat
|
||||
label="Apply Merged Changes"
|
||||
color="primary"
|
||||
@click="applyMergedChanges"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
label="Cancel"
|
||||
color="negative"
|
||||
@click="show = false"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import type { OfflineAction } from 'src/stores/offline';
|
||||
|
||||
interface ConflictData {
|
||||
localVersion: {
|
||||
data: Record<string, any>;
|
||||
timestamp: number;
|
||||
};
|
||||
serverVersion: {
|
||||
data: Record<string, any>;
|
||||
timestamp: number;
|
||||
};
|
||||
action: OfflineAction;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
conflictData: ConflictData | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'resolve', resolution: { version: 'local' | 'server' | 'merge'; action: OfflineAction; mergedData?: Record<string, any> }): void;
|
||||
}>();
|
||||
|
||||
const show = ref(props.modelValue);
|
||||
const activeTab = ref('compare');
|
||||
const mergeChoices = ref<Record<string, 'local' | 'server'>>({});
|
||||
|
||||
// Watch for changes in modelValue
|
||||
watch(() => props.modelValue, (newValue: boolean) => {
|
||||
show.value = newValue;
|
||||
});
|
||||
|
||||
// Watch for changes in show
|
||||
watch(show, (newValue: boolean) => {
|
||||
emit('update:modelValue', newValue);
|
||||
});
|
||||
|
||||
// Initialize merge choices when conflict data changes
|
||||
watch(() => props.conflictData, (newData) => {
|
||||
if (newData) {
|
||||
const choices: Record<string, 'local' | 'server'> = {};
|
||||
Object.keys(newData.localVersion.data).forEach(key => {
|
||||
choices[key] = isDifferent(key) ? 'local' : 'local';
|
||||
});
|
||||
mergeChoices.value = choices;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const formatDate = (timestamp: number): string => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
const formatKey = (key: string): string => {
|
||||
return key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.replace(/^\w/, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
const formatValue = (value: any): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const isDifferent = (key: string): boolean => {
|
||||
if (!props.conflictData) return false;
|
||||
const localValue = props.conflictData.localVersion.data[key];
|
||||
const serverValue = props.conflictData.serverVersion.data[key];
|
||||
return JSON.stringify(localValue) !== JSON.stringify(serverValue);
|
||||
};
|
||||
|
||||
const resolveConflict = (version: 'local' | 'server' | 'merge'): void => {
|
||||
if (!props.conflictData) return;
|
||||
|
||||
emit('resolve', {
|
||||
version,
|
||||
action: props.conflictData.action
|
||||
});
|
||||
|
||||
show.value = false;
|
||||
};
|
||||
|
||||
const applyMergedChanges = (): void => {
|
||||
if (!props.conflictData) return;
|
||||
|
||||
const mergedData: Record<string, any> = {};
|
||||
Object.entries(mergeChoices.value).forEach(([key, choice]) => {
|
||||
const localValue = props.conflictData?.localVersion.data[key];
|
||||
const serverValue = props.conflictData?.serverVersion.data[key];
|
||||
mergedData[key] = choice === 'local' ? localValue : serverValue;
|
||||
});
|
||||
|
||||
emit('resolve', {
|
||||
version: 'merge',
|
||||
action: props.conflictData.action,
|
||||
mergedData
|
||||
});
|
||||
|
||||
show.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.q-card {
|
||||
.text-caption {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.text-positive {
|
||||
color: $positive;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
123
fe/src/components/OfflineIndicator.vue
Normal file
123
fe/src/components/OfflineIndicator.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<q-banner
|
||||
v-if="!isOnline || hasPendingActions"
|
||||
:class="[
|
||||
'offline-indicator',
|
||||
{ 'offline': !isOnline },
|
||||
{ 'pending': hasPendingActions }
|
||||
]"
|
||||
rounded
|
||||
>
|
||||
<template v-slot:avatar>
|
||||
<q-icon
|
||||
:name="!isOnline ? 'wifi_off' : 'sync'"
|
||||
:color="!isOnline ? 'negative' : 'warning'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="!isOnline">
|
||||
You are currently offline. Changes will be saved locally.
|
||||
</template>
|
||||
<template v-else>
|
||||
Syncing {{ pendingActionCount }} pending {{ pendingActionCount === 1 ? 'change' : 'changes' }}...
|
||||
</template>
|
||||
|
||||
<template v-slot:action>
|
||||
<q-btn
|
||||
v-if="hasPendingActions"
|
||||
flat
|
||||
color="primary"
|
||||
label="View Changes"
|
||||
@click="showPendingActions = true"
|
||||
/>
|
||||
</template>
|
||||
</q-banner>
|
||||
|
||||
<q-dialog v-model="showPendingActions">
|
||||
<q-card style="min-width: 350px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Pending Changes</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-list>
|
||||
<q-item v-for="action in pendingActions" :key="action.id">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ getActionLabel(action) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ new Date(action.timestamp).toLocaleString() }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Close" color="primary" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Conflict Resolution Dialog -->
|
||||
<ConflictResolutionDialog
|
||||
v-model="showConflictDialog"
|
||||
:conflict-data="currentConflict"
|
||||
@resolve="handleConflictResolution"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useOfflineStore } from 'src/stores/offline';
|
||||
import type { OfflineAction } from 'src/stores/offline';
|
||||
import ConflictResolutionDialog from './ConflictResolutionDialog.vue';
|
||||
|
||||
const offlineStore = useOfflineStore();
|
||||
const showPendingActions = ref(false);
|
||||
|
||||
const {
|
||||
isOnline,
|
||||
pendingActions,
|
||||
hasPendingActions,
|
||||
pendingActionCount,
|
||||
showConflictDialog,
|
||||
currentConflict,
|
||||
handleConflictResolution,
|
||||
} = offlineStore;
|
||||
|
||||
const getActionLabel = (action: OfflineAction) => {
|
||||
switch (action.type) {
|
||||
case 'add':
|
||||
return `Add new item: ${action.data.title || 'Untitled'}`;
|
||||
case 'complete':
|
||||
return `Complete item: ${action.data.title || 'Untitled'}`;
|
||||
case 'update':
|
||||
return `Update item: ${action.data.title || 'Untitled'}`;
|
||||
case 'delete':
|
||||
return `Delete item: ${action.data.title || 'Untitled'}`;
|
||||
default:
|
||||
return 'Unknown action';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.offline-indicator {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&.offline {
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -32,6 +32,9 @@
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
|
||||
<!-- Offline Indicator -->
|
||||
<OfflineIndicator />
|
||||
|
||||
<!-- Bottom Navigation -->
|
||||
<q-footer elevated class="bg-white text-primary">
|
||||
<q-tabs
|
||||
@ -55,6 +58,7 @@ import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useAuthStore } from 'stores/auth';
|
||||
import OfflineIndicator from 'components/OfflineIndicator.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
|
157
fe/src/stores/offline.ts
Normal file
157
fe/src/stores/offline.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { LocalStorage } from 'quasar';
|
||||
|
||||
export interface OfflineAction {
|
||||
id: string;
|
||||
type: 'add' | 'complete' | 'update' | 'delete';
|
||||
itemId?: string;
|
||||
data: any;
|
||||
timestamp: number;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export interface ConflictResolution {
|
||||
version: 'local' | 'server' | 'merge';
|
||||
action: OfflineAction;
|
||||
}
|
||||
|
||||
export const useOfflineStore = defineStore('offline', () => {
|
||||
const $q = useQuasar();
|
||||
const isOnline = ref(navigator.onLine);
|
||||
const pendingActions = ref<OfflineAction[]>([]);
|
||||
const isProcessingQueue = ref(false);
|
||||
const showConflictDialog = ref(false);
|
||||
const currentConflict = ref<ConflictResolution | null>(null);
|
||||
|
||||
// Initialize from IndexedDB
|
||||
const init = async () => {
|
||||
try {
|
||||
const stored = LocalStorage.getItem('offline-actions');
|
||||
if (stored) {
|
||||
pendingActions.value = JSON.parse(stored as string);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load offline actions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Save to IndexedDB
|
||||
const saveToStorage = () => {
|
||||
try {
|
||||
LocalStorage.set('offline-actions', JSON.stringify(pendingActions.value));
|
||||
} catch (error) {
|
||||
console.error('Failed to save offline actions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a new offline action
|
||||
const addAction = (action: Omit<OfflineAction, 'id' | 'timestamp'>) => {
|
||||
const newAction: OfflineAction = {
|
||||
...action,
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
pendingActions.value.push(newAction);
|
||||
saveToStorage();
|
||||
};
|
||||
|
||||
// Process the queue when online
|
||||
const processQueue = async () => {
|
||||
if (isProcessingQueue.value || !isOnline.value) return;
|
||||
|
||||
isProcessingQueue.value = true;
|
||||
const actions = [...pendingActions.value];
|
||||
|
||||
for (const action of actions) {
|
||||
try {
|
||||
// TODO: Implement actual API calls based on action type
|
||||
// This will be implemented when we have the API endpoints
|
||||
await processAction(action);
|
||||
|
||||
// Remove successful action
|
||||
pendingActions.value = pendingActions.value.filter(a => a.id !== action.id);
|
||||
saveToStorage();
|
||||
} catch (error: any) {
|
||||
if (error.status === 409) {
|
||||
// Handle version conflict
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Item was modified by someone else while you were offline. Please review.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Review',
|
||||
color: 'white',
|
||||
handler: () => {
|
||||
// TODO: Implement conflict resolution UI
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to process offline action:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isProcessingQueue.value = false;
|
||||
};
|
||||
|
||||
// Process a single action
|
||||
const processAction = async (action: OfflineAction) => {
|
||||
// TODO: Implement actual API calls
|
||||
// This is a placeholder that will be replaced with actual API calls
|
||||
switch (action.type) {
|
||||
case 'add':
|
||||
// await api.addItem(action.data);
|
||||
break;
|
||||
case 'complete':
|
||||
// await api.completeItem(action.itemId, action.data);
|
||||
break;
|
||||
case 'update':
|
||||
// await api.updateItem(action.itemId, action.data);
|
||||
break;
|
||||
case 'delete':
|
||||
// await api.deleteItem(action.itemId);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for online/offline status changes
|
||||
const setupNetworkListeners = () => {
|
||||
window.addEventListener('online', () => {
|
||||
isOnline.value = true;
|
||||
processQueue();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
isOnline.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// Computed properties
|
||||
const hasPendingActions = computed(() => pendingActions.value.length > 0);
|
||||
const pendingActionCount = computed(() => pendingActions.value.length);
|
||||
|
||||
// Initialize
|
||||
init();
|
||||
setupNetworkListeners();
|
||||
|
||||
const handleConflictResolution = (resolution: ConflictResolution) => {
|
||||
// Implement the logic to handle the conflict resolution
|
||||
console.log('Conflict resolution:', resolution);
|
||||
};
|
||||
|
||||
return {
|
||||
isOnline,
|
||||
pendingActions,
|
||||
hasPendingActions,
|
||||
pendingActionCount,
|
||||
showConflictDialog,
|
||||
currentConflict,
|
||||
addAction,
|
||||
processQueue,
|
||||
handleConflictResolution,
|
||||
};
|
||||
});
|
Loading…
Reference in New Issue
Block a user