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 isMenuOpen = ref(false)
|
||||||
const menuContainerRef = ref<HTMLElement | null>(null)
|
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 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) {
|
function toggleMenu(event: Event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -58,7 +65,7 @@ onUnmounted(() => {
|
|||||||
<div class="list-card-main">
|
<div class="list-card-main">
|
||||||
<h3>{{ list.name }}</h3>
|
<h3>{{ list.name }}</h3>
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
{{ completedCount }}/{{ items.length }} done
|
{{ completedCount }}/{{ totalCount }} done
|
||||||
<span v-if="list.pendingSync" class="pending-tag">syncing…</span>
|
<span v-if="list.pendingSync" class="pending-tag">syncing…</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -235,16 +235,30 @@ describe('useListsStore', () => {
|
|||||||
expect(updated?.pendingSync).toBe(false)
|
expect(updated?.pendingSync).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pulls lists and items from the server and merges them locally', async () => {
|
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' }])
|
listsApiMocks.getListsApi.mockResolvedValueOnce([
|
||||||
listsApiMocks.getListItemsApi.mockResolvedValueOnce([
|
{ id: 'server-list-1', name: 'Groceries', total_items: 3, completed_items: 1 },
|
||||||
{ id: 'server-item-1', list_id: 'server-list-1', title: 'Milk', is_completed: false },
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const store = useListsStore()
|
const store = useListsStore()
|
||||||
await store.pullFromServer()
|
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()
|
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
|
// Pulls the authoritative lists from the server and merges them into local
|
||||||
// local storage. Entries that still have local unsynced changes
|
// storage. Entries that still have local unsynced changes (pendingSync)
|
||||||
// (pendingSync) are left untouched so we never clobber pending edits.
|
// 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> {
|
async function pullFromServer(): Promise<void> {
|
||||||
if (typeof navigator !== 'undefined' && !navigator.onLine) return
|
if (typeof navigator !== 'undefined' && !navigator.onLine) return
|
||||||
|
|
||||||
@@ -331,18 +336,6 @@ export const useListsStore = defineStore('lists', () => {
|
|||||||
if (!existingList || !existingList.pendingSync) {
|
if (!existingList || !existingList.pendingSync) {
|
||||||
await db.lists.put({ ...serverList, pendingSync: false })
|
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) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Failed to load lists from server'
|
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 {
|
return {
|
||||||
lists,
|
lists,
|
||||||
listItems,
|
listItems,
|
||||||
@@ -361,6 +382,7 @@ export const useListsStore = defineStore('lists', () => {
|
|||||||
error,
|
error,
|
||||||
itemsForList,
|
itemsForList,
|
||||||
loadLists,
|
loadLists,
|
||||||
|
loadListItems,
|
||||||
refresh,
|
refresh,
|
||||||
createList,
|
createList,
|
||||||
createListItem,
|
createListItem,
|
||||||
@@ -370,5 +392,6 @@ export const useListsStore = defineStore('lists', () => {
|
|||||||
removeUserFromList,
|
removeUserFromList,
|
||||||
sync,
|
sync,
|
||||||
pullFromServer,
|
pullFromServer,
|
||||||
|
pullListItems,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export interface List {
|
|||||||
name: string
|
name: string
|
||||||
created_at?: string
|
created_at?: string
|
||||||
modified_at?: string
|
modified_at?: string
|
||||||
|
total_items?: number
|
||||||
|
completed_items?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListItem {
|
export interface ListItem {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const itemError = ref('')
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
listsStore.loadLists()
|
listsStore.loadLists()
|
||||||
|
listsStore.loadListItems(props.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const list = computed(() => listsStore.lists.find((entry) => entry.id === props.id))
|
const list = computed(() => listsStore.lists.find((entry) => entry.id === props.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user