@fluid-app/portal-sdk 0.1.227 → 0.1.229

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.
@@ -46,12 +46,13 @@ function useRepContext() {
46
46
  const shareablesKeys = {
47
47
  media: {
48
48
  all: ["media"],
49
- list: (search, sortDesc, repContext) => [
49
+ list: (search, sortDesc, repContext, ownership) => [
50
50
  "media",
51
51
  "list",
52
52
  search,
53
53
  sortDesc,
54
- repContext
54
+ repContext,
55
+ ownership
55
56
  ],
56
57
  detail: (id, repContext) => [
57
58
  "media",
@@ -691,6 +692,37 @@ function ToggleButton({ active, label, onClick, children }) {
691
692
  });
692
693
  }
693
694
  //#endregion
695
+ //#region ../../shareables/ui/src/hooks/use-infinite-list-sentinel.ts
696
+ /**
697
+ * Wires a sentinel `<div>` to TanStack Query infinite-pagination state.
698
+ *
699
+ * Returns a ref the caller attaches to a 1px sentinel element rendered below
700
+ * the list. When the sentinel scrolls into view (with a 200px root margin)
701
+ * and the query has a next page, isn't already fetching, and hasn't errored,
702
+ * `fetchNextPage()` is invoked.
703
+ */
704
+ function useInfiniteListSentinel({ hasNextPage, isFetchingNextPage, fetchNextPage, error }) {
705
+ const sentinelRef = (0, react.useRef)(null);
706
+ (0, react.useEffect)(() => {
707
+ const target = sentinelRef.current;
708
+ if (!target) return;
709
+ const observer = new IntersectionObserver((entries) => {
710
+ if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage && !error) fetchNextPage();
711
+ }, {
712
+ threshold: 0,
713
+ rootMargin: "200px"
714
+ });
715
+ observer.observe(target);
716
+ return () => observer.disconnect();
717
+ }, [
718
+ fetchNextPage,
719
+ hasNextPage,
720
+ isFetchingNextPage,
721
+ error
722
+ ]);
723
+ return sentinelRef;
724
+ }
725
+ //#endregion
694
726
  //#region ../../shareables/ui/src/components/screens/ProductsScreen.tsx
695
727
  const PAGE_SIZE$5 = 24;
696
728
  const SORT_OPTIONS$1 = [
@@ -733,7 +765,6 @@ function ProductsScreen({ countryCode, fetchProducts: fetchPortalProducts, onNav
733
765
  const [debouncedSearch, setDebouncedSearch] = (0, react.useState)("");
734
766
  const [sortValue, setSortValue] = (0, react.useState)("created_at_desc");
735
767
  const [viewMode, setViewMode] = (0, react.useState)("grid");
736
- const observerTarget = (0, react.useRef)(null);
737
768
  (0, react.useEffect)(() => {
738
769
  const timer = setTimeout(() => {
739
770
  setDebouncedSearch(searchTerm);
@@ -782,23 +813,12 @@ function ProductsScreen({ countryCode, fetchProducts: fetchPortalProducts, onNav
782
813
  });
783
814
  const usePortal = !!fetchPortalProducts;
784
815
  const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, error } = usePortal ? portalQuery : legacyQuery;
785
- const handleIntersect = (0, react.useCallback)((entries) => {
786
- if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
787
- }, [
816
+ const sentinelRef = useInfiniteListSentinel({
788
817
  hasNextPage,
789
818
  isFetchingNextPage,
790
- fetchNextPage
791
- ]);
792
- (0, react.useEffect)(() => {
793
- const target = observerTarget.current;
794
- if (!target) return;
795
- const observer = new IntersectionObserver(handleIntersect, {
796
- threshold: .1,
797
- rootMargin: "200px"
798
- });
799
- observer.observe(target);
800
- return () => observer.disconnect();
801
- }, [handleIntersect]);
819
+ fetchNextPage,
820
+ error
821
+ });
802
822
  const products = (0, react.useMemo)(() => {
803
823
  if (!data) return [];
804
824
  if (usePortal) return data?.pages.flatMap((page) => (page.products ?? []).map((p) => ({
@@ -814,11 +834,16 @@ function ProductsScreen({ countryCode, fetchProducts: fetchPortalProducts, onNav
814
834
  media_count: void 0
815
835
  })) ?? [];
816
836
  }, [data, usePortal]);
837
+ const footer = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [isFetchingNextPage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
838
+ className: "flex justify-center py-4",
839
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" })
840
+ }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
841
+ className: "bg-destructive/10 text-destructive rounded-lg px-3 py-2",
842
+ children: "Failed to load products. Please try again."
843
+ })] });
817
844
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShareableListLayout, {
818
845
  isLoading,
819
- error,
820
- errorMessage: "Failed to load products. Please try again.",
821
- isEmpty: products.length === 0,
846
+ isEmpty: !error && products.length === 0 && !isFetchingNextPage && !hasNextPage,
822
847
  emptyMessage: debouncedSearch ? "No products match your search." : "No products available.",
823
848
  filters: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
824
849
  className: "flex items-center justify-end gap-2",
@@ -838,11 +863,8 @@ function ProductsScreen({ countryCode, fetchProducts: fetchPortalProducts, onNav
838
863
  })]
839
864
  }),
840
865
  loadingFilterShape: "search-view",
841
- footer: isFetchingNextPage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
842
- className: "flex justify-center py-4",
843
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" })
844
- }),
845
- sentinelRef: observerTarget,
866
+ footer,
867
+ sentinelRef,
846
868
  children: viewMode === "grid" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
847
869
  className: SHAREABLE_GRID_CLASS,
848
870
  children: products.map((product) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShareProductCard, {
@@ -1635,7 +1657,7 @@ function OwnerFilterTabs({ value, onValueChange, myLabel = "Mine" }) {
1635
1657
  children: "All"
1636
1658
  }),
1637
1659
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.TabsTrigger, {
1638
- value: "my",
1660
+ value: "mine",
1639
1661
  children: myLabel
1640
1662
  }),
1641
1663
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_src.TabsTrigger, {
@@ -1711,7 +1733,6 @@ function MediaListingScreen(_props) {
1711
1733
  }, 300);
1712
1734
  return () => clearTimeout(timer);
1713
1735
  }, [searchTerm]);
1714
- const observerTarget = (0, react.useRef)(null);
1715
1736
  const { mutate: deleteMedia, isPending: isDeleting } = (0, _tanstack_react_query.useMutation)({
1716
1737
  mutationFn: (id) => api.media.deleteMedia(id),
1717
1738
  onSuccess: () => {
@@ -1719,7 +1740,7 @@ function MediaListingScreen(_props) {
1719
1740
  title: "Media deleted",
1720
1741
  type: "success"
1721
1742
  });
1722
- queryClient.invalidateQueries({ queryKey: shareablesKeys.media.list(debouncedSearch, sortValue === "title_desc", repContext) });
1743
+ queryClient.invalidateQueries({ queryKey: shareablesKeys.media.all });
1723
1744
  setPendingDeleteId(null);
1724
1745
  },
1725
1746
  onError: (error) => {
@@ -1738,13 +1759,14 @@ function MediaListingScreen(_props) {
1738
1759
  const bffKind = kindFilter === "all" ? void 0 : kindFilter;
1739
1760
  const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, error } = (0, _tanstack_react_query.useInfiniteQuery)({
1740
1761
  queryKey: [
1741
- ...shareablesKeys.media.list(debouncedSearch, sortValue === "title_desc", repContext),
1762
+ ...shareablesKeys.media.list(debouncedSearch, sortValue === "title_desc", repContext, ownerFilter),
1742
1763
  sortValue,
1743
1764
  kindFilter
1744
1765
  ],
1745
1766
  queryFn: async ({ pageParam }) => {
1746
1767
  return await api.media.list({
1747
1768
  "filter[title]": debouncedSearch || void 0,
1769
+ "filter[ownership]": ownerFilter,
1748
1770
  "filter[content_format]": bffKind,
1749
1771
  "page[cursor]": pageParam,
1750
1772
  "page[limit]": PAGE_SIZE$4,
@@ -1755,24 +1777,13 @@ function MediaListingScreen(_props) {
1755
1777
  initialPageParam: void 0,
1756
1778
  placeholderData: _tanstack_react_query.keepPreviousData
1757
1779
  });
1758
- const allItems = (0, react.useMemo)(() => data?.pages.flatMap((p) => p.media) ?? [], [data?.pages]);
1759
- const filteredItems = ownerFilter === "all" ? allItems : ownerFilter === "my" ? allItems.filter((item) => item.owner_type === "user") : allItems.filter((item) => item.owner_type === "company");
1760
- (0, react.useEffect)(() => {
1761
- const target = observerTarget.current;
1762
- if (!target) return;
1763
- const observer = new IntersectionObserver((entries) => {
1764
- if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
1765
- }, {
1766
- threshold: 0,
1767
- rootMargin: "200px"
1768
- });
1769
- observer.observe(target);
1770
- return () => observer.disconnect();
1771
- }, [
1772
- fetchNextPage,
1780
+ const filteredItems = (0, react.useMemo)(() => data?.pages.flatMap((p) => p.media) ?? [], [data?.pages]);
1781
+ const sentinelRef = useInfiniteListSentinel({
1773
1782
  hasNextPage,
1774
- isFetchingNextPage
1775
- ]);
1783
+ isFetchingNextPage,
1784
+ fetchNextPage,
1785
+ error
1786
+ });
1776
1787
  const filterBar = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1777
1788
  className: "flex items-center gap-3",
1778
1789
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OwnerFilterTabs, {
@@ -1869,7 +1880,7 @@ function MediaListingScreen(_props) {
1869
1880
  }),
1870
1881
  filters: filterBar,
1871
1882
  footer,
1872
- sentinelRef: observerTarget,
1883
+ sentinelRef,
1873
1884
  loadingFilterShape: "search-view",
1874
1885
  children: viewMode === "grid" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1875
1886
  className: SHAREABLE_GRID_CLASS,
@@ -9071,7 +9082,7 @@ function PlaylistsListingScreen(_props) {
9071
9082
  const allPlaylists = (0, react.useMemo)(() => data?.pages.flatMap((page) => page.playlists) ?? [], [data?.pages]);
9072
9083
  const filteredPlaylists = (0, react.useMemo)(() => {
9073
9084
  if (ownerFilter === "all" || !user?.id) return allPlaylists;
9074
- if (ownerFilter === "my") return allPlaylists.filter((p) => p.user_id === user.id);
9085
+ if (ownerFilter === "mine") return allPlaylists.filter((p) => p.user_id === user.id);
9075
9086
  return allPlaylists.filter((p) => p.user_id !== user.id);
9076
9087
  }, [
9077
9088
  allPlaylists,
@@ -10826,7 +10837,6 @@ function FilesListingScreen({ onNavigate }) {
10826
10837
  }) }), []));
10827
10838
  const [searchTerm, setSearchTerm] = (0, react.useState)("");
10828
10839
  const [debouncedSearch, setDebouncedSearch] = (0, react.useState)("");
10829
- const observerTarget = (0, react.useRef)(null);
10830
10840
  (0, react.useEffect)(() => {
10831
10841
  const timer = setTimeout(() => {
10832
10842
  setDebouncedSearch(searchTerm);
@@ -10849,28 +10859,22 @@ function FilesListingScreen({ onNavigate }) {
10849
10859
  initialPageParam: 1
10850
10860
  });
10851
10861
  const files = (0, react.useMemo)(() => data?.pages.flatMap((page) => page.file_resources) ?? [], [data?.pages]);
10852
- const handleIntersect = (0, react.useCallback)((entries) => {
10853
- if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
10854
- }, [
10862
+ const sentinelRef = useInfiniteListSentinel({
10855
10863
  hasNextPage,
10856
10864
  isFetchingNextPage,
10857
- fetchNextPage
10858
- ]);
10859
- (0, react.useEffect)(() => {
10860
- const target = observerTarget.current;
10861
- if (!target) return;
10862
- const observer = new IntersectionObserver(handleIntersect, {
10863
- threshold: .1,
10864
- rootMargin: "200px"
10865
- });
10866
- observer.observe(target);
10867
- return () => observer.disconnect();
10868
- }, [handleIntersect]);
10865
+ fetchNextPage,
10866
+ error
10867
+ });
10868
+ const footer = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [isFetchingNextPage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10869
+ className: "flex justify-center py-4",
10870
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" })
10871
+ }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10872
+ className: "bg-destructive/10 text-destructive rounded-lg px-3 py-2",
10873
+ children: "Failed to load files. Please try again."
10874
+ })] });
10869
10875
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShareableListLayout, {
10870
10876
  isLoading,
10871
- error,
10872
- errorMessage: "Failed to load files. Please try again.",
10873
- isEmpty: files.length === 0,
10877
+ isEmpty: !error && files.length === 0 && !isFetchingNextPage && !hasNextPage,
10874
10878
  emptyMessage: debouncedSearch ? `No files match "${debouncedSearch}". Try a different search term.` : "No files available.",
10875
10879
  filters: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10876
10880
  className: "flex justify-end",
@@ -10883,11 +10887,8 @@ function FilesListingScreen({ onNavigate }) {
10883
10887
  })
10884
10888
  })
10885
10889
  }),
10886
- footer: isFetchingNextPage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10887
- className: "flex justify-center py-4",
10888
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "border-primary h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" })
10889
- }),
10890
- sentinelRef: observerTarget,
10890
+ footer,
10891
+ sentinelRef,
10891
10892
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10892
10893
  className: SHAREABLE_GRID_CLASS,
10893
10894
  children: files.map((file) => {
@@ -11383,6 +11384,7 @@ function createRawMediaAdapter(client) {
11383
11384
  "page[cursor]": params?.cursor,
11384
11385
  "page[limit]": params?.limit,
11385
11386
  "filter[title]": params?.["filter[title]"],
11387
+ "filter[ownership]": params?.["filter[ownership]"],
11386
11388
  "filter[content_format]": params?.["filter[content_format]"],
11387
11389
  sort: params?.sort
11388
11390
  });
@@ -11555,6 +11557,7 @@ function createMediaAdapter(client) {
11555
11557
  cursor: options?.["page[cursor]"],
11556
11558
  limit: options?.["page[limit]"],
11557
11559
  "filter[title]": options?.["filter[title]"],
11560
+ "filter[ownership]": options?.["filter[ownership]"],
11558
11561
  "filter[content_format]": options?.["filter[content_format]"],
11559
11562
  sort: options?.sort
11560
11563
  });
@@ -11565,7 +11568,7 @@ function createMediaAdapter(client) {
11565
11568
  },
11566
11569
  getMedia: async (options) => {
11567
11570
  const pageNumber = options?.page ?? 1;
11568
- const filterKey = `${options?.search_query ?? ""}|${options?.sorted_by ?? ""}`;
11571
+ const filterKey = `${options?.search_query ?? ""}|${options?.sorted_by ?? ""}|${options?.ownership ?? ""}`;
11569
11572
  if (filterKey !== lastFilterKey) {
11570
11573
  cursorByPage.clear();
11571
11574
  lastFilterKey = filterKey;
@@ -11575,10 +11578,13 @@ function createMediaAdapter(client) {
11575
11578
  let bffSort;
11576
11579
  if (rawSort === "title_asc") bffSort = "title_asc";
11577
11580
  else if (rawSort === "title_desc") bffSort = "title_desc";
11581
+ const ownership = options?.ownership;
11582
+ const bffOwnership = ownership === "all" || ownership === "mine" || ownership === "company" ? ownership : void 0;
11578
11583
  const response = await portAdapter.listMedia({
11579
11584
  cursor,
11580
11585
  limit: options?.per_page,
11581
11586
  "filter[title]": options?.search_query,
11587
+ "filter[ownership]": bffOwnership,
11582
11588
  sort: bffSort
11583
11589
  });
11584
11590
  const nextCursor = response.meta.pagination?.next_cursor;
@@ -12511,4 +12517,4 @@ Object.defineProperty(exports, "shareablesScreenPropertySchema", {
12511
12517
  }
12512
12518
  });
12513
12519
 
12514
- //# sourceMappingURL=ShareablesScreen-AMlPuFFk.cjs.map
12520
+ //# sourceMappingURL=ShareablesScreen-o6frhZUM.cjs.map