feat: make sidebar resizable (#1965)

Closes: #525

* Sidebar width is now adjustable via dragging
* The width is automatically saved to settings and restored across sessions.
This commit is contained in:
kolaente
2025-12-12 11:46:46 +01:00
committed by GitHub
parent 803effbb8f
commit 1288d0a99c
11 changed files with 357 additions and 17 deletions

View File

@@ -0,0 +1,123 @@
import {test, expect} from '../../support/fixtures'
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {BucketFactory} from '../../factories/bucket'
import {createDefaultViews} from '../project/prepareProjects'
async function seedTasks(userId: number, numberOfTasks = 5, startDueDate = new Date()) {
const project = (await ProjectFactory.create(1, {owner_id: userId}))[0]
const views = await createDefaultViews(project.id)
await BucketFactory.create(1, {
project_view_id: views[3].id,
})
const tasks = []
let dueDate = startDueDate
for (let i = 0; i < numberOfTasks; i++) {
const now = new Date()
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
tasks.push({
id: i + 1,
project_id: project.id,
done: false,
created_by_id: userId,
title: 'Test Task ' + i,
index: i + 1,
due_date: dueDate.toISOString(),
created: now.toISOString(),
updated: now.toISOString(),
})
}
await TaskFactory.seed(TaskFactory.table, tasks)
return {tasks, project}
}
test.describe('Sidebar Resize', () => {
test('should not reload tasks when resizing the sidebar', async ({authenticatedPage: page, currentUser}) => {
await page.setViewportSize({width: 1280, height: 720})
await seedTasks(currentUser.id, 5)
await page.goto('/')
await page.waitForLoadState('networkidle')
// Wait for showTasks to appear with longer timeout for CI
await expect(page.locator('[data-cy="showTasks"]')).toBeAttached({timeout: 30000})
await expect(page.locator('[data-cy="showTasks"] .card .task').first()).toBeVisible({timeout: 10000})
let taskApiCalls = 0
page.on('request', request => {
if (request.url().includes('/tasks') && request.method() === 'GET') {
taskApiCalls++
}
})
await page.waitForTimeout(500)
taskApiCalls = 0
const resizeHandle = page.locator('.resize-handle')
await expect(resizeHandle).toBeAttached()
const sidebar = page.locator('.menu-container')
const sidebarBox = await sidebar.boundingBox()
expect(sidebarBox).not.toBeNull()
const startX = sidebarBox!.x + sidebarBox!.width - 2
const startY = sidebarBox!.y + sidebarBox!.height / 2
await page.mouse.move(startX, startY)
await page.mouse.down()
await page.mouse.move(startX + 100, startY, {steps: 10})
await page.mouse.up()
await page.waitForTimeout(1500)
expect(taskApiCalls).toBe(0)
})
test('should persist sidebar width after resize', async ({authenticatedPage: page, currentUser}) => {
await page.setViewportSize({width: 1280, height: 720})
await seedTasks(currentUser.id, 1)
await page.goto('/')
await page.waitForLoadState('networkidle')
const sidebar = page.locator('.menu-container')
const initialBox = await sidebar.boundingBox()
expect(initialBox).not.toBeNull()
const initialWidth = initialBox!.width
const resizeHandle = page.locator('.resize-handle')
await expect(resizeHandle).toBeAttached()
const startX = initialBox!.x + initialBox!.width - 2
const startY = initialBox!.y + initialBox!.height / 2
await page.mouse.move(startX, startY)
await page.mouse.down()
await page.mouse.move(startX + 50, startY, {steps: 10})
await page.mouse.up()
await page.waitForTimeout(1000)
const newBox = await sidebar.boundingBox()
expect(newBox).not.toBeNull()
expect(newBox!.width).toBeGreaterThan(initialWidth)
await page.reload()
await page.waitForLoadState('networkidle')
const reloadedBox = await sidebar.boundingBox()
expect(reloadedBox).not.toBeNull()
expect(Math.abs(reloadedBox!.width - newBox!.width)).toBeLessThan(5)
})
test('should not show resize handle on mobile', async ({authenticatedPage: page, currentUser}) => {
await seedTasks(currentUser.id, 1)
await page.setViewportSize({width: 375, height: 667})
await page.goto('/')
await page.waitForLoadState('networkidle')
const resizeHandle = page.locator('.resize-handle')
await expect(resizeHandle).not.toBeAttached()
})
})

View File

@@ -26,11 +26,17 @@ export async function login(page: Page | null, apiContext: APIRequestContext, us
const body = await response.json()
const token = body.token
// Set token in localStorage before navigating (only if page is provided)
// Set token and API_URL before navigating (only if page is provided)
if (page) {
await page.addInitScript((token) => {
// Use 127.0.0.1 instead of localhost to match the frontend's origin for CORS
const apiUrl = process.env.API_URL || 'http://127.0.0.1:3456/api/v1'
await page.addInitScript(({token, apiUrl}) => {
// Set both localStorage AND window.API_URL
// The app uses window.API_URL for initialization (in base.ts loadApp)
window.localStorage.setItem('token', token)
}, token)
window.localStorage.setItem('API_URL', apiUrl)
window.API_URL = apiUrl
}, {token, apiUrl})
}
return {user, token}