mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
fix(projects): move only the dragged card on project boards (#40182)
The board reported every card of the target column on each drag, and the server wrote all of them into that column. A stale board in another browser therefore pulled cards moved by someone else back to their old column. The client now sends only the dragged issue and its predecessor, and the server computes the position in one transaction: - Only the dragged issue changes column, the others are just renumbered. - A predecessor that is no longer in the column puts the card at the end. Added an e2e test. Fixes https://github.com/go-gitea/gitea/issues/21243 Fixes https://github.com/go-gitea/gitea/issues/32070 Fixes https://github.com/go-gitea/gitea/issues/13498 --- Assisted-by: Codet:claude --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProject, createProjectColumn, randomString} from './utils.ts';
|
||||
import {
|
||||
login, apiCreateRepo, apiCreateIssue, apiCreateProject, apiDeleteRepo, createProject, createProjectColumn, randomString,
|
||||
} from './utils.ts';
|
||||
import type {Page} from '@playwright/test';
|
||||
|
||||
const projectCards = (page: Page, column: string) => page.locator('.project-column', {hasText: column}).locator('.cards');
|
||||
|
||||
async function dragProjectCardToTop(page: Page, title: string, column: string) {
|
||||
const card = page.locator('.issue-card', {hasText: title});
|
||||
const cards = projectCards(page, column);
|
||||
const moved = page.waitForResponse((resp) => resp.url().endsWith('/move'));
|
||||
await card.hover();
|
||||
await page.mouse.down();
|
||||
await card.hover({position: {x: 10, y: 10}});
|
||||
await expect(card).toHaveClass(/card-ghost/); // sortable ignores dragover until its deferred drag start ran
|
||||
await cards.hover({position: {x: 10, y: 10}});
|
||||
await expect(cards.locator('.issue-card').first()).toContainText(title);
|
||||
await page.mouse.up();
|
||||
await moved;
|
||||
}
|
||||
|
||||
test('assign issue to project and change column', async ({page}) => {
|
||||
const repoName = `e2e-issue-project-${randomString(8)}`;
|
||||
@@ -441,3 +460,25 @@ test('select projects on new issue page shows in sidebar', async ({page}) => {
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('project board drag keeps a card moved in another tab', async ({page}) => {
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
const repo = `e2e-project-board-${randomString(8)}`;
|
||||
const [otherPage] = await Promise.all([
|
||||
page.context().newPage(), login(page), apiCreateRepo(page.request, {name: repo, autoInit: false}),
|
||||
]);
|
||||
try {
|
||||
const projectID = await apiCreateProject(page.request, owner, repo, 'Board', {templateType: 'basic_kanban'});
|
||||
for (const title of ['first', 'second']) await apiCreateIssue(page.request, {owner, repo, title, projects: [projectID]});
|
||||
await Promise.all([page, otherPage].map((board) => board.goto(`/${owner}/${repo}/projects/${projectID}`)));
|
||||
|
||||
await dragProjectCardToTop(otherPage, 'first', 'To Do');
|
||||
await dragProjectCardToTop(page, 'second', 'Backlog');
|
||||
|
||||
await page.reload();
|
||||
await expect(projectCards(page, 'Backlog').locator('.issue-card')).toHaveText([/second/]);
|
||||
await expect(projectCards(page, 'To Do').locator('.issue-card')).toHaveText([/first/]);
|
||||
} finally {
|
||||
await apiDeleteRepo(page.request, owner, repo);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -130,10 +130,10 @@ export async function createProjectColumn(requestContext: APIRequestContext, own
|
||||
}), 'createProjectColumn');
|
||||
}
|
||||
|
||||
export async function apiCreateProject(requestContext: APIRequestContext, owner: string, repo: string, title: string): Promise<number> {
|
||||
export async function apiCreateProject(requestContext: APIRequestContext, owner: string, repo: string, title: string, {templateType}: {templateType?: string} = {}): Promise<number> {
|
||||
const response = await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/projects`, {
|
||||
headers: apiHeaders(),
|
||||
data: {title},
|
||||
data: {title, template_type: templateType},
|
||||
}), 'apiCreateProject');
|
||||
return (await response.json()).id;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestProjectWorkflowsWeb(t *testing.T) {
|
||||
assert.Contains(t, NewHTMLParser(t, resp.Body).Find(".timeline-item.event .comment-text-line").Text(), "Project workflow Item added to project")
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", link+"/3/move", map[string]any{
|
||||
"issues": []map[string]any{{"issueID": created.ID, "sorting": 0}},
|
||||
"issueID": created.ID, "prevIssueID": 0,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user