fix: utilizing new total/completed fields in /lists endpoint to lazy load items on demand
This commit is contained in:
@@ -13,8 +13,15 @@ const listsStore = useListsStore()
|
||||
const isMenuOpen = ref(false)
|
||||
const menuContainerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// The server now reports total_items/completed_items directly on the list,
|
||||
// so the overview can show progress without having to load every item of
|
||||
// every list. Fall back to counting locally cached items for lists that
|
||||
// haven't synced to the server yet (e.g. just created while offline).
|
||||
const items = computed(() => listsStore.itemsForList(props.list.id))
|
||||
const completedCount = computed(() => items.value.filter((item) => item.is_completed).length)
|
||||
const totalCount = computed(() => props.list.total_items ?? items.value.length)
|
||||
const completedCount = computed(
|
||||
() => props.list.completed_items ?? items.value.filter((item) => item.is_completed).length,
|
||||
)
|
||||
|
||||
function toggleMenu(event: Event) {
|
||||
event.preventDefault()
|
||||
@@ -58,7 +65,7 @@ onUnmounted(() => {
|
||||
<div class="list-card-main">
|
||||
<h3>{{ list.name }}</h3>
|
||||
<p class="meta">
|
||||
{{ completedCount }}/{{ items.length }} done
|
||||
{{ completedCount }}/{{ totalCount }} done
|
||||
<span v-if="list.pendingSync" class="pending-tag">syncing…</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -235,16 +235,30 @@ describe('useListsStore', () => {
|
||||
expect(updated?.pendingSync).toBe(false)
|
||||
})
|
||||
|
||||
it('pulls lists and items from the server and merges them locally', async () => {
|
||||
listsApiMocks.getListsApi.mockResolvedValueOnce([{ id: 'server-list-1', name: 'Groceries' }])
|
||||
listsApiMocks.getListItemsApi.mockResolvedValueOnce([
|
||||
{ id: 'server-item-1', list_id: 'server-list-1', title: 'Milk', is_completed: false },
|
||||
it('pulls lists from the server, including total/completed item counts, without fetching every item', async () => {
|
||||
listsApiMocks.getListsApi.mockResolvedValueOnce([
|
||||
{ id: 'server-list-1', name: 'Groceries', total_items: 3, completed_items: 1 },
|
||||
])
|
||||
|
||||
const store = useListsStore()
|
||||
await store.pullFromServer()
|
||||
|
||||
expect(store.lists.find((list) => list.id === 'server-list-1')).toBeDefined()
|
||||
const pulledList = store.lists.find((list) => list.id === 'server-list-1')
|
||||
expect(pulledList).toBeDefined()
|
||||
expect(pulledList?.total_items).toBe(3)
|
||||
expect(pulledList?.completed_items).toBe(1)
|
||||
expect(listsApiMocks.getListItemsApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('pulls the items of a single list on demand via pullListItems', async () => {
|
||||
listsApiMocks.getListItemsApi.mockResolvedValueOnce([
|
||||
{ id: 'server-item-1', list_id: 'server-list-1', title: 'Milk', is_completed: false },
|
||||
])
|
||||
|
||||
const store = useListsStore()
|
||||
await store.pullListItems('server-list-1')
|
||||
|
||||
expect(listsApiMocks.getListItemsApi).toHaveBeenCalledWith('server-list-1')
|
||||
expect(store.listItems.find((item) => item.id === 'server-item-1')).toBeDefined()
|
||||
})
|
||||
|
||||
|
||||
+38
-15
@@ -317,9 +317,14 @@ export const useListsStore = defineStore('lists', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Pulls the authoritative lists/items from the server and merges them into
|
||||
// local storage. Entries that still have local unsynced changes
|
||||
// (pendingSync) are left untouched so we never clobber pending edits.
|
||||
// Pulls the authoritative lists from the server and merges them into local
|
||||
// storage. Entries that still have local unsynced changes (pendingSync)
|
||||
// are left untouched so we never clobber pending edits.
|
||||
//
|
||||
// This no longer eagerly fetches every item of every list: the server now
|
||||
// reports total_items/completed_items directly on each list, which is all
|
||||
// the overview page needs. Items for a specific list are only pulled on
|
||||
// demand via pullListItems (e.g. when opening its detail view).
|
||||
async function pullFromServer(): Promise<void> {
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return
|
||||
|
||||
@@ -331,18 +336,6 @@ export const useListsStore = defineStore('lists', () => {
|
||||
if (!existingList || !existingList.pendingSync) {
|
||||
await db.lists.put({ ...serverList, pendingSync: false })
|
||||
}
|
||||
|
||||
try {
|
||||
const serverItems = await getListItemsApi(serverList.id)
|
||||
for (const serverItem of serverItems) {
|
||||
const existingItem = await db.listItems.get(serverItem.id)
|
||||
if (!existingItem || !existingItem.pendingSync) {
|
||||
await db.listItems.put({ ...serverItem, pendingSync: false })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore per-list failures so one broken list doesn't block the rest.
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to load lists from server'
|
||||
@@ -351,6 +344,34 @@ export const useListsStore = defineStore('lists', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Pulls the authoritative items of a single list from the server and
|
||||
// merges them into local storage. Used by the list detail view, which is
|
||||
// the only place that needs the full item set for a list.
|
||||
async function pullListItems(listId: string): Promise<void> {
|
||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return
|
||||
|
||||
try {
|
||||
const serverItems = await getListItemsApi(listId)
|
||||
for (const serverItem of serverItems) {
|
||||
const existingItem = await db.listItems.get(serverItem.id)
|
||||
if (!existingItem || !existingItem.pendingSync) {
|
||||
await db.listItems.put({ ...serverItem, pendingSync: false })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to load list items from server'
|
||||
} finally {
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadListItems(listId: string) {
|
||||
if (!isLoaded.value) {
|
||||
await refresh()
|
||||
}
|
||||
void pullListItems(listId)
|
||||
}
|
||||
|
||||
return {
|
||||
lists,
|
||||
listItems,
|
||||
@@ -361,6 +382,7 @@ export const useListsStore = defineStore('lists', () => {
|
||||
error,
|
||||
itemsForList,
|
||||
loadLists,
|
||||
loadListItems,
|
||||
refresh,
|
||||
createList,
|
||||
createListItem,
|
||||
@@ -370,5 +392,6 @@ export const useListsStore = defineStore('lists', () => {
|
||||
removeUserFromList,
|
||||
sync,
|
||||
pullFromServer,
|
||||
pullListItems,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface List {
|
||||
name: string
|
||||
created_at?: string
|
||||
modified_at?: string
|
||||
total_items?: number
|
||||
completed_items?: number
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
|
||||
@@ -15,6 +15,7 @@ const itemError = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
listsStore.loadLists()
|
||||
listsStore.loadListItems(props.id)
|
||||
})
|
||||
|
||||
const list = computed(() => listsStore.lists.find((entry) => entry.id === props.id))
|
||||
|
||||
Reference in New Issue
Block a user