Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"generate-schemas": "yarn generate-schema:StudyConfig && yarn generate-schema:GlobalConfig && yarn generate-schema:LibraryConfig",
"test": "playwright test",
"unittest": "vitest",
"preinstall": "node -e 'if(!/yarn\\.js$/.test(process.env.npm_execpath))throw new Error(\"Use yarn\")'",
"preinstall": "node -e \"if(!/yarn\\.js$/.test(process.env.npm_execpath))throw new Error('Use yarn')\"",
"postinstall": "husky"
},
"lint-staged": {
Expand Down
40 changes: 26 additions & 14 deletions public/demo-html/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@
"studyMetadata": {
"title": "HTML as a Stimulus",
"version": "pilot",
"authors": [
"The reVISit Team"
],
"authors": ["The reVISit Team"],
"date": "2023-04-14",
"description": "A simple demo of using stimuli in an HTML file that renders a D3 visualization. Data is collected via a numeric response field.",
"organizations": [
"University of Utah",
"WPI",
"University of Toronto"
]
"organizations": ["University of Utah", "WPI", "University of Toronto"]
},
"uiConfig": {
"contactEmail": "[email protected]",
Expand All @@ -23,7 +17,29 @@
"withSidebar": true,
"windowEventDebounceTime": 200,
"minHeightSize": 800,
"minWidthSize": 400
"minWidthSize": 400,
"browserRules": {
"allowed": [
{
"name": "chrome",
"minVersion": 100
},
{
"name": "firefox",
"minVersion": 100
},
{
"name": "safari",
"minVersion": 10
}
]
},
"deviceRules": {
"allowed": ["tablet", "desktop"]
},
"inputRules": {
"allowed": ["touch", "mouse"]
}
},
"components": {
"introduction": {
Expand Down Expand Up @@ -61,10 +77,6 @@
},
"sequence": {
"order": "fixed",
"components": [
"introduction",
"barChart",
"external_website"
]
"components": ["introduction", "barChart", "external_website"]
}
}
23 changes: 17 additions & 6 deletions public/demo-survey/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,11 @@
"Option 2",
"Option 3",
"Option 4"
],
"withDivider": true
]
},
{
"id": "divider1",
"type": "divider"
},
{
"id": "matrixHeaderTitle",
Expand All @@ -227,8 +230,11 @@
"type": "likert",
"numItems": 9,
"rightLabel": "Like",
"leftLabel": "Dislike",
"withDivider": true
"leftLabel": "Dislike"
},
{
"id": "divider2",
"type": "divider"
},
{
"id": "q-multi-satisfaction",
Expand Down Expand Up @@ -382,7 +388,8 @@
{
"id": "textField",
"type": "textOnly",
"prompt": "# Randomizing Questions in a Form\n\n This shows how to randomize the order of questions in the form. Notice how the number before each question is different from the order number specified in the text. Note that currently the title is also randomized; you can avoid that by putting the title in a markdown file and adding the questions as a response."
"prompt": "# Randomizing Questions in a Form\n\n This shows how to randomize the order of questions in the form. Notice how the number before each question is different from the order number specified in the text. Note that currently the title is not randomized; you can exclude a form element from being randomized if you need to.",
"excludeFromRandomization": true
},
{
"id": "q-dropdown",
Expand Down Expand Up @@ -645,7 +652,11 @@
"Option 2",
"Option 3"
],
"withDivider": true,
"location": "sidebar"
},
{
"id": "divider",
"type": "divider",
"location": "sidebar"
},
{
Expand Down
8 changes: 6 additions & 2 deletions public/tutorial/_answers/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@
"secondaryText": "1 being the worst health and 5 being the best health",
"numItems": 5,
"rightLabel": "Best health",
"leftLabel": "Worst health",
"withDivider": true
"leftLabel": "Worst health"
},
{
"id": "dividerResponse",
"type": "divider",
"location": "belowStimulus"
},
{
"id": "fruits",
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/individualStudy/summary/ResponseStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function ResponseStats({ visibleParticipants, studyConfig }: { visiblePar
data.push({
component: stat.name,
type: response.type,
question: response.prompt,
question: response.prompt || '',
options: getResponseOptions(response),
correctness: correctnessStr,
});
Expand Down
2 changes: 1 addition & 1 deletion src/analysis/individualStudy/summary/SummaryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function SummaryView({ visibleParticipants, studyConfig }: {
data.push({
component: stat.name,
type: response.type,
question: response.prompt,
question: response.prompt || '',
options: getResponseOptions(response),
correctness: correctnessStr,
});
Expand Down
2 changes: 2 additions & 0 deletions src/components/StepRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ResolutionWarning } from './interface/ResolutionWarning';
import { useFetchStylesheet } from '../utils/fetchStylesheet';
import { ScreenRecordingContext, useScreenRecording } from '../store/hooks/useScreenRecording';
import { ScreenRecordingRejection } from './interface/ScreenRecordingRejection';
import { DeviceWarning } from './interface/DeviceWarning';

export function StepRenderer() {
const windowEvents = useRef<EventType[]>([]);
Expand Down Expand Up @@ -148,6 +149,7 @@ export function StepRenderer() {
{showTitleBar && (
<AppHeader studyNavigatorEnabled={studyNavigatorEnabled} dataCollectionEnabled={dataCollectionEnabled} />
)}
<DeviceWarning />
<ResolutionWarning />
{isScreenRecordingUserRejected && <ScreenRecordingRejection />}
<HelpModal />
Expand Down
4 changes: 2 additions & 2 deletions src/components/interface/AlertModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function AlertModal() {
const storeDispatch = useStoreDispatch();

const [opened, setOpened] = useState(alertModal.show);
const close = useCallback(() => storeDispatch(setAlertModal({ ...alertModal, show: false })), [alertModal, setAlertModal, storeDispatch]);
const close = useCallback(() => storeDispatch(setAlertModal({ ...alertModal, show: false, title: '' })), [alertModal, setAlertModal, storeDispatch]);

useEffect(() => setOpened(alertModal.show), [alertModal.show]);

Expand All @@ -20,7 +20,7 @@ export function AlertModal() {
<Alert
color="red"
radius="xs"
title="Alert"
title={alertModal.title}
icon={<IconAlertCircle />}
onClose={close}
styles={{ root: { backgroundColor: 'unset' } }}
Expand Down
12 changes: 12 additions & 0 deletions src/components/interface/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ export function AppHeader({ studyNavigatorEnabled, dataCollectionEnabled }: { st
}
}, [answers, flatSequence, studyConfig, currentStep, storageEngine, dataCollectionEnabled, funcIndex]);

// Check if we have issues connecting to the database, if so show alert modal
const { setAlertModal } = useStoreActions();
const [firstMount, setFirstMount] = useState(true);
if (storageEngineFailedToConnect && firstMount) {
storeDispatch(setAlertModal({
show: true,
message: `You may be behind a firewall blocking access, or the server collecting data may be down. Study data will not be saved. If you're taking the study you will not be compensated for your efforts. You are welcome to look around. If you are attempting to participate in the study, please email ${studyConfig.uiConfig.contactEmail} for assistance.`,
title: 'Failed to connect to the storage engine',
}));
setFirstMount(false);
}

return (
<AppShell.Header className="header" p="md">
<Grid mt={-7} align="center">
Expand Down
120 changes: 120 additions & 0 deletions src/components/interface/DeviceWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
Modal, Text, Title, Stack, List,
Card,
Flex,
} from '@mantine/core';
import {
IconAlertTriangle, IconBrowser, IconDevices, IconHandClick,
} from '@tabler/icons-react';
import { useStudyConfig } from '../../store/hooks/useStudyConfig';
import { useDeviceRules } from '../../store/hooks/useDeviceRules';

export function DeviceWarning() {
const studyConfig = useStudyConfig();

const { browserRules, deviceRules, inputRules } = studyConfig.uiConfig;

const { isBrowserAllowed, isDeviceAllowed, isInputAllowed } = useDeviceRules(studyConfig.uiConfig);

if (isBrowserAllowed && isDeviceAllowed && isInputAllowed) {
return null;
}

return (
<Modal opened onClose={() => {}} fullScreen withCloseButton={false}>
<Stack align="center" justify="center">
<IconAlertTriangle size={64} color="orange" />
<Title order={3}> Browser or Device Not Supported </Title>

<Flex my="lg" wrap="wrap" justify="center">
{!isBrowserAllowed && (
<Card shadow="sm" padding="lg" radius="md" mx="md" my="md" withBorder w={400}>
<Card.Section bg="gray.3" mb="md" p="md" style={{ display: 'flex', justifyContent: 'center' }}>
<IconBrowser size={48} color="gray" />
</Card.Section>
{browserRules?.blockedMessage
? (
<Text size="md">
{browserRules.blockedMessage}
</Text>
) : (
<>
<Text size="md">
This study only works in the following browser(s):
</Text>
<List ml="md">
{browserRules?.allowed.map((browser, idx) => (
<List.Item key={idx}>
{browser.name}
{browser.minVersion && ` v${browser.minVersion} or later`}
</List.Item>
))}
</List>
</>
)}
</Card>
)}

{!isDeviceAllowed && (
<Card shadow="sm" padding="lg" radius="md" mx="md" my="md" withBorder w={400}>
<Card.Section bg="gray.3" mb="md" p="md" style={{ display: 'flex', justifyContent: 'center' }}>
<IconDevices size={48} color="gray" />
</Card.Section>
{deviceRules?.blockedMessage
? (
<Text size="md">
{deviceRules.blockedMessage}
</Text>
) : (
<>
<Text size="md">
This study only works in the following device(s):
</Text>
<List ml="md">
{deviceRules?.allowed.map((device, idx) => (
<List.Item key={idx}>
{device}
</List.Item>
))}
</List>
</>
)}
</Card>
)}

{!isInputAllowed && (
<Card shadow="sm" padding="lg" radius="md" mx="md" my="md" withBorder w={400}>
<Card.Section bg="gray.3" mb="md" p="md" style={{ display: 'flex', justifyContent: 'center' }}>
<IconHandClick size={48} color="gray" />
</Card.Section>
{inputRules?.blockedMessage
? (
<Text size="md">
{inputRules.blockedMessage}
</Text>
) : (
<>
<Text size="md">
This study only works on devices that support following input type(s):
</Text>
<List ml="md">
{inputRules?.allowed.map((input, idx) => (
<List.Item key={idx}>
{input}
</List.Item>
))}
</List>
</>
)}
</Card>
)}
</Flex>
<Text size="md" ta="center">
Please reopen the study link in one of the browsers/device listed above.
<br />
Thank you for your understanding!
</Text>
</Stack>
</Modal>
);
}
4 changes: 2 additions & 2 deletions src/components/response/ResponseBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function ResponseBlock({
const responses = useMemo(() => allResponses.filter((r) => (r.location ? r.location === location : location === 'belowStimulus')), [allResponses, location]);

const responsesWithDefaults = useMemo(() => responses.map((response) => {
if (response.type !== 'textOnly') {
if (response.type !== 'textOnly' && response.type !== 'divider') {
return {
...response,
required: response.required === undefined ? true : response.required,
Expand All @@ -85,7 +85,7 @@ export function ResponseBlock({
}), [responses]);

const allResponsesWithDefaults = useMemo(() => allResponses.map((response) => {
if (response.type !== 'textOnly') {
if (response.type !== 'textOnly' && response.type !== 'divider') {
return {
...response,
required: response.required === undefined ? true : response.required,
Expand Down
2 changes: 1 addition & 1 deletion src/components/response/ResponseSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export function ResponseSwitcher({
onChange={(event) => { dontKnowCheckbox?.onChange(event.currentTarget.checked); form.onChange(fieldInitialValue); }}
/>
)}
{responseDividers && <Divider mt="xl" mb="xs" />}
{(response.type === 'divider' || responseDividers) && <Divider mt="xl" mb="xs" />}
</Box>
);
}
1 change: 1 addition & 0 deletions src/controllers/ComponentController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function ComponentController() {
storeDispatch(setAlertModal({
show: true,
message: `There was an issue connecting to the ${import.meta.env.VITE_STORAGE_ENGINE} database. This could be caused by a network issue or your adblocker. If you are using an adblocker, please disable it for this website and refresh.`,
title: 'Failed to connect to the storage engine',
}));
}
}, [setAlertModal, storageEngine, storeDispatch]);
Expand Down
Loading