@pushwoosh/smart-components-kit 1.1.0 → 1.1.1

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.
@@ -1,12 +1,8 @@
1
1
  import { type FC } from 'react';
2
- export type MediaStoreProps = {
3
- /** Application code whose media files are listed. */
4
- application: string;
5
- /** Initial page to load (zero-based). Defaults to 0. */
6
- page?: number;
7
- /** Page size. Defaults to 30. */
8
- perPage?: number;
9
- /** Called with the chosen file URL (original or variant). */
10
- onSelect: (url: string) => void;
11
- };
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
+ * variant that keeps the page in internal state, use `MediaStorePaginated`.
7
+ */
12
8
  export declare const MediaStore: FC<MediaStoreProps>;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from 'react';
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';
@@ -9,14 +9,20 @@ import { CreateVariantDrawer } from './CreateVariantDrawer';
9
9
  import { getRatioLabel, getThumbUrl } from './mediaFile.utils';
10
10
  import { useLang } from './polyglot';
11
11
  import { Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, VariantBadge } from './styled';
12
- const DEFAULT_PER_PAGE = 30;
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
+ * variant that keeps the page in internal state, use `MediaStorePaginated`.
17
+ */
14
18
  export const MediaStore = ({
15
19
  application,
16
- page: initialPage = 0,
17
- perPage = DEFAULT_PER_PAGE,
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
- const loadTokenRef = useRef(0);
33
- const [files, setFiles] = useState([]);
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;
34
42
  const [variantsByUuid, setVariantsByUuid] = useState({});
35
- const [loading, setLoading] = useState(false);
36
- const [page, setPage] = useState(initialPage);
37
- const [total, setTotal] = useState(0);
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
48
  const [createVariantFor, setCreateVariantFor] = useState(null);
41
- const fetchFiles = useCallback(async p => {
42
- // Guard against races: a newer load invalidates in-flight results of older ones.
43
- const token = ++loadTokenRef.current;
44
- setLoading(true);
45
- setVariantsByUuid({});
46
- let originals;
47
- try {
48
- const response = await rpcGateway.mediaFiles.List({
49
- application,
50
- page: p,
51
- perPage
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
- // Originals are already on screen. The List of originals carries no variant
75
- // count, so fetch each original's variants in the background and let the
76
- // "N variants" badge appear per file as soon as its request resolves.
75
+ }, [listAsync.error, openToast, lang]);
76
+ // The List of originals carries no variant count, so once a page of originals
77
+ // resolves, fetch each one's variants in the background and let the
78
+ // "N variants" 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
+ setVariantsByUuid({});
85
+ let cancelled = false;
77
86
  originals.forEach(file => {
78
87
  void (async () => {
79
88
  try {
@@ -81,7 +90,7 @@ export const MediaStore = ({
81
90
  application,
82
91
  parentUuid: file.uuid
83
92
  });
84
- if (token !== loadTokenRef.current) return;
93
+ if (cancelled) return;
85
94
  setVariantsByUuid(prev => ({
86
95
  ...prev,
87
96
  [file.uuid]: response.files ?? []
@@ -91,10 +100,10 @@ export const MediaStore = ({
91
100
  }
92
101
  })();
93
102
  });
94
- }, [rpcGateway, application, perPage, openToast, lang]);
95
- useEffect(() => {
96
- void fetchFiles(initialPage);
97
- }, [fetchFiles, initialPage]);
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
- void fetchFiles(0);
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({
@@ -254,7 +269,7 @@ export const MediaStore = ({
254
269
  color: "secondary",
255
270
  size: "field",
256
271
  isDisabled: page <= 0,
257
- onClick: () => void fetchFiles(page - 1),
272
+ onClick: () => onPageChange(page - 1),
258
273
  children: lang({
259
274
  message: 'SMART.MEDIA_STORE.PREV_BUTTON'
260
275
  })
@@ -265,7 +280,7 @@ export const MediaStore = ({
265
280
  color: "secondary",
266
281
  size: "field",
267
282
  isDisabled: page + 1 >= totalPages,
268
- onClick: () => void fetchFiles(page + 1),
283
+ onClick: () => onPageChange(page + 1),
269
284
  children: lang({
270
285
  message: 'SMART.MEDIA_STORE.NEXT_BUTTON'
271
286
  })
@@ -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
+ };
@@ -1 +1,3 @@
1
- export { MediaStore, type MediaStoreProps } from './MediaStore';
1
+ export { MediaStore } from './MediaStore';
2
+ export { MediaStorePaginated } from './MediaStorePaginated';
3
+ export type { MediaStoreProps, MediaStorePaginatedProps } from './types';
@@ -1 +1,2 @@
1
- export { MediaStore } from './MediaStore';
1
+ export { MediaStore } from './MediaStore';
2
+ export { MediaStorePaginated } from './MediaStorePaginated';
@@ -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 variant). */
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 variant). */
21
+ onSelect: (url: string) => void;
22
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -49,5 +49,5 @@
49
49
  "react": "^16.14.0 || ^18.0.0",
50
50
  "react-dom": "^16.14.0 || ^18.0.0"
51
51
  },
52
- "version": "1.1.0"
52
+ "version": "1.1.1"
53
53
  }