@antscorp/antsomi-ui 2.0.57 → 2.0.58

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.
@@ -1,33 +1,133 @@
1
1
  import React from 'react';
2
2
  import { UploadProps as AntdUploadProps } from 'antd/lib/upload';
3
3
  export type MediaMode = 'image' | 'video';
4
- interface UploadImageProps extends AntdUploadProps {
4
+ /**
5
+ * Props for the UploadImage component.
6
+ */
7
+ export interface UploadImageProps extends AntdUploadProps {
8
+ /**
9
+ * Heading text for the image upload modal.
10
+ * @default "Image Selection"
11
+ */
5
12
  labelHeadingModal?: string;
13
+ /**
14
+ * Label for the button used to select an image.
15
+ * @default "Select Image from computer"
16
+ */
6
17
  labelButtonSelect?: string;
18
+ /**
19
+ * Label for the modal confirming image deletion.
20
+ * @default "Delete Image"
21
+ */
7
22
  labelModalDelete?: string;
23
+ /**
24
+ * Placeholder text for the search input inside the modal.
25
+ * @default "Search image..."
26
+ */
8
27
  searchPlaceholder?: string;
28
+ /**
29
+ * Callback triggered when an image is removed.
30
+ */
9
31
  onRemoveImage?: Function;
32
+ /**
33
+ * Callback triggered when an image is selected or changed.
34
+ */
10
35
  onChangeImage?: Function;
36
+ /**
37
+ * The currently selected image object.
38
+ */
11
39
  selectedImage?: UploadMediaObject;
40
+ /**
41
+ * Controls the visibility of the image upload modal.
42
+ * @default false
43
+ */
12
44
  isOpen?: boolean;
45
+ /**
46
+ * Determines if the component should operate in input mode.
47
+ * @default true
48
+ */
13
49
  isInputMode?: boolean;
50
+ /**
51
+ * Name of the icon to be displayed.
52
+ * @default "image-3"
53
+ */
14
54
  iconName?: string;
55
+ /**
56
+ * Mode in which the media should be handled.
57
+ * @default "image"
58
+ */
15
59
  mode?: MediaMode;
16
- width?: any;
60
+ /**
61
+ * Width of the upload component.
62
+ * @default "100%"
63
+ */
64
+ width?: string | number;
65
+ /**
66
+ * List of validation errors related to the upload.
67
+ * @default []
68
+ */
17
69
  errors?: Array<any>;
70
+ /**
71
+ * Allowed file extensions for the upload.
72
+ * @default [".jpg", ".png", ".jfif", ".jpeg", ".gif", ".webp"]
73
+ */
18
74
  extensions?: string[];
75
+ /**
76
+ * Maximum file size allowed for upload (in MB).
77
+ * @default 10
78
+ */
19
79
  maxSize?: number;
80
+ /**
81
+ * Title text for the upload section.
82
+ */
20
83
  title?: string;
84
+ /**
85
+ * Whether to display the image URL after upload.
86
+ * @default true
87
+ */
21
88
  showImageURL?: boolean;
89
+ /**
90
+ * Placeholder text for the upload input field.
91
+ * @default "Enter image URL"
92
+ */
22
93
  placeholder?: string;
94
+ /**
95
+ * Domain-specific media folder or endpoint.
96
+ * @default "https://sandbox-media-template.antsomi.com/cdp"
97
+ */
23
98
  domainMedia: string;
99
+ /**
100
+ * Unique slug identifier for media categorization.
101
+ * @default "api/v1"
102
+ */
24
103
  slug: string;
104
+ /**
105
+ * Configuration parameters for API requests.
106
+ */
25
107
  paramConfigs?: {
108
+ /**
109
+ * Authentication token for media uploads.
110
+ * @default ""
111
+ */
26
112
  token?: string;
113
+ /**
114
+ * User identifier for media ownership.
115
+ * @default ""
116
+ */
27
117
  userId?: string;
118
+ /**
119
+ * Account identifier for media association.
120
+ * @default ""
121
+ */
28
122
  accountId: string;
29
123
  };
124
+ /**
125
+ * Whether the image upload is required.
126
+ */
30
127
  required?: boolean;
128
+ /**
129
+ * Indicates if the input is currently focused.
130
+ */
31
131
  focused?: boolean;
32
132
  }
33
133
  interface UploadMediaObject {
@@ -3,15 +3,19 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  /* eslint-disable @typescript-eslint/no-use-before-define */
4
4
  /* eslint-disable react-hooks/exhaustive-deps */
5
5
  // Libraries
6
- import { useEffect, useState, useRef } from 'react';
6
+ import { useEffect, useState, useRef, useMemo } from 'react';
7
7
  import Upload from 'antd/lib/upload';
8
+ import { useQueryClient } from '@tanstack/react-query';
9
+ import { cloneDeep, flattenDeep } from 'lodash';
10
+ import { useInView } from 'react-intersection-observer';
8
11
  // Hooks
9
- import { useDeepCompareEffect } from '@antscorp/antsomi-ui/es/hooks';
12
+ import { useDebounce } from '@antscorp/antsomi-ui/es/hooks/useDebounce';
13
+ import { useStoreSavedMedia } from '@antscorp/antsomi-ui/es/queries';
10
14
  // Assets
11
15
  import PlaceholderImage from '@antscorp/antsomi-ui/es/assets/images/placeholder-image.png';
12
16
  import MediaIcon from './MediaIcon';
13
17
  // Service
14
- import { uploadFile, createSavedMedia, deleteSavedMedia, getListingSavedMedia, } from '@antscorp/antsomi-ui/es/services/MediaTemplateDesign/UploadFile';
18
+ import { uploadFile, createSavedMedia, deleteSavedMedia, } from '@antscorp/antsomi-ui/es/services/MediaTemplateDesign/UploadFile';
15
19
  // Atoms
16
20
  import { Button, Icon, Text, message, Spin, Input, Space, } from '@antscorp/antsomi-ui/es/components/atoms';
17
21
  // Molecules
@@ -19,10 +23,10 @@ import { Modal, InputSearch, Select } from '@antscorp/antsomi-ui/es/components/m
19
23
  // Styled
20
24
  import { Boxed, ErrorMessage, Image, Overlay, Paragraph, TextStyled, UploadImageWrapper, WrapperBtn, WrapperIcon, WrapperInputMode, WrapperListImages, } from './styled';
21
25
  // Utils
22
- import { handleError, safeParse } from '@antscorp/antsomi-ui/es/utils';
26
+ import { safeParse } from '@antscorp/antsomi-ui/es/utils';
23
27
  import { getMediaTypeByMode, getReplacementExtension } from './utils';
24
28
  // Constants
25
- import { THEME } from '@antscorp/antsomi-ui/es/constants';
29
+ import { QUERY_KEYS, THEME } from '@antscorp/antsomi-ui/es/constants';
26
30
  const flexStyleCenter = {
27
31
  display: 'flex',
28
32
  gap: '15px',
@@ -39,75 +43,45 @@ const SORT_OPTIONS = {
39
43
  label: 'Sort by Size',
40
44
  },
41
45
  };
42
- const PATH = '@antscorp/antsomi-ui/es/components/molecules/UploadImage/index.tsx';
43
46
  export const UploadImage = props => {
44
47
  const { labelHeadingModal, labelButtonSelect, labelModalDelete, searchPlaceholder, onRemoveImage, onChangeImage: onChangeMedia, isOpen, isInputMode, width, placeholder, extensions, maxSize, title, showImageURL: showMediaURL, selectedImage: selectedMediaProp, required, focused, domainMedia, slug, paramConfigs, errors, iconName, mode, } = props;
45
48
  const { Dragger } = Upload;
46
- const [storeSavedMedia, setStoreSavedMedia] = useState([]);
47
49
  const [selectedMedia, setSelectedMedia] = useState(selectedMediaProp || { url: '' });
48
50
  const [isModalOpen, setIsModalOpen] = useState(isOpen);
49
- const [listMedia, setListMedia] = useState(storeSavedMedia || []);
51
+ const [textSearchDebounce, textSearch, setTextSearch] = useDebounce('', 350);
50
52
  const [sortOption, setSortOption] = useState(SORT_OPTIONS.BY_UPLOAD_DATE.value);
51
53
  const [loading, setLoading] = useState(false);
52
54
  const [isOpenConfirmDelete, setOpenConfirmDelete] = useState(false);
53
- const [triggerRefresh, setTriggerRefresh] = useState(1);
54
55
  const uploadFilesRef = useRef([]);
55
56
  const uploadFilesTimeoutRef = useRef();
56
57
  const deleteMediaRef = useRef();
57
58
  const isError = safeParse(errors, []).length > 0;
58
- useDeepCompareEffect(() => {
59
- setListMedia(storeSavedMedia || []);
60
- setLoading(false);
61
- }, [storeSavedMedia]);
59
+ const queryClient = useQueryClient();
60
+ const paramsMemo = useMemo(() => ({
61
+ ...paramConfigs,
62
+ type: getMediaTypeByMode(mode),
63
+ }), [paramConfigs, mode]);
64
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading: isLoadingStore, refetch, } = useStoreSavedMedia(domainMedia, slug, paramsMemo);
65
+ const flattenedMedia = useMemo(() => {
66
+ const flattened = cloneDeep(flattenDeep(data?.pages));
67
+ // Filter media based on search text
68
+ const filteredMedia = textSearchDebounce
69
+ ? flattened.filter(media => media.name?.toLowerCase().includes(textSearchDebounce.toLowerCase()))
70
+ : flattened;
71
+ // Sort media based on the selected sort option
72
+ if (sortOption === SORT_OPTIONS.BY_SIZE.value) {
73
+ return filteredMedia.sort((media1, media2) => media1.size - media2.size);
74
+ }
75
+ return filteredMedia.sort((media1, media2) => media1.createdAt && media2.createdAt
76
+ ? media1.createdAt.isAfter(media2.createdAt)
77
+ ? -1
78
+ : 1
79
+ : 0);
80
+ }, [data?.pages, sortOption, textSearchDebounce]);
62
81
  // When selectedMediaProp onChange
63
82
  useEffect(() => {
64
83
  setSelectedMedia(selectedMediaProp || { url: '' });
65
84
  }, [selectedMediaProp]);
66
- useEffect(() => {
67
- if (sortOption === SORT_OPTIONS.BY_SIZE.value) {
68
- setListMedia(listMedia ? [...listMedia].sort((media1, media2) => media1.size - media2.size) : []);
69
- }
70
- else {
71
- setListMedia(listMedia
72
- ? [...listMedia].sort((media1, media2) => media1.createdAt && media2.createdAt
73
- ? media1.createdAt.isAfter(media2.createdAt)
74
- ? -1
75
- : 1
76
- : 0)
77
- : []);
78
- }
79
- }, [sortOption]);
80
- const handleGetStoreSavedMedia = async (domain, slug, paramConfigs) => {
81
- try {
82
- setLoading(true);
83
- const type = getMediaTypeByMode(mode);
84
- const params = {
85
- ...paramConfigs,
86
- type,
87
- };
88
- const result = await getListingSavedMedia(domain, slug, params);
89
- if (result) {
90
- setStoreSavedMedia(result || []);
91
- }
92
- }
93
- catch (error) {
94
- handleError(error, {
95
- path: PATH,
96
- name: 'handleGetStoreSavedMedia',
97
- args: {
98
- error,
99
- },
100
- });
101
- // eslint-disable-next-line no-console
102
- console.log('error :>', error);
103
- }
104
- finally {
105
- setLoading(false);
106
- }
107
- };
108
- useEffect(() => {
109
- handleGetStoreSavedMedia(domainMedia, slug, paramConfigs);
110
- }, [triggerRefresh]);
111
85
  const showModal = (e) => {
112
86
  e.stopPropagation();
113
87
  setIsModalOpen(true);
@@ -125,6 +99,14 @@ export const UploadImage = props => {
125
99
  const onChangeSort = (option) => {
126
100
  setSortOption(option);
127
101
  };
102
+ const { ref } = useInView({
103
+ triggerOnce: false,
104
+ onChange: inView => {
105
+ if (inView && hasNextPage && !isFetchingNextPage) {
106
+ fetchNextPage();
107
+ }
108
+ },
109
+ });
128
110
  const renderListMedia = (listMediaRender) => listMediaRender.map((media, idx) => (_jsxs(WrapperListImages, { className: "ants-group", children: [_jsxs(Boxed, { children: [_jsx(Image, { src: mode === 'video' ? media.thumbnail : media.url, alt: "img", loading: "lazy" }), _jsxs(WrapperBtn, { className: "group-hover", children: [_jsx(Button, { onClick: () => handleSelectMedia(media), style: { backgroundColor: 'rgb(255,255,255)' }, children: "USE" }), _jsx(Button, { onClick: () => handleRemoveUploadedMedia(media), style: { backgroundColor: 'rgb(255,255,255)' }, children: _jsx(Icon, { type: "icon-ants-remove-trash", size: 15, style: { color: THEME.token?.colorPrimary } }) })] }), _jsx(Overlay, { className: "group-hover" })] }), _jsx(Paragraph, { style: {
129
111
  overflow: 'hidden',
130
112
  textOverflow: 'ellipsis',
@@ -134,16 +116,7 @@ export const UploadImage = props => {
134
116
  }, title: media.name, children: media.name }), _jsxs(Paragraph, { children: ["Uploaded: ", media.createdAt.format('DD/MM/YYYY'), " at ", media.createdAt.format('hh:mm:ss A')] }), _jsxs(Paragraph, { children: ["Size: ", media.sizeString] })] }, idx)));
135
117
  const onChangeSearchImage = (e) => {
136
118
  const { value } = e.target;
137
- if (value && value.trim()) {
138
- setListMedia(storeSavedMedia
139
- ? storeSavedMedia
140
- .filter(item => item.name.toLocaleLowerCase().includes(value.trim().toLocaleLowerCase()))
141
- .slice(0, 15)
142
- : []);
143
- }
144
- else {
145
- setListMedia(storeSavedMedia);
146
- }
119
+ setTextSearch(value.trim());
147
120
  };
148
121
  const handleRemoveMedia = () => {
149
122
  if (onRemoveImage) {
@@ -161,7 +134,9 @@ export const UploadImage = props => {
161
134
  if (res && res.data && res.data.code === 200 && res.data.data) {
162
135
  const { success_media = [] } = res.data.data;
163
136
  if (Array.isArray(success_media) && success_media.length) {
164
- setTriggerRefresh(triggerRefresh + 1);
137
+ queryClient.invalidateQueries([QUERY_KEYS.GET_LIST_SAVED_MEDIA, domainMedia, slug, paramsMemo], {
138
+ exact: false,
139
+ });
165
140
  }
166
141
  }
167
142
  setOpenConfirmDelete(false);
@@ -235,7 +210,7 @@ export const UploadImage = props => {
235
210
  })));
236
211
  await Promise.all(arrPromise);
237
212
  setLoading(false);
238
- setTriggerRefresh(triggerRefresh + 1);
213
+ refetch({ refetchPage: (_page, index) => index === 0 });
239
214
  }
240
215
  else {
241
216
  setLoading(false);
@@ -279,7 +254,7 @@ export const UploadImage = props => {
279
254
  justifyContent: 'space-between',
280
255
  alignItems: 'center',
281
256
  marginBottom: 20,
282
- }, children: [_jsx(InputSearch, { style: { width: 232 }, onChange: onChangeSearchImage, placeholder: searchPlaceholder }), _jsx(Select, { defaultValue: SORT_OPTIONS.BY_UPLOAD_DATE.value, value: sortOption, options: Object.values(SORT_OPTIONS), style: { width: 232 }, onChange: onChangeSort })] }), _jsx(Spin, { spinning: loading, children: _jsx(Dragger, { ...props, accept: extensions?.join(','),
257
+ }, children: [_jsx(InputSearch, { value: textSearch, style: { width: 232 }, onChange: onChangeSearchImage, placeholder: searchPlaceholder }), _jsx(Select, { defaultValue: SORT_OPTIONS.BY_UPLOAD_DATE.value, value: sortOption, options: Object.values(SORT_OPTIONS), style: { width: 232 }, onChange: onChangeSort })] }), _jsx(Spin, { spinning: loading || isFetchingNextPage || isLoadingStore, children: _jsx(Dragger, { ...props, accept: extensions?.join(','),
283
258
  // action={`${APP_CONFIG.API_URL}/file-upload/file?&_token=${userInfo?.token}&_user_id=${userInfo?.user_id}&_account_id=${userInfo?.account_id}`}
284
259
  // beforeUpload={handleBeforeUploadFile}
285
260
  // onChange={onChangeFileUpload}
@@ -289,13 +264,13 @@ export const UploadImage = props => {
289
264
  }, children: [_jsx(Icon, { type: `icon-ants-${iconName}`, size: 36, style: { color: THEME.token?.colorIcon } }), _jsxs("div", { style: { ...flexStyleCenter, justifyContent: 'flex-start' }, children: [_jsx(TextStyled, { className: "ant-upload-text", children: "Drag & Drop file here" }), _jsx("span", { children: "or" }), _jsx(Button, { style: { backgroundColor: '#ffffff' }, children: labelButtonSelect }), _jsx(Button, { onClick: handleRemoveMedia, style: {
290
265
  display: 'none !important',
291
266
  backgroundColor: '#ffffff',
292
- }, children: _jsx(Icon, { type: "icon-ants-remove-trash", size: 15, style: { color: THEME.token?.colorPrimary } }) })] })] }) }) }), _jsx("div", { style: {
267
+ }, children: _jsx(Icon, { type: "icon-ants-remove-trash", size: 15, style: { color: THEME.token?.colorPrimary } }) })] })] }) }) }), _jsxs("div", { style: {
293
268
  display: 'flex',
294
269
  flexWrap: 'wrap',
295
270
  gap: '35px',
296
271
  paddingTop: 20,
297
272
  width: '100%',
298
- }, children: renderListMedia(listMedia) })] })] }));
273
+ }, children: [renderListMedia(flattenedMedia), _jsx("div", { ref: ref })] })] })] }));
299
274
  };
300
275
  UploadImage.defaultProps = {
301
276
  isOpen: false,
@@ -1,3 +1,4 @@
1
+ import { CSSProperties } from 'react';
1
2
  export declare const WrapperListImages: import("styled-components").StyledComponent<"div", any, {}, never>;
2
3
  export declare const TextStyled: import("styled-components").StyledComponent<"span", any, {}, never>;
3
4
  export declare const Boxed: import("styled-components").StyledComponent<"div", any, {}, never>;
@@ -6,7 +7,7 @@ export declare const WrapperBtn: import("styled-components").StyledComponent<"di
6
7
  export declare const Paragraph: import("styled-components").StyledComponent<"p", any, {}, never>;
7
8
  export declare const Overlay: import("styled-components").StyledComponent<"div", any, {}, never>;
8
9
  export declare const WrapperInputMode: import("styled-components").StyledComponent<"div", any, {
9
- width?: string | undefined;
10
+ width?: CSSProperties['width'];
10
11
  }, never>;
11
12
  export declare const WrapperIcon: import("styled-components").StyledComponent<"div", any, {}, never>;
12
13
  export declare const ErrorMessage: import("styled-components").StyledComponent<"div", any, {}, never>;
@@ -18,6 +18,7 @@ export declare const QUERY_KEYS: {
18
18
  GET_OBJECT_TEMPLATE_DETAIL: string;
19
19
  GET_SAVE_AS_GALLERY_PERMISSION_EMAILS: string;
20
20
  GET_LIST_FALLBACK_BO: string;
21
+ GET_LIST_SAVED_MEDIA: string;
21
22
  GET_DASHBOARD: string;
22
23
  GET_LIST_MENU: string;
23
24
  GET_LIST_MENU_PERMISSION: string;
@@ -27,6 +27,7 @@ export const QUERY_KEYS = {
27
27
  GET_SAVE_AS_GALLERY_PERMISSION_EMAILS: 'GET_SAVE_AS_GALLERY_PERMISSION_EMAILS',
28
28
  // THIRD PARTY
29
29
  GET_LIST_FALLBACK_BO: 'GET_LIST_FALLBACK_BO',
30
+ GET_LIST_SAVED_MEDIA: 'GET_LIST_SAVED_MEDIA',
30
31
  // Left menu
31
32
  GET_DASHBOARD: 'GET_DASHBOARD',
32
33
  GET_LIST_MENU: 'GET_LIST_MENU',
@@ -1,2 +1,3 @@
1
1
  export * from './useGetEventTrackingAttributes';
2
2
  export * from './useGetListFallbackBO';
3
+ export * from './useStoreSavedMedia';
@@ -1,2 +1,3 @@
1
1
  export * from './useGetEventTrackingAttributes';
2
2
  export * from './useGetListFallbackBO';
3
+ export * from './useStoreSavedMedia';
@@ -0,0 +1 @@
1
+ export declare const useStoreSavedMedia: (domain: string, slug: string, infos: any) => import("@tanstack/react-query").UseInfiniteQueryResult<import("../../models/SavedMedia").SavedMedia[], unknown>;
@@ -0,0 +1,12 @@
1
+ // Libraries
2
+ import { useInfiniteQuery } from '@tanstack/react-query';
3
+ // Services
4
+ import { getListingSavedMedia } from '../../services/MediaTemplateDesign/UploadFile';
5
+ // Constants
6
+ import { QUERY_KEYS } from '../../constants';
7
+ // Hook to fetch data with infinite scrolling
8
+ export const useStoreSavedMedia = (domain, slug, infos) => useInfiniteQuery({
9
+ queryKey: [QUERY_KEYS.GET_LIST_SAVED_MEDIA, domain, slug, infos],
10
+ queryFn: ({ pageParam }) => getListingSavedMedia(domain, slug, infos, pageParam),
11
+ getNextPageParam: (lastPage, pages) => (lastPage.length >= 10 ? pages.length + 1 : undefined),
12
+ });
@@ -1,6 +1,6 @@
1
1
  import { SavedMedia } from '@antscorp/antsomi-ui/es/models/SavedMedia';
2
2
  import { Upload } from '@antscorp/antsomi-ui/es/models/Upload';
3
- export declare const getListingSavedMedia: (domainUrl: string, slug: string, infos: any) => Promise<SavedMedia[]>;
3
+ export declare const getListingSavedMedia: (domainUrl: string, slug: string, infos: any, page?: number) => Promise<SavedMedia[]>;
4
4
  export declare const createSavedMedia: (domainUrl: string, slug: string, infos: any, data: any) => Promise<any>;
5
5
  export declare const deleteSavedMedia: (domainUrl: string, slug: string, infos: any, id: string | number) => Promise<any>;
6
6
  export declare const uploadFile: (domainUrl: string, slug: string, infos: any, files: any) => Promise<Upload[]>;
@@ -3,11 +3,11 @@
3
3
  import { SavedMedia } from '@antscorp/antsomi-ui/es/models/SavedMedia';
4
4
  import { Upload } from '@antscorp/antsomi-ui/es/models/Upload';
5
5
  import { services } from '@antscorp/antsomi-ui/es/services';
6
- export const getListingSavedMedia = async (domainUrl, slug, infos) => {
6
+ export const getListingSavedMedia = async (domainUrl, slug, infos, page = 1) => {
7
7
  try {
8
8
  const { type = '', ...restInfos } = infos || {};
9
9
  const { data } = await services.mediaTemplateDesign.getList({
10
- API_HOST: `${domainUrl}/${slug}/saved-media/index?type=${type}`,
10
+ API_HOST: `${domainUrl}/${slug}/saved-media/index?type=${type}&page=${page}&limit=10`,
11
11
  }, restInfos);
12
12
  let savedImages = data?.data || [];
13
13
  savedImages = savedImages.map((savedImage) => new SavedMedia(savedImage));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antscorp/antsomi-ui",
3
- "version": "2.0.57",
3
+ "version": "2.0.58",
4
4
  "description": "An enterprise-class UI design language and React UI library.",
5
5
  "sideEffects": [
6
6
  "dist/*",