feat(kanban): add setting to always show bucket task count (#1966)

Added "Always show task count on Kanban buckets" setting in user preferences to control the visibility of task counts on Kanban bucket headers
This commit is contained in:
kolaente
2025-12-12 00:27:13 +01:00
committed by GitHub
parent 49c43e87f0
commit 8b6082e8c7
9 changed files with 114 additions and 16 deletions

View File

@@ -5,6 +5,7 @@ import {TaskFactory} from '../../factories/task'
import {ProjectViewFactory} from '../../factories/project_view'
import {TaskBucketFactory} from '../../factories/task_buckets'
import {createTasksWithPriorities, createTasksWithSearch} from '../../support/filterTestHelpers'
import {updateUserSettings} from '../../support/updateUserSettings'
async function createSingleTaskInBucket(count = 1, attrs = {}) {
const projects = await ProjectFactory.create(1)
@@ -312,4 +313,60 @@ test.describe('Project View Kanban', () => {
// Verify only one task is shown (the search result) - count task headings
await expect(page.locator('main h2')).toHaveCount(1)
})
test('Should not show task count by default when bucket has no limit', async ({authenticatedPage: page}) => {
await createTaskWithBuckets(buckets, 5)
await page.goto('/projects/1/4')
// Wait for buckets to load
await expect(page.locator('.kanban .bucket .title').filter({hasText: buckets[0].title})).toBeVisible()
// Verify the task count span is not visible when no limit is set
await expect(page.locator('.kanban .bucket .bucket-header span.limit').first()).not.toBeVisible()
})
test('Should show task count when alwaysShowBucketTaskCount setting is enabled', async ({authenticatedPage: page, apiContext, userToken}) => {
await createTaskWithBuckets(buckets, 5)
// Enable the alwaysShowBucketTaskCount setting
await updateUserSettings(apiContext, userToken, {
frontendSettings: {
alwaysShowBucketTaskCount: true,
},
})
await page.goto('/projects/1/4')
// Wait for buckets to load
await expect(page.locator('.kanban .bucket .title').filter({hasText: buckets[0].title})).toBeVisible()
// Verify the task count is shown (without limit, just the count)
const limitSpan = page.locator('.kanban .bucket .bucket-header span.limit').first()
await expect(limitSpan).toBeVisible()
// Should show just the count (5) without a limit
await expect(limitSpan).toContainText('5')
// Should not contain a slash (no limit set)
await expect(limitSpan).not.toContainText('/')
})
test('Should show count/limit format when bucket has a limit set', async ({authenticatedPage: page}) => {
// Create tasks in the bucket
await createTaskWithBuckets(buckets, 2)
await page.goto('/projects/1/4')
// Set a bucket limit
const bucketDropdown = page.locator('.kanban .bucket .bucket-header .dropdown.options').first()
await bucketDropdown.locator('.dropdown-trigger').click()
await bucketDropdown.locator('.dropdown-menu .dropdown-item').filter({hasText: 'Limit: Not Set'}).click()
await bucketDropdown.locator('.dropdown-menu .field input.input').fill('10')
await bucketDropdown.locator('.dropdown-menu .field .control .button').click()
// Wait for the limit to be saved
await expect(page.locator('.global-notification')).toContainText('Success')
// Verify the count/limit format is shown (2/10)
const limitSpan = page.locator('.kanban .bucket .bucket-header span.limit').first()
await expect(limitSpan).toBeVisible()
await expect(limitSpan).toContainText('2/10')
})
})

View File

@@ -4,8 +4,9 @@ import {TEST_PASSWORD} from './constants'
/**
* This authenticates a user and puts the token in local storage which allows us to perform authenticated requests.
* Returns the user and token for use in tests that need to make authenticated API calls.
*/
export async function login(page: Page, apiContext: APIRequestContext, user?: any) {
export async function login(page: Page | null, apiContext: APIRequestContext, user?: any) {
if (!user) {
throw new Error('Needs user')
}
@@ -25,12 +26,14 @@ export async function login(page: Page, apiContext: APIRequestContext, user?: an
const body = await response.json()
const token = body.token
// Set token in localStorage before navigating
await page.addInitScript((token) => {
window.localStorage.setItem('token', token)
}, token)
// Set token in localStorage before navigating (only if page is provided)
if (page) {
await page.addInitScript((token) => {
window.localStorage.setItem('token', token)
}, token)
}
return user
return {user, token}
}
export async function createFakeUser() {

View File

@@ -6,13 +6,14 @@ export const test = base.extend<{
apiContext: APIRequestContext;
authenticatedPage: Page;
currentUser: any;
userToken: string;
}>({
apiContext: async ({playwright}, use) => {
const baseURL = process.env.API_URL || 'http://localhost:3456/api/v1/'
const apiContext = await playwright.request.newContext({
baseURL,
})
Factory.setRequestContext(apiContext)
await use(apiContext)
await apiContext.dispose()
@@ -23,8 +24,13 @@ export const test = base.extend<{
await use(user)
},
userToken: async ({apiContext, currentUser}, use) => {
const {token} = await login(null, apiContext, currentUser)
await use(token)
},
authenticatedPage: async ({page, apiContext, currentUser}, use) => {
await login(page, apiContext, currentUser)
const {token} = await login(page, apiContext, currentUser)
await use(page)
},
})

View File

@@ -1,4 +1,5 @@
import type {APIRequestContext} from '@playwright/test'
import {objectToSnakeCase} from '../../src/helpers/case'
export async function updateUserSettings(apiContext: APIRequestContext, token: string, settings: any) {
const apiUrl = process.env.API_URL || 'http://localhost:3456/api/v1'
@@ -9,15 +10,30 @@ export async function updateUserSettings(apiContext: APIRequestContext, token: s
},
})
const oldSettings = await userResponse.json()
const userData = await userResponse.json()
// GET /user returns { settings: { frontend_settings: ... }, ... }
// POST /user/settings/general expects { frontend_settings: ... } at the top level
const oldSettings = userData.settings || {}
const snakeSettings = objectToSnakeCase(settings)
// Deep merge frontend_settings if provided
const mergedSettings = {
...oldSettings,
...snakeSettings,
}
if (snakeSettings.frontend_settings) {
mergedSettings.frontend_settings = {
...(oldSettings.frontend_settings || {}),
...snakeSettings.frontend_settings,
}
}
await apiContext.post(`${apiUrl}/user/settings/general`, {
headers: {
'Authorization': `Bearer ${token}`,
},
data: {
...oldSettings,
...settings,
},
data: mergedSettings,
})
}