@pushwoosh/smart-components-kit 1.0.88 → 1.0.90

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 (90) hide show
  1. package/CampaignsConfigurationDrawer/CampaignsConfigurationDrawer.js +50 -16
  2. package/CampaignsConfigurationDrawer/polyglot/index.d.ts +2 -0
  3. package/CampaignsConfigurationDrawer/polyglot/index.js +10 -0
  4. package/CampaignsConfigurationDrawer/polyglot/messages_en.d.ts +3 -0
  5. package/CampaignsConfigurationDrawer/polyglot/messages_en.js +18 -0
  6. package/CampaignsConfigurationDrawer/polyglot/messages_ru.d.ts +3 -0
  7. package/CampaignsConfigurationDrawer/polyglot/messages_ru.js +18 -0
  8. package/CampaignsConfigurationDrawer/polyglot/types.d.ts +19 -0
  9. package/CampaignsConfigurationDrawer/polyglot/types.js +1 -0
  10. package/DynamicContentPickerForm/DynamicContentCustomPickerForm.js +5 -1
  11. package/DynamicContentPickerForm/DynamicContentEventPickerForm.js +11 -3
  12. package/DynamicContentPickerForm/DynamicContentTagPickerForm.js +11 -3
  13. package/DynamicContentPickerForm/SimpleDynamicContentPickerFormSmart.js +8 -2
  14. package/DynamicContentPickerForm/VariedDynamicContentPickerFormSmart.js +28 -14
  15. package/DynamicContentPickerForm/polyglot/index.d.ts +2 -0
  16. package/DynamicContentPickerForm/polyglot/index.js +10 -0
  17. package/DynamicContentPickerForm/polyglot/messages_en.d.ts +3 -0
  18. package/DynamicContentPickerForm/polyglot/messages_en.js +16 -0
  19. package/DynamicContentPickerForm/polyglot/messages_ru.d.ts +3 -0
  20. package/DynamicContentPickerForm/polyglot/messages_ru.js +16 -0
  21. package/DynamicContentPickerForm/polyglot/types.d.ts +17 -0
  22. package/DynamicContentPickerForm/polyglot/types.js +1 -0
  23. package/EmailTemplatePreview/EmailTemplatePreview.js +5 -1
  24. package/EmailTemplatePreview/polyglot/index.d.ts +5 -0
  25. package/EmailTemplatePreview/polyglot/index.js +10 -0
  26. package/EmailTemplatePreview/polyglot/messages_en.d.ts +3 -0
  27. package/EmailTemplatePreview/polyglot/messages_en.js +3 -0
  28. package/EmailTemplatePreview/polyglot/messages_ru.d.ts +3 -0
  29. package/EmailTemplatePreview/polyglot/messages_ru.js +3 -0
  30. package/EmailTemplatePreview/polyglot/types.d.ts +4 -0
  31. package/EmailTemplatePreview/polyglot/types.js +1 -0
  32. package/ExportCSV/ExportCsvBanner/ExportCsvBanner.js +24 -8
  33. package/ExportCSV/polyglot/index.d.ts +2 -0
  34. package/ExportCSV/polyglot/index.js +10 -0
  35. package/ExportCSV/polyglot/messages_en.d.ts +3 -0
  36. package/ExportCSV/polyglot/messages_en.js +9 -0
  37. package/ExportCSV/polyglot/messages_ru.d.ts +3 -0
  38. package/ExportCSV/polyglot/messages_ru.js +9 -0
  39. package/ExportCSV/polyglot/types.d.ts +10 -0
  40. package/ExportCSV/polyglot/types.js +1 -0
  41. package/MediaStore/CreateVariantDrawer.d.ts +12 -0
  42. package/MediaStore/CreateVariantDrawer.js +216 -0
  43. package/MediaStore/MediaStore.d.ts +12 -0
  44. package/MediaStore/MediaStore.js +258 -0
  45. package/MediaStore/index.d.ts +1 -0
  46. package/MediaStore/index.js +1 -0
  47. package/MediaStore/mediaFile.utils.d.ts +5 -0
  48. package/MediaStore/mediaFile.utils.js +10 -0
  49. package/MediaStore/polyglot/index.d.ts +2 -0
  50. package/MediaStore/polyglot/index.js +10 -0
  51. package/MediaStore/polyglot/messages_en.d.ts +3 -0
  52. package/MediaStore/polyglot/messages_en.js +28 -0
  53. package/MediaStore/polyglot/messages_ru.d.ts +3 -0
  54. package/MediaStore/polyglot/messages_ru.js +28 -0
  55. package/MediaStore/polyglot/types.d.ts +31 -0
  56. package/MediaStore/polyglot/types.js +1 -0
  57. package/MediaStore/styled.d.ts +27 -0
  58. package/MediaStore/styled.js +95 -0
  59. package/OpenActions/components/OpenActionsSummary/OpenActionsSummary.js +47 -3
  60. package/OpenActions/polyglot/index.d.ts +2 -0
  61. package/OpenActions/polyglot/index.js +10 -0
  62. package/OpenActions/polyglot/messages_en.d.ts +3 -0
  63. package/OpenActions/polyglot/messages_en.js +14 -0
  64. package/OpenActions/polyglot/messages_ru.d.ts +3 -0
  65. package/OpenActions/polyglot/messages_ru.js +14 -0
  66. package/OpenActions/polyglot/types.d.ts +18 -0
  67. package/OpenActions/polyglot/types.js +1 -0
  68. package/PlatformsConfigurationDrawer/components/DesktopPage/DesktopPage.js +29 -9
  69. package/PlatformsConfigurationDrawer/components/MainPage/MainPage.js +41 -13
  70. package/PlatformsConfigurationDrawer/components/MobilePage/MobilePage.js +35 -11
  71. package/PlatformsConfigurationDrawer/polyglot/index.d.ts +2 -0
  72. package/PlatformsConfigurationDrawer/polyglot/index.js +10 -0
  73. package/PlatformsConfigurationDrawer/polyglot/messages_en.d.ts +3 -0
  74. package/PlatformsConfigurationDrawer/polyglot/messages_en.js +24 -0
  75. package/PlatformsConfigurationDrawer/polyglot/messages_ru.d.ts +3 -0
  76. package/PlatformsConfigurationDrawer/polyglot/messages_ru.js +24 -0
  77. package/PlatformsConfigurationDrawer/polyglot/types.d.ts +25 -0
  78. package/PlatformsConfigurationDrawer/polyglot/types.js +1 -0
  79. package/ResendModal/ResendModal.js +23 -7
  80. package/ResendModal/polyglot/index.d.ts +2 -0
  81. package/ResendModal/polyglot/index.js +10 -0
  82. package/ResendModal/polyglot/messages_en.d.ts +3 -0
  83. package/ResendModal/polyglot/messages_en.js +9 -0
  84. package/ResendModal/polyglot/messages_ru.d.ts +3 -0
  85. package/ResendModal/polyglot/messages_ru.js +9 -0
  86. package/ResendModal/polyglot/types.d.ts +10 -0
  87. package/ResendModal/polyglot/types.js +1 -0
  88. package/index.d.ts +1 -0
  89. package/index.js +1 -0
  90. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { Color, ShapeRadius } from '@pushwoosh/kit-constants';
