@pega/react-sdk-overrides 0.25.5 → 0.25.7

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 (28) hide show
  1. package/lib/designSystemExtension/CaseSummaryFields/CaseSummaryFields.tsx +1 -1
  2. package/lib/field/RadioButtons/RadioButtons.tsx +1 -1
  3. package/lib/field/SelectableCard/utils.tsx +2 -5
  4. package/lib/field/SemanticLink/SemanticLink.tsx +1 -1
  5. package/lib/helpers/attachmentShared.ts +6 -0
  6. package/lib/helpers/common-utils.ts +1 -2
  7. package/lib/helpers/formatters/Currency.ts +9 -4
  8. package/lib/helpers/object-utils.ts +10 -0
  9. package/lib/infra/Assignment/Assignment.tsx +1 -1
  10. package/lib/infra/Containers/FlowContainer/FlowContainer.tsx +2 -2
  11. package/lib/infra/Containers/ModalViewContainer/ModalViewContainer.tsx +4 -3
  12. package/lib/infra/MultiStep/MultiStep.css +0 -2
  13. package/lib/infra/NavBar/NavBar.tsx +2 -3
  14. package/lib/infra/Stages/Stages.tsx +1 -1
  15. package/lib/template/AppShell/AppShell.tsx +5 -0
  16. package/lib/template/CaseSummary/CaseSummary.tsx +65 -4
  17. package/lib/template/CaseView/CaseView.tsx +3 -10
  18. package/lib/template/DataReference/DataReference.tsx +1 -2
  19. package/lib/template/ListView/ListView.tsx +1 -1
  20. package/lib/template/SimpleTable/SimpleTableManual/SimpleTableManual.tsx +1 -1
  21. package/lib/template/SingleReferenceReadOnly/SingleReferenceReadOnly.tsx +9 -1
  22. package/lib/widget/Attachment/Attachment.tsx +284 -210
  23. package/lib/widget/Attachment/Attachment.types.ts +96 -0
  24. package/lib/widget/Attachment/AttachmentUtils.ts +316 -0
  25. package/lib/widget/FileUtility/FileUtility/FileUtility.tsx +25 -16
  26. package/lib/widget/ToDo/ToDo.tsx +10 -5
  27. package/package.json +1 -1
  28. package/lib/helpers/attachmentHelpers.ts +0 -97
@@ -2,9 +2,19 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
2
  import { CircularProgress, IconButton, Menu, MenuItem, Button } from '@mui/material';
3
3
  import MoreVertIcon from '@mui/icons-material/MoreVert';
4
4
 
5
- import { getIconFromFileType, isFileUploadedToServer, useFileDownload, validateMaxSize } from '@pega/react-sdk-components/lib/components/helpers/attachmentHelpers';
6
-
7
5
  import { Utils } from '@pega/react-sdk-components/lib/components/helpers/utils';
6
+ import {
7
+ clearFieldErrorMessages,
8
+ deleteAttachments,
9
+ getIconFromFileType,
10
+ getMappedValue,
11
+ insertAttachments,
12
+ useDeepMemo,
13
+ useFileDownload,
14
+ validateFileExtension
15
+ } from './AttachmentUtils';
16
+ import { validateMaxSize } from '@pega/react-sdk-components/lib/components/helpers/attachmentShared';
17
+ import type { PageInstructionOptions } from './Attachment.types';
8
18
  import type { PConnFieldProps } from '@pega/react-sdk-components/lib/types/PConnProps';
9
19
 
10
20
  import './Attachment.css';
@@ -12,77 +22,58 @@ import './Attachment.css';
12
22
  interface AttachmentProps extends Omit<PConnFieldProps, 'value'> {
13
23
  // If any, enter additional props that only exist on this component
14
24
  value: any;
15
- allowMultiple: string;
25
+ allowMultiple: boolean | string;
16
26
  extensions: string;
27
+ editMode: string;
28
+ isTableFormatter: boolean;
17
29
  }
18
30
 
