@evoke-platform/ui-components 1.6.0-dev.16 → 1.6.0-dev.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ import { ActionType, FormEntry } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { FieldErrors, FieldValues, UseFormReturn, UseFormSetValue, UseFormUnregister } from 'react-hook-form';
4
+ type ActionButtonProps = {
5
+ onSubmit: (data: FieldValues) => void;
6
+ handleSubmit: UseFormReturn['handleSubmit'];
7
+ isModal: boolean;
8
+ submitButtonLabel?: string;
9
+ actionType?: ActionType;
10
+ onReset: () => void;
11
+ errors?: FieldErrors;
12
+ unregister: UseFormUnregister<FieldValues>;
13
+ entries: FormEntry[];
14
+ setValue: UseFormSetValue<FieldValues>;
15
+ formId?: string;
16
+ instance?: FieldValues;
17
+ };
18
+ declare function ActionButtons(props: ActionButtonProps): React.JSX.Element;
19
+ export default ActionButtons;
@@ -0,0 +1,106 @@
1
+ import { isEmpty, omit } from 'lodash';
2
+ import React, { useContext, useState } from 'react';
3
+ import { useResponsive } from '../../../../theme';
4
+ import { Button, LoadingButton } from '../../../core';
5
+ import { Box } from '../../../layout';
6
+ import { FormContext } from './FormContext';
7
+ import { entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, scrollIntoViewWithOffset, } from './utils';
8
+ function ActionButtons(props) {
9
+ const { onSubmit, submitButtonLabel, actionType, handleSubmit, onReset, unregister, errors, isModal, entries, setValue, formId, instance, } = props;
10
+ const { isXs } = useResponsive();
11
+ const [isSubmitLoading, setIsSubmitLoading] = useState(false);
12
+ const { getValues } = useContext(FormContext);
13
+ const unregisterHiddenFields = (entriesToCheck) => {
14
+ entriesToCheck.forEach((entry) => {
15
+ if (entry.type === 'sections' || entry.type === 'columns') {
16
+ const subEntries = entry.type === 'sections' ? entry.sections : entry.columns;
17
+ subEntries.forEach((subEntry) => {
18
+ if (subEntry.entries) {
19
+ unregisterHiddenFields(subEntry.entries);
20
+ }
21
+ });
22
+ }
23
+ if (!entryIsVisible(entry, getValues(), instance)) {
24
+ if (entry.type === 'sections' || entry.type === 'columns') {
25
+ const fieldsToUnregister = getNestedParameterIds(entry);
26
+ fieldsToUnregister.forEach(processFieldUnregister);
27
+ }
28
+ else {
29
+ const fieldId = getEntryId(entry);
30
+ if (fieldId)
31
+ processFieldUnregister(fieldId);
32
+ }
33
+ }
34
+ });
35
+ };
36
+ const processFieldUnregister = (fieldId) => {
37
+ if (isAddressProperty(fieldId)) {
38
+ // Unregister entire 'address' to clear hidden field errors, then restore existing values since unregistering address.line1 etc is not working
39
+ let addressValues = getValues('address');
40
+ addressValues = omit(addressValues, fieldId.split('.')[1]);
41
+ unregister('address');
42
+ setValue('address', addressValues);
43
+ }
44
+ else {
45
+ unregister(fieldId);
46
+ }
47
+ };
48
+ async function showErrorsOrSubmit() {
49
+ setIsSubmitLoading(true);
50
+ unregisterHiddenFields(entries ?? []);
51
+ try {
52
+ await handleSubmit((data) => onSubmit(actionType === 'delete' ? {} : data))();
53
+ }
54
+ finally {
55
+ setIsSubmitLoading(false);
56
+ }
57
+ if (!isEmpty(errors)) {
58
+ setTimeout(() => {
59
+ const errorElement = document.getElementById(`validation-error-display-${formId}`);
60
+ let modal = errorElement?.closest('.MuiPaper-root');
61
+ const hasCloseIcon = modal?.querySelector('svg[data-testid="CloseIcon"]');
62
+ modal = hasCloseIcon ? document.querySelector('.MuiDialogContent-root') : modal;
63
+ if (errorElement) {
64
+ scrollIntoViewWithOffset(errorElement, 170, modal);
65
+ }
66
+ }, 0);
67
+ }
68
+ }
69
+ return (React.createElement(Box, { sx: {
70
+ display: 'flex',
71
+ justifyContent: 'flex-end',
72
+ flexWrap: 'wrap-reverse',
73
+ width: '100%',
74
+ } },
75
+ React.createElement(Button, { key: "cancel", variant: "outlined", sx: {
76
+ margin: '5px',
77
+ marginX: isXs ? '0px' : undefined,
78
+ color: 'black',
79
+ border: '1px solid rgb(206, 212, 218)',
80
+ width: isXs ? '100%' : 'auto',
81
+ '&:hover': {
82
+ backgroundColor: '#f2f4f7',
83
+ border: '1px solid rgb(206, 212, 218)',
84
+ },
85
+ }, onClick: () => onReset() }, isModal ? 'Cancel' : 'Discard Changes'),
86
+ React.createElement(LoadingButton, { key: "submit", variant: "contained", sx: {
87
+ lineHeight: '2.75',
88
+ margin: '5px 0 5px 5px',
89
+ marginX: isXs ? '0px' : undefined,
90
+ padding: '0 23px',
91
+ backgroundColor: actionType === 'delete' ? '#A12723' : 'primary',
92
+ borderRadius: '8px',
93
+ boxShadow: 'none',
94
+ whiteSpace: 'nowrap',
95
+ width: isXs ? '100%' : 'auto',
96
+ '& .MuiCircularProgress-root': {
97
+ color: 'white',
98
+ },
99
+ '&:hover': {
100
+ backgroundColor: actionType === 'delete' ? ' #8C2421' : '#014E7B',
101
+ },
102
+ }, onClick: () => {
103
+ showErrorsOrSubmit();
104
+ }, loading: isSubmitLoading }, submitButtonLabel || 'Submit')));
105
+ }
106
+ export default ActionButtons;
@@ -1,6 +1,19 @@
1
1
  import { isArray } from 'lodash';