4
4
  import { Horizontal } from '@pushwoosh/kit-helpers';
5
5
  import { DangerTriangleIcon, DownloadIcon } from '@pushwoosh/kit-icons';
6
6
  import { Text } from '@pushwoosh/kit-typography';
7
+ import { useLang } from '../polyglot';
7
8
  import { useExportService } from '../useExportService';
8
9
  import { useExportVisibility } from './hooks/useExportVisibility';
9
10
  export const ExportCsvBanner = ({
@@ -14,6 +15,7 @@ export const ExportCsvBanner = ({
14
15
  isKeepPageOpenWarning,
15
16
  onClose
16
17
  }) => {
18
+ const lang = useLang();
17
19
  const {
18
20
  getLastTasks,
19
21
  getStatus,
@@ -72,13 +74,17 @@ export const ExportCsvBanner = ({
72
74
  status,
73
75
  filename: data.file,
74
76
  args,
75
- progressTitle: 'Exporting list:',
76
- resultSubTitle: 'Result:'
77
+ progressTitle: lang({
78
+ message: 'SMART.EXPORT_CSV.PROGRESS_TITLE'
79
+ }),
80
+ resultSubTitle: lang({
81
+ message: 'SMART.EXPORT_CSV.RESULT_SUBTITLE'
82
+ })
77
83
  });
78
84
  }
79
85
  });
80
86
  }
