Skip to content

Commit c9d1636

Browse files
fix(statuses): clarify default workflow state
1 parent ccd44b7 commit c9d1636

4 files changed

Lines changed: 141 additions & 13 deletions

File tree

components/products/ProductSettingsFeedback.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ const FALLBACK_STATUSES = [
191191
{ value: 'in_progress', name: 'In Progress', color: '#f59e0b' },
192192
{ value: 'completed', name: 'Completed', color: '#10b981' },
193193
{ value: 'closed', name: 'Closed', color: '#6b7280' },
194-
{ value: 'declined', name: 'Declined', color: '#ef4444' },
195194
]
196195
197196
export default {

components/products/ProductSettingsStatuses.vue

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
<Icon name="lucide:circle-dot" class="h-5 w-5" />
99
Feedback Statuses
1010
</CardTitle>
11-
<CardDescription>
12-
Customize the workflow statuses available for feedback items. Drag to reorder.
13-
</CardDescription>
11+
<CardDescription>{{ statusCardDescription }}</CardDescription>
1412
</div>
1513
<Button size="sm" @click="openCreateDialog">
1614
<Icon name="lucide:plus" class="w-4 h-4 mr-2" />
@@ -37,7 +35,7 @@
3735
<div v-else-if="isUsingDefaults" class="space-y-4">
3836
<div class="rounded-md bg-muted p-3 text-sm text-muted-foreground">
3937
<Icon name="lucide:info" class="w-4 h-4 inline mr-1.5 align-text-bottom" />
40-
Using default system statuses. Add a custom status to start customizing your workflow.
38+
Using the default starting workflow. Add a custom status to replace it with your own workflow.
4139
</div>
4240
<div class="space-y-2">
4341
<div
@@ -67,10 +65,13 @@
6765

6866
<!-- Custom Status List -->
6967
<div v-else class="space-y-2">
70-
<p class="text-xs text-muted-foreground">Drag statuses to reorder how they appear in dropdowns.</p>
68+
<p class="text-xs text-muted-foreground">
69+
{{ canReorderStatuses ? 'Drag statuses to reorder how they appear in dropdowns.' : 'Add another status to enable reordering.' }}
70+
</p>
7171
<div
7272
v-for="status in statuses"
7373
:key="status.id"
74+
:data-testid="`product-status-item-${status.id}`"
7475
class="flex items-center justify-between rounded-lg border p-3 transition-colors"
7576
:class="{
7677
'border-primary bg-primary/5': dragOverStatusId === status.id && draggedStatusId !== status.id,
@@ -83,10 +84,11 @@
8384
<div class="flex items-center gap-3">
8485
<button
8586
type="button"
87+
:data-testid="`product-status-drag-handle-${status.id}`"
8688
class="inline-flex h-8 w-8 cursor-grab items-center justify-center rounded-md text-muted-foreground hover:bg-accent disabled:cursor-not-allowed"
8789
:aria-label="`Drag to reorder ${status.name}`"
88-
:disabled="isReordering"
89-
draggable="true"
90+
:disabled="isReordering || !canReorderStatuses"
91+
:draggable="canReorderStatuses && !isReordering"
9092
@dragstart="onDragStart(status.id, $event)"
9193
@dragend="onDragEnd"
9294
>
@@ -240,6 +242,22 @@ export default {
240242
},
241243
}
242244
},
245+
computed: {
246+
canReorderStatuses() {
247+
return !this.isUsingDefaults && this.statuses.length > 1
248+
},
249+
statusCardDescription() {
250+
if (this.isUsingDefaults) {
251+
return 'Review the default starting workflow for feedback items. Add a custom status to start customizing it.'
252+
}
253+
254+
if (this.statuses.length <= 1) {
255+
return 'Customize the workflow statuses available for feedback items. Add another status to enable reordering.'
256+
}
257+
258+
return 'Customize the workflow statuses available for feedback items. Drag to reorder.'
259+
},
260+
},
243261
watch: {
244262
'project.slug': {
245263
handler() {
@@ -305,7 +323,7 @@ export default {
305323
},
306324
307325
onDragOver(statusId) {
308-
if (!this.draggedStatusId || this.draggedStatusId === statusId) return
326+
if (!this.canReorderStatuses || !this.draggedStatusId || this.draggedStatusId === statusId) return
309327
this.dragOverStatusId = statusId
310328
},
311329
@@ -321,7 +339,7 @@ export default {
321339
},
322340
323341
async onDrop(targetStatusId) {
324-
if (!this.draggedStatusId || this.draggedStatusId === targetStatusId || this.isReordering) {
342+
if (!this.canReorderStatuses || !this.draggedStatusId || this.draggedStatusId === targetStatusId || this.isReordering) {
325343
this.onDragEnd()
326344
return
327345
}

server/utils/project-statuses.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const SYSTEM_STATUSES = [
1313
{ value: 'in_progress', name: 'In Progress', color: '#f59e0b', description: 'Actively being worked on', sortOrder: 2, isDefault: false },
1414
{ value: 'completed', name: 'Completed', color: '#10b981', description: 'Released or resolved', sortOrder: 3, isDefault: false },
1515
{ value: 'closed', name: 'Closed', color: '#6b7280', description: 'Closed without action', sortOrder: 4, isDefault: false },
16-
{ value: 'declined', name: 'Declined', color: '#ef4444', description: 'Out of scope or will not fix', sortOrder: 5, isDefault: false },
1716
]
1817

1918
export function toStatusValue(name: string) {

tests/e2e/team-primary-workspace.spec.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { expect, test } from '@playwright/test'
2-
import type { APIRequestContext } from '@playwright/test'
3-
import { signInAndGetSessionCookie, withAuthHeaders } from './helpers/auth'
2+
import { loginViaProgrammatic, signInAndGetSessionCookie, withAuthHeaders } from './helpers/auth'
43

54
const TEST_EMAIL = process.env.E2E_USER_EMAIL || 'test@preview.local'
65
const TEST_PASSWORD = process.env.E2E_USER_PASSWORD || 'password123'
@@ -89,6 +88,119 @@ test('authenticated user can access products UI workflow', async ({ request, pag
8988
await expect(page.getByText('Point a CNAME record to Veerify to use a custom domain.')).toHaveCount(0)
9089
})
9190

91+
test('new project statuses tab shows the default starting workflow without declined or drag affordances', async ({
92+
request,
93+
page,
94+
}) => {
95+
await loginViaProgrammatic(request, { email: TEST_EMAIL, password: TEST_PASSWORD })
96+
97+
const authCookies = (await request.storageState()).cookies.filter((cookie) => cookie.name.startsWith('better-auth'))
98+
expect(authCookies.length).toBeGreaterThan(0)
99+
await page.context().addCookies(authCookies)
100+
101+
const activeTeamResponse = await request.get('/api/teams/active')
102+
const activeTeamPayload = await activeTeamResponse.json()
103+
expect(activeTeamResponse.ok()).toBeTruthy()
104+
const teamId = activeTeamPayload.data.id as string
105+
106+
const projectSlug = `e2e-statuses-${Date.now()}`
107+
const createProjectResponse = await request.post(`/api/teams/${teamId}/projects`, {
108+
data: {
109+
name: 'Statuses Product',
110+
slug: projectSlug,
111+
description: null,
112+
customDomain: null,
113+
},
114+
})
115+
expect(createProjectResponse.status()).toBe(201)
116+
117+
const statusesResponse = await request.get(`/api/projects/${projectSlug}/statuses`)
118+
const statusesPayload = await statusesResponse.json()
119+
expect(statusesResponse.ok()).toBeTruthy()
120+
const initialStatuses = statusesPayload.data as Array<{ name: string; value: string }>
121+
expect(initialStatuses.map((status) => status.name)).toEqual(['Open', 'Planned', 'In Progress', 'Completed', 'Closed'])
122+
expect(initialStatuses.map((status) => status.value)).not.toContain('declined')
123+
124+
await page.goto(`/products/${projectSlug}#statuses`, { waitUntil: 'domcontentloaded', timeout: 180_000 })
125+
await expect(page).toHaveURL(new RegExp(`/products/${projectSlug}#statuses$`))
126+
await expect(page.getByText('Feedback Statuses')).toBeVisible()
127+
await expect(
128+
page.getByText('Review the default starting workflow for feedback items. Add a custom status to start customizing it.')
129+
).toBeVisible()
130+
await expect(
131+
page.getByText('Using the default starting workflow. Add a custom status to replace it with your own workflow.')
132+
).toBeVisible()
133+
134+
for (const label of ['Open', 'Planned', 'In Progress', 'Completed', 'Closed']) {
135+
await expect(page.getByText(label, { exact: true })).toBeVisible()
136+
}
137+
await expect(page.getByText('Declined', { exact: true })).toHaveCount(0)
138+
await expect(page.locator('[data-testid^="product-status-drag-handle-"]')).toHaveCount(0)
139+
})
140+
141+
test('custom domain dns setup hides duplicate cname targets for the same host', async ({ request, page }) => {
142+
await loginViaProgrammatic(request, { email: TEST_EMAIL, password: TEST_PASSWORD })
143+
144+
const authCookies = (await request.storageState()).cookies.filter((cookie) => cookie.name.startsWith('better-auth'))
145+
expect(authCookies.length).toBeGreaterThan(0)
146+
await page.context().addCookies(authCookies)
147+
148+
const activeTeamResponse = await request.get('/api/teams/active')
149+
const activeTeamPayload = await activeTeamResponse.json()
150+
expect(activeTeamResponse.ok()).toBeTruthy()
151+
const teamId = activeTeamPayload.data.id as string
152+
153+
const projectSlug = `e2e-domain-${Date.now()}`
154+
const createProjectResponse = await request.post(`/api/teams/${teamId}/projects`, {
155+
data: {
156+
name: 'Domain Product',
157+
slug: projectSlug,
158+
description: null,
159+
customDomain: null,
160+
},
161+
})
162+
expect(createProjectResponse.status()).toBe(201)
163+
164+
await page.route(`**/api/projects/${projectSlug}/verify-domain?**`, async (route) => {
165+
await route.fulfill({
166+
status: 200,
167+
contentType: 'application/json',
168+
body: JSON.stringify({
169+
hostname: 'feedback.example.com',
170+
provider: 'vercel',
171+
verified: false,
172+
status: 'ownership_verification_required',
173+
dnsRecords: [
174+
{
175+
type: 'CNAME',
176+
name: 'feedback.example.com',
177+
value: '23f9267bd57617a5.vercel-dns-017.com.',
178+
},
179+
{
180+
type: 'CNAME',
181+
name: 'feedback.example.com',
182+
value: 'cname.vercel-dns.com.',
183+
},
184+
],
185+
configuredBy: 'CNAME',
186+
expected: '23f9267bd57617a5.vercel-dns-017.com.',
187+
resolvedTo: [],
188+
message: null,
189+
}),
190+
})
191+
})
192+
193+
await page.goto(`/products/${projectSlug}#domain`, { waitUntil: 'domcontentloaded', timeout: 180_000 })
194+
await expect(page).toHaveURL(new RegExp(`/products/${projectSlug}#domain$`))
195+
await expect(page.getByText('Required DNS records')).toBeVisible()
196+
197+
await page.locator('#custom-domain').fill('feedback.example.com')
198+
await page.getByRole('button', { name: 'Check DNS' }).click()
199+
200+
await expect(page.getByText('23f9267bd57617a5.vercel-dns-017.com.')).toBeVisible()
201+
await expect(page.getByText('cname.vercel-dns.com.')).toHaveCount(0)
202+
})
203+
92204
test('project categories API supports create/update/reorder/delete with reassignment rules', async ({ request }) => {
93205
const sessionCookie = await signInAndGetSessionCookie(request, { email: TEST_EMAIL, password: TEST_PASSWORD })
94206

0 commit comments

Comments
 (0)