fix(ui): some uxt fixes

This commit is contained in:
nick-delirium 2023-12-06 13:24:52 +01:00
parent 552c7e77e1
commit 2e6dd17f0b
10 changed files with 260 additions and 148 deletions

View file

@ -87,7 +87,7 @@ export function UxTFunnelBar(props: Props) {
}} }}
> >
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3"> <div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
{(filter.completed/(filter.completed+filter.skipped))*100}% {((filter.completed/(filter.completed+filter.skipped))*100).toFixed(1)}%
</div> </div>
</div> </div>
</div> </div>
@ -96,19 +96,22 @@ export function UxTFunnelBar(props: Props) {
<div className={'flex items-center gap-2'}> <div className={'flex items-center gap-2'}>
<div className="flex items-center"> <div className="flex items-center">
<Icon name="arrow-right-short" size="20" color="green" /> <Icon name="arrow-right-short" size="20" color="green" />
<span className="mx-1 font-medium">{filter.completed} completed this step</span> <span className="mx-1 font-medium">{filter.completed}</span><span>completed this step</span>
</div> </div>
<div className={'flex items-center'}> <div className={'flex items-center'}>
<Icon name="clock" size="20" color="green" /> <Icon name="clock" size="20" />
<span className="mx-1 font-medium"> <span className="mx-1 font-medium">
{durationFormatted(filter.avgCompletionTime)} Avg. completion time {durationFormatted(filter.avgCompletionTime)}
</span>
<span>
Avg. completion time
</span> </span>
</div> </div>
</div> </div>
{/* @ts-ignore */} {/* @ts-ignore */}
<div className="flex items-center"> <div className="flex items-center">
<Icon name="caret-down-fill" color="red" size={16} /> <Icon name="caret-down-fill" color="red" size={16} />
<span className="font-medium mx-1 color-red">{filter.skipped} skipped</span> <span className="font-medium mx-1">{filter.skipped}</span><span> skipped</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,6 +2,7 @@ import React from 'react';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { numberWithCommas } from 'App/utils'; import { numberWithCommas } from 'App/utils';
import { Input } from 'antd'; import { Input } from 'antd';
import ReloadButton from "Shared/ReloadButton";
import SessionItem from 'Shared/SessionItem'; import SessionItem from 'Shared/SessionItem';
import { Pagination } from 'UI'; import { Pagination } from 'UI';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
@ -23,6 +24,7 @@ function LiveTestsModal({ testId, closeModal }: { testId: string, closeModal: ()
return ( return (
<div className={'h-screen p-4 bg-white'}> <div className={'h-screen p-4 bg-white'}>
<div className={'border-b flex items-center justify-between mb-4 py-2'}> <div className={'border-b flex items-center justify-between mb-4 py-2'}>
<ReloadButton onClick={() => refreshData(page)} />
<div className={'w-3/4 font-semibold text-xl'}>Live Participants</div> <div className={'w-3/4 font-semibold text-xl'}>Live Participants</div>
<Input.Search <Input.Search
allowClear allowClear

View file

@ -0,0 +1,24 @@
import React from 'react';
import { Typography } from 'antd';
function ParticipantOverviewItem({
titleRow,
firstNum,
addedNum,
}: {
titleRow: any;
firstNum?: string;
addedNum?: string;
}) {
return (
<div className={'rounded border p-2 flex-1'}>
<div className={'flex items-center gap-2 mb-2'}>{titleRow}</div>
<div className={'flex items-baseline gap-2'}>
{firstNum ? <Typography.Title level={4}>{firstNum}</Typography.Title> : null}
{addedNum ? <Typography.Text>{addedNum}</Typography.Text> : null}
</div>
</div>
);
}
export default ParticipantOverviewItem;

View file

@ -3,21 +3,26 @@ import { useStore } from 'App/mstore';
import { numberWithCommas } from 'App/utils'; import { numberWithCommas } from 'App/utils';
import { Step } from 'Components/UsabilityTesting/TestEdit'; import { Step } from 'Components/UsabilityTesting/TestEdit';
import { Loader, NoContent, Pagination } from 'UI'; import { Loader, NoContent, Pagination } from 'UI';
import { Button, Typography } from 'antd'; import { Button, Typography, Input } from 'antd';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
const ResponsesOverview = observer(() => { const ResponsesOverview = observer(() => {
const { uxtestingStore } = useStore(); const { uxtestingStore } = useStore();
const [search, setSearch] = React.useState('');
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [showAll, setShowAll] = React.useState(false); const [showAll, setShowAll] = React.useState(false);
const [taskId, setTaskId] = React.useState(uxtestingStore.instance?.tasks[0].taskId); const [taskId, setTaskId] = React.useState(uxtestingStore.instance?.tasks[0].taskId);
React.useEffect(() => { React.useEffect(() => {
// @ts-ignore void refreshData();
uxtestingStore.fetchResponses(uxtestingStore.instance?.testId, taskId, page);
}, [page, taskId]); }, [page, taskId]);
const refreshData = () =>
taskId
? uxtestingStore.fetchResponses(uxtestingStore.instance!.testId!, taskId, page, search)
: null;
const selectedIndex = uxtestingStore.instance?.tasks.findIndex((task) => task.taskId === taskId)!; const selectedIndex = uxtestingStore.instance?.tasks.findIndex((task) => task.taskId === taskId)!;
const task = uxtestingStore.instance?.tasks.find((task) => task.taskId === taskId); const task = uxtestingStore.instance?.tasks.find((task) => task.taskId === taskId);
return ( return (
@ -25,7 +30,7 @@ const ResponsesOverview = observer(() => {
<Typography.Title style={{ marginBottom: 0 }} level={4}> <Typography.Title style={{ marginBottom: 0 }} level={4}>
Open-ended task responses Open-ended task responses
</Typography.Title> </Typography.Title>
<div className={'flex flex-col gap-1'}> <div className={'flex flex-col gap-1 relative'}>
<Typography.Text strong>Select Task / Question</Typography.Text> <Typography.Text strong>Select Task / Question</Typography.Text>
<Step <Step
ind={selectedIndex} ind={selectedIndex}
@ -41,29 +46,52 @@ const ResponsesOverview = observer(() => {
</div> </div>
} }
/> />
{showAll {showAll ? (
? uxtestingStore.instance?.tasks <div
className={
'flex flex-col overflow-auto absolute bottom-0 w-full gap-1 z-20 rounded bg-gray-lightest border shadow'
}
style={{ maxHeight: 300, transform: 'translateY(110%)' }}
>
{uxtestingStore.instance?.tasks
.filter((t) => t.taskId !== taskId && t.allowTyping) .filter((t) => t.taskId !== taskId && t.allowTyping)
.map((task) => ( .map((task) => (
<div className="cursor-pointer" onClick={() => setTaskId(task.taskId)}> <div
className="cursor-pointer"
onClick={() => {
setShowAll(false);
setTaskId(task.taskId);
}}
>
<Step <Step
hover
ind={uxtestingStore.instance?.tasks.findIndex((t) => t.taskId === task.taskId)!} ind={uxtestingStore.instance?.tasks.findIndex((t) => t.taskId === task.taskId)!}
title={task.title} title={task.title}
description={task.description} description={task.description}
/> />
</div> </div>
)) ))}
: null} </div>
) : null}
</div> </div>
<div className={'grid grid-cols-9 border-b'}> <div className={'grid grid-cols-9 border-b py-1'}>
<div className={'col-span-1'}> <div className={'col-span-1'}>
<Typography.Text strong># Response</Typography.Text> <Typography.Text strong># Response</Typography.Text>
</div> </div>
<div className={'col-span-2'}> <div className={'col-span-2'}>
<Typography.Text strong>Participant</Typography.Text> <Typography.Text strong>Participant</Typography.Text>
</div> </div>
<div className={'col-span-6'}> <div className={'col-span-6 flex items-center'}>
<Typography.Text strong>Response (add search text)</Typography.Text> <div style={{ minWidth: 240 }}>
<Typography.Text strong>Response</Typography.Text>
</div>
<Input.Search
allowClear
placeholder={'Filter by keyboard or participant'}
onChange={(e) => setSearch(e.target.value)}
classNames={{ input: '!border-0 focus:!border-0' }}
onSearch={() => refreshData()}
/>
</div> </div>
</div> </div>
<Loader loading={uxtestingStore.isLoading}> <Loader loading={uxtestingStore.isLoading}>
@ -71,13 +99,13 @@ const ResponsesOverview = observer(() => {
show={!uxtestingStore.responses[taskId!]?.list?.length} show={!uxtestingStore.responses[taskId!]?.list?.length}
title={<div className={'col-span-9'}>No data yet</div>} title={<div className={'col-span-9'}>No data yet</div>}
> >
<div className={'grid grid-cols-9 border-b'}> <div>
{uxtestingStore.responses[taskId!]?.list.map((r, i) => ( {uxtestingStore.responses[taskId!]?.list.map((r, i) => (
<> <div className={'grid grid-cols-9 py-2 border-b hover:bg-active-blue'}>
<div className={'col-span-1'}>{i + 10 * (page - 1) + 1}</div> <div className={'col-span-1'}>{i + 10 * (page - 1) + 1}</div>
<div className={'col-span-2'}>{r.user_id || 'Anonymous User'}</div> <div className={'col-span-2'}>{r.user_id || 'Anonymous User'}</div>
<div className={'col-span-6'}>{r.comment}</div> <div className={'col-span-6'}>{r.comment}</div>
</> </div>
))} ))}
</div> </div>
<div className={'p-2 flex items-center justify-between'}> <div className={'p-2 flex items-center justify-between'}>

View file

@ -55,8 +55,12 @@ function TestEdit() {
} }
}); });
} else { } else {
setConclusion(uxtestingStore.instance!.conclusionMessage) if (!uxtestingStore.instance) {
setGuidelines(uxtestingStore.instance!.guidelines) history.push(withSiteId(usabilityTesting(), siteId));
} else {
setConclusion(uxtestingStore.instance!.conclusionMessage);
setGuidelines(uxtestingStore.instance!.guidelines);
}
} }
}, []); }, []);
if (!uxtestingStore.instance) { if (!uxtestingStore.instance) {
@ -111,7 +115,8 @@ function TestEdit() {
} }
}; };
const isPublished = uxtestingStore.instance.status !== undefined && uxtestingStore.instance.status !== 'preview' const isPublished =
uxtestingStore.instance.status !== undefined && uxtestingStore.instance.status !== 'preview';
return ( return (
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}> <div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
<Breadcrumb <Breadcrumb
@ -125,7 +130,7 @@ function TestEdit() {
to: isPublished ? withSiteId(usabilityTestingView(testId), siteId) : undefined, to: isPublished ? withSiteId(usabilityTestingView(testId), siteId) : undefined,
}, },
{ {
label: 'Edit', label: isPublished ? 'Create' : 'Edit',
}, },
]} ]}
/> />
@ -153,7 +158,7 @@ function TestEdit() {
value={newTestTitle} value={newTestTitle}
onChange={(e) => { onChange={(e) => {
setHasChanged(true); setHasChanged(true);
setNewTestTitle(e.target.value) setNewTestTitle(e.target.value);
}} }}
/> />
<Typography.Text strong>Test Objective (optional)</Typography.Text> <Typography.Text strong>Test Objective (optional)</Typography.Text>
@ -161,7 +166,7 @@ function TestEdit() {
value={newTestDescription} value={newTestDescription}
onChange={(e) => { onChange={(e) => {
setHasChanged(true); setHasChanged(true);
setNewTestDescription(e.target.value) setNewTestDescription(e.target.value);
}} }}
placeholder="Share a brief statement about what you aim to discover through this study." placeholder="Share a brief statement about what you aim to discover through this study."
/> />
@ -181,7 +186,7 @@ function TestEdit() {
</div> </div>
<div className={'p-4 rounded bg-white border flex flex-col gap-2'}> <div className={'p-4 rounded bg-white border flex flex-col gap-2'}>
<Typography.Text strong>🏁 Starting point</Typography.Text> <Typography.Title level={5}>🏁 Starting point</Typography.Title>
<Input <Input
style={{ width: 400 }} style={{ width: 400 }}
type={'url'} type={'url'}
@ -197,17 +202,21 @@ function TestEdit() {
</div> </div>
<div className={'p-4 rounded bg-white border flex flex-col gap-2'}> <div className={'p-4 rounded bg-white border flex flex-col gap-2'}>
<Typography.Text strong>📖 Introduction and Guidelines for Participants</Typography.Text> <Typography.Title level={5}>
📖 Introduction and Guidelines for Participants
</Typography.Title>
<Typography.Text></Typography.Text> <Typography.Text></Typography.Text>
{isOverviewEditing ? ( {isOverviewEditing ? (
<Input.TextArea <Input.TextArea
autoFocus autoFocus
rows={5} rows={5}
placeholder={'Enter a brief introduction to the test and its goals here. Follow with clear, step-by-step guidelines for participants.'} placeholder={
'Enter a brief introduction to the test and its goals here. Follow with clear, step-by-step guidelines for participants.'
}
value={guidelines} value={guidelines}
onChange={(e) => { onChange={(e) => {
setHasChanged(true); setHasChanged(true);
setGuidelines(e.target.value) setGuidelines(e.target.value);
}} }}
/> />
) : ( ) : (
@ -247,7 +256,7 @@ function TestEdit() {
</div> </div>
<div className={'p-4 rounded bg-white border flex flex-col gap-2'}> <div className={'p-4 rounded bg-white border flex flex-col gap-2'}>
<Typography.Text strong>📋 Tasks</Typography.Text> <Typography.Title level={5}>📋 Tasks</Typography.Title>
{uxtestingStore.instance!.tasks.map((task, index) => ( {uxtestingStore.instance!.tasks.map((task, index) => (
<Step <Step
ind={index} ind={index}
@ -255,21 +264,26 @@ function TestEdit() {
description={task.description} description={task.description}
buttons={ buttons={
<> <>
<Button size={'small'} disabled={isPublished} icon={<EditOutlined rev={undefined} />} onClick={() => { <Button
showModal( size={'small'}
<StepsModal disabled={isPublished}
editTask={task} icon={<EditOutlined rev={undefined} />}
onHide={hideModal} onClick={() => {
onAdd={(task) => { showModal(
setHasChanged(true); <StepsModal
const tasks = [...uxtestingStore.instance!.tasks]; editTask={task}
tasks[index] = task; onHide={hideModal}
uxtestingStore.instance!.setProperty('tasks', tasks); onAdd={(task) => {
}} setHasChanged(true);
/>, const tasks = [...uxtestingStore.instance!.tasks];
{ right: true, width: 600 } tasks[index] = task;
) uxtestingStore.instance!.setProperty('tasks', tasks);
}} /> }}
/>,
{ right: true, width: 600 }
);
}}
/>
<Button <Button
onClick={() => { onClick={() => {
setHasChanged(true); setHasChanged(true);
@ -315,15 +329,17 @@ function TestEdit() {
</div> </div>
<div className={'p-4 rounded bg-white border flex flex-col gap-2'}> <div className={'p-4 rounded bg-white border flex flex-col gap-2'}>
<Typography.Text strong>🎉 Conclusion Message</Typography.Text> <Typography.Title level={5}>🎉 Conclusion Message</Typography.Title>
<div> <div>
{isConclusionEditing ? ( {isConclusionEditing ? (
<Input.TextArea <Input.TextArea
placeholder={'Enter your closing remarks here, thanking participants for their time and contributions.'} placeholder={
'Enter your closing remarks here, thanking participants for their time and contributions.'
}
value={conclusion} value={conclusion}
onChange={(e) => { onChange={(e) => {
setHasChanged(true); setHasChanged(true);
setConclusion(e.target.value) setConclusion(e.target.value);
}} }}
/> />
) : ( ) : (
@ -374,14 +390,16 @@ export function Step({
ind, ind,
title, title,
description, description,
hover,
}: { }: {
buttons?: React.ReactNode; buttons?: React.ReactNode;
ind: number; ind: number;
title: string; title: string;
description: string | null; description: string | null;
hover?: boolean;
}) { }) {
return ( return (
<div className={'p-4 rounded border bg-active-blue flex items-start gap-2'}> <div className={`p-4 rounded border ${hover ? 'bg-white hover:' : ''}bg-active-blue flex items-start gap-2`}>
<div className={'w-6 h-6 bg-white rounded-full border flex items-center justify-center'}> <div className={'w-6 h-6 bg-white rounded-full border flex items-center justify-center'}>
{ind + 1} {ind + 1}
</div> </div>

View file

@ -3,7 +3,7 @@ import usePageTitle from 'App/hooks/usePageTitle';
import { numberWithCommas } from 'App/utils'; import { numberWithCommas } from 'App/utils';
import { getPdf2 } from 'Components/AssistStats/pdfGenerator'; import { getPdf2 } from 'Components/AssistStats/pdfGenerator';
import { useModal } from 'Components/Modal'; import { useModal } from 'Components/Modal';
import LiveTestsModal from "Components/UsabilityTesting/LiveTestsModal"; import LiveTestsModal from 'Components/UsabilityTesting/LiveTestsModal';
import React from 'react'; import React from 'react';
import { Button, Typography, Select, Space, Popover, Dropdown } from 'antd'; import { Button, Typography, Select, Space, Popover, Dropdown } from 'antd';
import { withSiteId, usabilityTesting, usabilityTestingEdit } from 'App/routes'; import { withSiteId, usabilityTesting, usabilityTestingEdit } from 'App/routes';
@ -33,6 +33,7 @@ import copy from 'copy-to-clipboard';
import { Stage } from 'Components/Funnels/FunnelWidget/FunnelWidget'; import { Stage } from 'Components/Funnels/FunnelWidget/FunnelWidget';
import { confirm } from 'UI'; import { confirm } from 'UI';
import ResponsesOverview from './ResponsesOverview'; import ResponsesOverview from './ResponsesOverview';
import ParticipantOverviewItem from 'Components/UsabilityTesting/ParticipantOverview';
const { Option } = Select; const { Option } = Select;
@ -65,14 +66,15 @@ function TestOverview() {
const { siteId, testId } = useParams(); const { siteId, testId } = useParams();
const { showModal, hideModal } = useModal(); const { showModal, hideModal } = useModal();
const { uxtestingStore } = useStore(); const { uxtestingStore } = useStore();
usePageTitle(`Usability Tests | ${uxtestingStore.instance?.title || ''}`);
React.useEffect(() => { React.useEffect(() => {
uxtestingStore.getTest(testId); uxtestingStore.getTest(testId)
}, [testId]); }, [testId]);
if (!uxtestingStore.instance) { if (!uxtestingStore.instance) {
return <Loader loading={uxtestingStore.isLoading}>No data.</Loader>; return <Loader loading={uxtestingStore.isLoading}>No data.</Loader>;
} else {
document.title = `Usability Tests | ${uxtestingStore.instance.title}`
} }
const onPageChange = (page: number) => { const onPageChange = (page: number) => {
@ -97,16 +99,23 @@ function TestOverview() {
{uxtestingStore.instance.liveCount ? ( {uxtestingStore.instance.liveCount ? (
<div className={'p-4 flex items-center gap-2'}> <div className={'p-4 flex items-center gap-2'}>
<div className={'relative h-4 w-4'}> <div className={'relative h-4 w-4'}>
<div className={'absolute w-4 h-4 animate-ping bg-red rounded-full opacity-75'} /> <div className={'absolute w-4 h-4 animate-ping bg-tealx rounded-full opacity-75'} />
<div className={'absolute w-4 h-4 bg-red rounded-full'} /> <div className={'absolute w-4 h-4 bg-tealx rounded-full'} />
</div> </div>
<Typography.Text> <Typography.Text>
{uxtestingStore.instance.liveCount} participants are engaged in this usability test at {uxtestingStore.instance.liveCount} participants are engaged in this usability test at
the moment. the moment.
</Typography.Text> </Typography.Text>
<Button onClick={() => { <Button
showModal(<LiveTestsModal closeModal={hideModal} testId={testId} />, { right: true, width: 900 }) type={'primary'}
}}> ghost
onClick={() => {
showModal(<LiveTestsModal closeModal={hideModal} testId={testId} />, {
right: true,
width: 900,
});
}}
>
<Space align={'center'}> <Space align={'center'}>
Moderate Real-Time Moderate Real-Time
<ArrowRightOutlined rev={undefined} /> <ArrowRightOutlined rev={undefined} />
@ -123,7 +132,7 @@ function TestOverview() {
Open-ended task responses Open-ended task responses
</Typography.Title> </Typography.Title>
{uxtestingStore.instance.responsesCount ? ( {uxtestingStore.instance.responsesCount ? (
<Button onClick={() => showModal(<ResponsesOverview />, { right: true, width: 900 })}> <Button type={'primary'} ghost onClick={() => showModal(<ResponsesOverview />, { right: true, width: 900 })}>
<Space align={'center'}> <Space align={'center'}>
Review All {uxtestingStore.instance.responsesCount} Responses Review All {uxtestingStore.instance.responsesCount} Responses
<ArrowRightOutlined rev={undefined} /> <ArrowRightOutlined rev={undefined} />
@ -187,70 +196,69 @@ const ParticipantOverview = observer(() => {
<Typography.Title level={5}>Participant Overview</Typography.Title> <Typography.Title level={5}>Participant Overview</Typography.Title>
{uxtestingStore.testStats ? ( {uxtestingStore.testStats ? (
<div className={'flex gap-4'}> <div className={'flex gap-4'}>
<div className={'rounded border p-2 flex-1'}> <ParticipantOverviewItem
<div className={'flex items-center gap-2 mb-2'}> titleRow={
<UserOutlined style={{ fontSize: 18, color: '#394EFF' }} rev={undefined} /> <>
<Typography.Text strong>Total Participants</Typography.Text> <UserOutlined style={{ fontSize: 18, color: '#394EFF' }} rev={undefined} />
</div> <Typography.Text strong>Total Participants</Typography.Text>
<Typography.Title level={4}>{uxtestingStore.testStats.tests_attempts}</Typography.Title> </>
</div> }
<div className={'rounded border p-2 flex-1'}> firstNum={uxtestingStore.testStats.tests_attempts?.toString()}
<div className={'flex items-center gap-2 mb-2'}> />
<CheckCircleOutlined style={{ fontSize: 18, color: '#389E0D' }} rev={undefined} /> <ParticipantOverviewItem
<Typography.Text strong>Completed all tasks</Typography.Text> titleRow={
</div> <>
<div className={'flex items-center gap-2'}> <CheckCircleOutlined style={{ fontSize: 18, color: '#389E0D' }} rev={undefined} />
{uxtestingStore.testStats.tests_attempts > 0 ? ( <Typography.Text strong>Completed all tasks</Typography.Text>
<Typography.Title level={4}> </>
{Math.round( }
firstNum={
uxtestingStore.testStats.tests_attempts > 0
? `${Math.round(
(uxtestingStore.testStats.completed_all_tasks / (uxtestingStore.testStats.completed_all_tasks /
uxtestingStore.testStats.tests_attempts) * uxtestingStore.testStats.tests_attempts) *
100 100
)} )}%`
% : undefined
</Typography.Title> }
) : null} addedNum={uxtestingStore.testStats.completed_all_tasks.toString()}
<Typography.Text>{uxtestingStore.testStats.completed_all_tasks}</Typography.Text> />
</div> <ParticipantOverviewItem
</div> titleRow={
<div className={'rounded border p-2 flex-1'}> <>
<div className={'flex items-center gap-2 mb-2'}> <FastForwardOutlined style={{ fontSize: 18, color: '#874D00' }} rev={undefined} />
<FastForwardOutlined style={{ fontSize: 18, color: '#874D00' }} rev={undefined} /> <Typography.Text strong>Skipped tasks</Typography.Text>
<Typography.Text strong>Skipped tasks</Typography.Text> </>
</div> }
<div className={'flex items-center gap-2'}> firstNum={
{uxtestingStore.testStats.tests_attempts > 0 ? ( uxtestingStore.testStats.tests_attempts > 0
<Typography.Title level={4}> ? `${Math.round(
{Math.round(
(uxtestingStore.testStats.tasks_skipped / (uxtestingStore.testStats.tasks_skipped /
uxtestingStore.testStats.tests_attempts) * uxtestingStore.testStats.tests_attempts) *
100 100
)} )}%`
% : undefined
</Typography.Title> }
) : null} addedNum={uxtestingStore.testStats.tasks_skipped.toString()}
<Typography.Text>{uxtestingStore.testStats.tasks_skipped}</Typography.Text> />
</div> <ParticipantOverviewItem
</div> titleRow={
<div className={'rounded border p-2 flex-1'}> <>
<div className={'flex items-center gap-2 mb-2'}> <UserDeleteOutlined style={{ fontSize: 18, color: '#CC0000' }} rev={undefined} />
<UserDeleteOutlined style={{ fontSize: 18, color: '#CC0000' }} rev={undefined} /> <Typography.Text strong>Aborted the test</Typography.Text>
<Typography.Text strong>Aborted the test</Typography.Text> </>
</div> }
<div className={'flex items-center gap-2'}> firstNum={
{uxtestingStore.testStats.tests_attempts > 0 ? ( uxtestingStore.testStats.tests_attempts > 0
<Typography.Title level={4}> ? `${Math.round(
{Math.round(
(uxtestingStore.testStats.tests_skipped / (uxtestingStore.testStats.tests_skipped /
uxtestingStore.testStats.tests_attempts) * uxtestingStore.testStats.tests_attempts) *
100 100
)} )}%`
% : undefined
</Typography.Title> }
) : null} addedNum={uxtestingStore.testStats.tests_skipped.toString()}
<Typography.Text>{uxtestingStore.testStats.tests_skipped}</Typography.Text> />
</div>
</div>
</div> </div>
) : null} ) : null}
</div> </div>
@ -259,7 +267,11 @@ const ParticipantOverview = observer(() => {
const TaskSummary = observer(() => { const TaskSummary = observer(() => {
const { uxtestingStore } = useStore(); const { uxtestingStore } = useStore();
const totalAttempts = uxtestingStore.testStats?.tests_attempts ?? 0 const [showAll, setShowAll] = React.useState(false);
const totalAttempts = uxtestingStore.testStats?.tests_attempts ?? 0;
const shouldHide = uxtestingStore.taskStats.length > 5
const shownTasks = shouldHide ? showAll ? uxtestingStore.taskStats : uxtestingStore.taskStats.slice(0, 5) : uxtestingStore.taskStats;
return ( return (
<div className={'mt-2 rounded border p-4 bg-white'}> <div className={'mt-2 rounded border p-4 bg-white'}>
<div className={'flex justify-between items-center'}> <div className={'flex justify-between items-center'}>
@ -283,9 +295,14 @@ const TaskSummary = observer(() => {
) : null} ) : null}
</div> </div>
{!uxtestingStore.taskStats.length ? <NoContent show title={'No data'} /> : null} {!uxtestingStore.taskStats.length ? <NoContent show title={'No data'} /> : null}
{uxtestingStore.taskStats.map((tst, index) => ( {shownTasks.map((tst, index) => (
<Stage stage={{ ...tst, isActive: true, skipped: tst.skipped || totalAttempts - tst.completed }} uxt index={index + 1} /> <Stage
stage={{ ...tst, isActive: true, skipped: tst.skipped || totalAttempts - tst.completed }}
uxt
index={index + 1}
/>
))} ))}
{shouldHide ? <div onClick={() => setShowAll(!showAll)} className={'link mt-4'}>{showAll ? 'Hide' : 'Show All'}</div> : null}
</div> </div>
); );
}); });
@ -324,7 +341,10 @@ const Title = observer(({ testId, siteId }: any) => {
return null; return null;
} }
const truncatedDescr = uxtestingStore.instance.description.length > 250 && truncate ? uxtestingStore.instance?.description.substring(0, 250) + '...' : uxtestingStore.instance?.description; const truncatedDescr =
uxtestingStore.instance.description.length > 250 && truncate
? uxtestingStore.instance?.description.substring(0, 250) + '...'
: uxtestingStore.instance?.description;
const redirectToEdit = async () => { const redirectToEdit = async () => {
if ( if (
await confirm({ await confirm({
@ -395,10 +415,12 @@ const Title = observer(({ testId, siteId }: any) => {
<Button icon={<MoreOutlined rev={undefined} />}></Button> <Button icon={<MoreOutlined rev={undefined} />}></Button>
</Dropdown> </Dropdown>
</div> </div>
<div className={'whitespace-pre-wrap'}> <div className={'whitespace-pre-wrap'}>{truncatedDescr}</div>
{truncatedDescr} {uxtestingStore.instance.description.length > 250 ? (
</div> <div className={'link'} onClick={() => setTruncate(!truncate)}>
{uxtestingStore.instance.description.length > 250 ? (<div className={'link'} onClick={() => setTruncate(!truncate)}>{truncate ? 'Show more' : 'Show less'}</div>) : null} {truncate ? 'Show more' : 'Show less'}
</div>
) : null}
</div> </div>
); );
}); });

View file

@ -193,7 +193,9 @@ function Row({ test, siteId }: { test: UxTListEntry, siteId: string }) {
<div className={'grid grid-cols-8 p-4 border-b hover:bg-active-blue cursor-pointer'} onClick={redirect}> <div className={'grid grid-cols-8 p-4 border-b hover:bg-active-blue cursor-pointer'} onClick={redirect}>
<Cell size={4}> <Cell size={4}>
<div className={'flex items-center gap-2'}> <div className={'flex items-center gap-2'}>
<div style={{ minWidth: 40 }}>
<Avatar size={'large'} icon={<UnorderedListOutlined rev={undefined} />} /> <Avatar size={'large'} icon={<UnorderedListOutlined rev={undefined} />} />
</div>
<div style={{ maxWidth: 550 }}> <div style={{ maxWidth: 550 }}>
<Link className='link' to={test.status === 'preview' ? editLink : link}> <Link className='link' to={test.status === 'preview' ? editLink : link}>
{test.title} {test.title}

View file

@ -3,30 +3,43 @@ import { Icon } from 'UI';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
interface Props { interface Props {
items: any items: any;
} }
function Breadcrumb(props: Props) { function Breadcrumb(props: Props) {
const { items } = props; const { items } = props;
return ( return (
<div className="mb-3 flex items-center text-lg"> <div className="mb-3 flex items-center text-lg">
{items.map((item: any, index: any) => { {items.map((item: any, index: any) => {
if (index === items.length - 1) { if (index === items.length - 1) {
return ( return (
<span key={index} className="color-gray-medium capitalize-first">{item.label}</span> <span key={index} className="color-gray-medium capitalize-first">
); {item.label}
} </span>
return ( );
<div key={index} className="color-gray-darkest hover:text-teal group flex items-center"> }
<Link to={item.to} className="flex items-center"> if (item.to === undefined) {
{index === 0 && <Icon name="chevron-left" size={16} className="mr-1 group-hover:fill-teal" />} return (
<span className="capitalize-first">{item.label}</span> <div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
</Link> <span className="color-gray-medium capitalize-first">{item.label}</span>
<span className="mx-2">/</span> <span className="mx-2">/</span>
</div> </div>
); );
})} }
</div> return (
); <div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
<Link to={item.to} className="flex items-center">
{index === 0 && (
<Icon name="chevron-left" size={16} className="mr-1 group-hover:fill-teal" />
)}
<span className="capitalize-first">{item.label}</span>
</Link>
<span className="mx-2">/</span>
</div>
);
})}
</div>
);
} }
export default Breadcrumb; export default Breadcrumb;

View file

@ -134,10 +134,10 @@ export default class UxtestingStore {
this.instance.setProperty('status', status); this.instance.setProperty('status', status);
}; };
fetchResponses = async (testId: number, taskId: number, page: number) => { fetchResponses = async (testId: number, taskId: number, page: number, query?: string) => {
this.setLoading(true); this.setLoading(true);
try { try {
this.responses[taskId] = await this.client.fetchTaskResponses(testId, taskId, page, 10); this.responses[taskId] = await this.client.fetchTaskResponses(testId, taskId, page, 10, query);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {

View file

@ -86,11 +86,11 @@ export default class UxtestingService extends BaseService {
return await r.json(); return await r.json();
} }
async fetchTaskResponses(id: number, task: number, page: number, limit: number) { async fetchTaskResponses(id: number, task: number, page: number, limit: number, query?: string) {
const r = await this.client.get(`${this.prefix}/${id}/responses/${task}`, { const r = await this.client.get(`${this.prefix}/${id}/responses/${task}`, {
page, page,
limit, limit,
// query: 'comment', query,
}); });
const j = await r.json(); const j = await r.json();
return j.data || []; return j.data || [];