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 @@