Skip to content

Commit

Permalink
Merge pull request #26 from EAVFW/pks/visibility
Browse files Browse the repository at this point in the history
Pks/visibility
  • Loading branch information
pksorensen authored Jun 27, 2024
2 parents 4fe9e65 + 512821d commit f6af055
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 45 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/model/json-definitions/Layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type RowColumnsLayout = {
* If type is unspecified we know its a set of columns.
*/
type?: "row";
order?: number;
columns: ColumnsLayoutDefinition;
}

Expand All @@ -85,5 +86,6 @@ export type ColumnLayout = {
export type QuestionRef = {
style?: React.CSSProperties;
type: "question";
order?: number;
ref: string;
}
8 changes: 6 additions & 2 deletions packages/core/src/state/QuickformReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ export const quickformReducer = (state: QuickformState, action: QuickformAction)
}
}
}


if (!action.intermediate)
state= VisibilityHandler.updateVisibleState(state);;

return state;

}

case 'SET_VALIDATION_RESULT': {
Expand Down Expand Up @@ -114,7 +117,8 @@ export const quickformReducer = (state: QuickformState, action: QuickformAction)
});
}
}

return state;
//DISCUSS WITH KBA - should we not run this in answer insteaad?
return VisibilityHandler.updateVisibleState(state);;
}

Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/state/action-handlers/VisibilityHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { QuestionModel } from "../../model/QuestionModel";
import { resolveQuickFormService } from "../../services";
import { allQuestionsMap, getAllQuestionsWithVisibilityRule } from "../../utils/quickformUtils";
import { QuickformState } from "../QuickformState";

const engines = {} as any;
export type VisibilityRuleEngineContext = {
questions: { [key: string]: QuestionModel };
} & QuickformState;

export type VisibilityRuleEngineHandler = (rule: any, context: VisibilityRuleEngineContext, question: QuestionModel) => boolean;
const engines: { [key: string]: VisibilityRuleEngineHandler } = {} ;

