Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions admin/src/PlaytestDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,22 @@ describe('PlaytestDetailPage shell', () => {
await waitFor(() => expect(screen.getByTestId('distribution-tab')).toBeInTheDocument())
})

it('Survey tab shows the notify notice with a Learn more / Show less toggle', async () => {
renderDetail('autumn-draft')
const user = userEvent.setup()
await user.click(screen.getByRole('tab', { name: 'Survey' }))
const notice = await screen.findByTestId('survey-notify-notice')
// Compact line always shown; the detail (bullets + caveat) is hidden until "Learn more".
expect(within(notice).getByText(/your approved testers are notified automatically/i)).toBeInTheDocument()
expect(within(notice).queryByText(/won't re-send any DMs/i)).not.toBeInTheDocument()

await user.click(within(notice).getByText(/learn more/i))
expect(within(notice).getByText(/won't re-send any DMs/i)).toBeInTheDocument()

await user.click(within(notice).getByText(/show less/i))
expect(within(notice).queryByText(/won't re-send any DMs/i)).not.toBeInTheDocument()
})

it('Info tab renders the read-only field grid + Edit button', () => {
renderDetail('autumn-draft')
expect(screen.getByTestId('playtest-info-tab')).toBeInTheDocument()
Expand Down
294 changes: 164 additions & 130 deletions admin/src/tabs/SurveyTab.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
import { useAppUIContext } from '@accelbyte/sdk-extend-app-ui'
import { InfoCircleFilled } from '@ant-design/icons'
import { useQueryClient } from '@tanstack/react-query'
import {
Alert,
Button,
Card,
Checkbox,
Form,
Input,
Popconfirm,
Select,
Space,
Spin,
Typography,
message
} from 'antd'
import { Alert, Button, Card, Checkbox, Form, Input, Popconfirm, Select, Space, Spin, Typography, message } from 'antd'
import { useState } from 'react'
import type { V1MultiChoiceOption } from '../playtesthubapi/generated-definitions/V1MultiChoiceOption'
import type { V1Playtest } from '../playtesthubapi/generated-definitions/V1Playtest'
import type { V1Survey } from '../playtesthubapi/generated-definitions/V1Survey'
import type { V1SurveyQuestion } from '../playtesthubapi/generated-definitions/V1SurveyQuestion'
import {
Key_PlaytesthubServiceAdmin,
usePlaytesthubServiceAdminApi_CreateSurvey_ByPlaytestIdMutation,
usePlaytesthubServiceAdminApi_PatchSurvey_ByPlaytestIdMutation
} from '../playtesthubapi/generated-admin/queries/PlaytesthubServiceAdmin.query'
import type { V1MultiChoiceOption } from '../playtesthubapi/generated-definitions/V1MultiChoiceOption'
import type { V1Playtest } from '../playtesthubapi/generated-definitions/V1Playtest'
import type { V1Survey } from '../playtesthubapi/generated-definitions/V1Survey'
import type { V1SurveyQuestion } from '../playtesthubapi/generated-definitions/V1SurveyQuestion'
import { usePlaytesthubServiceApi_GetSurveyPlayer_ByPlaytestId } from '../playtesthubapi/generated-public/queries/PlaytesthubService.query'
import { PlaytestStatus } from '../shared/playtesthub-enums'
import { toastError } from '../shared/api-error'
import { PlaytestStatus } from '../shared/playtesthub-enums'

const QUESTION_TYPE_TEXT = 'SURVEY_QUESTION_TYPE_TEXT'
const QUESTION_TYPE_RATING = 'SURVEY_QUESTION_TYPE_RATING'
Expand Down Expand Up @@ -117,11 +105,7 @@ export function SurveyTab({ playtest }: { playtest: V1Playtest }) {
// Player GetSurvey is the authoritative read path (no admin GET in proto).
// Returns NotFound for DRAFT playtests — render the warning + blank form in
// that case so first-version edits still work.
const surveyQuery = usePlaytesthubServiceApi_GetSurveyPlayer_ByPlaytestId(
sdk,
{ playtestId },
{ enabled: hasSurvey, retry: false }
)
const surveyQuery = usePlaytesthubServiceApi_GetSurveyPlayer_ByPlaytestId(sdk, { playtestId }, { enabled: hasSurvey, retry: false })

if (hasSurvey && surveyQuery.isLoading) return <Spin description="Loading existing survey..." />

Expand Down Expand Up @@ -156,6 +140,7 @@ function SurveyTabForm({ playtestId, initialSurvey, hasSurvey, draftPreloadFaile
if (initialSurvey?.questions?.length) return initialSurvey.questions.map(questionToDraft)
return [freshTextQuestion()]
})
const [showNotifyDetail, setShowNotifyDetail] = useState(false)
const version = initialSurvey?.version ?? null

const createMutation = usePlaytesthubServiceAdminApi_CreateSurvey_ByPlaytestIdMutation(sdk, {
Expand Down Expand Up @@ -239,116 +224,165 @@ function SurveyTabForm({ playtestId, initialSurvey, hasSurvey, draftPreloadFaile
const saving = createMutation.isPending || editMutation.isPending

return (
<Card
data-testid="survey-tab"
title="Post-Playtest Survey"
extra={
<Button type="primary" onClick={onSave} loading={saving} disabled={questions.length === 0}>
{hasSurvey ? 'Save new version' : 'Create survey'}
</Button>
}
>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Typography.Text type="secondary">
{hasSurvey ? 'Editing existing survey.' : 'Configure the post-playtest survey for approved players.'}
{version != null && ` Current version v${version} — saving creates v${version + 1}.`}
</Typography.Text>
<>
<Alert
type="info"
showIcon
icon={<InfoCircleFilled style={{ marginTop: 3 }} />}
style={{ marginBottom: 16, alignItems: 'flex-start' }}
data-testid="survey-notify-notice"
message={
<div>
<Typography.Text>When you create this survey, your approved testers are notified automatically.</Typography.Text>
{!showNotifyDetail && (
<>
{' '}
<Typography.Link onClick={() => setShowNotifyDetail(true)}>Learn more</Typography.Link>
</>
)}
{showNotifyDetail && (
<>
<ul style={{ margin: '8px 0 0', paddingInlineStart: 20, listStyleType: 'disc' }}>
<li>
<Typography.Text strong>Already approved</Typography.Text> (NDA accepted, if required): a one-time DM with the link.
</li>
<li>
<Typography.Text strong>Approved later</Typography.Text>: the link is added to their approval DM automatically.
</li>
<li>
<Typography.Text strong>Anytime</Typography.Text>: it also appears on the Playtest sign-up page (shown after the player
signs up).
</li>
</ul>
<Typography.Paragraph type="secondary" style={{ margin: '8px 0 0' }}>
Editing the survey afterward won't re-send any DMs.
</Typography.Paragraph>
<Typography.Link onClick={() => setShowNotifyDetail(false)} style={{ display: 'inline-block', marginTop: 8 }}>
Show less
</Typography.Link>
</>
)}
</div>
}
/>
<Card
data-testid="survey-tab"
title="Post-Playtest Survey"
extra={
<Button type="primary" onClick={onSave} loading={saving} disabled={questions.length === 0}>
{hasSurvey ? 'Save new version' : 'Create survey'}
</Button>
}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Typography.Text type="secondary">
{hasSurvey ? 'Editing existing survey.' : 'Configure the post-playtest survey for approved players.'}
{version != null && ` Current version v${version} — saving creates v${version + 1}.`}
</Typography.Text>

{draftPreloadFailed && (
<Alert
type="warning"
showIcon
message="DRAFT playtest survey can't be previewed"
description="Loading existing survey questions requires the playtest to be OPEN. Saving here will create a new version that won't preserve question/option ids — only safe before any responses exist."
/>
)}
{draftPreloadFailed && (
<Alert
type="warning"
showIcon
message="DRAFT playtest survey can't be previewed"
description="Loading existing survey questions requires the playtest to be OPEN. Saving here will create a new version that won't preserve question/option ids — only safe before any responses exist."
/>
)}

<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
{questions.map((q, i) => (
<div
key={q.key}
data-testid="survey-question"
style={{ background: '#fafafa', border: '1px solid #f0f0f0', borderRadius: 8, padding: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Typography.Text strong>Question {i + 1}</Typography.Text>
<Space size={4}>
<Button size="small" onClick={() => moveQuestion(q.key, -1)} disabled={i === 0} aria-label={`Move question ${i + 1} up`}>
↑
</Button>
<Button
size="small"
onClick={() => moveQuestion(q.key, 1)}
disabled={i === questions.length - 1}
aria-label={`Move question ${i + 1} down`}>
↓
</Button>
<Popconfirm title="Remove this question?" okText="Remove" okButtonProps={{ danger: true }} onConfirm={() => removeQuestion(q.key)}>
<Button size="small" danger aria-label={`Remove question ${i + 1}`}>
Remove
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
{questions.map((q, i) => (
<div
key={q.key}
data-testid="survey-question"
style={{ background: '#fafafa', border: '1px solid #f0f0f0', borderRadius: 8, padding: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Typography.Text strong>Question {i + 1}</Typography.Text>
<Space size={4}>
<Button
size="small"
onClick={() => moveQuestion(q.key, -1)}
disabled={i === 0}
aria-label={`Move question ${i + 1} up`}>
↑
</Button>
</Popconfirm>
</Space>
<Button
size="small"
onClick={() => moveQuestion(q.key, 1)}
disabled={i === questions.length - 1}
aria-label={`Move question ${i + 1} down`}>
↓
</Button>
<Popconfirm
title="Remove this question?"
okText="Remove"
okButtonProps={{ danger: true }}
onConfirm={() => removeQuestion(q.key)}>
<Button size="small" danger aria-label={`Remove question ${i + 1}`}>
Remove
</Button>
</Popconfirm>
</Space>
</div>
<Form layout="vertical">
<Form.Item label="Type" style={{ marginBottom: 12 }}>
<Select
value={q.type}
onChange={val => setQuestionType(q.key, val)}
options={Object.entries(QUESTION_TYPE_LABEL).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item label="Prompt" style={{ marginBottom: 12 }}>
<Input.TextArea
value={q.prompt}
maxLength={MAX_PROMPT}
showCount
onChange={e => updateQuestion(q.key, { prompt: e.target.value })}
rows={2}
placeholder="What did you think of the build?"
/>
</Form.Item>
<Form.Item style={{ marginBottom: q.type === QUESTION_TYPE_MULTI_CHOICE ? 12 : 0 }}>
<Checkbox checked={q.required} onChange={e => updateQuestion(q.key, { required: e.target.checked })}>
Required
</Checkbox>
</Form.Item>
{q.type === QUESTION_TYPE_MULTI_CHOICE && (
<>
<Form.Item style={{ marginBottom: 12 }}>
<Checkbox checked={q.allowMultiple} onChange={e => updateQuestion(q.key, { allowMultiple: e.target.checked })}>
Allow multiple selections
</Checkbox>
</Form.Item>
<Form.Item label={`Options (${q.options.length}/${MAX_OPTIONS})`} style={{ marginBottom: 0 }}>
<Space direction="vertical" style={{ display: 'flex' }}>
{q.options.map((opt, oIdx) => (
<Space key={oIdx} style={{ width: '100%' }}>
<Input
value={opt.label}
maxLength={MAX_OPTION_LABEL}
onChange={e => updateOption(q.key, oIdx, e.target.value)}
placeholder={`Option ${oIdx + 1}`}
/>
<Button onClick={() => removeOption(q.key, oIdx)} disabled={q.options.length <= MIN_OPTIONS}>
×
</Button>
</Space>
))}
<Button onClick={() => addOption(q.key)} disabled={q.options.length >= MAX_OPTIONS}>
Add option
</Button>
</Space>
</Form.Item>
</>
)}
</Form>
</div>
<Form layout="vertical">
<Form.Item label="Type" style={{ marginBottom: 12 }}>
<Select
value={q.type}
onChange={val => setQuestionType(q.key, val)}
options={Object.entries(QUESTION_TYPE_LABEL).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item label="Prompt" style={{ marginBottom: 12 }}>
<Input.TextArea
value={q.prompt}
maxLength={MAX_PROMPT}
showCount
onChange={e => updateQuestion(q.key, { prompt: e.target.value })}
rows={2}
placeholder="What did you think of the build?"
/>
</Form.Item>
<Form.Item style={{ marginBottom: q.type === QUESTION_TYPE_MULTI_CHOICE ? 12 : 0 }}>
<Checkbox checked={q.required} onChange={e => updateQuestion(q.key, { required: e.target.checked })}>
Required
</Checkbox>
</Form.Item>
{q.type === QUESTION_TYPE_MULTI_CHOICE && (
<>
<Form.Item style={{ marginBottom: 12 }}>
<Checkbox checked={q.allowMultiple} onChange={e => updateQuestion(q.key, { allowMultiple: e.target.checked })}>
Allow multiple selections
</Checkbox>
</Form.Item>
<Form.Item label={`Options (${q.options.length}/${MAX_OPTIONS})`} style={{ marginBottom: 0 }}>
<Space direction="vertical" style={{ display: 'flex' }}>
{q.options.map((opt, oIdx) => (
<Space key={oIdx} style={{ width: '100%' }}>
<Input
value={opt.label}
maxLength={MAX_OPTION_LABEL}
onChange={e => updateOption(q.key, oIdx, e.target.value)}
placeholder={`Option ${oIdx + 1}`}
/>
<Button onClick={() => removeOption(q.key, oIdx)} disabled={q.options.length <= MIN_OPTIONS}>
×
</Button>
</Space>
))}
<Button onClick={() => addOption(q.key)} disabled={q.options.length >= MAX_OPTIONS}>
Add option
</Button>
</Space>
</Form.Item>
</>
)}
</Form>
</div>
))}
<Button onClick={addQuestion} disabled={questions.length >= MAX_QUESTIONS}>
+ Add question
</Button>
))}
<Button onClick={addQuestion} disabled={questions.length >= MAX_QUESTIONS}>
+ Add question
</Button>
</Space>
</Space>
</Space>
</Card>
</Card>
</>
)
}
Loading