mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-04-30 16:28:23 -05:00
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:
@@ -64,11 +64,11 @@
|
||||
{{ bucket.title }}
|
||||
</h2>
|
||||
<span
|
||||
v-if="bucket.limit > 0"
|
||||
:class="{'is-max': bucket.count >= bucket.limit}"
|
||||
v-if="bucket.limit > 0 || alwaysShowBucketTaskCount"
|
||||
:class="{'is-max': bucket.limit > 0 && bucket.count >= bucket.limit}"
|
||||
class="limit"
|
||||
>
|
||||
{{ bucket.count }}/{{ bucket.limit }}
|
||||
{{ bucket.limit > 0 ? `${bucket.count}/${bucket.limit}` : bucket.count }}
|
||||
</span>
|
||||
<Dropdown
|
||||
v-if="canWrite && !collapsedBuckets[bucket.id]"
|
||||
@@ -295,6 +295,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
import FilterPopup from '@/components/project/partials/FilterPopup.vue'
|
||||
@@ -348,6 +349,9 @@ const baseStore = useBaseStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const taskStore = useTaskStore()
|
||||
const projectStore = useProjectStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const alwaysShowBucketTaskCount = computed(() => authStore.settings.frontendSettings.alwaysShowBucketTaskCount)
|
||||
const {handleTaskDropToProject} = useTaskDragToProject()
|
||||
const taskPositionService = ref(new TaskPositionService())
|
||||
const taskBucketService = ref(new TaskBucketService())
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
|
||||
"playSoundWhenDone": "Play a sound when marking tasks as done",
|
||||
"allowIconChanges": "Show special logos during certain times",
|
||||
"alwaysShowBucketTaskCount": "Always show task count on Kanban buckets",
|
||||
"defaultTaskRelationType": "Default task relation type",
|
||||
"weekStart": "Week starts on",
|
||||
"weekStartSunday": "Sunday",
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface IFrontendSettings {
|
||||
timeFormat: TimeFormat
|
||||
defaultTaskRelationType: IRelationKind
|
||||
backgroundBrightness: number | null
|
||||
alwaysShowBucketTaskCount: boolean
|
||||
}
|
||||
|
||||
export interface IExtraSettingsLink {
|
||||
|
||||
@@ -30,6 +30,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
|
||||
dateDisplay: DATE_DISPLAY.RELATIVE,
|
||||
timeFormat: TIME_FORMAT.HOURS_24,
|
||||
defaultTaskRelationType: RELATION_KIND.RELATED,
|
||||
alwaysShowBucketTaskCount: false,
|
||||
}
|
||||
extraSettingsLinks = {}
|
||||
|
||||
|
||||
@@ -314,6 +314,15 @@
|
||||
{{ $t('user.settings.general.allowIconChanges') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input
|
||||
v-model="settings.frontendSettings.alwaysShowBucketTaskCount"
|
||||
type="checkbox"
|
||||
>
|
||||
{{ $t('user.settings.general.alwaysShowBucketTaskCount') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="two-col">
|
||||
<span>
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
// 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() {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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/'
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user