feat: add participant interview link action
Add a copy button to scheduled interview sessions with confirmation, clipboard error handling, and regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
afc8f0d5a7
commit
e4fee06e2f
2 changed files with 27 additions and 1 deletions
|
|
@ -814,6 +814,7 @@ describe('Go Guess frontend', () => {
|
||||||
it('edits shared documentation and displays notes received over authenticated fetch streaming', async () => {
|
it('edits shared documentation and displays notes received over authenticated fetch streaming', async () => {
|
||||||
authenticate()
|
authenticate()
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
const writeText = vi.spyOn(navigator.clipboard, 'writeText')
|
||||||
const scheduled = {
|
const scheduled = {
|
||||||
id: 80,
|
id: 80,
|
||||||
jobId: 7,
|
jobId: 7,
|
||||||
|
|
@ -866,6 +867,11 @@ describe('Go Guess frontend', () => {
|
||||||
})
|
})
|
||||||
renderApp('/scheduled-interviews/80')
|
renderApp('/scheduled-interviews/80')
|
||||||
expect(await screen.findByText('Strong system design answer.')).toBeInTheDocument()
|
expect(await screen.findByText('Strong system design answer.')).toBeInTheDocument()
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Copy participant interview link' }))
|
||||||
|
expect(writeText).toHaveBeenCalledWith('/participant/meeting/meeting-token')
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: 'Copy participant interview link' }),
|
||||||
|
).toHaveTextContent('Link copied')
|
||||||
const document = screen.getByLabelText('Shared documentation')
|
const document = screen.getByLabelText('Shared documentation')
|
||||||
await user.clear(document)
|
await user.clear(document)
|
||||||
await user.type(document, 'Updated candidate exercise')
|
await user.type(document, 'Updated candidate exercise')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Check, UsersRound, X } from 'lucide-react'
|
import { Check, Copy, UsersRound, X } from 'lucide-react'
|
||||||
import { type FormEvent, useEffect, useMemo, useState } from 'react'
|
import { type FormEvent, useEffect, useMemo, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { api, ApiError } from '../../api/client'
|
import { api, ApiError } from '../../api/client'
|
||||||
|
|
@ -145,6 +145,7 @@ export function ScheduledInterviewPage() {
|
||||||
const [notes, setNotes] = useState<ScheduledInterview['notes']>([])
|
const [notes, setNotes] = useState<ScheduledInterview['notes']>([])
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [linkCopied, setLinkCopied] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true
|
let active = true
|
||||||
|
|
@ -218,6 +219,16 @@ export function ScheduledInterviewPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyParticipantLink(url: string) {
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url)
|
||||||
|
setLinkCopied(true)
|
||||||
|
} catch {
|
||||||
|
setError('Could not copy the participant interview link')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!interview && !error) return <Loading />
|
if (!interview && !error) return <Loading />
|
||||||
if (!interview) return <ErrorAlert error={error} />
|
if (!interview) return <ErrorAlert error={error} />
|
||||||
return (
|
return (
|
||||||
|
|
@ -236,6 +247,15 @@ export function ScheduledInterviewPage() {
|
||||||
{error && <ErrorAlert error={error} />}
|
{error && <ErrorAlert error={error} />}
|
||||||
<DateAndPlace interview={interview} />
|
<DateAndPlace interview={interview} />
|
||||||
<div className="session-actions">
|
<div className="session-actions">
|
||||||
|
<button
|
||||||
|
className="button secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void copyParticipantLink(interview.candidateUrl)}
|
||||||
|
aria-label="Copy participant interview link"
|
||||||
|
>
|
||||||
|
{linkCopied ? <Check size={17} /> : <Copy size={17} />}
|
||||||
|
{linkCopied ? 'Link copied' : 'Copy participant link'}
|
||||||
|
</button>
|
||||||
{interview.status !== 'started' && interview.status !== 'completed' && (
|
{interview.status !== 'started' && interview.status !== 'completed' && (
|
||||||
<button
|
<button
|
||||||
className="button primary"
|
className="button primary"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue