@pushwoosh/smart-components-kit 1.3.15 → 1.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/MediaStorage/MediaStorage.js +93 -6
  2. package/MediaStorage/hooks.d.ts +3 -6
  3. package/MediaStorage/hooks.js +51 -6
  4. package/MediaStorage/polyglot/messages_en.js +9 -0
  5. package/MediaStorage/polyglot/messages_ru.js +9 -0
  6. package/MediaStorage/polyglot/types.d.ts +17 -0
  7. package/MediaStorage/styled.d.ts +6 -1
  8. package/MediaStorage/styled.js +35 -19
  9. package/ProductUpdates/ProductUpdateCard.d.ts +2 -0
  10. package/ProductUpdates/ProductUpdateCard.js +79 -0
  11. package/ProductUpdates/ProductUpdates.d.ts +2 -0
  12. package/ProductUpdates/ProductUpdates.js +57 -0
  13. package/ProductUpdates/ProductUpdatesModal.d.ts +2 -0
  14. package/ProductUpdates/ProductUpdatesModal.js +59 -0
  15. package/ProductUpdates/assets/fallback.svg +1 -0
  16. package/ProductUpdates/constants.d.ts +7 -0
  17. package/ProductUpdates/constants.js +6 -0
  18. package/ProductUpdates/helpers.d.ts +4 -0
  19. package/ProductUpdates/helpers.js +33 -0
  20. package/ProductUpdates/hooks/useIsAllowProductUpdates.d.ts +1 -0
  21. package/ProductUpdates/hooks/useIsAllowProductUpdates.js +9 -0
  22. package/ProductUpdates/hooks/useProductUpdates.d.ts +10 -0
  23. package/ProductUpdates/hooks/useProductUpdates.js +57 -0
  24. package/ProductUpdates/index.d.ts +5 -0
  25. package/ProductUpdates/index.js +4 -0
  26. package/ProductUpdates/polyglot/index.d.ts +2 -0
  27. package/ProductUpdates/polyglot/index.js +10 -0
  28. package/ProductUpdates/polyglot/messages_en.d.ts +3 -0
  29. package/ProductUpdates/polyglot/messages_en.js +6 -0
  30. package/ProductUpdates/polyglot/messages_ru.d.ts +3 -0
  31. package/ProductUpdates/polyglot/messages_ru.js +6 -0
  32. package/ProductUpdates/polyglot/types.d.ts +9 -0
  33. package/ProductUpdates/polyglot/types.js +1 -0
  34. package/ProductUpdates/styles.d.ts +1 -0
  35. package/ProductUpdates/styles.js +8 -0
  36. package/ProductUpdates/types.d.ts +16 -0
  37. package/ProductUpdates/types.js +1 -0
  38. package/index.d.ts +1 -0
  39. package/index.js +1 -0
  40. package/package.json +4 -4
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import { Button, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, useToast } from '@pushwoosh/dumb-components';
3
+ import { Button, Checkbox, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, useToast } from '@pushwoosh/dumb-components';
4
4
  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
8
  import { CreateCropDrawer } from './CreateCropDrawer';
9
- import { useDeleteMediaFile } from './hooks';
9
+ import { useDeleteMediaFile, useDeleteMediaFiles } from './hooks';
10
10
  import { formatBytes, getCropCaption, getThumbUrl } from './mediaFile.utils';
11
- import { useLang } from './polyglot';
12
- import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, UsageLine, WarningBadge } from './styled';
11
+ import { Lang, useLang } from './polyglot';
12
+ import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, SelectionBar, SelectionCount, Tile, TileActions, TileCheckbox, TileImage, UsageLine, WarningBadge } from './styled';
13
13
  const UPLOAD_PATH = '/api/media/upload';
14
14
  const ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