81
- }, [getLastTasks, isHidden, lastTasksParams, updateExportTask]);
87
+ }, [getLastTasks, isHidden, lastTasksParams, updateExportTask, lang]);
82
88
  const handleClose = useCallback(() => {
83
89
  hide();
84
90
  onClose === null || onClose === void 0 || onClose();
@@ -99,7 +105,9 @@ export const ExportCsvBanner = ({
99
105
  "$bgColor": Color.CLEAR
100
106
  }, status === 'STATUS_SUCCESS' && React.createElement(ExportResult, {
101
107
  type: "success",
102
- title: "Exporting complete",
108
+ title: lang({
109
+ message: 'SMART.EXPORT_CSV.SUCCESS_TITLE'
110
+ }),
103
111
  subtitle: `${resultSubTitle} ${filename}`,
104
112
  handleClose: handleClose,
105
113
  rightSlot: React.createElement("a", {
@@ -112,7 +120,9 @@ export const ExportCsvBanner = ({
112
120
  size: "small",
113
121
  view: "filled"
114
122
  })
115
- }, "Download CSV"))
123
+ }, lang({
124
+ message: 'SMART.EXPORT_CSV.DOWNLOAD'
125
+ })))
116
126
  }), status === 'STATUS_PENDING' && (isKeepPageOpenWarning ? React.createElement(ProgressBar, {
117
127
  title: progressTitle,
118
128
  percentValue: progress,
@@ -120,14 +130,20 @@ export const ExportCsvBanner = ({
120
130
  "$color": Color.WARNING,
121
131
  "$variant": "h5",
122
132
  "$uppercase": true
123
- }, "Keep page open during export")
133
+ }, lang({
134
+ message: 'SMART.EXPORT_CSV.KEEP_PAGE_OPEN'
135
+ }))
124
136
  }) : React.createElement(ProgressBar, {
125
137
  title: progressTitle,
126
138
  percentValue: progress
127
139
  })), status === 'STATUS_FAILED' && React.createElement(ExportResult, {
128
140
  type: "error",
129
- title: "Export failed",
130
- subtitle: "Please try again",
141
+ title: lang({
142
+ message: 'SMART.EXPORT_CSV.FAILED_TITLE'
143
+ }),
144
+ subtitle: lang({
145
+ message: 'SMART.EXPORT_CSV.FAILED_SUBTITLE'
146
+ }),
131
147
  leftSlotIcon: React.createElement(DangerTriangleIcon, {
132
148
  size: "medium",
133
149
  view: "filled"
@@ -0,0 +1,2 @@
1
+ import { type ExportCsvMessages } from './types';
2
+ export declare const Lang: import("react").FC<import("@pushwoosh/dumb-components/polyglot").LangProps<ExportCsvMessages>>, useLang: () => (props: import("@pushwoosh/dumb-components/polyglot").LangProps<ExportCsvMessages>) => string;
@@ -0,0 +1,10 @@
1
+ import { createLang } from '@pushwoosh/dumb-components/polyglot';
2
+ import { messages as en } from './messages_en';
3
+ import { messages as ru } from './messages_ru';
4
+ export const {
5
+ Lang,
6
+ useLang
7
+ } = createLang({
8
+ en,
9
+ ru
10
+ });
@@ -0,0 +1,3 @@
1
+ import type { LangMessages } from '@pushwoosh/dumb-components/polyglot';
2
+ import { type ExportCsvMessages } from './types';
3
+ export declare const messages: LangMessages<ExportCsvMessages>;
@@ -0,0 +1,9 @@
1
+ export const messages = {
2
+ 'SMART.EXPORT_CSV.PROGRESS_TITLE': 'Exporting list:',
3
+ 'SMART.EXPORT_CSV.RESULT_SUBTITLE': 'Result:',
4
+ 'SMART.EXPORT_CSV.SUCCESS_TITLE': 'Exporting complete',
5
+ 'SMART.EXPORT_CSV.DOWNLOAD': 'Download CSV',
6
+ 'SMART.EXPORT_CSV.KEEP_PAGE_OPEN': 'Keep page open during export',
7
+ 'SMART.EXPORT_CSV.FAILED_TITLE': 'Export failed',
8
+ 'SMART.EXPORT_CSV.FAILED_SUBTITLE': 'Please try again'
9
+ };
@@ -0,0 +1,3 @@
1
+ import type { LangMessages } from '@pushwoosh/dumb-components/polyglot';
2
+ import { type ExportCsvMessages } from './types';
3
+ export declare const messages: LangMessages<ExportCsvMessages>;
@@ -0,0 +1,9 @@
1
+ export const messages = {
2
+ 'SMART.EXPORT_CSV.PROGRESS_TITLE': 'Экспорт списка:',
3
+ 'SMART.EXPORT_CSV.RESULT_SUBTITLE': 'Результат:',
4
+ 'SMART.EXPORT_CSV.SUCCESS_TITLE': 'Экспорт завершён',
5
+ 'SMART.EXPORT_CSV.DOWNLOAD': 'Скачать CSV',
6
+ 'SMART.EXPORT_CSV.KEEP_PAGE_OPEN': 'Не закрывайте страницу во время экспорта',
7
+ 'SMART.EXPORT_CSV.FAILED_TITLE': 'Ошибка экспорта',
8
+ 'SMART.EXPORT_CSV.FAILED_SUBTITLE': 'Попробуйте ещё раз'
9
+ };
@@ -0,0 +1,10 @@
1
+ import type { NoParams } from '@pushwoosh/dumb-components/polyglot';
2
+ export type ExportCsvMessages = {
3
+ 'SMART.EXPORT_CSV.PROGRESS_TITLE': NoParams;
4
+ 'SMART.EXPORT_CSV.RESULT_SUBTITLE': NoParams;
5
+ 'SMART.EXPORT_CSV.SUCCESS_TITLE': NoParams;
6
+ 'SMART.EXPORT_CSV.DOWNLOAD': NoParams;
7
+ 'SMART.EXPORT_CSV.KEEP_PAGE_OPEN': NoParams;
8
+ 'SMART.EXPORT_CSV.FAILED_TITLE': NoParams;
9
+ 'SMART.EXPORT_CSV.FAILED_SUBTITLE': NoParams;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { type FC } from 'react';
2
+ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
3
+ type CreateVariantDrawerProps = {
4
+ application: string;
5
+ original: MediaFile | null;
6
+ isOpen: boolean;
7
+ existingVariants: MediaFile[];
8
+ onClose: () => void;
9
+ onCreated: (parentUuid: string, variant: MediaFile) => void;
10
+ };
11
+ export declare const CreateVariantDrawer: FC<CreateVariantDrawerProps>;
12
+ export {};
@@ -0,0 +1,216 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle, NativeInput, useToast } from '@pushwoosh/dumb-components';
3
+ import { Columns, Vertical } from '@pushwoosh/kit-helpers';
4
+ import { CloseIcon } from '@pushwoosh/kit-icons';
5
+ import { Text } from '@pushwoosh/kit-typography';
6
+ import { useSPAContext } from '@pushwoosh/spa-context';
7
+ import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
8
+ import { useLang } from './polyglot';
9
+ import { Chip, Chips, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage, VariantStrip, VariantThumb, VariantThumbCaption } from './styled';
10
+ const RATIOS = [{
11
+ value: 'ASPECT_RATIO_1_1',
12
+ label: '1:1',
13
+ ar: 1
14
+ }, {
15
+ value: 'ASPECT_RATIO_4_3',
16
+ label: '4:3',
17
+ ar: 4 / 3
18
+ }, {
19
+ value: 'ASPECT_RATIO_3_2',
20
+ label: '3:2',
21
+ ar: 3 / 2
22
+ }, {
23
+ value: 'ASPECT_RATIO_4_5',
24
+ label: '4:5',
25
+ ar: 4 / 5
26
+ }, {
27
+ value: 'ASPECT_RATIO_5_4',
28
+ label: '5:4',
29
+ ar: 5 / 4
30
+ }, {
31
+ value: 'ASPECT_RATIO_16_9',
32
+ label: '16:9',
33
+ ar: 16 / 9
34
+ }, {
35
+ value: 'ASPECT_RATIO_16_10',
36
+ label: '16:10',
37
+ ar: 16 / 10
38
+ }, {
39
+ value: 'ASPECT_RATIO_21_9',
40
+ label: '21:9',
41
+ ar: 21 / 9
42
+ }, {
43
+ value: 'ASPECT_RATIO_9_16',
44
+ label: '9:16',
45
+ ar: 9 / 16
46
+ }];
47
+ const DEFAULT_RATIO = 'ASPECT_RATIO_16_9';
48
+ const DEFAULT_SIZE = '1080';
49
+ const DEFAULT_QUALITY = '90';
50
+ export const CreateVariantDrawer = ({
51
+ application,
52
+ original,
53
+ isOpen,
54
+ existingVariants,
55
+ onClose,
56
+ onCreated
57
+ }) => {
58
+ const lang = useLang();
59
+ const {
60
+ rpcGateway
61
+ } = useSPAContext();
62
+ const {
63
+ openToast
64
+ } = useToast();
65
+ const [ratio, setRatio] = useState(DEFAULT_RATIO);
66
+ const [dimension, setDimension] = useState('width');
67
+ const [size, setSize] = useState(DEFAULT_SIZE);
68
+ const [quality, setQuality] = useState(DEFAULT_QUALITY);
69
+ const [isCreating, setIsCreating] = useState(false);
70
+ // The drawer mounts once and is toggled via `isOpen`, so reset every field
71
+ // whenever it opens for a different original — otherwise values (and the
72
+ // `ratioExists` guard) leak from the previously edited image.
73
+ useEffect(() => {
74
+ if (original) {
75
+ setRatio(DEFAULT_RATIO);
76
+ setDimension('width');
77
+ setSize(DEFAULT_SIZE);
78
+ setQuality(DEFAULT_QUALITY);
79
+ }
80
+ }, [original]);
81
+ const isJpeg = /jpe?g/i.test((original === null || original === void 0 ? void 0 : original.mimeType) ?? '');
82
+ const sizeValue = Number(size);
83
+ const isSizeValid = Number.isFinite(sizeValue) && sizeValue > 0;
84
+ const qualityValue = Number(quality);
85
+ const isQualityValid = !isJpeg || Number.isFinite(qualityValue) && qualityValue >= 1 && qualityValue <= 100;
86
+ const selectedRatio = RATIOS.find(item => item.value === ratio) ?? RATIOS[0];
87
+ const outputWidth = dimension === 'width' ? sizeValue : Math.round(sizeValue * selectedRatio.ar);
88
+ const outputHeight = dimension === 'height' ? sizeValue : Math.round(sizeValue / selectedRatio.ar);
89
+ const ratioExists = existingVariants.some(variant => {
90
+ var _variant$meta;
91
+ return ((_variant$meta = variant.meta) === null || _variant$meta === void 0 ? void 0 : _variant$meta.type) === 'imageMeta' && variant.meta.data.ratio === selectedRatio.label;
92
+ });
93
+ const handleCreate = async () => {
94
+ if (!original || !isSizeValid || !isQualityValid || ratioExists) return;
95
+ // Capture the parent up front so a concurrent reopen for another image
96
+ // can't misroute the created variant.
97
+ const parent = original;
98
+ setIsCreating(true);
99
+ try {
100
+ const response = await rpcGateway.mediaFiles.CreateVariant({
101
+ application,
102
+ uuid: parent.uuid,
103
+ ratio,
104
+ size: {
105
+ type: dimension,
106
+ data: sizeValue
107
+ },
108
+ ...(isJpeg ? {
109
+ quality: qualityValue
110
+ } : {})
111
+ });
112
+ openToast({
113
+ title: lang({
114
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS'
115
+ }),
116
+ type: 'success'
117
+ });
118
+ onCreated(parent.uuid, response.file);
119
+ } catch (error) {
120
+ openToast({
121
+ title: error.message || lang({
122
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR'
123
+ }),
124
+ type: 'error'
125
+ });
126
+ } finally {
127
+ setIsCreating(false);
128
+ }
129
+ };
130
+ return React.createElement(Drawer, {
131
+ size: "medium",
132
+ isOpen: isOpen,
133
+ onClose: onClose
134
+ }, React.createElement(DrawerHeader, null, React.createElement(DrawerTitle, null, React.createElement("span", null, lang({
135
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE'
136
+ })), React.createElement(Button, {
137
+ view: "ghost",
138
+ color: "secondary",
139
+ size: "compact",
140
+ icon: React.createElement(CloseIcon, {
141
+ size: "small"
142
+ }),
143
+ "aria-label": lang({
144
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE'
145
+ }),
146
+ onClick: onClose
147
+ }))), React.createElement(DrawerBody, null, original && React.createElement(Vertical, {
148
+ "$gap": 20
149
+ }, React.createElement("div", null, React.createElement(FieldLabel, null, lang({
150
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL'
151
+ })), React.createElement(Chips, null, RATIOS.map(item => React.createElement(Chip, {
152
+ key: item.value,
153
+ type: "button",
154
+ "$active": item.value === ratio,
155
+ onClick: () => setRatio(item.value)
156
+ }, item.label)))), React.createElement("div", null, React.createElement(FieldLabel, null, lang({
157
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL'
158
+ })), React.createElement(Columns, {
159
+ "$sizes": "auto 1fr",
160
+ "$gap": 8,
161
+ "$alignItems": "center"
162
+ }, React.createElement(Segment, null, React.createElement(SegmentItem, {
163
+ type: "button",
164
+ "$active": dimension === 'width',
165
+ onClick: () => setDimension('width')
166
+ }, lang({
167
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH'
168
+ })), React.createElement(SegmentItem, {
169
+ type: "button",
170
+ "$active": dimension === 'height',
171
+ onClick: () => setDimension('height')
172
+ }, lang({
173
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT'
174
+ }))), React.createElement(NativeInput, {
175
+ type: "number",
176
+ min: 1,
177
+ value: size,
178
+ "$isErrored": !isSizeValid,
179
+ onChange: event => setSize(event.target.value)
180
+ }))), React.createElement(Vertical, {
181
+ "$gap": 6
182
+ }, React.createElement(PreviewStage, null, React.createElement(PreviewFrame, {
183
+ "$ratio": selectedRatio.ar
184
+ }, React.createElement(PreviewImage, {
185
+ src: original.url,
186
+ alt: original.filename
187
+ }))), isSizeValid && React.createElement(Text, null, selectedRatio.label, ' · ', outputWidth, ' × ', outputHeight)), isJpeg && React.createElement("div", null, React.createElement(FieldLabel, null, lang({
188
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL'
189
+ })), React.createElement(NativeInput, {
190
+ type: "number",
191
+ min: 1,
192
+ max: 100,
193
+ value: quality,
194
+ "$isErrored": !isQualityValid,
195
+ onChange: event => setQuality(event.target.value)
196
+ })), ratioExists && React.createElement(Text, null, lang({
197
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS'
198
+ })), React.createElement(Button, {
199
+ color: "primary",
200
+ size: "field",
201
+ isDisabled: !isSizeValid || !isQualityValid || ratioExists,
202
+ isLoading: isCreating,
203
+ onClick: () => void handleCreate()
204
+ }, lang({
205
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON'
206
+ })), React.createElement("div", null, React.createElement(FieldLabel, null, lang({
207
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL'
208
+ })), existingVariants.length === 0 ? React.createElement(Text, null, lang({
209
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS'
210
+ })) : React.createElement(VariantStrip, null, existingVariants.map(variant => React.createElement(VariantThumb, {
211
+ key: variant.uuid
212
+ }, React.createElement(TileImage, {
213
+ src: getThumbUrl(variant),
214
+ alt: variant.filename
215
+ }), React.createElement(VariantThumbCaption, null, getRatioLabel(variant) || variant.filename))))))));
216
+ };
@@ -0,0 +1,12 @@
1
+ import { type FC } from 'react';
2
+ export type MediaStoreProps = {
3
+ /** Application code whose media files are listed. */
4
+ application: string;
5
+ /** Initial page to load (zero-based). Defaults to 0. */
6
+ page?: number;
7
+ /** Page size. Defaults to 30. */
8
+ perPage?: number;
9
+ /** Called with the chosen file URL (original or variant). */
10
+ onSelect: (url: string) => void;
11
+ };
12
+ export declare const MediaStore: FC<MediaStoreProps>;
@@ -0,0 +1,258 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Button, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, useToast } from '@pushwoosh/dumb-components';
3
+ import { Horizontal } from '@pushwoosh/kit-helpers';
4
+ import { CheckIcon, EditIcon, UploadIcon } from '@pushwoosh/kit-icons';
5
+ import { Text } from '@pushwoosh/kit-typography';
6
+ import { useSPAContext } from '@pushwoosh/spa-context';
7
+ import { CreateVariantDrawer } from './CreateVariantDrawer';
8
+ import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
9
+ import { useLang } from './polyglot';
10
+ import { Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, VariantBadge } from './styled';
11
+ const DEFAULT_PER_PAGE = 30;
12
+ const UPLOAD_PATH = '/api/media/upload';
13
+ export const MediaStore = ({
14
+ application,
15
+ page: initialPage = 0,
16
+ perPage = DEFAULT_PER_PAGE,
17
+ onSelect
18
+ }) => {
19
+ const lang = useLang();
20
+ const {
21
+ rpcGateway,
22
+ httpClient,
23
+ helpers: {
24
+ getApiOrigin
25
+ }
26
+ } = useSPAContext();
27
+ const {
28
+ openToast
29
+ } = useToast();
30
+ const uploadInputRef = useRef(null);
31
+ const loadTokenRef = useRef(0);
32
+ const [files, setFiles] = useState([]);
33
+ const [variantsByUuid, setVariantsByUuid] = useState({});
34
+ const [loading, setLoading] = useState(false);
35
+ const [page, setPage] = useState(initialPage);
36
+ const [total, setTotal] = useState(0);
37
+ const [isUploading, setIsUploading] = useState(false);
38
+ const [isDragOver, setIsDragOver] = useState(false);
39
+ const [createVariantFor, setCreateVariantFor] = useState(null);
40
+ const fetchFiles = useCallback(async p => {
41
+ // Guard against races: a newer load invalidates in-flight results of older ones.
42
+ const token = ++loadTokenRef.current;
43
+ setLoading(true);
44
+ setVariantsByUuid({});
45
+ let originals;
46
+ try {
47
+ const response = await rpcGateway.mediaFiles.List({
48
+ application,
49
+ page: p,
50
+ perPage
51
+ });
52
+ if (token !== loadTokenRef.current) return;
53
+ // protobuf JSON omits empty `repeated` fields, so `files` can be undefined.
54
+ originals = response.files ?? [];
55
+ setFiles(originals);
56
+ setTotal(response.total);
57
+ setPage(response.page);
58
+ } catch (error) {
59
+ if (token === loadTokenRef.current) {
60
+ openToast({
61
+ title: error.message || lang({
62
+ message: 'SMART.MEDIA_STORE.LOAD_ERROR'
63
+ }),
64
+ type: 'error'
65
+ });
66
+ }
67
+ return;
68
+ } finally {
69
+ if (token === loadTokenRef.current) {
70
+ setLoading(false);
71
+ }
72
+ }
73
+ // Originals are already on screen. The List of originals carries no variant
74
+ // count, so fetch each original's variants in the background and let the
75
+ // "N variants" badge appear per file as soon as its request resolves.
76
+ originals.forEach(file => {
77
+ void (async () => {
78
+ try {
79
+ const response = await rpcGateway.mediaFiles.List({
80
+ application,
81
+ parentUuid: file.uuid
82
+ });
83
+ if (token !== loadTokenRef.current) return;
84
+ setVariantsByUuid(prev => ({
85
+ ...prev,
86
+ [file.uuid]: response.files ?? []
87
+ }));
88
+ } catch {
89
+ // Ignore — the badge just won't appear for this file.
90
+ }
91
+ })();
92
+ });
93
+ }, [rpcGateway, application, perPage, openToast, lang]);
94
+ useEffect(() => {
95
+ void fetchFiles(initialPage);
96
+ }, [fetchFiles, initialPage]);
97
+ const uploadFiles = async selected => {
98
+ const images = selected.filter(file => file.type.startsWith('image/'));
99
+ if (images.length === 0) return;
100
+ setIsUploading(true);
101
+ try {
102
+ const results = await Promise.allSettled(images.map(file => {
103
+ const formData = new FormData();
104
+ formData.append('application', application);
105
+ formData.append('file', file);
106
+ formData.append('filename', file.name);
107
+ return httpClient.post(`${getApiOrigin('rpc-api')}${UPLOAD_PATH}`, {
108
+ body: formData,
109
+ params: undefined,
110
+ query: undefined,
111
+ withAuthorization: true
112
+ });
113
+ }));
114
+ const failed = results.filter(result => result.status === 'rejected').length;
115
+ if (failed < images.length) {
116
+ openToast({
117
+ title: lang({
118
+ message: 'SMART.MEDIA_STORE.UPLOAD_SUCCESS'
119
+ }),
120
+ type: 'success'
121
+ });
122
+ void fetchFiles(0);
123
+ }
124
+ if (failed > 0) {
125
+ openToast({
126
+ title: lang({
127
+ message: 'SMART.MEDIA_STORE.UPLOAD_ERROR'
128
+ }),
129
+ type: 'error'
130
+ });
131
+ }
132
+ } finally {
133
+ if (uploadInputRef.current) {
134
+ uploadInputRef.current.value = '';
135
+ }
136
+ setIsUploading(false);
137
+ }
138
+ };
139
+ const handleVariantCreated = (parentUuid, variant) => {
140
+ setVariantsByUuid(prev => ({
141
+ ...prev,
142
+ [parentUuid]: [...(prev[parentUuid] ?? []), variant]
143
+ }));
144
+ };
145
+ const totalPages = Math.ceil(total / perPage);
146
+ return React.createElement(React.Fragment, null, React.createElement(Dropzone, {
147
+ type: "button",
148
+ "$dragOver": isDragOver,
149
+ disabled: isUploading,
150
+ onClick: () => {
151
+ var _uploadInputRef$curre;
152
+ return (_uploadInputRef$curre = uploadInputRef.current) === null || _uploadInputRef$curre === void 0 ? void 0 : _uploadInputRef$curre.click();
153
+ },
154
+ onDragOver: event => {
155
+ event.preventDefault();
156
+ setIsDragOver(true);
157
+ },
158
+ onDragLeave: () => setIsDragOver(false),
159
+ onDrop: event => {
160
+ event.preventDefault();
161
+ setIsDragOver(false);
162
+ void uploadFiles(Array.from(event.dataTransfer.files));
163
+ }
164
+ }, React.createElement(UploadIcon, {
165
+ size: "medium",
166
+ view: "lined"
167
+ }), isUploading ? lang({
168
+ message: 'SMART.MEDIA_STORE.UPLOADING'
169
+ }) : lang({
170
+ message: 'SMART.MEDIA_STORE.DROPZONE_HINT'
171
+ })), React.createElement(HiddenInput, {
172
+ ref: uploadInputRef,
173
+ type: "file",
174
+ accept: "image/*",
175
+ multiple: true,
176
+ onChange: event => void uploadFiles(Array.from(event.target.files ?? []))
177
+ }), loading && React.createElement(EmptyState, null, lang({
178
+ message: 'SMART.MEDIA_STORE.LOADING'
179
+ })), !loading && files.length === 0 && React.createElement(EmptyState, null, lang({
180
+ message: 'SMART.MEDIA_STORE.EMPTY'
181
+ })), !loading && files.length > 0 && React.createElement(React.Fragment, null, React.createElement(Grid, null, files.map(file => {
182
+ const variants = variantsByUuid[file.uuid] ?? [];
183
+ return React.createElement(Tile, {
184
+ key: file.uuid
185
+ }, React.createElement(TileImage, {
186
+ src: getThumbUrl(file),
187
+ alt: file.filename
188
+ }), variants.length > 0 && React.createElement(VariantBadge, null, lang({
189
+ message: 'SMART.MEDIA_STORE.VARIANTS_BADGE',
190
+ count: variants.length
191
+ })), React.createElement(TileActions, null, variants.length > 0 ? React.createElement(DropdownMenuButton, {
192
+ color: "secondary",
193
+ size: "compact",
194
+ icon: React.createElement(CheckIcon, {
195
+ size: "small"
196
+ }),
197
+ menuPlacement: "bottom-start"
198
+ }, React.createElement(DropdownMenuSection, null, React.createElement(DropdownMenuItem, {
199
+ onClick: () => onSelect(file.url)
200
+ }, lang({
201
+ message: 'SMART.MEDIA_STORE.SELECT_ORIGINAL'
202
+ })), variants.map(variant => React.createElement(DropdownMenuItem, {
203
+ key: variant.uuid,
204
+ onClick: () => onSelect(variant.url)
205
+ }, getRatioLabel(variant) || variant.filename)))) : React.createElement(Button, {
206
+ color: "secondary",
207
+ size: "compact",
208
+ icon: React.createElement(CheckIcon, {
209
+ size: "small"
210
+ }),
211
+ "aria-label": lang({
212
+ message: 'SMART.MEDIA_STORE.SELECT_HINT'
213
+ }),
214
+ onClick: () => onSelect(file.url)
215
+ }), React.createElement(Button, {
216
+ color: "secondary",
217
+ size: "compact",
218
+ icon: React.createElement(EditIcon, {
219
+ size: "small",
220
+ view: "lined"
221
+ }),
222
+ "aria-label": lang({
223
+ message: 'SMART.MEDIA_STORE.CREATE_VARIANT_HINT'
224
+ }),
225
+ onClick: () => setCreateVariantFor(file)
226
+ })));
227
+ })), totalPages > 1 && React.createElement(Horizontal, {
228
+ "$justifyContent": "center",
229
+ "$alignItems": "center",
230
+ "$gap": 8,
231
+ "$margin": {
232
+ top: 16
233
+ }
234
+ }, React.createElement(Button, {
235
+ view: "ghost",
236
+ color: "secondary",
237
+ size: "field",
238
+ isDisabled: page <= 0,
239
+ onClick: () => void fetchFiles(page - 1)
240
+ }, lang({
241
+ message: 'SMART.MEDIA_STORE.PREV_BUTTON'
242
+ })), React.createElement(Text, null, `${page + 1} / ${totalPages}`), React.createElement(Button, {
243
+ view: "ghost",
244
+ color: "secondary",
245
+ size: "field",
246
+ isDisabled: page + 1 >= totalPages,
247
+ onClick: () => void fetchFiles(page + 1)
248
+ }, lang({
249
+ message: 'SMART.MEDIA_STORE.NEXT_BUTTON'
250
+ })))), React.createElement(CreateVariantDrawer, {
251
+ application: application,
252
+ original: createVariantFor,
253
+ isOpen: createVariantFor !== null,
254
+ existingVariants: createVariantFor ? variantsByUuid[createVariantFor.uuid] ?? [] : [],
255
+ onClose: () => setCreateVariantFor(null),
256
+ onCreated: handleVariantCreated
257
+ }));
258
+ };
@@ -0,0 +1 @@
1
+ export { MediaStore, type MediaStoreProps } from './MediaStore';
@@ -0,0 +1 @@
1
+ export { MediaStore } from './MediaStore';
@@ -0,0 +1,5 @@
1
+ import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
2
+ /** Thumbnail URL from image meta, falling back to the full-size URL. */
3
+ export declare const getThumbUrl: (file: MediaFile) => string;
4
+ /** Aspect-ratio label (e.g. "16:9") for a variant; empty for originals. */
5
+ export declare const getRatioLabel: (file: MediaFile) => string;
@@ -0,0 +1,10 @@
1
+ /** Thumbnail URL from image meta, falling back to the full-size URL. */
2
+ export const getThumbUrl = file => {
3
+ var _file$meta;
4
+ return ((_file$meta = file.meta) === null || _file$meta === void 0 ? void 0 : _file$meta.type) === 'imageMeta' ? file.meta.data.thumbnailUrl : file.url;
5
+ };
6
+ /** Aspect-ratio label (e.g. "16:9") for a variant; empty for originals. */
7
+ export const getRatioLabel = file => {
8
+ var _file$meta2;
9
+ return ((_file$meta2 = file.meta) === null || _file$meta2 === void 0 ? void 0 : _file$meta2.type) === 'imageMeta' ? file.meta.data.ratio : '';
10
+ };
@@ -0,0 +1,2 @@
1
+ import { type MediaStoreMessages } from './types';
2
+ export declare const Lang: import("react").FC<import("@pushwoosh/dumb-components/polyglot").LangProps<MediaStoreMessages>>, useLang: () => (props: import("@pushwoosh/dumb-components/polyglot").LangProps<MediaStoreMessages>) => string;