diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index c6a9974ad..3ec8d8031 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -87,7 +87,7 @@ export function UxTFunnelBar(props: Props) { }} >
- {(filter.completed/(filter.completed+filter.skipped))*100}% + {((filter.completed/(filter.completed+filter.skipped))*100).toFixed(1)}%
@@ -96,19 +96,22 @@ export function UxTFunnelBar(props: Props) {
- {filter.completed} completed this step + {filter.completed}completed this step
- + - {durationFormatted(filter.avgCompletionTime)} Avg. completion time + {durationFormatted(filter.avgCompletionTime)} + + + Avg. completion time
{/* @ts-ignore */}
- {filter.skipped} skipped + {filter.skipped} skipped
diff --git a/frontend/app/components/UsabilityTesting/LiveTestsModal.tsx b/frontend/app/components/UsabilityTesting/LiveTestsModal.tsx index a99ffa9ca..b15bee809 100644 --- a/frontend/app/components/UsabilityTesting/LiveTestsModal.tsx +++ b/frontend/app/components/UsabilityTesting/LiveTestsModal.tsx @@ -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 (
+ refreshData(page)} />
Live Participants
+
{titleRow}
+
+ {firstNum ? {firstNum} : null} + {addedNum ? {addedNum} : null} +
+
+ ); +} + +export default ParticipantOverviewItem; diff --git a/frontend/app/components/UsabilityTesting/ResponsesOverview.tsx b/frontend/app/components/UsabilityTesting/ResponsesOverview.tsx index 77fa346d3..3d5b0c5f5 100644 --- a/frontend/app/components/UsabilityTesting/ResponsesOverview.tsx +++ b/frontend/app/components/UsabilityTesting/ResponsesOverview.tsx @@ -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(() => { Open-ended task responses -
+
Select Task / Question {
} /> - {showAll - ? uxtestingStore.instance?.tasks + {showAll ? ( +
+ {uxtestingStore.instance?.tasks .filter((t) => t.taskId !== taskId && t.allowTyping) .map((task) => ( -
setTaskId(task.taskId)}> +
{ + setShowAll(false); + setTaskId(task.taskId); + }} + > t.taskId === task.taskId)!} title={task.title} description={task.description} />
- )) - : null} + ))} +
+ ) : null}
-
+
# Response
Participant
-
- Response (add search text) +
+
+ Response +
+ setSearch(e.target.value)} + classNames={{ input: '!border-0 focus:!border-0' }} + onSearch={() => refreshData()} + />
@@ -71,13 +99,13 @@ const ResponsesOverview = observer(() => { show={!uxtestingStore.responses[taskId!]?.list?.length} title={
No data yet
} > -
+
{uxtestingStore.responses[taskId!]?.list.map((r, i) => ( - <> +
{i + 10 * (page - 1) + 1}
{r.user_id || 'Anonymous User'}
{r.comment}
- +
))}
diff --git a/frontend/app/components/UsabilityTesting/TestEdit.tsx b/frontend/app/components/UsabilityTesting/TestEdit.tsx index d2974e216..04c1dbf58 100644 --- a/frontend/app/components/UsabilityTesting/TestEdit.tsx +++ b/frontend/app/components/UsabilityTesting/TestEdit.tsx @@ -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 (
@@ -153,7 +158,7 @@ function TestEdit() { value={newTestTitle} onChange={(e) => { setHasChanged(true); - setNewTestTitle(e.target.value) + setNewTestTitle(e.target.value); }} /> Test Objective (optional) @@ -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() {
- 🏁 Starting point + 🏁 Starting point
- 📖 Introduction and Guidelines for Participants + + 📖 Introduction and Guidelines for Participants + {isOverviewEditing ? ( { setHasChanged(true); - setGuidelines(e.target.value) + setGuidelines(e.target.value); }} /> ) : ( @@ -247,7 +256,7 @@ function TestEdit() {
- 📋 Tasks + 📋 Tasks {uxtestingStore.instance!.tasks.map((task, index) => ( -
- 🎉 Conclusion Message + 🎉 Conclusion Message
{isConclusionEditing ? ( { 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 ( -
+
{ind + 1}
diff --git a/frontend/app/components/UsabilityTesting/TestOverview.tsx b/frontend/app/components/UsabilityTesting/TestOverview.tsx index 6aa17dc00..7102c9851 100644 --- a/frontend/app/components/UsabilityTesting/TestOverview.tsx +++ b/frontend/app/components/UsabilityTesting/TestOverview.tsx @@ -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 No data.; + } else { + document.title = `Usability Tests | ${uxtestingStore.instance.title}` } const onPageChange = (page: number) => { @@ -97,16 +99,23 @@ function TestOverview() { {uxtestingStore.instance.liveCount ? (
-
-
+
+
{uxtestingStore.instance.liveCount} participants are engaged in this usability test at the moment. -
@@ -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 (
@@ -283,9 +295,14 @@ const TaskSummary = observer(() => { ) : null}
{!uxtestingStore.taskStats.length ? : null} - {uxtestingStore.taskStats.map((tst, index) => ( - + {shownTasks.map((tst, index) => ( + ))} + {shouldHide ?
setShowAll(!showAll)} className={'link mt-4'}>{showAll ? 'Hide' : 'Show All'}
: null}
); }); @@ -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) => {
-
- {truncatedDescr} -
- {uxtestingStore.instance.description.length > 250 ? (
setTruncate(!truncate)}>{truncate ? 'Show more' : 'Show less'}
) : null} +
{truncatedDescr}
+ {uxtestingStore.instance.description.length > 250 ? ( +
setTruncate(!truncate)}> + {truncate ? 'Show more' : 'Show less'} +
+ ) : null}
); }); diff --git a/frontend/app/components/UsabilityTesting/UsabilityTesting.tsx b/frontend/app/components/UsabilityTesting/UsabilityTesting.tsx index e8bc2fc8b..839621d95 100644 --- a/frontend/app/components/UsabilityTesting/UsabilityTesting.tsx +++ b/frontend/app/components/UsabilityTesting/UsabilityTesting.tsx @@ -193,7 +193,9 @@ function Row({ test, siteId }: { test: UxTListEntry, siteId: string }) {
+
} /> +
{test.title} diff --git a/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx b/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx index 7a48dedf5..fc3efdff3 100644 --- a/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx +++ b/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx @@ -3,30 +3,43 @@ import { Icon } from 'UI'; import { Link } from 'react-router-dom'; interface Props { - items: any + items: any; } + function Breadcrumb(props: Props) { - const { items } = props; - return ( -
- {items.map((item: any, index: any) => { - if (index === items.length - 1) { - return ( - {item.label} - ); - } - return ( -
- - {index === 0 && } - {item.label} - - / -
- ); - })} -
- ); + const { items } = props; + return ( +
+ {items.map((item: any, index: any) => { + if (index === items.length - 1) { + return ( + + {item.label} + + ); + } + if (item.to === undefined) { + return ( +
+ {item.label} + / +
+ ); + } + return ( +
+ + {index === 0 && ( + + )} + {item.label} + + / +
+ ); + })} +
+ ); } export default Breadcrumb; diff --git a/frontend/app/mstore/uxtestingStore.ts b/frontend/app/mstore/uxtestingStore.ts index c9a63243b..86812d825 100644 --- a/frontend/app/mstore/uxtestingStore.ts +++ b/frontend/app/mstore/uxtestingStore.ts @@ -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 { diff --git a/frontend/app/services/UxtestingService.ts b/frontend/app/services/UxtestingService.ts index e5e64f268..fea505a23 100644 --- a/frontend/app/services/UxtestingService.ts +++ b/frontend/app/services/UxtestingService.ts @@ -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 || [];