Implement conditional visibility for examples and tasks (#59)
* Implement conditional visibility for examples * Add filtering for tasks * Update the readme with visibleIf syntax documentation
This commit is contained in:
Родитель
a314e44f31
Коммит
1ac1e258b2
|
@ -86,6 +86,7 @@ If you can't run node locally, there is also an API available. Send your `survey
|
|||
#### Example properties
|
||||
- name: The title of the example. Appears in the help dialog as a heading.
|
||||
- details: The message for the example. HTML is supported.
|
||||
- visibleIf: A logic expression that controls whether or not this example is visible. This can be used to provide contextual examples based on the user's choices in the survey. [Syntax documentation](#syntax-for-visibleif-logic)
|
||||
|
||||
#### Task card properties
|
||||
- message: A message to show for the entire group of tasks.
|
||||
|
@ -94,6 +95,14 @@ If you can't run node locally, there is also an API available. Send your `survey
|
|||
#### Task properties
|
||||
- name: The title of the task.
|
||||
- details: The message for to the task. HTML is supported.
|
||||
- visibleIf: A logic expression that controls whether or not this task is visible. This can be used to provide contextual tasks based on the user's choices in the survey. [Syntax documentation](#syntax-for-visibleif-logic)
|
||||
|
||||
### Syntax for visibleIf logic
|
||||
SurveyJS includes a robust domain-specific language for controlling the flow of the survey. See https://surveyjs.io/Documentation/Library#conditions for the official documentation and examples. Configuring logic in the [survey creator](https://surveyjs.io/create-survey) is a good option if you don't want to handwrite code.
|
||||
|
||||
Here are some basic examples to get you started.
|
||||
- Check if the user selected a certain choice: `"visibleIf": "{questionName} = 'choice'"`
|
||||
- Check if the user **did not** select a choice: `"visibleIf": "{questionName} <> 'choice'"`
|
||||
|
||||
# Contributing
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ interface AppProps {
|
|||
|
||||
// Captures the survey model object upon the first time handleValueChanged is called
|
||||
// Exposes some encapsulated state needed for undo functionality
|
||||
let surveyModel: ReactSurveyModel;
|
||||
export let surveyModel: ReactSurveyModel;
|
||||
|
||||
function createTaskMap(contentData: any) {
|
||||
const questions = surveyModel?.getAllQuestions() ?? [];
|
||||
|
@ -121,7 +121,7 @@ const App: React.FunctionComponent<AppProps> = ({ surveyData, contentData }) =>
|
|||
const scenarioMsg = contentData.taskInstructions?.message;
|
||||
const categories = Array.from(taskMap.keys());
|
||||
const numTasks = categories.length === 0 ? 0 :
|
||||
categories.map(category => taskMap.get(category) as TaskCard[])
|
||||
categories.map(category => TaskCard.filterTasks(taskMap.get(category) ?? []))
|
||||
.flat()
|
||||
.map(card => card.tasks)
|
||||
.map(tasks => tasks.length)
|
||||
|
|
|
@ -13,8 +13,9 @@ interface CollapsibleSectionProps {
|
|||
|
||||
const CollapsibleSection: React.FunctionComponent<CollapsibleSectionProps> = ({ taskMap, category }) => {
|
||||
const [isExpanded, setExpanded] = useState(true);
|
||||
const numTasks = taskMap.get(category)?.map(task => task.tasks.length).reduce((prev, n) => prev + n) ?? 0;
|
||||
const hasMessage = taskMap.get(category)?.find(tc => !!tc.message) != null;
|
||||
const tasks = TaskCard.filterTasks(taskMap.get(category) ?? []);
|
||||
const numTasks = tasks.map(task => task.tasks.length).reduce((prev, n) => prev + n) ?? 0;
|
||||
const hasMessage = tasks.find(tc => !!tc.message) != null;
|
||||
// If there are no tasks and there's no message for the category, then render nothing
|
||||
return (numTasks > 0 || hasMessage) ? (
|
||||
<React.Fragment>
|
||||
|
@ -25,7 +26,7 @@ const CollapsibleSection: React.FunctionComponent<CollapsibleSectionProps> = ({
|
|||
{numTasks}
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded ? taskMap.get(category)?.map(tc => <TaskCardComponent key={tc.id} card={tc} />) : null}
|
||||
{isExpanded ? tasks.map(tc => <TaskCardComponent key={tc.id} card={tc} />) : null}
|
||||
</React.Fragment>
|
||||
) : null;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
import React from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { surveyModel } from '../App'
|
||||
import { ConditionRunner } from 'survey-react'
|
||||
|
||||
interface HelpProps {
|
||||
name: string,
|
||||
|
@ -13,13 +15,26 @@ interface HelpProps {
|
|||
onClose: () => void
|
||||
}
|
||||
|
||||
function filterExamples(examples: Array<any>) {
|
||||
if (surveyModel) {
|
||||
const values = surveyModel.getAllValues();
|
||||
const properties = surveyModel.getFilteredProperties();
|
||||
return examples.filter(ex => new ConditionRunner(ex.visibleIf ?? "true").run(values, properties));
|
||||
} else {
|
||||
console.log("Could not filter examples because surveyModel is null");
|
||||
return examples;
|
||||
}
|
||||
}
|
||||
|
||||
const App: React.FunctionComponent<HelpProps> = ({ name, examples, show, onClose }) => {
|
||||
const body = examples?.map((example, i) => {
|
||||
const visibleExamples = filterExamples(examples);
|
||||
console.log(`Filtered ${visibleExamples.length} visible examples out of ${examples.length} total examples for help=${name}`);
|
||||
const body = visibleExamples?.map((example, i) => {
|
||||
return (
|
||||
<>
|
||||
<h5>{example.name}</h5>
|
||||
<div dangerouslySetInnerHTML={{ __html: example.details }}></div>
|
||||
{i < examples.length-1 ? (<hr style={{ width: "100%", marginTop: "1.5em", marginBottom: "1.5em" }}/>) : null}
|
||||
{i < visibleExamples.length-1 ? (<hr style={{ width: "100%", marginTop: "1.5em", marginBottom: "1.5em" }}/>) : null}
|
||||
</>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -114,11 +114,13 @@
|
|||
"examples": [
|
||||
{
|
||||
"name": "",
|
||||
"details": "Search systems enable users to retrieve information by entering text queries."
|
||||
"details": "Search systems enable users to retrieve information by entering text queries.",
|
||||
"visibleIf": "{systemDomain} = 'search'"
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"details": "<p>Recommender systems enable users to explore various content, products, or services that match their behavior or preferences by entering one or more keywords.</p> <ul> <li>Music playlist generators will ask onboarding users to select a few songs or genres to tailor recommendations.</li> <li>Fitness apps will ask onboarding users to answer questions about lifestyle and routine behavior to tailor recommendations.</li> </ul>"
|
||||
"details": "<p>Recommender systems enable users to explore various content, products, or services that match their behavior or preferences by entering one or more keywords.</p> <ul> <li>Music playlist generators will ask onboarding users to select a few songs or genres to tailor recommendations.</li> <li>Fitness apps will ask onboarding users to answer questions about lifestyle and routine behavior to tailor recommendations.</li> </ul>",
|
||||
"visibleIf": "{systemDomain} = 'recommendation'"
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
|
|
|
@ -4,13 +4,12 @@
|
|||
// This file defines the types used in the application.
|
||||
|
||||
import contentData from '../data/content.json';
|
||||
import { ConditionRunner } from 'survey-react'
|
||||
import { surveyModel } from '../App'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export type HelpLevel = "info" | "warning";
|
||||
|
||||
function getChoice(questionName: string, choiceValue: string) {
|
||||
function getChoiceFromContent(questionName: string, choiceValue: string) {
|
||||
if (questionName == null || choiceValue == null) {
|
||||
console.log("getChoice null args: questionName %s choiceValue %s", questionName, choiceValue);
|
||||
return null;
|
||||
}
|
||||
const metadata: any = contentData.questions.find((q: any) => q.name === questionName);
|
||||
|
@ -47,25 +46,42 @@ export class TaskCard {
|
|||
}
|
||||
|
||||
static fromQuestionChoice(questionName: string, choiceValue: string) {
|
||||
const choice = getChoice(questionName, choiceValue);
|
||||
const choice = getChoiceFromContent(questionName, choiceValue);
|
||||
if (choice == null || choice.taskCard == null || choice.taskCard.tasks == null) {
|
||||
console.log("Null taskcard for question %s choice %s", questionName, choiceValue);
|
||||
return null;
|
||||
}
|
||||
|
||||
const tasks = choice.taskCard.tasks.map((task: any) => { return new Task(task.name, task.details) });
|
||||
const tasks = choice.taskCard.tasks.map((task: any) => { return new Task(task.name, task.details, task.visibleIf) });
|
||||
return new TaskCard(choice.taskCard.title, choice.taskCard.message, questionName, tasks);
|
||||
}
|
||||
|
||||
static filterTasks(taskCards: TaskCard[]) {
|
||||
if (surveyModel) {
|
||||
const values = surveyModel.getAllValues();
|
||||
const properties = surveyModel.getFilteredProperties();
|
||||
const filteredCards: TaskCard[] = [];
|
||||
taskCards.forEach(tc => {
|
||||
const filtered = tc.tasks.filter(task => new ConditionRunner(task.visibleIf ?? "true").run(values, properties))
|
||||
filteredCards.push(new TaskCard(tc.title, tc.message, tc.question, filtered));
|
||||
})
|
||||
return filteredCards;
|
||||
} else {
|
||||
return taskCards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Task {
|
||||
name: string;
|
||||
details: string;
|
||||
visibleIf: string;
|
||||
id: string;
|
||||
|
||||
constructor(name: string, details: string) {
|
||||
constructor(name: string, details: string, visibleIf: string) {
|
||||
this.name = name;
|
||||
this.details = details;
|
||||
this.visibleIf = visibleIf;
|
||||
this.id = uuidv4();
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче