@orchestrator-ui/orchestrator-ui-components 5.5.2 → 5.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestrator-ui/orchestrator-ui-components",
3
- "version": "5.5.2",
3
+ "version": "5.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Library of UI Components used to display the workflow orchestrator frontend",
6
6
  "author": {
@@ -48,7 +48,7 @@
48
48
  "next-query-params": "^5.0.0",
49
49
  "object-hash": "^3.0.0",
50
50
  "prism-themes": "^1.9.0",
51
- "pydantic-forms": "^0.7.4",
51
+ "pydantic-forms": "^0.8.0",
52
52
  "react-diff-view": "^3.2.0",
53
53
  "react-draggable": "^4.4.6",
54
54
  "react-redux": "^9.1.2",
@@ -686,7 +686,6 @@ export function WfoUserInputForm({
686
686
  </em>
687
687
  </section>
688
688
  )}
689
-
690
689
  {renderButtons(buttons)}
691
690
  </AutoForm>
692
691
  </AutoFieldProvider>
@@ -21,6 +21,11 @@ export const getAcceptFieldStyles = ({ theme }: WfoTheme) => {
21
21
  marginTop: 0,
22
22
  },
23
23
  },
24
+
25
+ '.labelTitle': {
26
+ fontWeight: '600',
27
+ color: theme.colors.text,
28
+ },
24
29
  },
25
30
  });
26
31
 
@@ -27,3 +27,4 @@ export * from './deprecated/FileUploadField';
27
27
  export * from './commonStyles';
28
28
  export * from './types';
29
29
  export * from './SummaryFieldStyling';
30
+ export * from './AcceptFieldStyling';
@@ -8,7 +8,12 @@ import React from 'react';
8
8
  import { useTranslations } from 'next-intl';
9
9
  import { usePydanticFormContext } from 'pydantic-forms';
10
10
 
