-import { useEditForm } from "@/stores/editForm";
-import { FormControl } from "frappe-ui";
-
-const editFormStore = useEditForm();
-
-const getFieldTypeOptions = () => {
- return [
- "Data",
- "Number",
- "Email",
- "Date",
- "Date Time",
- "Date Range",
- "Time Picker",
- "Password",
- "Select",
- "Switch",
- "Textarea",
- "Text Editor",
- "Attach",
- "Link",
- "Checkbox",
- ];
-};
-
-const getFieldProperties = () => {
- return [
- { label: "Label", type: "text", field: "label", required: true },
- {
- label: "Fieldname",
- type: "text",
- field: "fieldname",
- required: true,
- },
- {
- label: "Fieldtype",
- type: "select",
- field: "fieldtype",
- required: true,
- options: getFieldTypeOptions(),
- },
- { label: "Description", type: "textarea", field: "description", required: false },
- { label: "Mandatory", type: "checkbox", field: "reqd", required: false },
- { label: "Options", type: "textarea", field: "options", required: false },
- { label: "Default", type: "textarea", field: "default", required: false },
- ];
-};
-
-
-
-
Edit Properties
-
-
-
-
-
-
-
-
diff --git a/frontend/src/components/builder/field-editor/ConditionalLogicSection.vue b/frontend/src/components/builder/field-editor/ConditionalLogicSection.vue
new file mode 100644
index 0000000..19f9049
--- /dev/null
+++ b/frontend/src/components/builder/field-editor/ConditionalLogicSection.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+ When
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Then
+
+
+
+
+
+
+
diff --git a/frontend/src/components/builder/field-editor/FieldPropertiesForm.vue b/frontend/src/components/builder/field-editor/FieldPropertiesForm.vue
new file mode 100644
index 0000000..368e0c9
--- /dev/null
+++ b/frontend/src/components/builder/field-editor/FieldPropertiesForm.vue
@@ -0,0 +1,116 @@
+
+
+
+
Edit Properties
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/submission/FormRenderer.vue b/frontend/src/components/submission/FormRenderer.vue
index 08c1a15..e7190a6 100644
--- a/frontend/src/components/submission/FormRenderer.vue
+++ b/frontend/src/components/submission/FormRenderer.vue
@@ -2,6 +2,9 @@
import { ErrorMessage, LoadingIndicator, Button } from "frappe-ui";
import { useSubmissionForm } from "@/stores/submissionForm";
import FieldRenderer from "@/components/builder/FieldRenderer.vue";
+import { computed } from "vue";
+import { shouldFieldBeVisible, shouldFieldBeRequired } from "@/utils/conditionals";
+import type { FormField } from "@/types/formfield";
const submissionFormStore = useSubmissionForm();
@@ -14,6 +17,15 @@ const props = withDefaults(
}
);
+// Computed property to get visible fields based on conditional logic
+// This will automatically update when form values change
+const visibleFields = computed(() => {
+ const fields = submissionFormStore.formResource.data?.fields || [];
+ return fields.filter((field: FormField) =>
+ shouldFieldBeVisible(field, submissionFormStore.fields, fields)
+ );
+});
+
function handleSubmitForm() {
submissionFormStore.submitForm();
}
@@ -23,11 +35,18 @@ function handleSubmitForm() {
-
+
diff --git a/frontend/src/stores/submissionForm.ts b/frontend/src/stores/submissionForm.ts
index 7400720..d51814c 100644
--- a/frontend/src/stores/submissionForm.ts
+++ b/frontend/src/stores/submissionForm.ts
@@ -5,6 +5,10 @@ import { computed, ref } from "vue";
import { FormField } from "@/types/formfield";
import { useStorage } from "@vueuse/core";
import { session } from "@/data/session";
+import {
+ shouldFieldBeRequired,
+ shouldFieldBeVisible,
+} from "@/utils/conditionals";
export type UserSubmission = {
name: string;
@@ -163,8 +167,19 @@ export const useSubmissionForm = defineStore("submissionForm", () => {
function validateValues() {
errors.value = [];
- formResource.value.data.fields.forEach((field: FormField) => {
- if (field.reqd && !fields.value[field.fieldname]) {
+ const allFields = formResource.value.data.fields || [];
+
+ allFields.forEach((field: FormField) => {
+ // Only validate visible fields
+ const isVisible = shouldFieldBeVisible(field, fields.value, allFields);
+ if (!isVisible) {
+ return;
+ }
+
+ // Check if field is required (including conditional requirements)
+ const isRequired = shouldFieldBeRequired(field, fields.value, allFields);
+
+ if (isRequired && !fields.value[field.fieldname]) {
errors.value.push(`${field.label} is required`);
}
});
diff --git a/frontend/src/types/conditional-render.types.ts b/frontend/src/types/conditional-render.types.ts
new file mode 100644
index 0000000..f5733bd
--- /dev/null
+++ b/frontend/src/types/conditional-render.types.ts
@@ -0,0 +1,40 @@
+export enum ConditionalOperators {
+ Is = "Is",
+ IsNot = "Is Not",
+ IsEmpty = "Is Empty",
+ IsNotEmpty = "Is Not Empty",
+ IsSet = "Is Set",
+ Contains = "Contains",
+ DoesNotContain = "Does Not Contain",
+ IsLessThan = "Is Less Than",
+ IsLessThanOrEqualTo = "Is Less Than Or Equal To",
+ IsGreaterThan = "Is Greater Than",
+ IsGreaterThanOrEqualTo = "Is Greater Than Or Equal To",
+ StartsWith = "Starts With",
+ DoesNotStartWith = "Does Not Start With",
+ EndsWith = "Ends With",
+ DoesNotEndWith = "Does Not End With",
+}
+
+export enum LogicOperators {
+ And = "And",
+ Or = "Or",
+}
+
+export type Condition = {
+ fieldname: string;
+ operator: ConditionalOperators;
+ value: string | number | boolean;
+};
+
+export enum Actions {
+ ShowField = "Show Field",
+ HideField = "Hide Field",
+ RequireAnswer = "Require Answer",
+}
+
+export type ConditionalLogic = {
+ conditions: Condition[];
+ action: Actions;
+ target_field: string | null;
+};
diff --git a/frontend/src/types/formfield.ts b/frontend/src/types/formfield.ts
index ae8dc36..513415b 100644
--- a/frontend/src/types/formfield.ts
+++ b/frontend/src/types/formfield.ts
@@ -1,10 +1,30 @@
-export interface FormField {
+export enum FormFieldTypes {
+ Attach = "Attach",
+ Data = "Data",
+ Number = "Number",
+ Email = "Email",
+ Date = "Date",
+ DateTime = "Date Time",
+ DateRange = "Date Range",
+ TimePicker = "Time Picker",
+ Password = "Password",
+ Select = "Select",
+ Switch = "Switch",
+ Textarea = "Textarea",
+ TextEditor = "Text Editor",
+ Link = "Link",
+ Checkbox = "Checkbox",
+ Rating = "Rating",
+}
+
+export type FormField = {
label: string;
fieldname: string;
- fieldtype: string;
+ fieldtype: FormFieldTypes;
description?: string;
reqd?: boolean;
options?: string;
default?: string;
idx?: number;
-}
+ conditional_logic?: string;
+};
diff --git a/frontend/src/utils/conditionals.ts b/frontend/src/utils/conditionals.ts
new file mode 100644
index 0000000..f48bc9e
--- /dev/null
+++ b/frontend/src/utils/conditionals.ts
@@ -0,0 +1,325 @@
+import {
+ ConditionalLogic,
+ ConditionalOperators,
+ Condition,
+ Actions,
+} from "@/types/conditional-render.types";
+import { FormField } from "@/types/formfield";
+
+/**
+ * Parse conditional_logic string into ConditionalLogic object
+ */
+export function parseConditionalLogic(
+ conditionalLogic: string | undefined | null
+): ConditionalLogic | null {
+ if (!conditionalLogic || conditionalLogic.trim() === "") {
+ return null;
+ }
+
+ try {
+ return JSON.parse(conditionalLogic) as ConditionalLogic;
+ } catch (e) {
+ console.error("Failed to parse conditional_logic:", e);
+ return null;
+ }
+}
+
+/**
+ * Get the value of a field from form data, handling different field types
+ */
+function getFieldValue(
+ fieldValue: any,
+ fieldType: string
+): string | number | boolean | null {
+ if (fieldValue === null || fieldValue === undefined || fieldValue === "") {
+ return null;
+ }
+
+ // Handle boolean/switch fields
+ if (fieldType === "Switch" || fieldType === "Checkbox") {
+ return Boolean(fieldValue);
+ }
+
+ // Handle number fields
+ if (fieldType === "Number") {
+ const num = Number(fieldValue);
+ return isNaN(num) ? null : num;
+ }
+
+ // Handle string fields
+ return String(fieldValue);
+}
+
+/**
+ * Normalize values for comparison (convert to strings for most comparisons)
+ */
+function normalizeValue(value: any): string | number | boolean | null {
+ if (value === null || value === undefined) {
+ return null;
+ }
+ if (typeof value === "boolean") {
+ return value;
+ }
+ if (typeof value === "number") {
+ return value;
+ }
+ return String(value).trim();
+}
+
+/**
+ * Evaluate a single condition against form field values
+ */
+function evaluateCondition(
+ condition: Condition,
+ formValues: Record
,
+ allFields: FormField[]
+): boolean {
+ const { fieldname, operator, value } = condition;
+
+ // Get the field definition to understand its type
+ const field = allFields.find((f) => f.fieldname === fieldname);
+ if (!field) {
+ console.warn(`Field ${fieldname} not found in form fields`);
+ return false;
+ }
+
+ const fieldValue = getFieldValue(formValues[fieldname], field.fieldtype);
+ const conditionValue = normalizeValue(value);
+ const normalizedFieldValue = normalizeValue(fieldValue);
+
+ switch (operator) {
+ case ConditionalOperators.Is:
+ return normalizedFieldValue === conditionValue;
+
+ case ConditionalOperators.IsNot:
+ return normalizedFieldValue !== conditionValue;
+
+ case ConditionalOperators.IsEmpty:
+ return (
+ normalizedFieldValue === null ||
+ normalizedFieldValue === "" ||
+ normalizedFieldValue === undefined
+ );
+
+ case ConditionalOperators.IsNotEmpty:
+ return (
+ normalizedFieldValue !== null &&
+ normalizedFieldValue !== "" &&
+ normalizedFieldValue !== undefined
+ );
+
+ case ConditionalOperators.IsSet:
+ return (
+ normalizedFieldValue !== null && normalizedFieldValue !== undefined
+ );
+
+ case ConditionalOperators.Contains:
+ if (normalizedFieldValue === null) return false;
+ return String(normalizedFieldValue)
+ .toLowerCase()
+ .includes(String(conditionValue).toLowerCase());
+
+ case ConditionalOperators.DoesNotContain:
+ if (normalizedFieldValue === null) return true;
+ return !String(normalizedFieldValue)
+ .toLowerCase()
+ .includes(String(conditionValue).toLowerCase());
+
+ case ConditionalOperators.IsLessThan:
+ if (
+ typeof normalizedFieldValue === "number" &&
+ typeof conditionValue === "number"
+ ) {
+ return normalizedFieldValue < conditionValue;
+ }
+ return false;
+
+ case ConditionalOperators.IsLessThanOrEqualTo:
+ if (
+ typeof normalizedFieldValue === "number" &&
+ typeof conditionValue === "number"
+ ) {
+ return normalizedFieldValue <= conditionValue;
+ }
+ return false;
+
+ case ConditionalOperators.IsGreaterThan:
+ if (
+ typeof normalizedFieldValue === "number" &&
+ typeof conditionValue === "number"
+ ) {
+ return normalizedFieldValue > conditionValue;
+ }
+ return false;
+
+ case ConditionalOperators.IsGreaterThanOrEqualTo:
+ if (
+ typeof normalizedFieldValue === "number" &&
+ typeof conditionValue === "number"
+ ) {
+ return normalizedFieldValue >= conditionValue;
+ }
+ return false;
+
+ case ConditionalOperators.StartsWith:
+ if (normalizedFieldValue === null) return false;
+ return String(normalizedFieldValue)
+ .toLowerCase()
+ .startsWith(String(conditionValue).toLowerCase());
+
+ case ConditionalOperators.DoesNotStartWith:
+ if (normalizedFieldValue === null) return true;
+ return !String(normalizedFieldValue)
+ .toLowerCase()
+ .startsWith(String(conditionValue).toLowerCase());
+
+ case ConditionalOperators.EndsWith:
+ if (normalizedFieldValue === null) return false;
+ return String(normalizedFieldValue)
+ .toLowerCase()
+ .endsWith(String(conditionValue).toLowerCase());
+
+ case ConditionalOperators.DoesNotEndWith:
+ if (normalizedFieldValue === null) return true;
+ return !String(normalizedFieldValue)
+ .toLowerCase()
+ .endsWith(String(conditionValue).toLowerCase());
+
+ default:
+ console.warn(`Unknown operator: ${operator}`);
+ return false;
+ }
+}
+
+/**
+ * Evaluate all conditions in a ConditionalLogic object
+ * Currently supports AND logic (all conditions must be true)
+ * TODO: Add support for OR logic if needed
+ */
+function evaluateConditions(
+ conditionalLogic: ConditionalLogic,
+ formValues: Record,
+ allFields: FormField[]
+): boolean {
+ if (
+ !conditionalLogic.conditions ||
+ conditionalLogic.conditions.length === 0
+ ) {
+ return false;
+ }
+
+ // All conditions must be true (AND logic)
+ return conditionalLogic.conditions.every((condition) =>
+ evaluateCondition(condition, formValues, allFields)
+ );
+}
+
+/**
+ * Determine if a field should be visible based on conditional logic rules
+ * defined on other fields that target this field. Visibility is driven by
+ * rules on other fields, not a property on the field itself. A field is visible if:
+ * - No conditional logic rules target it, OR
+ * - At least one "Show Field" action evaluates to true, OR
+ * - No "Hide Field" actions evaluate to true
+ */
+export function shouldFieldBeVisible(
+ field: FormField,
+ formValues: Record,
+ allFields: FormField[]
+): boolean {
+ // Find all conditional logic rules that target this field
+ const targetingRules: ConditionalLogic[] = [];
+
+ allFields.forEach((otherField) => {
+ if (!otherField.conditional_logic) return;
+
+ const logic = parseConditionalLogic(otherField.conditional_logic);
+ if (logic && logic.target_field === field.fieldname) {
+ targetingRules.push(logic);
+ }
+ });
+
+ // If no rules target this field, it's visible
+ if (targetingRules.length === 0) {
+ return true;
+ }
+
+ // Check each rule
+ let hasShowRule = false;
+ let hasHideRule = false;
+
+ for (const rule of targetingRules) {
+ const conditionsMet = evaluateConditions(rule, formValues, allFields);
+
+ if (conditionsMet) {
+ if (rule.action === Actions.ShowField) {
+ hasShowRule = true;
+ } else if (rule.action === Actions.HideField) {
+ hasHideRule = true;
+ }
+ }
+ }
+
+ // If any "Show Field" rule is met, field is visible
+ if (hasShowRule) {
+ return true;
+ }
+
+ // If any "Hide Field" rule is met, field is hidden
+ if (hasHideRule) {
+ return false;
+ }
+
+ // Default: field is visible if no rules are met
+ return true;
+}
+
+/**
+ * Determine if a field should be required based on conditional logic
+ */
+export function shouldFieldBeRequired(
+ field: FormField,
+ formValues: Record,
+ allFields: FormField[]
+): boolean {
+ // Start with the field's base required status
+ let isRequired = field.reqd || false;
+
+ // Find all conditional logic rules that target this field with "Require Answer"
+ const targetingRules: ConditionalLogic[] = [];
+
+ allFields.forEach((otherField) => {
+ if (!otherField.conditional_logic) return;
+
+ const logic = parseConditionalLogic(otherField.conditional_logic);
+ if (
+ logic &&
+ logic.target_field === field.fieldname &&
+ logic.action === Actions.RequireAnswer
+ ) {
+ targetingRules.push(logic);
+ }
+ });
+
+ // If any "Require Answer" rule conditions are met, make field required
+ for (const rule of targetingRules) {
+ if (evaluateConditions(rule, formValues, allFields)) {
+ isRequired = true;
+ break;
+ }
+ }
+
+ return isRequired;
+}
+
+/**
+ * Get all fields that should be visible based on current form values
+ */
+export function getVisibleFields(
+ fields: FormField[],
+ formValues: Record
+): FormField[] {
+ return fields.filter((field) =>
+ shouldFieldBeVisible(field, formValues, fields)
+ );
+}