This commit is contained in:
Julien Nahum
2023-12-18 10:35:00 +01:00
parent 7c2db2052a
commit 3b798c12fd
21 changed files with 657 additions and 188 deletions

View File

@@ -0,0 +1,325 @@
export function conditionsMet (conditions, formData) {
if (conditions === undefined || conditions === null) {
return false
}
// If it's not a group, just a single condition
if (conditions.operatorIdentifier === undefined) {
return propertyConditionMet(conditions.value, conditions.value ? formData[conditions.value.property_meta.id] : null)
}
if (conditions.operatorIdentifier === 'and') {
let isvalid = true
conditions.children.forEach(childrenCondition => {
if (!conditionsMet(childrenCondition, formData)) {
isvalid = false
}
})
return isvalid
} else if (conditions.operatorIdentifier === 'or') {
let isvalid = false
conditions.children.forEach(childrenCondition => {
if (conditionsMet(childrenCondition, formData)) {
isvalid = true
}
})
return isvalid
}
throw new Error('Unexcepted operatorIdentifier:' + conditions.operatorIdentifier)
}
function propertyConditionMet (propertyCondition, value) {
if (!propertyCondition) {
return false
}
switch (propertyCondition.property_meta.type) {
case 'text':
case 'url':
case 'email':
case 'phone_number':
return textConditionMet(propertyCondition, value)
case 'number':
return numberConditionMet(propertyCondition, value)
case 'checkbox':
return checkboxConditionMet(propertyCondition, value)
case 'select':
return selectConditionMet(propertyCondition, value)
case 'date':
return dateConditionMet(propertyCondition, value)
case 'multi_select':
return multiSelectConditionMet(propertyCondition, value)
case 'files':
return filesConditionMet(propertyCondition, value)
}
return false
}
function checkEquals (condition, fieldValue) {
return condition.value === fieldValue
}
function checkContains (condition, fieldValue) {
return (fieldValue) ? fieldValue.includes(condition.value) : false
}
function checkListContains (condition, fieldValue) {
if (!fieldValue) return false
if (Array.isArray(condition.value)) {
return condition.value.every(r => fieldValue.includes(r))
} else {
return fieldValue.includes(condition.value)
}
}
function checkStartsWith (condition, fieldValue) {
return fieldValue.startsWith(condition.value)
}
function checkendsWith (condition, fieldValue) {
return fieldValue && fieldValue.endsWith(condition.value)
}
function checkIsEmpty (condition, fieldValue) {
return (!fieldValue || fieldValue.length === 0)
}
function checkGreaterThan (condition, fieldValue) {
return (condition.value && fieldValue && parseFloat(fieldValue) > parseFloat(condition.value))
}
function checkGreaterThanEqual (condition, fieldValue) {
return (condition.value && fieldValue && parseFloat(fieldValue) >= parseFloat(condition.value))
}
function checkLessThan (condition, fieldValue) {
return (condition.value && fieldValue && parseFloat(fieldValue) < parseFloat(condition.value))
}
function checkLessThanEqual (condition, fieldValue) {
return (condition.value && fieldValue && parseFloat(fieldValue) <= parseFloat(condition.value))
}
function checkBefore (condition, fieldValue) {
return (condition.value && fieldValue && fieldValue > condition.value)
}
function checkAfter (condition, fieldValue) {
return (condition.value && fieldValue && fieldValue < condition.value)
}
function checkOnOrBefore (condition, fieldValue) {
return (condition.value && fieldValue && fieldValue >= condition.value)
}
function checkOnOrAfter (condition, fieldValue) {
return (condition.value && fieldValue && fieldValue <= condition.value)
}
function checkPastWeek (condition, fieldValue) {
if (!fieldValue) return false
const fieldDate = new Date(fieldValue)
const today = new Date()
return (fieldDate <= today && fieldDate >= new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7))
}
function checkPastMonth (condition, fieldValue) {
if (!fieldValue) return false
const fieldDate = new Date(fieldValue)
const today = new Date()
return (fieldDate <= today && fieldDate >= new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()))
}
function checkPastYear (condition, fieldValue) {
if (!fieldValue) return false
const fieldDate = new Date(fieldValue)
const today = new Date()
return (fieldDate <= today && fieldDate >= new Date(today.getFullYear() - 1, today.getMonth(), today.getDate()))
}
function checkNextWeek (condition, fieldValue) {
if (!fieldValue) return false
const fieldDate = new Date(fieldValue)
const today = new Date()
return (fieldDate >= today && fieldDate <= new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7))
}
function checkNextMonth (condition, fieldValue) {
if (!fieldValue) return false
const fieldDate = new Date(fieldValue)
const today = new Date()
return (fieldDate >= today && fieldDate <= new Date(today.getFullYear(), today.getMonth() + 1, today.getDate()))
}
function checkNextYear (condition, fieldValue) {
if (!fieldValue) return false
const fieldDate = new Date(fieldValue)
const today = new Date()
return (fieldDate >= today && fieldDate <= new Date(today.getFullYear() + 1, today.getMonth(), today.getDate()))
}
function checkLength (condition, fieldValue, operator = '===') {
if(!fieldValue || fieldValue.length === 0) return false;
switch (operator) {
case '===':
return fieldValue.length === parseInt(condition.value)
case '!==':
return fieldValue.length !== parseInt(condition.value)
case '>':
return fieldValue.length > parseInt(condition.value)
case '>=':
return fieldValue.length >= parseInt(condition.value)
case '<':
return fieldValue.length < parseInt(condition.value)
case '<=':
return fieldValue.length <= parseInt(condition.value)
}
return false
}
function textConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'equals':
return checkEquals(propertyCondition, value)
case 'does_not_equal':
return !checkEquals(propertyCondition, value)
case 'contains':
return checkContains(propertyCondition, value)
case 'does_not_contain':
return !checkContains(propertyCondition, value)
case 'starts_with':
return checkStartsWith(propertyCondition, value)
case 'ends_with':
return checkendsWith(propertyCondition, value)
case 'is_empty':
return checkIsEmpty(propertyCondition, value)
case 'is_not_empty':
return !checkIsEmpty(propertyCondition, value)
case 'content_length_equals':
return checkLength(propertyCondition, value, '===')
case 'content_length_does_not_equal':
return checkLength(propertyCondition, value, '!==')
case 'content_length_greater_than':
return checkLength(propertyCondition, value, '>')
case 'content_length_greater_than_or_equal_to':
return checkLength(propertyCondition, value, '>=')
case 'content_length_less_than':
return checkLength(propertyCondition, value, '<')
case 'content_length_less_than_or_equal_to':
return checkLength(propertyCondition, value, '<=')
}
return false
}
function numberConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'equals':
return checkEquals(propertyCondition, value)
case 'does_not_equal':
return !checkEquals(propertyCondition, value)
case 'greater_than':
return checkGreaterThan(propertyCondition, value)
case 'less_than':
return checkLessThan(propertyCondition, value)
case 'greater_than_or_equal_to':
return checkGreaterThanEqual(propertyCondition, value)
case 'less_than_or_equal_to':
return checkLessThanEqual(propertyCondition, value)
case 'is_empty':
return checkIsEmpty(propertyCondition, value)
case 'is_not_empty':
return checkIsEmpty(propertyCondition, value)
case 'content_length_equals':
return checkLength(propertyCondition, value, '===')
case 'content_length_does_not_equal':
return checkLength(propertyCondition, value, '!==')
case 'content_length_greater_than':
return checkLength(propertyCondition, value, '>')
case 'content_length_greater_than_or_equal_to':
return checkLength(propertyCondition, value, '>=')
case 'content_length_less_than':
return checkLength(propertyCondition, value, '<')
case 'content_length_less_than_or_equal_to':
return checkLength(propertyCondition, value, '<=')
}
return false
}
function checkboxConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'equals':
return checkEquals(propertyCondition, value)
case 'does_not_equal':
return !checkEquals(propertyCondition, value)
}
return false
}
function selectConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'equals':
return checkEquals(propertyCondition, value)
case 'does_not_equal':
return !checkEquals(propertyCondition, value)
case 'is_empty':
return checkIsEmpty(propertyCondition, value)
case 'is_not_empty':
return !checkIsEmpty(propertyCondition, value)
}
return false
}
function dateConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'equals':
return checkEquals(propertyCondition, value)
case 'before':
return checkBefore(propertyCondition, value)
case 'after':
return checkAfter(propertyCondition, value)
case 'on_or_before':
return checkOnOrBefore(propertyCondition, value)
case 'on_or_after':
return checkOnOrAfter(propertyCondition, value)
case 'is_empty':
return checkIsEmpty(propertyCondition, value)
case 'past_week':
return checkPastWeek(propertyCondition, value)
case 'past_month':
return checkPastMonth(propertyCondition, value)
case 'past_year':
return checkPastYear(propertyCondition, value)
case 'next_week':
return checkNextWeek(propertyCondition, value)
case 'next_month':
return checkNextMonth(propertyCondition, value)
case 'next_year':
return checkNextYear(propertyCondition, value)
}
return false
}
function multiSelectConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'contains':
return checkListContains(propertyCondition, value)
case 'does_not_contain':
return !checkListContains(propertyCondition, value)
case 'is_empty':
return checkIsEmpty(propertyCondition, value)
case 'is_not_empty':
return !checkIsEmpty(propertyCondition, value)
}
return false
}
function filesConditionMet (propertyCondition, value) {
switch (propertyCondition.operator) {
case 'is_empty':
return checkIsEmpty(propertyCondition, value)
case 'is_not_empty':
return !checkIsEmpty(propertyCondition, value)
}
return false
}

View File

@@ -0,0 +1,60 @@
import { conditionsMet } from './FormLogicConditionChecker'
class FormLogicPropertyResolver {
conditionsMet = conditionsMet;
property = null;
formData = null;
logic = false;
constructor (property, formData) {
this.property = property
this.formData = formData
this.logic = (property.logic !== undefined) ? property.logic : false
}
isHidden () {
if (!this.logic) {
return this.property.hidden
}
const conditionsMet = this.conditionsMet(this.logic.conditions, this.formData)
if (conditionsMet && this.property.hidden && this.logic.actions.length > 0 && this.logic.actions.includes('show-block')) {
return false
} else if (conditionsMet && !this.property.hidden && this.logic.actions.length > 0 && this.logic.actions.includes('hide-block')) {
return true
} else {
return this.property.hidden
}
}
isRequired () {
if (!this.logic) {
return this.property.required
}
const conditionsMet = this.conditionsMet(this.logic.conditions, this.formData)
if (conditionsMet && this.property.required && this.logic.actions.length > 0 && this.logic.actions.includes('make-it-optional')) {
return false
} else if (conditionsMet && !this.property.required && this.logic.actions.length > 0 && this.logic.actions.includes('require-answer')) {
return true
} else {
return this.property.required
}
}
isDisabled () {
if (!this.logic) {
return this.property.disabled
}
const conditionsMet = this.conditionsMet(this.logic.conditions, this.formData)
if (conditionsMet && this.property.disabled && this.logic.actions.length > 0 && this.logic.actions.includes('enable-block')) {
return false
} else if (conditionsMet && !this.property.disabled && this.logic.actions.length > 0 && this.logic.actions.includes('disable-block')) {
return true
} else {
return this.property.disabled
}
}
}
export default FormLogicPropertyResolver

