@antscorp/antsomi-ui 2.0.121 → 2.0.123

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 (32) hide show
  1. package/CHANGELOG.md +92 -92
  2. package/es/components/organism/BasicLogin/components/AuthenticateResult/AuthenticateResult.js +1 -1
  3. package/es/components/organism/BasicLogin/components/ChooseMethod/ChooseMethod.js +37 -19
  4. package/es/components/organism/BasicLogin/components/ChooseMethod/Method.d.ts +1 -0
  5. package/es/components/organism/BasicLogin/components/ChooseMethod/Method.js +4 -5
  6. package/es/components/organism/BasicLogin/components/ChooseMethod/styled.d.ts +10 -3
  7. package/es/components/organism/BasicLogin/components/ChooseMethod/styled.js +54 -6
  8. package/es/components/organism/BasicLogin/components/LoginWithEmail/LoginWithEmail.js +2 -2
  9. package/es/components/organism/BasicLogin/components/LoginWithGG/LoginWithGG.js +3 -3
  10. package/es/components/organism/Login/components/ChooseMethod/ChooseMethod.js +36 -19
  11. package/es/components/organism/Login/components/ChooseMethod/ChooseMethod.spec.d.ts +1 -0
  12. package/es/components/organism/Login/components/ChooseMethod/ChooseMethod.spec.js +50 -0
  13. package/es/components/organism/Login/components/ChooseMethod/Method.d.ts +1 -0
  14. package/es/components/organism/Login/components/ChooseMethod/Method.js +4 -5
  15. package/es/components/organism/Login/components/ChooseMethod/styled.d.ts +10 -3
  16. package/es/components/organism/Login/components/ChooseMethod/styled.js +54 -6
  17. package/es/components/organism/Login/components/LoginSelectPortal/LoginSelectPortal.js +1 -1
  18. package/es/components/organism/Login/components/LoginWithEmail/LoginWithEmail.js +1 -1
  19. package/es/components/organism/Login/components/LoginWithGG/LoginWithGG.js +1 -1
  20. package/es/components/organism/Login/components/WidgetLayout/styled.d.ts +3 -0
  21. package/es/components/organism/Login/components/WidgetLayout/styled.js +6 -0
  22. package/es/components/organism/Login/styled.d.ts +1 -0
  23. package/es/components/organism/Login/styled.js +1 -0
  24. package/es/components/organism/TicketEditor/Content.js +74 -35
  25. package/es/components/organism/TicketEditor/util.d.ts +1 -0
  26. package/es/components/organism/TicketEditor/util.js +1 -0
  27. package/es/components/organism/TicketEditorV2/Content.js +62 -28
  28. package/es/components/organism/TicketEditorV2/util.d.ts +1 -0
  29. package/es/components/organism/TicketEditorV2/util.js +1 -0
  30. package/es/utils/uploadEmbeddedImages.d.ts +7 -0
  31. package/es/utils/uploadEmbeddedImages.js +65 -0
  32. package/package.json +1 -1
@@ -1,10 +1,17 @@
1
- /// <reference types="react" />
2
1
  export declare const TextInfo: import("styled-components").StyledComponent<"span", any, {
3
2
  isMobile: boolean;
4
3
  }, never>;
5
4
  export declare const SubTitle: import("styled-components").StyledComponent<"span", any, {
6
5
  isMobile: boolean;
7
6
  }, never>;
8
- export declare const BlockMethod: import("styled-components").StyledComponent<import("react").ForwardRefExoticComponent<import("antd").FlexProps<import("antd/es/_util/type").AnyObject> & import("react").RefAttributes<HTMLElement>>, any, {
9
- disable?: boolean | undefined;
7
+ export declare const BlockMethod: import("styled-components").StyledComponent<"button", any, {
8
+ $disable?: boolean | undefined;
9
+ }, never>;
10
+ export declare const MethodIcon: import("styled-components").StyledComponent<"span", any, {}, never>;
11
+ export declare const MethodContent: import("styled-components").StyledComponent<"span", any, {}, never>;
12
+ export declare const MethodLabel: import("styled-components").StyledComponent<"span", any, {
13
+ $disable?: boolean | undefined;
14
+ }, never>;
15
+ export declare const MethodDescription: import("styled-components").StyledComponent<"span", any, {
16
+ $disable?: boolean | undefined;
10
17
  }, never>;
@@ -1,4 +1,3 @@
1
- import { Flex } from 'antd';
2
1
  import styled from 'styled-components';
3
2
  export const TextInfo = styled.span `
4
3
  color: #7f7f7f;
@@ -9,16 +8,65 @@ export const SubTitle = styled.span `
9
8
  font-size: ${(props) => (props.isMobile ? ' 16px ' : '14px')};
10
9
  font-weight: 500;
11
10
  `;
12
- export const BlockMethod = styled(Flex) `
11
+ export const BlockMethod = styled.button `
12
+ display: flex;
13
13
  align-items: center;
14
14
  gap: 10px;
15
+ width: 100%;
16
+ min-height: 64px;
15
17
  padding: 10px;
16
- cursor: ${props => (props.disable ? 'not-allowed' : 'pointer')};
18
+ box-sizing: border-box;
19
+ cursor: ${props => (props.$disable ? 'not-allowed' : 'pointer')};
20
+ background-color: #ffffff;
21
+ border: 1px solid #b8cfe6;
17
22
  border-radius: 10px;
23
+ font-family: inherit;
24
+ text-align: left;
25
+ opacity: ${props => (props.$disable ? '0.5' : '1')};
18
26
 
19
- opacity: ${props => (props.disable ? '0.5' : '1')};
27
+ &:hover:not(:disabled) {
28
+ background-color: #f2f9ff;
29
+ border-color: #81bcf4;
30
+ }
31
+
32
+ &:focus-visible {
33
+ outline: 2px solid #005eb8;
34
+ outline-offset: 2px;
35
+ }
36
+ `;
37
+ export const MethodIcon = styled.span `
38
+ display: flex;
39
+ flex: 0 0 24px;
40
+ align-self: flex-start;
41
+ align-items: flex-start;
42
+ justify-content: flex-start;
43
+ margin-top: 2px;
44
+
45
+ svg {
46
+ width: 24px;
47
+ height: 24px;
48
+ }
49
+ `;
50
+ export const MethodContent = styled.span `
51
+ display: flex;
52
+ flex: 1;
53
+ min-width: 0;
54
+ flex-direction: column;
55
+ gap: 5px;
56
+ `;
57
+ export const MethodLabel = styled.span `
58
+ color: ${props => (props.$disable ? '#9c9b9b' : '#000000')};
59
+ font-size: 14px;
60
+
61
+ @media (max-width: 768px) {
62
+ font-size: 16px;
63
+ }
64
+ `;
65
+ export const MethodDescription = styled.span `
66
+ color: ${props => (props.$disable ? '#9c9b9b' : '#595959')};
67
+ font-size: 12px;
20
68
 
21
- &:hover {
22
- background-color: ${props => (props.disable ? '#f2f2f2' : '#e6f0ff')};
69
+ @media (max-width: 768px) {
70
+ font-size: 13px;
23
71
  }
24
72
  `;
@@ -177,7 +177,7 @@ export const LoginSelectPortal = props => {
177
177
  return draftListNetwork.filter(netWork => netWork.networkName.toLocaleLowerCase().indexOf(state.portalSearch.toLocaleLowerCase()) !==
178
178
  -1);
179
179
  }, [listNetworks, state.portalSearch]);