19
- const getAttachmentKey = (name, embeddedReference) => {
20
- return `attachmentsList${embeddedReference}.${name}`;
21
- };
22
-
23
- const getCurrentAttachmentsList = (key, context) => {
24
- return PCore.getStoreValue(`.${key}`, 'context_data', context) || [];
25
- };
26
-
27
- const updateAttachmentState = (pConn, key, attachments) => {
28
- PCore.getStateUtils().updateState(pConn.getContextName(), key, attachments, {
29
- pageReference: 'context_data',
30
- isArrayDeepMerge: false
31
- });
32
- };
33
-
34
31
  export default function Attachment(props: AttachmentProps) {
35
- const { value, getPConnect, label, validatemessage, allowMultiple, extensions, displayMode, helperText } = props;
32
+ const { value, getPConnect, label, validatemessage, extensions, displayMode, helperText, editMode, isTableFormatter } = props;
36
33
  /* this is a temporary fix because required is supposed to be passed as a boolean and NOT as a string */
37
- let { required, disabled } = props;
38
- [required, disabled] = [required, disabled].map(prop => prop === true || (typeof prop === 'string' && prop === 'true'));
34
+ let { required, disabled, allowMultiple } = props;
35
+ [required, disabled, allowMultiple] = [required, disabled, allowMultiple].map(
36
+ prop => prop === true || (typeof prop === 'string' && prop === 'true')
37
+ );
39
38
  const pConn = getPConnect();
39
+ const localizationService = pConn.getLocalizationService();
40
40
 
41
41
  const actionSequencer = useMemo(() => PCore.getActionsSequencer(), []);
42
- const caseID = PCore.getStoreValue('.pyID', 'caseInfo.content', pConn.getContextName());
42
+ const rawValue = pConn.getComponentConfig().value;
43
+ const isAttachmentAnnotationPresent = typeof rawValue === 'object' ? false : rawValue?.includes('@ATTACHMENT');
44
+ const { attachments, isOldAttachment } = isAttachmentAnnotationPresent ? value : PCore.getAttachmentUtils().prepareAttachmentData(value);
45
+
46
+ let valueRef = (pConn.getStateProps() as any).value;
47
+ valueRef = valueRef.indexOf('.') === 0 ? valueRef.substring(1) : valueRef;
48
+
49
+ pConn.setReferenceList(`.${valueRef}`);
50
+
51
+ const isMultiAttachmentInInlineEditTable = isTableFormatter && allowMultiple && editMode === 'tableRows';
52
+
53
+ const [files, setFiles] = useState<any[]>(attachments);
54
+ const overrideLocalState = useRef(false);
55
+ const attachmentCount = useRef(attachments.length);
56
+ const filesWithError = useRef<any[]>([]);
57
+ const multiAttachmentsInInlineEdit = useRef([]);
58
+ const thumbnailURLs = useRef<any[]>([]);
59
+ const contextName = pConn.getContextName();
60
+ const onFileDownload = useFileDownload(contextName);
61
+
43
62
  const localizedVal = PCore.getLocaleUtils().getLocaleValue;
44
63
  const localeCategory = 'CosmosFields';
45
64
  const uploadMultipleFilesLabel = localizedVal('file_upload_text_multiple', localeCategory);
46
65
  const uploadSingleFileLabel = localizedVal('file_upload_text_one', localeCategory);
47
66
  const deleteIcon = Utils.getImageSrc('trash', Utils.getSDKStaticConentUrl());
48
67
  const srcImg = Utils.getImageSrc('document-doc', Utils.getSDKStaticConentUrl());
49
- let valueRef = (pConn.getStateProps() as any).value;
50
- valueRef = valueRef.indexOf('.') === 0 ? valueRef.substring(1) : valueRef;
68
+
51
69
  const [anchorEl, setAnchorEl] = useState(null);
52
70
  const open = Boolean(anchorEl);
53
71
 
54
- const rawValue = pConn.getComponentConfig().value;
55
- const isAttachmentAnnotationPresent = typeof rawValue === 'object' ? false : rawValue?.includes('@ATTACHMENT');
56
- const { hasUploadedFiles, attachments, categoryName } = isAttachmentAnnotationPresent
57
- ? value
58
- : PCore.getAttachmentUtils().prepareAttachmentData(value);
59
-
60
72
  const fileInputRef = useRef<HTMLInputElement>(null);
61
- const [files, setFiles] = useState<any[]>(attachments);
62
- const [filesWithError, setFilesWithError] = useState<any[]>([]);
63
73
  const [toggleUploadBegin, setToggleUploadBegin] = useState(false);
64
74
 
65
- const context = pConn.getContextName();
66
- const onFileDownload = useFileDownload(context);
67
-
68
- let embeddedProperty = pConn
69
- .getPageReference()
70
- .replace(PCore.getConstants().CASE_INFO.CASE_INFO_CONTENT, '')
71
- .replace(PCore.getConstants().DATA_INFO.DATA_INFO_CONTENT, '');
72
-
73
- if (valueRef?.indexOf('.') > 0) {
74
- embeddedProperty = valueRef.substring(0, valueRef.indexOf('.') + 1);
75
- }
76
-
77
- const resetAttachmentStoredState = () => {
78
- PCore.getStateUtils().updateState(pConn.getContextName(), getAttachmentKey(valueRef, embeddedProperty), undefined, {
79
- pageReference: 'context_data',
80
- isArrayDeepMerge: false
81
- });
82
- };
83
-
84
75
  const deleteFile = useCallback(
85
- file => {
76
+ (file, fileIndex) => {
86
77
  setAnchorEl(null);
87
78
 
88
79
  // reset the file input so that it will allow re-uploading the same file after deletion
@@ -90,84 +81,112 @@ export default function Attachment(props: AttachmentProps) {
90
81
  fileInputRef.current.value = ''; // Reset the input
91
82
  }
92
83
 
93
- let attachmentsList: any[] = [];
94
- let currentAttachmentList = getCurrentAttachmentsList(getAttachmentKey(valueRef, embeddedProperty), pConn.getContextName());
84
+ if (filesWithError.current.length > 0) {
85
+ filesWithError.current = filesWithError.current.filter(fileWithError => fileWithError.props.id !== file.props.id);
86
+ if (filesWithError.current.length === 0) {
87
+ clearFieldErrorMessages(pConn);
88
+ }
89
+ }
95
90
 
96
- // If file to be deleted is the one added in previous stage i.e. for which a file instance is created in server
97
- // no need to filter currentAttachmentList as we will get another entry of file in redux with delete & label
98
- if (hasUploadedFiles && isFileUploadedToServer(file)) {
99
- const updatedAttachments = files.map(f => {
100
- if (f.responseProps && f.responseProps.pzInsKey === file.responseProps.pzInsKey) {
101
- return { ...f, delete: true, label: valueRef };
102
- }
103
- return f;
91
+ if (file.inProgress) {
92
+ // @ts-ignore - Expected 1 arguments, but got 2.ts(2554)
93
+ PCore.getAttachmentUtils().cancelRequest(file.props.id, contextName);
94
+ actionSequencer.deRegisterBlockingAction(contextName).catch(() => {});
95
+ setFiles(localFiles => {
96
+ return localFiles.filter(localFile => localFile.props.id !== file.props.id);
104
97
  });
105
-
106
- // updating the redux store to help form-handler in passing the data to delete the file from server
107
- updateAttachmentState(pConn, getAttachmentKey(valueRef, embeddedProperty), [...updatedAttachments]);
108
- setFiles(current => {
109
- const newlyAddedFiles = current.filter(f => !!f.ID);
110
- const filesPostDelete = current.filter(f => isFileUploadedToServer(f) && f.responseProps?.ID !== file.responseProps?.ID);
111
- attachmentsList = [...filesPostDelete, ...newlyAddedFiles];
112
- return attachmentsList;
113
- });
114
- } // if the file being deleted is the added in this stage i.e. whose data is not yet created in server
115
- else {
116
- // filter newly added files in this stage, later the updated current stage files will be added to redux once files state is updated in below setFiles()
117
- currentAttachmentList = currentAttachmentList.filter(f => !f.props.error && (f.delete || f.label !== valueRef));
118
- setFiles(current => current.filter(f => f.ID !== file.ID));
119
- updateAttachmentState(pConn, getAttachmentKey(valueRef, embeddedProperty), [...currentAttachmentList, ...attachmentsList]);
120
- if (file.inProgress) {
121
- // @ts-expect-error - 3rd parameter "responseEncoding" should be optional
122
- PCore.getAttachmentUtils().cancelRequest(file.ID, pConn.getContextName());
123
- actionSequencer.deRegisterBlockingAction(pConn.getContextName()).catch(error => {
124
- console.log(error);
98
+ } else {
99
+ deleteAttachments([file], pConn, multiAttachmentsInInlineEdit.current, {
100
+ allowMultiple,
101
+ isOldAttachment,
102
+ isMultiAttachmentInInlineEditTable,
103
+ attachmentCount: attachmentCount.current,
104
+ deleteIndex: fileIndex
105
+ } as any);
106
+ // Filter out without deleted file and reset the file indexes
107
+ setFiles(localFiles => {
108
+ let tempLocalFiles = [...localFiles];
109
+ tempLocalFiles = tempLocalFiles.filter(localFile => localFile.props.id !== file.props.id);
110
+ tempLocalFiles.forEach(localFile => {
111
+ if (!localFile.props.error && !file.props.error) {
112
+ const updatedDeleteIndex =
113
+ localFile.responseProps.deleteIndex > fileIndex ? localFile.responseProps.deleteIndex - 1 : localFile.responseProps.deleteIndex;
114
+
115
+ localFile.props.onDelete = () => deleteFile(localFile, updatedDeleteIndex);
116
+
117
+ localFile.responseProps.deleteIndex = updatedDeleteIndex;
118
+ }
125
119
  });
120
+ return tempLocalFiles;
121
+ });
122
+ if (!file.props.error) {
123
+ attachmentCount.current -= 1;
126
124
  }
127
125
  }
128
126
 
129
127
  setToggleUploadBegin(false);
130
- setFilesWithError(prevFilesWithError => {
131
- return prevFilesWithError.filter(f => f.ID !== file.ID);
132
- });
133
128
  },
134
- [valueRef, pConn, hasUploadedFiles, filesWithError, hasUploadedFiles, actionSequencer]
129
+ [pConn]
135
130
  );
136
131
 
137
- const onUploadProgress = () => {};
132
+ const onUploadProgress = (id, ev) => {
133
+ const progress = Math.floor((ev.loaded / ev.total) * 100);
134
+ setFiles(localFiles => [
135
+ ...localFiles.map(localFile => {
136
+ if (localFile.props?.id === id) {
137
+ localFile.inProgress = true;
138
+ localFile.props.progress = progress;
139
+ }
140
+ return localFile;
141
+ })
142
+ ]);
143
+ };
138
144
 
139
- const errorHandler = (isFetchCanceled, attachedFile) => {
145
+ const populateErrorAndUpdateRedux = file => {
146
+ const fieldName = (pConn.getStateProps() as any).value;
147
+ // set errors to property to block submit even on errors in file upload
148
+ PCore.getMessageManager().addMessages({
149
+ messages: [
150
+ {
151
+ type: 'error',
152
+ message: localizationService.getLocalizedText('Error with one or more files')
153
+ }
154
+ ],
155
+ property: fieldName,
156
+ pageReference: pConn.getPageReference(),
157
+ context: contextName
158
+ });
159
+ insertAttachments([file], pConn, multiAttachmentsInInlineEdit.current, {
160
+ allowMultiple,
161
+ isOldAttachment,
162
+ isMultiAttachmentInInlineEditTable,
163
+ attachmentCount: attachmentCount.current
164
+ } as any);
165
+ };
166
+
167
+ const errorHandler = (isFetchCanceled, file) => {
140
168
  return error => {
141
169
  if (!isFetchCanceled(error)) {
142
- let uploadFailMsg = pConn.getLocalizedValue('Something went wrong', '', '');
170
+ let uploadFailMsg = localizationService.getLocalizedText('Something went wrong');
143
171
  if (error.response && error.response.data && error.response.data.errorDetails) {
144
- uploadFailMsg = pConn.getLocalizedValue(error.response.data.errorDetails[0].localizedValue, '', '');
172
+ uploadFailMsg = localizationService.getLocalizedText(error.response.data.errorDetails[0].localizedValue);
145
173
  }
174
+
146
175
  setFiles(current => {
147
- return current.map(f => {
148
- if (f.ID === attachedFile.ID) {
149
- f.props.meta = uploadFailMsg;
150
- f.props.error = true;
151
- f.props.onDelete = () => deleteFile(f);
152
- f.props.icon = getIconFromFileType(f.type);
153
- f.props.name = pConn.getLocalizedValue('Unable to upload file', '', '');
154
- f.inProgress = false;
155
- const fieldName = (pConn.getStateProps() as any).value;
156
- // set errors to property to block submit even on errors in file upload
157
- PCore.getMessageManager().addMessages({
158
- messages: [
159
- {
160
- type: 'error',
161
- message: pConn.getLocalizedValue('Error with one or more files', '', '')
162
- }
163
- ],
164
- property: fieldName,
165
- pageReference: pConn.getPageReference(),
166
- context
167
- });
168
- delete f.props.progress;
176
+ return current.map((localFile, index) => {
177
+ if (localFile.props.id === file.props.id) {
178
+ localFile.props.meta = uploadFailMsg;
179
+ localFile.props.error = true;
180
+ localFile.props.onDelete = () => deleteFile(localFile, index);
181
+ localFile.props.icon = getIconFromFileType(localFile.type);
182
+ localFile.props.name = localizationService.getLocalizedText('Unable to upload file');
183
+ localFile.inProgress = false;
184
+ delete localFile.props.progress;
185
+ filesWithError.current.push(localFile);
186
+
187
+ populateErrorAndUpdateRedux(localFile);
169
188
  }
170
- return f;
189
+ return localFile;
171
190
  });
172
191
  });
173
192
  }
@@ -175,50 +194,28 @@ export default function Attachment(props: AttachmentProps) {
175
194
  };
176
195
  };
177
196
 
178
- const validateFileExtension = (fileObj, allowedExtensions) => {
179
- if (!allowedExtensions) {
180
- return true;
181
- }
182
- const allowedExtensionList = allowedExtensions
183
- .toLowerCase()
184
- .split(',')
185
- .map(item => item.replaceAll('.', '').trim());
186
- const extension = fileObj.name.split('.').pop().toLowerCase();
187
- return allowedExtensionList.includes(extension);
188
- };
189
-
190
- const clearFieldErrorMessages = () => {
191
- const fieldName = (pConn.getStateProps() as any).value;
192
- PCore.getMessageManager().clearMessages({
193
- type: PCore.getConstants().MESSAGES.MESSAGES_TYPE_ERROR,
194
- property: fieldName,
195
- pageReference: pConn.getPageReference(),
196
- context
197
- });
198
- };
199
-
200
197
  const onFileAdded = event => {
201
198
  let addedFiles = Array.from(event.target.files);
202
- addedFiles = allowMultiple === 'true' ? addedFiles : [addedFiles[0]];
199
+ addedFiles = allowMultiple ? addedFiles : [addedFiles[0]];
203
200
  const maxAttachmentSize = PCore.getEnvironmentInfo().getMaxAttachmentSize() || '5';
204
201
  const tempFilesToBeUploaded = [
205
202
  ...addedFiles.map((f: any, index) => {
206
203
  f.ID = `${new Date().getTime()}I${index}`;
207
- f.inProgress = true;
208
204
  f.props = {
209
205
  type: f.type,
210
206
  name: f.name,
207
+ id: f.ID,
208
+ format: f.name.split('.').pop(),
211
209
  icon: getIconFromFileType(f.type),
212
- onDelete: () => deleteFile(f)
210
+ onDelete: () => deleteFile(f, index),
211
+ thumbnail: window.URL.createObjectURL(f)
213
212
  };
214
213
  if (!validateMaxSize(f, maxAttachmentSize)) {
215
214
  f.props.error = true;
216
- f.inProgress = false;
217
- f.props.meta = pConn.getLocalizedValue(`File is too big. Max allowed size is ${maxAttachmentSize}MB.`, '', '');
215
+ f.props.meta = localizationService.getLocalizedText(`File is too big. Max allowed size is ${maxAttachmentSize}MB.`);
218
216
  } else if (!validateFileExtension(f, extensions)) {
219
217
  f.props.error = true;
220
- f.inProgress = false;
221
- f.props.meta = `${pConn.getLocalizedValue('File has invalid extension. Allowed extensions are:', '', '')} ${extensions.replaceAll(
218
+ f.props.meta = `${localizationService.getLocalizedText('File has invalid extension. Allowed extensions are:')} ${extensions.replaceAll(
222
219
  '.',
223
220
  ''
224
221
  )}`;
@@ -229,22 +226,30 @@ export default function Attachment(props: AttachmentProps) {
229
226
  messages: [
230
227
  {
231
228
  type: 'error',
232
- message: pConn.getLocalizedValue('Error with one or more files', '', '')
229
+ message: localizationService.getLocalizedText('Error with one or more files')
233
230
  }
234
231
  ],
235
232
  property: fieldName,
236
233
  pageReference: pConn.getPageReference(),
237
- context
234
+ context: contextName
238
235
  });
239
236
  }
240
237
  return f;
241
238
  })
242
239
  ];
240
+
243
241
  const tempFilesWithError = tempFilesToBeUploaded.filter(f => f.props.error);
244
242
  if (tempFilesWithError.length > 0) {
245
- setFilesWithError(tempFilesWithError);
243
+ filesWithError.current = [...filesWithError.current, ...tempFilesWithError];
244
+
245
+ insertAttachments(tempFilesWithError, pConn, multiAttachmentsInInlineEdit.current, {
246
+ allowMultiple,
247
+ isOldAttachment,
248
+ isMultiAttachmentInInlineEditTable,
249
+ attachmentCount: attachmentCount.current
250
+ } as PageInstructionOptions);
246
251
  }
247
- setFiles(current => (allowMultiple !== 'true' ? [...tempFilesToBeUploaded] : [...current, ...tempFilesToBeUploaded]));
252
+ setFiles(current => (!allowMultiple ? [...tempFilesToBeUploaded] : [...current, ...tempFilesToBeUploaded]));
248
253
  setToggleUploadBegin(true);
249
254
  };
250
255
 
@@ -253,102 +258,129 @@ export default function Attachment(props: AttachmentProps) {
253
258
  .filter(e => {
254
259
  const isFileUploaded = e.props && e.props.progress === 100;
255
260
  const fileHasError = e.props && e.props.error;
256
- const isFileUploadedinLastStep = e.responseProps && e.responseProps.pzInsKey;
257
- return !isFileUploaded && !fileHasError && !isFileUploadedinLastStep;
261
+ const isFileUploadedInLastStep = e.responseProps && e.responseProps.ID !== 'temp';
262
+ const isFileUploadInProgress = e.inProgress;
263
+ return !isFileUploadInProgress && !isFileUploaded && !fileHasError && !isFileUploadedInLastStep;
258
264
  })
259
- .map(f =>
260
- window.PCore.getAttachmentUtils().uploadAttachment(
261
- f,
262
- () => {
263
- onUploadProgress();
265
+ .map(file =>
266
+ PCore.getAttachmentUtils().uploadAttachment(
267
+ file,
268
+ ev => {
269
+ onUploadProgress(file.props.id, ev);
264
270
  },
265
271
  isFetchCanceled => {
266
- return errorHandler(isFetchCanceled, f);
272
+ return errorHandler(isFetchCanceled, file);
267
273
  },
268
- pConn.getContextName()
274
+ contextName
269
275
  )
270
276
  );
277
+
278
+ // allow new files to be added when other files upload is still in progress
279
+ setToggleUploadBegin(false);
271
280
  Promise.allSettled(filesToBeUploaded)
272
281
  .then((fileResponses: any) => {
273
282
  fileResponses = fileResponses.filter(fr => fr.status !== 'rejected'); // in case of deleting an in progress file, promise gets cancelled but still enters then block
274
283
  if (fileResponses.length > 0) {
275
- setFiles(current => {
276
- const tempFilesUploaded = [...current];
277
- tempFilesUploaded.forEach(f => {
278
- const index = fileResponses.findIndex((fr: any) => fr.value.clientFileID === f.ID);
284
+ setFiles(localFiles => {
285
+ const tempFilesUploaded = [...localFiles];
286
+ tempFilesUploaded.forEach(localFile => {
287
+ // if attach field has multiple files & in bw any error files are present
288
+ // Example : files = [properFile1, errFile, errFile, properFile2]
289
+ // indexes for delete & preview should be for files [properFile1, properFile2] which is [1,2]
290
+ const index = fileResponses.findIndex(fileResponse => fileResponse.value.clientFileID === localFile.props.id);
279
291
  if (index >= 0) {
280
- f.props.meta = pConn.getLocalizedValue('Uploaded successfully', '', '');
281
- f.props.progress = 100;
282
- f.inProgress = false;
283
- f.handle = fileResponses[index].value.ID;
284
- f.label = valueRef;
285
- f.category = categoryName;
286
- f.responseProps = {
287
- pzInsKey: 'temp',
288
- pyAttachName: f.props.name
292
+ fileResponses[index].value.thumbnail = localFile.props.thumbnail;
293
+ localFile.inProgress = false;
294
+ localFile.ID = fileResponses[index].value.ID;
295
+ localFile.props.meta = localizationService.getLocalizedText('Uploaded successfully');
296
+ localFile.props.progress = 100;
297
+ localFile.handle = fileResponses[index].value.ID;
298
+ localFile.label = valueRef;
299
+ localFile.responseProps = {
300
+ pzInsKey: 'temp'
289
301
  };
290
302
  }
291
303
  });
292
304
  return tempFilesUploaded;
293
305
  });
294
306
 
295
- if (filesWithError.length === 0) {
296
- clearFieldErrorMessages();
307
+ insertAttachments(fileResponses, pConn, multiAttachmentsInInlineEdit.current, {
308
+ allowMultiple,
309
+ isOldAttachment,
310
+ isMultiAttachmentInInlineEditTable,
311
+ attachmentCount: attachmentCount.current,
312
+ insert: true
313
+ } as any);
314
+ attachmentCount.current += fileResponses.length;
315
+
316
+ if (filesWithError.current.length === 0) {
317
+ clearFieldErrorMessages(pConn);
297
318
  }
298
319
  }
299
- setToggleUploadBegin(false);
320
+ actionSequencer.deRegisterBlockingAction(contextName).catch(() => {});
300
321
  })
301
322
  .catch(error => {
302
323
  console.log(error);
303
324
  setToggleUploadBegin(false);
304
325
  });
305
- }, [files, filesWithError]);
326
+ }, [files]);
306
327
 
307
328
  useEffect(() => {
308
329
  if (toggleUploadBegin && files.length > 0) {
309
- uploadFiles();
330
+ actionSequencer.registerBlockingAction(contextName).then(() => {
331
+ uploadFiles();
332
+ });
310
333
  }
311
334
  }, [toggleUploadBegin]);
312
335
 
313
336
  useEffect(() => {
314
- if (files.length > 0 && displayMode !== 'DISPLAY_ONLY') {
315
- const currentAttachmentList = getCurrentAttachmentsList(getAttachmentKey(valueRef, embeddedProperty), pConn.getContextName());
316
- // block duplicate files to redux store when added 1 after another to prevent multiple duplicates being added to the case on submit
317
- const tempFiles = files.filter(f => currentAttachmentList.findIndex(fr => fr.ID === f.ID) === -1 && !f.inProgress && f.responseProps);
318
-
319
- const updatedAttList = [...currentAttachmentList, ...tempFiles];
320
- updateAttachmentState(pConn, getAttachmentKey(valueRef, embeddedProperty), updatedAttList);
321
- }
322
- }, [files]);
323
-
324
- useEffect(() => {
325
- if (filesWithError.length === 0) {
326
- clearFieldErrorMessages();
337
+ if (filesWithError.current.length === 0) {
338
+ clearFieldErrorMessages(pConn);
327
339
  }
328
340
  }, [filesWithError]);
329
341
 
330
- useEffect(() => {
331
- let tempUploadedFiles = getCurrentAttachmentsList(getAttachmentKey(valueRef, embeddedProperty), pConn.getContextName());
332
- tempUploadedFiles = tempUploadedFiles.filter(f => f.label === valueRef);
333
- setFiles(current => {
334
- return [
335
- ...current.map(f => {
336
- return f.responseProps.pzInsKey && !f.responseProps.pzInsKey.includes('temp')
337
- ? {
338
- ...f,
339
- props: {
340
- ...f.props,
341
- onDelete: () => deleteFile(f)
342
- }
343
- }
344
- : { ...f };
345
- }),
346
- ...tempUploadedFiles
347
- ];
342
+ const memoizedAttachments = useDeepMemo(() => {
343
+ return attachments;
344
+ }, [attachments]);
345
+
346
+ // Prepares new structure as per Cosmos component
347
+ const transformAttachments = () => {
348
+ const transformedFiles = [...attachments];
349
+ let deleteIndex = -1;
350
+ transformedFiles.forEach(attachment => {
351
+ attachment.props.id = attachment.responseProps.ID;
352
+ attachment.props.format = attachment.props.name.split('.').pop();
353
+ if (attachment.props.error) {
354
+ attachment.responseProps.deleteIndex = deleteIndex;
355
+ } else {
356
+ deleteIndex += 1;
357
+ attachment.responseProps.deleteIndex = deleteIndex;
358
+ }
359
+ if (attachment.props.thumbnail) {
360
+ thumbnailURLs.current.push(attachment.props.thumbnail);
361
+ }
348
362
  });
363
+
364
+ return transformedFiles;
365
+ };
366
+
367
+ useEffect(() => {
368
+ const caseID = PCore.getStoreValue(`.${getMappedValue('pyID')}`, PCore.getResolvedConstantValue('caseInfo.content'), contextName);
349
369
  if (displayMode !== 'DISPLAY_ONLY') {
350
- PCore.getPubSubUtils().subscribe(PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS.ASSIGNMENT_SUBMISSION, resetAttachmentStoredState, caseID);
370
+ PCore.getPubSubUtils().subscribe(
371
+ PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS.ASSIGNMENT_SUBMISSION,
372
+ () => {
373
+ overrideLocalState.current = true;
374
+ },
375
+ caseID
376
+ );
351
377
  }
378
+
379
+ // When component mounts, only set local files state from redux.
380
+ const serverFiles = transformAttachments();
381
+ setFiles(serverFiles);
382
+ filesWithError.current = serverFiles.filter(file => file.props.error);
383
+
352
384
  return () => {
353
385
  if (displayMode !== 'DISPLAY_ONLY') {
354
386
  PCore.getPubSubUtils().unsubscribe(PCore.getConstants().PUB_SUB_EVENTS.CASE_EVENTS.ASSIGNMENT_SUBMISSION, caseID);
@@ -356,6 +388,48 @@ export default function Attachment(props: AttachmentProps) {
356
388
  };
357
389
  }, []);
358
390
 
391
+ useEffect(() => {
392
+ if (overrideLocalState.current) {
393
+ const serverFiles = transformAttachments();
394
+ overrideLocalState.current = false;
395
+ attachmentCount.current = attachments.length;
396
+ filesWithError.current = [];
397
+ setFiles(serverFiles);
398
+ } else {
399
+ // Determine whether refresh call has overridden any error files in redux, push error files back to redux from local state to perform client side validation during assignment submit
400
+ const errorFiles = attachments.filter(attachment => attachment.props.error);
401
+ if (errorFiles.length === 0 && filesWithError.current.length > 0) {
402
+ // Check if local file state contains error files and push those to redux
403
+ const uniqueKey = getMappedValue('pzInsKey');
404
+ const transformedErrorFiles = filesWithError.current.map(errorFile => {
405
+ const filename = errorFile.props.name;
406
+ return {
407
+ [uniqueKey]: errorFile.props.id,
408
+ FileName: filename,
409
+ Category: '',
410
+ FileExtension: filename.split('.').pop() ?? filename,
411
+ error: errorFile.props.error || null
412
+ };
413
+ });
414
+ let key = '';
415
+ let updatedAttachments: any = [];
416
+ if (allowMultiple || isOldAttachment) {
417
+ key = isOldAttachment ? `${valueRef}.pxResults` : valueRef;
418
+ const existingAttachments = PCore.getStoreValue(`.${key}`, pConn.getPageReference(), pConn.getContextName()) || [];
419
+ updatedAttachments = [...existingAttachments, ...transformedErrorFiles];
420
+ } else {
421
+ key = valueRef;
422
+ updatedAttachments = transformedErrorFiles[0];
423
+ }
424
+ PCore.getStateUtils().updateState(pConn.getContextName(), key, updatedAttachments, {
425
+ pageReference: pConn.getPageReference(),
426
+ isArrayDeepMerge: false,
427
+ removePropertyFromChangedList: true
428
+ });
429
+ }
430
+ }
431
+ }, [memoizedAttachments]);
432
+
359
433
  const handleClick = event => {
360
434
  setAnchorEl(event.currentTarget);
361
435
  };
@@ -376,13 +450,13 @@ export default function Attachment(props: AttachmentProps) {
376
450
  id={valueRef}
377
451
  name='upload-photo'
378
452
  type='file'
379
- multiple={allowMultiple === 'true'}
453
+ multiple={allowMultiple}
380
454
  required={required}
381
455
  disabled={disabled}
382
456
  onChange={onFileAdded}
383
457
  />
384
458
  <Button style={{ textTransform: 'none' }} variant='outlined' color='primary' component='span'>
385
- {allowMultiple === 'true'
459
+ {allowMultiple
386
460
  ? uploadMultipleFilesLabel === 'file_upload_text_multiple'
387
461
  ? 'Choose files'
388
462
  : uploadMultipleFilesLabel
@@ -416,7 +490,7 @@ export default function Attachment(props: AttachmentProps) {
416
490
  </div>
417
491
  <div className='psdk-utility-action'>
418
492
  {item.ID && (
419
- <button type='button' className='psdk-utility-button' aria-label='Delete Attachment' onClick={() => deleteFile(item)}>
493
+ <button type='button' className='psdk-utility-button' aria-label='Delete Attachment' onClick={() => deleteFile(item, index)}>
420
494
  <img className='psdk-utility-card-action-svg-icon' src={deleteIcon} />
421
495
  </button>
422
496
  )}
@@ -443,7 +517,7 @@ export default function Attachment(props: AttachmentProps) {
443
517
  >
444
518
  Download
445
519
  </MenuItem>
446
- <MenuItem style={{ fontSize: '14px' }} key='delete' onClick={() => deleteFile(item)}>
520
+ <MenuItem style={{ fontSize: '14px' }} key='delete' onClick={() => deleteFile(item, index)}>
447
521
  Delete
448
522
  </MenuItem>
449
523
  </Menu>
@@ -459,7 +533,7 @@ export default function Attachment(props: AttachmentProps) {
459
533
  return (
460
534
  <div className='file-upload-container'>
461
535
  <span className={`label ${required ? 'file-label' : ''}`}>{label}</span>
462
- {((files.length === 0 && allowMultiple !== 'true') || allowMultiple === 'true') && <section>{content}</section>}
536
+ {((files.length === 0 && !allowMultiple) || allowMultiple) && <section>{content}</section>}
463
537
  {validatemessage !== '' ? <span className='file-error'>{validatemessage}</span> : <span style={{ fontSize: '14px' }}>{helperText}</span>}
464
538
  {files && files.length > 0 && <section>{fileDisplay}</section>}
465
539
  </div>