export const registerVisibilityEngine = (type: string, engine: any) => {
engines[type] = engine;
Expand Down Expand Up @@ -36,9 +42,9 @@ export class VisibilityHandler {
try {

if (question.visible.engine in engines) {
result = engines[question.visible.engine](question.visible.rule, context)
result = engines[question.visible.engine](question.visible.rule, context,question)
} else {
result = functionInScope(question.visible?.rule, context);
result = functionInScope(question.visible?.rule, context, question);
}
logger.log("[visibility handler] Result for {question} is {result}", question.logicalName, result);

Expand All @@ -47,6 +53,10 @@ export class VisibilityHandler {
}
hasChanges = hasChanges || question.visible.isVisible !== result;
question.visible.isVisible = result;
if (question.visible.isVisible === false) {
question.answered = false;
question.output = '';
}
}
}
return state;
Expand Down Expand Up @@ -79,7 +89,7 @@ interface Context {
* @param context
* @returns
*/
function functionInScope(js: string, context: Context): boolean {
function functionInScope(js: string, context: any, question: QuestionModel): boolean {
const keys = Object.keys(context);
const values = keys.map(key => context[key]);
const func: Function = new Function(...keys, `return ${js};`);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/quickformUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const allQuestionsMap = (slides: SlideModel[]): { [key: string]: Question
.map(s => s.questions)
.flat()
.reduce((acc, question) => {
acc[question.logicalName] = question;
acc[question.questionKey] = question;
return acc;
}, {} as { [key: string]: QuestionModel });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export const QuickFormSettingsViewHeader: React.FC = () => {
</Field>
}
<VisibilityQueryField />
<Field label="Visible Rule">
<Input value={questions[activeQuestion!]?.visible?.rule ?? ''} required type="text" id={"question-schema-name"} onChange={(e, d) => updateQuickFormPayload(old => { old.questions[activeQuestion!].visible = { engine: "JsEval", rule: d.value }; return { ...old }; })} />
</Field>
{/*<Field label="Visible Rule">*/}
{/* <Input value={JSON.stringify( questions[activeQuestion!]?.visible?.rule ?? '')} required type="text" id={"question-schema-name"} onChange={(e, d) => updateQuickFormPayload(old => { old.questions[activeQuestion!].visible = { engine: "JsEval", rule: d.value }; return { ...old }; })} />*/}
{/*</Field>*/}
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
Expand Down
22 changes: 13 additions & 9 deletions packages/designer/src/Components/Views/QuickFormLayoutView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect, useMemo } from "react";
import { removeNonAlphanumeric } from "@eavfw/utils";
import { SerializedNodes } from "@craftjs/core"
import { QuickFormDesignerDefinition } from "../../Types/QuickFormDefinition";
import { RowColumnsLayout } from "@eavfw/quickform-core/src/model/json-definitions/Layout";



Expand Down Expand Up @@ -78,7 +79,7 @@ export const QuickFormLayoutView = ({ dispatch, slideId, layout }: {
Object.fromEntries(
Object.entries(
layout.slides[slideId]?.rows ?? {
}).map(([rowid, row]) => {
}).sort(([a, aa], [b, bb]) => (aa.order??-1) - (bb.order??-1)).map(([rowid, row]) => {

if (row.type !== "row")
throw new Error("Only Row is supported currently");
Expand Down Expand Up @@ -215,24 +216,27 @@ export const QuickFormLayoutView = ({ dispatch, slideId, layout }: {
if (!slide)
return quickform;

for (let rowid of nodes?.ROOT.nodes) {
slide.rows = Object.fromEntries(nodes?.ROOT.nodes.map((rowid) => {

let row = nodes[rowid];
if (!row.props.questionid)
continue;
return [];

const type = typeof row.type === "string" ? row.type : row.type.resolvedName;
if (type !== "Question")
continue;
return [];


if (!slide.rows)
slide.rows = {};

slide.rows[rowid] = {
return [[rowid, {
...slide.rows?.[rowid] ?? {},
type: "row",
order: nodes?.ROOT.nodes.indexOf(rowid),
columns: { "column1": { type: "question", ref: row.props.questionid } }
};
}
} as RowColumnsLayout]];

}).flat());


return { ...quickform };
});
Expand Down
31 changes: 25 additions & 6 deletions packages/querybuilder/src/VisibilityQueryField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PropsWithChildren, useCallback, useState } from 'react';

import { useQuickFormDefinition } from "@eavfw/quickform-designer";
import { QueryBuilderFluent } from '@react-querybuilder/fluent';
import { QueryBuilder, defaultValidator, defaultOperators, formatQuery, RuleGroupType, RuleType } from 'react-querybuilder';
import { QueryBuilder, defaultValidator, defaultOperators, formatQuery, RuleGroupType, RuleType, Field as QueryField } from 'react-querybuilder';

import { EditRegular } from "@fluentui/react-icons";
import { FieldTypes, InputComponentFieldMetadata, InputComponentMetadata, InputComponentSelectFieldMetadata, QuickformState, resolveInputComponent } from '@eavfw/quickform-core';
Expand Down Expand Up @@ -65,6 +65,11 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode, resetRu
}
}

const operators = [...defaultOperators,
{ name: "is-visible", value: "is-visible", label: "is visible", arity: "unary" },
{ name: "has-product-option", value: "has-product-option", label: "has product option", arity: "unary" },
];


export const VisibilityQueryField = () => {

Expand All @@ -83,9 +88,10 @@ export const VisibilityQueryField = () => {
.filter(hasFieldMetadata)
.map(([qkey, q, metadata]) => {
const type = "type" in metadata.field ? metadata.field.type : metadata.field.typeProvider(q);
return ({
name: q.logicalName!,
label: q.schemaName!,
const result = {
qkey:qkey,
name: qkey,
label: q.schemaName ? qkey: q.text,
valueEditorType: type,
...(isSelectField(metadata, type) ? { values: metadata.field.listValuesProvider(q) } : {})
// label2: `Question ${q.schemaName}`,
Expand All @@ -94,7 +100,9 @@ export const VisibilityQueryField = () => {
// ... (metadata.field.type === "select" ? { listValues: metadata.field.listValuesProvider(q) } : {})
// },
// ...metadata.field,
})
};
console.log("VisibilityQueryField FieldGenerator", [qkey,type, q,result]);
return result;
})

// tree: QbUtils.checkTree(QbUtils.loadTree(queryValue), initial_config),
Expand Down Expand Up @@ -124,6 +132,16 @@ export const VisibilityQueryField = () => {

}));

const getOperators = (fieldName: string, { fieldData }: { fieldData: QueryField }) => {
console.log("getOperators", [fieldName, fieldData]);
if (typeof fieldData.qkey === "string") {
const schema = resolveInputComponent(questions?.[fieldData.qkey]?.inputType??'')?.inputSchema;
if (schema?.label === "Product Collection") {
return operators.filter(x => x.name === "has-product-option")
}
}
return operators;
};

const setOpen = (open: boolean) => setState(old => { old.isOpen = open; return { ...old }; });
console.log("VisibilityQueryField", [isOpen, value, query, fields]);
Expand All @@ -141,8 +159,9 @@ export const VisibilityQueryField = () => {
listsAsArrays
parseNumbers
showNotToggle
getOperators={getOperators}
validator={defaultValidator}
operators={[...defaultOperators, { name: "is-visible", value: "is-visible", label: "is visible", arity: "unary" }]}
operators={operators}
controlClassnames={{ queryBuilder: 'queryBuilder-branches' }}
fields={fields}
query={query}
Expand Down
46 changes: 26 additions & 20 deletions packages/querybuilder/src/reactQueryBuilderVisibilityEngine.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
"use client";
import { resolveInputComponent, type QuickformState } from "@eavfw/quickform-core";
import type { QuestionModel } from "@eavfw/quickform-core/src/model";
import { registerVisibilityEngine } from "@eavfw/quickform-core/src/state/action-handlers/VisibilityHandler";
import { VisibilityRuleEngineContext, registerVisibilityEngine } from "@eavfw/quickform-core/src/state/action-handlers/VisibilityHandler";
import type { RuleGroupType, RuleType, defaultOperators } from "react-querybuilder";

type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;

type operators = ArrayElement<(typeof defaultOperators)>["name"] | "is-visible";
type operators = ArrayElement<(typeof defaultOperators)>["name"] | "is-visible" | "has-product-option";

function isArrayType<T>(obj: T | T[], isArray: boolean): obj is Array<T> {
return isArray;
}
function evalRule(rule: RuleType, context: QuickformState & { questions: { [logicalName: string]: QuestionModel } }): boolean {
function evalRule(rule: RuleType, context: VisibilityRuleEngineContext, currentQuestion: QuestionModel): boolean {

const value = rule.value;
const question = context.questions[rule.field];
const metadata = resolveInputComponent(question.inputType).inputSchema!;
const type = "type" in metadata.field! ? metadata.field.type : metadata.field!.typeProvider(question.inputProperties!);
const targetQuestion = context.questions[rule.field];
const metadata = resolveInputComponent(targetQuestion.inputType).inputSchema!;
const type = "type" in metadata.field! ? metadata.field.type : metadata.field!.typeProvider(targetQuestion.inputProperties!);
const isArray = type === "select" || type === "multiselect";
const isMultiArray = type === "multiselect";
const sourceValue = question.output;

const sourceValue = targetQuestion.output;
console.log("Evaluating Rule " + rule.operator + " " + rule.field, [rule, context, targetQuestion,currentQuestion]);
/**
*
* OBS == comparison is used to compare values, this is not the same as === in javascript. "1" == 1 is true, "1" === 1 is false
* OBSOBS - == not working as "" == 0 is true.
*/
switch (rule.operator as operators) {
case "!=":
return !(question.answered && isArrayType<string>(sourceValue, isMultiArray) && isArrayType(value, isArray) ? isMultiArray ? sourceValue.every(sv => value.indexOf(sv) !== -1) : sourceValue === value : value === sourceValue);
return !(targetQuestion.answered && isArrayType<string>(sourceValue, isMultiArray) && isArrayType(value, isArray) ? isMultiArray ? sourceValue.every(sv => value.indexOf(sv) !== -1) : sourceValue === value : value === sourceValue);
// case "<": return false;
// case "<=": return false;
case "=":
console.log("EvalRule", [question, metadata, type, sourceValue, isMultiArray, value, isArray]);
return question.answered && isArrayType<string>(sourceValue, isMultiArray) && isArrayType(value, isArray) ?
console.log("EvalRule", [targetQuestion, metadata, type, sourceValue, isMultiArray, value, isArray]);
return targetQuestion.answered && isArrayType<string>(sourceValue, isMultiArray) && isArrayType(value, isArray) ?

isMultiArray ? sourceValue.every(sv => value.indexOf(sv) !== -1) : sourceValue == value
: value == sourceValue;
isMultiArray ? sourceValue.every(sv => value.indexOf(sv) !== -1) : value?.toString() === sourceValue?.toString()
: value?.toString() === sourceValue?.toString();
// case ">": return false;
// case ">=": return false;
// case "beginsWith": return false;
Expand All @@ -45,8 +47,12 @@ function evalRule(rule: RuleType, context: QuickformState & { questions: { [logi
// case "doesNotBeginWith": return false;
// case "doesNotEndWith": return false;
// case "endsWith": return false;
case "in": return question.answered && isArrayType<string>(sourceValue, isMultiArray) && isArrayType(value, isArray) ? isMultiArray ? sourceValue.some(sv => value.indexOf(sv) !== -1) : sourceValue.some(sv => sv === value[0]) : value === sourceValue;
case "is-visible": return question.visible?.isVisible ?? false;
case "in": return targetQuestion.answered && isArrayType<string>(sourceValue, isMultiArray) && isArrayType(value, isArray) ? isMultiArray ? sourceValue.some(sv => value.indexOf(sv) !== -1) : sourceValue.some(sv => sv === value[0]) : value === sourceValue;
case "is-visible": return targetQuestion.visible?.isVisible ?? false;
case "has-product-option":
console.log("[visibility handler]", ["has-product-option", rule, context, currentQuestion.inputProperties, JSON.stringify(targetQuestion), sourceValue]);
const inputProperties = currentQuestion.inputProperties as { products: Array<string> };
return inputProperties?.products?.some(p => p === sourceValue) ?? false;
// case "notBetween": return false;
// case "notIn": return false;
// case "notNull": return false;
Expand All @@ -58,15 +64,15 @@ function evalRule(rule: RuleType, context: QuickformState & { questions: { [logi
return false;
}

function evalRuleGroup(rule: RuleGroupType, context: any): boolean {
const rules = rule.rules.map(r => "field" in r ? evalRule(r, context) : evalRuleGroup(r, context));

function evalRuleGroup(rule: RuleGroupType, context: VisibilityRuleEngineContext, question: QuestionModel): boolean {
const rules = rule.rules.map(r => "field" in r ? evalRule(r, context,question) : evalRuleGroup(r, context, question));
console.log("[visibility handler]", [rule, rules]);
return rule.combinator === "and" ? rules.every(x => x) : rules.some(x => x);
}

registerVisibilityEngine("react-querybuilder", (rule: RuleGroupType, context: any) => {
registerVisibilityEngine("react-querybuilder", (rule: RuleGroupType, context: VisibilityRuleEngineContext, question: QuestionModel) => {
console.log("[visibility handler]", [rule, context]);
const result = evalRuleGroup(rule, context);
const result = evalRuleGroup(rule, context, question);

console.log("[visibility handler]", [result]);
return result;
Expand Down

0 comments on commit f6af055

Please sign in to comment.