View File

@@ -0,0 +1,123 @@
import OpenFilters from '../../data/open_filters.json'
class FormPropertyLogicRule {
property = null
logic = null
isConditionCorrect = true
isActionCorrect = true
ACTIONS_VALUES = [
'show-block',
'hide-block',
'make-it-optional',
'require-answer',
'enable-block',
'disable-block'
]
CONDITION_MAPPING = OpenFilters
constructor (property) {
this.property = property
this.logic = (property.logic !== undefined && property.logic) ? property.logic : null
}
isValid () {
if (this.logic && this.logic['conditions']) {
this.checkConditions(this.logic['conditions'])
this.checkActions((this.logic && this.logic['actions']) ? this.logic['actions'] : null)
}
return this.isConditionCorrect && this.isActionCorrect
}
checkConditions (conditions) {
if (conditions && conditions['operatorIdentifier']) {
if ((conditions['operatorIdentifier'] !== 'and') && (conditions['operatorIdentifier'] !== 'or')) {
this.isConditionCorrect = false
return
}
if (conditions['operatorIdentifier']['children'] !== undefined || !Array.isArray(conditions['children'])) {
this.isConditionCorrect = false
return
}
conditions['children'].forEach(childrenCondition => {
this.checkConditions(childrenCondition)
})
} else if (conditions && conditions['identifier']) {
this.checkBaseCondition(conditions)
}
}
checkBaseCondition (condition) {
if (condition['value'] === undefined ||
condition['value']['property_meta'] === undefined ||
condition['value']['property_meta']['type'] === undefined ||
condition['value']['operator'] === undefined ||
condition['value']['value'] === undefined
) {
this.isConditionCorrect = false
return
}
const typeField = condition['value']['property_meta']['type']
const operator = condition['value']['operator']
const value = condition['value']['value']
if (this.CONDITION_MAPPING[typeField] === undefined ||
this.CONDITION_MAPPING[typeField]['comparators'][operator] === undefined
) {
this.isConditionCorrect = false
return
}
const type = this.CONDITION_MAPPING[typeField]['comparators'][operator]['expected_type']
if (Array.isArray(type)) {
let foundCorrectType = false
type.forEach(subtype => {
if (this.valueHasCorrectType(subtype, value)) {
foundCorrectType = true
}
})
if (!foundCorrectType) {
this.isConditionCorrect = false
}
} else {
if (!this.valueHasCorrectType(type, value)) {
this.isConditionCorrect = false
}
}
}
valueHasCorrectType (type, value) {
if (
(type === 'string' && typeof value !== 'string') ||
(type === 'boolean' && typeof value !== 'boolean') ||
(type === 'number' && typeof value !== 'number') ||
(type === 'object' && !Array.isArray(value))
) {
return false
}
return true
}
checkActions (conditions) {
if (Array.isArray(conditions) && conditions.length > 0) {
conditions.forEach(val => {
if (this.ACTIONS_VALUES.indexOf(val) === -1 ||
(['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image'].indexOf(this.property["type"]) > -1 && ['hide-block', 'show-block'].indexOf(val) === -1) ||
(this.property["hidden"] !== undefined && this.property["hidden"] && ['show-block', 'require-answer'].indexOf(val) === -1) ||
(this.property["required"] !== undefined && this.property["required"] && ['make-it-optional', 'hide-block', 'disable-block'].indexOf(val) === -1) ||
(this.property["disabled"] !== undefined && this.property["disabled"] && ['enable-block', 'require-answer', 'make-it-optional'].indexOf(val) === -1)
) {
this.isActionCorrect = false
return
}
})
} else {
this.isActionCorrect = false
}
}
}
export default FormPropertyLogicRule