2
2
  import { DateTime } from 'luxon';
3
3
  import Handlebars from 'no-eval-handlebars';
4
+ function isNumericValidation(validation) {
5
+ return 'minimum' in validation || 'maximum' in validation;
6
+ }
7
+ function isCalendarValidation(validation) {
8
+ return 'to' in validation || 'from' in validation;
9
+ }
10
+ function isStringValidation(validation) {
11
+ return 'operator' in validation;
12
+ }
13
+ function isDocumentValidation(validation) {
14
+ const documentValidationKeys = ['minDocuments', 'maxDocuments', 'errorMessage'];
15
+ return !validation || Object.keys(validation).every((key) => documentValidationKeys.includes(key));
16
+ }
4
17
  export const handleValidation = (entries, register, formValues, parameters, instance) => {
5
18
  entries?.forEach((entry) => {
6
19
  if (entry.type === 'sections' || entry.type === 'columns') {
@@ -51,13 +64,13 @@ export const handleValidation = (entries, register, formValues, parameters, inst
51
64
  };
52
65
  }
53
66
  // Min/max number fields
54
- if (typeof validation.maximum === 'number') {
67
+ if (isNumericValidation(validation) && validation.maximum) {
55
68
  validationRules.max = {
56
69
  value: validation.maximum,
57
70
  message: errorMsg || `${fieldName} must have a value under ${validation.maximum}`,
58
71
  };
59
72
  }
60
- if (typeof validation.minimum === 'number') {
73
+ if (isNumericValidation(validation) && validation.minimum) {
61
74
  validationRules.min = {
62
75
  value: validation.minimum,
63
76
  message: errorMsg || `${fieldName} must have a value over ${validation.minimum}`,
@@ -67,13 +80,12 @@ export const handleValidation = (entries, register, formValues, parameters, inst
67
80
  if (!value)
68
81
  return true;
69
82
  // Document validation
70
- if (validation.maxDocuments || validation.minDocuments) {
83
+ if (isDocumentValidation(validation)) {
71
84
  const amountOfDocuments = isArray(value) ? value.length : 0;
72
- const min = validation.minDocuments;
73
- const max = validation.maxDocuments;
85
+ const min = validation?.minDocuments;
86
+ const max = validation?.maxDocuments;
74
87
  if (max && min && (amountOfDocuments > max || amountOfDocuments < min)) {
75
- return (errorMsg ||
76
- `Please select between ${validation.minDocuments} and ${validation.maxDocuments} document${max > 1 ? 's' : ''}`);
88
+ return errorMsg || `Please select between ${min} and ${max} document${max > 1 ? 's' : ''}`;
77
89
  }
78
90
  else if (min && amountOfDocuments < min) {
79
91
  return errorMsg || `Please select at least ${min} document${min > 1 ? 's' : ''}`;
@@ -83,10 +95,10 @@ export const handleValidation = (entries, register, formValues, parameters, inst
83
95
  }
84
96
  }
85
97
  // Date and Time validation
86
- if (validation.from || validation.to) {
98
+ if (isCalendarValidation(validation)) {
87
99
  const data = {
88
100
  __today__: DateTime.now().toISODate(),
89
- input: { ...instance, ...formValues },
101
+ input: formValues,
90
102
  };
91
103
  if (validation.from) {
92
104
  let earliestAllowed = validation.from;
@@ -110,7 +122,7 @@ export const handleValidation = (entries, register, formValues, parameters, inst
110
122
  }
111
123
  }
112
124
  // Regex validation
113
- if (validation.rules) {
125
+ if (isStringValidation(validation) && validation.rules) {
114
126
  const rules = validation.rules;
115
127
  const failedRules = rules.filter((rule) => {
116
128
  const regex = new RegExp(rule.regex);
@@ -1,5 +1,22 @@
1
- import { Property } from '@evoke-platform/context';
1
+ import { Columns, FormEntry, Property, Sections } from '@evoke-platform/context';
2
+ import { LocalDateTime } from '@js-joda/core';
3
+ import { FieldValues } from 'react-hook-form';
2
4
  import { AutocompleteOption } from '../../../core';
5
+ export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
6
+ export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
7
+ export declare function isAddressProperty(key: string): boolean;
8
+ /**
9
+ * Determine if a form entry is visible or not.
10
+ */
11
+ export declare const entryIsVisible: (entry: FormEntry, formValues: FieldValues, instance?: FieldValues) => boolean;
12
+ /**
13
+ * Recursively retrieves all parameter IDs from a given entry of type Sections or Columns.
14
+ *
15
+ * @param {Sections | Columns} entry - The entry object, which can be of type Sections or Columns.
16
+ * @returns {string[]} - An array of parameter IDs found within the entry.
17
+ */
18
+ export declare const getNestedParameterIds: (entry: Sections | Columns) => string[];
19
+ export declare const getEntryId: (entry: FormEntry) => string | undefined;
3
20
  export declare function getPrefixedUrl(url: string): string;
4
21
  export declare const isOptionEqualToValue: (option: AutocompleteOption | string, value: unknown) => boolean;
5
22
  export declare function addressProperties(addressProperty: Property): Property[];
@@ -1,3 +1,116 @@
1
+ import { LocalDateTime } from '@js-joda/core';
2
+ import jsonLogic from 'json-logic-js';
3
+ import { get, isArray, isObject } from 'lodash';
4
+ export const scrollIntoViewWithOffset = (el, offset, container) => {
5
+ const elementRect = el.getBoundingClientRect();
6
+ const containerRect = container ? container.getBoundingClientRect() : document.body.getBoundingClientRect();
7
+ const topPosition = elementRect.top - containerRect.top + (container?.scrollTop || 0) - offset;
8
+ if (container) {
9
+ container.scrollTo({
10
+ behavior: 'smooth',
11
+ top: topPosition,
12
+ });
13
+ }
14
+ else {
15
+ window.scrollTo({
16
+ behavior: 'smooth',
17
+ top: topPosition,
18
+ });
19
+ }
20
+ };
21
+ export const normalizeDateTime = (dateTime) => new Date(dateTime.toString()).toISOString();
22
+ const evaluateCondition = (condition, formValues, instance) => {
23
+ if (typeof condition !== 'object') {
24
+ console.error('Invalid condition format: ', condition);
25
+ return true;
26
+ }
27
+ if (isArray(condition)) {
28
+ const firstCondition = condition[0];
29
+ const { property, value, operator } = firstCondition;
30
+ let fieldValue = firstCondition.isInstanceProperty ? get(instance, property) : get(formValues, property);
31
+ if (typeof fieldValue === 'object' && fieldValue !== null) {
32
+ if (fieldValue instanceof LocalDateTime) {
33
+ fieldValue = normalizeDateTime(fieldValue);
34
+ }
35
+ else {
36
+ fieldValue = Object.values(fieldValue).includes(value)
37
+ ? value
38
+ : Object.values(fieldValue).find((val) => val !== undefined && val !== null);
39
+ }
40
+ }
41
+ switch (operator) {
42
+ case 'ne':
43
+ return fieldValue != value;
44
+ case 'eq':
45
+ return fieldValue == value;
46
+ default:
47
+ console.error(`Unsupported operator: ${operator}`);
48
+ return false;
49
+ }
50
+ }
51
+ // Handling custom JSON logic
52
+ else {
53
+ const data = {
54
+ data: formValues,
55
+ instance: instance,
56
+ };
57
+ const result = jsonLogic.apply(condition, data);
58
+ return result === true;
59
+ }
60
+ };
61
+ export function isAddressProperty(key) {
62
+ return /\.line1|\.line2|\.city|\.county|\.state|\.zipCode$/.test(key);
63
+ }
64
+ /**
65
+ * Determine if a form entry is visible or not.
66
+ */
67
+ export const entryIsVisible = (entry, formValues, instance) => {
68
+ const display = 'display' in entry ? entry.display : undefined;
69
+ const { visibility } = display ?? ('visibility' in entry ? entry : {});
70
+ if (isObject(visibility) && 'conditions' in visibility && isArray(visibility.conditions)) {
71
+ // visibility is a simple condition
72
+ const { conditions } = visibility;
73
+ return evaluateCondition(conditions, formValues, instance);
74
+ }
75
+ else if (visibility) {
76
+ // visibility is a JSONlogic condition
77
+ return evaluateCondition(visibility, formValues, instance);
78
+ }
79
+ return true;
80
+ };
81
+ /**
82
+ * Recursively retrieves all parameter IDs from a given entry of type Sections or Columns.
83
+ *
84
+ * @param {Sections | Columns} entry - The entry object, which can be of type Sections or Columns.
85
+ * @returns {string[]} - An array of parameter IDs found within the entry.
86
+ */
87
+ export const getNestedParameterIds = (entry) => {
88
+ const parameterIds = [];
89
+ const entries = entry.type === 'columns'
90
+ ? entry.columns.flatMap((column) => column.entries ?? [])
91
+ : entry.sections.flatMap((section) => section.entries ?? []);
92
+ for (const subEntry of entries) {
93
+ if (subEntry.type === 'columns' || subEntry.type === 'sections') {
94
+ parameterIds.push(...getNestedParameterIds(subEntry));
95
+ }
96
+ else {
97
+ const paramId = getEntryId(subEntry);
98
+ if (paramId && paramId !== 'collection') {
99
+ parameterIds.push(paramId);
100
+ }
101
+ }
102
+ }
103
+ return parameterIds;
104
+ };
105
+ export const getEntryId = (entry) => {
106
+ return entry.type === 'input'
107
+ ? entry.parameterId
108
+ : entry.type === 'readonlyField'
109
+ ? entry.propertyId
110
+ : entry.type === 'inputField'
111
+ ? entry.input.id
112
+ : undefined;
113
+ };
1
114
  export function getPrefixedUrl(url) {
2
115
  const wcsMatchers = ['/apps', '/pages', '/widgets'];
3
116
  const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/locations'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.6.0-dev.16",
3
+ "version": "1.6.0-dev.18",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -54,6 +54,7 @@
54
54
  "@testing-library/user-event": "^14.5.2",
55
55
  "@types/flat": "^5.0.5",
56
56
  "@types/jest": "^28.1.4",
57
+ "@types/json-logic-js": "^2.0.8",
57
58
  "@types/luxon": "^3.4.2",
58
59
  "@types/nanoid-dictionary": "^4.2.3",
59
60
  "@types/node": "^18.0.0",