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">
{(filter.completed/(filter.completed+filter.skipped))*100}%
{((filter.completed/(filter.completed+filter.skipped))*100).toFixed(1)}%
</div>
</div>
</div>
@ -96,19 +96,22 @@ export function UxTFunnelBar(props: Props) {
<div className={'flex items-center gap-2'}>
<div className="flex items-center">
<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 className={'flex items-center'}>
<Icon name="clock" size="20" color="green" />
<Icon name="clock" size="20" />
<span className="mx-1 font-medium">
{durationFormatted(filter.avgCompletionTime)} Avg. completion time
{durationFormatted(filter.avgCompletionTime)}
</span>
<span>
Avg. completion time
</span>
</div>
</div>
{/* @ts-ignore */}
<div className="flex items-center">
<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>

View file

@ -2,6 +2,7 @@ import React from 'react';
import { useStore } from 'App/mstore';
import { numberWithCommas } from 'App/utils';
import { Input } from 'antd';
import ReloadButton from "Shared/ReloadButton";
import SessionItem from 'Shared/SessionItem';
import { Pagination } from 'UI';
import { observer } from 'mobx-react-lite';
@ -23,6 +24,7 @@ function LiveTestsModal({ testId, closeModal }: { testId: string, closeModal: ()
return (
<div className={'h-screen p-4 bg-white'}>
<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>
<Input.Search
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 { Step } from 'Components/UsabilityTesting/TestEdit';
import { Loader, NoContent, Pagination } from 'UI';
import { Button, Typography } from 'antd';
import { Button, Typography, Input } from 'antd';
import { observer } from 'mobx-react-lite';
import { DownOutlined } from '@ant-design/icons';
const ResponsesOverview = observer(() => {
const { uxtestingStore } = useStore();
const [search, setSearch] = React.useState('');
const [page, setPage] = React.useState(1);
const [showAll, setShowAll] = React.useState(false);
const [taskId, setTaskId] = React.useState(uxtestingStore.instance?.tasks[0].taskId);
React.useEffect(() => {
// @ts-ignore
uxtestingStore.fetchResponses(uxtestingStore.instance?.testId, taskId, page);
void refreshData();
}, [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 task = uxtestingStore.instance?.tasks.find((task) => task.taskId === taskId);
return (
@ -25,7 +30,7 @@ const ResponsesOverview = observer(() => {
<Typography.Title style={{ marginBottom: 0 }} level={4}>
Open-ended task responses
</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>
<Step
ind={selectedIndex}
@ -41,29 +46,52 @@ const ResponsesOverview = observer(() => {
</div>
}
/>
{showAll
? uxtestingStore.instance?.tasks
{showAll ? (
<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)
.map((task) => (
<div className="cursor-pointer" onClick={() => setTaskId(task.taskId)}>
<div
className="cursor-pointer"
onClick={() => {
setShowAll(false);
setTaskId(task.taskId);
}}
>
<Step
hover
ind={uxtestingStore.instance?.tasks.findIndex((t) => t.taskId === task.taskId)!}
title={task.title}
description={task.description}
/>
</div>
))
: null}
))}
</div>
<div className={'grid grid-cols-9 border-b'}>
) : null}
</div>
<div className={'grid grid-cols-9 border-b py-1'}>
<div className={'col-span-1'}>
<Typography.Text strong># Response</Typography.Text>
</div>
<div className={'col-span-2'}>
<Typography.Text strong>Participant</Typography.Text>
</div>
<div className={'col-span-6'}>
<Typography.Text strong>Response (add search text)</Typography.Text>
<div className={'col-span-6 flex items-center'}>
<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>
<Loader loading={uxtestingStore.isLoading}>
@ -71,13 +99,13 @@ const ResponsesOverview = observer(() => {
show={!uxtestingStore.responses[taskId!]?.list?.length}
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) => (
<>
<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-2'}>{r.user_id || 'Anonymous User'}</div>
<div className={'col-span-6'}>{r.comment}</div>
</>
</div>
))}
</div>
<div className={'p-2 flex items-center justify-between'}>

View file

@ -55,8 +55,12 @@ function TestEdit() {
}
});
} else {
setConclusion(uxtestingStore.instance!.conclusionMessage)
setGuidelines(uxtestingStore.instance!.guidelines)
if (!uxtestingStore.instance) {
history.push(withSiteId(usabilityTesting(), siteId));
} else {
setConclusion(uxtestingStore.instance!.conclusionMessage);
setGuidelines(uxtestingStore.instance!.guidelines);
}
}
}, []);
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 (
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
<Breadcrumb
@ -125,7 +130,7 @@ function TestEdit() {
to: isPublished ? withSiteId(usabilityTestingView(testId), siteId) : undefined,
},
{
label: 'Edit',
label: isPublished ? 'Create' : 'Edit',
},
]}
/>
@ -153,7 +158,7 @@ function TestEdit() {
value={newTestTitle}
onChange={(e) => {
setHasChanged(true);
setNewTestTitle(e.target.value)
setNewTestTitle(e.target.value);
}}
/>
<Typography.Text strong>Test Objective (optional)</Typography.Text>
@ -161,7 +166,7 @@ function TestEdit() {
value={newTestDescription}
onChange={(e) => {
setHasChanged(true);
setNewTestDescription(e.target.value)
setNewTestDescription(e.target.value);
}}
placeholder="Share a brief statement about what you aim to discover through this study."
/>
@ -181,7 +186,7 @@ function TestEdit() {
</div>
<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
style={{ width: 400 }}
type={'url'}
@ -197,17 +202,21 @@ function TestEdit() {
</div>
<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>
{isOverviewEditing ? (
<Input.TextArea
autoFocus
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}
onChange={(e) => {
setHasChanged(true);
setGuidelines(e.target.value)
setGuidelines(e.target.value);
}}
/>
) : (
@ -247,7 +256,7 @@ function TestEdit() {
</div>
<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) => (
<Step
ind={index}
@ -255,7 +264,11 @@ function TestEdit() {
description={task.description}
buttons={
<>
<Button size={'small'} disabled={isPublished} icon={<EditOutlined rev={undefined} />} onClick={() => {
<Button
size={'small'}
disabled={isPublished}
icon={<EditOutlined rev={undefined} />}
onClick={() => {
showModal(
<StepsModal
editTask={task}
@ -268,8 +281,9 @@ function TestEdit() {
}}
/>,
{ right: true, width: 600 }
)
}} />
);
}}
/>
<Button
onClick={() => {
setHasChanged(true);
@ -315,15 +329,17 @@ function TestEdit() {
</div>
<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>
{isConclusionEditing ? (
<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}
onChange={(e) => {
setHasChanged(true);
setConclusion(e.target.value)
setConclusion(e.target.value);
}}
/>
) : (
@ -374,14 +390,16 @@ export function Step({
ind,
title,
description,
hover,
}: {
buttons?: React.ReactNode;
ind: number;
title: string;
description: string | null;
hover?: boolean;
}) {
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'}>
{ind + 1}
</div>

View file

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

View file

@ -3,8 +3,9 @@ import { Icon } from 'UI';
import { Link } from 'react-router-dom';
interface Props {
items: any
items: any;
}
function Breadcrumb(props: Props) {
const { items } = props;
return (
@ -12,13 +13,25 @@ function Breadcrumb(props: Props) {
{items.map((item: any, index: any) => {
if (index === items.length - 1) {
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>
);
}
if (item.to === undefined) {
return (
<div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
<span className="color-gray-medium capitalize-first">{item.label}</span>
<span className="mx-2">/</span>
</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" />}
{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>

View file

@ -134,10 +134,10 @@ export default class UxtestingStore {
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);
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) {
console.error(e);
} finally {

View file

@ -86,11 +86,11 @@ export default class UxtestingService extends BaseService {
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}`, {
page,
limit,
// query: 'comment',
query,
});
const j = await r.json();
return j.data || [];