fix(ui): some uxt fixes
This commit is contained in:
parent
552c7e77e1
commit
2e6dd17f0b
10 changed files with 260 additions and 148 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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'}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 || [];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue