xladmin 0.2.8 → 0.2.9

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/dist/client.d.ts CHANGED
@@ -2,6 +2,14 @@ import type { AdminChoicesResponse, AdminDeletePreviewResponse, AdminDetailRespo
2
2
  export type XLAdminRequestOptions = {
3
3
  signal?: AbortSignal;
4
4
  };
5
+ export type XLAdminSelectionScope = {
6
+ q?: string;
7
+ filters?: Record<string, string>;
8
+ };
9
+ export type XLAdminSelectionOptions = {
10
+ selectAll?: boolean;
11
+ selectionScope?: XLAdminSelectionScope;
12
+ };
5
13
  export type XLAdminClient = {
6
14
  getModels: () => Promise<AdminModelsResponse>;
7
15
  getModel: (slug: string) => Promise<AdminModelMeta>;
@@ -16,11 +24,11 @@ export type XLAdminClient = {
16
24
  patchItem: (slug: string, id: string | number, payload: Record<string, unknown>) => Promise<AdminDetailResponse>;
17
25
  deleteItem: (slug: string, id: string | number) => Promise<void>;
18
26
  getDeletePreview: (slug: string, id: string | number) => Promise<AdminDeletePreviewResponse>;
19
- bulkDelete: (slug: string, ids: Array<string | number>) => Promise<{
27
+ bulkDelete: (slug: string, ids: Array<string | number>, options?: XLAdminSelectionOptions) => Promise<{
20
28
  deleted: number;
21
29
  }>;
22
- getBulkDeletePreview: (slug: string, ids: Array<string | number>) => Promise<AdminDeletePreviewResponse>;
23
- runBulkAction: (slug: string, actionSlug: string, ids: Array<string | number>, payload?: Record<string, unknown>) => Promise<{
30
+ getBulkDeletePreview: (slug: string, ids: Array<string | number>, options?: XLAdminSelectionOptions) => Promise<AdminDeletePreviewResponse>;
31
+ runBulkAction: (slug: string, actionSlug: string, ids: Array<string | number>, payload?: Record<string, unknown>, options?: XLAdminSelectionOptions) => Promise<{
24
32
  processed: number;
25
33
  } & Record<string, unknown>>;
26
34
  runObjectAction: (slug: string, id: string | number, actionSlug: string, payload?: Record<string, unknown>) => Promise<AdminObjectActionResponse>;
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { XLAdminClient } from '../client';
2
3
  import type { XLAdminRouter } from '../router';
3
4
  type AdminModelPageProps = {
@@ -5,7 +6,24 @@ type AdminModelPageProps = {
5
6
  basePath: string;
6
7
  slug: string;
7
8
  router?: XLAdminRouter;
9
+ renderBeforePagination?: (context: ModelPageToolbarContext) => ReactNode;
8
10
  };
9
11
  export type ModelPageProps = AdminModelPageProps;
10
- export declare function ModelPage({ client, basePath, slug, router }: ModelPageProps): import("react/jsx-runtime").JSX.Element;
12
+ export type ModelPageToolbarContext = {
13
+ client: XLAdminClient;
14
+ slug: string;
15
+ meta: {
16
+ slug: string;
17
+ title: string;
18
+ };
19
+ selectedIds: Array<string | number>;
20
+ isAllMatchingSelected: boolean;
21
+ selectionCount: number;
22
+ total: number;
23
+ appliedQuery: string;
24
+ sortValue: string;
25
+ appliedFilters: Record<string, string>;
26
+ refresh: () => Promise<void>;
27
+ };
28
+ export declare function ModelPage({ client, basePath, slug, router, renderBeforePagination }: ModelPageProps): import("react/jsx-runtime").JSX.Element;
11
29
  export {};
@@ -16,6 +16,7 @@ export declare function useModelPageController({ client, slug, pathname, locatio
16
16
  appliedQuery: string;
17
17
  bulkActionMenuAnchor: HTMLElement | null;
18
18
  bulkActions: import("@xladmin-core/types").AdminBulkActionMeta[];
19
+ clearBulkDeleteState: () => void;
19
20
  createOpen: boolean;
20
21
  currentPage: number;
21
22
  data: AdminListResponse | null;
@@ -25,6 +26,9 @@ export declare function useModelPageController({ client, slug, pathname, locatio
25
26
  error: string | null;
26
27
  fieldMap: Map<string, import("@xladmin-core/types").AdminFieldMeta>;
27
28
  filtersOpen: boolean;
29
+ handleClearDeletePreview: () => void;
30
+ handleCloseBulkActionMenu: () => void;
31
+ handleCloseRowMenu: () => void;
28
32
  handleConfirmDelete: () => Promise<void>;
29
33
  handleFilterChange: (filterSlug: string, value: string) => void;
30
34
  handleOpenRowMenu: (event: {
@@ -36,10 +40,13 @@ export declare function useModelPageController({ client, slug, pathname, locatio
36
40
  handleRowDelete: (rowId: string | number) => Promise<void>;
37
41
  handleRunNamedBulkAction: (actionSlug: string) => Promise<void>;
38
42
  handleSearchCommit: (nextQuery: string) => void;
43
+ handleSelectAllMatching: () => void;
39
44
  handleToggleAllVisible: (checked: boolean) => void;
40
45
  handleToggleSelection: (rowId: string | number, checked: boolean) => void;
41
46
  hasListFilters: boolean;
47
+ hasSelection: boolean;
42
48
  hasVisibleSelection: boolean;
49
+ isAllMatchingSelected: boolean;
43
50
  isDeletePreviewLoading: boolean;
44
51
  isDeleteSubmitting: boolean;
45
52
  isLoading: boolean;
@@ -53,6 +60,7 @@ export declare function useModelPageController({ client, slug, pathname, locatio
53
60
  rowActionMenuAnchor: HTMLElement | null;
54
61
  rowActionMenuId: string | number | null;
55
62
  rows: Record<string, unknown>[];
63
+ selectionCount: number;
56
64
  selectedIdSet: Set<string>;
57
65
  selectedIds: (string | number)[];
58
66
  setBulkActionMenuAnchor: import("react").Dispatch<import("react").SetStateAction<HTMLElement | null>>;
@@ -62,9 +70,7 @@ export declare function useModelPageController({ client, slug, pathname, locatio
62
70
  setDeletePreviewOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
63
71
  setFiltersOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
64
72
  setPageInput: import("react").Dispatch<import("react").SetStateAction<string>>;
65
- setPendingDeleteIds: import("react").Dispatch<import("react").SetStateAction<(string | number)[]>>;
66
- setRowActionMenuAnchor: import("react").Dispatch<import("react").SetStateAction<HTMLElement | null>>;
67
- setRowActionMenuId: import("react").Dispatch<import("react").SetStateAction<string | number | null>>;
73
+ sortValue: string;
68
74
  sortFields: string[];
69
75
  toggleSort: (fieldName: string) => void;
70
76
  total: number;
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ export { createAxiosXLAdminClient, createFetchXLAdminClient, createFetchXLAdminT
2
2
  export { XLAdminRouterProvider, buildUrlWithParams, createBrowserXLAdminRouter, handleNavLinkClick, type XLAdminAnchorProps, type XLAdminLocation, type XLAdminRouter, useXLAdminLocation, useXLAdminRouter, } from './router';
3
3
  export { FormDialog, type FormDialogProps } from './components/FormDialog';
4
4
  export { OverviewPage, type OverviewPageProps } from './components/OverviewPage';
5
- export { ModelPage, type ModelPageProps } from './components/ModelPage';
5
+ export { ModelPage, type ModelPageProps, type ModelPageToolbarContext } from './components/ModelPage';
6
6
  export { ObjectPage, type ObjectPageProps } from './components/ObjectPage';
7
7
  export { Shell, type ShellProps } from './components/Shell';
8
8
  export { FieldEditor, type FieldEditorProps } from './components/FieldEditor';
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { createAxiosXLAdminClient, createFetchXLAdminClient, createFetchXLAdminT
2
2
  export { XLAdminRouterProvider, buildUrlWithParams, createBrowserXLAdminRouter, handleNavLinkClick, type XLAdminAnchorProps, type XLAdminLocation, type XLAdminRouter, useXLAdminLocation, useXLAdminRouter, } from './router';
3
3
  export { FormDialog, type FormDialogProps } from './components/FormDialog';
4
4
  export { OverviewPage, type OverviewPageProps } from './components/OverviewPage';
5
- export { ModelPage, type ModelPageProps } from './components/ModelPage';
5
+ export { ModelPage, type ModelPageProps, type ModelPageToolbarContext } from './components/ModelPage';
6
6
  export { ObjectPage, type ObjectPageProps } from './components/ObjectPage';
7
7
  export { Shell, type ShellProps } from './components/Shell';
8
8
  export { FieldEditor, type FieldEditorProps } from './components/FieldEditor';
package/dist/index.js CHANGED
@@ -25,17 +25,25 @@ function createXLAdminClient(transport) {
25
25
  async getDeletePreview(slug, id) {
26
26
  return await transportGet(transport, `/xladmin/models/${slug}/items/${id}/delete-preview/`);
27
27
  },
28
- async bulkDelete(slug, ids) {
29
- return await transportPost(transport, `/xladmin/models/${slug}/bulk-delete/`, { ids });
28
+ async bulkDelete(slug, ids, options) {
29
+ return await transportPost(
30
+ transport,
31
+ `/xladmin/models/${slug}/bulk-delete/`,
32
+ buildSelectionPayload(ids, void 0, options)
33
+ );
30
34
  },
31
- async getBulkDeletePreview(slug, ids) {
32
- return await transportPost(transport, `/xladmin/models/${slug}/bulk-delete-preview/`, { ids });
35
+ async getBulkDeletePreview(slug, ids, options) {
36
+ return await transportPost(
37
+ transport,
38
+ `/xladmin/models/${slug}/bulk-delete-preview/`,
39
+ buildSelectionPayload(ids, void 0, options)
40
+ );
33
41
  },
34
- async runBulkAction(slug, actionSlug, ids, payload) {
42
+ async runBulkAction(slug, actionSlug, ids, payload, options) {
35
43
  return await transportPost(
36
44
  transport,
37
45
  `/xladmin/models/${slug}/bulk-actions/${actionSlug}/`,
38
- { ids, ...payload != null ? payload : {} }
46
+ buildSelectionPayload(ids, payload, options)
39
47
  );
40
48
  },
41
49
  async runObjectAction(slug, id, actionSlug, payload) {
@@ -181,6 +189,23 @@ function buildSearchParams(params) {
181
189
  }
182
190
  return searchParams.toString();
183
191
  }
192
+ function buildSelectionPayload(ids, payload, options) {
193
+ var _a;
194
+ const result = {
195
+ ids,
196
+ ...payload != null ? payload : {}
197
+ };
198
+ if (options == null ? void 0 : options.selectAll) {
199
+ result.select_all = true;
200
+ }
201
+ if (options == null ? void 0 : options.selectionScope) {
202
+ result.selection_scope = {
203
+ q: options.selectionScope.q,
204
+ filters: (_a = options.selectionScope.filters) != null ? _a : {}
205
+ };
206
+ }
207
+ return result;
208
+ }
184
209
 
185
210
  // src/router.tsx
186
211
  import { createContext, useContext, useMemo, useRef, useSyncExternalStore } from "react";
@@ -2562,6 +2587,7 @@ function useModelPageController({
2562
2587
  })
2563
2588
  )) != null ? _c : null;
2564
2589
  const [selectedIds, setSelectedIds] = useState9([]);
2590
+ const [isAllMatchingSelected, setIsAllMatchingSelected] = useState9(false);
2565
2591
  const [createOpen, setCreateOpen] = useState9(false);
2566
2592
  const [data, setData] = useState9(() => initialCachedResponse);
2567
2593
  const [error, setError] = useState9(null);
@@ -2580,6 +2606,8 @@ function useModelPageController({
2580
2606
  const [isDeletePreviewLoading, setIsDeletePreviewLoading] = useState9(false);
2581
2607
  const [isDeleteSubmitting, setIsDeleteSubmitting] = useState9(false);
2582
2608
  const [pendingDeleteIds, setPendingDeleteIds] = useState9([]);
2609
+ const [pendingDeleteSelectAll, setPendingDeleteSelectAll] = useState9(false);
2610
+ const [pendingDeleteScope, setPendingDeleteScope] = useState9(null);
2583
2611
  const [pendingDeleteMode, setPendingDeleteMode] = useState9("single");
2584
2612
  const [filtersOpen, setFiltersOpen] = useState9(false);
2585
2613
  const requestIdRef = useRef3(0);
@@ -2602,15 +2630,32 @@ function useModelPageController({
2602
2630
  const listFilters = (_j = meta == null ? void 0 : meta.list_filters) != null ? _j : [];
2603
2631
  const hasListFilters = listFilters.length > 0;
2604
2632
  const bulkActions = (_k = meta == null ? void 0 : meta.bulk_actions) != null ? _k : [];
2605
- const selectedIdSet = useMemo8(() => new Set(selectedIds.map((item) => String(item))), [selectedIds]);
2606
- const allVisibleSelected = rows.length > 0 && rows.every((row) => {
2633
+ const selectionScope = useMemo8(
2634
+ () => ({
2635
+ q: appliedQuery || void 0,
2636
+ filters: { ...appliedFilters }
2637
+ }),
2638
+ [appliedFilters, appliedQuery]
2639
+ );
2640
+ const selectedIdSet = useMemo8(() => {
2641
+ if (isAllMatchingSelected) {
2642
+ return new Set(rows.map((row) => {
2643
+ var _a2;
2644
+ return String(row[(_a2 = meta == null ? void 0 : meta.pk_field) != null ? _a2 : "id"]);
2645
+ }));
2646
+ }
2647
+ return new Set(selectedIds.map((item) => String(item)));
2648
+ }, [isAllMatchingSelected, meta == null ? void 0 : meta.pk_field, rows, selectedIds]);
2649
+ const allVisibleSelected = rows.length > 0 && (isAllMatchingSelected || rows.every((row) => {
2607
2650
  var _a2;
2608
2651
  return selectedIdSet.has(String(row[(_a2 = meta == null ? void 0 : meta.pk_field) != null ? _a2 : "id"]));
2609
- });
2610
- const hasVisibleSelection = rows.some((row) => {
2652
+ }));
2653
+ const hasVisibleSelection = isAllMatchingSelected || rows.some((row) => {
2611
2654
  var _a2;
2612
2655
  return selectedIdSet.has(String(row[(_a2 = meta == null ? void 0 : meta.pk_field) != null ? _a2 : "id"]));
2613
2656
  });
2657
+ const hasSelection = isAllMatchingSelected || selectedIds.length > 0;
2658
+ const selectionCount = isAllMatchingSelected ? total : selectedIds.length;
2614
2659
  useEffect9(() => {
2615
2660
  dataRef.current = data;
2616
2661
  }, [data]);
@@ -2630,6 +2675,10 @@ function useModelPageController({
2630
2675
  setPageInput(String(nextPage));
2631
2676
  setAppliedFilters(nextFilters);
2632
2677
  }, [locationSearch]);
2678
+ const clearSelection = useCallback4(() => {
2679
+ setSelectedIds([]);
2680
+ setIsAllMatchingSelected(false);
2681
+ }, []);
2633
2682
  const loadItems = useCallback4(async () => {
2634
2683
  var _a2;
2635
2684
  const activeRequestId = requestIdRef.current + 1;
@@ -2646,7 +2695,9 @@ function useModelPageController({
2646
2695
  const cachedResponse = (_a2 = getClientCacheBucket(client).listResponseCache.get(requestKey)) != null ? _a2 : null;
2647
2696
  if (cachedResponse) {
2648
2697
  setData(cachedResponse);
2649
- setSelectedIds([]);
2698
+ if (!isAllMatchingSelected) {
2699
+ setSelectedIds([]);
2700
+ }
2650
2701
  setIsLoading(false);
2651
2702
  return;
2652
2703
  }
@@ -2661,7 +2712,9 @@ function useModelPageController({
2661
2712
  }
2662
2713
  pageSizeRef.current = response.meta.page_size;
2663
2714
  setData(response);
2664
- setSelectedIds([]);
2715
+ if (!isAllMatchingSelected) {
2716
+ setSelectedIds([]);
2717
+ }
2665
2718
  } catch (reason) {
2666
2719
  setError(reason instanceof Error ? reason.message : t("model_load_error"));
2667
2720
  } finally {
@@ -2669,7 +2722,7 @@ function useModelPageController({
2669
2722
  setIsLoading(false);
2670
2723
  }
2671
2724
  }
2672
- }, [appliedFilters, appliedQuery, client, currentPage, slug, sortValue, t]);
2725
+ }, [appliedFilters, appliedQuery, client, currentPage, isAllMatchingSelected, slug, sortValue, t]);
2673
2726
  useEffect9(() => {
2674
2727
  void loadItems();
2675
2728
  }, [loadItems]);
@@ -2684,11 +2737,12 @@ function useModelPageController({
2684
2737
  if (nextQuery === appliedQuery) {
2685
2738
  return;
2686
2739
  }
2740
+ clearSelection();
2687
2741
  setAppliedQuery(nextQuery);
2688
2742
  setCurrentPage(1);
2689
2743
  setPageInput("1");
2690
2744
  replaceLocation(nextQuery, sortValue, 1, appliedFilters);
2691
- }, [appliedFilters, appliedQuery, replaceLocation, sortValue]);
2745
+ }, [appliedFilters, appliedQuery, clearSelection, replaceLocation, sortValue]);
2692
2746
  const handlePageChange = useCallback4((nextPage) => {
2693
2747
  const safePage = Math.min(Math.max(nextPage, 1), totalPages);
2694
2748
  setCurrentPage(safePage);
@@ -2723,22 +2777,26 @@ function useModelPageController({
2723
2777
  if (!value) {
2724
2778
  delete nextFilters[filterSlug];
2725
2779
  }
2780
+ clearSelection();
2726
2781
  setAppliedFilters(nextFilters);
2727
2782
  setCurrentPage(1);
2728
2783
  setPageInput("1");
2729
2784
  replaceLocation(appliedQuery, sortValue, 1, nextFilters);
2730
- }, [appliedFilters, appliedQuery, replaceLocation, sortValue]);
2785
+ }, [appliedFilters, appliedQuery, clearSelection, replaceLocation, sortValue]);
2731
2786
  const handleResetFilters = useCallback4(() => {
2732
2787
  if (Object.keys(appliedFilters).length === 0) {
2733
2788
  return;
2734
2789
  }
2790
+ clearSelection();
2735
2791
  setAppliedFilters({});
2736
2792
  setCurrentPage(1);
2737
2793
  setPageInput("1");
2738
2794
  replaceLocation(appliedQuery, sortValue, 1, {});
2739
- }, [appliedFilters, appliedQuery, replaceLocation, sortValue]);
2795
+ }, [appliedFilters, appliedQuery, clearSelection, replaceLocation, sortValue]);
2740
2796
  const openSingleDeletePreview = useCallback4(async (rowId) => {
2741
2797
  setPendingDeleteIds([rowId]);
2798
+ setPendingDeleteSelectAll(false);
2799
+ setPendingDeleteScope(null);
2742
2800
  setPendingDeleteMode("single");
2743
2801
  setDeletePreviewOpen(true);
2744
2802
  setDeletePreview(null);
@@ -2754,17 +2812,23 @@ function useModelPageController({
2754
2812
  }
2755
2813
  }, [client, slug, t]);
2756
2814
  const openBulkDeletePreview = useCallback4(async () => {
2757
- if (selectedIds.length === 0) {
2815
+ if (!hasSelection) {
2758
2816
  return;
2759
2817
  }
2760
- setPendingDeleteIds(selectedIds);
2818
+ setPendingDeleteIds(isAllMatchingSelected ? [] : selectedIds);
2819
+ setPendingDeleteSelectAll(isAllMatchingSelected);
2820
+ setPendingDeleteScope(isAllMatchingSelected ? selectionScope : null);
2761
2821
  setPendingDeleteMode("bulk");
2762
2822
  setDeletePreviewOpen(true);
2763
2823
  setDeletePreview(null);
2764
2824
  setDeletePreviewError(null);
2765
2825
  setIsDeletePreviewLoading(true);
2766
2826
  try {
2767
- const preview = await client.getBulkDeletePreview(slug, selectedIds);
2827
+ const preview = await client.getBulkDeletePreview(
2828
+ slug,
2829
+ isAllMatchingSelected ? [] : selectedIds,
2830
+ isAllMatchingSelected ? { selectAll: true, selectionScope } : void 0
2831
+ );
2768
2832
  setDeletePreview(preview);
2769
2833
  } catch (reason) {
2770
2834
  setDeletePreviewError(reason instanceof Error ? reason.message : t("delete_preview_error"));
@@ -2772,9 +2836,12 @@ function useModelPageController({
2772
2836
  setIsDeletePreviewLoading(false);
2773
2837
  setBulkActionMenuAnchor(null);
2774
2838
  }
2775
- }, [client, selectedIds, slug, t]);
2839
+ }, [client, hasSelection, isAllMatchingSelected, selectedIds, selectionScope, slug, t]);
2776
2840
  const handleConfirmDelete = useCallback4(async () => {
2777
- if (pendingDeleteIds.length === 0) {
2841
+ if (pendingDeleteMode === "single" && pendingDeleteIds.length === 0) {
2842
+ return;
2843
+ }
2844
+ if (pendingDeleteMode === "bulk" && pendingDeleteIds.length === 0 && !pendingDeleteSelectAll) {
2778
2845
  return;
2779
2846
  }
2780
2847
  setIsDeleteSubmitting(true);
@@ -2783,21 +2850,28 @@ function useModelPageController({
2783
2850
  if (pendingDeleteMode === "single") {
2784
2851
  await client.deleteItem(slug, pendingDeleteIds[0]);
2785
2852
  } else {
2786
- await client.bulkDelete(slug, pendingDeleteIds);
2853
+ await client.bulkDelete(
2854
+ slug,
2855
+ pendingDeleteIds,
2856
+ pendingDeleteSelectAll && pendingDeleteScope ? { selectAll: true, selectionScope: pendingDeleteScope } : void 0
2857
+ );
2787
2858
  }
2788
2859
  setDeletePreviewOpen(false);
2789
2860
  setDeletePreview(null);
2790
2861
  setDeletePreviewError(null);
2791
2862
  setPendingDeleteIds([]);
2863
+ setPendingDeleteSelectAll(false);
2864
+ setPendingDeleteScope(null);
2865
+ clearSelection();
2792
2866
  await refresh();
2793
2867
  } catch (reason) {
2794
2868
  setDeletePreviewError(reason instanceof Error ? reason.message : t("object_delete_error"));
2795
2869
  } finally {
2796
2870
  setIsDeleteSubmitting(false);
2797
2871
  }
2798
- }, [client, pendingDeleteIds, pendingDeleteMode, refresh, slug, t]);
2872
+ }, [clearSelection, client, pendingDeleteIds, pendingDeleteMode, pendingDeleteScope, pendingDeleteSelectAll, refresh, slug, t]);
2799
2873
  const handleRunNamedBulkAction = useCallback4(async (actionSlug) => {
2800
- if (!actionSlug || selectedIds.length === 0) {
2874
+ if (!actionSlug || !hasSelection) {
2801
2875
  return;
2802
2876
  }
2803
2877
  if (actionSlug === "delete") {
@@ -2805,19 +2879,40 @@ function useModelPageController({
2805
2879
  return;
2806
2880
  }
2807
2881
  try {
2808
- await client.runBulkAction(slug, actionSlug, selectedIds);
2882
+ await client.runBulkAction(
2883
+ slug,
2884
+ actionSlug,
2885
+ isAllMatchingSelected ? [] : selectedIds,
2886
+ void 0,
2887
+ isAllMatchingSelected ? { selectAll: true, selectionScope } : void 0
2888
+ );
2809
2889
  setBulkActionMenuAnchor(null);
2890
+ clearSelection();
2810
2891
  await refresh();
2811
2892
  } catch (reason) {
2812
2893
  setError(reason instanceof Error ? reason.message : t("object_action_error"));
2813
2894
  }
2814
- }, [client, openBulkDeletePreview, refresh, selectedIds, slug, t]);
2895
+ }, [clearSelection, client, hasSelection, isAllMatchingSelected, openBulkDeletePreview, refresh, selectedIds, selectionScope, slug, t]);
2815
2896
  const handleRowDelete = useCallback4(async (rowId) => {
2816
2897
  setRowActionMenuAnchor(null);
2817
2898
  setRowActionMenuId(null);
2818
2899
  await openSingleDeletePreview(rowId);
2819
2900
  }, [openSingleDeletePreview]);
2820
2901
  const handleToggleSelection = useCallback4((rowId, checked) => {
2902
+ if (isAllMatchingSelected) {
2903
+ if (checked) {
2904
+ return;
2905
+ }
2906
+ setIsAllMatchingSelected(false);
2907
+ if (!meta) {
2908
+ setSelectedIds([]);
2909
+ return;
2910
+ }
2911
+ setSelectedIds(
2912
+ rows.map((row) => row[meta.pk_field]).filter((item) => String(item) !== String(rowId))
2913
+ );
2914
+ return;
2915
+ }
2821
2916
  setSelectedIds((current) => {
2822
2917
  const rowKey = String(rowId);
2823
2918
  if (checked) {
@@ -2828,27 +2923,54 @@ function useModelPageController({
2828
2923
  }
2829
2924
  return current.filter((item) => String(item) !== rowKey);
2830
2925
  });
2831
- }, []);
2926
+ }, [isAllMatchingSelected, meta, rows]);
2832
2927
  const handleToggleAllVisible = useCallback4((checked) => {
2833
2928
  if (!meta) {
2834
2929
  return;
2835
2930
  }
2836
2931
  if (checked) {
2932
+ setIsAllMatchingSelected(false);
2837
2933
  setSelectedIds(rows.map((row) => row[meta.pk_field]));
2838
2934
  return;
2839
2935
  }
2936
+ clearSelection();
2937
+ }, [clearSelection, meta, rows]);
2938
+ const handleSelectAllMatching = useCallback4(() => {
2939
+ if (!hasSelection || total === 0) {
2940
+ return;
2941
+ }
2840
2942
  setSelectedIds([]);
2841
- }, [meta, rows]);
2943
+ setIsAllMatchingSelected(true);
2944
+ }, [hasSelection, total]);
2945
+ const clearBulkDeleteState = useCallback4(() => {
2946
+ setPendingDeleteIds([]);
2947
+ setPendingDeleteSelectAll(false);
2948
+ setPendingDeleteScope(null);
2949
+ }, []);
2950
+ const handleClearDeletePreview = useCallback4(() => {
2951
+ setDeletePreviewOpen(false);
2952
+ setDeletePreview(null);
2953
+ setDeletePreviewError(null);
2954
+ clearBulkDeleteState();
2955
+ }, [clearBulkDeleteState]);
2842
2956
  const handleOpenRowMenu = useCallback4((event, rowId) => {
2843
2957
  setRowActionMenuAnchor(event.currentTarget);
2844
2958
  setRowActionMenuId(rowId);
2845
2959
  }, []);
2960
+ const handleCloseRowMenu = useCallback4(() => {
2961
+ setRowActionMenuAnchor(null);
2962
+ setRowActionMenuId(null);
2963
+ }, []);
2964
+ const handleCloseBulkActionMenu = useCallback4(() => {
2965
+ setBulkActionMenuAnchor(null);
2966
+ }, []);
2846
2967
  return {
2847
2968
  allVisibleSelected,
2848
2969
  appliedFilters,
2849
2970
  appliedQuery,
2850
2971
  bulkActionMenuAnchor,
2851
2972
  bulkActions,
2973
+ clearBulkDeleteState,
2852
2974
  createOpen,
2853
2975
  currentPage,
2854
2976
  data,
@@ -2858,6 +2980,9 @@ function useModelPageController({
2858
2980
  error,
2859
2981
  fieldMap,
2860
2982
  filtersOpen,
2983
+ handleClearDeletePreview,
2984
+ handleCloseBulkActionMenu,
2985
+ handleCloseRowMenu,
2861
2986
  handleConfirmDelete,
2862
2987
  handleFilterChange,
2863
2988
  handleOpenRowMenu,
@@ -2867,10 +2992,13 @@ function useModelPageController({
2867
2992
  handleRowDelete,
2868
2993
  handleRunNamedBulkAction,
2869
2994
  handleSearchCommit,
2995
+ handleSelectAllMatching,
2870
2996
  handleToggleAllVisible,
2871
2997
  handleToggleSelection,
2872
2998
  hasListFilters,
2999
+ hasSelection,
2873
3000
  hasVisibleSelection,
3001
+ isAllMatchingSelected,
2874
3002
  isDeletePreviewLoading,
2875
3003
  isDeleteSubmitting,
2876
3004
  isLoading,
@@ -2884,6 +3012,7 @@ function useModelPageController({
2884
3012
  rowActionMenuAnchor,
2885
3013
  rowActionMenuId,
2886
3014
  rows,
3015
+ selectionCount,
2887
3016
  selectedIdSet,
2888
3017
  selectedIds,
2889
3018
  setBulkActionMenuAnchor,
@@ -2893,9 +3022,7 @@ function useModelPageController({
2893
3022
  setDeletePreviewOpen,
2894
3023
  setFiltersOpen,
2895
3024
  setPageInput,
2896
- setPendingDeleteIds,
2897
- setRowActionMenuAnchor,
2898
- setRowActionMenuId,
3025
+ sortValue,
2899
3026
  sortFields,
2900
3027
  toggleSort,
2901
3028
  total,
@@ -2971,10 +3098,11 @@ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
2971
3098
  var SEARCH_DEBOUNCE_MS = 300;
2972
3099
  var CHECKBOX_COLUMN_WIDTH = 56;
2973
3100
  var ACTIONS_COLUMN_WIDTH = 56;
2974
- function ModelPage({ client, basePath, slug, router }) {
3101
+ function ModelPage({ client, basePath, slug, router, renderBeforePagination }) {
2975
3102
  var _a, _b;
2976
3103
  const locale = useAdminLocale();
2977
3104
  const t = useAdminTranslation();
3105
+ const selectAllLabel = locale === "ru" ? "\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435" : "Select All";
2978
3106
  const resolvedRouter = useXLAdminRouter(router);
2979
3107
  const location = useXLAdminLocation(resolvedRouter);
2980
3108
  const theme = useTheme2();
@@ -2995,6 +3123,22 @@ function ModelPage({ client, basePath, slug, router }) {
2995
3123
  return /* @__PURE__ */ jsx19(ModelPageSkeleton, {});
2996
3124
  }
2997
3125
  const meta = controller.meta;
3126
+ const beforePagination = renderBeforePagination == null ? void 0 : renderBeforePagination({
3127
+ client,
3128
+ slug,
3129
+ meta: {
3130
+ slug: meta.slug,
3131
+ title: meta.title
3132
+ },
3133
+ selectedIds: controller.selectedIds,
3134
+ isAllMatchingSelected: controller.isAllMatchingSelected,
3135
+ selectionCount: controller.selectionCount,
3136
+ total: controller.total,
3137
+ appliedQuery: controller.appliedQuery,
3138
+ sortValue: controller.sortValue,
3139
+ appliedFilters: controller.appliedFilters,
3140
+ refresh: controller.refresh
3141
+ });
2998
3142
  return /* @__PURE__ */ jsx19(XLAdminRouterProvider, { router: resolvedRouter, children: /* @__PURE__ */ jsxs12(Stack9, { spacing: 1.5, sx: { height: "100%", minHeight: 0 }, children: [
2999
3143
  /* @__PURE__ */ jsx19(
3000
3144
  MainHeader,
@@ -3006,8 +3150,8 @@ function ModelPage({ client, basePath, slug, router }) {
3006
3150
  "aria-label": t("create"),
3007
3151
  onClick: () => controller.setCreateOpen(true),
3008
3152
  sx: {
3009
- width: 30,
3010
- height: 30,
3153
+ width: 29,
3154
+ height: 29,
3011
3155
  p: 0.5,
3012
3156
  backgroundColor: "primary.main",
3013
3157
  color: "primary.contrastText",
@@ -3031,28 +3175,53 @@ function ModelPage({ client, basePath, slug, router }) {
3031
3175
  flexShrink: 0
3032
3176
  },
3033
3177
  children: /* @__PURE__ */ jsxs12(Stack9, { direction: { xs: "column", lg: "row" }, spacing: 1.5, alignItems: { lg: "center" }, children: [
3034
- /* @__PURE__ */ jsx19(Stack9, { direction: { xs: "column", xl: "row" }, spacing: 1.5, sx: { flex: 1, minWidth: 0 }, children: /* @__PURE__ */ jsx19(
3035
- SearchField,
3178
+ /* @__PURE__ */ jsxs12(
3179
+ Stack9,
3036
3180
  {
3037
- value: controller.appliedQuery,
3038
- onCommit: controller.handleSearchCommit,
3039
- debounceMs: SEARCH_DEBOUNCE_MS,
3040
- placeholder: t("search")
3181
+ direction: { xs: "column", xl: "row" },
3182
+ spacing: 1.5,
3183
+ alignItems: { xl: "center" },
3184
+ sx: { flex: 1, minWidth: 0 },
3185
+ children: [
3186
+ /* @__PURE__ */ jsx19(
3187
+ SearchField,
3188
+ {
3189
+ value: controller.appliedQuery,
3190
+ onCommit: controller.handleSearchCommit,
3191
+ debounceMs: SEARCH_DEBOUNCE_MS,
3192
+ placeholder: t("search")
3193
+ }
3194
+ ),
3195
+ controller.hasSelection ? /* @__PURE__ */ jsxs12(Stack9, { direction: "row", spacing: 1, alignItems: "center", sx: { minWidth: 0, flexWrap: "wrap" }, children: [
3196
+ /* @__PURE__ */ jsx19(
3197
+ Button4,
3198
+ {
3199
+ variant: "outlined",
3200
+ endIcon: /* @__PURE__ */ jsx19(ExpandMoreIcon4, {}),
3201
+ onClick: (event) => controller.setBulkActionMenuAnchor(event.currentTarget),
3202
+ children: t("actions")
3203
+ }
3204
+ ),
3205
+ !controller.isAllMatchingSelected && controller.selectionCount < controller.total ? /* @__PURE__ */ jsx19(
3206
+ Button4,
3207
+ {
3208
+ variant: "text",
3209
+ onClick: controller.handleSelectAllMatching,
3210
+ sx: { px: 0.5, minWidth: "auto" },
3211
+ children: selectAllLabel
3212
+ }
3213
+ ) : null,
3214
+ /* @__PURE__ */ jsxs12(Typography5, { color: "text.secondary", sx: { alignSelf: "center", whiteSpace: "nowrap" }, children: [
3215
+ t("selected_count", { count: controller.selectionCount }),
3216
+ " / ",
3217
+ controller.total
3218
+ ] })
3219
+ ] }) : null
3220
+ ]
3041
3221
  }
3042
- ) }),
3043
- controller.selectedIds.length > 0 ? /* @__PURE__ */ jsxs12(Stack9, { direction: "row", spacing: 1, sx: { flex: { lg: 1 }, minWidth: 0 }, children: [
3044
- /* @__PURE__ */ jsx19(
3045
- Button4,
3046
- {
3047
- variant: "outlined",
3048
- endIcon: /* @__PURE__ */ jsx19(ExpandMoreIcon4, {}),
3049
- onClick: (event) => controller.setBulkActionMenuAnchor(event.currentTarget),
3050
- children: t("actions")
3051
- }
3052
- ),
3053
- /* @__PURE__ */ jsx19(Typography5, { color: "text.secondary", sx: { alignSelf: "center" }, children: t("selected_count", { count: controller.selectedIds.length }) })
3054
- ] }) : null,
3222
+ ),
3055
3223
  /* @__PURE__ */ jsxs12(Stack9, { direction: "row", spacing: 1, alignItems: "center", sx: { marginLeft: "auto" }, children: [
3224
+ beforePagination ? /* @__PURE__ */ jsx19(Box9, { sx: { display: "flex", alignItems: "center", flexShrink: 0 }, children: beforePagination }) : null,
3056
3225
  isMobile && controller.hasListFilters ? /* @__PURE__ */ jsx19(Tooltip, { title: t("filters"), children: /* @__PURE__ */ jsx19(
3057
3226
  IconButton3,
3058
3227
  {
@@ -3257,7 +3426,7 @@ function ModelPage({ client, basePath, slug, router }) {
3257
3426
  {
3258
3427
  anchorEl: controller.bulkActionMenuAnchor,
3259
3428
  open: Boolean(controller.bulkActionMenuAnchor),
3260
- onClose: () => controller.setBulkActionMenuAnchor(null),
3429
+ onClose: controller.handleCloseBulkActionMenu,
3261
3430
  children: controller.bulkActions.map((action) => /* @__PURE__ */ jsx19(
3262
3431
  MenuItem2,
3263
3432
  {
@@ -3273,10 +3442,7 @@ function ModelPage({ client, basePath, slug, router }) {
3273
3442
  {
3274
3443
  anchorEl: controller.rowActionMenuAnchor,
3275
3444
  open: Boolean(controller.rowActionMenuAnchor),
3276
- onClose: () => {
3277
- controller.setRowActionMenuAnchor(null);
3278
- controller.setRowActionMenuId(null);
3279
- },
3445
+ onClose: controller.handleCloseRowMenu,
3280
3446
  children: /* @__PURE__ */ jsx19(
3281
3447
  MenuItem2,
3282
3448
  {
@@ -3353,10 +3519,7 @@ function ModelPage({ client, basePath, slug, router }) {
3353
3519
  if (controller.isDeleteSubmitting) {
3354
3520
  return;
3355
3521
  }
3356
- controller.setDeletePreviewOpen(false);
3357
- controller.setDeletePreview(null);
3358
- controller.setDeletePreviewError(null);
3359
- controller.setPendingDeleteIds([]);
3522
+ controller.handleClearDeletePreview();
3360
3523
  },
3361
3524
  onConfirm: () => void controller.handleConfirmDelete()
3362
3525
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xladmin",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Framework-agnostic React + MUI admin frontend for xladmin.",
5
5
  "license": "MIT",
6
6
  "type": "module",