@evoke-platform/ui-components 1.2.1-testing.1 → 1.3.0

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.
@@ -71,7 +71,7 @@ const CustomRuleGroup = (props) => {
71
71
  } }))));
72
72
  };
73
73
  const customButton = (props) => {
74
- const { title, handleOnClick, label, path } = props;
74
+ const { title, handleOnClick, label, path, context } = props;
75
75
  let buttonLabel = label;
76
76
  const nestedConditionLimit = 2;
77
77
  switch (title) {
@@ -82,7 +82,7 @@ const customButton = (props) => {
82
82
  buttonLabel = 'Add Condition';
83
83
  break;
84
84
  }
85
- return (React.createElement(React.Fragment, null, !!path.length && (path.length < nestedConditionLimit || title === 'Add rule') && (React.createElement(Button, { onClick: handleOnClick, startIcon: React.createElement(AddRounded, null), sx: {
85
+ return (React.createElement(React.Fragment, null, !!path.length && (path.length < nestedConditionLimit || title === 'Add rule') && !context.disabled && (React.createElement(Button, { onClick: handleOnClick, startIcon: React.createElement(AddRounded, null), sx: {
86
86
  padding: '6px 16px',
87
87
  fontSize: '0.875rem',
88
88
  marginRight: path.length === 0 ? '.5rem' : '0px',
@@ -116,11 +116,11 @@ const customSelector = (props) => {
116
116
  if (!!context.treeViewOpts && !!context.propertyTreeMap?.[rule.field]) {
117
117
  inputType = context.propertyTreeMap[rule.field].type;
118
118
  }
119
- let readOnly = false;
120
119
  const isTreeViewEnabled = context.treeViewOpts && title === 'Fields';
121
120
  const fetchObject = context.treeViewOpts?.fetchObject;
122
121
  const object = context.treeViewOpts?.object;
123
- if (context.disabledCriteria) {
122
+ let readOnly = context.disabled;
123
+ if (!readOnly && context.disabledCriteria) {
124
124
  readOnly =
125
125
  Object.entries(context.disabledCriteria.criteria).some(([key, value]) => key === rule.field && value === rule.value && rule.operator === '=') && level === context.disabledCriteria.level;
126
126
  const keys = Object.keys(context.disabledCriteria.criteria);
@@ -219,7 +219,7 @@ const customSelector = (props) => {
219
219
  };
220
220
  const customCombinator = (props) => {
221
221
  const { value, handleOnChange, context, level, path } = props;
222
- const isReadOnly = !!context.disabledCriteria && context.disabledCriteria.level - 1 === level;
222
+ const isReadOnly = context.disabled || (!!context.disabledCriteria && context.disabledCriteria.level - 1 === level);
223
223
  const conditionPositionInGroup = path[path.length - 1];
224
224
  const toggleCombinator = () => {
225
225
  const newCombinator = value === 'and' ? 'or' : 'and';
@@ -231,7 +231,7 @@ const customCombinator = (props) => {
231
231
  backgroundColor: isReadOnly ? '#f5f5f5' : '#fff',
232
232
  border: '1px solid #ddd',
233
233
  borderRadius: '8px',
234
- cursor: 'pointer',
234
+ cursor: isReadOnly ? 'cursor' : 'pointer',
235
235
  width: '72px',
236
236
  padding: '5px 10px',
237
237
  fontSize: '16px',
@@ -250,8 +250,8 @@ const customDelete = (props) => {
250
250
  const { handleOnClick, context, level } = props;
251
251
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
252
  const rule = props.ruleOrGroup;
253
- let hideDelete = false;
254
- if (context.disabledCriteria) {
253
+ let hideDelete = context.disabled;
254
+ if (!hideDelete && context.disabledCriteria) {
255
255
  hideDelete =
256
256
  Object.entries(context.disabledCriteria.criteria).some(([key, value]) => key === rule.field && value === rule.value && rule.operator === '=') && level === context.disabledCriteria.level;
257
257
  }
@@ -428,7 +428,7 @@ const CriteriaBuilder = (props) => {
428
428
  background: '#F9FAFB',
429
429
  },
430
430
  '.ruleGroup:hover, .ruleGroup:has(:focus)': {
431
- borderColor: '#0678a9',
431
+ borderColor: disabled ? undefined : '#0678a9',
432
432
  },
433
433
  '.ruleGroup:hover > .ruleGroup-header > .ruleGroup-remove, .ruleGroup:has(:focus) > .ruleGroup-header > .ruleGroup-remove ': {
434
434
  opacity: 1,
@@ -502,6 +502,7 @@ const CriteriaBuilder = (props) => {
502
502
  presetValues,
503
503
  enablePresetValues,
504
504
  presetGroupLabel,
505
+ disabled,
505
506
  disabledCriteria,
506
507
  treeViewOpts: treeViewOpts
507
508
  ? {
@@ -519,7 +520,7 @@ const CriteriaBuilder = (props) => {
519
520
  }, operators: operators
520
521
  ? ALL_OPERATORS.filter((o) => operators.includes(o.name))
521
522
  : ALL_OPERATORS })),
522
- React.createElement(Box, { sx: {
523
+ !disabled && (React.createElement(Box, { sx: {
523
524
  display: 'flex',
524
525
  justifyContent: 'space-between',
525
526
  alignItems: 'center',
@@ -554,7 +555,7 @@ const CriteriaBuilder = (props) => {
554
555
  backgroundColor: 'transparent',
555
556
  },
556
557
  ...styles.buttons,
557
- }, onClick: handleClearAll, title: "Clear all conditions", disabled: isEmpty(query.rules) }, "Clear all"))));
558
+ }, onClick: handleClearAll, title: "Clear all conditions", disabled: isEmpty(query.rules) }, "Clear all")))));
558
559
  }
559
560
  return React.createElement(React.Fragment, null);
560
561
  };
@@ -40,12 +40,12 @@ const ValueEditor = (props) => {
40
40
  // Manages input value for Autocomplete when using 'in/not in' operators, ensuring correct handling on blur.
41
41
  const [inputValue, setInputValue] = useState('');
42
42
  const disabled = ['null', 'notNull'].includes(operator);
43
- const presetValues = context.presetValues ?? [];
43
+ const presetValues = context.presetValues?.filter((val) => !val.type || val.type === inputType) ?? [];
44
44
  const isPresetValue = (value) => value?.startsWith('{{{') && value?.endsWith('}}}');
45
45
  const isPresetValueSelected = presetValues && typeof value === 'string' && isPresetValue(value);
46
46
  const presetDisplayValue = presetValues?.find((option) => option.value.name === value)?.label ?? '';
47
- let readOnly = false;
48
- if (context.disabledCriteria) {
47
+ let readOnly = context.disabled;
48
+ if (!readOnly && context.disabledCriteria) {
49
49
  readOnly =
50
50
  Object.entries(context.disabledCriteria.criteria).some(([key, value]) => key === rule.field && value === rule.value && rule.operator === '=') && level === context.disabledCriteria.level;
51
51
  }
@@ -53,6 +53,7 @@ const ValueEditor = (props) => {
53
53
  input: {
54
54
  width: '33%',
55
55
  background: readOnly ? '#f4f6f8' : '#fff',
56
+ borderRadius: '8px',
56
57
  },
57
58
  };
58
59
  useEffect(() => {
@@ -197,7 +198,7 @@ const ValueEditor = (props) => {
197
198
  isOptionEqualToValue: (option, value) => option === value, renderInput: (params) => (React.createElement(TextField, { label: params.label, ...params, size: "small" })), groupBy: (option) => isPresetValue(option.value?.name) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sx: styles.input, readOnly: readOnly }));
198
199
  }
199
200
  else {
200
- return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: ['null', 'notNull'].includes(operator), onChange: (e) => {
201
+ return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: disabled || ['null', 'notNull'].includes(operator), onChange: (e) => {
201
202
  if (inputType === 'number') {
202
203
  handleOnChange(e.target.value ?? '');
203
204
  }
@@ -26,6 +26,7 @@ export type PresetValue = {
26
26
  label: string;
27
27
  sublabel?: string;
28
28
  };
29
+ type?: string;
29
30
  };
30
31
  export type TreeViewProperty = ObjectProperty & {
31
32
  children?: TreeViewProperty[];
@@ -7,6 +7,7 @@ import '../../../../styles/form-component.css';
7
7
  import { Skeleton, Snackbar } from '../../../core';
8
8
  import { Box } from '../../../layout';
9
9
  import { ButtonComponent, DocumentComponent, FormFieldComponent, ImageComponent, ObjectComponent, RepeatableFieldComponent, UserComponent, ViewOnlyComponent, } from '../FormComponents';
10
+ import { CriteriaComponent } from '../FormComponents/CriteriaComponent/CriteriaComponent';
10
11
  import { addObjectPropertiesToComponentProps, buildComponentPropsFromDocumentProperties, buildComponentPropsFromObjectProperties, convertFormToComponents, getFlattenEntries, getPrefixedUrl, } from '../utils';
11
12
  const usePrevious = (value) => {
12
13
  const ref = useRef();
@@ -43,6 +44,7 @@ export function Form(props) {
43
44
  ViewOnlyManyToManyRepeatableField: RepeatableFieldComponent,
44
45
  ViewOnlyRepeatableField: RepeatableFieldComponent,
45
46
  ViewOnlyImage: ImageComponent,
47
+ ViewOnlyCriteria: CriteriaComponent,
46
48
  ViewOnlyTime: ViewOnlyComponent,
47
49
  };
48
50
  Components.setComponents({
@@ -66,6 +68,7 @@ export function Form(props) {
66
68
  ManyToManyRepeatableField: RepeatableFieldComponent,
67
69
  RepeatableField: RepeatableFieldComponent,
68
70
  Image: ImageComponent,
71
+ Criteria: CriteriaComponent,
69
72
  Time: FormFieldComponent,
70
73
  ...(isReadOnly && viewDetailsComponents),
71
74
  });
@@ -0,0 +1,13 @@
1
+ import { ApiServices, Property } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ type CriteriaProps = {
4
+ value?: CriteriaValue | null;
5
+ handleChange: (propertyId: string, value: CriteriaValue | null) => void;
6
+ property: Property;
7
+ apiServices: ApiServices;
8
+ canUpdateProperty: boolean;
9
+ error: boolean;
10
+ };
11
+ type CriteriaValue = Record<string, unknown>;
12
+ export declare const Criteria: (props: CriteriaProps) => React.JSX.Element;
13
+ export {};
@@ -0,0 +1,64 @@
1
+ import { CircularProgress } from '@mui/material';
2
+ import React, { useCallback, useEffect, useState } from 'react';
3
+ import { Button, Typography } from '../../../../..';
4
+ import { Box } from '../../../../layout';
5
+ import CriteriaBuilder from '../../../CriteriaBuilder';
6
+ import { getPrefixedUrl } from '../../utils';
7
+ export const Criteria = (props) => {
8
+ const { handleChange, property, value, canUpdateProperty, apiServices, error } = props;
9
+ const [properties, setProperties] = useState([]);
10
+ const [loading, setLoading] = useState(false);
11
+ const [loadingError, setLoadingError] = useState(false);
12
+ const fetchProperties = useCallback(async () => {
13
+ if (property.objectId) {
14
+ setLoading(true);
15
+ apiServices.get(getPrefixedUrl(`/objects/${property.objectId}/effective/properties`), { params: { fields: ['properties'] } }, (error, properties) => {
16
+ if (error) {
17
+ console.error('Error fetching object properties', error);
18
+ setLoadingError(true);
19
+ }
20
+ if (properties) {
21
+ setProperties(properties);
22
+ setLoadingError(false);
23
+ }
24
+ setLoading(false);
25
+ });
26
+ }
27
+ }, [property.objectId, apiServices]);
28
+ useEffect(() => {
29
+ fetchProperties();
30
+ }, [fetchProperties]);
31
+ const handleUpdate = (criteria) => {
32
+ handleChange(property.id, criteria ?? null);
33
+ };
34
+ if (loadingError) {
35
+ return (React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
36
+ React.createElement(Typography, { sx: { color: 'rgb(114 124 132)', fontSize: '14px', paddingLeft: '10px' } },
37
+ "An error occurred when retrieving data needed for this criteria.",
38
+ ' '),
39
+ React.createElement(Button, { sx: {
40
+ padding: 0,
41
+ '&:hover': { backgroundColor: 'transparent' },
42
+ minWidth: '44px',
43
+ }, variant: "text", onClick: fetchProperties, disabled: loading }, "Retry"),
44
+ loading && React.createElement(CircularProgress, { size: 20, sx: { paddingLeft: '10px' } })));
45
+ }
46
+ return (React.createElement(Box, { sx: { borderRadius: '8px', border: error ? '1px solid #FF0000' : '1px solid #ddd' } },
47
+ React.createElement(CriteriaBuilder, { criteria: value ?? undefined, properties: properties, setCriteria: handleUpdate, disabled: !canUpdateProperty, hideBorder: true, presetValues: [
48
+ {
49
+ label: 'Current Date',
50
+ value: { name: '{{{currentDate}}}', label: 'Current Date' },
51
+ type: 'date',
52
+ },
53
+ {
54
+ label: 'Current Time',
55
+ value: { name: '{{{currentTime}}}', label: 'Current Time' },
56
+ type: 'time',
57
+ },
58
+ {
59
+ label: 'Current Date Time',
60
+ value: { name: '{{{currentDateTime}}}', label: 'Current Date Time' },
61
+ type: 'date-time',
62
+ },
63
+ ], enablePresetValues: true })));
64
+ };
@@ -0,0 +1,25 @@
1
+ import { ReactComponent } from '@formio/react';
2
+ import { Root } from 'react-dom/client';
3
+ import { BaseFormComponentProps } from '../../types';
4
+ export declare class CriteriaComponent extends ReactComponent {
5
+ [x: string]: any;
6
+ static schema: any;
7
+ component: BaseFormComponentProps & {
8
+ initialValue?: string;
9
+ };
10
+ errorDetails: any;
11
+ componentRoot?: Root;
12
+ constructor(component: BaseFormComponentProps, options: any, data: any);
13
+ init(): void;
14
+ clearErrors(): void;
15
+ handleValidation(): void;
16
+ hasErrors(): boolean;
17
+ errorMessages(): string;
18
+ /**
19
+ * Synchronizes out-of-the-box formio errors with this field's errorDetails object
20
+ */
21
+ manageFormErrors(): void;
22
+ handleChange: (key: string, value: Record<string, unknown> | null) => void;
23
+ beforeSubmit(): void;
24
+ attachReact(element: Element): void;
25
+ }
@@ -0,0 +1,121 @@
1
+ import { ReactComponent } from '@formio/react';
2
+ import { isEmpty } from 'lodash';
3
+ import React from 'react';
4
+ import ReactDOM from 'react-dom';
5
+ import { FormComponentWrapper } from '../../Common';
6
+ import { isPropertyVisible } from '../../utils';
7
+ import { Criteria } from './Criteria';
8
+ export class CriteriaComponent extends ReactComponent {
9
+ constructor(component, options, data) {
10
+ super({
11
+ ...component,
12
+ canUpdateProperty: !component.readOnly,
13
+ hideLabel: true,
14
+ }, options, data);
15
+ this.handleChange = (key, value) => {
16
+ delete this.errorDetails['api-error'];
17
+ // set the value on the form instance at this.root.data
18
+ // does not pick up changes when value is null, so we need to pass an empty string
19
+ this.setValue(value ?? '');
20
+ this.updateValue(value, { modified: true });
21
+ this.handleValidation();
22
+ this.emit('changed-' + this.component.key, value);
23
+ this.attach(this.element);
24
+ if (this.component.autoSave) {
25
+ this.component.autoSave({ [key]: value });
26
+ }
27
+ };
28
+ this.errorDetails = {};
29
+ this.handleChange = this.handleChange.bind(this);
30
+ }
31
+ init() {
32
+ if (this.component.initialValue && /^{{.*}}$/.test(this.component.initialValue)) {
33
+ const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedProperty>[a-zA-Z][a-zA-Z0-9_]*)}}$/;
34
+ const groups = regex.exec(this.component.initialValue)?.groups;
35
+ if (groups?.relatedObjectProperty && groups?.nestedProperty) {
36
+ this.on(`changed-${groups.relatedObjectProperty}`, (value) => {
37
+ if (value) {
38
+ this.setValue(value?.[groups.nestedProperty]);
39
+ this.updateValue(value?.[groups.nestedProperty], { modified: true });
40
+ this.attach(this.element);
41
+ this.attachReact(this.element);
42
+ }
43
+ });
44
+ }
45
+ }
46
+ this.on(`api-error`, (details) => {
47
+ const error = details.find((detail) => detail.code === 'errorMessage' && detail.path.replace('/', '') === this.component.key);
48
+ if (error) {
49
+ if (!this.root.customErrors.find((err) => err.formattedKeyOrPath === this.component.key && err.message === error.message)) {
50
+ this.root.customErrors = [
51
+ ...this.root.customErrors,
52
+ {
53
+ ...error,
54
+ code: 'api-error',
55
+ component: this.component,
56
+ formattedKeyOrPath: this.component.key,
57
+ },
58
+ ];
59
+ }
60
+ this.errorDetails['api-error'] = error?.message;
61
+ }
62
+ else {
63
+ this.root.customErrors = this.root.customErrors.filter((item) => item.formattedKeyOrPath !== this.component.key);
64
+ delete this.errorDetails['api-error'];
65
+ }
66
+ this.attach(this.element);
67
+ this.attachReact(this.element);
68
+ });
69
+ }
70
+ clearErrors() {
71
+ this.errorDetails = {};
72
+ this.root.customErrors = this.root.customErrors.filter((error) => error.formattedKeyOrPath !== this.component.key);
73
+ }
74
+ handleValidation() {
75
+ if (!isPropertyVisible(this.component.conditional, this.root.data)) {
76
+ return;
77
+ }
78
+ // check for out-of-the-box formio errors which store on this.root.errors
79
+ this.checkValidity(this.dataValue, true, this.data);
80
+ this.manageFormErrors();
81
+ }
82
+ hasErrors() {
83
+ return !isEmpty(this.errorDetails);
84
+ }
85
+ errorMessages() {
86
+ return Object.values(this.errorDetails).join(', ');
87
+ }
88
+ /**
89
+ * Synchronizes out-of-the-box formio errors with this field's errorDetails object
90
+ */
91
+ manageFormErrors() {
92
+ const outOfTheBoxError = this.root.errors.find((error) => {
93
+ return error.component.key === this.component.key;
94
+ })?.message;
95
+ // add OoB formio error to errorDetails object to show under field
96
+ if (outOfTheBoxError) {
97
+ this.errorDetails['rootError'] = outOfTheBoxError;
98
+ }
99
+ else {
100
+ delete this.errorDetails['rootError'];
101
+ }
102
+ }
103
+ beforeSubmit() {
104
+ this.handleValidation();
105
+ this.element && this.attach(this.element);
106
+ }
107
+ attachReact(element) {
108
+ let root = ReactDOM.findDOMNode(element);
109
+ if (!root) {
110
+ root = element;
111
+ }
112
+ // FormIO uses id for an enclosing div, so we need to give the input field a different id.
113
+ const inputId = `${this.component.id}-input`;
114
+ /* TODO: You'll see warnings to upgrade to React 18's createRoot();
115
+ * It'll cause issues with: field-level errors not showing up, conditional
116
+ * visibility not working, focus moving out of the form on keypress.
117
+ * Will need to revisit later. Possibly look into using `this.ref` */
118
+ return ReactDOM.render(React.createElement("div", null, !this.component.hidden ? (React.createElement(FormComponentWrapper, { ...this.component, inputId: inputId, viewOnly: !this.component.canUpdateProperty, errorMessage: this.errorMessages() },
119
+ React.createElement(Criteria, { ...this.component, value: this.dataValue, handleChange: this.handleChange, error: this.hasErrors() }))) : null), root);
120
+ }
121
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './ButtonComponent';
2
+ export * from './CriteriaComponent/CriteriaComponent';
2
3
  export * from './DocumentComponent/DocumentComponent';
3
4
  export * from './FormFieldComponent';
4
5
  export * from './ImageComponent/ImageComponent';
@@ -1,4 +1,5 @@
1
1
  export * from './ButtonComponent';
2
+ export * from './CriteriaComponent/CriteriaComponent';
2
3
  export * from './DocumentComponent/DocumentComponent';
3
4
  export * from './FormFieldComponent';
4
5
  export * from './ImageComponent/ImageComponent';
@@ -27,6 +27,7 @@ export function determineComponentType(properties, parameter) {
27
27
  return 'Decimal';
28
28
  case 'richText':
29
29
  return 'RichText';
30
+ case 'criteria':
30
31
  case 'date':
31
32
  case 'integer':
32
33
  case 'image':
@@ -842,6 +843,8 @@ export const buildComponentPropsFromObjectProperties = (properties, objectId, in
842
843
  return 'Image';
843
844
  case 'richText':
844
845
  return 'RichText';
846
+ case 'criteria':
847
+ return 'Criteria';
845
848
  case 'time':
846
849
  return 'Time';
847
850
  case 'date-time':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.2.1-testing.1",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -95,7 +95,7 @@
95
95
  "@dnd-kit/sortable": "^7.0.1",
96
96
  "@emotion/react": "^11.13.5",
97
97
  "@emotion/styled": "^11.8.1",
98
- "@evoke-platform/context": "^1.0.0-dev.126",
98
+ "@evoke-platform/context": "^1.1.0-testing.0",
99
99
  "@formio/react": "^5.2.4-rc.1",
100
100
  "@js-joda/core": "^3.2.0",
101
101
  "@js-joda/locale_en-us": "^3.2.2",