@pushwoosh/smart-components-kit 1.1.0 → 1.1.2
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/MediaStore/{CreateVariantDrawer.d.ts → CreateCropDrawer.d.ts} +4 -4
- package/MediaStore/{CreateVariantDrawer.js → CreateCropDrawer.js} +28 -28
- package/MediaStore/MediaStore.d.ts +6 -10
- package/MediaStore/MediaStore.js +92 -77
- package/MediaStore/MediaStorePaginated.d.ts +8 -0
- package/MediaStore/MediaStorePaginated.js +24 -0
- package/MediaStore/index.d.ts +3 -1
- package/MediaStore/index.js +2 -1
- package/MediaStore/mediaFile.utils.d.ts +1 -1
- package/MediaStore/mediaFile.utils.js +1 -1
- package/MediaStore/polyglot/messages_en.js +15 -15
- package/MediaStore/polyglot/messages_ru.js +15 -15
- package/MediaStore/polyglot/types.d.ts +15 -15
- package/MediaStore/styled.d.ts +4 -4
- package/MediaStore/styled.js +9 -9
- package/MediaStore/types.d.ts +22 -0
- package/MediaStore/types.js +1 -0
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type FC } from 'react';
|
|
2
2
|
import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
|
|
3
|
-
type
|
|
3
|
+
type CreateCropDrawerProps = {
|
|
4
4
|
application: string;
|
|
5
5
|
original: MediaFile | null;
|
|
6
6
|
isOpen: boolean;
|
|
7
|
-
|
|
7
|
+
existingCrops: MediaFile[];
|
|
8
8
|
onClose: () => void;
|
|
9
|
-
onCreated: (parentUuid: string,
|
|
9
|
+
onCreated: (parentUuid: string, crop: MediaFile) => void;
|
|
10
10
|
};
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const CreateCropDrawer: FC<CreateCropDrawerProps>;
|
|
12
12
|
export {};
|
|
@@ -7,7 +7,7 @@ import { Text } from '@pushwoosh/kit-typography';
|
|
|
7
7
|
import { useSPAContext } from '@pushwoosh/spa-context';
|
|
8
8
|
import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
|
|
9
9
|
import { useLang } from './polyglot';
|
|
10
|
-
import { Chip, Chips, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage
|
|
10
|
+
import { Chip, Chips, CropStrip, CropThumb, CropThumbCaption, FieldLabel, PreviewFrame, PreviewImage, PreviewStage, Segment, SegmentItem, TileImage } from './styled';
|
|
11
11
|
const RATIOS = [{
|
|
12
12
|
value: 'ASPECT_RATIO_1_1',
|
|
13
13
|
label: '1:1',
|
|
@@ -48,11 +48,11 @@ const RATIOS = [{
|
|
|
48
48
|
const DEFAULT_RATIO = 'ASPECT_RATIO_16_9';
|
|
49
49
|
const DEFAULT_SIZE = '1080';
|
|
50
50
|
const DEFAULT_QUALITY = '90';
|
|
51
|
-
export const
|
|
51
|
+
export const CreateCropDrawer = ({
|
|
52
52
|
application,
|
|
53
53
|
original,
|
|
54
54
|
isOpen,
|
|
55
|
-
|
|
55
|
+
existingCrops,
|
|
56
56
|
onClose,
|
|
57
57
|
onCreated
|
|
58
58
|
}) => {
|
|
@@ -87,14 +87,14 @@ export const CreateVariantDrawer = ({
|
|
|
87
87
|
const selectedRatio = RATIOS.find(item => item.value === ratio) ?? RATIOS[0];
|
|
88
88
|
const outputWidth = dimension === 'width' ? sizeValue : Math.round(sizeValue * selectedRatio.ar);
|
|
89
89
|
const outputHeight = dimension === 'height' ? sizeValue : Math.round(sizeValue / selectedRatio.ar);
|
|
90
|
-
const ratioExists =
|
|
91
|
-
var
|
|
92
|
-
return ((
|
|
90
|
+
const ratioExists = existingCrops.some(crop => {
|
|
91
|
+
var _crop$meta;
|
|
92
|
+
return ((_crop$meta = crop.meta) === null || _crop$meta === void 0 ? void 0 : _crop$meta.type) === 'imageMeta' && crop.meta.data.ratio === selectedRatio.label;
|
|
93
93
|
});
|
|
94
94
|
const handleCreate = async () => {
|
|
95
95
|
if (!original || !isSizeValid || !isQualityValid || ratioExists) return;
|
|
96
96
|
// Capture the parent up front so a concurrent reopen for another image
|
|
97
|
-
// can't misroute the created
|
|
97
|
+
// can't misroute the created crop.
|
|
98
98
|
const parent = original;
|
|
99
99
|
setIsCreating(true);
|
|
100
100
|
try {
|
|
@@ -112,7 +112,7 @@ export const CreateVariantDrawer = ({
|
|
|
112
112
|
});
|
|
113
113
|
openToast({
|
|
114
114
|
title: lang({
|
|
115
|
-
message: 'SMART.MEDIA_STORE.
|
|
115
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS'
|
|
116
116
|
}),
|
|
117
117
|
type: 'success'
|
|
118
118
|
});
|
|
@@ -120,7 +120,7 @@ export const CreateVariantDrawer = ({
|
|
|
120
120
|
} catch (error) {
|
|
121
121
|
openToast({
|
|
122
122
|
title: error.message || lang({
|
|
123
|
-
message: 'SMART.MEDIA_STORE.
|
|
123
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.ERROR'
|
|
124
124
|
}),
|
|
125
125
|
type: 'error'
|
|
126
126
|
});
|
|
@@ -136,7 +136,7 @@ export const CreateVariantDrawer = ({
|
|
|
136
136
|
children: _jsxs(DrawerTitle, {
|
|
137
137
|
children: [_jsx("span", {
|
|
138
138
|
children: lang({
|
|
139
|
-
message: 'SMART.MEDIA_STORE.
|
|
139
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.TITLE'
|
|
140
140
|
})
|
|
141
141
|
}), _jsx(Button, {
|
|
142
142
|
view: "ghost",
|
|
@@ -146,7 +146,7 @@ export const CreateVariantDrawer = ({
|
|
|
146
146
|
size: "small"
|
|
147
147
|
}),
|
|
148
148
|
"aria-label": lang({
|
|
149
|
-
message: 'SMART.MEDIA_STORE.
|
|
149
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.CLOSE'
|
|
150
150
|
}),
|
|
151
151
|
onClick: onClose
|
|
152
152
|
})]
|
|
@@ -157,7 +157,7 @@ export const CreateVariantDrawer = ({
|
|
|
157
157
|
children: [_jsxs("div", {
|
|
158
158
|
children: [_jsx(FieldLabel, {
|
|
159
159
|
children: lang({
|
|
160
|
-
message: 'SMART.MEDIA_STORE.
|
|
160
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL'
|
|
161
161
|
})
|
|
162
162
|
}), _jsx(Chips, {
|
|
163
163
|
children: RATIOS.map(item => _jsx(Chip, {
|
|
@@ -170,7 +170,7 @@ export const CreateVariantDrawer = ({
|
|
|
170
170
|
}), _jsxs("div", {
|
|
171
171
|
children: [_jsx(FieldLabel, {
|
|
172
172
|
children: lang({
|
|
173
|
-
message: 'SMART.MEDIA_STORE.
|
|
173
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL'
|
|
174
174
|
})
|
|
175
175
|
}), _jsxs(Columns, {
|
|
176
176
|
"$sizes": "auto 1fr",
|
|
@@ -182,14 +182,14 @@ export const CreateVariantDrawer = ({
|
|
|
182
182
|
"$active": dimension === 'width',
|
|
183
183
|
onClick: () => setDimension('width'),
|
|
184
184
|
children: lang({
|
|
185
|
-
message: 'SMART.MEDIA_STORE.
|
|
185
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.WIDTH'
|
|
186
186
|
})
|
|
187
187
|
}), _jsx(SegmentItem, {
|
|
188
188
|
type: "button",
|
|
189
189
|
"$active": dimension === 'height',
|
|
190
190
|
onClick: () => setDimension('height'),
|
|
191
191
|
children: lang({
|
|
192
|
-
message: 'SMART.MEDIA_STORE.
|
|
192
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT'
|
|
193
193
|
})
|
|
194
194
|
})]
|
|
195
195
|
}), _jsx(NativeInput, {
|
|
@@ -216,7 +216,7 @@ export const CreateVariantDrawer = ({
|
|
|
216
216
|
}), isJpeg && _jsxs("div", {
|
|
217
217
|
children: [_jsx(FieldLabel, {
|
|
218
218
|
children: lang({
|
|
219
|
-
message: 'SMART.MEDIA_STORE.
|
|
219
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL'
|
|
220
220
|
})
|
|
221
221
|
}), _jsx(NativeInput, {
|
|
222
222
|
type: "number",
|
|
@@ -228,7 +228,7 @@ export const CreateVariantDrawer = ({
|
|
|
228
228
|
})]
|
|
229
229
|
}), ratioExists && _jsx(Text, {
|
|
230
230
|
children: lang({
|
|
231
|
-
message: 'SMART.MEDIA_STORE.
|
|
231
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS'
|
|
232
232
|
})
|
|
233
233
|
}), _jsx(Button, {
|
|
234
234
|
color: "primary",
|
|
@@ -237,26 +237,26 @@ export const CreateVariantDrawer = ({
|
|
|
237
237
|
isLoading: isCreating,
|
|
238
238
|
onClick: () => void handleCreate(),
|
|
239
239
|
children: lang({
|
|
240
|
-
message: 'SMART.MEDIA_STORE.
|
|
240
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON'
|
|
241
241
|
})
|
|
242
242
|
}), _jsxs("div", {
|
|
243
243
|
children: [_jsx(FieldLabel, {
|
|
244
244
|
children: lang({
|
|
245
|
-
message: 'SMART.MEDIA_STORE.
|
|
245
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL'
|
|
246
246
|
})
|
|
247
|
-
}),
|
|
247
|
+
}), existingCrops.length === 0 ? _jsx(Text, {
|
|
248
248
|
children: lang({
|
|
249
|
-
message: 'SMART.MEDIA_STORE.
|
|
249
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS'
|
|
250
250
|
})
|
|
251
|
-
}) : _jsx(
|
|
252
|
-
children:
|
|
251
|
+
}) : _jsx(CropStrip, {
|
|
252
|
+
children: existingCrops.map(crop => _jsxs(CropThumb, {
|
|
253
253
|
children: [_jsx(TileImage, {
|
|
254
|
-
src: getThumbUrl(
|
|
255
|
-
alt:
|
|
256
|
-
}), _jsx(
|
|
257
|
-
children: getRatioLabel(
|
|
254
|
+
src: getThumbUrl(crop),
|
|
255
|
+
alt: crop.filename
|
|
256
|
+
}), _jsx(CropThumbCaption, {
|
|
257
|
+
children: getRatioLabel(crop) || crop.filename
|
|
258
258
|
})]
|
|
259
|
-
},
|
|
259
|
+
}, crop.uuid))
|
|
260
260
|
})]
|
|
261
261
|
})]
|
|
262
262
|
})
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { type FC } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
perPage?: number;
|
|
9
|
-
/** Called with the chosen file URL (original or variant). */
|
|
10
|
-
onSelect: (url: string) => void;
|
|
11
|
-
};
|
|
2
|
+
import type { MediaStoreProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Controlled media gallery: the parent owns the current page (and page size)
|
|
5
|
+
* and is notified of navigation via `onPageChange`. For a self-contained
|
|
6
|
+
* version that keeps the page in internal state, use `MediaStorePaginated`.
|
|
7
|
+
*/
|
|
12
8
|
export declare const MediaStore: FC<MediaStoreProps>;
|
package/MediaStore/MediaStore.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Button, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, useToast } from '@pushwoosh/dumb-components';
|
|
4
|
-
import { Horizontal } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { Horizontal, useAsync } from '@pushwoosh/kit-helpers';
|
|
5
5
|
import { CheckIcon, EditIcon, UploadIcon } from '@pushwoosh/kit-icons';
|
|
6
6
|
import { Text } from '@pushwoosh/kit-typography';
|
|
7
7
|
import { useSPAContext } from '@pushwoosh/spa-context';
|
|
8
|
-
import {
|
|
8
|
+
import { CreateCropDrawer } from './CreateCropDrawer';
|
|
9
9
|
import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
|
|
10
10
|
import { useLang } from './polyglot';
|
|
11
|
-
import { Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage
|
|
12
|
-
const DEFAULT_PER_PAGE = 30;
|
|
11
|
+
import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage } from './styled';
|
|
13
12
|
const UPLOAD_PATH = '/api/media/upload';
|
|
13
|
+
/**
|
|
14
|
+
* Controlled media gallery: the parent owns the current page (and page size)
|
|
15
|
+
* and is notified of navigation via `onPageChange`. For a self-contained
|
|
16
|
+
* version that keeps the page in internal state, use `MediaStorePaginated`.
|
|
17
|
+
*/
|
|
14
18
|
export const MediaStore = ({
|
|
15
19
|
application,
|
|
16
|
-
page
|
|
17
|
-
perPage
|
|
20
|
+
page,
|
|
21
|
+
perPage,
|
|
22
|
+
onPageChange,
|
|
18
23
|
onSelect
|
|
19
24
|
}) => {
|
|
25
|
+
var _listAsync$value, _listAsync$value2;
|
|
20
26
|
const lang = useLang();
|
|
21
27
|
const {
|
|
22
28
|
rpcGateway,
|
|
@@ -29,51 +35,54 @@ export const MediaStore = ({
|
|
|
29
35
|
openToast
|
|
30
36
|
} = useToast();
|
|
31
37
|
const uploadInputRef = useRef(null);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const [
|
|
37
|
-
|
|
38
|
+
// `uploadFiles` resolves asynchronously; read the latest page from the ref so
|
|
39
|
+
// the post-upload decision isn't made on a value captured at click time.
|
|
40
|
+
const pageRef = useRef(page);
|
|
41
|
+
pageRef.current = page;
|
|
42
|
+
const [cropsByUuid, setCropsByUuid] = useState({});
|
|
43
|
+
// Bumped to force a reload of the current page (e.g. after an upload) when
|
|
44
|
+
// the page prop itself does not change.
|
|
45
|
+
const [reloadToken, setReloadToken] = useState(0);
|
|
38
46
|
const [isUploading, setIsUploading] = useState(false);
|
|
39
47
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
40
|
-
const [
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const [createCropFor, setCreateCropFor] = useState(null);
|
|
49
|
+
const listAsync = useAsync(async () => {
|
|
50
|
+
const response = await rpcGateway.mediaFiles.List({
|
|
51
|
+
application,
|
|
52
|
+
page,
|
|
53
|
+
perPage
|
|
54
|
+
});
|
|
55
|
+
// protobuf JSON omits empty `repeated` fields, so `files` can be undefined.
|
|
56
|
+
return {
|
|
57
|
+
files: response.files ?? [],
|
|
58
|
+
total: response.total
|
|
59
|
+
};
|
|
60
|
+
}, [rpcGateway, application, page, perPage, reloadToken]);
|
|
61
|
+
const {
|
|
62
|
+
loading
|
|
63
|
+
} = listAsync;
|
|
64
|
+
const files = ((_listAsync$value = listAsync.value) === null || _listAsync$value === void 0 ? void 0 : _listAsync$value.files) ?? [];
|
|
65
|
+
const total = ((_listAsync$value2 = listAsync.value) === null || _listAsync$value2 === void 0 ? void 0 : _listAsync$value2.total) ?? 0;
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (listAsync.error) {
|
|
68
|
+
openToast({
|
|
69
|
+
title: listAsync.error.message || lang({
|
|
70
|
+
message: 'SMART.MEDIA_STORE.LOAD_ERROR'
|
|
71
|
+
}),
|
|
72
|
+
type: 'error'
|
|
52
73
|
});
|
|
53
|
-
if (token !== loadTokenRef.current) return;
|
|
54
|
-
// protobuf JSON omits empty `repeated` fields, so `files` can be undefined.
|
|
55
|
-
originals = response.files ?? [];
|
|
56
|
-
setFiles(originals);
|
|
57
|
-
setTotal(response.total);
|
|
58
|
-
setPage(response.page);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (token === loadTokenRef.current) {
|
|
61
|
-
openToast({
|
|
62
|
-
title: error.message || lang({
|
|
63
|
-
message: 'SMART.MEDIA_STORE.LOAD_ERROR'
|
|
64
|
-
}),
|
|
65
|
-
type: 'error'
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
} finally {
|
|
70
|
-
if (token === loadTokenRef.current) {
|
|
71
|
-
setLoading(false);
|
|
72
|
-
}
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
}, [listAsync.error, openToast, lang]);
|
|
76
|
+
// The List of originals carries no crop count, so once a page of originals
|
|
77
|
+
// resolves, fetch each one's crops in the background and let the
|
|
78
|
+
// "N crops" badge appear per file as soon as its request resolves. The
|
|
79
|
+
// cleanup flag drops results of a page that has since been replaced.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
var _listAsync$value3;
|
|
82
|
+
const originals = (_listAsync$value3 = listAsync.value) === null || _listAsync$value3 === void 0 ? void 0 : _listAsync$value3.files;
|
|
83
|
+
if (!originals) return undefined;
|
|
84
|
+
setCropsByUuid({});
|
|
85
|
+
let cancelled = false;
|
|
77
86
|
originals.forEach(file => {
|
|
78
87
|
void (async () => {
|
|
79
88
|
try {
|
|
@@ -81,8 +90,8 @@ export const MediaStore = ({
|
|
|
81
90
|
application,
|
|
82
91
|
parentUuid: file.uuid
|
|
83
92
|
});
|
|
84
|
-
if (
|
|
85
|
-
|
|
93
|
+
if (cancelled) return;
|
|
94
|
+
setCropsByUuid(prev => ({
|
|
86
95
|
...prev,
|
|
87
96
|
[file.uuid]: response.files ?? []
|
|
88
97
|
}));
|
|
@@ -91,10 +100,10 @@ export const MediaStore = ({
|
|
|
91
100
|
}
|
|
92
101
|
})();
|
|
93
102
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}, [
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
};
|
|
106
|
+
}, [listAsync.value, rpcGateway, application]);
|
|
98
107
|
const uploadFiles = async selected => {
|
|
99
108
|
const images = selected.filter(file => file.type.startsWith('image/'));
|
|
100
109
|
if (images.length === 0) return;
|
|
@@ -120,7 +129,13 @@ export const MediaStore = ({
|
|
|
120
129
|
}),
|
|
121
130
|
type: 'success'
|
|
122
131
|
});
|
|
123
|
-
|
|
132
|
+
// Jump back to the first page to surface the freshly uploaded files. If
|
|
133
|
+
// already there, the prop won't change, so bump the reload token too.
|
|
134
|
+
if (pageRef.current === 0) {
|
|
135
|
+
setReloadToken(token => token + 1);
|
|
136
|
+
} else {
|
|
137
|
+
onPageChange(0);
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
if (failed > 0) {
|
|
126
141
|
openToast({
|
|
@@ -137,10 +152,10 @@ export const MediaStore = ({
|
|
|
137
152
|
setIsUploading(false);
|
|
138
153
|
}
|
|
139
154
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
155
|
+
const handleCropCreated = (parentUuid, crop) => {
|
|
156
|
+
setCropsByUuid(prev => ({
|
|
142
157
|
...prev,
|
|
143
|
-
[parentUuid]: [...(prev[parentUuid] ?? []),
|
|
158
|
+
[parentUuid]: [...(prev[parentUuid] ?? []), crop]
|
|
144
159
|
}));
|
|
145
160
|
};
|
|
146
161
|
const totalPages = Math.ceil(total / perPage);
|
|
@@ -188,18 +203,18 @@ export const MediaStore = ({
|
|
|
188
203
|
}), !loading && files.length > 0 && _jsxs(_Fragment, {
|
|
189
204
|
children: [_jsx(Grid, {
|
|
190
205
|
children: files.map(file => {
|
|
191
|
-
const
|
|
206
|
+
const crops = cropsByUuid[file.uuid] ?? [];
|
|
192
207
|
return _jsxs(Tile, {
|
|
193
208
|
children: [_jsx(TileImage, {
|
|
194
209
|
src: getThumbUrl(file),
|
|
195
210
|
alt: file.filename
|
|
196
|
-
}),
|
|
211
|
+
}), crops.length > 0 && _jsx(CropBadge, {
|
|
197
212
|
children: lang({
|
|
198
|
-
message: 'SMART.MEDIA_STORE.
|
|
199
|
-
count:
|
|
213
|
+
message: 'SMART.MEDIA_STORE.CROPS_BADGE',
|
|
214
|
+
count: crops.length
|
|
200
215
|
})
|
|
201
216
|
}), _jsxs(TileActions, {
|
|
202
|
-
children: [
|
|
217
|
+
children: [crops.length > 0 ? _jsx(DropdownMenuButton, {
|
|
203
218
|
color: "secondary",
|
|
204
219
|
size: "compact",
|
|
205
220
|
icon: _jsx(CheckIcon, {
|
|
@@ -212,10 +227,10 @@ export const MediaStore = ({
|
|
|
212
227
|
children: lang({
|
|
213
228
|
message: 'SMART.MEDIA_STORE.SELECT_ORIGINAL'
|
|
214
229
|
})
|
|
215
|
-
}),
|
|
216
|
-
onClick: () => onSelect(
|
|
217
|
-
children: getRatioLabel(
|
|
218
|
-
},
|
|
230
|
+
}), crops.map(crop => _jsx(DropdownMenuItem, {
|
|
231
|
+
onClick: () => onSelect(crop.url),
|
|
232
|
+
children: getRatioLabel(crop) || crop.filename
|
|
233
|
+
}, crop.uuid))]
|
|
219
234
|
})
|
|
220
235
|
}) : _jsx(Button, {
|
|
221
236
|
color: "secondary",
|
|
@@ -235,9 +250,9 @@ export const MediaStore = ({
|
|
|
235
250
|
view: "lined"
|
|
236
251
|
}),
|
|
237
252
|
"aria-label": lang({
|
|
238
|
-
message: 'SMART.MEDIA_STORE.
|
|
253
|
+
message: 'SMART.MEDIA_STORE.CREATE_CROP_HINT'
|
|
239
254
|
}),
|
|
240
|
-
onClick: () =>
|
|
255
|
+
onClick: () => setCreateCropFor(file)
|
|
241
256
|
})]
|
|
242
257
|
})]
|
|
243
258
|
}, file.uuid);
|
|
@@ -254,7 +269,7 @@ export const MediaStore = ({
|
|
|
254
269
|
color: "secondary",
|
|
255
270
|
size: "field",
|
|
256
271
|
isDisabled: page <= 0,
|
|
257
|
-
onClick: () =>
|
|
272
|
+
onClick: () => onPageChange(page - 1),
|
|
258
273
|
children: lang({
|
|
259
274
|
message: 'SMART.MEDIA_STORE.PREV_BUTTON'
|
|
260
275
|
})
|
|
@@ -265,19 +280,19 @@ export const MediaStore = ({
|
|
|
265
280
|
color: "secondary",
|
|
266
281
|
size: "field",
|
|
267
282
|
isDisabled: page + 1 >= totalPages,
|
|
268
|
-
onClick: () =>
|
|
283
|
+
onClick: () => onPageChange(page + 1),
|
|
269
284
|
children: lang({
|
|
270
285
|
message: 'SMART.MEDIA_STORE.NEXT_BUTTON'
|
|
271
286
|
})
|
|
272
287
|
})]
|
|
273
288
|
})]
|
|
274
|
-
}), _jsx(
|
|
289
|
+
}), _jsx(CreateCropDrawer, {
|
|
275
290
|
application: application,
|
|
276
|
-
original:
|
|
277
|
-
isOpen:
|
|
278
|
-
|
|
279
|
-
onClose: () =>
|
|
280
|
-
onCreated:
|
|
291
|
+
original: createCropFor,
|
|
292
|
+
isOpen: createCropFor !== null,
|
|
293
|
+
existingCrops: createCropFor ? cropsByUuid[createCropFor.uuid] ?? [] : [],
|
|
294
|
+
onClose: () => setCreateCropFor(null),
|
|
295
|
+
onCreated: handleCropCreated
|
|
281
296
|
})]
|
|
282
297
|
});
|
|
283
298
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
import type { MediaStorePaginatedProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Self-contained {@link MediaStore} that owns the current page in internal
|
|
5
|
+
* state. Use this when the host has no opinion on pagination state; reach for
|
|
6
|
+
* `MediaStore` directly to drive the page from the URL or another store.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MediaStorePaginated: FC<MediaStorePaginatedProps>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { MediaStore } from './MediaStore';
|
|
4
|
+
const DEFAULT_PER_PAGE = 30;
|
|
5
|
+
/**
|
|
6
|
+
* Self-contained {@link MediaStore} that owns the current page in internal
|
|
7
|
+
* state. Use this when the host has no opinion on pagination state; reach for
|
|
8
|
+
* `MediaStore` directly to drive the page from the URL or another store.
|
|
9
|
+
*/
|
|
10
|
+
export const MediaStorePaginated = ({
|
|
11
|
+
application,
|
|
12
|
+
initialPage = 0,
|
|
13
|
+
perPage = DEFAULT_PER_PAGE,
|
|
14
|
+
onSelect
|
|
15
|
+
}) => {
|
|
16
|
+
const [page, setPage] = useState(initialPage);
|
|
17
|
+
return _jsx(MediaStore, {
|
|
18
|
+
application: application,
|
|
19
|
+
page: page,
|
|
20
|
+
perPage: perPage,
|
|
21
|
+
onPageChange: setPage,
|
|
22
|
+
onSelect: onSelect
|
|
23
|
+
});
|
|
24
|
+
};
|
package/MediaStore/index.d.ts
CHANGED
package/MediaStore/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { MediaStore } from './MediaStore';
|
|
1
|
+
export { MediaStore } from './MediaStore';
|
|
2
|
+
export { MediaStorePaginated } from './MediaStorePaginated';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
|
|
2
2
|
/** Thumbnail URL from image meta, falling back to the full-size URL. */
|
|
3
3
|
export declare const getThumbUrl: (file: MediaFile) => string;
|
|
4
|
-
/** Aspect-ratio label (e.g. "16:9") for a
|
|
4
|
+
/** Aspect-ratio label (e.g. "16:9") for a crop; empty for originals. */
|
|
5
5
|
export declare const getRatioLabel: (file: MediaFile) => string;
|
|
@@ -3,7 +3,7 @@ export const getThumbUrl = file => {
|
|
|
3
3
|
var _file$meta;
|
|
4
4
|
return ((_file$meta = file.meta) === null || _file$meta === void 0 ? void 0 : _file$meta.type) === 'imageMeta' ? file.meta.data.thumbnailUrl : file.url;
|
|
5
5
|
};
|
|
6
|
-
/** Aspect-ratio label (e.g. "16:9") for a
|
|
6
|
+
/** Aspect-ratio label (e.g. "16:9") for a crop; empty for originals. */
|
|
7
7
|
export const getRatioLabel = file => {
|
|
8
8
|
var _file$meta2;
|
|
9
9
|
return ((_file$meta2 = file.meta) === null || _file$meta2 === void 0 ? void 0 : _file$meta2.type) === 'imageMeta' ? file.meta.data.ratio : '';
|
|
@@ -9,20 +9,20 @@ export const messages = {
|
|
|
9
9
|
'SMART.MEDIA_STORE.LOAD_ERROR': 'Failed to load media files',
|
|
10
10
|
'SMART.MEDIA_STORE.UPLOAD_ERROR': 'Failed to upload image',
|
|
11
11
|
'SMART.MEDIA_STORE.UPLOAD_SUCCESS': 'Image uploaded',
|
|
12
|
-
'SMART.MEDIA_STORE.
|
|
12
|
+
'SMART.MEDIA_STORE.CROPS_BADGE': '{{count}} cr.',
|
|
13
13
|
'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Original',
|
|
14
|
-
'SMART.MEDIA_STORE.
|
|
15
|
-
'SMART.MEDIA_STORE.
|
|
16
|
-
'SMART.MEDIA_STORE.
|
|
17
|
-
'SMART.MEDIA_STORE.
|
|
18
|
-
'SMART.MEDIA_STORE.
|
|
19
|
-
'SMART.MEDIA_STORE.
|
|
20
|
-
'SMART.MEDIA_STORE.
|
|
21
|
-
'SMART.MEDIA_STORE.
|
|
22
|
-
'SMART.MEDIA_STORE.
|
|
23
|
-
'SMART.MEDIA_STORE.
|
|
24
|
-
'SMART.MEDIA_STORE.
|
|
25
|
-
'SMART.MEDIA_STORE.
|
|
26
|
-
'SMART.MEDIA_STORE.
|
|
27
|
-
'SMART.MEDIA_STORE.
|
|
14
|
+
'SMART.MEDIA_STORE.CREATE_CROP_HINT': 'Create crops',
|
|
15
|
+
'SMART.MEDIA_STORE.CREATE_CROP.TITLE': 'Create crop',
|
|
16
|
+
'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': 'Close',
|
|
17
|
+
'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': 'Aspect ratio',
|
|
18
|
+
'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL': 'Size',
|
|
19
|
+
'SMART.MEDIA_STORE.CREATE_CROP.WIDTH': 'Width',
|
|
20
|
+
'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT': 'Height',
|
|
21
|
+
'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL': 'JPEG quality (1–100)',
|
|
22
|
+
'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON': 'Create crop',
|
|
23
|
+
'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS': 'A crop with this aspect ratio already exists',
|
|
24
|
+
'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': 'Existing crops',
|
|
25
|
+
'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': 'No crops yet',
|
|
26
|
+
'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': 'Crop created',
|
|
27
|
+
'SMART.MEDIA_STORE.CREATE_CROP.ERROR': 'Failed to create crop'
|
|
28
28
|
};
|
|
@@ -9,20 +9,20 @@ export const messages = {
|
|
|
9
9
|
'SMART.MEDIA_STORE.LOAD_ERROR': 'Не удалось загрузить медиафайлы',
|
|
10
10
|
'SMART.MEDIA_STORE.UPLOAD_ERROR': 'Не удалось загрузить изображение',
|
|
11
11
|
'SMART.MEDIA_STORE.UPLOAD_SUCCESS': 'Изображение загружено',
|
|
12
|
-
'SMART.MEDIA_STORE.
|
|
12
|
+
'SMART.MEDIA_STORE.CROPS_BADGE': '{{count}} кр.',
|
|
13
13
|
'SMART.MEDIA_STORE.SELECT_ORIGINAL': 'Оригинал',
|
|
14
|
-
'SMART.MEDIA_STORE.
|
|
15
|
-
'SMART.MEDIA_STORE.
|
|
16
|
-
'SMART.MEDIA_STORE.
|
|
17
|
-
'SMART.MEDIA_STORE.
|
|
18
|
-
'SMART.MEDIA_STORE.
|
|
19
|
-
'SMART.MEDIA_STORE.
|
|
20
|
-
'SMART.MEDIA_STORE.
|
|
21
|
-
'SMART.MEDIA_STORE.
|
|
22
|
-
'SMART.MEDIA_STORE.
|
|
23
|
-
'SMART.MEDIA_STORE.
|
|
24
|
-
'SMART.MEDIA_STORE.
|
|
25
|
-
'SMART.MEDIA_STORE.
|
|
26
|
-
'SMART.MEDIA_STORE.
|
|
27
|
-
'SMART.MEDIA_STORE.
|
|
14
|
+
'SMART.MEDIA_STORE.CREATE_CROP_HINT': 'Создать кропы',
|
|
15
|
+
'SMART.MEDIA_STORE.CREATE_CROP.TITLE': 'Создание кропа',
|
|
16
|
+
'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': 'Закрыть',
|
|
17
|
+
'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': 'Соотношение сторон',
|
|
18
|
+
'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL': 'Размер',
|
|
19
|
+
'SMART.MEDIA_STORE.CREATE_CROP.WIDTH': 'Ширина',
|
|
20
|
+
'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT': 'Высота',
|
|
21
|
+
'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL': 'Качество JPEG (1–100)',
|
|
22
|
+
'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON': 'Создать кроп',
|
|
23
|
+
'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS': 'Кроп с таким соотношением сторон уже существует',
|
|
24
|
+
'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': 'Созданные кропы',
|
|
25
|
+
'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': 'Кропов пока нет',
|
|
26
|
+
'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': 'Кроп создан',
|
|
27
|
+
'SMART.MEDIA_STORE.CREATE_CROP.ERROR': 'Не удалось создать кроп'
|
|
28
28
|
};
|
|
@@ -10,22 +10,22 @@ export type MediaStoreMessages = {
|
|
|
10
10
|
'SMART.MEDIA_STORE.LOAD_ERROR': NoParams;
|
|
11
11
|
'SMART.MEDIA_STORE.UPLOAD_ERROR': NoParams;
|
|
12
12
|
'SMART.MEDIA_STORE.UPLOAD_SUCCESS': NoParams;
|
|
13
|
-
'SMART.MEDIA_STORE.
|
|
13
|
+
'SMART.MEDIA_STORE.CROPS_BADGE': {
|
|
14
14
|
count: number;
|
|
15
15
|
};
|
|
16
16
|
'SMART.MEDIA_STORE.SELECT_ORIGINAL': NoParams;
|
|
17
|
-
'SMART.MEDIA_STORE.
|
|
18
|
-
'SMART.MEDIA_STORE.
|
|
19
|
-
'SMART.MEDIA_STORE.
|
|
20
|
-
'SMART.MEDIA_STORE.
|
|
21
|
-
'SMART.MEDIA_STORE.
|
|
22
|
-
'SMART.MEDIA_STORE.
|
|
23
|
-
'SMART.MEDIA_STORE.
|
|
24
|
-
'SMART.MEDIA_STORE.
|
|
25
|
-
'SMART.MEDIA_STORE.
|
|
26
|
-
'SMART.MEDIA_STORE.
|
|
27
|
-
'SMART.MEDIA_STORE.
|
|
28
|
-
'SMART.MEDIA_STORE.
|
|
29
|
-
'SMART.MEDIA_STORE.
|
|
30
|
-
'SMART.MEDIA_STORE.
|
|
17
|
+
'SMART.MEDIA_STORE.CREATE_CROP_HINT': NoParams;
|
|
18
|
+
'SMART.MEDIA_STORE.CREATE_CROP.TITLE': NoParams;
|
|
19
|
+
'SMART.MEDIA_STORE.CREATE_CROP.CLOSE': NoParams;
|
|
20
|
+
'SMART.MEDIA_STORE.CREATE_CROP.RATIO_LABEL': NoParams;
|
|
21
|
+
'SMART.MEDIA_STORE.CREATE_CROP.SIZE_LABEL': NoParams;
|
|
22
|
+
'SMART.MEDIA_STORE.CREATE_CROP.WIDTH': NoParams;
|
|
23
|
+
'SMART.MEDIA_STORE.CREATE_CROP.HEIGHT': NoParams;
|
|
24
|
+
'SMART.MEDIA_STORE.CREATE_CROP.QUALITY_LABEL': NoParams;
|
|
25
|
+
'SMART.MEDIA_STORE.CREATE_CROP.CREATE_BUTTON': NoParams;
|
|
26
|
+
'SMART.MEDIA_STORE.CREATE_CROP.RATIO_EXISTS': NoParams;
|
|
27
|
+
'SMART.MEDIA_STORE.CREATE_CROP.EXISTING_LABEL': NoParams;
|
|
28
|
+
'SMART.MEDIA_STORE.CREATE_CROP.NO_CROPS': NoParams;
|
|
29
|
+
'SMART.MEDIA_STORE.CREATE_CROP.SUCCESS': NoParams;
|
|
30
|
+
'SMART.MEDIA_STORE.CREATE_CROP.ERROR': NoParams;
|
|
31
31
|
};
|
package/MediaStore/styled.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const Grid: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
2
2
|
export declare const Tile: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
3
3
|
export declare const TileImage: import("styled-components").StyledComponent<"img", any, {}, never>;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const CropBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
5
5
|
export declare const TileActions: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
6
6
|
export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
7
7
|
export declare const Dropzone: import("styled-components").StyledComponent<"button", any, {
|
|
@@ -22,6 +22,6 @@ export declare const Segment: import("styled-components").StyledComponent<"div",
|
|
|
22
22
|
export declare const SegmentItem: import("styled-components").StyledComponent<"button", any, {
|
|
23
23
|
$active?: boolean;
|
|
24
24
|
}, never>;
|
|
25
|
-
export declare const
|
|
26
|
-
export declare const
|
|
27
|
-
export declare const
|
|
25
|
+
export declare const CropStrip: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
26
|
+
export declare const CropThumb: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
27
|
+
export declare const CropThumbCaption: import("styled-components").StyledComponent<"div", any, {}, never>;
|
package/MediaStore/styled.js
CHANGED
|
@@ -12,8 +12,8 @@ export const TileImage = styled.img.withConfig({
|
|
|
12
12
|
displayName: "TileImage",
|
|
13
13
|
componentId: "sc-1d9kmgw-2"
|
|
14
14
|
})(["width:100%;aspect-ratio:1;object-fit:cover;display:block;"]);
|
|
15
|
-
export const
|
|
16
|
-
displayName: "
|
|
15
|
+
export const CropBadge = styled.span.withConfig({
|
|
16
|
+
displayName: "CropBadge",
|
|
17
17
|
componentId: "sc-1d9kmgw-3"
|
|
18
18
|
})(["position:absolute;top:6px;right:6px;font-size:10px;font-weight:", ";line-height:1;color:", ";background:", ";border-radius:20px;padding:3px 7px;pointer-events:none;"], FontWeight.BOLD, Color.CLEAR, Color.PRIMARY);
|
|
19
19
|
export const TileActions = styled.div.withConfig({
|
|
@@ -36,7 +36,7 @@ export const HiddenInput = styled.input.withConfig({
|
|
|
36
36
|
displayName: "HiddenInput",
|
|
37
37
|
componentId: "sc-1d9kmgw-7"
|
|
38
38
|
})(["display:none;"]);
|
|
39
|
-
/* ----------
|
|
39
|
+
/* ---------- CreateCropDrawer ---------- */
|
|
40
40
|
export const PreviewStage = styled.div.withConfig({
|
|
41
41
|
displayName: "PreviewStage",
|
|
42
42
|
componentId: "sc-1d9kmgw-8"
|
|
@@ -81,15 +81,15 @@ export const SegmentItem = styled.button.withConfig({
|
|
|
81
81
|
}) => $active ? Color.PRIMARY : Color.CLEAR, ({
|
|
82
82
|
$active
|
|
83
83
|
}) => $active ? Color.CLEAR : Color.MAIN, Color.DIVIDER);
|
|
84
|
-
export const
|
|
85
|
-
displayName: "
|
|
84
|
+
export const CropStrip = styled.div.withConfig({
|
|
85
|
+
displayName: "CropStrip",
|
|
86
86
|
componentId: "sc-1d9kmgw-16"
|
|
87
87
|
})(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
|
|
88
|
-
export const
|
|
89
|
-
displayName: "
|
|
88
|
+
export const CropThumb = styled.div.withConfig({
|
|
89
|
+
displayName: "CropThumb",
|
|
90
90
|
componentId: "sc-1d9kmgw-17"
|
|
91
91
|
})(["width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
|
|
92
|
-
export const
|
|
93
|
-
displayName: "
|
|
92
|
+
export const CropThumbCaption = styled.div.withConfig({
|
|
93
|
+
displayName: "CropThumbCaption",
|
|
94
94
|
componentId: "sc-1d9kmgw-18"
|
|
95
95
|
})(["font-size:10px;text-align:center;padding:3px 2px;color:", ";"], Color.LOCKED);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type MediaStoreProps = {
|
|
2
|
+
/** Application code whose media files are listed. */
|
|
3
|
+
application: string;
|
|
4
|
+
/** Current page (zero-based). Controlled by the parent. */
|
|
5
|
+
page: number;
|
|
6
|
+
/** Page size. */
|
|
7
|
+
perPage: number;
|
|
8
|
+
/** Called when the user requests a different page. */
|
|
9
|
+
onPageChange: (page: number) => void;
|
|
10
|
+
/** Called with the chosen file URL (original or crop). */
|
|
11
|
+
onSelect: (url: string) => void;
|
|
12
|
+
};
|
|
13
|
+
export type MediaStorePaginatedProps = {
|
|
14
|
+
/** Application code whose media files are listed. */
|
|
15
|
+
application: string;
|
|
16
|
+
/** Initial page to load (zero-based). Defaults to 0. */
|
|
17
|
+
initialPage?: number;
|
|
18
|
+
/** Page size. Defaults to 30. */
|
|
19
|
+
perPage?: number;
|
|
20
|
+
/** Called with the chosen file URL (original or crop). */
|
|
21
|
+
onSelect: (url: string) => void;
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED