Skip to content

Commit fa16893

Browse files
committed
feat: hide completed work items by default in collection views
1 parent 6e44579 commit fa16893

4 files changed

Lines changed: 89 additions & 25 deletions

File tree

frontend/src/lib/features/collections/CollectionBacklog.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@
410410
const boundaryItem = await api.items.getBacklogBoundary(
411411
workspaceId,
412412
collectionId,
413-
collectionStore.subFilterQL,
413+
collectionStore.effectiveSubFilterQL,
414414
boundary,
415415
);
416416
if (!boundaryItem || boundaryItem.id === item.id) return;

frontend/src/lib/features/collections/CollectionBoard.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
154154
$effect(() => {
155155
const query = searchQuery.trim();
156-
const scope = `${workspaceId ?? ''}|${collectionId ?? ''}|${collectionStore.subFilterQL}`;
156+
const scope = `${workspaceId ?? ''}|${collectionId ?? ''}|${collectionStore.effectiveSubFilterQL}`;
157157
const effectId = ++searchEffectId;
158158
collectionStore.clearBoardSearch();
159159
@@ -168,7 +168,7 @@
168168
if (
169169
effectId === searchEffectId &&
170170
query === searchQuery.trim() &&
171-
scope === `${workspaceId ?? ''}|${collectionId ?? ''}|${collectionStore.subFilterQL}`
171+
scope === `${workspaceId ?? ''}|${collectionId ?? ''}|${collectionStore.effectiveSubFilterQL}`
172172
) {
173173
searchDebouncing = false;
174174
}

frontend/src/lib/features/collections/SubFilterBar.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { QLBuilder } from '../../utils/ql.js';
44
import DynamicFieldFilter from '../items/DynamicFieldFilter.svelte';
55
import DynamicFilterPopover from '../shared/DynamicFilterPopover.svelte';
6+
import Toggle from '../../components/Toggle.svelte';
7+
import { t } from '../../stores/i18n.svelte.js';
68
79
let { workspaceId: _workspaceId = null } = $props();
810
@@ -15,6 +17,14 @@
1517
}
1618
</script>
1719
20+
<Toggle
21+
checked={!collectionStore.showCompleted}
22+
onchange={(checked) => collectionStore.setShowCompleted(!checked)}
23+
label={t('milestones.hideCompleted')}
24+
labelPosition="left"
25+
dataTestid="collection-hide-completed"
26+
/>
27+
1828
<DynamicFilterPopover
1929
bind:filters
2030
applied={Boolean(collectionStore.subFilterQL)}

frontend/src/lib/stores/collectionContext.svelte.js

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ class CollectionStore {
120120
// Raw filter rows backing the QL — kept so the SubFilterBar UI can hydrate
121121
// its builder when remounted on a different view of the same collection.
122122
subFilterRows = $state([]);
123+
showCompleted = $state(false);
124+
125+
get effectiveSubFilterQL() {
126+
if (this.showCompleted) return this.subFilterQL;
127+
return this.subFilterQL
128+
? `(${this.subFilterQL}) AND status_completed = false`
129+
: 'status_completed = false';
130+
}
131+
132+
#completionPreferenceKey(wsId, colId, view) {
133+
const scope = colId ? `collection-${colId}` : `workspace-${wsId}`;
134+
return `collection-show-completed:${scope}:${view?.split('-').at(-1)}`;
135+
}
136+
137+
async setShowCompleted(value) {
138+
this.showCompleted = value;
139+
try {
140+
localStorage.setItem(
141+
this.#completionPreferenceKey(this.#wsId, this.#colId, this.#currentView),
142+
String(value)
143+
);
144+
} catch {
145+
// Keep the setting usable when browser storage is unavailable.
146+
}
147+
this.clearBoardSearch();
148+
if (this.#wsId || this.#colId) {
149+
await this.load(this.#wsId, this.#colId, this.#currentView);
150+
}
151+
}
123152

124153
// Server-side sort state
125154
sortableFields = $state([]);
@@ -189,19 +218,32 @@ class CollectionStore {
189218
const sameCollection = wsId === this.#wsId && colId === this.#colId;
190219
const viewChanged = view !== this.#currentView;
191220
const targetInitialLimit = initialItemsPageSize(view);
221+
const previousShowCompleted = this.showCompleted;
222+
if (!sameCollection || viewChanged) {
223+
try {
224+
this.showCompleted =
225+
localStorage.getItem(this.#completionPreferenceKey(wsId, colId, view)) === 'true';
226+
} catch {
227+
this.showCompleted = false;
228+
}
229+
}
192230

193231
// Switching between passive collection views does not need another network
194232
// roundtrip when the already-loaded item page is large enough and there is
195233
// no active server-side sort/filter. Board views may need capped-column
196234
// fetches, so they intentionally keep loading.
197-
const canReuseTargetData = loadsItems(view)
198-
? this.items.length > 0 &&
199-
!this.boardDeferred &&
200-
(this.itemsPagination?.limit ?? 0) >= targetInitialLimit
201-
: this.backlogPagination !== null;
235+
const canReuseTargetData =
236+
loadsItems(view) &&
237+
loadsItems(this.#currentView) &&
238+
this.items.length > 0 &&
239+
!this.boardDeferred &&
240+
(this.itemsPagination?.limit ?? 0) >= targetInitialLimit;
202241
if (
203242
sameCollection &&
204243
viewChanged &&
244+
previousShowCompleted === this.showCompleted &&
245+
!this.loading &&
246+
!BOARD_VIEWS.has(this.#currentView) &&
205247
canReuseTargetData &&
206248
!this.subFilterQL &&
207249
!this.#sortBy &&
@@ -278,7 +320,7 @@ class CollectionStore {
278320
? fetchCollectionBacklog(wsId, colId, {
279321
page: 1,
280322
limit: DEFAULT_PAGE_SIZE,
281-
sub_ql: this.subFilterQL || undefined,
323+
sub_ql: this.effectiveSubFilterQL || undefined,
282324
collection,
283325
})
284326
: Promise.resolve(null),
@@ -352,7 +394,7 @@ class CollectionStore {
352394
* completed statuses are paged separately so they cannot hide active work.
353395
*/
354396
async #resolveBoardPartition(wsId, colId, view) {
355-
if (!BOARD_VIEWS.has(view)) return null;
397+
if (!BOARD_VIEWS.has(view) || !this.showCompleted) return null;
356398
try {
357399
const config = await this.getBoardConfiguration(wsId, colId);
358400
let statuses = this.boardStatuses;
@@ -371,7 +413,7 @@ class CollectionStore {
371413
}
372414

373415
const completedStatusIds = statuses
374-
.filter((status) => status.is_completed || status.category_name === 'Done')
416+
.filter((status) => status.is_completed)
375417
.map((status) => status.id);
376418
const retentionDays = Number(config?.completed_item_retention_days);
377419
const completedActivityDays =
@@ -457,7 +499,7 @@ class CollectionStore {
457499
fetchCollectionItems(wsId, colId, {
458500
page,
459501
limit: pageLimit,
460-
sub_ql: this.subFilterQL || undefined,
502+
sub_ql: this.effectiveSubFilterQL || undefined,
461503
collection,
462504
...this.#itemSortOptions(),
463505
...this.#boardExclusionFilter(boardPartition?.statusIds),
@@ -483,7 +525,7 @@ class CollectionStore {
483525
const result = await fetchCollectionItems(wsId, colId, {
484526
page,
485527
limit: BOARD_UNFINISHED_PAGE_SIZE,
486-
sub_ql: this.subFilterQL || undefined,
528+
sub_ql: this.effectiveSubFilterQL || undefined,
487529
collection,
488530
...this.#itemSortOptions(),
489531
...this.#boardExclusionFilter(boardPartition?.statusIds),
@@ -530,7 +572,7 @@ class CollectionStore {
530572
const result = await fetchCollectionItems(wsId, colId, {
531573
page,
532574
limit: BOARD_UNFINISHED_PAGE_SIZE,
533-
sub_ql: this.subFilterQL || undefined,
575+
sub_ql: this.effectiveSubFilterQL || undefined,
534576
collection,
535577
...this.#itemSortOptions(),
536578
...this.#boardExclusionFilter(boardPartition?.statusIds),
@@ -554,7 +596,7 @@ class CollectionStore {
554596
return fetchCollectionItems(wsId, colId, {
555597
page: 1,
556598
limit,
557-
sub_ql: this.subFilterQL || undefined,
599+
sub_ql: this.effectiveSubFilterQL || undefined,
558600
collection,
559601
status_id: boardPartition.statusIds.join(','),
560602
completed_activity_days: boardPartition.completedActivityDays || undefined,
@@ -666,7 +708,7 @@ class CollectionStore {
666708
page,
667709
limit: BOARD_SEARCH_PAGE_SIZE,
668710
search: query,
669-
sub_ql: this.subFilterQL || undefined,
711+
sub_ql: this.effectiveSubFilterQL || undefined,
670712
collection: this.boardCollection ?? undefined,
671713
...this.#itemSortOptions(),
672714
});
@@ -701,7 +743,7 @@ class CollectionStore {
701743
const result = await fetchCollectionItems(this.#wsId, this.#colId, {
702744
page: nextPage,
703745
limit: pagination?.limit ?? DEFAULT_PAGE_SIZE,
704-
sub_ql: this.subFilterQL || undefined,
746+
sub_ql: this.effectiveSubFilterQL || undefined,
705747
...this.#itemSortOptions(),
706748
...(deferred
707749
? {
@@ -747,7 +789,7 @@ class CollectionStore {
747789
const result = await fetchCollectionBacklog(this.#wsId, this.#colId, {
748790
page: nextPage,
749791
limit: this.backlogPagination?.limit ?? DEFAULT_PAGE_SIZE,
750-
sub_ql: this.subFilterQL || undefined,
792+
sub_ql: this.effectiveSubFilterQL || undefined,
751793
});
752794

753795
if (loadId !== this.#loadId) return;
@@ -778,7 +820,7 @@ class CollectionStore {
778820
const result = await fetchCollectionItems(this.#wsId, this.#colId, {
779821
page,
780822
limit,
781-
sub_ql: this.subFilterQL || undefined,
823+
sub_ql: this.effectiveSubFilterQL || undefined,
782824
...this.#itemSortOptions(),
783825
});
784826

@@ -850,7 +892,7 @@ class CollectionStore {
850892
? fetchCollectionBacklog(this.#wsId, this.#colId, {
851893
page: 1,
852894
limit: backlogLimit,
853-
sub_ql: this.subFilterQL || undefined,
895+
sub_ql: this.effectiveSubFilterQL || undefined,
854896
collection,
855897
})
856898
: Promise.resolve(null),
@@ -978,6 +1020,7 @@ class CollectionStore {
9781020

9791021
applyItem(item) {
9801022
if (!item?.id) return;
1023+
if (this.#hideCompletedItem(item)) return;
9811024
const index = this.items.findIndex((current) => current.id === item.id);
9821025
if (index === -1) {
9831026
this.items = [...this.items, item];
@@ -1004,7 +1047,7 @@ class CollectionStore {
10041047
try {
10051048
const changes = await fetchCollectionItemChanges(this.#wsId, this.#colId, {
10061049
since: this.#changesWatermark,
1007-
sub_ql: this.subFilterQL || undefined,
1050+
sub_ql: this.effectiveSubFilterQL || undefined,
10081051
});
10091052
if (loadId !== this.#loadId) return;
10101053
this.#changesWatermark = changes?.watermark ?? this.#changesWatermark;
@@ -1060,13 +1103,14 @@ class CollectionStore {
10601103
const wsId = this.#wsId;
10611104
const colId = this.#colId;
10621105
const changes = await fetchCollectionItemChanges(wsId, colId, {
1063-
sub_ql: this.subFilterQL || undefined,
1106+
sub_ql: this.effectiveSubFilterQL || undefined,
10641107
});
10651108
if (loadId !== this.#loadId || wsId !== this.#wsId || colId !== this.#colId) return;
10661109
this.#changesWatermark = changes?.watermark ?? 0;
10671110
}
10681111

10691112
#applyUpdatedItem(updated) {
1113+
if (this.#hideCompletedItem(updated)) return;
10701114
const idx = this.items.findIndex((i) => i.id === updated.id);
10711115
if (idx !== -1) Object.assign(this.items[idx], updated);
10721116
const bIdx = this.backlogItems.findIndex((i) => i.id === updated.id);
@@ -1075,6 +1119,16 @@ class CollectionStore {
10751119
if (searchIdx !== -1) Object.assign(this.boardSearchItems[searchIdx], updated);
10761120
}
10771121

1122+
#hideCompletedItem(item) {
1123+
if (this.showCompleted) return false;
1124+
const status =
1125+
workspaceDataStore.statuses.find((candidate) => candidate.id === item.status_id) ??
1126+
this.boardStatuses.find((candidate) => candidate.id === item.status_id);
1127+
if (!status?.is_completed) return false;
1128+
this.#removeItemsById(new Set([item.id]));
1129+
return true;
1130+
}
1131+
10781132
#removeItemsById(ids) {
10791133
const beforeBacklog = this.backlogItems.length;
10801134
const deferredSet = new Set(this.boardDeferred?.statusIds ?? []);
@@ -1096,7 +1150,7 @@ class CollectionStore {
10961150
if (removedItems > 0 && this.itemsPagination) {
10971151
this.itemsPagination = {
10981152
...this.itemsPagination,
1099-
total: Math.max(0, (this.itemsPagination.total ?? 0) - removedItems),
1153+
total_items: Math.max(0, (this.itemsPagination.total_items ?? 0) - removedItems),
11001154
};
11011155
}
11021156
if (removedDeferredItems > 0 && this.boardDeferred) {
@@ -1105,7 +1159,7 @@ class CollectionStore {
11051159
const pagination = this.boardDeferred.pagination
11061160
? {
11071161
...this.boardDeferred.pagination,
1108-
total,
1162+
total_items: total,
11091163
total_pages: Math.ceil(total / limit),
11101164
}
11111165
: null;
@@ -1122,7 +1176,7 @@ class CollectionStore {
11221176
if (removedBacklog > 0 && this.backlogPagination) {
11231177
this.backlogPagination = {
11241178
...this.backlogPagination,
1125-
total: Math.max(0, (this.backlogPagination.total ?? 0) - removedBacklog),
1179+
total_items: Math.max(0, (this.backlogPagination.total_items ?? 0) - removedBacklog),
11261180
};
11271181
this.backlogHasMore = calcHasMore(this.backlogPagination);
11281182
}

0 commit comments

Comments
 (0)