Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions frontend/src/lib/features/collections/CollectionBacklog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,12 @@
const membership = await resolveCreatedItemMembership(newItem, {
workspaceId,
collectionId,
subFilterQL: collectionStore.subFilterQL ? collectionStore.effectiveSubFilterQL : '',
subFilterQL: collectionStore.effectiveBacklogSubFilterQL,
});
if (membership === 'member') {
// insertCreatedItem deduplicates by id so a repeated create event cannot
// append the same backlog row twice.
collectionStore.insertCreatedItem(newItem, { backlog: true });
// Backlog membership also depends on configured statuses, so let the
// authoritative backlog request decide whether the new item belongs.
scheduleCollectionReconciliation();
} else if (membership === 'unknown') {
scheduleCollectionReconciliation();
}
Expand Down Expand Up @@ -439,7 +439,7 @@
const boundaryItem = await items.getBacklogBoundary(
workspaceId,
collectionId,
collectionStore.effectiveSubFilterQL,
collectionStore.effectiveBacklogSubFilterQL,
boundary,
);
if (!boundaryItem || boundaryItem.id === item.id) return;
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/lib/features/collections/collectionFilters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Applies the optional completed-item visibility filter used by collection
* views. Backlogs pass hideCompleted=false because their configured status IDs
* are already the authoritative visibility rule.
*/
export function collectionSubFilterQL(subFilterQL = '', { hideCompleted = true } = {}) {
if (!hideCompleted) return subFilterQL;
return subFilterQL ? `(${subFilterQL}) AND status_completed = false` : 'status_completed = false';
}
29 changes: 20 additions & 9 deletions frontend/src/lib/stores/collectionContext.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { collections } from '../api/collections.js';
import { items as itemsApi } from '../api/items.js';
import { BACKGROUND_READ, FOREGROUND_READ } from '../api/readScheduler.js';
import { normalizeCompletedLimit } from '../features/collections/boardConfigPayload.js';
import { collectionSubFilterQL } from '../features/collections/collectionFilters.js';
import {
fetchCollectionBacklog,
fetchCollectionItemChanges,
Expand Down Expand Up @@ -159,10 +160,11 @@ class CollectionStore {
showCompleted = $state(false);

get effectiveSubFilterQL() {
if (this.showCompleted) return this.subFilterQL;
return this.subFilterQL
? `(${this.subFilterQL}) AND status_completed = false`
: 'status_completed = false';
return collectionSubFilterQL(this.subFilterQL, { hideCompleted: !this.showCompleted });
}

get effectiveBacklogSubFilterQL() {
return collectionSubFilterQL(this.subFilterQL, { hideCompleted: false });
}

#completionPreferenceKey(wsId, colId, view) {
Expand Down Expand Up @@ -506,7 +508,7 @@ class CollectionStore {
? fetchCollectionBacklog(wsId, colId, {
page: 1,
limit: DEFAULT_PAGE_SIZE,
sub_ql: this.effectiveSubFilterQL || undefined,
sub_ql: this.effectiveBacklogSubFilterQL || undefined,
collection,
requestOptions: FOREGROUND_READ,
})
Expand Down Expand Up @@ -1173,7 +1175,7 @@ class CollectionStore {
const result = await fetchCollectionBacklog(this.#wsId, this.#colId, {
page: nextPage,
limit: this.backlogPagination?.limit ?? DEFAULT_PAGE_SIZE,
sub_ql: this.effectiveSubFilterQL || undefined,
sub_ql: this.effectiveBacklogSubFilterQL || undefined,
requestOptions: BACKGROUND_READ,
});

Expand Down Expand Up @@ -1288,7 +1290,7 @@ class CollectionStore {
this.#wsId,
this.#colId,
backlogLimit,
this.effectiveSubFilterQL || undefined,
this.effectiveBacklogSubFilterQL || undefined,
collection,
loadId
)
Expand Down Expand Up @@ -1514,7 +1516,10 @@ class CollectionStore {

const changes = await fetchCollectionItemChanges(this.#wsId, this.#colId, {
since: this.#changesWatermark,
sub_ql: this.effectiveSubFilterQL || undefined,
sub_ql:
(BACKLOG_VIEWS.has(this.#currentView)
? this.effectiveBacklogSubFilterQL
: this.effectiveSubFilterQL) || undefined,
});
if (loadId !== this.#loadId) return false;
const nextWatermark = changes?.watermark ?? this.#changesWatermark;
Expand Down Expand Up @@ -1593,7 +1598,10 @@ class CollectionStore {
const wsId = this.#wsId;
const colId = this.#colId;
const changes = await fetchCollectionItemChanges(wsId, colId, {
sub_ql: this.effectiveSubFilterQL || undefined,
sub_ql:
(BACKLOG_VIEWS.has(this.#currentView)
? this.effectiveBacklogSubFilterQL
: this.effectiveSubFilterQL) || undefined,
});
if (loadId !== this.#loadId || wsId !== this.#wsId || colId !== this.#colId) return;
this.#changesWatermark = changes?.watermark ?? 0;
Expand All @@ -1610,6 +1618,9 @@ class CollectionStore {
}

#hideCompletedItem(item) {
// Backlog membership is defined by the configured status IDs, which may
// intentionally include completed statuses.
if (BACKLOG_VIEWS.has(this.#currentView)) return false;
if (this.showCompleted) return false;
const status =
workspaceDataStore.statuses.find((candidate) => candidate.id === item.status_id) ??
Expand Down
29 changes: 21 additions & 8 deletions internal/repository/item_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -1159,21 +1159,34 @@ func (r *ItemRepository) scanItemList(rows *sql.Rows) ([]models.Item, error) {
}

// GetBacklogStatusIDs returns status IDs for backlog items.
// It first checks board_configurations for the workspace, then falls back to non-completed statuses.
func (r *ItemRepository) GetBacklogStatusIDs(workspaceID int) ([]int, error) {
return r.GetBacklogStatusIDsContext(context.Background(), workspaceID)
// It first checks board_configurations for the collection or workspace, then
// falls back to the global non-completed statuses.
func (r *ItemRepository) GetBacklogStatusIDs(workspaceID, collectionID int) ([]int, error) {
return r.GetBacklogStatusIDsContext(context.Background(), workspaceID, collectionID)
}

// GetBacklogStatusIDsContext is the request-aware form of GetBacklogStatusIDs.
func (r *ItemRepository) GetBacklogStatusIDsContext(ctx context.Context, workspaceID int) ([]int, error) {
// First, check if there's a board configuration with backlog_status_ids
if workspaceID > 0 {
// A collection-scoped board configuration owns that collection's backlog
// statuses; a request without a collection uses the workspace default. When
// neither is configured, the global non-completed, non-default statuses apply.
func (r *ItemRepository) GetBacklogStatusIDsContext(ctx context.Context, workspaceID, collectionID int) ([]int, error) {
scopeColumn, scopeID := "", 0
if collectionID > 0 {
scopeColumn, scopeID = "collection_id", collectionID
} else if workspaceID > 0 {
scopeColumn, scopeID = "workspace_id", workspaceID
}
if scopeID > 0 {
var backlogStatusIDsJSON sql.NullString
err := r.db.QueryRowContext(ctx, `
SELECT backlog_status_ids
FROM board_configurations
WHERE workspace_id = ?`, workspaceID).Scan(&backlogStatusIDsJSON)

WHERE `+scopeColumn+` = ?
ORDER BY id
LIMIT 1`, scopeID).Scan(&backlogStatusIDsJSON)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("failed to load backlog configuration: %w", err)
}
if err == nil && backlogStatusIDsJSON.Valid && backlogStatusIDsJSON.String != "" {
var statusIDs []int
if err := json.Unmarshal([]byte(backlogStatusIDsJSON.String), &statusIDs); err != nil {
Expand Down
5 changes: 3 additions & 2 deletions internal/services/item_crud_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,8 @@ type BacklogParams struct {
OmitDescriptions bool
}

// GetBacklogItems retrieves items with non-completed statuses for a workspace/collection
// GetBacklogItems retrieves items whose status is configured for the backlog
// of the requested collection or workspace.
func (s *ItemCRUDService) GetBacklogItems(params BacklogParams) ([]models.Item, int, error) {
return s.GetBacklogItemsContext(context.Background(), params)
}
Expand All @@ -632,7 +633,7 @@ func (s *ItemCRUDService) GetBacklogItemsContext(ctx context.Context, params Bac
}

// Resolve backlog status IDs
backlogStatusIDs, err := s.repo.GetBacklogStatusIDsContext(ctx, params.WorkspaceID)
backlogStatusIDs, err := s.repo.GetBacklogStatusIDsContext(ctx, params.WorkspaceID, params.CollectionID)
if err != nil {
return nil, 0, fmt.Errorf("failed to get backlog statuses: %w", err)
}
Expand Down
Loading