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:
Nicholas King 2021-02-12 13:54:10 -08:00 коммит произвёл GitHub
Родитель a314e44f31
Коммит 1ac1e258b2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 59 добавлений и 16 удалений

Просмотреть файл

@ -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();
}
}