Skip to content

Commit 3e1b453

Browse files
authored
Merge pull request #43 from singyichen/fix/task-dataset-multi-file-upload
feat(task-management): redesign member management add-member UI
2 parents 34f3e24 + d04cad0 commit 3e1b453

16 files changed

Lines changed: 2470 additions & 1238 deletions

design/prototype/pages/task-management/task-detail.html

Lines changed: 1522 additions & 631 deletions
Large diffs are not rendered by default.

design/prototype/pages/task-management/task-detail.panels/member-management.html

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,53 @@ <h2 class="panel-title" id="memberListTitle">目前成員清單</h2>
2222
</div>
2323
</section>
2424

25-
<section class="panel" aria-labelledby="candidateListTitle">
26-
<h2 class="panel-title" id="candidateListTitle">可加入成員名單</h2>
27-
<div class="table-wrap">
28-
<div class="table-scroll">
29-
<table class="member-table" role="table" aria-label="Candidate users table">
30-
<thead>
31-
<tr>
32-
<th id="thCandidateName">姓名</th>
33-
<th id="thCandidateEmail">Email</th>
34-
<th id="thCandidateTaskCount">目前已在任務數量</th>
35-
<th id="thCandidateAssignRole">指派任務角色</th>
36-
<th id="thCandidateActions">操作</th>
37-
</tr>
38-
</thead>
39-
<tbody id="candidateTableBody"></tbody>
40-
</table>
41-
</div>
25+
<section class="panel" aria-labelledby="memberAddTitle">
26+
<h2 class="panel-title" id="memberAddTitle">加入成員</h2>
27+
<p class="panel-subtitle" id="memberAddSubtitle">可透過搜尋平台現有成員,或直接填寫 Email 寄送邀請加入任務。</p>
28+
29+
<div class="member-entry-grid">
30+
<section class="member-entry-card" aria-labelledby="memberSearchTitle">
31+
<h3 class="member-entry-title" id="memberSearchTitle">搜尋平台成員</h3>
32+
<p class="member-entry-hint" id="memberSearchSubtitle">支援以帳號、姓名或 Email 搜尋目前平台成員。</p>
33+
<div class="member-entry-row">
34+
<input class="input-text" id="memberSearchInput" type="text" placeholder="輸入帳號、姓名或 Email" />
35+
<select class="role-select-inline" id="memberSearchRoleSelect">
36+
<option value="annotator">標記員</option>
37+
<option value="reviewer">審核員</option>
38+
</select>
39+
</div>
40+
<div class="table-wrap">
41+
<div class="table-scroll">
42+
<table class="member-table" role="table" aria-label="Search platform users table">
43+
<thead>
44+
<tr>
45+
<th id="thSearchAccount">帳號</th>
46+
<th id="thSearchName">姓名</th>
47+
<th id="thSearchEmail">Email</th>
48+
<th id="thSearchTaskCount">目前已在任務數量</th>
49+
<th id="thSearchActions">操作</th>
50+
</tr>
51+
</thead>
52+
<tbody id="memberSearchResultsBody"></tbody>
53+
</table>
54+
</div>
55+
</div>
56+
</section>
57+
58+
<section class="member-entry-card" aria-labelledby="memberInviteTitle">
59+
<h3 class="member-entry-title" id="memberInviteTitle">Email 邀請</h3>
60+
<p class="member-entry-hint" id="memberInviteSubtitle">填寫對方 Email 並指定任務角色後,寄送邀請加入任務。</p>
61+
<div class="member-entry-stack">
62+
<input class="input-text" id="memberInviteEmailInput" type="email" placeholder="輸入邀請 Email" />
63+
<div class="member-entry-row">
64+
<select class="role-select-inline" id="memberInviteRoleSelect">
65+
<option value="annotator">標記員</option>
66+
<option value="reviewer">審核員</option>
67+
</select>
68+
<button class="mini-btn mini-btn-primary" id="memberInviteSendBtn" type="button">寄送邀請</button>
69+
</div>
70+
</div>
71+
</section>
4272
</div>
4373
</section>
4474
</div>

design/prototype/pages/task-management/task-detail.panels/overview.html

Lines changed: 171 additions & 163 deletions
Large diffs are not rendered by default.

design/prototype/pages/task-management/task-new.html

Lines changed: 313 additions & 236 deletions
Large diffs are not rendered by default.

design/prototype/tests/task-management/task-detail-guideline-edit.spec.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from '@playwright/test';
22

3-
const TASK_DETAIL_URL = '/pages/task-management/task-detail.html';
3+
const TASK_DETAIL_URL = '/pages/task-management/task-detail.html?task_role=project_leader&task_id=T001';
44

55
test.describe('Task detail guideline edit state', () => {
66
test('uses task-new step4-like edit UI in overview guideline section', async ({ page }) => {
@@ -17,12 +17,65 @@ test.describe('Task detail guideline edit state', () => {
1717
await expect(saveBtn).toHaveClass(/hidden/);
1818
await expect(cancelBtn).toHaveClass(/hidden/);
1919

20-
await editBtn.click();
20+
await page.evaluate(() => {
21+
const button = document.getElementById('guidelineEditBtn') as HTMLButtonElement | null;
22+
button?.click();
23+
});
2124

2225
await expect(page.locator('#guidelineSummaryView')).toHaveClass(/hidden/);
2326
await expect(page.locator('#guidelineEditForm')).not.toHaveClass(/hidden/);
24-
await expect(page.locator('#guidelineEditForm')).toContainText('說明內容');
25-
await expect(page.locator('#guidelineEditForm')).toContainText('上傳檔案');
27+
await expect(page.locator('#guidelineEditForm')).toContainText('提供給標記員');
28+
await expect(page.locator('#guidelineEditForm')).toContainText('標記說明內容');
29+
await expect(page.locator('#guidelineEditForm')).toContainText('提供給審核員');
30+
await expect(page.locator('#guidelineEditForm')).toContainText('審核說明內容');
2631
await expect(page.locator('#guidelineEditForm')).toContainText('開始標記前強制顯示');
32+
await expect(page.locator('#editAnnotatorGuidelineFileInput')).toHaveJSProperty('multiple', true);
33+
await expect(page.locator('#editReviewerGuidelineFileInput')).toHaveJSProperty('multiple', true);
34+
});
35+
36+
test('uploads files for annotator and reviewer guideline sections', async ({ page }) => {
37+
await page.goto(TASK_DETAIL_URL);
38+
39+
await expect(page.locator('#guidelineEditBtn')).toBeEnabled();
40+
41+
await page.evaluate(() => {
42+
const button = document.getElementById('guidelineEditBtn') as HTMLButtonElement | null;
43+
button?.click();
44+
});
45+
46+
await expect(page.locator('#guidelineEditForm')).not.toHaveClass(/hidden/);
47+
48+
await page.locator('#editAnnotatorGuidelineFileInput').setInputFiles({
49+
name: 'annotator-guide.pdf',
50+
mimeType: 'application/pdf',
51+
buffer: Buffer.from('annotator file'),
52+
});
53+
await page.evaluate(() => {
54+
const win = window as typeof window & {
55+
handleGuidelineFileInputChange?: (role: string, input: HTMLInputElement) => void;
56+
};
57+
const input = document.getElementById('editAnnotatorGuidelineFileInput') as HTMLInputElement | null;
58+
if (input && win.handleGuidelineFileInputChange) {
59+
win.handleGuidelineFileInputChange('annotator', input);
60+
}
61+
});
62+
63+
await page.locator('#editReviewerGuidelineFileInput').setInputFiles({
64+
name: 'reviewer-guide.md',
65+
mimeType: 'text/markdown',
66+
buffer: Buffer.from('# reviewer'),
67+
});
68+
await page.evaluate(() => {
69+
const win = window as typeof window & {
70+
handleGuidelineFileInputChange?: (role: string, input: HTMLInputElement) => void;
71+
};
72+
const input = document.getElementById('editReviewerGuidelineFileInput') as HTMLInputElement | null;
73+
if (input && win.handleGuidelineFileInputChange) {
74+
win.handleGuidelineFileInputChange('reviewer', input);
75+
}
76+
});
77+
78+
await expect(page.locator('#editAnnotatorGuidelineFileList')).toContainText('annotator-guide.pdf');
79+
await expect(page.locator('#editReviewerGuidelineFileList')).toContainText('reviewer-guide.md');
2780
});
2881
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const TASK_DETAIL_URL = '/pages/task-management/task-detail.html';
4+
5+
test.describe('Task detail member management add flows', () => {
6+
test('supports search-add and email invite instead of candidate list', async ({ page }) => {
7+
await page.goto(TASK_DETAIL_URL);
8+
9+
await page.locator('#tabMemberManagement').click();
10+
await expect(page.locator('#memberManagementPanel')).not.toHaveClass(/hidden/);
11+
12+
await expect(page.locator('#candidateListTitle')).toHaveCount(0);
13+
await expect(page.locator('#memberSearchTitle')).toHaveText('搜尋平台成員');
14+
await expect(page.locator('#memberInviteTitle')).toHaveText('Email 邀請');
15+
await expect(page.locator('#memberSearchResultsBody')).toContainText('請先輸入帳號、姓名或 Email 再開始搜尋');
16+
await expect(page.locator('#memberSearchResultsBody')).not.toContainText('Jason Huang');
17+
18+
await page.locator('#memberTableBody tr').filter({ hasText: 'Jason Huang' }).locator('button:has-text("移除")').click();
19+
await page.locator('#memberActionConfirmBtn').click();
20+
21+
await page.locator('#memberSearchInput').fill('jason');
22+
await expect(page.locator('#memberSearchResultsBody tr')).toHaveCount(1);
23+
await expect(page.locator('#memberSearchResultsBody')).toContainText('Jason Huang');
24+
await expect(page.locator('#memberSearchResultsBody')).toContainText('jason.h@labelsuite.io');
25+
await expect(page.locator('#memberSearchResultsBody')).toContainText('jason.h');
26+
27+
await page.locator('#memberSearchRoleSelect').selectOption('reviewer');
28+
await page.locator('#memberSearchResultsBody button:has-text("加入任務")').click();
29+
30+
await expect(page.locator('#memberTableBody')).toContainText('Jason Huang');
31+
await expect(page.locator('#memberTableBody')).toContainText('jason.h@labelsuite.io');
32+
await expect(page.locator('#memberTableBody')).toContainText('審核員');
33+
34+
await page.locator('#memberInviteEmailInput').fill('new.reviewer@example.com');
35+
await page.locator('#memberInviteRoleSelect').selectOption('reviewer');
36+
await page.locator('#memberInviteSendBtn').click();
37+
38+
await expect(page.locator('#memberTableBody')).toContainText('new.reviewer@example.com');
39+
await expect(page.locator('#memberTableBody')).toContainText('邀請中');
40+
});
41+
});

design/prototype/tests/task-management/task-detail-overview-edit.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from '@playwright/test';
22

3-
const TASK_DETAIL_URL = '/pages/task-management/task-detail.html';
3+
const TASK_DETAIL_URL = '/pages/task-management/task-detail.html?task_id=T001';
44

55
test.describe('Task detail overview edit state', () => {
66
test('hides draft-only hint text and uses simplified edit mode layout', async ({ page }) => {
@@ -26,7 +26,7 @@ test.describe('Task detail overview edit state', () => {
2626
await expect(page.locator('#editTaskTypeLabel .required')).toHaveText('*');
2727
await expect(page.locator('#editDatasetLabel .required')).toHaveText('*');
2828
await expect(page.locator('#editDatasetFileList .overview-upload-file-preview')).toHaveCount(1);
29-
await expect(page.locator('#editDatasetFileList .overview-upload-file-name')).toContainText('customer_feedback_v3.csv');
29+
await expect(page.locator('#editDatasetFileList .overview-upload-file-name')).toContainText('news_headlines_multilabel_2026.csv');
3030

3131
const cancelBox = await cancelBtn.boundingBox();
3232
const saveBox = await saveBtn.boundingBox();

design/prototype/tests/task-management/task-detail-run-control-i18n.spec.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ test('run control stepper and metric labels translate to english', async ({ page
88
await expect(page.locator('#executionTitle')).toHaveText('任務狀態與執行控制');
99
await expect(page.locator('#statusStepper .step-label-wrap')).toHaveText([
1010
'草稿',
11-
'試標進行中',
12-
'待 IAA 確認',
11+
'試標階段',
1312
'正式標記中',
1413
'已完成',
1514
]);
@@ -19,14 +18,13 @@ test('run control stepper and metric labels translate to english', async ({ page
1918
await expect(page.locator('#executionTitle')).toHaveText('Task status and run control');
2019
await expect(page.locator('#statusStepper .step-label-wrap')).toHaveText([
2120
'Draft',
22-
'Dry run in progress',
23-
'Waiting IAA confirmation',
21+
'Trial stage',
2422
'Official run in progress',
2523
'Completed',
2624
]);
2725
await expect(page.locator('#trialRoundLabel')).toHaveText('Trial round');
28-
await expect(page.locator('#targetAgreementLabel')).toHaveText('Target IAA');
29-
await expect(page.locator('#currentAgreementLabel')).toHaveText('Current IAA');
30-
await expect(page.locator('#currentStdLabel')).toHaveText('Current standard deviation');
26+
await expect(page.locator('#trialRoundsUsedLabel')).toHaveText('Trial rounds used');
27+
await expect(page.locator('#currentAgreementLabel')).toHaveText('Latest round IAA');
28+
await expect(page.locator('#officialPoolLabel')).toHaveText('Official pool');
3129
await expect(page.locator('#stopConditionTitle')).toHaveText('Stop conditions');
3230
});

design/prototype/tests/task-management/task-detail-sampling-edit.spec.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,27 @@ test.describe('Task detail sampling edit state', () => {
2727
await expect(cancelBtn).toBeVisible();
2828
await expect(page.locator('#samplingSummaryView')).toHaveClass(/hidden/);
2929
await expect(page.locator('#samplingEditForm')).not.toHaveClass(/hidden/);
30-
await expect(page.locator('#samplingModePercent')).toBeEnabled();
31-
await expect(page.locator('#samplingModeCount')).toBeEnabled();
3230
await expect(page.locator('#samplingValue')).toBeEnabled();
31+
await expect(page.locator('#iaaMethodSelect')).toBeEnabled();
32+
await expect(page.locator('#samplingRoundInput')).toBeEnabled();
33+
await expect(page.locator('#targetAgreementInput')).toBeEnabled();
34+
await expect(page.locator('#minAnnotatorsInput')).toBeEnabled();
3335
await expect(page.locator('#isolationToggle')).toBeEnabled();
36+
37+
const firstRowFields = page.locator('#samplingEditForm .sampling-fields').first().locator('.field-group');
38+
await expect(firstRowFields).toHaveCount(2);
39+
await expect(firstRowFields.nth(0).locator('label')).toContainText('抽樣筆數');
40+
await expect(firstRowFields.nth(1).locator('label')).toContainText('試標回合');
41+
42+
const secondRowFields = page.locator('#samplingEditForm .sampling-fields-advanced').first().locator('.field-group');
43+
await expect(secondRowFields.nth(0).locator('label')).toContainText('IAA 計算方式');
44+
45+
const samplingBox = await firstRowFields.nth(0).boundingBox();
46+
const roundBox = await firstRowFields.nth(1).boundingBox();
47+
48+
expect(samplingBox).not.toBeNull();
49+
expect(roundBox).not.toBeNull();
50+
expect(Math.abs((samplingBox?.y ?? 0) - (roundBox?.y ?? 0))).toBeLessThanOrEqual(2);
51+
expect(Math.abs(((samplingBox?.y ?? 0) + (samplingBox?.height ?? 0)) - ((roundBox?.y ?? 0) + (roundBox?.height ?? 0)))).toBeLessThanOrEqual(2);
3452
});
3553
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const TASK_DETAIL_URL = '/pages/task-management/task-detail.html?task_id=T001';
4+
5+
test('keeps the 4-stage stepper while showing a complete trial-to-official flow', async ({ page }) => {
6+
await page.goto(TASK_DETAIL_URL);
7+
8+
await expect(page.locator('#statusStepper .step-label-wrap')).toHaveText([
9+
'草稿',
10+
'試標階段',
11+
'正式標記中',
12+
'已完成',
13+
]);
14+
15+
await expect(page.locator('#trialRoundTimeline .round-timeline-item')).toHaveCount(0);
16+
await expect(page.locator('#roundHistoryTitle')).toHaveText('試標回合歷程');
17+
await expect(page.locator('#roundHistoryHint')).toHaveText('每個回合使用新的抽樣樣本;通過的回合會解鎖正式標記。');
18+
await expect(page.locator('#trialRoundTimeline')).toContainText('尚未建立任何試標回合');
19+
await expect(page.locator('#trialDecisionTitle')).toHaveText('尚未建立試標回合');
20+
await expect(page.locator('#trialDecisionDesc')).toHaveText('先建立第一個試標回合,確認一致性門檻是否合理,再決定是否進入正式標記。');
21+
22+
const stopRow = page.locator('#execStopRow');
23+
const dryRunBtn = page.locator('#publishDryRunBtn');
24+
await expect(dryRunBtn).toHaveText('新增試標回合 R1');
25+
await expect(stopRow.locator('#publishDryRunBtn')).toHaveText('新增試標回合 R1');
26+
27+
await dryRunBtn.click();
28+
29+
await expect(page.locator('#executionStageTitle')).toHaveText('試標階段');
30+
await expect(page.locator('#trialRoundTimeline .round-timeline-item')).toHaveCount(1);
31+
await expect(page.locator('#trialRoundTimeline .round-timeline-item').first()).toContainText('R1');
32+
await expect(page.locator('#trialRoundTimeline .round-status-badge').first()).toHaveText('未通過');
33+
await expect(page.locator('#splitLegendDynamic')).toContainText('R1 10筆');
34+
await expect(page.locator('#splitLegendDynamic')).toContainText('正式 3190筆');
35+
36+
await page.locator('#publishDryRunBtn').click();
37+
38+
await expect(page.locator('#trialRoundTimeline .round-timeline-item')).toHaveCount(2);
39+
await expect(page.locator('#trialRoundTimeline .round-timeline-item').nth(1)).toContainText('R2');
40+
await expect(page.locator('#trialRoundTimeline .round-status-badge').nth(1)).toHaveText('已通過');
41+
await expect(page.locator('#publishOfficialRunBtn')).toHaveText('開始正式標記');
42+
await expect(page.locator('#splitLegendDynamic')).toContainText('R1 10筆');
43+
await expect(page.locator('#splitLegendDynamic')).toContainText('R2 10筆');
44+
await expect(page.locator('#splitLegendDynamic')).toContainText('正式 3180筆');
45+
46+
await page.locator('#publishOfficialRunBtn').click();
47+
48+
await expect(page.locator('#executionStageTitle')).toHaveText('正式標記中');
49+
await expect(page.locator('#trialDecisionTitle')).toHaveText('正式標記進行中,共 3180 筆');
50+
await expect(page.locator('#publishCompleteBtn')).toHaveText('標記完成');
51+
52+
await page.locator('#publishCompleteBtn').click();
53+
54+
await expect(page.locator('#executionStageTitle')).toHaveText('已完成');
55+
await expect(page.locator('#trialDecisionTitle')).toHaveText('正式標記已完成,共 3180 筆');
56+
});

0 commit comments

Comments
 (0)