@pushwoosh/smart-components-kit 1.3.37 → 1.5.0
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/MediaStorage/CanvaImportDrawer.d.ts +14 -0
- package/MediaStorage/CanvaImportDrawer.js +386 -0
- package/MediaStorage/MediaStorage.js +65 -30
- package/MediaStorage/canva.d.ts +18 -0
- package/MediaStorage/canva.js +86 -0
- package/MediaStorage/canvaPages.d.ts +2 -0
- package/MediaStorage/canvaPages.js +7 -0
- package/MediaStorage/index.d.ts +1 -0
- package/MediaStorage/index.js +2 -1
- package/MediaStorage/mediaFile.utils.d.ts +4 -0
- package/MediaStorage/mediaFile.utils.js +8 -0
- package/MediaStorage/polyglot/messages_ar.js +25 -1
- package/MediaStorage/polyglot/messages_bn.js +25 -1
- package/MediaStorage/polyglot/messages_de.js +25 -1
- package/MediaStorage/polyglot/messages_en.js +25 -1
- package/MediaStorage/polyglot/messages_es.js +25 -1
- package/MediaStorage/polyglot/messages_fr.js +25 -1
- package/MediaStorage/polyglot/messages_he.js +25 -1
- package/MediaStorage/polyglot/messages_hi.js +25 -1
- package/MediaStorage/polyglot/messages_ja.js +25 -1
- package/MediaStorage/polyglot/messages_ko.js +25 -1
- package/MediaStorage/polyglot/messages_pt.js +25 -1
- package/MediaStorage/polyglot/messages_ru.js +25 -1
- package/MediaStorage/polyglot/messages_th.js +25 -1
- package/MediaStorage/polyglot/messages_zh.js +25 -1
- package/MediaStorage/polyglot/types.d.ts +35 -0
- package/MediaStorage/styles.d.ts +8 -1
- package/MediaStorage/styles.js +49 -25
- package/package.json +3 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
import type { GetCanvaStatusResponse } from '@pushwoosh/rpc-v2-http-api-data/canva';
|
|
3
|
+
import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
|
|
4
|
+
type CanvaImportDrawerProps = {
|
|
5
|
+
application: string;
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
status: GetCanvaStatusResponse;
|
|
8
|
+
onStatusChange: () => void;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onImported: (files: MediaFile[]) => void;
|
|
11
|
+
};
|
|
12
|
+
/** Drawer that connects the user's Canva and imports pages of a chosen design into the library. */
|
|
13
|
+
export declare const CanvaImportDrawer: FC<CanvaImportDrawerProps>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Button, Chip, Drawer, DrawerBody, DrawerHeader, DrawerTitle, NativeInput, useToast } from '@pushwoosh/dumb-components';
|
|
4
|
+
import { FlexContainer, useAsync, Vertical } from '@pushwoosh/kit-helpers';
|
|
5
|
+
import { CloseIcon } from '@pushwoosh/kit-icons';
|
|
6
|
+
import { Text } from '@pushwoosh/kit-typography';
|
|
7
|
+
import { useSPAContext } from '@pushwoosh/spa-context';
|
|
8
|
+
import { ConsentClosedError, connectCanva, disconnectCanva, PopupBlockedError, UnsafeReturnPageError } from './canva';
|
|
9
|
+
import { togglePages } from './canvaPages';
|
|
10
|
+
import { useLang } from './polyglot';
|
|
11
|
+
import { Chips, DesignCaption, DesignTile, EmptyState, FieldLabel, Grid, Segment, SegmentItem, TileImage } from './styles';
|
|
12
|
+
const MAX_PAGES = 10;
|
|
13
|
+
const SEARCH_DELAY_MS = 400;
|
|
14
|
+
const POLL_INTERVAL_MS = 1500;
|
|
15
|
+
const IMPORT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
16
|
+
const errorMessage = (error, fallback) => error instanceof Error && error.message ? error.message : fallback;
|
|
17
|
+
const sleep = ms => new Promise(resolve => {
|
|
18
|
+
window.setTimeout(resolve, ms);
|
|
19
|
+
});
|
|
20
|
+
/** Drawer that connects the user's Canva and imports pages of a chosen design into the library. */
|
|
21
|
+
export const CanvaImportDrawer = ({
|
|
22
|
+
application,
|
|
23
|
+
isOpen,
|
|
24
|
+
status,
|
|
25
|
+
onStatusChange,
|
|
26
|
+
onClose,
|
|
27
|
+
onImported
|
|
28
|
+
}) => {
|
|
29
|
+
const lang = useLang();
|
|
30
|
+
const {
|
|
31
|
+
rpcGateway
|
|
32
|
+
} = useSPAContext();
|
|
33
|
+
const {
|
|
34
|
+
openToast
|
|
35
|
+
} = useToast();
|
|
36
|
+
const [query, setQuery] = useState('');
|
|
37
|
+
const [debouncedQuery, setDebouncedQuery] = useState('');
|
|
38
|
+
const [designs, setDesigns] = useState([]);
|
|
39
|
+
const [continuation, setContinuation] = useState('');
|
|
40
|
+
const [selected, setSelected] = useState(null);
|
|
41
|
+
const [pages, setPages] = useState([1]);
|
|
42
|
+
const [format, setFormat] = useState('CANVA_IMPORT_FORMAT_PNG');
|
|
43
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
44
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
45
|
+
const [progress, setProgress] = useState(null);
|
|
46
|
+
const runRef = useRef(0);
|
|
47
|
+
const queryRef = useRef('');
|
|
48
|
+
const isOpenRef = useRef(isOpen);
|
|
49
|
+
isOpenRef.current = isOpen;
|
|
50
|
+
queryRef.current = debouncedQuery;
|
|
51
|
+
const isReady = status.connected && status.tokenValid;
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const timer = window.setTimeout(() => setDebouncedQuery(query.trim()), SEARCH_DELAY_MS);
|
|
54
|
+
return () => window.clearTimeout(timer);
|
|
55
|
+
}, [query]);
|
|
56
|
+
const designsAsync = useAsync(async () => {
|
|
57
|
+
if (!isOpen || !isReady) return null;
|
|
58
|
+
return rpcGateway.canva.ListDesigns({
|
|
59
|
+
query: debouncedQuery
|
|
60
|
+
});
|
|
61
|
+
}, [rpcGateway, isOpen, isReady, debouncedQuery]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setDesigns(designsAsync.value?.designs ?? []);
|
|
64
|
+
setContinuation(designsAsync.value?.continuation ?? '');
|
|
65
|
+
}, [designsAsync.value]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (designsAsync.error) {
|
|
68
|
+
openToast({
|
|
69
|
+
title: errorMessage(designsAsync.error, lang({
|
|
70
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.ERROR'
|
|
71
|
+
})),
|
|
72
|
+
type: 'error'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}, [designsAsync.error, openToast, lang]);
|
|
76
|
+
useEffect(() => () => {
|
|
77
|
+
runRef.current += 1;
|
|
78
|
+
}, []);
|
|
79
|
+
const loadMore = async () => {
|
|
80
|
+
if (isLoadingMore) return;
|
|
81
|
+
const queryAtStart = debouncedQuery;
|
|
82
|
+
setIsLoadingMore(true);
|
|
83
|
+
try {
|
|
84
|
+
const page = await rpcGateway.canva.ListDesigns({
|
|
85
|
+
query: queryAtStart,
|
|
86
|
+
continuation
|
|
87
|
+
});
|
|
88
|
+
if (queryRef.current !== queryAtStart) return;
|
|
89
|
+
setDesigns(prev => {
|
|
90
|
+
const seen = new Set(prev.map(design => design.id));
|
|
91
|
+
return [...prev, ...(page.designs ?? []).filter(design => !seen.has(design.id))];
|
|
92
|
+
});
|
|
93
|
+
setContinuation(page.continuation ?? '');
|
|
94
|
+
} catch (error) {
|
|
95
|
+
openToast({
|
|
96
|
+
title: errorMessage(error, lang({
|
|
97
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.ERROR'
|
|
98
|
+
})),
|
|
99
|
+
type: 'error'
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoadingMore(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const handleConnect = async () => {
|
|
106
|
+
setIsConnecting(true);
|
|
107
|
+
try {
|
|
108
|
+
await connectCanva(rpcGateway);
|
|
109
|
+
onStatusChange();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (!(error instanceof ConsentClosedError)) {
|
|
112
|
+
const title = error instanceof PopupBlockedError ? lang({
|
|
113
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.POPUP_BLOCKED'
|
|
114
|
+
}) : lang({
|
|
115
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.CONNECT_FAILED'
|
|
116
|
+
});
|
|
117
|
+
if (error instanceof UnsafeReturnPageError) {
|
|
118
|
+
console.error('Canva connect refused: /robots.txt on this origin is not served as plain text');
|
|
119
|
+
}
|
|
120
|
+
openToast({
|
|
121
|
+
title,
|
|
122
|
+
type: 'error'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
setIsConnecting(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const handleDisconnect = async () => {
|
|
130
|
+
try {
|
|
131
|
+
await disconnectCanva(rpcGateway);
|
|
132
|
+
onStatusChange();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
openToast({
|
|
135
|
+
title: errorMessage(error, lang({
|
|
136
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.ERROR'
|
|
137
|
+
})),
|
|
138
|
+
type: 'error'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const selectDesign = design => {
|
|
143
|
+
setSelected(design);
|
|
144
|
+
setPages([1]);
|
|
145
|
+
};
|
|
146
|
+
const togglePage = page => {
|
|
147
|
+
setPages(prev => togglePages(prev, page, MAX_PAGES));
|
|
148
|
+
};
|
|
149
|
+
const handleImport = async () => {
|
|
150
|
+
if (!selected) return;
|
|
151
|
+
runRef.current += 1;
|
|
152
|
+
const run = runRef.current;
|
|
153
|
+
setProgress(lang({
|
|
154
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.EXPORTING'
|
|
155
|
+
}));
|
|
156
|
+
try {
|
|
157
|
+
const {
|
|
158
|
+
importId
|
|
159
|
+
} = await rpcGateway.canva.StartImport({
|
|
160
|
+
application,
|
|
161
|
+
designId: selected.id,
|
|
162
|
+
pages,
|
|
163
|
+
format
|
|
164
|
+
});
|
|
165
|
+
const startedAt = Date.now();
|
|
166
|
+
for (;;) {
|
|
167
|
+
await sleep(POLL_INTERVAL_MS);
|
|
168
|
+
if (runRef.current !== run) return;
|
|
169
|
+
if (Date.now() - startedAt > IMPORT_TIMEOUT_MS) {
|
|
170
|
+
throw new Error(lang({
|
|
171
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.ERROR'
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
const state = await rpcGateway.canva.GetImport({
|
|
175
|
+
importId
|
|
176
|
+
});
|
|
177
|
+
if (runRef.current !== run) return;
|
|
178
|
+
if (state.state === 'CANVA_IMPORT_STATE_FAILED') {
|
|
179
|
+
throw new Error(state.error);
|
|
180
|
+
}
|
|
181
|
+
if (state.state === 'CANVA_IMPORT_STATE_DONE') {
|
|
182
|
+
openToast({
|
|
183
|
+
title: lang({
|
|
184
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.SUCCESS'
|
|
185
|
+
}),
|
|
186
|
+
type: 'success'
|
|
187
|
+
});
|
|
188
|
+
onImported(state.files ?? []);
|
|
189
|
+
if (isOpenRef.current) onClose();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (state.state === 'CANVA_IMPORT_STATE_UPLOADING') {
|
|
193
|
+
setProgress(lang({
|
|
194
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.IMPORTING',
|
|
195
|
+
done: state.pagesDone,
|
|
196
|
+
total: state.pagesTotal
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (runRef.current === run) {
|
|
202
|
+
openToast({
|
|
203
|
+
title: errorMessage(error, lang({
|
|
204
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.ERROR'
|
|
205
|
+
})),
|
|
206
|
+
type: 'error'
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
if (runRef.current === run) setProgress(null);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const pageCount = selected ? Math.max(selected.pageCount, 1) : 0;
|
|
214
|
+
return _jsxs(Drawer, {
|
|
215
|
+
size: "medium",
|
|
216
|
+
isOpen: isOpen,
|
|
217
|
+
onClose: onClose,
|
|
218
|
+
children: [_jsx(DrawerHeader, {
|
|
219
|
+
children: _jsxs(DrawerTitle, {
|
|
220
|
+
children: [_jsx("span", {
|
|
221
|
+
children: lang({
|
|
222
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.TITLE'
|
|
223
|
+
})
|
|
224
|
+
}), _jsx(Button, {
|
|
225
|
+
view: "ghost",
|
|
226
|
+
color: "secondary",
|
|
227
|
+
size: "compact",
|
|
228
|
+
icon: _jsx(CloseIcon, {
|
|
229
|
+
size: "small"
|
|
230
|
+
}),
|
|
231
|
+
"aria-label": lang({
|
|
232
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.CLOSE'
|
|
233
|
+
}),
|
|
234
|
+
onClick: onClose
|
|
235
|
+
})]
|
|
236
|
+
})
|
|
237
|
+
}), _jsxs(DrawerBody, {
|
|
238
|
+
children: [!isReady && _jsxs(Vertical, {
|
|
239
|
+
"$gap": 12,
|
|
240
|
+
children: [_jsx(Text, {
|
|
241
|
+
children: status.connected ? lang({
|
|
242
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.RECONNECT_HINT'
|
|
243
|
+
}) : lang({
|
|
244
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.CONNECT_HINT'
|
|
245
|
+
})
|
|
246
|
+
}), _jsx("div", {
|
|
247
|
+
children: _jsx(Button, {
|
|
248
|
+
color: "primary",
|
|
249
|
+
isLoading: isConnecting,
|
|
250
|
+
isDisabled: isConnecting,
|
|
251
|
+
onClick: () => void handleConnect(),
|
|
252
|
+
children: lang({
|
|
253
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.CONNECT'
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
})]
|
|
257
|
+
}), isReady && _jsxs(Vertical, {
|
|
258
|
+
"$gap": 16,
|
|
259
|
+
children: [_jsxs(FlexContainer, {
|
|
260
|
+
"$alignItems": "center",
|
|
261
|
+
"$gap": 8,
|
|
262
|
+
children: [_jsx(Text, {
|
|
263
|
+
children: lang({
|
|
264
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.CONNECTED_AS',
|
|
265
|
+
name: status.identity
|
|
266
|
+
})
|
|
267
|
+
}), _jsx(Button, {
|
|
268
|
+
view: "ghost",
|
|
269
|
+
color: "secondary",
|
|
270
|
+
size: "compact",
|
|
271
|
+
onClick: () => void handleDisconnect(),
|
|
272
|
+
children: lang({
|
|
273
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.DISCONNECT'
|
|
274
|
+
})
|
|
275
|
+
})]
|
|
276
|
+
}), _jsx(NativeInput, {
|
|
277
|
+
value: query,
|
|
278
|
+
placeholder: lang({
|
|
279
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.SEARCH'
|
|
280
|
+
}),
|
|
281
|
+
"aria-label": lang({
|
|
282
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.SEARCH'
|
|
283
|
+
}),
|
|
284
|
+
onChange: event => setQuery(event.target.value)
|
|
285
|
+
}), !designsAsync.loading && designs.length === 0 && _jsx(EmptyState, {
|
|
286
|
+
children: lang({
|
|
287
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.EMPTY'
|
|
288
|
+
})
|
|
289
|
+
}), _jsx(Grid, {
|
|
290
|
+
children: designs.map(design => _jsxs(DesignTile, {
|
|
291
|
+
type: "button",
|
|
292
|
+
"$selected": selected?.id === design.id,
|
|
293
|
+
disabled: progress !== null,
|
|
294
|
+
onClick: () => selectDesign(design),
|
|
295
|
+
children: [_jsx(TileImage, {
|
|
296
|
+
src: design.thumbnailUrl,
|
|
297
|
+
alt: "",
|
|
298
|
+
loading: "lazy"
|
|
299
|
+
}), _jsxs(DesignCaption, {
|
|
300
|
+
children: [_jsx("span", {
|
|
301
|
+
children: design.title || lang({
|
|
302
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.UNTITLED'
|
|
303
|
+
})
|
|
304
|
+
}), _jsx("span", {
|
|
305
|
+
children: lang({
|
|
306
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.PAGES',
|
|
307
|
+
count: Math.max(design.pageCount, 1)
|
|
308
|
+
})
|
|
309
|
+
})]
|
|
310
|
+
})]
|
|
311
|
+
}, design.id))
|
|
312
|
+
}), continuation && _jsx("div", {
|
|
313
|
+
children: _jsx(Button, {
|
|
314
|
+
view: "ghost",
|
|
315
|
+
color: "secondary",
|
|
316
|
+
isLoading: isLoadingMore,
|
|
317
|
+
isDisabled: isLoadingMore,
|
|
318
|
+
onClick: () => void loadMore(),
|
|
319
|
+
children: lang({
|
|
320
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.LOAD_MORE'
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
}), selected && _jsxs(Vertical, {
|
|
324
|
+
"$gap": 12,
|
|
325
|
+
children: [pageCount > 1 && _jsxs("div", {
|
|
326
|
+
children: [_jsx(FieldLabel, {
|
|
327
|
+
children: lang({
|
|
328
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.PAGES_LABEL',
|
|
329
|
+
max: MAX_PAGES
|
|
330
|
+
})
|
|
331
|
+
}), _jsx(Chips, {
|
|
332
|
+
children: Array.from({
|
|
333
|
+
length: pageCount
|
|
334
|
+
}, (_, index) => index + 1).map(page => _jsx(Chip, {
|
|
335
|
+
"$selected": pages.includes(page),
|
|
336
|
+
"aria-pressed": pages.includes(page),
|
|
337
|
+
onClick: () => togglePage(page),
|
|
338
|
+
children: page
|
|
339
|
+
}, page))
|
|
340
|
+
})]
|
|
341
|
+
}), _jsxs("div", {
|
|
342
|
+
children: [_jsx(FieldLabel, {
|
|
343
|
+
children: lang({
|
|
344
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.FORMAT_LABEL'
|
|
345
|
+
})
|
|
346
|
+
}), _jsxs(Segment, {
|
|
347
|
+
role: "radiogroup",
|
|
348
|
+
"aria-label": lang({
|
|
349
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.FORMAT_LABEL'
|
|
350
|
+
}),
|
|
351
|
+
children: [_jsx(SegmentItem, {
|
|
352
|
+
type: "button",
|
|
353
|
+
role: "radio",
|
|
354
|
+
"aria-checked": format === 'CANVA_IMPORT_FORMAT_PNG',
|
|
355
|
+
"$active": format === 'CANVA_IMPORT_FORMAT_PNG',
|
|
356
|
+
onClick: () => setFormat('CANVA_IMPORT_FORMAT_PNG'),
|
|
357
|
+
children: "PNG"
|
|
358
|
+
}), _jsx(SegmentItem, {
|
|
359
|
+
type: "button",
|
|
360
|
+
role: "radio",
|
|
361
|
+
"aria-checked": format === 'CANVA_IMPORT_FORMAT_JPG',
|
|
362
|
+
"$active": format === 'CANVA_IMPORT_FORMAT_JPG',
|
|
363
|
+
onClick: () => setFormat('CANVA_IMPORT_FORMAT_JPG'),
|
|
364
|
+
children: "JPG"
|
|
365
|
+
})]
|
|
366
|
+
})]
|
|
367
|
+
}), _jsxs(FlexContainer, {
|
|
368
|
+
"$alignItems": "center",
|
|
369
|
+
"$gap": 12,
|
|
370
|
+
children: [_jsx(Button, {
|
|
371
|
+
color: "primary",
|
|
372
|
+
isLoading: progress !== null,
|
|
373
|
+
isDisabled: progress !== null,
|
|
374
|
+
onClick: () => void handleImport(),
|
|
375
|
+
children: lang({
|
|
376
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.IMPORT'
|
|
377
|
+
})
|
|
378
|
+
}), progress && _jsx(Text, {
|
|
379
|
+
children: progress
|
|
380
|
+
})]
|
|
381
|
+
})]
|
|
382
|
+
})]
|
|
383
|
+
})]
|
|
384
|
+
})]
|
|
385
|
+
});
|
|
386
|
+
};
|
|
@@ -5,11 +5,12 @@ import { FlexContainer, useAsync } from '@pushwoosh/kit-helpers';
|
|
|
5
5
|
import { CheckIcon, EditIcon, TrashIcon, UploadIcon } from '@pushwoosh/kit-icons';
|
|
6
6
|
import { Text } from '@pushwoosh/kit-typography';
|
|
7
7
|
import { useSPAContext } from '@pushwoosh/spa-context';
|
|
8
|
+
import { CanvaImportDrawer } from './CanvaImportDrawer';
|
|
8
9
|
import { CreateCropDrawer } from './CreateCropDrawer';
|
|
9
10
|
import { useDeleteMediaFile, useDeleteMediaFiles } from './hooks';
|
|
10
|
-
import { formatBytes, getCropCaption, getThumbUrl } from './mediaFile.utils';
|
|
11
|
+
import { formatBytes, getCropCaption, getSourceLabel, getThumbUrl, hasVersions } from './mediaFile.utils';
|
|
11
12
|
import { Lang, useLang } from './polyglot';
|
|
12
|
-
import {
|
|
13
|
+
import { CountBadge, Dropzone, DropzoneRow, EmptyState, Grid, HiddenInput, SelectionBar, SelectionCount, SourceBadge, Tile, TileActions, TileBadges, TileCheckbox, TileImage, UsageLine, WarningBadge } from './styles';
|
|
13
14
|
const UPLOAD_PATH = '/api/media/upload';
|
|
14
15
|
const ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
|
15
16
|
const isAllowedImage = file => {
|
|
@@ -80,6 +81,10 @@ export const MediaStorage = ({
|
|
|
80
81
|
const usageAsync = useAsync(() => rpcGateway.mediaFiles.GetUsage({
|
|
81
82
|
application
|
|
82
83
|
}), [rpcGateway, application, reloadToken, usageToken]);
|
|
84
|
+
const [canvaToken, setCanvaToken] = useState(0);
|
|
85
|
+
const [isCanvaOpen, setIsCanvaOpen] = useState(false);
|
|
86
|
+
const canvaStatusAsync = useAsync(() => rpcGateway.canva.GetStatus({}), [rpcGateway, canvaToken]);
|
|
87
|
+
const canvaStatus = canvaStatusAsync.value;
|
|
83
88
|
const {
|
|
84
89
|
loading
|
|
85
90
|
} = listAsync;
|
|
@@ -242,28 +247,36 @@ export const MediaStorage = ({
|
|
|
242
247
|
const totalPages = Math.ceil(total / perPage);
|
|
243
248
|
const showPagination = !loading && files.length > 0 && totalPages > 1;
|
|
244
249
|
return _jsxs(_Fragment, {
|
|
245
|
-
children: [_jsxs(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
event
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
event
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
250
|
+
children: [_jsxs(DropzoneRow, {
|
|
251
|
+
children: [_jsxs(Dropzone, {
|
|
252
|
+
type: "button",
|
|
253
|
+
"$dragOver": isDragOver,
|
|
254
|
+
disabled: isUploading,
|
|
255
|
+
onClick: () => uploadInputRef.current?.click(),
|
|
256
|
+
onDragOver: event => {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
setIsDragOver(true);
|
|
259
|
+
},
|
|
260
|
+
onDragLeave: () => setIsDragOver(false),
|
|
261
|
+
onDrop: event => {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
setIsDragOver(false);
|
|
264
|
+
void uploadFiles(Array.from(event.dataTransfer.files));
|
|
265
|
+
},
|
|
266
|
+
children: [_jsx(UploadIcon, {
|
|
267
|
+
size: "medium",
|
|
268
|
+
view: "lined"
|
|
269
|
+
}), isUploading ? lang({
|
|
270
|
+
message: 'SMART.MEDIA_STORAGE.UPLOADING'
|
|
271
|
+
}) : lang({
|
|
272
|
+
message: 'SMART.MEDIA_STORAGE.DROPZONE_HINT'
|
|
273
|
+
})]
|
|
274
|
+
}), canvaStatus?.available && _jsx(Button, {
|
|
275
|
+
color: "secondary",
|
|
276
|
+
onClick: () => setIsCanvaOpen(true),
|
|
277
|
+
children: lang({
|
|
278
|
+
message: 'SMART.MEDIA_STORAGE.CANVA.BUTTON'
|
|
279
|
+
})
|
|
267
280
|
})]
|
|
268
281
|
}), _jsx(HiddenInput, {
|
|
269
282
|
ref: uploadInputRef,
|
|
@@ -314,6 +327,7 @@ export const MediaStorage = ({
|
|
|
314
327
|
children: files.map(file => {
|
|
315
328
|
const crops = cropsByUuid[file.uuid] ?? [];
|
|
316
329
|
const warning = fileWarning?.(file);
|
|
330
|
+
const sourceLabel = getSourceLabel(file);
|
|
317
331
|
return _jsxs(Tile, {
|
|
318
332
|
"$selected": selectedUuids.includes(file.uuid),
|
|
319
333
|
children: [_jsx(TileImage, {
|
|
@@ -328,11 +342,20 @@ export const MediaStorage = ({
|
|
|
328
342
|
isDisabled: isBulkDeleting,
|
|
329
343
|
onChange: () => toggleSelected(file.uuid)
|
|
330
344
|
})
|
|
331
|
-
}),
|
|
332
|
-
children:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
345
|
+
}), _jsxs(TileBadges, {
|
|
346
|
+
children: [sourceLabel && _jsx(SourceBadge, {
|
|
347
|
+
children: sourceLabel
|
|
348
|
+
}), hasVersions(file) && _jsx(CountBadge, {
|
|
349
|
+
children: lang({
|
|
350
|
+
message: 'SMART.MEDIA_STORAGE.VERSIONS_BADGE',
|
|
351
|
+
count: file.versionsCount
|
|
352
|
+
})
|
|
353
|
+
}), crops.length > 0 && _jsx(CountBadge, {
|
|
354
|
+
children: lang({
|
|
355
|
+
message: 'SMART.MEDIA_STORAGE.CROPS_BADGE',
|
|
356
|
+
count: crops.length
|
|
357
|
+
})
|
|
358
|
+
})]
|
|
336
359
|
}), warning && _jsx(WarningBadge, {
|
|
337
360
|
title: warning,
|
|
338
361
|
children: warning
|
|
@@ -428,6 +451,18 @@ export const MediaStorage = ({
|
|
|
428
451
|
size: formatBytes(usageAsync.value.totalSize)
|
|
429
452
|
})
|
|
430
453
|
})]
|
|
454
|
+
}), canvaStatus?.available && _jsx(CanvaImportDrawer, {
|
|
455
|
+
application: application,
|
|
456
|
+
isOpen: isCanvaOpen,
|
|
457
|
+
status: canvaStatus,
|
|
458
|
+
onStatusChange: () => setCanvaToken(token => token + 1),
|
|
459
|
+
onClose: () => setIsCanvaOpen(false),
|
|
460
|
+
onImported: () => {
|
|
461
|
+
if (pageRef.current !== 0) {
|
|
462
|
+
onPageChange(0);
|
|
463
|
+
}
|
|
464
|
+
setReloadToken(token => token + 1);
|
|
465
|
+
}
|
|
431
466
|
}), _jsx(CreateCropDrawer, {
|
|
432
467
|
application: application,
|
|
433
468
|
original: createCropFor,
|
|
@@ -442,7 +477,7 @@ export const MediaStorage = ({
|
|
|
442
477
|
message: 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.TITLE'
|
|
443
478
|
}),
|
|
444
479
|
description: lang({
|
|
445
|
-
message: 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION'
|
|
480
|
+
message: pendingDelete && hasVersions(pendingDelete) ? 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION_VERSION' : 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION'
|
|
446
481
|
}),
|
|
447
482
|
confirmButtonText: lang({
|
|
448
483
|
message: 'SMART.MEDIA_STORAGE.DELETE_CONFIRM.BUTTON'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { API as RpcGatewayAPI } from '@pushwoosh/rpc-v2-http-api-data';
|
|
2
|
+
/** Error raised when the browser refused to open the Canva consent window. */
|
|
3
|
+
export declare class PopupBlockedError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** Consent result read from the pop-up once it is back on our origin; null while it is still elsewhere. */
|
|
6
|
+
export declare const readConsentReturn: (location: Pick<Location, "origin" | "hash">, origin: string) => URLSearchParams | null;
|
|
7
|
+
/** Error raised when the user closed the consent pop-up without finishing. */
|
|
8
|
+
export declare class ConsentClosedError extends Error {
|
|
9
|
+
}
|
|
10
|
+
/** Error raised when the return page is not plain static text: the code must not land in an app. */
|
|
11
|
+
export declare class UnsafeReturnPageError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** True when the response is served as plain text, i.e. no app bundle runs on that page. */
|
|
14
|
+
export declare const isStaticText: (contentType: string | null) => boolean;
|
|
15
|
+
/** Connects the user's Canva in a pop-up that returns to a static text file on this origin. */
|
|
16
|
+
export declare const connectCanva: (rpcGateway: RpcGatewayAPI) => Promise<void>;
|
|
17
|
+
/** Revokes and forgets the signed-in user's Canva connection. */
|
|
18
|
+
export declare const disconnectCanva: (rpcGateway: RpcGatewayAPI) => Promise<unknown>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const PROVIDER = 'canva';
|
|
2
|
+
const POLL_INTERVAL_MS = 500;
|
|
3
|
+
const CONNECT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
/** Error raised when the browser refused to open the Canva consent window. */
|
|
5
|
+
export class PopupBlockedError extends Error {}
|
|
6
|
+
/** Consent result read from the pop-up once it is back on our origin; null while it is still elsewhere. */
|
|
7
|
+
export const readConsentReturn = (location, origin) => {
|
|
8
|
+
if (location.origin !== origin) return null;
|
|
9
|
+
const params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
|
10
|
+
return params.get('oauth_provider') === PROVIDER ? params : null;
|
|
11
|
+
};
|
|
12
|
+
/** Error raised when the user closed the consent pop-up without finishing. */
|
|
13
|
+
export class ConsentClosedError extends Error {}
|
|
14
|
+
const waitForReturn = (popup, origin) => new Promise((resolve, reject) => {
|
|
15
|
+
const startedAt = Date.now();
|
|
16
|
+
const timer = window.setInterval(() => {
|
|
17
|
+
if (popup.closed) {
|
|
18
|
+
window.clearInterval(timer);
|
|
19
|
+
reject(new ConsentClosedError());
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (Date.now() - startedAt > CONNECT_TIMEOUT_MS) {
|
|
23
|
+
window.clearInterval(timer);
|
|
24
|
+
popup.close();
|
|
25
|
+
reject(new Error('timeout'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let params = null;
|
|
29
|
+
try {
|
|
30
|
+
params = readConsentReturn(popup.location, origin);
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!params) return;
|
|
35
|
+
window.clearInterval(timer);
|
|
36
|
+
popup.close();
|
|
37
|
+
resolve(params);
|
|
38
|
+
}, POLL_INTERVAL_MS);
|
|
39
|
+
});
|
|
40
|
+
/** Error raised when the return page is not plain static text: the code must not land in an app. */
|
|
41
|
+
export class UnsafeReturnPageError extends Error {}
|
|
42
|
+
/** True when the response is served as plain text, i.e. no app bundle runs on that page. */
|
|
43
|
+
export const isStaticText = contentType => (contentType ?? '').toLowerCase().startsWith('text/plain');
|
|
44
|
+
const ensureStaticReturn = async url => {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method: 'HEAD',
|
|
47
|
+
cache: 'no-store',
|
|
48
|
+
credentials: 'omit'
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok || !isStaticText(response.headers.get('content-type'))) throw new UnsafeReturnPageError();
|
|
51
|
+
};
|
|
52
|
+
/** Connects the user's Canva in a pop-up that returns to a static text file on this origin. */
|
|
53
|
+
export const connectCanva = async rpcGateway => {
|
|
54
|
+
const {
|
|
55
|
+
origin
|
|
56
|
+
} = window.location;
|
|
57
|
+
const returnTo = `${origin}/robots.txt`;
|
|
58
|
+
const popup = window.open('', 'pushwoosh-canva-connect', 'width=640,height=760');
|
|
59
|
+
if (!popup) throw new PopupBlockedError();
|
|
60
|
+
try {
|
|
61
|
+
await ensureStaticReturn(returnTo);
|
|
62
|
+
const {
|
|
63
|
+
authUrl
|
|
64
|
+
} = await rpcGateway.integrations.GetOAuthAuthURL({
|
|
65
|
+
provider: PROVIDER,
|
|
66
|
+
returnTo
|
|
67
|
+
});
|
|
68
|
+
popup.location.href = authUrl;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
popup.close();
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const params = await waitForReturn(popup, origin);
|
|
74
|
+
if (params.get('oauth_status') !== 'pending') {
|
|
75
|
+
throw new Error(params.get('oauth_error') || 'Canva consent was not given');
|
|
76
|
+
}
|
|
77
|
+
await rpcGateway.integrations.CompleteOAuthAuthorization({
|
|
78
|
+
provider: PROVIDER,
|
|
79
|
+
state: params.get('oauth_state') ?? '',
|
|
80
|
+
code: params.get('oauth_code') ?? ''
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
/** Revokes and forgets the signed-in user's Canva connection. */
|
|
84
|
+
export const disconnectCanva = rpcGateway => rpcGateway.integrations.DeleteOAuthIntegration({
|
|
85
|
+
provider: PROVIDER
|
|
86
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Next page selection after a click: at least one page stays selected, at most `max`, in order. */
|
|
2
|
+
export const togglePages = (selected, page, max) => {
|
|
3
|
+
if (selected.includes(page)) {
|
|
4
|
+
return selected.length > 1 ? selected.filter(item => item !== page) : selected;
|
|
5
|
+
}
|
|
6
|
+
return selected.length < max ? [...selected, page].sort((a, b) => a - b) : selected;
|
|
7
|
+
};
|
package/MediaStorage/index.d.ts
CHANGED
package/MediaStorage/index.js
CHANGED