@pushwoosh/smart-components-kit 1.0.77 → 1.0.89
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.
- package/DocsButton/DocsButton.js +5 -1
- package/DocsButton/polyglot/index.d.ts +5 -0
- package/DocsButton/polyglot/index.js +10 -0
- package/DocsButton/polyglot/messages_en.d.ts +3 -0
- package/DocsButton/polyglot/messages_en.js +3 -0
- package/DocsButton/polyglot/messages_ru.d.ts +3 -0
- package/DocsButton/polyglot/messages_ru.js +3 -0
- package/DocsButton/polyglot/types.d.ts +4 -0
- package/DocsButton/polyglot/types.js +1 -0
- package/DynamicContentPicker/styled.d.ts +16 -104
- package/DynamicContentPickerForm/styled.d.ts +18 -106
- package/LanguagesSelector/LanguagesModal.js +11 -3
- package/LanguagesSelector/LanguagesSelectForm.js +34 -16
- package/LanguagesSelector/LanguagesSelector.js +10 -6
- package/LanguagesSelector/polyglot/index.d.ts +2 -0
- package/LanguagesSelector/polyglot/index.js +10 -0
- package/LanguagesSelector/polyglot/messages_en.d.ts +3 -0
- package/LanguagesSelector/polyglot/messages_en.js +13 -0
- package/LanguagesSelector/polyglot/messages_ru.d.ts +3 -0
- package/LanguagesSelector/polyglot/messages_ru.js +13 -0
- package/LanguagesSelector/polyglot/types.d.ts +17 -0
- package/LanguagesSelector/polyglot/types.js +1 -0
- package/LanguagesSelector/styled.d.ts +8 -52
- package/MediaStore/CreateVariantDrawer.d.ts +12 -0
- package/MediaStore/CreateVariantDrawer.js +216 -0
- package/MediaStore/MediaStore.d.ts +12 -0
- package/MediaStore/MediaStore.js +258 -0
- package/MediaStore/index.d.ts +1 -0
- package/MediaStore/index.js +1 -0
- package/MediaStore/mediaFile.utils.d.ts +5 -0
- package/MediaStore/mediaFile.utils.js +10 -0
- package/MediaStore/polyglot/index.d.ts +2 -0
- package/MediaStore/polyglot/index.js +10 -0
- package/MediaStore/polyglot/messages_en.d.ts +3 -0
- package/MediaStore/polyglot/messages_en.js +28 -0
- package/MediaStore/polyglot/messages_ru.d.ts +3 -0
- package/MediaStore/polyglot/messages_ru.js +28 -0
- package/MediaStore/polyglot/types.d.ts +31 -0
- package/MediaStore/polyglot/types.js +1 -0
- package/MediaStore/styled.d.ts +27 -0
- package/MediaStore/styled.js +95 -0
- package/ModalCloningJourney/ModalCloningJourney.js +67 -22
- package/ModalCloningJourney/polyglot/index.d.ts +2 -0
- package/ModalCloningJourney/polyglot/index.js +10 -0
- package/ModalCloningJourney/polyglot/messages_en.d.ts +3 -0
- package/ModalCloningJourney/polyglot/messages_en.js +22 -0
- package/ModalCloningJourney/polyglot/messages_ru.d.ts +3 -0
- package/ModalCloningJourney/polyglot/messages_ru.js +22 -0
- package/ModalCloningJourney/polyglot/types.d.ts +25 -0
- package/ModalCloningJourney/polyglot/types.js +1 -0
- package/OpenActions/components/OpenActionsSummary/styles.d.ts +8 -52
- package/RichMessageEditorSmart/NewDynamicContentForm.js +5 -1
- package/RichMessageEditorSmart/polyglot/index.d.ts +5 -0
- package/RichMessageEditorSmart/polyglot/index.js +10 -0
- package/RichMessageEditorSmart/polyglot/messages_en.d.ts +3 -0
- package/RichMessageEditorSmart/polyglot/messages_en.js +3 -0
- package/RichMessageEditorSmart/polyglot/messages_ru.d.ts +3 -0
- package/RichMessageEditorSmart/polyglot/messages_ru.js +3 -0
- package/RichMessageEditorSmart/polyglot/types.d.ts +4 -0
- package/RichMessageEditorSmart/polyglot/types.js +1 -0
- package/Table/components/DataTable/DataTable.js +27 -8
- package/Table/components/DataTableList/DataTableList.js +29 -9
- package/Table/components/TablePagination/TablePagination.js +5 -1
- package/Table/polyglot/index.d.ts +2 -0
- package/Table/polyglot/index.js +10 -0
- package/Table/polyglot/messages_en.d.ts +3 -0
- package/Table/polyglot/messages_en.js +18 -0
- package/Table/polyglot/messages_ru.d.ts +3 -0
- package/Table/polyglot/messages_ru.js +18 -0
- package/Table/polyglot/types.d.ts +21 -0
- package/Table/polyglot/types.js +1 -0
- package/Table/styles.d.ts +34 -210
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +3 -2
|
@@ -1,56 +1,12 @@
|
|
|
1
|
-
export declare const ButtonGroup: import("styled-components").StyledComponent<"div", any, {
|
|
2
|
-
$alignContent?: "
|
|
3
|
-
$alignItems?: "
|
|
4
|
-
$
|
|
5
|
-
$
|
|
6
|
-
|
|
7
|
-
right?: string | undefined;
|
|
8
|
-
bottom?: string | undefined;
|
|
9
|
-
left?: string | undefined;
|
|
10
|
-
} | {
|
|
11
|
-
horizontal: string;
|
|
12
|
-
top?: string | undefined;
|
|
13
|
-
bottom?: string | undefined;
|
|
14
|
-
} | {
|
|
15
|
-
vertical: string;
|
|
16
|
-
right?: string | undefined;
|
|
17
|
-
left?: string | undefined;
|
|
18
|
-
}) | undefined;
|
|
19
|
-
$borderRadius?: (string | number) | undefined;
|
|
20
|
-
$boxShadow?: string | undefined;
|
|
21
|
-
$gap?: (string | number) | undefined;
|
|
1
|
+
export declare const ButtonGroup: import("styled-components").StyledComponent<"div", any, import("@pushwoosh/kit-helpers/styled-helpers").CommonBoxProps & {
|
|
2
|
+
$alignContent?: import("@pushwoosh/kit-helpers/styled-helpers").AlignContentValue | undefined;
|
|
3
|
+
$alignItems?: import("@pushwoosh/kit-helpers/styled-helpers").ItemAlignmentValue | undefined;
|
|
4
|
+
$gap?: import("@pushwoosh/kit-helpers/styled-helpers").StrNum | undefined;
|
|
5
|
+
$justifyContent?: import("@pushwoosh/kit-helpers/styled-helpers").JustifyContentValue | undefined;
|
|
6
|
+
} & {
|
|
22
7
|
$gridAutoFlow?: "row" | "column" | "dense" | "row dense" | "column dense" | undefined;
|
|
23
|
-
$
|
|
24
|
-
$
|
|
25
|
-
$margin?: (string | number) | ({
|
|
26
|
-
top?: (string | number) | undefined;
|
|
27
|
-
right?: (string | number) | undefined;
|
|
28
|
-
bottom?: (string | number) | undefined;
|
|
29
|
-
left?: (string | number) | undefined;
|
|
30
|
-
} | {
|
|
31
|
-
horizontal: string | number;
|
|
32
|
-
top?: (string | number) | undefined;
|
|
33
|
-
bottom?: (string | number) | undefined;
|
|
34
|
-
} | {
|
|
35
|
-
vertical: string | number;
|
|
36
|
-
right?: (string | number) | undefined;
|
|
37
|
-
left?: (string | number) | undefined;
|
|
38
|
-
}) | undefined;
|
|
39
|
-
$padding?: (string | number) | ({
|
|
40
|
-
top?: (string | number) | undefined;
|
|
41
|
-
right?: (string | number) | undefined;
|
|
42
|
-
bottom?: (string | number) | undefined;
|
|
43
|
-
left?: (string | number) | undefined;
|
|
44
|
-
} | {
|
|
45
|
-
horizontal: string | number;
|
|
46
|
-
top?: (string | number) | undefined;
|
|
47
|
-
bottom?: (string | number) | undefined;
|
|
48
|
-
} | {
|
|
49
|
-
vertical: string | number;
|
|
50
|
-
right?: (string | number) | undefined;
|
|
51
|
-
left?: (string | number) | undefined;
|
|
52
|
-
}) | undefined;
|
|
53
|
-
$placeItems?: "end" | "start" | "center" | "stretch" | undefined;
|
|
8
|
+
$justifyItems?: import("@pushwoosh/kit-helpers/styled-helpers").ItemAlignmentValue | undefined;
|
|
9
|
+
$placeItems?: import("@pushwoosh/kit-helpers/styled-helpers").ItemAlignmentValue | undefined;
|
|
54
10
|
} & {
|
|
55
11
|
$gridAutoFlow: string;
|
|
56
12
|
}, "$gridAutoFlow">;
|
|
@@ -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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const messages = {
|
|
2
|
+
'SMART.MEDIA_STORE.UPLOADING': 'Uploading...',
|
|
3
|
+
'SMART.MEDIA_STORE.DROPZONE_HINT': 'Drag & drop images here or click to upload',
|
|
4
|
+
'SMART.MEDIA_STORE.LOADING': 'Loading...',
|
|
5
|
+
'SMART.MEDIA_STORE.EMPTY': 'No media files found',
|
|
6
|
+
'SMART.MEDIA_STORE.SELECT_HINT': 'Click to select',
|
|
7
|
+
'SMART.MEDIA_STORE.PREV_BUTTON': 'Previous',
|
|
8
|
+
'SMART.MEDIA_STORE.NEXT_BUTTON': 'Next',
|
|
9
|
+
'SMART.MEDIA_STORE.LOAD_ERROR': 'Failed to load media files',
|
|
10
|
+
'SMART.MEDIA_STORE.UPLOAD_ERROR': 'Failed to upload image',
|
|
11
|
+
'SMART.MEDIA_STORE.UPLOAD_SUCCESS': 'Image uploaded',
|
|
12
|
+
'SMART.MEDIA_STORE.VARIANTS_BADGE': '{{count}} var.',
|
|
13
|
+
'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Original',
|
|
14
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT_HINT': 'Create variants',
|
|
15
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE': 'Create variant',
|
|
16
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE': 'Close',
|
|
17
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL': 'Aspect ratio',
|
|
18
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL': 'Size',
|
|
19
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH': 'Width',
|
|
20
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT': 'Height',
|
|
21
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL': 'JPEG quality (1–100)',
|
|
22
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON': 'Create variant',
|
|
23
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS': 'A variant with this aspect ratio already exists',
|
|
24
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL': 'Existing variants',
|
|
25
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS': 'No variants yet',
|
|
26
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS': 'Variant created',
|
|
27
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR': 'Failed to create variant'
|
|
28
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const messages = {
|
|
2
|
+
'SMART.MEDIA_STORE.UPLOADING': 'Загрузка...',
|
|
3
|
+
'SMART.MEDIA_STORE.DROPZONE_HINT': 'Перетащите изображения сюда или нажмите для загрузки',
|
|
4
|
+
'SMART.MEDIA_STORE.LOADING': 'Загрузка...',
|
|
5
|
+
'SMART.MEDIA_STORE.EMPTY': 'Медиафайлы не найдены',
|
|
6
|
+
'SMART.MEDIA_STORE.SELECT_HINT': 'Нажмите, чтобы выбрать',
|
|
7
|
+
'SMART.MEDIA_STORE.PREV_BUTTON': 'Назад',
|
|
8
|
+
'SMART.MEDIA_STORE.NEXT_BUTTON': 'Вперёд',
|
|
9
|
+
'SMART.MEDIA_STORE.LOAD_ERROR': 'Не удалось загрузить медиафайлы',
|
|
10
|
+
'SMART.MEDIA_STORE.UPLOAD_ERROR': 'Не удалось загрузить изображение',
|
|
11
|
+
'SMART.MEDIA_STORE.UPLOAD_SUCCESS': 'Изображение загружено',
|
|
12
|
+
'SMART.MEDIA_STORE.VARIANTS_BADGE': '{{count}} вар.',
|
|
13
|
+
'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Оригинал',
|
|
14
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT_HINT': 'Создать варианты',
|
|
15
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.TITLE': 'Создание варианта',
|
|
16
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.CLOSE': 'Закрыть',
|
|
17
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_LABEL': 'Соотношение сторон',
|
|
18
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.SIZE_LABEL': 'Размер',
|
|
19
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.WIDTH': 'Ширина',
|
|
20
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.HEIGHT': 'Высота',
|
|
21
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.QUALITY_LABEL': 'Качество JPEG (1–100)',
|
|
22
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.CREATE_BUTTON': 'Создать вариант',
|
|
23
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.RATIO_EXISTS': 'Вариант с таким соотношением сторон уже существует',
|
|
24
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.EXISTING_LABEL': 'Созданные варианты',
|
|
25
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.NO_VARIANTS': 'Вариантов пока нет',
|
|
26
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.SUCCESS': 'Вариант создан',
|
|
27
|
+
'SMART.MEDIA_STORE.CREATE_VARIANT.ERROR': 'Не удалось создать вариант'
|
|
28
|
+
};
|