@openmrs/esm-stock-management-app 1.0.1-pre.783 → 1.0.1-pre.788

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.
Files changed (128) hide show
  1. package/__mocks__/index.ts +1 -0
  2. package/__mocks__/operation-type.mock.ts +532 -0
  3. package/dist/155.js +1 -0
  4. package/dist/155.js.map +1 -0
  5. package/dist/172.js +1 -1
  6. package/dist/20.js +1 -1
  7. package/dist/290.js +1 -1
  8. package/dist/493.js +2 -0
  9. package/dist/493.js.map +1 -0
  10. package/dist/606.js +1 -1
  11. package/dist/627.js +1 -1
  12. package/dist/922.js +1 -0
  13. package/dist/922.js.map +1 -0
  14. package/dist/main.js +1 -1
  15. package/dist/main.js.map +1 -1
  16. package/dist/openmrs-esm-stock-management-app.js +1 -1
  17. package/dist/openmrs-esm-stock-management-app.js.buildmanifest.json +75 -51
  18. package/dist/openmrs-esm-stock-management-app.js.map +1 -1
  19. package/dist/routes.json +1 -1
  20. package/package.json +1 -1
  21. package/src/config-schema.ts +6 -0
  22. package/src/core/utils/utils.ts +29 -0
  23. package/src/index.ts +4 -0
  24. package/src/routes.json +9 -0
  25. package/src/stock-items/add-stock-item/transactions/printout/transactions-stockcard-printout.component.tsx +8 -12
  26. package/src/stock-items/add-stock-item/transactions/transactions.component.tsx +8 -12
  27. package/src/stock-items/stock-items.resource.ts +5 -5
  28. package/src/stock-lookups/stock-lookups.resource.ts +2 -2
  29. package/src/stock-operations/add-stock-operation/stock-operations-expanded-row/stock-items-table.scss +34 -0
  30. package/src/stock-operations/add-stock-operation/stock-operations-expanded-row/stock-items-table.tsx +111 -0
  31. package/src/stock-operations/add-stock-operation/stock-operations-expanded-row/stock-operation-expanded-row.component.tsx +87 -0
  32. package/src/stock-operations/add-stock-operation/stock-operations-expanded-row/stock-operation-expanded-row.scss +31 -0
  33. package/src/stock-operations/add-stock-operation/stock-operations-expanded-row/stock-operations-status.tsx +45 -0
  34. package/src/stock-operations/edit-stock-operation/edit-stock-operation-action-menu.component.tsx +41 -16
  35. package/src/stock-operations/stock-operation-reference.component.tsx +64 -0
  36. package/src/stock-operations/stock-operation-status/stock-operation-status-row.tsx +77 -0
  37. package/src/stock-operations/stock-operation-status/stock-operation-status.scss +32 -0
  38. package/src/stock-operations/stock-operation-status/stock-operation-status.tsx +45 -0
  39. package/src/stock-operations/stock-operation-types-selector/stock-operation-types-selector.component.tsx +30 -29
  40. package/src/stock-operations/stock-operation.utils.tsx +16 -79
  41. package/src/stock-operations/stock-operations-dialog/stock-operations-issue-stock-button.component.tsx +27 -39
  42. package/src/stock-operations/stock-operations-dialog/stock-operations-print-button.component.tsx +51 -59
  43. package/src/stock-operations/{stock-item-selector/stock-item-selector.resource.tsx → stock-operations-forms/hooks/useFilterableStockItems.ts} +4 -4
  44. package/src/stock-operations/stock-operations-forms/hooks/useFilteredOperationTypesByRoles.ts +30 -0
  45. package/src/stock-operations/stock-operations-forms/hooks/useOperationTypePermisions.ts +29 -0
  46. package/src/stock-operations/stock-operations-forms/hooks/useParties.ts +73 -0
  47. package/src/stock-operations/{users-selector/users-selector.resource.tsx → stock-operations-forms/hooks/useSearchUser.ts} +9 -7
  48. package/src/stock-operations/{batch-no-selector/batch-no-selector.resource.tsx → stock-operations-forms/hooks/useStockItemBatchNumbers.ts} +3 -3
  49. package/src/stock-operations/stock-operations-forms/hooks/useStockOperationLinks.ts +20 -0
  50. package/src/stock-operations/stock-operations-forms/input-components/batch-no-selector.component.tsx +72 -0
  51. package/src/stock-operations/stock-operations-forms/input-components/batch-no-selector.test.tsx +90 -0
  52. package/src/stock-operations/{add-stock-operation/stock-item-search/stock-item-search.scss → stock-operations-forms/input-components/input-components-styles.scss} +2 -2
  53. package/src/stock-operations/stock-operations-forms/input-components/qty-uim-selector.test.tsx +157 -0
  54. package/src/stock-operations/stock-operations-forms/input-components/quantity-uom-selector.component.tsx +53 -0
  55. package/src/stock-operations/stock-operations-forms/input-components/stock-item-search.component.tsx +79 -0
  56. package/src/stock-operations/stock-operations-forms/input-components/stock-operation-reason-selector.component.tsx +59 -0
  57. package/src/stock-operations/stock-operations-forms/input-components/stock-operation-reason-selector.test.tsx +216 -0
  58. package/src/stock-operations/{batch-no-selector → stock-operations-forms/input-components}/unique-batch-no-entry-input.component.tsx +12 -7
  59. package/src/stock-operations/stock-operations-forms/input-components/user-selector.test.tsx +110 -0
  60. package/src/stock-operations/stock-operations-forms/input-components/users-selector.component.tsx +111 -0
  61. package/src/stock-operations/stock-operations-forms/step1.test.tsx +303 -0
  62. package/src/stock-operations/stock-operations-forms/step2.test.tsx +254 -0
  63. package/src/stock-operations/stock-operations-forms/step3.test.tsx +223 -0
  64. package/src/stock-operations/stock-operations-forms/steps/base-operation-details-form-step.tsx +241 -0
  65. package/src/stock-operations/stock-operations-forms/steps/quantity-uom-cell.component.tsx +33 -0
  66. package/src/stock-operations/stock-operations-forms/steps/received-items.component.tsx +110 -0
  67. package/src/stock-operations/stock-operations-forms/steps/stock-availability-cell.component.tsx +51 -0
  68. package/src/stock-operations/stock-operations-forms/steps/stock-operation-item-batch-no-cell.component.tsx +40 -0
  69. package/src/stock-operations/stock-operations-forms/steps/stock-operation-item-cell.component.tsx +50 -0
  70. package/src/stock-operations/stock-operations-forms/steps/stock-operation-item-expiry-cell.component.tsx +41 -0
  71. package/src/stock-operations/stock-operations-forms/steps/stock-operation-items-form-step.component.tsx +281 -0
  72. package/src/stock-operations/stock-operations-forms/steps/stock-operation-items-form-step.scc.scss +64 -0
  73. package/src/stock-operations/stock-operations-forms/steps/stock-operation-submission-form-step.component.tsx +243 -0
  74. package/src/stock-operations/stock-operations-forms/stock-issue-form-initializer-with-related-requisition-operation.component.tsx +55 -0
  75. package/src/stock-operations/stock-operations-forms/stock-item-form/stock-item-form.scss +41 -0
  76. package/src/stock-operations/stock-operations-forms/stock-item-form/stock-item-form.workspace.tsx +211 -0
  77. package/src/stock-operations/stock-operations-forms/stock-operation-form-header.component.tsx +166 -0
  78. package/src/stock-operations/stock-operations-forms/stock-operation-form.component.tsx +205 -0
  79. package/src/stock-operations/stock-operations-forms/stock-operation-form.scss +111 -0
  80. package/src/stock-operations/stock-operations-forms/stock-operation-related-link.component.tsx +45 -0
  81. package/src/stock-operations/stock-operations-forms/stock-operation-stepper/stepper.scss +41 -0
  82. package/src/stock-operations/stock-operations-forms/stock-operation-stepper/stock-operation-stepper.component.tsx +52 -0
  83. package/src/stock-operations/stock-operations-forms/stock-operations-form-utils.ts +32 -0
  84. package/src/stock-operations/stock-operations-table.component.tsx +57 -92
  85. package/src/stock-operations/stock-operations.resource.ts +16 -13
  86. package/src/stock-operations/validation-schema.ts +72 -14
  87. package/dist/766.js +0 -2
  88. package/dist/766.js.map +0 -1
  89. package/dist/822.js +0 -1
  90. package/dist/822.js.map +0 -1
  91. package/src/stock-operations/add-stock-operation/add-stock-operation.component.tsx +0 -349
  92. package/src/stock-operations/add-stock-operation/add-stock-operation.resource.tsx +0 -27
  93. package/src/stock-operations/add-stock-operation/add-stock-operation.scss +0 -60
  94. package/src/stock-operations/add-stock-operation/add-stock-operation.test.tsx +0 -192
  95. package/src/stock-operations/add-stock-operation/add-stock-operation.utils.tsx +0 -152
  96. package/src/stock-operations/add-stock-operation/add-stock-utils.ts +0 -103
  97. package/src/stock-operations/add-stock-operation/base-operation-details.component.tsx +0 -439
  98. package/src/stock-operations/add-stock-operation/base-operation-details.scss +0 -30
  99. package/src/stock-operations/add-stock-operation/received-items.component.tsx +0 -93
  100. package/src/stock-operations/add-stock-operation/stock-item-search/stock-item-search.component.tsx +0 -70
  101. package/src/stock-operations/add-stock-operation/stock-items-addition-row.component.tsx +0 -357
  102. package/src/stock-operations/add-stock-operation/stock-items-addition-row.resource.tsx +0 -0
  103. package/src/stock-operations/add-stock-operation/stock-items-addition-row.scss +0 -12
  104. package/src/stock-operations/add-stock-operation/stock-items-addition-row.test.tsx +0 -10
  105. package/src/stock-operations/add-stock-operation/stock-items-addition.component.scss +0 -17
  106. package/src/stock-operations/add-stock-operation/stock-items-addition.component.tsx +0 -254
  107. package/src/stock-operations/add-stock-operation/stock-operation-context/useStockOperationContext.tsx +0 -16
  108. package/src/stock-operations/add-stock-operation/stock-operation-reference.component.tsx +0 -39
  109. package/src/stock-operations/add-stock-operation/stock-operation-related-link.component.tsx +0 -38
  110. package/src/stock-operations/add-stock-operation/stock-operation-status.component.tsx +0 -170
  111. package/src/stock-operations/add-stock-operation/stock-operation-submission.component.tsx +0 -189
  112. package/src/stock-operations/add-stock-operation/stock-operation-submission.test.tsx +0 -138
  113. package/src/stock-operations/add-stock-operation/types.ts +0 -55
  114. package/src/stock-operations/add-stock-operation/validationSchema.ts +0 -54
  115. package/src/stock-operations/batch-no-selector/batch-no-selector.component.tsx +0 -114
  116. package/src/stock-operations/batch-no-selector/batch-no-selector.scss +0 -0
  117. package/src/stock-operations/batch-no-selector/batch-no-selector.test.tsx +0 -101
  118. package/src/stock-operations/party-selector/party-selector.component.tsx +0 -59
  119. package/src/stock-operations/qty-uom-selector/qty-uom-selector.component.tsx +0 -65
  120. package/src/stock-operations/qty-uom-selector/qty-uom-selector.resource.tsx +0 -0
  121. package/src/stock-operations/qty-uom-selector/qty-uom-selector.scss +0 -0
  122. package/src/stock-operations/qty-uom-selector/qty-uom-selector.test.tsx +0 -10
  123. package/src/stock-operations/stock-item-selector/stock-item-selector.component.tsx +0 -69
  124. package/src/stock-operations/stock-item-selector/stock-item-selector.scss +0 -0
  125. package/src/stock-operations/stock-item-selector/stock-item-selector.test.tsx +0 -10
  126. package/src/stock-operations/stock-operation-reason-selector/stock-operation-reason-selector.component.tsx +0 -62
  127. package/src/stock-operations/users-selector/users-selector.component.tsx +0 -75
  128. /package/dist/{766.js.LICENSE.txt → 493.js.LICENSE.txt} +0 -0
@@ -0,0 +1,216 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React from 'react';
4
+
5
+ import { useConfig } from '@openmrs/esm-framework';
6
+ import { useFormContext } from 'react-hook-form';
7
+ import { useConcept } from '../../../stock-lookups/stock-lookups.resource';
8
+ import StockOperationReasonSelector from './stock-operation-reason-selector.component';
9
+
10
+ // Mock the hooks
11
+ jest.mock('@openmrs/esm-framework', () => ({
12
+ useConfig: jest.fn(),
13
+ }));
14
+
15
+ jest.mock('react-hook-form', () => ({
16
+ useFormContext: jest.fn(),
17
+ Controller: ({ render }) => render({ field: {}, fieldState: {} }),
18
+ }));
19
+ jest.mock('../../../stock-lookups/stock-lookups.resource');
20
+ jest.mock('react-i18next', () => ({
21
+ useTranslation: () => ({ t: (key: string) => key }),
22
+ }));
23
+
24
+ const mockUseConcept = useConcept as jest.Mock;
25
+ const mockUseConfig = useConfig as jest.Mock;
26
+ const mockUseFormContext = useFormContext as jest.Mock;
27
+
28
+ describe('StockoperationReasonSelector', () => {
29
+ const mockConcepts = {
30
+ uuid: '3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25',
31
+ display: 'Stock Adjustment Reason',
32
+ name: {
33
+ display: 'Stock Adjustment Reason',
34
+ uuid: '4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
35
+ name: 'Stock Adjustment Reason',
36
+ locale: 'en',
37
+ localePreferred: true,
38
+ conceptNameType: 'FULLY_SPECIFIED',
39
+ links: [
40
+ {
41
+ rel: 'self',
42
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
43
+ resourceAlias: 'name',
44
+ },
45
+ {
46
+ rel: 'full',
47
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/4eb6556a-a2d4-4e85-9b62-4d076a1063fc?v=full',
48
+ resourceAlias: 'name',
49
+ },
50
+ ],
51
+ resourceVersion: '1.9',
52
+ },
53
+ datatype: {
54
+ uuid: '8d4a48b6-c2cc-11de-8d13-0010c6dffd0f',
55
+ display: 'Coded',
56
+ links: [
57
+ {
58
+ rel: 'self',
59
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/conceptdatatype/8d4a48b6-c2cc-11de-8d13-0010c6dffd0f',
60
+ resourceAlias: 'conceptdatatype',
61
+ },
62
+ ],
63
+ },
64
+ conceptClass: {
65
+ uuid: '8d491e50-c2cc-11de-8d13-0010c6dffd0f',
66
+ display: 'Question',
67
+ links: [
68
+ {
69
+ rel: 'self',
70
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/conceptclass/8d491e50-c2cc-11de-8d13-0010c6dffd0f',
71
+ resourceAlias: 'conceptclass',
72
+ },
73
+ ],
74
+ },
75
+ set: false,
76
+ version: null,
77
+ retired: false,
78
+ names: [
79
+ {
80
+ uuid: '4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
81
+ display: 'Stock Adjustment Reason',
82
+ links: [
83
+ {
84
+ rel: 'self',
85
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
86
+ resourceAlias: 'name',
87
+ },
88
+ ],
89
+ },
90
+ {
91
+ uuid: '36eb0855-c810-4816-9bed-1de5c615e702',
92
+ display: 'Stock Adjustment Reason',
93
+ links: [
94
+ {
95
+ rel: 'self',
96
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/36eb0855-c810-4816-9bed-1de5c615e702',
97
+ resourceAlias: 'name',
98
+ },
99
+ ],
100
+ },
101
+ ],
102
+ descriptions: [
103
+ {
104
+ uuid: '67311e07-1935-448b-8305-7d11abf0de63',
105
+ display: 'Stock Adjustment Reason',
106
+ links: [
107
+ {
108
+ rel: 'self',
109
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/description/67311e07-1935-448b-8305-7d11abf0de63',
110
+ resourceAlias: 'description',
111
+ },
112
+ ],
113
+ },
114
+ ],
115
+ mappings: [],
116
+ answers: [
117
+ {
118
+ uuid: '165420AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
119
+ display: 'Drug not available due to expired medication',
120
+ links: [
121
+ {
122
+ rel: 'self',
123
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/165420AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
124
+ resourceAlias: 'concept',
125
+ },
126
+ ],
127
+ },
128
+ {
129
+ uuid: '160584AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
130
+ display: 'Lost or ran out of medication',
131
+ links: [
132
+ {
133
+ rel: 'self',
134
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/160584AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
135
+ resourceAlias: 'concept',
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ uuid: '122835AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
141
+ display: 'Work Shift Change',
142
+ links: [
143
+ {
144
+ rel: 'self',
145
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/122835AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
146
+ resourceAlias: 'concept',
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ uuid: '160561AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
152
+ display: 'New drug available',
153
+ links: [
154
+ {
155
+ rel: 'self',
156
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/160561AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
157
+ resourceAlias: 'concept',
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ setMembers: [],
163
+ attributes: [],
164
+ links: [
165
+ {
166
+ rel: 'self',
167
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25',
168
+ resourceAlias: 'concept',
169
+ },
170
+ {
171
+ rel: 'full',
172
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25?v=full',
173
+ resourceAlias: 'concept',
174
+ },
175
+ ],
176
+ resourceVersion: '2.0',
177
+ };
178
+ beforeEach(() => {
179
+ jest.clearAllMocks();
180
+ mockUseConfig.mockReturnValue({ stockAdjustmentReasonUUID: 'uuid' });
181
+ mockUseConcept.mockReturnValue({
182
+ isLoading: false,
183
+ items: mockConcepts,
184
+ });
185
+ });
186
+
187
+ it('should display loading state while loading reason concepts', () => {
188
+ mockUseFormContext.mockReturnValue({ control: {} });
189
+ mockUseConcept.mockReturnValue({
190
+ isLoading: true,
191
+ items: mockConcepts,
192
+ });
193
+ render(<StockOperationReasonSelector />);
194
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
195
+ });
196
+ it('should display error notification when error encountered while fetching concepts', () => {
197
+ const errorMessahe = 'Error message';
198
+ mockUseConcept.mockReturnValue({
199
+ isLoading: false,
200
+ items: mockConcepts,
201
+ error: new Error(errorMessahe),
202
+ });
203
+ render(<StockOperationReasonSelector />);
204
+ expect(screen.getByRole('status')).toBeInTheDocument();
205
+ expect(screen.getByText(errorMessahe)).toBeInTheDocument();
206
+ });
207
+
208
+ it('renders ComboBox with reasons', async () => {
209
+ render(<StockOperationReasonSelector />);
210
+ const combobox = screen.getByRole('combobox');
211
+ await userEvent.click(combobox);
212
+ mockConcepts.answers.forEach((ans) => {
213
+ expect(screen.getByText(`${ans?.display}`)).toBeInTheDocument();
214
+ });
215
+ });
216
+ });
@@ -1,31 +1,35 @@
1
1
  import { TextInput } from '@carbon/react';
2
2
  import React, { ChangeEvent, useEffect, useMemo, useState } from 'react';
3
- import { useStockItemBatchNos } from './batch-no-selector.resource';
4
3
  import { TextInputSkeleton } from '@carbon/react';
4
+ import { useStockItemBatchNumbers } from '../hooks/useStockItemBatchNumbers';
5
+ import { useTranslation } from 'react-i18next';
5
6
 
6
7
  type UniqueBatchNoEntryInputProps = {
7
8
  defaultValue?: string;
8
9
  onValueChange?: (value: string) => void;
9
10
  error?: string;
10
11
  stockItemUuid: string;
12
+ stockOperationItemUuid: string;
11
13
  };
12
14
  const UniqueBatchNoEntryInput: React.FC<UniqueBatchNoEntryInputProps> = ({
13
15
  defaultValue,
14
16
  onValueChange,
15
17
  error,
16
18
  stockItemUuid,
19
+ stockOperationItemUuid,
17
20
  }) => {
18
- const { isLoading, stockItemBatchNos } = useStockItemBatchNos(stockItemUuid);
21
+ const { isLoading, stockItemBatchNos } = useStockItemBatchNumbers(stockItemUuid);
19
22
  const [value, setValue] = useState(defaultValue);
20
23
  const [_error, setError] = useState<string>();
21
-
24
+ const { t } = useTranslation();
25
+ const isNewItem = useMemo(() => stockOperationItemUuid.startsWith('new-item'), [stockOperationItemUuid]);
22
26
  const batchNoAlreadyUsed = useMemo(
23
- () => stockItemBatchNos?.findIndex((batchNo) => batchNo.batchNo === value) !== -1,
24
- [stockItemBatchNos, value],
27
+ () => isNewItem && stockItemBatchNos?.findIndex((batchNo) => batchNo.batchNo === value) !== -1,
28
+ [stockItemBatchNos, value, isNewItem],
25
29
  );
26
30
 
27
31
  useEffect(() => {
28
- if (defaultValue) setValue(defaultValue);
32
+ if (defaultValue) setValue(defaultValue ?? '');
29
33
  }, [defaultValue]);
30
34
 
31
35
  useEffect(() => {
@@ -41,12 +45,13 @@ const UniqueBatchNoEntryInput: React.FC<UniqueBatchNoEntryInputProps> = ({
41
45
 
42
46
  return (
43
47
  <TextInput
44
- size="sm"
45
48
  maxLength={50}
46
49
  onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
47
50
  value={value}
48
51
  invalidText={_error ?? error}
49
52
  invalid={_error ?? error}
53
+ placeholder={t('batchNumber', 'Batch Number')}
54
+ labelText={t('batchNumber', 'Batch Number')}
50
55
  />
51
56
  );
52
57
  };
@@ -0,0 +1,110 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { useFormContext } from 'react-hook-form';
4
+ import { useUser } from '../../../stock-lookups/stock-lookups.resource';
5
+ import useSearchUser from '../hooks/useSearchUser';
6
+ import UsersSelector from './users-selector.component';
7
+ import { otherUser } from '../../../core/utils/utils';
8
+ import userEvent from '@testing-library/user-event';
9
+
10
+ jest.mock('../hooks/useSearchUser');
11
+ jest.mock('../../../stock-lookups/stock-lookups.resource');
12
+ jest.mock('react-hook-form', () => ({
13
+ useFormContext: jest.fn(),
14
+ Controller: ({ render }) => render({ field: {}, fieldState: {} }),
15
+ }));
16
+ jest.mock('react-i18next', () => ({
17
+ useTranslation: () => ({ t: (key: string) => key }),
18
+ }));
19
+
20
+ const mockUseSearchUser = useSearchUser as jest.Mock;
21
+ const mockUseUser = useUser as jest.Mock;
22
+ const mockUseFormContext = useFormContext as jest.Mock;
23
+
24
+ describe('UsersSelector', () => {
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ mockUseFormContext.mockReturnValue({
28
+ control: {},
29
+ watch: jest.fn().mockImplementation((field) => {
30
+ if (field === 'responsiblePersonUuid') return 'responsibleperson.uuid';
31
+ if (field === 'responsiblePersonOther') return 'responsiblepersonother.uuid';
32
+ return '';
33
+ }),
34
+ resetField: jest.fn(),
35
+ });
36
+ });
37
+
38
+ it('renders loading state', async () => {
39
+ mockUseSearchUser.mockReturnValue({ isLoading: true, userList: [], setSearchString: jest.fn() });
40
+ mockUseUser.mockReturnValue({ isLoading: true, data: null, error: null });
41
+
42
+ render(<UsersSelector />);
43
+
44
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
45
+ });
46
+
47
+ it('renders error state', () => {
48
+ const errorMessage = 'Error message';
49
+ mockUseSearchUser.mockReturnValue({ isLoading: false, userList: [], setSearchString: jest.fn() });
50
+ mockUseUser.mockReturnValue({ isLoading: false, data: null, error: new Error(errorMessage) });
51
+ render(<UsersSelector />);
52
+ expect(screen.getByText('responsiblePersonError')).toBeInTheDocument();
53
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
54
+ });
55
+
56
+ it('renders ComboBox with user list', async () => {
57
+ mockUseSearchUser.mockReturnValue({
58
+ isLoading: false,
59
+ userList: [
60
+ { uuid: '1', person: { display: 'User 1' } },
61
+ { uuid: '2', person: { display: 'User 2' } },
62
+ ],
63
+ setSearchString: jest.fn(),
64
+ });
65
+
66
+ mockUseUser.mockReturnValue({ isLoading: false, data: null, error: null });
67
+ render(<UsersSelector />);
68
+ expect(screen.getByText('responsiblePerson')).toBeInTheDocument();
69
+ const combobox = screen.getByRole('combobox');
70
+ fireEvent.click(combobox);
71
+ expect(screen.getByText('User 1')).toBeInTheDocument();
72
+ expect(screen.getByText('User 2')).toBeInTheDocument();
73
+ });
74
+
75
+ it('renders TextInput for other user', async () => {
76
+ mockUseSearchUser.mockReturnValue({ isLoading: false, userList: [], setSearchString: jest.fn() });
77
+ mockUseFormContext.mockReturnValue({
78
+ control: {},
79
+ watch: jest.fn().mockImplementation((field) => {
80
+ if (field === 'responsiblePersonUuid') return otherUser.uuid;
81
+ if (field === 'responsiblePersonOther') return '';
82
+ return '';
83
+ }),
84
+ resetField: jest.fn(),
85
+ });
86
+ mockUseUser.mockReturnValue({ isLoading: false, data: null, error: null });
87
+
88
+ render(<UsersSelector />);
89
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
90
+ expect(screen.getByPlaceholderText('pleaseSpecify')).toBeInTheDocument();
91
+ });
92
+
93
+ it('calls setSearchString on input change after delay simulating debounce timout', async () => {
94
+ const setSearchString = jest.fn();
95
+ mockUseSearchUser.mockReturnValue({
96
+ isLoading: false,
97
+ userList: [],
98
+ setSearchString,
99
+ });
100
+
101
+ mockUseUser.mockReturnValue({ isLoading: false, data: null, error: null });
102
+
103
+ render(<UsersSelector />);
104
+ const combobox = screen.getByRole('combobox');
105
+ await userEvent.click(combobox);
106
+ await userEvent.type(combobox, 'test');
107
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate debounce
108
+ expect(setSearchString).toHaveBeenCalledWith('test');
109
+ });
110
+ });
@@ -0,0 +1,111 @@
1
+ import { ComboBox, InlineNotification, SelectSkeleton } from '@carbon/react';
2
+ import React, { useEffect } from 'react';
3
+ import { Controller, useFormContext } from 'react-hook-form';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { User } from '../../../core/api/types/identity/User';
6
+ import { useDebounce } from '../../../core/hooks/debounce-hook';
7
+ import { otherUser } from '../../../core/utils/utils';
8
+ import { useUser } from '../../../stock-lookups/stock-lookups.resource';
9
+ import useSearchUser from '../hooks/useSearchUser';
10
+ import { Column } from '@carbon/react';
11
+ import { TextInput } from '@carbon/react';
12
+
13
+ const UsersSelector = () => {
14
+ const { isLoading, userList, setSearchString } = useSearchUser();
15
+ const { t } = useTranslation();
16
+ const debouncedSearch = useDebounce((query: string) => {
17
+ setSearchString(query);
18
+ }, 1000);
19
+
20
+ const form = useFormContext<{ responsiblePersonUuid: string; responsiblePersonOther: string }>();
21
+ const observableresponsiblePersonUuid = form.watch('responsiblePersonUuid');
22
+ const observableresponsiblePersonOther = form.watch('responsiblePersonOther');
23
+ const {
24
+ data,
25
+ isLoading: isLoadingUser,
26
+ error: userError,
27
+ } = useUser(observableresponsiblePersonUuid ?? null, 'custom:(uuid,display,person:(uuid,display))');
28
+ useEffect(() => {
29
+ // Whenever person uuid changes and not equal to other person, the other is reset to initial default value
30
+ if (observableresponsiblePersonUuid && observableresponsiblePersonUuid !== otherUser.uuid) {
31
+ form.resetField('responsiblePersonOther');
32
+ }
33
+ }, [observableresponsiblePersonUuid, form]);
34
+
35
+ if (isLoadingUser && observableresponsiblePersonUuid !== otherUser.uuid && observableresponsiblePersonUuid)
36
+ return <SelectSkeleton role="progressbar" />;
37
+
38
+ if (observableresponsiblePersonUuid && observableresponsiblePersonUuid !== otherUser.uuid && userError)
39
+ return (
40
+ <InlineNotification
41
+ lowContrast
42
+ title={t('responsiblePersonError', 'Error loading responsible person')}
43
+ subtitle={userError?.message}
44
+ />
45
+ );
46
+
47
+ return (
48
+ <React.Fragment>
49
+ <Column>
50
+ <Controller
51
+ name={'responsiblePersonUuid'}
52
+ control={form.control}
53
+ render={({ field, fieldState: { error } }) => (
54
+ <ComboBox
55
+ readOnly={field.disabled}
56
+ titleText={t('responsiblePerson', 'Responsible Person')}
57
+ name={'responsiblePersonUuid'}
58
+ id={'responsiblePersonUuid'}
59
+ size={'xl'}
60
+ items={[...(userList || []), otherUser]}
61
+ onChange={(data: { selectedItem: User }) => {
62
+ field.onChange(data.selectedItem?.uuid);
63
+ }}
64
+ initialSelectedItem={
65
+ field.value
66
+ ? field.value === otherUser.uuid
67
+ ? otherUser
68
+ : data
69
+ : observableresponsiblePersonOther && !observableresponsiblePersonUuid
70
+ ? otherUser
71
+ : ''
72
+ }
73
+ itemToString={(item) => item?.person?.display || ''}
74
+ onInputChange={debouncedSearch}
75
+ placeholder={t('filter', 'Filter ...')}
76
+ invalid={error?.message}
77
+ invalidText={error?.message}
78
+ ref={field.ref}
79
+ />
80
+ )}
81
+ />
82
+ </Column>
83
+ {(observableresponsiblePersonUuid === otherUser.uuid ||
84
+ (observableresponsiblePersonOther && !observableresponsiblePersonUuid)) && (
85
+ <Column>
86
+ <Controller
87
+ control={form.control}
88
+ name="responsiblePersonOther"
89
+ render={({ field, fieldState: { error } }) => (
90
+ <TextInput
91
+ {...field}
92
+ readOnly={field.disabled}
93
+ disabled={false}
94
+ id="responsiblePersonOther"
95
+ name="responsiblePersonOther"
96
+ size={'xl'}
97
+ labelText={t('responsiblePerson', 'Responsible Person')}
98
+ placeholder={t('pleaseSpecify', 'Please Specify')}
99
+ invalid={error?.message}
100
+ invalidText={error?.message}
101
+ />
102
+ )}
103
+ />
104
+ </Column>
105
+ )}
106
+ </React.Fragment>
107
+ // {isLoading && <InlineLoading status="active" iconDescription="Searching" description="Searching..." />}
108
+ );
109
+ };
110
+
111
+ export default UsersSelector;