15
15
  const isAllowedImage = file => {
@@ -42,6 +42,7 @@ export const MediaStorage = ({
42
42
  openToast
43
43
  } = useToast();
44
44
  const deleteMediaFile = useDeleteMediaFile(application);
45
+ const deleteMediaFiles = useDeleteMediaFiles(application);
45
46
  const uploadInputRef = useRef(null);
46
47
  // `uploadFiles` resolves asynchronously; read the latest page from the ref so
47
48
  // the post-upload decision isn't made on a value captured at click time.
@@ -58,6 +59,9 @@ export const MediaStorage = ({
58
59
  const [isDragOver, setIsDragOver] = useState(false);
59
60
  const [createCropFor, setCreateCropFor] = useState(null);
60
61
  const [pendingDelete, setPendingDelete] = useState(null);
62
+ const [selectedUuids, setSelectedUuids] = useState([]);
63
+ const [isBulkDeleteOpen, setIsBulkDeleteOpen] = useState(false);
64
+ const [isBulkDeleting, setIsBulkDeleting] = useState(false);
61
65
  // Format filtering belongs to the query, not the response: `total` and the page count come
62
66
  // from the filtered set, so a client-side filter would paginate over files it then hides.
63
67
  const listAsync = useAsync(async () => {
@@ -102,6 +106,7 @@ export const MediaStorage = ({
102
106
  const originals = listAsync.value?.files;
103
107
  if (!originals) return undefined;
104
108
  setCropsByUuid({});
109
+ setSelectedUuids([]);
105
110
  let cancelled = false;
106
111
  originals.forEach(file => {
107
112
  void (async () => {
@@ -209,6 +214,31 @@ export const MediaStorage = ({
209
214
  }
210
215
  setReloadToken(token => token + 1);
211
216
  };
217
+ const handleBulkDeleteConfirmed = async () => {
218
+ const uuids = selectedUuids;
219
+ setIsBulkDeleteOpen(false);
220
+ if (uuids.length === 0 || isBulkDeleting) return;
221
+ // The batch outlives the page it started on, so the step-back decision uses
222
+ // the page and count captured here and is skipped if the page moved since.
223
+ const pageAtStart = pageRef.current;
224
+ const countAtStart = filesCountRef.current;
225
+ setIsBulkDeleting(true);
226
+ try {
227
+ const deleted = await deleteMediaFiles(uuids);
228
+ if (deleted === 0) return;
229
+ setSelectedUuids([]);
230
+ if (deleted >= countAtStart && pageAtStart > 0 && pageRef.current === pageAtStart) {
231
+ onPageChange(pageAtStart - 1);
232
+ }
233
+ setReloadToken(token => token + 1);
234
+ } finally {
235
+ setIsBulkDeleting(false);
236
+ }
237
+ };
238
+ const toggleSelected = uuid => {
239
+ setSelectedUuids(prev => prev.includes(uuid) ? prev.filter(item => item !== uuid) : [...prev, uuid]);
240
+ };
241
+ const isAllSelected = files.length > 0 && selectedUuids.length === files.length;
212
242
  const totalPages = Math.ceil(total / perPage);
213
243
  const showPagination = !loading && files.length > 0 && totalPages > 1;
214
244
  return _jsxs(_Fragment, {
@@ -249,14 +279,55 @@ export const MediaStorage = ({
249
279
  children: lang({
250
280
  message: 'SMART.MEDIA_STORAGE.EMPTY'
251
281
  })
282
+ }), !loading && files.length > 0 && _jsxs(SelectionBar, {
283
+ children: [_jsx(Checkbox, {
284
+ label: lang({
285
+ message: 'SMART.MEDIA_STORAGE.SELECT_ALL'
286
+ }),
287
+ isChecked: isAllSelected,
288
+ isDisabled: isBulkDeleting,
289
+ isIndeterminate: selectedUuids.length > 0 && !isAllSelected,
290
+ onChange: () => setSelectedUuids(isAllSelected ? [] : files.map(file => file.uuid))
291
+ }), selectedUuids.length > 0 && _jsxs(_Fragment, {
292
+ children: [_jsx(SelectionCount, {
293
+ children: _jsx(Lang, {
294
+ message: "SMART.MEDIA_STORAGE.SELECTED_COUNT",
295
+ count: selectedUuids.length
296
+ })
297
+ }), _jsx(Button, {
298
+ color: "danger",
299
+ size: "compact",
300
+ view: "ghost",
301
+ icon: _jsx(TrashIcon, {
302
+ size: "small",
303
+ view: "lined"
304
+ }),
305
+ isLoading: isBulkDeleting,
306
+ isDisabled: isBulkDeleting,
307
+ onClick: () => setIsBulkDeleteOpen(true),
308
+ children: _jsx(Lang, {
309
+ message: "SMART.MEDIA_STORAGE.BULK_DELETE_BUTTON"
310
+ })
311
+ })]
312
+ })]
252
313
  }), !loading && files.length > 0 && _jsx(Grid, {
253
314
  children: files.map(file => {
254
315
  const crops = cropsByUuid[file.uuid] ?? [];
255
316
  const warning = fileWarning?.(file);
256
317
  return _jsxs(Tile, {
318
+ "$selected": selectedUuids.includes(file.uuid),
257
319
  children: [_jsx(TileImage, {
258
320
  src: getThumbUrl(file),
259
321
  alt: file.filename
322
+ }), _jsx(TileCheckbox, {
323
+ title: lang({
324
+ message: 'SMART.MEDIA_STORAGE.SELECT_FILE_HINT'
325
+ }),
326
+ children: _jsx(Checkbox, {
327
+ isChecked: selectedUuids.includes(file.uuid),
328
+ isDisabled: isBulkDeleting,
329
+ onChange: () => toggleSelected(file.uuid)
330
+ })
260
331
  }), crops.length > 0 && _jsx(CropBadge, {
261
332
  children: lang({
262
333
  message: 'SMART.MEDIA_STORAGE.CROPS_BADGE',
@@ -315,6 +386,7 @@ export const MediaStorage = ({
315
386
  "aria-label": lang({
316
387
  message: 'SMART.MEDIA_STORAGE.DELETE_HINT'
317
388
  }),
389
+ isDisabled: isBulkDeleting,
318
390
  onClick: () => setPendingDelete(file)
319
391
  })]
320
392
  })]
@@ -332,7 +404,7 @@ export const MediaStorage = ({
332
404
  view: "ghost",
333
405
  color: "secondary",
334
406
  size: "field",
335
- isDisabled: page <= 0,
407
+ isDisabled: page <= 0 || isBulkDeleting,
336
408
  onClick: () => onPageChange(page - 1),
337
409
  children: lang({
338
410
  message: 'SMART.MEDIA_STORAGE.PREV_BUTTON'
@@ -343,7 +415,7 @@ export const MediaStorage = ({
343
415
  view: "ghost",
344
416
  color: "secondary",
345
417
  size: "field",
346
- isDisabled: page + 1 >= totalPages,
418
+ isDisabled: page + 1 >= totalPages || isBulkDeleting,
347
419
  onClick: () => onPageChange(page + 1),
348
420
  children: lang({
349
421
  message: 'SMART.MEDIA_STORAGE.NEXT_BUTTON'
@@ -378,6 +450,21 @@ export const MediaStorage = ({
378
450
  confirmButtonColor: "danger",
379
451
  onAccept: () => void handleDeleteConfirmed(),
380
452
  onCancel: () => setPendingDelete(null)
453
+ }), _jsx(ModalConfirm, {
454
+ isOpen: isBulkDeleteOpen,
455
+ title: lang({
456
+ message: 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.TITLE',
457
+ count: selectedUuids.length
458
+ }),
459
+ description: lang({
460
+ message: 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.DESCRIPTION'
461
+ }),
462
+ confirmButtonText: lang({
463
+ message: 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.BUTTON'
464
+ }),
465
+ confirmButtonColor: "danger",
466
+ onAccept: () => void handleBulkDeleteConfirmed(),
467
+ onCancel: () => setIsBulkDeleteOpen(false)
381
468
  })]
382
469
  });
383
470
  };
@@ -1,7 +1,4 @@
1
- /**
2
- * Returns a function that deletes a media file (original or crop) and shows the
3
- * success/error toast. Resolves to `true` on success so the caller can run its
4
- * own post-delete routing (refetch, state update, pagination), `false` on
5
- * failure. Both `MediaStorage` and `CreateCropDrawer` share this.
6
- */
1
+ /** Deletes one media file with its own toast; resolves to `true` on success. */
7
2
  export declare function useDeleteMediaFile(application: string): (uuid: string) => Promise<boolean>;
3
+ /** Deletes media files in a bounded-concurrency batch; resolves to the number deleted. */
4
+ export declare function useDeleteMediaFiles(application: string): (uuids: string[]) => Promise<number>;
@@ -2,12 +2,7 @@ import { useToast } from '@pushwoosh/dumb-components';
2
2
  import { usePersistentFunction } from '@pushwoosh/kit-helpers';
3
3
  import { useSPAContext } from '@pushwoosh/spa-context';
4
4
  import { useLang } from './polyglot';
5
- /**
6
- * Returns a function that deletes a media file (original or crop) and shows the
7
- * success/error toast. Resolves to `true` on success so the caller can run its
8
- * own post-delete routing (refetch, state update, pagination), `false` on
9
- * failure. Both `MediaStorage` and `CreateCropDrawer` share this.
10
- */
5
+ /** Deletes one media file with its own toast; resolves to `true` on success. */
11
6
  export function useDeleteMediaFile(application) {
12
7
  const {
13
8
  rpcGateway
@@ -39,4 +34,54 @@ export function useDeleteMediaFile(application) {
39
34
  return false;
40
35
  }
41
36
  });
37
+ }
38
+ const DELETE_CONCURRENCY = 5;
39
+ /** Deletes media files in a bounded-concurrency batch; resolves to the number deleted. */
40
+ export function useDeleteMediaFiles(application) {
41
+ const {
42
+ rpcGateway
43
+ } = useSPAContext();
44
+ const {
45
+ openToast
46
+ } = useToast();
47
+ const lang = useLang();
48
+ return usePersistentFunction(async uuids => {
49
+ const queue = [...uuids];
50
+ let failed = 0;
51
+ const worker = async () => {
52
+ for (let uuid = queue.shift(); uuid !== undefined; uuid = queue.shift()) {
53
+ try {
54
+ await rpcGateway.mediaFiles.Delete({
55
+ application,
56
+ uuid
57
+ });
58
+ } catch {
59
+ failed += 1;
60
+ }
61
+ }
62
+ };
63
+ await Promise.all(Array.from({
64
+ length: Math.min(DELETE_CONCURRENCY, queue.length)
65
+ }, () => worker()));
66
+ const deleted = uuids.length - failed;
67
+ if (deleted > 0) {
68
+ openToast({
69
+ title: lang({
70
+ message: 'SMART.MEDIA_STORAGE.BULK_DELETE_SUCCESS',
71
+ count: deleted
72
+ }),
73
+ type: 'success'
74
+ });
75
+ }
76
+ if (failed > 0) {
77
+ openToast({
78
+ title: lang({
79
+ message: 'SMART.MEDIA_STORAGE.BULK_DELETE_ERROR',
80
+ count: failed
81
+ }),
82
+ type: 'error'
83
+ });
84
+ }
85
+ return deleted;
86
+ });
42
87
  }
@@ -17,6 +17,15 @@ export const messages = {
17
17
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Delete',
18
18
  'SMART.MEDIA_STORAGE.DELETE_SUCCESS': 'Deleted',
19
19
  'SMART.MEDIA_STORAGE.DELETE_ERROR': 'Failed to delete',
20
+ 'SMART.MEDIA_STORAGE.SELECT_ALL': 'Select all on page',
21
+ 'SMART.MEDIA_STORAGE.SELECT_FILE_HINT': 'Select file',
22
+ 'SMART.MEDIA_STORAGE.SELECTED_COUNT': '{{count}} selected',
23
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_BUTTON': 'Delete selected',
24
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_SUCCESS': '{{count}} files deleted',
25
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_ERROR': 'Failed to delete {{count}} files',
26
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.TITLE': 'Delete {{count}} files?',
27
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.DESCRIPTION': 'Deleting an original also removes all of its crops. This can’t be undone.',
28
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.BUTTON': 'Delete',
20
29
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.TITLE': 'Delete this file?',
21
30
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION': 'Deleting the original also removes all of its crops. This can’t be undone.',
22
31
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.BUTTON': 'Delete',
@@ -17,6 +17,15 @@ export const messages = {
17
17
  'SMART.MEDIA_STORAGE.DELETE_HINT': 'Удалить',
18
18
  'SMART.MEDIA_STORAGE.DELETE_SUCCESS': 'Удалено',
19
19
  'SMART.MEDIA_STORAGE.DELETE_ERROR': 'Не удалось удалить',
20
+ 'SMART.MEDIA_STORAGE.SELECT_ALL': 'Выбрать все на странице',
21
+ 'SMART.MEDIA_STORAGE.SELECT_FILE_HINT': 'Выбрать файл',
22
+ 'SMART.MEDIA_STORAGE.SELECTED_COUNT': 'Выбрано: {{count}}',
23
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_BUTTON': 'Удалить выбранные',
24
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_SUCCESS': 'Удалено файлов: {{count}}',
25
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_ERROR': 'Не удалось удалить файлов: {{count}}',
26
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.TITLE': 'Удалить {{count}} файлов?',
27
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.DESCRIPTION': 'Удаление оригинала также удалит все его кропы. Действие необратимо.',
28
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.BUTTON': 'Удалить',
20
29
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.TITLE': 'Удалить файл?',
21
30
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION': 'Удаление оригинала также удалит все его кропы. Действие необратимо.',
22
31
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.BUTTON': 'Удалить',
@@ -23,6 +23,23 @@ export type MediaStorageMessages = {
23
23
  'SMART.MEDIA_STORAGE.DELETE_HINT': NoParams;
24
24
  'SMART.MEDIA_STORAGE.DELETE_SUCCESS': NoParams;
25
25
  'SMART.MEDIA_STORAGE.DELETE_ERROR': NoParams;
26
+ 'SMART.MEDIA_STORAGE.SELECT_ALL': NoParams;
27
+ 'SMART.MEDIA_STORAGE.SELECT_FILE_HINT': NoParams;
28
+ 'SMART.MEDIA_STORAGE.SELECTED_COUNT': {
29
+ count: number;
30
+ };
31
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_BUTTON': NoParams;
32
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_SUCCESS': {
33
+ count: number;
34
+ };
35
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_ERROR': {
36
+ count: number;
37
+ };
38
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.TITLE': {
39
+ count: number;
40
+ };
41
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.DESCRIPTION': NoParams;
42
+ 'SMART.MEDIA_STORAGE.BULK_DELETE_CONFIRM.BUTTON': NoParams;
26
43
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.TITLE': NoParams;
27
44
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.DESCRIPTION': NoParams;
28
45
  'SMART.MEDIA_STORAGE.DELETE_CONFIRM.BUTTON': NoParams;
@@ -1,8 +1,13 @@
1
1
  export declare const Grid: import("styled-components").StyledComponent<"div", any, {}, never>;
2
- export declare const Tile: import("styled-components").StyledComponent<"div", any, {}, never>;
2
+ export declare const Tile: import("styled-components").StyledComponent<"div", any, {
3
+ $selected?: boolean;
4
+ }, never>;
3
5
  export declare const TileImage: import("styled-components").StyledComponent<"img", any, {}, never>;
4
6
  export declare const CropBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
5
7
  export declare const WarningBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
8
+ export declare const TileCheckbox: import("styled-components").StyledComponent<"div", any, {}, never>;
9
+ export declare const SelectionBar: import("styled-components").StyledComponent<"div", any, {}, never>;
10
+ export declare const SelectionCount: import("styled-components").StyledComponent<"span", any, {}, never>;
6
11
  export declare const TileActions: import("styled-components").StyledComponent<"div", any, {}, never>;
7
12
  export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
8
13
  export declare const Dropzone: import("styled-components").StyledComponent<"button", any, {
@@ -8,7 +8,11 @@ export const Grid = styled.div.withConfig({
8
8
  export const Tile = styled.div.withConfig({
9
9
  displayName: "Tile",
10
10
  componentId: "sc-14317rj-1"
11
- })(["position:relative;border-radius:6px;overflow:hidden;border:1px solid ", ";transition:border-color 0.15s;&:hover{border-color:", ";}"], Color.DIVIDER, Color.PRIMARY);
11
+ })(["position:relative;border-radius:6px;overflow:hidden;border:1px solid ", ";box-shadow:", ";transition:border-color 0.15s,box-shadow 0.15s;&:hover{border-color:", ";}"], ({
12
+ $selected
13
+ }) => $selected ? Color.PRIMARY : Color.DIVIDER, ({
14
+ $selected
15
+ }) => $selected ? `0 0 0 1px ${Color.PRIMARY}` : 'none', Color.PRIMARY);
12
16
  export const TileImage = styled.img.withConfig({
13
17
  displayName: "TileImage",
14
18
  componentId: "sc-14317rj-2"
@@ -21,17 +25,29 @@ export const WarningBadge = styled.span.withConfig({
21
25
  displayName: "WarningBadge",
22
26
  componentId: "sc-14317rj-4"
23
27
  })(["position:absolute;bottom:0;left:0;right:0;font-size:10px;line-height:1.2;text-align:center;color:", ";background:", ";padding:3px 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none;"], Color.CLEAR, Color.WARNING);
28
+ export const TileCheckbox = styled.div.withConfig({
29
+ displayName: "TileCheckbox",
30
+ componentId: "sc-14317rj-5"
31
+ })(["position:absolute;top:6px;left:6px;z-index:2;padding:2px;border-radius:4px;background:", ";"], Color.CLEAR);
32
+ export const SelectionBar = styled.div.withConfig({
33
+ displayName: "SelectionBar",
34
+ componentId: "sc-14317rj-6"
35
+ })(["display:flex;align-items:center;gap:12px;min-height:32px;margin-bottom:12px;"]);
36
+ export const SelectionCount = styled.span.withConfig({
37
+ displayName: "SelectionCount",
38
+ componentId: "sc-14317rj-7"
39
+ })(["font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
24
40
  export const TileActions = styled.div.withConfig({
25
41
  displayName: "TileActions",
26
- componentId: "sc-14317rj-5"
27
- })(["position:absolute;top:6px;left:6px;display:flex;gap:6px;padding:4px;border-radius:8px;background:rgba(0,0,0,0.45);backdrop-filter:blur(2px);opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], Tile);
42
+ componentId: "sc-14317rj-8"
43
+ })(["position:absolute;inset:0;z-index:1;display:flex;align-items:center;justify-content:center;gap:6px;background:rgba(0,0,0,0.45);backdrop-filter:blur(2px);opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], Tile);
28
44
  export const EmptyState = styled.div.withConfig({
29
45
  displayName: "EmptyState",
30
- componentId: "sc-14317rj-6"
46
+ componentId: "sc-14317rj-9"
31
47
  })(["text-align:center;padding:48px 16px;color:", ";"], Color.LOCKED);
32
48
  export const Dropzone = styled.button.withConfig({
33
49
  displayName: "Dropzone",
34
- componentId: "sc-14317rj-7"
50
+ componentId: "sc-14317rj-10"
35
51
  })(["width:100%;min-height:96px;margin-bottom:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;font:inherit;font-size:13px;color:", ";cursor:pointer;border:2px dashed ", ";border-radius:8px;background:", ";transition:border-color 0.15s,background 0.15s;&:hover{border-color:", ";}&:disabled{cursor:default;opacity:0.7;}"], Color.LOCKED, ({
36
52
  $dragOver
37
53
  }) => $dragOver ? Color.PRIMARY : Color.DIVIDER, ({
@@ -39,46 +55,46 @@ export const Dropzone = styled.button.withConfig({
39
55
  }) => $dragOver ? Color.SOFT_LIGHT : Color.CLEAR, Color.PRIMARY);
40
56
  export const HiddenInput = styled.input.withConfig({
41
57
  displayName: "HiddenInput",
42
- componentId: "sc-14317rj-8"
58
+ componentId: "sc-14317rj-11"
43
59
  })(["display:none;"]);
44
60
  export const UsageLine = styled.div.withConfig({
45
61
  displayName: "UsageLine",
46
- componentId: "sc-14317rj-9"
62
+ componentId: "sc-14317rj-12"
47
63
  })(["margin-left:auto;font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
48
64
  /* ---------- CreateCropDrawer ---------- */
49
65
  export const PreviewStage = styled.div.withConfig({
50
66
  displayName: "PreviewStage",
51
- componentId: "sc-14317rj-10"
67
+ componentId: "sc-14317rj-13"
52
68
  })(["display:flex;align-items:center;justify-content:center;min-height:200px;max-height:360px;padding:12px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
53
69
  export const PreviewFrame = styled.div.withConfig({
54
70
  displayName: "PreviewFrame",
55
- componentId: "sc-14317rj-11"
71
+ componentId: "sc-14317rj-14"
56
72
  })(["aspect-ratio:", ";max-width:100%;max-height:336px;overflow:hidden;border-radius:4px;"], ({
57
73
  $ratio
58
74
  }) => $ratio);
59
75
  export const PreviewImage = styled.img.withConfig({
60
76
  displayName: "PreviewImage",
61
- componentId: "sc-14317rj-12"
77
+ componentId: "sc-14317rj-15"
62
78
  })(["width:100%;height:100%;object-fit:cover;display:block;"]);
63
79
  export const FieldLabel = styled.div.withConfig({
64
80
  displayName: "FieldLabel",
65
- componentId: "sc-14317rj-13"
81
+ componentId: "sc-14317rj-16"
66
82
  })(["font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;color:", ";margin-bottom:6px;"], Color.LOCKED);
67
83
  export const SizeInput = styled(NativeInput).withConfig({
68
84
  displayName: "SizeInput",
69
- componentId: "sc-14317rj-14"
85
+ componentId: "sc-14317rj-17"
70
86
  })(["&[readonly]{background:", ";color:", ";}"], Color.SOFT_LIGHT, Color.LOCKED);
71
87
  export const Chips = styled.div.withConfig({
72
88
  displayName: "Chips",
73
- componentId: "sc-14317rj-15"
89
+ componentId: "sc-14317rj-18"
74
90
  })(["display:flex;flex-wrap:wrap;gap:7px;"]);
75
91
  export const Segment = styled.div.withConfig({
76
92
  displayName: "Segment",
77
- componentId: "sc-14317rj-16"
93
+ componentId: "sc-14317rj-19"
78
94
  })(["display:inline-flex;border:1px solid ", ";border-radius:6px;overflow:hidden;"], Color.DIVIDER);
79
95
  export const SegmentItem = styled.button.withConfig({
80
96
  displayName: "SegmentItem",
81
- componentId: "sc-14317rj-17"
97
+ componentId: "sc-14317rj-20"
82
98
  })(["font:inherit;font-size:13px;cursor:pointer;padding:7px 14px;border:none;background:", ";color:", ";& + &{border-left:1px solid ", ";}"], ({
83
99
  $active
84
100
  }) => $active ? Color.PRIMARY : Color.CLEAR, ({
@@ -86,17 +102,17 @@ export const SegmentItem = styled.button.withConfig({
86
102
  }) => $active ? Color.CLEAR : Color.MAIN, Color.DIVIDER);
87
103
  export const CropStrip = styled.div.withConfig({
88
104
  displayName: "CropStrip",
89
- componentId: "sc-14317rj-18"
105
+ componentId: "sc-14317rj-21"
90
106
  })(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
91
107
  export const CropThumb = styled.div.withConfig({
92
108
  displayName: "CropThumb",
93
- componentId: "sc-14317rj-19"
109
+ componentId: "sc-14317rj-22"
94
110
  })(["position:relative;width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
95
111
  export const CropThumbDelete = styled.div.withConfig({
96
112
  displayName: "CropThumbDelete",
97
- componentId: "sc-14317rj-20"
113
+ componentId: "sc-14317rj-23"
98
114
  })(["position:absolute;top:4px;right:4px;opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], CropThumb);
99
115
  export const CropThumbCaption = styled.div.withConfig({
100
116
  displayName: "CropThumbCaption",
101
- componentId: "sc-14317rj-21"
117
+ componentId: "sc-14317rj-24"
102
118
  })(["font-size:10px;text-align:center;padding:3px 2px;color:", ";"], Color.LOCKED);
@@ -0,0 +1,2 @@
1
+ import type { ProductUpdateCardProps } from './types';
2
+ export declare function ProductUpdateCard({ update, onNavigate }: ProductUpdateCardProps): import("react").JSX.Element;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Badge, Button } from '@pushwoosh/dumb-components';
4
+ import { Color } from '@pushwoosh/kit-constants';
5
+ import { Cluster, Columns, Horizontal, Vertical } from '@pushwoosh/kit-helpers';
6
+ import { Paragraph, Text } from '@pushwoosh/kit-typography';
7
+ import { useSPAContext } from '@pushwoosh/spa-context';
8
+ import FallbackImage from './assets/fallback.svg?url';
9
+ import { ALERT_STATUS_UNREAD } from './constants';
10
+ import { formatUpdateDate, openActionLink } from './helpers';
11
+ import { useLang } from './polyglot';
12
+ import { CardImage } from './styles';
13
+ export function ProductUpdateCard({
14
+ update,
15
+ onNavigate
16
+ }) {
17
+ const lang = useLang();
18
+ const {
19
+ history,
20
+ language
21
+ } = useSPAContext();
22
+ const [imageSrc, setImageSrc] = useState(update.imageUrl || FallbackImage);
23
+ const handleAction = link => () => {
24
+ if (openActionLink(link, history)) {
25
+ onNavigate();
26
+ }
27
+ };
28
+ return _jsxs(Columns, {
29
+ "$sizes": "256px 1fr",
30
+ "$gap": 32,
31
+ "$alignItems": "start",
32
+ children: [_jsx(CardImage, {
33
+ src: imageSrc,
34
+ alt: "",
35
+ loading: "lazy",
36
+ onError: () => setImageSrc(FallbackImage)
37
+ }), _jsxs(Vertical, {
38
+ "$gap": 16,
39
+ children: [_jsxs(Vertical, {
40
+ "$gap": 4,
41
+ children: [_jsxs(Horizontal, {
42
+ "$gap": 8,
43
+ "$alignItems": "center",
44
+ "$justifyContent": "start",
45
+ children: [_jsx(Text, {
46
+ "$variant": "footnote",
47
+ "$color": Color.LOCKED,
48
+ "$uppercase": true,
49
+ children: formatUpdateDate(update.created, language)
50
+ }), update.status === ALERT_STATUS_UNREAD && _jsx(Badge, {
51
+ color: "sky",
52
+ children: lang({
53
+ message: 'SMART.PRODUCT_UPDATES.BADGE.NEW'
54
+ })
55
+ })]
56
+ }), _jsxs(Vertical, {
57
+ children: [_jsx(Text, {
58
+ "$variant": "h4",
59
+ children: update.title
60
+ }), update.content && _jsx(Paragraph, {
61
+ children: update.content
62
+ })]
63
+ })]
64
+ }), (update.actionLink || update.secondaryActionLink) && _jsxs(Cluster, {
65
+ "$gap": 8,
66
+ "$flexWrap": "wrap",
67
+ children: [update.actionLink && _jsx(Button, {
68
+ color: "primary",
69
+ onClick: handleAction(update.actionLink),
70
+ children: update.actionLinkLabel || update.actionLink
71
+ }), update.secondaryActionLink && _jsx(Button, {
72
+ color: "secondary",
73
+ onClick: handleAction(update.secondaryActionLink),
74
+ children: update.secondaryActionLinkLabel || update.secondaryActionLink
75
+ })]
76
+ })]
77
+ })]
78
+ });
79
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProductUpdatesProps } from './types';
2
+ export declare function ProductUpdates({ autoOpenOnUnread }: ProductUpdatesProps): import("react").JSX.Element | null;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Button } from '@pushwoosh/dumb-components';
4
+ import { MegaphoneIcon } from '@pushwoosh/kit-icons';
5
+ import { useIsAllowProductUpdates } from './hooks/useIsAllowProductUpdates';
6
+ import { useProductUpdates } from './hooks/useProductUpdates';
7
+ import { useLang } from './polyglot';
8
+ import { ProductUpdatesModal } from './ProductUpdatesModal';
9
+ export function ProductUpdates({
10
+ autoOpenOnUnread = false
11
+ }) {
12
+ const lang = useLang();
13
+ const [isOpen, setIsOpen] = useState(false);
14
+ const hasAutoOpened = useRef(false);
15
+ const isAllowed = useIsAllowProductUpdates();
16
+ const {
17
+ updates,
18
+ unreadCount,
19
+ isLoading,
20
+ hasUpdates,
21
+ markAllRead
22
+ } = useProductUpdates();
23
+ useEffect(() => {
24
+ if (!isAllowed || !autoOpenOnUnread || hasAutoOpened.current || unreadCount === 0) {
25
+ return;
26
+ }
27
+ hasAutoOpened.current = true;
28
+ setIsOpen(true);
29
+ }, [isAllowed, autoOpenOnUnread, unreadCount]);
30
+ const handleClose = () => {
31
+ setIsOpen(false);
32
+ markAllRead();
33
+ };
34
+ if (!isAllowed || isLoading || !hasUpdates) {
35
+ return null;
36
+ }
37
+ return _jsxs(_Fragment, {
38
+ children: [_jsx(Button, {
39
+ color: "secondary",
40
+ view: "ghost",
41
+ size: "field",
42
+ icon: _jsx(MegaphoneIcon, {
43
+ size: "medium",
44
+ view: "lined"
45
+ }),
46
+ onClick: () => setIsOpen(true),
47
+ children: lang({
48
+ message: 'SMART.PRODUCT_UPDATES.BUTTON'
49
+ })
50
+ }), _jsx(ProductUpdatesModal, {
51
+ isOpen: isOpen,
52
+ onClose: handleClose,
53
+ updates: updates,
54
+ unreadCount: unreadCount
55
+ })]
56
+ });
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProductUpdatesModalProps } from './types';
2
+ export declare function ProductUpdatesModal(props: ProductUpdatesModalProps): import("react").JSX.Element;
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ import { Divider, Modal, ModalBody, ModalHeader, ModalTitle } from '@pushwoosh/dumb-components';
4
+ import { Color } from '@pushwoosh/kit-constants';
5
+ import { Horizontal, Vertical } from '@pushwoosh/kit-helpers';
6
+ import { Text } from '@pushwoosh/kit-typography';
7
+ import { useLang } from './polyglot';
8
+ import { ProductUpdateCard } from './ProductUpdateCard';
9
+ export function ProductUpdatesModal(props) {
10
+ const {
11
+ isOpen,
12
+ onClose,
13
+ updates,
14
+ unreadCount
15
+ } = props;
16
+ const lang = useLang();
17
+ // A colon only reads right when a count follows it.
18
+ const base = lang({
19
+ message: 'SMART.PRODUCT_UPDATES.TITLE'
20
+ });
21
+ const title = unreadCount > 0 ? `${base}:` : base;
22
+ return _jsxs(Modal, {
23
+ size: "large",
24
+ isOpen: isOpen,
25
+ onClose: onClose,
26
+ children: [_jsx(ModalHeader, {
27
+ children: _jsx(ModalTitle, {
28
+ onClose: onClose,
29
+ children: _jsxs(Horizontal, {
30
+ "$gap": 8,
31
+ "$alignItems": "baseline",
32
+ "$justifyContent": "start",
33
+ children: [_jsx("span", {
34
+ children: title
35
+ }), unreadCount > 0 && _jsx(Text, {
36
+ "$color": Color.LOCKED,
37
+ children: lang({
38
+ message: 'SMART.PRODUCT_UPDATES.UNREAD',
39
+ count: unreadCount
40
+ })
41
+ })]
42
+ })
43
+ })
44
+ }), _jsx(ModalBody, {
45
+ children: _jsx(Vertical, {
46
+ "$gap": 16,
47
+ "$padding": {
48
+ top: 8
49
+ },
50
+ children: updates.map((update, index) => _jsxs(Fragment, {
51
+ children: [index > 0 && _jsx(Divider, {}), _jsx(ProductUpdateCard, {
52
+ update: update,
53
+ onNavigate: onClose
54
+ })]
55
+ }, update.uuid))
56
+ })
57
+ })]
58
+ });
59
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="160" fill="none" xmlns:v="https://vecta.io/nano"><style><![CDATA[.B{fill:#f8f1f2}.C{color-interpolation-filters:sRGB}.D{flood-opacity:0}.E{fill:#fef6dc}]]></style><g clip-path="url(#M)"><path d="M0 0H256V160.121H0V0Z" fill="url(#E)"/><path d="M0 0H256V160.121H0V0Z" fill="url(#F)" fill-opacity=".8"/><path d="M100.375 15.723l.131.025c.54.382 2.935 5.782 3.5 6.798l-3.945 2.133-.04.007c-.5-.212-3.101-5.928-3.545-6.884l3.899-2.078z" class="B"/><path d="M173.172 32.816l3.98 2.787-3.917 6.102c-.944-.386-3.281-2.062-4.045-2.756l3.982-6.134z" class="E"/><g class="B"><path d="M162.016 68.984l.287-.014c.658.298 3.35 2.004 3.793 2.563-.171.605-2.636 4.86-3.127 5.338-1.258-.62-3.105-1.969-4.434-2.784l3.481-5.103zm-68.752 1.277c.667-.121.974.565 1.269 1.122.647 1.221 1.876 2.681 2.145 3.991.061.296-3.135 2.159-3.556 2.407l-.279-.093c-.46-.544-3.02-4.612-3.209-5.236.441-.377 3.035-1.859 3.63-2.192zm64.949-52.46l3.815 2.169-3.244 5.859c-.418.27-3.313-1.602-3.829-1.942l3.258-6.086z"/></g><path d="M90.04 43.218c.615.156 2.971 4.202 3.335 4.96-1.008.862-2.332 1.814-3.403 2.603-.884-1.169-2.505-3.868-3.364-5.209l3.432-2.354z" class="E"/><g class="B"><path d="M179.019 58.206c.341.114 2.681 2.363 3.115 2.768l-3.772 4.684-3.275-2.662c.997-1.395 2.794-3.512 3.932-4.79zm-100.934-.105c.382.275 3.239 4.144 3.66 4.753-.775.817-2.096 1.685-3.127 2.738-1.333-1.337-2.462-3.167-3.821-4.624l3.288-2.867z"/></g><path d="M78.363 32.098c.321.177 2.145 2.956 2.62 3.519-1.031.669-1.633 1.324-2.498 2.187-.474-.187-2.316-2.766-2.784-3.363l2.662-2.343z" class="E"/><g filter="url(#A)"><path d="M128.951 99.645l-.079 34.866-34.607-16.706V92.623l34.686 7.021zm32.554 18.482l-32.632 16.384.078-34.865 33.107-6.295-.553 24.776z" fill="url(#G)"/></g><g filter="url(#B)"><path d="M128.968 99.645l33.817-6.295-.566 24.776-33.331 16.384-35.349-16.706V92.623l35.429 7.021z" fill="url(#H)"/></g><path d="M128.888 134.51l.08-34.865 33.816-6.295-.565 24.776-33.331 16.384z" fill="url(#I)"/><path d="M128.888 134.51l.081-34.865-35.43-7.021v25.18l35.349 16.707z" fill="url(#H)"/><path d="M163.228 83.342l-34.26 16.303-36.156-16.383 34.946-10.169 35.47 10.25z" fill="url(#J)"/><g filter="url(#C)"><path d="M153.436 58.335h.585l-.146.517.146.171h-.194l-.388 1.383h-12.31v33.469l-12.08 5.77-12.906-5.836V60.405h-12.494l-.387-1.383h-.195l.146-.172-.145-.516h.584l24.893-29.274 24.891 29.275z" fill="url(#K)"/></g><path opacity=".8" d="M154.02 58.335l-.581 2.07h-49.79l-.581-2.07h50.952z" fill="#fff3d5"/><g filter="url(#D)"><path d="M154.021 59.023h-12.892v34.851l-12.08 5.77-12.906-5.836V59.023h-13.076l25.478-29.962 25.476 29.962z" fill="url(#L)"/></g><g class="B"><use href="#N"/><path d="M111.374 63.892l.524 1.414 1.413.523-1.413.523-.524 1.414-.523-1.414-1.413-.523 1.413-.523.523-1.414zm-4.68 5.165l.523 1.414 1.414.523-1.414.523-.523 1.414-.523-1.414-1.414-.523 1.414-.523.523-1.414zm-12.591-9.039l.48 1.296 1.296.48-1.296.48-.48 1.296-.48-1.296-1.296-.48 1.296-.48.48-1.296zm-6.295-4.842l.349.942.942.349-.942.349-.349.943-.349-.943-.942-.349.942-.349.349-.942zm84.257-5.488l.524 1.414 1.413.523-1.413.523-.524 1.414-.523-1.414-1.414-.523 1.414-.523.523-1.414z"/></g><path d="M146.724 101.259l-17.756-1.615 34.26-16.303 15.455 1.856-31.959 16.061z" fill="#fefcf9"/><path d="M111.213 101.258l17.755-1.614-36.156-16.383-14.608 2.018 33.009 15.98z" fill="#fefdfa"/></g><defs><filter id="A" x="93.619" y="92.623" width="69.084" height="43.178" filterUnits="userSpaceOnUse" class="C"><feFlood result="A" class="D"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="B"/><feOffset dy="0.645649"/><feGaussianBlur stdDeviation=".323"/><feComposite in2="B" operator="out"/><feColorMatrix values="0 0 0 0 0.363468 0 0 0 0 0.269585 0 0 0 0 0.457351 0 0 0 0.2 0"/><feBlend in2="A"/><feBlend in="SourceGraphic"/></filter><filter id="B" x="91.602" y="91.332" width="74.411" height="47.052" filterUnits="userSpaceOnUse" class="C"><feFlood result="A" class="D"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="B"/><feOffset dx="0.645649" dy="1.2913"/><feGaussianBlur stdDeviation="1.291"/><feComposite in2="B" operator="out"/><feColorMatrix values="0 0 0 0 0.363468 0 0 0 0 0.269585 0 0 0 0 0.457351 0 0 0 0.1 0"/><feBlend in2="A"/><feBlend in="SourceGraphic"/></filter><filter id="C" x="98.548" y="26.478" width="59.992" height="79.623" filterUnits="userSpaceOnUse" class="C"><feFlood result="A" class="D"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="B"/><feOffset dy="1.93695"/><feGaussianBlur stdDeviation="2.26"/><feComposite in2="B" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 0.904522 0 0 0 0 0.713567 0 0 0 1 0"/><feBlend in2="A"/><feBlend in="SourceGraphic"/></filter><filter id="D" x="103.067" y="29.061" width="50.953" height="72.521" filterUnits="userSpaceOnUse" class="C"><feFlood class="D"/><feBlend in="SourceGraphic" result="B"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="C"/><feOffset dy="1.93695"/><feGaussianBlur stdDeviation="2.905"/><feComposite in2="C" operator="arithmetic" k2="-1" k3="1"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="B"/></filter><radialGradient id="E" cx="0" cy="0" r="1" gradientTransform="translate(128 101.061) rotate(90) scale(138.718 138.718)" href="#O"><stop stop-color="#fef6df"/><stop offset=".067" stop-color="#fef5df"/><stop offset=".133" stop-color="#fcf4df"/><stop offset=".2" stop-color="#faf1e0"/><stop offset=".267" stop-color="#f6ede0"/><stop offset=".333" stop-color="#f2e8e1"/><stop offset=".4" stop-color="#ede2e2"/><stop offset=".467" stop-color="#e7dbe3"/><stop offset=".533" stop-color="#e2d4e4"/><stop offset=".6" stop-color="#dccde5"/><stop offset=".667" stop-color="#d7c7e6"/><stop offset=".733" stop-color="#d2c2e7"/><stop offset=".8" stop-color="#cfbee8"/><stop offset=".867" stop-color="#cdbbe8"/><stop offset=".933" stop-color="#cbb9e9"/></radialGradient><radialGradient id="F" cx="0" cy="0" r="1" gradientTransform="translate(128 125.061) rotate(-90) scale(29 108.326)" href="#O"><stop stop-color="#b68fcb"/><stop offset="1" stop-color="#d0b9f5" stop-opacity="0"/></radialGradient><radialGradient id="G" cx="0" cy="0" r="1" gradientTransform="matrix(-34.7657 16.4641 -13.6341 -30.037 128.951 101.985)" href="#O"><stop stop-color="#fef1e7"/><stop offset="1" stop-color="#f7e8f5"/></radialGradient><radialGradient id="H" cx="0" cy="0" r="1" gradientTransform="translate(128.969 101.985) rotate(155.126) scale(39.1417 33.1083)" href="#O"><stop stop-color="#fef1e7"/><stop offset="1" stop-color="#f7e8f5"/></radialGradient><radialGradient id="I" cx="0" cy="0" r="1" gradientTransform="translate(128.807 100.371) rotate(36.3738) scale(37.2877 30.7075)" href="#O"><stop stop-color="#fadaca"/><stop offset="1" stop-color="#cfbede"/></radialGradient><radialGradient id="J" cx="0" cy="0" r="1" gradientTransform="translate(128 95.5281) rotate(-90) scale(24.2926 35.1879)" href="#O"><stop stop-color="#ffe5b3"/><stop offset="1" stop-color="#fffefc"/></radialGradient><linearGradient id="K" x1="128.544" y1="21.763" x2="128.544" y2="99.645" href="#O"><stop stop-color="#fff4d8"/><stop offset=".586" stop-color="#fffbf1"/><stop offset="1" stop-color="#fff3d3"/></linearGradient><linearGradient id="L" x1="128.544" y1="21.763" x2="128.544" y2="99.645" href="#O"><stop stop-color="#ffedbf"/><stop offset=".717" stop-color="#fffbf1"/><stop offset="1" stop-color="#fff3d3"/></linearGradient><clipPath id="M"><path fill="#fff" d="M0 0h256v160H0z"/></clipPath><path id="N" d="M149.79 64.053l.524 1.414 1.413.523-1.413.523-.524 1.414-.523-1.414-1.413-.523 1.413-.523.523-1.414z"/><linearGradient id="O" gradientUnits="userSpaceOnUse"/></defs></svg>
@@ -0,0 +1,7 @@
1
+ import type { AlertStatus, AlertType, OrderBy, OrderDirection } from '@pushwoosh/rpc-v2-http-api-data/pushwoosh_accounts_alerts_v1';
2
+ export declare const UPDATES_PER_PAGE = 20;
3
+ export declare const ALERT_TYPE_PRODUCT_UPDATE: AlertType;
4
+ export declare const ALERT_STATUS_READ: AlertStatus;
5
+ export declare const ALERT_STATUS_UNREAD: AlertStatus;
6
+ export declare const ORDER_BY_CREATED: OrderBy;
7
+ export declare const ORDER_DIRECTION_DESC: OrderDirection;
@@ -0,0 +1,6 @@
1
+ export const UPDATES_PER_PAGE = 20;
2
+ export const ALERT_TYPE_PRODUCT_UPDATE = 'ALERT_TYPE_PRODUCT_UPDATE';
3
+ export const ALERT_STATUS_READ = 'ALERT_STATUS_READ';
4
+ export const ALERT_STATUS_UNREAD = 'ALERT_STATUS_UNREAD';
5
+ export const ORDER_BY_CREATED = 'ORDER_BY_CREATED';
6
+ export const ORDER_DIRECTION_DESC = 'ORDER_DIRECTION_DESC';
@@ -0,0 +1,4 @@
1
+ export declare function openActionLink(link: string, history: {
2
+ push: (path: string) => void;
3
+ }): boolean;
4
+ export declare function formatUpdateDate(value: string | Date | undefined, locale: string): string;
@@ -0,0 +1,33 @@
1
+ // Same-origin links stay inside the SPA; the root config resolves the /applications/app-code
2
+ // placeholder on the routing event, so a deeplink needs no application code here.
3
+ export function openActionLink(link, history) {
4
+ let url;
5
+ try {
6
+ url = new URL(link, window.location.origin);
7
+ } catch {
8
+ return false;
9
+ }
10
+ if (url.origin !== window.location.origin) {
11
+ window.open(url.href, '_blank', 'noopener,noreferrer');
12
+ return false;
13
+ }
14
+ history.push(`${url.pathname}${url.search}${url.hash}`);
15
+ return true;
16
+ }
17
+ // UTC on purpose: an announcement is dated once for everyone, and a local-time
18
+ // render turns a midnight-UTC send into the previous day west of Greenwich.
19
+ export function formatUpdateDate(value, locale) {
20
+ if (!value) {
21
+ return '';
22
+ }
23
+ const date = value instanceof Date ? value : new Date(value);
24
+ if (Number.isNaN(date.getTime())) {
25
+ return '';
26
+ }
27
+ return date.toLocaleDateString(locale, {
28
+ month: 'short',
29
+ day: 'numeric',
30
+ year: 'numeric',
31
+ timeZone: 'UTC'
32
+ });
33
+ }
@@ -0,0 +1 @@
1
+ export declare function useIsAllowProductUpdates(): boolean;
@@ -0,0 +1,9 @@
1
+ import { useSPAContext } from '@pushwoosh/spa-context';
2
+ // Product updates are Pushwoosh announcements; a whitelabel tenant must not see them.
3
+ // Same gate DocsButton uses, and it is deliberately on display, not on the broadcast.
4
+ export function useIsAllowProductUpdates() {
5
+ const {
6
+ environment
7
+ } = useSPAContext();
8
+ return !environment.isOMO;
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { ProductUpdate } from '../types';
2
+ type UseProductUpdates = {
3
+ readonly updates: ProductUpdate[];
4
+ readonly unreadCount: number;
5
+ readonly isLoading: boolean;
6
+ readonly hasUpdates: boolean;
7
+ readonly markAllRead: () => Promise<void>;
8
+ };
9
+ export declare function useProductUpdates(): UseProductUpdates;
10
+ export {};
@@ -0,0 +1,57 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useAsync } from '@pushwoosh/kit-helpers';
3
+ import { useSPAContext } from '@pushwoosh/spa-context';
4
+ import { ALERT_STATUS_READ, ALERT_STATUS_UNREAD, ALERT_TYPE_PRODUCT_UPDATE, ORDER_BY_CREATED, ORDER_DIRECTION_DESC, UPDATES_PER_PAGE } from '../constants';
5
+ import { useIsAllowProductUpdates } from './useIsAllowProductUpdates';
6
+ const EMPTY = [];
7
+ export function useProductUpdates() {
8
+ const {
9
+ rpcGateway
10
+ } = useSPAContext();
11
+ const isAllowed = useIsAllowProductUpdates();
12
+ const [isMarkedRead, setIsMarkedRead] = useState(false);
13
+ const state = useAsync(async () => {
14
+ if (!isAllowed) {
15
+ return EMPTY;
16
+ }
17
+ const response = await rpcGateway.internalAlerts.List({
18
+ type: ALERT_TYPE_PRODUCT_UPDATE,
19
+ page: 0,
20
+ perPage: UPDATES_PER_PAGE,
21
+ orderBy: ORDER_BY_CREATED,
22
+ orderDirection: ORDER_DIRECTION_DESC
23
+ });
24
+ return response.alerts;
25
+ }, [rpcGateway, isAllowed]);
26
+ const fetched = state.value ?? EMPTY;
27
+ // Read is all-or-nothing: closing the modal marks everything it showed,
28
+ // so one flag is enough to reflect it without refetching.
29
+ const updates = isMarkedRead ? fetched.map(update => ({
30
+ ...update,
31
+ status: ALERT_STATUS_READ
32
+ })) : fetched;
33
+ const markAllRead = useCallback(async () => {
34
+ const unread = fetched.filter(update => update.status === ALERT_STATUS_UNREAD);
35
+ if (unread.length === 0) {
36
+ return;
37
+ }
38
+ try {
39
+ await rpcGateway.internalAlerts.UpdateStatus({
40
+ alerts: unread.map(update => ({
41
+ uuid: update.uuid,
42
+ status: ALERT_STATUS_READ
43
+ }))
44
+ });
45
+ setIsMarkedRead(true);
46
+ } catch (error) {
47
+ console.error('Failed to mark product updates as read:', error);
48
+ }
49
+ }, [rpcGateway, fetched]);
50
+ return {
51
+ updates,
52
+ unreadCount: updates.filter(update => update.status === ALERT_STATUS_UNREAD).length,
53
+ isLoading: state.loading,
54
+ hasUpdates: updates.length > 0,
55
+ markAllRead
56
+ };
57
+ }
@@ -0,0 +1,5 @@
1
+ export { ProductUpdates } from './ProductUpdates';
2
+ export { ProductUpdatesModal } from './ProductUpdatesModal';
3
+ export { useProductUpdates } from './hooks/useProductUpdates';
4
+ export { useIsAllowProductUpdates } from './hooks/useIsAllowProductUpdates';
5
+ export type { ProductUpdate, ProductUpdateCardProps, ProductUpdatesModalProps, ProductUpdatesProps, } from './types';
@@ -0,0 +1,4 @@
1
+ export { ProductUpdates } from './ProductUpdates';
2
+ export { ProductUpdatesModal } from './ProductUpdatesModal';
3
+ export { useProductUpdates } from './hooks/useProductUpdates';
4
+ export { useIsAllowProductUpdates } from './hooks/useIsAllowProductUpdates';
@@ -0,0 +1,2 @@
1
+ import { type ProductUpdatesMessages } from './types';
2
+ export declare const Lang: import("react").FC<import("@pushwoosh/dumb-components/polyglot").LangProps<ProductUpdatesMessages>>, useLang: () => (props: import("@pushwoosh/dumb-components/polyglot").LangProps<ProductUpdatesMessages>) => string;
@@ -0,0 +1,10 @@
1
+ import { createLang } from '@pushwoosh/dumb-components/polyglot';
2
+ import { messages as en } from './messages_en';
3
+ import { messages as ru } from './messages_ru';
4
+ export const {
5
+ Lang,
6
+ useLang
7
+ } = createLang({
8
+ en,
9
+ ru
10
+ });
@@ -0,0 +1,3 @@
1
+ import type { LangMessages } from '@pushwoosh/dumb-components/polyglot';
2
+ import { type ProductUpdatesMessages } from './types';
3
+ export declare const messages: LangMessages<ProductUpdatesMessages>;
@@ -0,0 +1,6 @@
1
+ export const messages = {
2
+ 'SMART.PRODUCT_UPDATES.BUTTON': 'Product updates',
3
+ 'SMART.PRODUCT_UPDATES.TITLE': 'Product updates',
4
+ 'SMART.PRODUCT_UPDATES.UNREAD': '{{count}} unread',
5
+ 'SMART.PRODUCT_UPDATES.BADGE.NEW': 'NEW'
6
+ };
@@ -0,0 +1,3 @@
1
+ import type { LangMessages } from '@pushwoosh/dumb-components/polyglot';
2
+ import { type ProductUpdatesMessages } from './types';
3
+ export declare const messages: LangMessages<ProductUpdatesMessages>;
@@ -0,0 +1,6 @@
1
+ export const messages = {
2
+ 'SMART.PRODUCT_UPDATES.BUTTON': 'Обновления продукта',
3
+ 'SMART.PRODUCT_UPDATES.TITLE': 'Обновления продукта',
4
+ 'SMART.PRODUCT_UPDATES.UNREAD': 'непрочитанных: {{count}}',
5
+ 'SMART.PRODUCT_UPDATES.BADGE.NEW': 'NEW'
6
+ };
@@ -0,0 +1,9 @@
1
+ import type { NoParams } from '@pushwoosh/dumb-components/polyglot';
2
+ export type ProductUpdatesMessages = {
3
+ 'SMART.PRODUCT_UPDATES.BUTTON': NoParams;
4
+ 'SMART.PRODUCT_UPDATES.TITLE': NoParams;
5
+ 'SMART.PRODUCT_UPDATES.UNREAD': {
6
+ count: number;
7
+ };
8
+ 'SMART.PRODUCT_UPDATES.BADGE.NEW': NoParams;
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const CardImage: import("styled-components").StyledComponent<"img", any, {}, never>;
@@ -0,0 +1,8 @@
1
+ import styled from 'styled-components';
2
+ import { ShapeRadius } from '@pushwoosh/kit-constants';
3
+ // The only rule kit-helpers cannot express: object-fit has no prop there, and it is
4
+ // what keeps a wrongly proportioned upload from stretching the card.
5
+ export const CardImage = styled.img.withConfig({
6
+ displayName: "CardImage",
7
+ componentId: "sc-11vonu6-0"
8
+ })(["width:256px;height:160px;object-fit:cover;border-radius:", ";display:block;"], ShapeRadius.SECTION);
@@ -0,0 +1,16 @@
1
+ import type { Alert } from '@pushwoosh/rpc-v2-http-api-data/internal_alerts';
2
+ export type ProductUpdate = Alert;
3
+ export type ProductUpdatesProps = {
4
+ /** Opens the modal by itself once, when the user has never-read updates. */
5
+ readonly autoOpenOnUnread?: boolean;
6
+ };
7
+ export type ProductUpdatesModalProps = {
8
+ readonly isOpen: boolean;
9
+ readonly onClose: () => void;
10
+ readonly updates: ProductUpdate[];
11
+ readonly unreadCount: number;
12
+ };
13
+ export type ProductUpdateCardProps = {
14
+ readonly update: ProductUpdate;
15
+ readonly onNavigate: () => void;
16
+ };
@@ -0,0 +1 @@
1
+ export {};
package/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './DynamicContentPickerForm';
6
6
  export * from './EmailTemplatePreview';
7
7
  export * from './ExportCSV';
8
8
  export * from './PlatformsConfigurationDrawer';
9
+ export * from './ProductUpdates';
9
10
  export * from './LanguagesSelector';
10
11
  export * from './MediaStorage';
11
12
  export * from './mmsAttachments';
package/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './DynamicContentPickerForm';
6
6
  export * from './EmailTemplatePreview';
7
7
  export * from './ExportCSV';
8
8
  export * from './PlatformsConfigurationDrawer';
9
+ export * from './ProductUpdates';
9
10
  export * from './LanguagesSelector';
10
11
  export * from './MediaStorage';
11
12
  export * from './mmsAttachments';
package/package.json CHANGED
@@ -24,12 +24,12 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@floating-ui/react-dom": "^2.1.2",
27
- "@pushwoosh/dumb-components": "^1.1.202",
27
+ "@pushwoosh/dumb-components": "^1.1.209",
28
28
  "@pushwoosh/kit-constants": "^1.11.1",
29
29
  "@pushwoosh/kit-helpers": "^4.12.0",
30
- "@pushwoosh/kit-icons": "^2.1.22",
30
+ "@pushwoosh/kit-icons": "^2.2.3",
31
31
  "@pushwoosh/kit-typography": "^1.7.6",
32
- "@pushwoosh/rpc-v2-http-api-data": "^0.2.114",
32
+ "@pushwoosh/rpc-v2-http-api-data": "^0.2.151",
33
33
  "@pushwoosh/spa-context": "^1.7.1",
34
34
  "idb-keyval": "^6.2.2",
35
35
  "react-hook-form": "^7.68.0",
@@ -52,5 +52,5 @@
52
52
  "react": "^16.14.0 || ^18.0.0",
53
53
  "react-dom": "^16.14.0 || ^18.0.0"
54
54
  },
55
- "version": "1.3.15"
55
+ "version": "1.3.17"
56
56
  }