From 7038777f5e57ccfb864be97034b17239cd3ba6a8 Mon Sep 17 00:00:00 2001 From: Daniel Raab Date: Mon, 12 Oct 2026 09:14:38 +0200 Subject: [PATCH] feat(collections): implement shared list-column configuration for list and tree views --- .../collections/CollectionList.svelte | 88 +---- .../collections/CollectionTree.svelte | 312 +++++++++--------- .../collections/listColumnConfig.svelte.js | 97 ++++++ 3 files changed, 270 insertions(+), 227 deletions(-) create mode 100644 frontend/src/lib/features/collections/listColumnConfig.svelte.js diff --git a/frontend/src/lib/features/collections/CollectionList.svelte b/frontend/src/lib/features/collections/CollectionList.svelte index 7232c0b49..4324cb232 100644 --- a/frontend/src/lib/features/collections/CollectionList.svelte +++ b/frontend/src/lib/features/collections/CollectionList.svelte @@ -1,23 +1,18 @@ {#snippet paginationControls()} @@ -422,8 +479,19 @@ -
- +
+
+ +
+
+ columnConfig.save(columns)} + /> +
@@ -480,15 +548,14 @@
-
+
- + +
+ {#each listColumns as column (column.field_identifier)} +
{getListColumnLabel(column, customFieldDefinitions)}
+ {/each}
-
{t('collectionTree.itemKey')}
-
{t('common.summary')}
-
{t('common.status')}
-
{t('common.priority')}
-
{t('common.created')}
@@ -497,60 +564,51 @@ {#if item.isTestCase}
-
-
- - +
- -
- handleTestCaseClick(e, item.id, item.testCaseData?.workspaceId)} - class="text-xs font-mono px-1.5 py-0.5 rounded cursor-pointer transition-colors text-ds-text-accent-green bg-ds-accent-green-subtle hover:opacity-80" - > - TC-{item.id} - -
- - -
- handleTestCaseClick(e, item.id, item.testCaseData?.workspaceId)} - class="text-left w-full text-sm transition-colors truncate cursor-pointer text-ds-text-accent-green hover:opacity-80" - > - {item.title} - -
- - -
{t('testing.testCase')}
-
-
+ {#each listColumns as column (column.field_identifier)} +
+ {#if column.field_identifier === 'key'} + handleTestCaseClick(e, item.id, item.testCaseData?.workspaceId)} + class="text-xs font-mono px-1.5 py-0.5 rounded cursor-pointer transition-colors text-ds-text-accent-green bg-ds-accent-green-subtle hover:opacity-80" + > + TC-{item.id} + + {:else if column.field_identifier === 'title'} + handleTestCaseClick(e, item.id, item.testCaseData?.workspaceId)} + class="block text-left w-full text-sm transition-colors truncate cursor-pointer text-ds-text-accent-green hover:opacity-80" + > + {item.title} + + {:else if column.field_identifier === 'status'} + {t('testing.testCase')} + {/if} +
+ {/each} +
{:else} - {@const typeInfo = getItemTypeInfo(item)} - {@const selectedStatus = statuses.find(s => s.id === item.status_id)} - {@const statusCategory = selectedStatus ? statusCategories.find(sc => sc.id === selectedStatus.category_id) : null} - {@const selectedPriority = priorities.find(p => p.id === item.priority_id)} {@const testCaseCount = (showTestCases && itemTestCaseLinksStore.get(item.id)) ? itemTestCaseLinksStore.get(item.id).length : 0}
- -
- + +
{#if item.hasChildren}
{/if} - - -
- - -
- - - {#if showTestCases && testCaseCount > 0} @@ -595,46 +633,29 @@ {/if}
- -
- - {item.title} - -
- - -
- -
- - -
- {#if selectedPriority} -
- - - {selectedPriority.name} - -
- {:else} - - - {/if} -
- - -
- {formatDate(item.created_at) || '-'} -
+ {#each listColumns as column (column.field_identifier)} +
+ +
+ {/each} +
{/if} {/each} @@ -674,7 +695,4 @@ background-color: var(--ds-background-neutral-hovered); } - :global(.summary-link:hover) { - color: var(--ds-link) !important; - } diff --git a/frontend/src/lib/features/collections/listColumnConfig.svelte.js b/frontend/src/lib/features/collections/listColumnConfig.svelte.js new file mode 100644 index 000000000..ccbe875f4 --- /dev/null +++ b/frontend/src/lib/features/collections/listColumnConfig.svelte.js @@ -0,0 +1,97 @@ +import { untrack } from 'svelte'; +import { api } from '../../api.js'; +import { collectionStore } from '../../stores/collectionContext.js'; +import { t } from '../../stores/i18n.svelte.js'; +import { authStore } from '../../stores/index.js'; +import { errorToast } from '../../stores/toasts.svelte.js'; +import { workspacePermissions } from '../../stores/workspacePermissions.svelte.js'; +import { + buildListColumnConfiguration, + DEFAULT_LIST_COLUMNS, + listColumnsFromConfig, +} from '../../utils/workItemListColumns.js'; +import { canConfigureListColumns } from './collectionService.js'; + +/** + * Shared list-column configuration for list-shaped collection views (list, + * tree). Columns live on the board configuration of the viewed collection or + * workspace, so every view of the same scope shows the same fields. + * + * @param {() => { workspaceId: any, collectionId: any }} getScope + */ +export function useListColumnConfig(getScope) { + let boardConfig = $state(null); + let columns = $state([]); + let loadToken = 0; + + // Collection columns follow collection ownership; workspace defaults follow + // workspace administration. Mirrors the backend board-write authorization. + const canConfigure = $derived.by(() => { + const { workspaceId, collectionId } = getScope(); + return canConfigureListColumns({ + collectionId, + collectionOwnerId: collectionStore.boardCollection?.created_by ?? null, + currentUserId: authStore.currentUser?.id ?? null, + canAdminWorkspace: workspacePermissions.canAdminWorkspace(workspaceId), + }); + }); + + // Board config depends only on the viewed collection/workspace, not on the + // item set or load state — fetch it when the view changes, not every time + // the collection reloads (which toggles collectionStore.loading). + $effect(() => { + const { workspaceId, collectionId } = getScope(); + if (collectionId || workspaceId) { + untrack(() => load(workspaceId, collectionId)); + } + }); + + async function load(workspaceId, collectionId) { + const token = ++loadToken; + try { + const config = await collectionStore.getBoardConfiguration(workspaceId, collectionId); + if (token !== loadToken) return; + boardConfig = config; + columns = listColumnsFromConfig(config); + } catch { + if (token !== loadToken) return; + boardConfig = null; + columns = [...DEFAULT_LIST_COLUMNS]; + } + } + + async function save(newColumns) { + const { workspaceId, collectionId } = getScope(); + try { + const configData = buildListColumnConfiguration(boardConfig, newColumns); + const saved = boardConfig?.id + ? await api.collections.updateBoardConfiguration( + collectionId, + boardConfig.id, + configData, + workspaceId + ) + : // Pass raw collectionId so the API can detect workspace-level config + await api.collections.createBoardConfiguration(collectionId, workspaceId, configData); + loadToken++; + boardConfig = saved; + columns = listColumnsFromConfig(saved); + // Keep the store's cached config current so switching views does not + // show the columns from before this save. + collectionStore.boardConfiguration = saved; + } catch (error) { + console.error('Failed to save board configuration:', error); + errorToast(t('dialogs.alerts.failedToSave', { error: error.message || error })); + } + } + + return { + get columns() { + return columns; + }, + get canConfigure() { + return canConfigure; + }, + save, + }; +}