feat: implement offline support for chore archiving and unarchiving, enhance state management for pending commands
This commit is contained in:
@@ -26,6 +26,34 @@ class CommandQueue {
|
||||
return sanitized
|
||||
}
|
||||
|
||||
_clearPendingFlags(chore = {}) {
|
||||
const next = { ...chore }
|
||||
delete next._pending
|
||||
delete next._pendingUpdate
|
||||
return next
|
||||
}
|
||||
|
||||
async _rollbackCancelledCommand(command) {
|
||||
if (!command) return
|
||||
|
||||
if (
|
||||
command.commandType !== CommandType.ARCHIVE_CHORE &&
|
||||
command.commandType !== CommandType.UNARCHIVE_CHORE
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const cachedChore = await offlineDB.getChore(command.entityId)
|
||||
if (!cachedChore) return
|
||||
|
||||
const restoredChore = this._clearPendingFlags({
|
||||
...cachedChore,
|
||||
isActive: command.commandType === CommandType.ARCHIVE_CHORE,
|
||||
})
|
||||
|
||||
await offlineDB.saveChores([restoredChore])
|
||||
}
|
||||
|
||||
// Enqueue a domain command
|
||||
async enqueue(type, entityId, payload) {
|
||||
if (!isOfflineFeatureEnabled()) {
|
||||
@@ -81,6 +109,11 @@ class CommandQueue {
|
||||
// Cancel/undo a pending command
|
||||
async cancel(commandId) {
|
||||
if (!isOfflineFeatureEnabled()) return
|
||||
|
||||
const allCommands = await offlineDB.getCommands()
|
||||
const command = allCommands.find(c => String(c.id) === String(commandId))
|
||||
|
||||
await this._rollbackCancelledCommand(command)
|
||||
return offlineDB.removeCommand(commandId)
|
||||
}
|
||||
|
||||
|
||||
@@ -536,6 +536,7 @@ const ChoreView = () => {
|
||||
try {
|
||||
const response = await UnArchiveChore(choreId)
|
||||
if (response.ok) {
|
||||
await offlineDB.saveChores([{ ...chore, isActive: true }])
|
||||
setChore({ ...chore, isActive: true })
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
}
|
||||
@@ -548,12 +549,16 @@ const ChoreView = () => {
|
||||
choreId,
|
||||
{ id: choreId },
|
||||
)
|
||||
await offlineDB.saveChores([
|
||||
{ ...chore, isActive: true, _pending: 'unarchive' },
|
||||
])
|
||||
setChore({ ...chore, isActive: true })
|
||||
queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
|
||||
showSuccess({
|
||||
message: "You're offline — restore will sync when back online",
|
||||
undoAction: async () => {
|
||||
await commandQueue.cancel(cmdId)
|
||||
await offlineDB.saveChores([{ ...chore, isActive: false }])
|
||||
setChore({ ...chore, isActive: false })
|
||||
queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { commandQueue, CommandType } from '../../utils/CommandQueue'
|
||||
import { DeleteChore, GetArchivedChores } from '../../utils/Fetcher'
|
||||
import { offlineDB } from '../../utils/OfflineDB'
|
||||
import LoadingComponent from '../components/Loading'
|
||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||
import ChoreCard from './ChoreCard'
|
||||
@@ -38,6 +39,48 @@ import ChoreListView from './ChoreListView.jsx'
|
||||
import CompactChoreCard from './CompactChoreCard'
|
||||
import MultiSelectHelp from './MultiSelectHelp'
|
||||
|
||||
const sortByUpdatedAtDesc = chores =>
|
||||
(chores || []).sort((a, b) => {
|
||||
const dateA = new Date(a.updatedAt || 0)
|
||||
const dateB = new Date(b.updatedAt || 0)
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
const applyPendingArchivedState = async chores => {
|
||||
const pending = await commandQueue.getPending()
|
||||
|
||||
const pendingArchiveIds = new Set(
|
||||
pending
|
||||
.filter(cmd => cmd.commandType === CommandType.ARCHIVE_CHORE)
|
||||
.map(cmd => String(cmd.entityId)),
|
||||
)
|
||||
const pendingUnarchiveIds = new Set(
|
||||
pending
|
||||
.filter(cmd => cmd.commandType === CommandType.UNARCHIVE_CHORE)
|
||||
.map(cmd => String(cmd.entityId)),
|
||||
)
|
||||
const pendingDeleteIds = new Set(
|
||||
pending
|
||||
.filter(cmd => cmd.commandType === CommandType.DELETE_CHORE)
|
||||
.map(cmd => String(cmd.entityId)),
|
||||
)
|
||||
|
||||
return (chores || [])
|
||||
.filter(chore => !pendingDeleteIds.has(String(chore.id)))
|
||||
.filter(chore => {
|
||||
const id = String(chore.id)
|
||||
if (pendingUnarchiveIds.has(id)) return false
|
||||
return chore.isActive === false || pendingArchiveIds.has(id)
|
||||
})
|
||||
.map(chore => {
|
||||
const id = String(chore.id)
|
||||
if (pendingArchiveIds.has(id)) {
|
||||
return { ...chore, isActive: false, _pending: 'archive' }
|
||||
}
|
||||
return chore
|
||||
})
|
||||
}
|
||||
|
||||
const ArchivedTasks = () => {
|
||||
const { data: userProfile, isLoading: isUserProfileLoading } =
|
||||
useUserProfile()
|
||||
@@ -71,19 +114,28 @@ const ArchivedTasks = () => {
|
||||
try {
|
||||
const response = await GetArchivedChores()
|
||||
const data = await response.json()
|
||||
// Sort by updatedAt (most recent first)
|
||||
const sortedChores = data.res.sort((a, b) => {
|
||||
const dateA = new Date(a.updatedAt || 0)
|
||||
const dateB = new Date(b.updatedAt || 0)
|
||||
return dateB - dateA
|
||||
})
|
||||
if (data?.res?.length) {
|
||||
await offlineDB.saveChores(data.res)
|
||||
}
|
||||
const archivedWithPending = await applyPendingArchivedState(
|
||||
data?.res || [],
|
||||
)
|
||||
const sortedChores = sortByUpdatedAtDesc(archivedWithPending)
|
||||
setArchivedChores(sortedChores)
|
||||
setFilteredChores(sortedChores)
|
||||
} catch (error) {
|
||||
try {
|
||||
const cached = await offlineDB.getChores(true)
|
||||
const archivedWithPending = await applyPendingArchivedState(cached)
|
||||
const sortedChores = sortByUpdatedAtDesc(archivedWithPending)
|
||||
setArchivedChores(sortedChores)
|
||||
setFilteredChores(sortedChores)
|
||||
} catch {
|
||||
showError({
|
||||
title: 'Failed to load archived tasks',
|
||||
message: 'Please try again later.',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -337,6 +389,9 @@ const ArchivedTasks = () => {
|
||||
onError: async error => {
|
||||
if (isNetworkError(error)) {
|
||||
await commandQueue.enqueue(CommandType.UNARCHIVE_CHORE, chore.id, { id: chore.id })
|
||||
await offlineDB.saveChores([
|
||||
{ ...chore, isActive: true, _pending: 'unarchive' },
|
||||
])
|
||||
queuedTasks.push(chore)
|
||||
resolve()
|
||||
} else {
|
||||
|
||||
@@ -481,6 +481,9 @@ export const useChoreActions = ({
|
||||
chore.id,
|
||||
{ id: chore.id },
|
||||
)
|
||||
await offlineDB.saveChores([
|
||||
{ ...chore, isActive: false, _pending: 'archive' },
|
||||
])
|
||||
setChores(prev => prev.filter(c => c.id !== chore.id))
|
||||
setFilteredChores(prev =>
|
||||
prev.filter(c => c.id !== chore.id),
|
||||
@@ -493,6 +496,9 @@ export const useChoreActions = ({
|
||||
"You're offline — archive will sync when back online",
|
||||
undoAction: async () => {
|
||||
await commandQueue.cancel(cmdId)
|
||||
await offlineDB.saveChores([
|
||||
{ ...chore, isActive: true },
|
||||
])
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['pendingCommands'],
|
||||
})
|
||||
@@ -529,6 +535,9 @@ export const useChoreActions = ({
|
||||
chore.id,
|
||||
{ id: chore.id },
|
||||
)
|
||||
await offlineDB.saveChores([
|
||||
{ ...chore, isActive: true, _pending: 'unarchive' },
|
||||
])
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['pendingCommands'],
|
||||
})
|
||||
@@ -537,6 +546,9 @@ export const useChoreActions = ({
|
||||
"You're offline — restore will sync when back online",
|
||||
undoAction: async () => {
|
||||
await commandQueue.cancel(cmdId)
|
||||
await offlineDB.saveChores([
|
||||
{ ...chore, isActive: false },
|
||||
])
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['pendingCommands'],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user