123
client/lib/forms/form-themes.js vendored Normal file
View File

@@ -0,0 +1,123 @@
/**
Input classes for each supported form themes
*/
export const themes = {
default: {
default: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full py-2 px-4 bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
help: 'text-gray-400 dark:text-gray-500'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg filter hover:brightness-110'
},
CodeInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden',
help: 'text-gray-400 dark:text-gray-500'
},
RichTextAreaInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2',
help: 'text-gray-400 dark:text-gray-500'
},
SelectInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'relative w-full rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full px-4 bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent',
help: 'text-gray-400 dark:text-gray-500'
},
ScaleInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
button: 'cursor-pointer text-gray-700 inline-block rounded-lg border-gray-300 px-4 py-2 flex-grow dark:bg-notion-dark-light dark:text-gray-300 text-center',
unselectedButton: 'bg-white hover:bg-gray-50 border',
help: 'text-gray-400 dark:text-gray-500'
},
fileInput: {
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded-lg',
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded-lg shadow-sm max-w-[10rem]'
}
},
simple: {
default: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full py-2 px-2 bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:border-transparent focus:ring-opacity-100',
help: 'text-gray-400 dark:text-gray-500'
},
Button: {
body: 'transition ease-in duration-200 text-center font-semibold focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-110'
},
SelectInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'relative w-full flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full px-2 bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 text-base focus:outline-none focus:ring-2 focus:border-transparent',
help: 'text-gray-400 dark:text-gray-500'
},
CodeInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'border border-gray-300 dark:border-gray-600 overflow-hidden',
help: 'text-gray-400 dark:text-gray-500'
},
RichTextAreaInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
input: 'border-transparent flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2',
help: 'text-gray-400 dark:text-gray-500'
},
ScaleInput: {
label: 'text-gray-700 dark:text-gray-300 font-semibold',
button: 'flex-1 appearance-none border-gray-300 dark:border-gray-600 w-full py-2 px-2 bg-gray-50 text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 text-center',
unselectedButton: 'bg-white hover:bg-gray-50 border -mx-4',
help: 'text-gray-400 dark:text-gray-500'
},
fileInput: {
input: 'min-h-40 border border-dashed border-gray-300 p-4',
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light shadow-sm max-w-[10rem]'
}
},
notion: {
default: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full py-2 px-2 bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion',
help: 'text-notion-input-help dark:text-gray-500'
},
Button: {
body: 'rounded-md transition ease-in duration-200 text-center font-semibold shadow shadow-inner-notion focus:outline-none focus:ring-2 focus:ring-offset-2 filter hover:brightness-110'
},
SelectInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded relative w-full border-transparent flex-1 appearance-none bg-notion-input-background shadow-inner-notion w-full px-2 text-gray-900 placeholder-gray-400 dark:bg-notion-dark-light dark:placeholder-gray-500 text-base focus:outline-none focus:ring-0 focus:border-transparent focus:shadow-focus-notion',
help: 'text-notion-input-help dark:text-gray-500'
},
CodeInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded shadow-inner-notion border border-gray-300 dark:border-gray-600 overflow-hidden',
help: 'text-notion-input-help dark:text-gray-500'
},
RichTextAreaInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
input: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion border border-gray-300 dark:border-gray-600 w-full text-gray-900 bg-notion-input-background dark:bg-notion-dark-light shadow-inner dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:ring-opacity-100 focus:border-transparent focus:ring-0 focus:shadow-focus-notion',
help: 'text-notion-input-help dark:text-gray-500'
},
ScaleInput: {
label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4',
button: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full py-2 px-2 bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 text-center',
unselectedButton: 'bg-notion-input-background dark:bg-notion-dark-light hover:bg-gray-50 border',
help: 'text-notion-input-help dark:text-gray-500'
},
fileInput: {
input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded bg-notion-input-background',
inputHover: {
light: 'bg-neutral-50',
dark: 'bg-notion-dark-light'
},
uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded shadow-sm max-w-[10rem]'
}
}
}