11
+ import { getCommonFormFieldStyles } from '@/components';
12
+ import { useWithOrchestratorTheme } from '@/hooks';
13
+
11
14
  export const RenderFormErrors = () => {
15
+ const { errorStyle } = useWithOrchestratorTheme(getCommonFormFieldStyles);
16
+
12
17
  const { errorDetails } = usePydanticFormContext();
13
18
  const t = useTranslations('pydanticForms.userInputForm');
14
19
  if (!errorDetails) {
@@ -22,7 +27,7 @@ export const RenderFormErrors = () => {
22
27
  const otherErrors = errors.filter((err) => !err.loc.includes('__root__'));
23
28
 
24
29
  return (
25
- <em className="error backend-validation-metadata">
30
+ <em css={errorStyle}>
26
31
  {rootError && <div>{rootError.msg}</div>}
27
32
  {otherErrors?.length >= 1 &&
28
33
  t('inputFieldsHaveValidationErrors', {
@@ -341,6 +341,7 @@ export const WfoPydanticForm = ({
341
341
  labelProvider: pydanticLabelProvider,
342
342
  rowRenderer: Row,
343
343
  customTranslations: customTranslations,
344
+ loadingComponent: <WfoLoading />,
344
345
  locale: getLocale(),
345
346
  };
346
347
  }, [
@@ -353,10 +354,9 @@ export const WfoPydanticForm = ({
353
354
 
354
355
  return (
355
356
  <PydanticForm
356
- id={processName}
357
+ formKey={processName}
357
358
  onSuccess={onSuccess}
358
359
  onCancel={handleCancel}
359
- loadingComponent={<WfoLoading />}
360
360
  config={config}
361
361
  />
362
362
  );
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect } from 'react';
2
2
  import ReactSelect, { components } from 'react-select';
3
3
  import type { GroupBase, InputProps } from 'react-select';
4
4
 
@@ -33,10 +33,20 @@ export const WfoReactSelect = <ValueType,>({
33
33
  hasError = false,
34
34
  refetch,
35
35
  }: WfoReactSelectProps<ValueType>) => {
36
- const selectedValue = options.find(
36
+ useEffect(() => {
37
+ const selectedValue = options.find(
38
+ (option: Option<ValueType>) => option.value === value,
39
+ );
40
+ setSelectedValue(selectedValue || null);
41
+ }, [options, value]);
42
+
43
+ const initialValue = options.find(
37
44
  (option: Option<ValueType>) => option.value === value,
38
45
  );
39
46
 
47
+ const [selectedValue, setSelectedValue] =
48
+ React.useState<Option<ValueType> | null>(initialValue || null);
49
+
40
50
  // React select allows callbacks to supply style for innercomponents: https://react-select.com/styles#inner-components
41
51
  const {
42
52
  reactSelectInnerComponentStyles,
@@ -79,8 +89,17 @@ export const WfoReactSelect = <ValueType,>({
79
89
  id={id}
80
90
  inputId={`${id}.search`}
81
91
  onChange={(option) => {
82
- const selectedValue = option?.value;
83
- onChange(selectedValue);
92
+ if (option === null) {
93
+ // By default reactSelect reverts to the initial option when cleared
94
+ // this is to make sure we can also deselect the value after it is
95
+ // initialized from error state for example.
96
+ setSelectedValue(null);
97
+ onChange(undefined);
98
+ } else {
99
+ const selectedValue = option?.value;
100
+ setSelectedValue(option);
101
+ onChange(selectedValue);
102
+ }
84
103
  }}
85
104
  css={reactSelectStyle}
86
105
  isLoading={isLoading}
@@ -5,7 +5,11 @@ import type { PydanticFormElement } from 'pydantic-forms';
5
5
 
6
6
  import { EuiFlexItem, EuiFormRow, EuiText } from '@elastic/eui';
7
7
 
8
- import { getCommonFormFieldStyles, summaryFieldStyles } from '@/components';
8
+ import {
9
+ getCommonFormFieldStyles,
10
+ getNestedSummaryLabel,
11
+ summaryFieldStyles,
12
+ } from '@/components';
9
13
  import { useWithOrchestratorTheme } from '@/hooks';
10
14
  import { snakeToHuman } from '@/utils';
11
15
 
@@ -29,7 +33,11 @@ export const WfoSummary: PydanticFormElement = ({ pydanticFormField }) => {
29
33
 
30
34
  const rows = columns[0].map((row, index) => (
31
35
  <tr key={index}>
32
- {labels && <td className={`label`}>{labels[index]}</td>}
36
+ {labels && (
37
+ <td className={`label`}>
38
+ {getNestedSummaryLabel(labels, index)}
39
+ </td>
40
+ )}
33
41
  <td className={`value`}>
34
42
  {typeof row === 'string' && row.includes('<!doctype html>') ? (
35
43
  <div
@@ -43,7 +51,7 @@ export const WfoSummary: PydanticFormElement = ({ pydanticFormField }) => {
43
51
  {extraColumnsData &&
44
52
  extraColumnsData.map((_, idx) => (
45
53
  <td className={`value`} key={idx}>
46
- {extraColumnsData[idx][index]}
54
+ {extraColumnsData[idx][index].toString()}
47
55
  </td>
48
56
  ))}
49
57
  </tr>
@@ -11,3 +11,4 @@ export * from './WfoInteger';
11
11
  export * from './WfoDropdown';
12
12
  export * from './WfoReactSelect';
13
13
  export * from './WfoMultiCheckboxField';
14
+ export * from './wfoPydanticFormUtils';
@@ -0,0 +1,32 @@
1
+ import { getNestedSummaryLabel } from '@/components';
2
+
3
+ describe('getNestedSummaryLabel', () => {
4
+ it('returns string value as-is', () => {
5
+ const labels = ['name', 'age', 'city'];
6
+ expect(getNestedSummaryLabel(labels, 0)).toBe('name');
7
+ expect(getNestedSummaryLabel(labels, 1)).toBe('age');
8
+ expect(getNestedSummaryLabel(labels, 2)).toBe('city');
9
+ });
10
+
11
+ it('returns capitalized first key when value is an object', () => {
12
+ const labels = [{ first: 'John', last: 'Doe' }];
13
+ expect(getNestedSummaryLabel(labels, 0)).toBe('First');
14
+ });
15
+
16
+ it('handles object with multiple keys', () => {
17
+ const labels = [{ last: 'Doe', first: 'John' }];
18
+ // only the first key should matter
19
+ expect(getNestedSummaryLabel(labels, 0)).toBe('Last');
20
+ });
21
+
22
+ it('handles empty object', () => {
23
+ const labels = [{}];
24
+ expect(getNestedSummaryLabel(labels, 0)).toBe('');
25
+ });
26
+
27
+ it('handles null and undefined values safely', () => {
28
+ const labels = [null, undefined];
29
+ expect(getNestedSummaryLabel(labels, 0)).toBe('null');
30
+ expect(getNestedSummaryLabel(labels, 1)).toBe('undefined');
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ import { capitalize } from 'lodash';
2
+
3
+ export type SummaryFormLabel =
4
+ | string
5
+ | null
6
+ | undefined
7
+ | Record<string, unknown>;
8
+
9
+ export const getNestedSummaryLabel = (
10
+ labels: SummaryFormLabel[],
11
+ index: number,
12
+ ): string => {
13
+ const value = labels[index];
14
+ if (typeof value === 'object' && value !== null) {
15
+ const firstKey: string = Object.keys(value)[0] ?? '';
16
+ return capitalize(firstKey);
17
+ }
18
+ return String(value);
19
+ };
@@ -11,6 +11,7 @@ import {
11
11
  } from '@elastic/eui';
12
12
 
13
13
  import { WfoJsonCodeBlock, WfoTableCodeBlock } from '@/components';
14
+ import { WfoStepFormOld } from '@/components/WfoWorkflowSteps/WfoStep/WfoStepFormOld';
14
15
  import { useOrchestratorTheme, useWithOrchestratorTheme } from '@/hooks';
15
16
  import { WfoChevronDown, WfoChevronUp } from '@/icons';
16
17
  import type { EmailState } from '@/types';
@@ -117,6 +118,11 @@ export const WfoStep = React.forwardRef(
117
118
  );
118
119
  };
119
120
 
121
+ const whitelist = ['confirm_corelink'];
122
+ const isCorelinkForm = whitelist.includes(
123
+ Object.keys(userInputForm?.properties ?? {})[0],
124
+ );
125
+
120
126
  return (
121
127
  <div ref={ref}>
122
128
  <EuiPanel>
@@ -209,14 +215,23 @@ export const WfoStep = React.forwardRef(
209
215
  )}
210
216
  </div>
211
217
  )}
212
- {step.status === StepStatus.SUSPEND && userInputForm && (
213
- <WfoStepForm
214
- userInputForm={userInputForm}
215
- isTask={isTask}
216
- processId={processId}
217
- userPermissions={userPermissions}
218
- />
219
- )}
218
+ {step.status === StepStatus.SUSPEND &&
219
+ userInputForm &&
220
+ (isCorelinkForm ? (
221
+ <WfoStepForm
222
+ userInputForm={userInputForm}
223
+ isTask={isTask}
224
+ processId={processId ?? ''}
225
+ userPermissions={userPermissions}
226
+ />
227
+ ) : (
228
+ <WfoStepFormOld
229
+ userInputForm={userInputForm}
230
+ isTask={isTask}
231
+ processId={processId}
232
+ userPermissions={userPermissions}
233
+ />
234
+ ))}
220
235
  </EuiPanel>
221
236
  </div>
222
237
  );
@@ -1,8 +1,11 @@
1
- import React, { useState } from 'react';
1
+ import React, { useCallback, useMemo } from 'react';
2
+
3
+ import { PydanticForm, PydanticFormApiProvider } from 'pydantic-forms';
2
4
 
3
5
  import { EuiFlexItem } from '@elastic/eui';
4
6
 
5
- import { UserInputFormWizard, WfoError, WfoLoading } from '@/components';
7
+ import { Header, Row, useWfoPydanticFormConfig } from '@/components';
8
+ import { StepFormFooter } from '@/components/WfoWorkflowSteps/WfoStep/WfoStepFormFooter';
6
9
  import { useOrchestratorTheme } from '@/hooks';
7
10
  import { HttpStatus } from '@/rtk';
8
11
  import { useResumeProcessMutation } from '@/rtk/endpoints/forms';
@@ -11,7 +14,7 @@ import { FormUserPermissions, InputForm } from '@/types/forms';
11
14
  interface WfoStepFormProps {
12
15
  userInputForm: InputForm;
13
16
  isTask: boolean;
14
- processId?: string;
17
+ processId: string;
15
18
  userPermissions: FormUserPermissions;
16
19
  }
17
20
 
@@ -21,47 +24,66 @@ export const WfoStepForm = ({
21
24
  processId,
22
25
  userPermissions,
23
26
  }: WfoStepFormProps) => {
24
- const [isProcessing, setIsProcessing] = useState<boolean>(false);
25
- const [hasError, setHasError] = useState<boolean>(false);
26
27
  const { theme } = useOrchestratorTheme();
28
+ const {
29
+ wfoComponentMatcherExtender,
30
+ pydanticLabelProvider,
31
+ customTranslations,
32
+ } = useWfoPydanticFormConfig();
27
33
  const [resumeProcess] = useResumeProcessMutation();
28
34
 
29
- const submitForm = (processInput: object[]) => {
30
- if (!processId) {
31
- return Promise.reject();
32
- }
35
+ const getInitialStepInput = useMemo(() => userInputForm, [userInputForm]);
33
36
 
34
- return resumeProcess({ processId, userInputs: processInput })
35
- .unwrap()
36
- .then(() => {
37
- setIsProcessing(true);
38
- })
39
- .catch((error) => {
40
- if (error?.status !== HttpStatus.FormNotComplete) {
41
- if (error?.status === HttpStatus.BadRequest) {
42
- // Rethrow the error so userInputForm can catch it and display validation errors
43
- throw error;
44
- }
45
- console.error(error);
46
- setHasError(true);
47
- } else {
48
- throw error;
37
+ const getStepFormProvider = useCallback(
38
+ (): PydanticFormApiProvider =>
39
+ async ({ requestBody = [] }) => {
40
+ if (requestBody.length === 0) {
41
+ return {
42
+ form: getInitialStepInput,
43
+ meta: { hasNext: false },
44
+ };
49
45
  }
50
- });
51
- };
46
+
47
+ return resumeProcess({ processId, userInputs: requestBody })
48
+ .unwrap()
49
+ .catch((error) => {
50
+ if (error.status === HttpStatus.BadGateway) {
51
+ return {};
52
+ } else if (
53
+ error.status === HttpStatus.FormNotComplete
54
+ ) {
55
+ return error.data;
56
+ } else if (error.status === HttpStatus.BadRequest) {
57
+ return {
58
+ ...error.data,
59
+ status: error.status,
60
+ };
61
+ }
62
+ throw error;
63
+ });
64
+ },
65
+ [getInitialStepInput, processId, resumeProcess],
66
+ );
52
67
 
53
68
  return (
54
69
  <EuiFlexItem css={{ margin: theme.size.m }}>
55
- {(hasError && <WfoError />) || (isProcessing && <WfoLoading />) || (
56
- <UserInputFormWizard
57
- stepUserInput={userInputForm}
58
- stepSubmit={submitForm}
59
- hasNext={false}
60
- isTask={isTask}
61
- isResuming={true}
62
- allowSubmit={userPermissions.resumeAllowed}
63
- />
64
- )}
70
+ <PydanticForm
71
+ formKey={processId}
72
+ config={{
73
+ apiProvider: getStepFormProvider(),
74
+ footerRenderer: () => (
75
+ <StepFormFooter
76
+ isTask={isTask}
77
+ isResumeAllowed={userPermissions.resumeAllowed}
78
+ />
79
+ ),
80
+ headerRenderer: Header,
81
+ componentMatcherExtender: wfoComponentMatcherExtender,
82
+ rowRenderer: Row,
83
+ labelProvider: pydanticLabelProvider,
84
+ customTranslations: customTranslations,
85
+ }}
86
+ />
65
87
  </EuiFlexItem>
66
88
  );
67
89
  };
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+
3
+ import { useTranslations } from 'next-intl';
4
+
5
+ import { EuiButton, EuiHorizontalRule } from '@elastic/eui';
6
+
7
+ import { RenderFormErrors } from '@/components/WfoPydanticForm/RenderFormErrors';
8
+ import { WfoPlayCircle } from '@/icons';
9
+
10
+ interface StepFormFooterProps {
11
+ isTask: boolean;
12
+ isResumeAllowed?: boolean;
13
+ }
14
+
15
+ export const StepFormFooter = ({
16
+ isTask,
17
+ isResumeAllowed,
18
+ }: StepFormFooterProps) => {
19
+ const t = useTranslations('pydanticForms.userInputForm');
20
+
21
+ const SubmitButton = () => {
22
+ const submitButtonLabel = isTask
23
+ ? t('resumeTask')
24
+ : t('resumeWorkflow');
25
+ return (
26
+ <EuiButton
27
+ data-testid="button-submit-form-submit"
28
+ id="button-submit-form-submit"
29
+ tabIndex={0}
30
+ fill
31
+ color="primary"
32
+ iconType={() => (
33
+ <WfoPlayCircle
34
+ color={'currentColor'}
35
+ width="18"
36
+ height="18"
37
+ />
38
+ )}
39
+ iconSide="right"
40
+ type="submit"
41
+ aria-label={submitButtonLabel}
42
+ disabled={!isResumeAllowed}
43
+ >
44
+ {submitButtonLabel}
45
+ </EuiButton>
46
+ );
47
+ };
48
+
49
+ return (
50
+ <div data-testid="pydantic-step-form-footer">
51
+ <RenderFormErrors />
52
+ <EuiHorizontalRule />
53
+ <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
54
+ <SubmitButton />
55
+ </div>
56
+ </div>
57
+ );
58
+ };
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react';
2
+
3
+ import { EuiFlexItem } from '@elastic/eui';
4
+
5
+ import { UserInputFormWizard, WfoError, WfoLoading } from '@/components';
6
+ import { useOrchestratorTheme } from '@/hooks';
7
+ import { HttpStatus } from '@/rtk';
8
+ import { useResumeProcessMutation } from '@/rtk/endpoints/forms';
9
+ import { FormUserPermissions, InputForm } from '@/types/forms';
10
+
11
+ interface WfoStepFormProps {
12
+ userInputForm: InputForm;
13
+ isTask: boolean;
14
+ processId?: string;
15
+ userPermissions: FormUserPermissions;
16
+ }
17
+
18
+ export const WfoStepFormOld = ({
19
+ userInputForm,
20
+ isTask,
21
+ processId,
22
+ userPermissions,
23
+ }: WfoStepFormProps) => {
24
+ const [isProcessing, setIsProcessing] = useState<boolean>(false);
25
+ const [hasError, setHasError] = useState<boolean>(false);
26
+ const { theme } = useOrchestratorTheme();
27
+ const [resumeProcess] = useResumeProcessMutation();
28
+
29
+ const submitForm = (processInput: object[]) => {
30
+ if (!processId) {
31
+ return Promise.reject();
32
+ }
33
+
34
+ return resumeProcess({ processId, userInputs: processInput })
35
+ .unwrap()
36
+ .then(() => {
37
+ setIsProcessing(true);
38
+ })
39
+ .catch((error) => {
40
+ if (error?.status !== HttpStatus.FormNotComplete) {
41
+ if (error?.status === HttpStatus.BadRequest) {
42
+ // Rethrow the error so userInputForm can catch it and display validation errors
43
+ throw error;
44
+ }
45
+ console.error(error);
46
+ setHasError(true);
47
+ } else {
48
+ throw error;
49
+ }
50
+ });
51
+ };
52
+
53
+ return (
54
+ <EuiFlexItem css={{ margin: theme.size.m }}>
55
+ {(hasError && <WfoError />) || (isProcessing && <WfoLoading />) || (
56
+ <UserInputFormWizard
57
+ stepUserInput={userInputForm}
58
+ stepSubmit={submitForm}
59
+ hasNext={false}
60
+ isTask={isTask}
61
+ isResuming={true}
62
+ allowSubmit={userPermissions.resumeAllowed}
63
+ />
64
+ )}
65
+ </EuiFlexItem>
66
+ );
67
+ };
@@ -1 +1 @@
1
- export const ORCHESTRATOR_UI_LIBRARY_VERSION = '5.5.2';
1
+ export const ORCHESTRATOR_UI_LIBRARY_VERSION = '5.6.0';
@@ -15,7 +15,6 @@ export const useGetTranslationMessages = (locale: string | undefined) => {
15
15
  return enGB;
16
16
  case Locale.nlNL:
17
17
  return nlNL;
18
-
19
18
  default:
20
19
  return enGB;
21
20
  }