diff --git a/frontend/src/lib/features/collections/BacklogFieldSelector.svelte b/frontend/src/lib/features/collections/BacklogFieldSelector.svelte
new file mode 100644
index 000000000..363f1c86b
--- /dev/null
+++ b/frontend/src/lib/features/collections/BacklogFieldSelector.svelte
@@ -0,0 +1,121 @@
+
+
+
+
+{#if $open}
+
+
+
+ {t('fields.configureFields')}
+
+
+
+
+
+ {#each BACKLOG_FIELD_OPTIONS as field (field.id)}
+
+ setField(field.id, checked)}
+ dataTestid={`backlog-field-${field.id}`}
+ />
+
+ {/each}
+
+
+
+
+
+
+
+{/if}
diff --git a/frontend/src/lib/features/collections/BacklogIterationSection.svelte b/frontend/src/lib/features/collections/BacklogIterationSection.svelte
index be8c1874f..24c0548bd 100644
--- a/frontend/src/lib/features/collections/BacklogIterationSection.svelte
+++ b/frontend/src/lib/features/collections/BacklogIterationSection.svelte
@@ -28,6 +28,7 @@
sectionHighlight = false,
assignableIterations = [],
pendingActionItemIds = new Set(),
+ visibleFields = [],
onToggleCollapse,
onOpenItem,
onMoveItemToBoundary = null,
@@ -61,6 +62,7 @@
let canStart = $derived(iteration && !iteration.is_global && iteration.status === 'planned');
let canComplete = $derived(iteration && !iteration.is_global && iteration.status === 'active');
let sectionId = $derived(iteration ? iteration.id : 'unassigned');
+ let visibleFieldSet = $derived(new Set(visibleFields));
let headerClass = $derived(
`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors select-none iteration-header` +
@@ -201,8 +203,13 @@
{statuses}
{statusCategories}
onclick={(e) => onOpenItem?.(item.id, e)}
- showStatus={true}
- showStoryPoints={storyPointsConfiguredForItem?.(item) ?? false}
+ showStatus={visibleFieldSet.has('status')}
+ showPriority={visibleFieldSet.has('priority')}
+ showParent={visibleFieldSet.has('parent')}
+ showAssignee={visibleFieldSet.has('assignee')}
+ showDueDate={visibleFieldSet.has('due_date')}
+ showMilestones={visibleFieldSet.has('milestone')}
+ showStoryPoints={visibleFieldSet.has('story_points') && (storyPointsConfiguredForItem?.(item) ?? false)}
storyPointsSaving={storyPointsPendingItemIds.has(item.id)}
onStoryPointsChange={(value) => onUpdateStoryPoints?.(item, value)}
>
diff --git a/frontend/src/lib/features/collections/CollectionBacklog.svelte b/frontend/src/lib/features/collections/CollectionBacklog.svelte
index 6697c467d..c86428837 100644
--- a/frontend/src/lib/features/collections/CollectionBacklog.svelte
+++ b/frontend/src/lib/features/collections/CollectionBacklog.svelte
@@ -35,6 +35,8 @@
import CompleteIterationDialog from '../../dialogs/CompleteIterationDialog.svelte';
import { formatItemCount } from '../../utils/itemCount.js';
import { isSystemFieldAvailableInEffectiveConfig } from '../../utils/screenFields.js';
+ import BacklogFieldSelector from './BacklogFieldSelector.svelte';
+ import { DEFAULT_BACKLOG_FIELDS, normalizeBacklogFields } from './backlogFields.js';
let { workspaceId, collectionId = null } = $props();
@@ -64,6 +66,7 @@
let sectionDropHighlight = $state(new Map()); // iterationId|'unassigned' -> boolean
let pendingActionItemIds = $state(new Set());
let pendingStoryPointsItemIds = $state(new Set());
+ let visibleFields = $state([...DEFAULT_BACKLOG_FIELDS]);
// --- Complete Iteration dialog state ---
let completeIterationShow = $state(false);
@@ -116,6 +119,14 @@
if (typeof storedShowCompleted === 'boolean') {
showCompletedIterations = storedShowCompleted;
}
+
+ const storedVisibleFields = collectionPreferences.viewStateValue(
+ workspaceId,
+ collectionId,
+ 'backlog',
+ 'visible_fields'
+ );
+ visibleFields = normalizeBacklogFields(storedVisibleFields);
}
function setShowCompletedIterations(value) {
@@ -125,6 +136,13 @@
});
}
+ function setVisibleFields(fields) {
+ visibleFields = normalizeBacklogFields(fields);
+ collectionPreferences.patchViewState(workspaceId, collectionId, 'backlog', {
+ visible_fields: visibleFields,
+ });
+ }
+
function persistGlobalIds() {
collectionPreferences.patchViewState(workspaceId, collectionId, 'backlog', {
global_iteration_ids: [...addedGlobalIds],
@@ -318,11 +336,10 @@
} catch (error) {
console.error('Failed to load iterations:', error);
}
-
- restorePersistedState();
} else {
await workspaceDataStore.initializeGlobal();
}
+ restorePersistedState();
loading = false;
});
@@ -703,6 +720,7 @@
+
field.id));
+
+export function normalizeBacklogFields(fields) {
+ if (!Array.isArray(fields)) return [...DEFAULT_BACKLOG_FIELDS];
+ return [...new Set(fields.filter((field) => backlogFieldIds.has(field)))];
+}
diff --git a/frontend/src/lib/features/collections/backlogFields.test.mjs b/frontend/src/lib/features/collections/backlogFields.test.mjs
new file mode 100644
index 000000000..c109bd03d
--- /dev/null
+++ b/frontend/src/lib/features/collections/backlogFields.test.mjs
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+import { DEFAULT_BACKLOG_FIELDS, normalizeBacklogFields } from './backlogFields.js';
+
+describe('backlog field configuration', () => {
+ it('uses the current backlog metadata as the default', () => {
+ expect(normalizeBacklogFields(undefined)).toEqual(DEFAULT_BACKLOG_FIELDS);
+ });
+
+ it('allows every field to be hidden', () => {
+ expect(normalizeBacklogFields([])).toEqual([]);
+ });
+
+ it('drops unknown and duplicate stored fields', () => {
+ expect(normalizeBacklogFields(['parent', 'unknown', 'parent', 'assignee'])).toEqual([
+ 'parent',
+ 'assignee',
+ ]);
+ });
+});
diff --git a/frontend/src/lib/features/items/WorkItemRow.svelte b/frontend/src/lib/features/items/WorkItemRow.svelte
index 3ee93b6fe..58151479a 100644
--- a/frontend/src/lib/features/items/WorkItemRow.svelte
+++ b/frontend/src/lib/features/items/WorkItemRow.svelte
@@ -1,12 +1,16 @@