180
- return (_jsx(WidgetLayout, { header: _jsx(Avatar, { image: get(loginData, 'personal.avatar'), name: get(loginData, 'personal.full_name'), email: get(loginData, 'personal.email') }), gapContent: 30, isShowLogo: false, title: "Select Portal", content: _jsxs(Flex, { vertical: true, gap: 30, children: [_jsxs(Flex, { vertical: true, gap: 20, children: [_jsx(StyleInput, { isMobile: isMobile, placeholder: "Search", suffix: _jsx(Icon, { type: "icon-ants-search-2", style: {
180
+ return (_jsx(WidgetLayout, { header: _jsx(Avatar, { image: get(loginData, 'personal.avatar'), name: get(loginData, 'personal.full_name'), email: get(loginData, 'personal.email') }), gapContent: 20, isShowLogo: false, title: "Select Portal", content: _jsxs(Flex, { vertical: true, gap: 30, children: [_jsxs(Flex, { vertical: true, gap: 20, children: [_jsx(StyleInput, { isMobile: isMobile, placeholder: "Search", suffix: _jsx(Icon, { type: "icon-ants-search-2", style: {
181
181
  fontSize: 24,
182
182
  }, color: "#595959" }), onChange: e => setPortalSearch(e.target.value) }), isEmpty(filterListNetworks) ? (_jsxs(Flex, { vertical: true, align: "center", justify: "center", style: { height: 514 }, gap: 15, children: [!isMobile && _jsx(Image, { src: NoData, alt: "", width: 80, height: 80 }), _jsx(Text, { style: {
183
183
  color: '#595959',
@@ -205,7 +205,7 @@ export const LoginWithEmail = ({ loginData, domainLogin, codeKey = 'code', token
205
205
  const baseText = `Please enter the 6-digit code send to ${maskedEmail} Haven't received it? `;
206
206
  return (_jsx(Flex, { gap: 5, align: "center", children: _jsxs(Text, { isMobile: isMobile, color: "#595959", children: [baseText, state.isShowResend ? (_jsx(Text, { color: state.isDisableResend ? '#a2a2a2' : '#005eb8', onClick: state.isDisableResend ? undefined : onResendCode, isMobile: isMobile, style: { cursor: state.isDisableResend ? 'not-allowed' : 'pointer' }, children: "Resend." })) : (`Resend code in ${seconds}s.`)] }) }));
207
207
  };
208
- return (_jsx(WidgetLayout, { header: _jsx(Avatar, { image: userAvatar, name: userFullName, email: userEmail }), isShowLogo: false, title: _jsx(Title, { isMobile: isMobile, style: { marginTop: 20 }, children: "Identity Verification" }), gapContent: 30, content: _jsxs(Flex, { vertical: true, gap: 50, style: { marginTop: 20 }, children: [_jsxs(Flex, { vertical: true, gap: 20, align: "center", children: [_jsx("img", { src: loginStep2Image, alt: "Identity Verification" }), _jsx(Text, { isMobile: isMobile, color: "#595959", children: "For added security, we need to verify your identity." })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 10 : 15, children: [renderResendSection(), _jsxs(Flex, { vertical: true, gap: isMobile ? 20 : 11, children: [_jsxs(Flex, { vertical: true, gap: 10, children: [_jsx(StyleInput, { isMobile: isMobile, maxLength: CODE_LENGTH, placeholder: "Input your code", value: state.code, onChange: handleCodeChange, status: errorRef.current && state.codeError ? 'error' : undefined, disabled: state.isTooManyMistakes }), renderErrorMessages()] }), _jsx(Checkbox, { checked: state.isTrust, onChange: handleTrustChange, disabled: state.isTooManyMistakes, children: _jsx(TextInfo, { isMobile: isMobile, children: "Remember this browser for future logins" }) })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 30 : 15, style: { marginTop: isMobile ? 20 : 15 }, children: [_jsx(StyleButton, { isMobile: isMobile, type: "primary", onClick: signIn, loading: state.isLoading, disabled: state.isTooManyMistakes, children: "Verify" }), _jsx(StyleButton, { isMobile: isMobile, onClick: handleBack, type: "text", children: "Back to login" })] })] })] }), footer: _jsx(Flex, { vertical: true, gap: 115, children: _jsx(Flex, { justify: "center", children: _jsx(Button, { type: "text", onClick: handleBack, style: {
208
+ return (_jsx(WidgetLayout, { header: _jsx(Avatar, { image: userAvatar, name: userFullName, email: userEmail }), isShowLogo: false, title: _jsx(Title, { isMobile: isMobile, style: { marginTop: 20 }, children: "Identity Verification" }), gapContent: 20, content: _jsxs(Flex, { vertical: true, gap: 50, children: [_jsxs(Flex, { vertical: true, gap: 20, align: "center", children: [_jsx("img", { src: loginStep2Image, alt: "Identity Verification" }), _jsx(Text, { isMobile: isMobile, color: "#595959", isCenter: true, children: "For your security, we need to verify your identity before continuing." })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 10 : 15, children: [renderResendSection(), _jsxs(Flex, { vertical: true, gap: isMobile ? 20 : 11, children: [_jsxs(Flex, { vertical: true, gap: 10, children: [_jsx(StyleInput, { isMobile: isMobile, maxLength: CODE_LENGTH, placeholder: "Input your code", value: state.code, onChange: handleCodeChange, status: errorRef.current && state.codeError ? 'error' : undefined, disabled: state.isTooManyMistakes }), renderErrorMessages()] }), _jsx(Checkbox, { checked: state.isTrust, onChange: handleTrustChange, disabled: state.isTooManyMistakes, children: _jsx(TextInfo, { isMobile: isMobile, children: "Remember this browser for future logins" }) })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 30 : 15, style: { marginTop: isMobile ? 20 : 15 }, children: [_jsx(StyleButton, { isMobile: isMobile, type: "primary", onClick: signIn, loading: state.isLoading, disabled: state.isTooManyMistakes, children: "Verify" }), _jsx(StyleButton, { isMobile: isMobile, onClick: handleBack, type: "text", children: "Back to login" })] })] })] }), footer: _jsx(Flex, { vertical: true, gap: 115, children: _jsx(Flex, { justify: "center", children: _jsx(Button, { type: "text", onClick: handleBack, style: {
209
209
  fontSize: isMobile ? '16px' : '14px',
210
210
  }, children: "Sign in with a different account" }) }) }) }));
211
211
  };
@@ -98,7 +98,7 @@ export const LoginWithGG = props => {
98
98
  });
99
99
  }
100
100
  }, [state.code]);
101
- return (_jsx(WidgetLayout, { header: _jsx(Avatar, { image: get(loginData, 'personal.avatar'), name: get(loginData, 'personal.full_name'), email: get(loginData, 'personal.email') }), isShowLogo: false, title: _jsx(Title, { isMobile: isMobile, style: { marginTop: 20 }, children: "Identity Verification" }), gapContent: 30, content: _jsxs(Flex, { vertical: true, gap: 50, style: { marginTop: 20 }, children: [_jsxs(Flex, { vertical: true, gap: 20, align: "center", children: [_jsx("img", { src: loginStep2Image, alt: "" }), _jsx(Text, { isMobile: isMobile, color: "#595959", children: "For added security, we need to verify your identity." })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 10 : 15, children: [_jsxs(Flex, { gap: 5, align: "center", children: [_jsx(Text, { isMobile: isMobile, color: "#595959", children: "Please enter the 6-digit code registered to your device." }), _jsx(IconInfo, { image: step2, title: _jsx(TextAntsomiUI, { style: {
101
+ return (_jsx(WidgetLayout, { header: _jsx(Avatar, { image: get(loginData, 'personal.avatar'), name: get(loginData, 'personal.full_name'), email: get(loginData, 'personal.email') }), isShowLogo: false, title: _jsx(Title, { isMobile: isMobile, style: { marginTop: 20 }, children: "Identity Verification" }), gapContent: 20, content: _jsxs(Flex, { vertical: true, gap: 50, children: [_jsxs(Flex, { vertical: true, gap: 20, align: "center", children: [_jsx("img", { src: loginStep2Image, alt: "" }), _jsx(Text, { isMobile: isMobile, color: "#595959", isCenter: true, children: "For your security, we need to verify your identity before continuing." })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 10 : 15, children: [_jsxs(Flex, { gap: 5, align: "center", children: [_jsx(Text, { isMobile: isMobile, color: "#595959", children: "Please enter the 6-digit code registered to your device." }), _jsx(IconInfo, { image: step2, title: _jsx(TextAntsomiUI, { style: {
102
102
  marginTop: 20,
103
103
  textAlign: 'center',
104
104
  }, children: _jsxs(Text, { isMobile: isMobile, children: ["Open Google Authenticator and enter the code from ", _jsx("b", { children: "antsomi.com" })] }) }) })] }), _jsxs(Flex, { vertical: true, gap: isMobile ? 20 : 11, children: [_jsxs(Flex, { vertical: true, gap: 10, children: [_jsx(StyleInput, { isMobile: isMobile, maxLength: 6, placeholder: "Input your code", value: state.code, onChange: e => {
@@ -2,4 +2,7 @@
2
2
  export declare const Title: import("styled-components").StyledComponent<"div", any, {
3
3
  isMobile: boolean;
4
4
  }, never>;
5
+ export declare const Label: import("styled-components").StyledComponent<"div", any, {
6
+ isMobile: boolean;
7
+ }, never>;
5
8
  export declare const WidgetLayoutMain: import("styled-components").StyledComponent<import("react").ForwardRefExoticComponent<import("antd").FlexProps<import("antd/es/_util/type").AnyObject> & import("react").RefAttributes<HTMLElement>>, any, {}, never>;
@@ -6,6 +6,12 @@ export const Title = styled.div `
6
6
  color: #005eb8;
7
7
  text-align: center;
8
8
  `;
9
+ export const Label = styled.div `
10
+ font-size: ${props => (props.isMobile ? '16px' : '14px')};
11
+ font-weight: 500;
12
+ color: #005eb8;
13
+ text-align: center;
14
+ `;
9
15
  export const WidgetLayoutMain = styled(Flex) `
10
16
  position: relative;
11
17
 
@@ -48,4 +48,5 @@ export declare const Text: import("styled-components").StyledComponent<"div", an
48
48
  color?: string | undefined;
49
49
  isBold?: boolean | undefined;
50
50
  isMobile?: boolean | undefined;
51
+ isCenter?: boolean | undefined;
51
52
  }, never>;
@@ -126,4 +126,5 @@ export const Text = styled.div `
126
126
  color: ${props => (props.color ? props.color : ' #000000')};
127
127
  font-size: ${props => (props.isMobile ? ' 14px !important' : '12px')};
128
128
  font-weight: ${props => (props.isBold ? 'bold' : 'normal')};
129
+ text-align: ${props => (props.isCenter ? 'center' : 'left')};
129
130
  `;
@@ -10,7 +10,7 @@ import { Spin, Tooltip, Icon, Button, EditableName, SelectAccount, QuillEditor,
10
10
  import Service from './Service';
11
11
  import { MESSAGE_TYPE, TICKET_CUSTOM_MESSAGE_KEY } from './constant';
12
12
  import { WrapperContent, WrapperContentInput, WrapperEditor, WrapperIconEditor, WrapperInputFile, WrapperLable, WrapperLeftContent, WrapperLinkItemFiles, WrapperMessageContent, WrapperRightContent, WrapperTextEdit, WrapperTextInput, DrawerHeader, } from './styled';
13
- import { formatAccountId, formatDatarender, formatParams, handleValidateContent, postCustomEvent, } from './util';
13
+ import { formatAccountId, formatDatarender, formatParams, handleValidateContent, postCustomEvent, uploadEmbeddedImages, } from './util';
14
14
  import { TCallBack } from '../../molecules/QuillEditor/QuillEditor';
15
15
  import { isBoolean } from 'lodash';
16
16
  const initValueInput = {
@@ -49,6 +49,7 @@ const Content = ({ portalId, token, action, ticketId, listUsers, domainTicket, d
49
49
  isError: false,
50
50
  message: '',
51
51
  });
52
+ const [embeddedImageError, setEmbeddedImageError] = useState('');
52
53
  const isUpdate = action === 'edit' && ticketId;
53
54
  const valueInputRef = useRef(valueInput);
54
55
  valueInputRef.current = valueInput;
@@ -94,20 +95,54 @@ const Content = ({ portalId, token, action, ticketId, listUsers, domainTicket, d
94
95
  setIsLoading(false);
95
96
  });
96
97
  };
97
- const handleSubmit = () => {
98
+ const uploadImagesBeforeSubmit = async (message) => {
99
+ setIsLoadingUpload(true);
100
+ setEmbeddedImageError('');
101
+ try {
102
+ const uploadedMessage = await uploadEmbeddedImages(message, Service.tickets.callApi.uploadImg({
103
+ domain: domainUpload,
104
+ token,
105
+ userId,
106
+ }));
107
+ if (uploadedMessage !== message) {
108
+ setTextValue(uploadedMessage);
109
+ updateValueInput({ message: uploadedMessage });
110
+ }
111
+ return uploadedMessage;
112
+ }
113
+ catch (error) {
114
+ setEmbeddedImageError(error instanceof Error && error.message === 'FILE_TOO_LARGE'
115
+ ? '*Maximum upload file size: 10MB'
116
+ : '*Image upload failed. Please try again.');
117
+ throw error;
118
+ }
119
+ finally {
120
+ setIsLoadingUpload(false);
121
+ }
122
+ };
123
+ const handleSubmit = async () => {
98
124
  const { title, ownerId, message } = valueInputRef.current;
99
125
  if (!title || !ownerId.length || !message) {
100
126
  setIsEmptyField(true);
101
127
  }
102
128
  else {
103
129
  if (!errFile.isError) {
104
- const params = formatParams(valueInputRef.current);
105
- const submitterEmail = listUsers?.find(user => user.userId === params.ownerId)?.email;
106
- fetchCreateTicket({
107
- ...params,
108
- submitterId: Number(userId),
109
- submitterEmail: submitterEmail,
110
- });
130
+ try {
131
+ const uploadedMessage = await uploadImagesBeforeSubmit(message);
132
+ const params = formatParams({
133
+ ...valueInputRef.current,
134
+ message: uploadedMessage,
135
+ });
136
+ const submitterEmail = listUsers?.find(user => user.userId === params.ownerId)?.email;
137
+ fetchCreateTicket({
138
+ ...params,
139
+ submitterId: Number(userId),
140
+ submitterEmail,
141
+ });
142
+ }
143
+ catch (error) {
144
+ // The upload error is shown below the editor.
145
+ }
111
146
  }
112
147
  // handleSendData(valueInput)
113
148
  }
@@ -231,6 +266,7 @@ const Content = ({ portalId, token, action, ticketId, listUsers, domainTicket, d
231
266
  }
232
267
  }, [isUpdate, openDrawer]);
233
268
  const handleEditorChange = content => {
269
+ setEmbeddedImageError('');
234
270
  setTextValue(content);
235
271
  updateValueInput({ message: content });
236
272
  // setIsLoadingUpload(content.includes('<img src="data:image/png;base64'));
@@ -244,32 +280,34 @@ const Content = ({ portalId, token, action, ticketId, listUsers, domainTicket, d
244
280
  }
245
281
  fetchUpdateFollwers(params);
246
282
  };
247
- const handleUpdateComment = () => {
248
- setIsLoading(true);
249
- const params = {
250
- followers: [],
251
- submitterId: null,
252
- ownerId: null,
253
- submitterEmail: '',
254
- properties: {},
255
- message: '',
256
- attachmentsZendesk: [],
257
- };
258
- if (valueInput?.followers?.length) {
259
- params.followers = formatAccountId(valueInput?.followers);
260
- }
261
- if (valueInput?.files?.length > 0) {
262
- params.properties = {
263
- attachments: valueInput?.files,
283
+ const handleUpdateComment = async () => {
284
+ const currentValueInput = valueInputRef.current;
285
+ try {
286
+ const uploadedMessage = await uploadImagesBeforeSubmit(currentValueInput.message);
287
+ const params = {
288
+ followers: [],
289
+ submitterId: userId,
290
+ ownerId: Number(currentValueInput?.ownerId[0]?.userId),
291
+ submitterEmail: listUsers?.find(user => +user.userId === +userId)?.email,
292
+ properties: {},
293
+ message: uploadedMessage,
294
+ attachmentsZendesk: currentValueInput.fileZendesk.map(file => file.token),
264
295
  };
296
+ if (currentValueInput?.followers?.length) {
297
+ params.followers = formatAccountId(currentValueInput.followers);
298
+ }
299
+ if (currentValueInput?.files?.length > 0) {
300
+ params.properties = {
301
+ attachments: currentValueInput.files,
302
+ };
303
+ }
304
+ setIsLoading(true);
305
+ fetchUpdateComment(params);
306
+ handleUpdateFollowers(currentValueInput?.followers?.map((fol) => fol.userId));
307
+ }
308
+ catch (error) {
309
+ // The upload error is shown below the editor.
265
310
  }
266
- params.submitterId = userId;
267
- params.submitterEmail = listUsers?.find(user => +user.userId === +userId)?.email;
268
- params.ownerId = Number(valueInput?.ownerId[0]?.userId);
269
- params.message = valueInput?.message;
270
- params.attachmentsZendesk = valueInput.fileZendesk.map(file => file.token);
271
- fetchUpdateComment(params);
272
- handleUpdateFollowers(valueInput?.followers?.map((fol) => fol.userId));
273
311
  };
274
312
  const handleUploadFile = async (file) => {
275
313
  setIsLoading(true);
@@ -387,7 +425,8 @@ const Content = ({ portalId, token, action, ticketId, listUsers, domainTicket, d
387
425
  isLoadingDetails ||
388
426
  isLoadingDataSouce ||
389
427
  isLoadingFollower ||
390
- isLoadingSelectedAccount, children: [_jsxs(DrawerHeader, { children: [_jsx(EditableName, { value: valueInput.title, onChange: handleOnchangeTitle, readonly: isUpdate, error: isEmptyField && (!valueInput.title || !valueInput.title.trim())
428
+ isLoadingSelectedAccount ||
429
+ isLoadingUpload, children: [_jsxs(DrawerHeader, { children: [_jsx(EditableName, { value: valueInput.title, onChange: handleOnchangeTitle, readonly: isUpdate, error: isEmptyField && (!valueInput.title || !valueInput.title.trim())
391
430
  ? "Title can't be empty"
392
431
  : '' }), _jsx("div", { id: "header-journey-right", className: "right-content", children: !isUpdate && (_jsx(Button, { disabled: isLoadingUpload, type: "primary", onClick: handleSubmit, children: "Save" })) })] }), _jsxs(WrapperContent, { style: { height: '100%' }, children: [_jsxs(WrapperLeftContent, { children: [_jsxs(WrapperContentInput, { style: { alignItems: 'flex-start' }, children: [_jsxs(WrapperLable, { children: ["Owner", _jsx("span", { style: { color: '#ff0000' }, children: "*" })] }), _jsxs("div", { children: [_jsx(SelectAccount, { type: "default", initData: (valueInput?.ownerId || []).map((item) => item.userId), nameKey: "userName", userIdKey: "userId", users: listUsers, isViewMode: isUpdate || !isAllAccount || accountEdit !== 4, onChange: (_, users) => updateValueInput({ ownerId: users }), onlyOne: true }), isEmptyField && !valueInput?.ownerId?.length && (_jsx("div", { className: "error-message", children: "*This field can't be empty" }))] })] }), _jsxs(WrapperContentInput, { style: { marginTop: '15px', alignItems: 'flex-start' }, children: [_jsx(WrapperLable, { children: "Follower(s)" }), _jsx(SelectAccount, { type: "default", initData: [
393
432
  ...accountManage,
@@ -404,7 +443,7 @@ const Content = ({ portalId, token, action, ticketId, listUsers, domainTicket, d
404
443
  background: `${!handleValidateContent(textValue) || isLoadingUpload
405
444
  ? '#ccc'
406
445
  : '#1f5fac'}`,
407
- }, onClick: handleUpdateComment, children: "Reply" }))] })] })] }), errFile.isError ? (_jsx("div", { className: "error-message", children: errFile.message })) : (isEmptyField &&
446
+ }, onClick: handleUpdateComment, children: "Reply" }))] })] })] }), embeddedImageError ? (_jsx("div", { className: "error-message", children: embeddedImageError })) : errFile.isError ? (_jsx("div", { className: "error-message", children: errFile.message })) : (isEmptyField &&
408
447
  !valueInput?.message && (_jsx("div", { className: "error-message", children: "*This field can't be empty" })))] }), isUpdate && (_jsx(Spin, { spinning: isLoadingComment, children: _jsx(WrapperMessageContent, { children: listComment?.map(comment => (_jsx(MessageComponent, { toUser: comment?.toUser, fromUser: comment?.fromUser, followers: comment?.followers, message: comment?.message, date: comment?.createdDate, attachments: comment?.attachments, timeZone: timeZone, submitterEmail: comment?.submitterEmail }, comment?.id))) }) }))] })] })] })] }));
409
448
  };
410
449
  export default Content;
@@ -27,6 +27,7 @@ export declare function getEntriesV2(res: any, def: any): {
27
27
  messageAPI?: undefined;
28
28
  };
29
29
  export declare const formatAccountId: (data: any) => any;
30
+ export { dataUrlToFile, uploadEmbeddedImages } from '../../../utils/uploadEmbeddedImages';
30
31
  export declare const formatParams: (data: any) => {
31
32
  title: string;
32
33
  ownerId: undefined;
@@ -294,6 +294,7 @@ export const formatAccountId = data => {
294
294
  const results = data?.map(item => item?.userId);
295
295
  return results;
296
296
  };
297
+ export { dataUrlToFile, uploadEmbeddedImages } from '../../../utils/uploadEmbeddedImages';
297
298
  export const formatParams = data => {
298
299
  const params = {
299
300
  title: '',
@@ -8,7 +8,7 @@ import MessageComponent from './components/MessageComponent';
8
8
  import { Spin, Tooltip, Icon, Button, SelectAccount, QuillEditor, } from '@antscorp/antsomi-ui/es/components';
9
9
  import Service from './Service';
10
10
  import { WrapperContent, WrapperContentInput, WrapperEditor, WrapperIconEditor, WrapperInputFile, WrapperLable, WrapperLeftContent, WrapperLinkItemFiles, WrapperMessageContent, WrapperRightContent, WrapperTextEdit, WrapperTextInput, } from './styled';
11
- import { formatDatarender, handleValidateContent } from './util';
11
+ import { formatDatarender, handleValidateContent, uploadEmbeddedImages, } from './util';
12
12
  import { get, keyBy } from 'lodash';
13
13
  import { TCallBack } from '../../molecules/QuillEditor/QuillEditor';
14
14
  const initValueInput = {
@@ -44,6 +44,7 @@ const Content = ({ apiKey, domain, portalId, token, action, ticketId, listUsers,
44
44
  isError: false,
45
45
  message: '',
46
46
  });
47
+ const [embeddedImageError, setEmbeddedImageError] = useState('');
47
48
  const isUpdate = action === 'edit' && ticketId;
48
49
  const valueInputRef = useRef(valueInput);
49
50
  valueInputRef.current = valueInput;
@@ -136,6 +137,7 @@ const Content = ({ apiKey, domain, portalId, token, action, ticketId, listUsers,
136
137
  }, 1000);
137
138
  }, []);
138
139
  const handleEditorChange = content => {
140
+ setEmbeddedImageError('');
139
141
  setTextValue(content);
140
142
  updateValueInput({ message: content });
141
143
  // setIsLoadingUpload(content.includes('<img src="data:image/png;base64'));
@@ -147,33 +149,61 @@ const Content = ({ apiKey, domain, portalId, token, action, ticketId, listUsers,
147
149
  params.followers = arrFolowers;
148
150
  fetchUpdateFollwers(params);
149
151
  };
150
- const handleUpdateComment = () => {
151
- setIsLoading(true);
152
- const params = {
153
- followers: [],
154
- submitterId: null,
155
- ownerId: null,
156
- message: '',
157
- properties: {
158
- attachments: [],
159
- },
160
- submitterEmail: '',
161
- attachmentsZendesk: [],
162
- };
163
- if (valueInput?.followers?.length) {
164
- params.followers = valueInput?.followers;
152
+ const uploadImagesBeforeSubmit = async (message) => {
153
+ setIsLoadingUpload(true);
154
+ setEmbeddedImageError('');
155
+ try {
156
+ const uploadedMessage = await uploadEmbeddedImages(message, Service.tickets.callApi.uploadFile({
157
+ domain: domainUpload,
158
+ }));
159
+ if (uploadedMessage !== message) {
160
+ setTextValue(uploadedMessage);
161
+ updateValueInput({ message: uploadedMessage });
162
+ }
163
+ return uploadedMessage;
164
+ }
165
+ catch (error) {
166
+ setEmbeddedImageError(error instanceof Error && error.message === 'FILE_TOO_LARGE'
167
+ ? '*Maximum upload file size: 10MB'
168
+ : '*Image upload failed. Please try again.');
169
+ throw error;
170
+ }
171
+ finally {
172
+ setIsLoadingUpload(false);
165
173
  }
166
- if (valueInput?.files?.length > 0) {
167
- params.properties.attachments = valueInput?.files;
174
+ };
175
+ const handleUpdateComment = async () => {
176
+ const currentValueInput = valueInputRef.current;
177
+ try {
178
+ const uploadedMessage = await uploadImagesBeforeSubmit(currentValueInput.message);
179
+ const params = {
180
+ followers: [],
181
+ submitterId: null,
182
+ ownerId: null,
183
+ message: uploadedMessage,
184
+ properties: {
185
+ attachments: [],
186
+ },
187
+ submitterEmail: '',
188
+ attachmentsZendesk: [],
189
+ };
190
+ if (currentValueInput?.followers?.length) {
191
+ params.followers = currentValueInput.followers;
192
+ }
193
+ if (currentValueInput?.files?.length > 0) {
194
+ params.properties.attachments = currentValueInput.files;
195
+ }
196
+ const mapUsers = keyBy(listUsers, 'email');
197
+ params.submitterId = +mapUsers[email]?.userId || 0;
198
+ params.submitterEmail = email;
199
+ params.attachmentsZendesk = currentValueInput.fileZendesk?.map(file => file?.token);
200
+ setIsLoading(true);
201
+ fetchUpdateComment(params);
202
+ handleUpdateFollowers(currentValueInput.followers?.map((fol) => fol.userId));
203
+ }
204
+ catch (error) {
205
+ // The upload error is shown below the editor.
168
206
  }
169
- const mapUsers = keyBy(listUsers, 'email');
170
- params.submitterId = +mapUsers[email]?.userId || 0;
171
- params.submitterEmail = email;
172
- params.ownerId = null;
173
- params.message = valueInput?.message;
174
- params.attachmentsZendesk = valueInput?.fileZendesk?.map(file => file?.token);
175
- fetchUpdateComment(params);
176
- handleUpdateFollowers(valueInput?.followers?.map((fol) => fol.userId));
177
207
  };
178
208
  const onChangeFollowers = arrFolowers => {
179
209
  updateValueInput({ followers: arrFolowers });
@@ -301,7 +331,11 @@ const Content = ({ apiKey, domain, portalId, token, action, ticketId, listUsers,
301
331
  break;
302
332
  }
303
333
  };
304
- return (_jsxs("div", { style: { height: '100%' }, children: [_jsxs(Helmet, { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("title", { children: browserTitle })] }), _jsx(Spin, { style: { height: '100vh' }, spinning: isLoading || isLoadingDetails || isLoadingDataSouce || isLoadingFollower, children: _jsxs(WrapperContent, { style: { height: '100%' }, children: [_jsxs(WrapperLeftContent, { children: [_jsxs(WrapperContentInput, { style: { alignItems: 'flex-start' }, children: [_jsxs(WrapperLable, { children: ["Owner", _jsx("span", { style: { color: '#ff0000' }, children: "*" })] }), _jsx("div", { children: ownerEmail })] }), _jsxs(WrapperContentInput, { style: { marginTop: '15px', alignItems: 'flex-start' }, children: [_jsx(WrapperLable, { children: "Follower(s)" }), _jsx(SelectAccount, { type: "default", initData: ticketDetails?.followers || [], nameKey: "userName", userIdKey: "userId", users: listUsers, onChange: (_, followers) => onChangeFollowers(followers) })] }), dataSelects?.map((data) => _jsx(_Fragment, { children: renderOptions(data) })), _jsxs(WrapperContentInput, { style: { marginTop: '15px' }, children: [_jsx(WrapperLable, { htmlFor: "referenceUrl", children: "Reference URL" }), isUpdate ? (ticketDetails.referenceUrl ? (_jsx(Tooltip, { title: ticketDetails.referenceUrl, placement: "top", children: _jsx(WrapperTextEdit, { color: "#005fb8", href: ticketDetails.referenceUrl, target: "_blank", children: ticketDetails.referenceUrl }) })) : (_jsx(WrapperTextEdit, { color: "#005fb8", href: ticketDetails.referenceUrl, target: "_blank", children: ticketDetails.referenceUrl }))) : (_jsx(WrapperTextInput, { placeholder: "Reference URL", id: "referenceUrl",
334
+ return (_jsxs("div", { style: { height: '100%' }, children: [_jsxs(Helmet, { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("title", { children: browserTitle })] }), _jsx(Spin, { style: { height: '100vh' }, spinning: isLoading ||
335
+ isLoadingDetails ||
336
+ isLoadingDataSouce ||
337
+ isLoadingFollower ||
338
+ isLoadingUpload, children: _jsxs(WrapperContent, { style: { height: '100%' }, children: [_jsxs(WrapperLeftContent, { children: [_jsxs(WrapperContentInput, { style: { alignItems: 'flex-start' }, children: [_jsxs(WrapperLable, { children: ["Owner", _jsx("span", { style: { color: '#ff0000' }, children: "*" })] }), _jsx("div", { children: ownerEmail })] }), _jsxs(WrapperContentInput, { style: { marginTop: '15px', alignItems: 'flex-start' }, children: [_jsx(WrapperLable, { children: "Follower(s)" }), _jsx(SelectAccount, { type: "default", initData: ticketDetails?.followers || [], nameKey: "userName", userIdKey: "userId", users: listUsers, onChange: (_, followers) => onChangeFollowers(followers) })] }), dataSelects?.map((data) => _jsx(_Fragment, { children: renderOptions(data) })), _jsxs(WrapperContentInput, { style: { marginTop: '15px' }, children: [_jsx(WrapperLable, { htmlFor: "referenceUrl", children: "Reference URL" }), isUpdate ? (ticketDetails.referenceUrl ? (_jsx(Tooltip, { title: ticketDetails.referenceUrl, placement: "top", children: _jsx(WrapperTextEdit, { color: "#005fb8", href: ticketDetails.referenceUrl, target: "_blank", children: ticketDetails.referenceUrl }) })) : (_jsx(WrapperTextEdit, { color: "#005fb8", href: ticketDetails.referenceUrl, target: "_blank", children: ticketDetails.referenceUrl }))) : (_jsx(WrapperTextInput, { placeholder: "Reference URL", id: "referenceUrl",
305
339
  // width="300px"
306
340
  onChange: handleOnchangeInput, name: "referenceUrl", value: valueInput.referenceUrl }))] })] }), _jsxs(WrapperRightContent, { children: [_jsxs(WrapperEditor, { children: [_jsxs(_Fragment, { children: [_jsx(QuillEditor, { value: textValue, uploadService: Service.tickets.callApi.uploadFile({
307
341
  domain: domainUpload,
@@ -309,7 +343,7 @@ const Content = ({ apiKey, domain, portalId, token, action, ticketId, listUsers,
309
343
  background: `${!handleValidateContent(textValue) || isLoadingUpload
310
344
  ? '#ccc'
311
345
  : '#1f5fac'}`,
312
- }, onClick: handleUpdateComment, children: "Reply" }))] })] })] }), errFile.isError ? (_jsx("div", { className: "error-message", children: errFile.message })) : (isEmptyField &&
346
+ }, onClick: handleUpdateComment, children: "Reply" }))] })] })] }), embeddedImageError ? (_jsx("div", { className: "error-message", children: embeddedImageError })) : errFile.isError ? (_jsx("div", { className: "error-message", children: errFile.message })) : (isEmptyField &&
313
347
  !valueInput?.message && (_jsx("div", { className: "error-message", children: "* This field can't be empty" })))] }), isUpdate && (_jsx(Spin, { spinning: isLoadingComment, children: _jsx(WrapperMessageContent, { children: listComment?.map(comment => (_jsx(MessageComponent, { toUser: comment?.toUser, fromUser: comment?.fromUser, followers: comment?.followers,
314
348
  // mailFollower={comment?.mailFollower}
315
349
  message: comment?.message, date: comment?.createdDate, attachments: comment?.attachments, timeZone: timeZone, submitterEmail: comment?.submitterEmail }, comment?.id))) }) }))] })] }) })] }));
@@ -28,6 +28,7 @@ export declare function getEntriesV2(res: any, def: any): {
28
28
  messageAPI?: undefined;
29
29
  };
30
30
  export declare const formatAccountId: (data: any) => any;
31
+ export { dataUrlToFile, uploadEmbeddedImages } from '../../../utils/uploadEmbeddedImages';
31
32
  export declare const formatParams: (data: any) => {
32
33
  title: string;
33
34
  ownerId: undefined;
@@ -287,6 +287,7 @@ export const formatAccountId = data => {
287
287
  const results = data?.map(item => item?.userId);
288
288
  return results;
289
289
  };
290
+ export { dataUrlToFile, uploadEmbeddedImages } from '../../../utils/uploadEmbeddedImages';
290
291
  export const formatParams = data => {
291
292
  const params = {
292
293
  title: '',
@@ -0,0 +1,7 @@
1
+ type TUploadEmbeddedImages = (params: {
2
+ files: File[];
3
+ mode: 'file';
4
+ }) => Promise<unknown>;
5
+ export declare const dataUrlToFile: (dataUrl: string, index: number) => File;
6
+ export declare const uploadEmbeddedImages: (message: string, uploadService: TUploadEmbeddedImages) => Promise<string>;
7
+ export {};
@@ -0,0 +1,65 @@
1
+ const DATA_IMAGE_PREFIX = /^\s*data:image\//i;
2
+ const DATA_IMAGE_URL = /^data:(image\/[^;,]+);base64,(.+)$/i;
3
+ const getUploadedImageUrl = (uploadedImage) => {
4
+ if (!uploadedImage || typeof uploadedImage !== 'object' || !('url' in uploadedImage)) {
5
+ return null;
6
+ }
7
+ const { url } = uploadedImage;
8
+ if (typeof url !== 'string' || !url.trim() || /^\s*data:/i.test(url)) {
9
+ return null;
10
+ }
11
+ try {
12
+ const parsedUrl = new URL(url.trim());
13
+ return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'
14
+ ? parsedUrl.href
15
+ : null;
16
+ }
17
+ catch (error) {
18
+ return null;
19
+ }
20
+ };
21
+ export const dataUrlToFile = (dataUrl, index) => {
22
+ const matches = dataUrl.trim().match(DATA_IMAGE_URL);
23
+ if (!matches) {
24
+ throw new Error('INVALID_IMAGE_DATA');
25
+ }
26
+ const [, rawMimeType, base64] = matches;
27
+ const mimeType = rawMimeType.toLowerCase();
28
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
29
+ const fileSize = (base64.length * 3) / 4 - padding;
30
+ if (fileSize > 10 * 1024 * 1024) {
31
+ throw new Error('FILE_TOO_LARGE');
32
+ }
33
+ const binary = atob(base64);
34
+ const bytes = new Uint8Array(binary.length);
35
+ for (let byteIndex = 0; byteIndex < binary.length; byteIndex += 1) {
36
+ bytes[byteIndex] = binary.charCodeAt(byteIndex);
37
+ }
38
+ const extension = mimeType.split('/')[1].replace('jpeg', 'jpg');
39
+ return new File([bytes], `ticket-image-${index + 1}.${extension}`, { type: mimeType });
40
+ };
41
+ export const uploadEmbeddedImages = async (message, uploadService) => {
42
+ const container = document.createElement('div');
43
+ container.innerHTML = message;
44
+ const images = Array.from(container.querySelectorAll('img')).filter(image => DATA_IMAGE_PREFIX.test(image.getAttribute('src') || ''));
45
+ if (!images.length) {
46
+ return message;
47
+ }
48
+ const files = images.map((image, index) => dataUrlToFile(image.getAttribute('src') || '', index));
49
+ const response = await uploadService({ files, mode: 'file' });
50
+ const uploadedImages = response && typeof response === 'object' && 'data' in response && Array.isArray(response.data)
51
+ ? response.data
52
+ : [];
53
+ const uploadedUrls = uploadedImages.map(getUploadedImageUrl);
54
+ if (uploadedUrls.length !== images.length || uploadedUrls.some(uploadedUrl => !uploadedUrl)) {
55
+ throw new Error('UPLOAD_FAILED');
56
+ }
57
+ images.forEach((image, index) => {
58
+ image.setAttribute('src', uploadedUrls[index]);
59
+ });
60
+ const hasEmbeddedImage = Array.from(container.querySelectorAll('img')).some(image => DATA_IMAGE_PREFIX.test(image.getAttribute('src') || ''));
61
+ if (hasEmbeddedImage) {
62
+ throw new Error('UPLOAD_FAILED');
63
+ }
64
+ return container.innerHTML;
65
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antscorp/antsomi-ui",
3
- "version": "2.0.121",
3
+ "version": "2.0.123",
4
4
  "description": "An enterprise-class UI design language and React UI library.",
5
5
  "sideEffects": [
6
6
  "dist/*",