npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

storage-apitables

v0.2.0

Published

Server-driven React data tables with per-table Redux, filters, sorting, row/bulk actions, and host-injected UI. npm: https://www.npmjs.com/package/storage-apitables

Readme

storage-apitables

npm version

Server-driven data tables for React — UI, Redux state, filters, sorting, row actions, bulk actions, and custom-control modals.

The backend defines columns, filters, sorting, and actions as JSON. This package renders them with a dedicated Redux store per table instance. You inject your axios client, design-system components, and feature-specific modals via ApiTablesHostProvider.

Author

Mohamed Hasan@mhmdhasan

Frontend documentation and npm package by Mohamed Hasan while at Storage IT Solutions. ApiTables is a broader team effort; this package covers the UI module and its frontend logic only.

What you get

| Layer | Included | | --- | --- | | Entry | ReactApiTable, ApiTablesController, ApiTablesComponent | | State | Per-table Redux (tableCore, tableColumns, bulkActions, rowActions) | | Data | useTableStructure, useTableFetcher, useUtilsProvider | | UI | Toolbar, inline or sheet filters, sorting, pagination, column visibility, row/bulk actions | | Modals | Confirm/view row data, static column viewers, custom-control router |

Not included: backend APIs, database schema, or app-specific custom-control forms — those stay in your host app and register through config.

Install

pnpm add storage-apitables
pnpm add react react-dom react-redux @reduxjs/toolkit axios react-data-table-component
pnpm add sonner   # optional — used for toasts when configured

Mental model

  1. StructureuseTableStructure GETs /api-table/control-tables/load-table/{name} (columns, filters, actions, filter layout)
  2. StateApiTablesProvider creates one isolated Redux store per table
  3. Filters — inline layout maps filters into column headers; sheet layout uses a side panel; unmapped filters use an overflow popover
  4. QueryuseTableFetcher POSTs rich filter objects, sorts, pagination, and optional route params
  5. ActionsuseUtilsProvider handles row/bulk POSTs and refresh rules

The reference implementation lives in bolesa-app-ui at app/ApiTables/. Sync source changes into this package with scripts/sync-from-app.sh before publishing.

Quick start

1. Bootstrap the host config (once per app)

Call setExternalStoreGetter at app startup if you use global Redux bridges:

// app/providers.tsx or similar
import { setExternalStoreGetter } from 'storage-apitables';
import { store } from '@/store/store';

setExternalStoreGetter(() => store);

2. Create a shared host provider

Centralize config so every table page does not repeat the component map:

// app/ApiTablesHost.tsx
'use client';

import { useSelector } from 'react-redux';
import { ApiTablesHostProvider, type ApiTablesConfig } from 'storage-apitables';
import { axiosTable } from '@/lib/axios';
import { store } from '@/store/store';
import { Link, usePathname } from '@/i18n/navigation';
import { useLocale, useTranslations } from 'next-intl';
import useSecureLS from '@/hooks/useSecureLS';
import Popup from '@/components/Popup';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
// …import every primitive listed in "Required components" below

import { EditProduct } from '@/features/tables/custom-controls/EditProduct';
import { AddToCreditBulkModal } from '@/features/tables/custom-controls/AddToCreditBulkModal';

const config: ApiTablesConfig = {
  axiosTable,
  getExternalStore: () => store,
  useOutScopeRefresher: () => useSelector((s: { app: { outScopeTableRefresher: unknown } }) => s.app.outScopeTableRefresher),
  useLocale,
  useTranslations,
  usePathname,
  useSecureLS,
  Link,
  components: {
    Button,
    Card,
    Badge,
    Popup,
    // …full map — see "Required components"
  },
  customControls: [
    { match: '/cl/products', component: EditProduct },
    { match: '/cl/categories', component: EditCategory },
  ],
  bulkActionModals: [
    { actionKey: 'create_withdrawal_bulk', component: AddToCreditBulkModal },
  ],
  onBulkActionSuccess: (action, response) => {
    // e.g. update cart count after bulk reload on specific routes
  },
  setMainLoader: ({ status, msg, icon }) => {
    store.dispatch(_setMainLoader({ status, msg, icon }));
  },
};

export function ApiTablesHost({ children }: { children: React.ReactNode }) {
  return <ApiTablesHostProvider config={config}>{children}</ApiTablesHostProvider>;
}

Wrap your layout or individual pages:

<ApiTablesHost>
  {children}
</ApiTablesHost>

3. Add a table page

'use client';

import { useEffect } from 'react';
import { ReactApiTable, useTableStructure } from 'storage-apitables';

export default function OrdersTablePage() {
  const { getTableStructure, tableStructure, tableStructureLoading } = useTableStructure();

  useEffect(() => {
    getTableStructure({
      table: '/api-table/control-tables/load-table/orders',
    });
  }, [getTableStructure]);

  return (
    <ReactApiTable
      table={tableStructure}
      tableStructureLoading={tableStructureLoading}
    />
  );
}

useTableStructure must run inside ApiTablesHostProvider when the table mounts — either wrap the page or a parent layout.

ApiTablesConfig reference

| Field | Required | Purpose | | --- | --- | --- | | axiosTable | Yes | GET structure, POST query-table, row/bulk actions | | components | Yes | Design-system primitives (see below) | | getExternalStore | Recommended | Global Redux for getExternalState() bridges | | useOutScopeRefresher | Recommended | Remounts per-table Redux when global state changes | | useLocale | Recommended | Sends ln header on structure requests | | usePathname | Recommended | Re-fetches rows when route changes | | useTranslations | Recommended | Filter labels and UI copy | | useSecureLS | Optional | Persists page size and column visibility | | Link | If using redirect actions | i18n-aware link for redirect row actions | | customControls | If using OpenModalForm | URL-matched modal forms | | bulkActionModals | If using custom bulk modals | Matched by action_key | | rowActionModals | Optional | Extra response modals (e.g. announcement preview) | | customElements | Optional | String-keyed slots (withdrawal_requests, …) | | axiosAuth | Optional | 401 logout endpoint | | onUnauthorized | Optional | Custom 401 handler | | onBulkActionSuccess | Optional | Cart count, route-specific side effects | | setMainLoader | Optional | Global loading overlay during bulk POST | | toast | Optional | { success, error } — otherwise silent | | downloadBlob / downloadPDF / generatePDF | Optional | Export and invoice actions |

Required components map

Map your design system into config.components. Keys are case-sensitive and must match these names:

| Key | Used in | | --- | --- | | Button | Toolbar, filters, actions, modals | | Badge | Sort chips, applied filters, cells | | Card | Loading shell | | Skeleton | Table loaders | | Popup | All modals | | FullPageTableLoader | Structure loading state | | Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue | Filters, page size | | Input, InputWrapper | Filter fields | | Label | Filter form labels | | Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger | Filter panel | | Tooltip, TooltipContent, TooltipTrigger | Filter hints | | Popover, PopoverContent, PopoverTrigger | Row actions overflow, column visibility | | DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger | Bulk actions | | Checkbox | Row selection, column visibility | | Switch | Boolean filters, toggle actions | | Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow | View-row modal | | MultiSelect, MultiSelectContent, MultiSelectGroup, MultiSelectItem, MultiSelectTrigger, MultiSelectValue | Multi-select filters | | DatePicker | Date filters | | TagsInput | Tag filters | | LoadingButton | Async action buttons | | CustomPagination | Pagination bar | | AppCurrency | Currency custom elements |

Missing a key throws at runtime when that UI renders.

Custom controls

Row actions with onSuccess: "OpenModalForm" open a modal after a prefetch POST. Register forms by URL substring — first match wins:

customControls: [
  { match: '/cl/products', component: EditProduct },
  { match: /\/announces\/batches/, component: EditAnnouncements },
],

Control component props:

type CustomControlProps = {
  action: Record<string, unknown>;  // server payload + metadata
  closeModal: () => void;
  rowRefetch?: (row: Record<string, unknown>) => void;
};

Read form defaults from action.payload or legacy action_payload. Submit via useUtilsProvider() from inside the control.

Bulk action modals

When selectedBulkAction.action_key matches, a custom modal renders instead of the default confirmation:

bulkActionModals: [
  { actionKey: 'create_withdrawal_bulk', component: AddToCreditBulkModal },
],

Filter layouts

The backend chooses a default layout; users can switch when allowed.

| Layout | UI | When to use | | --- | --- | --- | | inline (default) | Filters render in a synthetic first row aligned with visible columns | Dense tables where filters map to column data_src | | sheet | Filters live in a side panel (Sheet) opened from the toolbar | Many filters or filters that do not map to columns |

Structure fields (camelCase or snake_case both accepted):

| Field | Default | Purpose | | --- | --- | --- | | filtersLayout / filters_layout | "inline" | Default layout | | filterLayoutSwitchable / filter_layout_switchable | true | Show inline/sheet toggle in toolbar |

When switchable, the user preference is persisted as {tableName}_filter_layout ("inline" or "sheet") via useSecureLS.

Which filters appear where

| Filter | Inline column row | Additional popover | Sheet | | --- | --- | --- | --- | | filter_name matches a visible column data_src | Yes | No | Yes | | Unmapped filters (no matching column) | No | Yes (inline mode) | Yes | | type: "range" | No | No | No | | Legacy created_at date filter | No | No | No | | Paired dates (pair_with, or *_from / *_to) | No | Yes / sheet | Yes |

Align filter_name with column data_src when you want a filter in the inline header row.

Supported filter types

text, select, boolean, null, number, multi_select, date

Filter schema:

{
  "type": "text",
  "filter_name": "sku",
  "label": "SKU",
  "pair_with": false,
  "props": {
    "operators": ["=", "like"],
    "options": {},
    "select_options": { "1": "Active", "0": "Inactive" }
  }
}

Query-table filter payload

Applied filters are not flat key→value. Each active filter is sent as a rich object keyed by filter_name:

{
  "perPage": 25,
  "page": 1,
  "filters": {
    "sku": {
      "key": "sku",
      "value": "ABC-123",
      "label": "SKU",
      "type": "text",
      "operator": "like",
      "valueLable": "ABC-123",
      "props": { "operators": ["=", "like"] }
    }
  },
  "sorts": {
    "label": "Created at",
    "direction": "desc",
    "created_at": "desc"
  },
  "params": { "batch_id": 42 }
}

Conventions:

  • Dates sent to the API use dd-MM-yyyy; ranges use [from, to]
  • Tags with operator in are comma-split into an array
  • params comes from <ReactApiTable params={…} /> — use for route-scoped tables (id, batch_id, …)

Advanced page options

<ReactApiTable
  table={tableStructure}
  tableStructureLoading={tableStructureLoading}
  params={{ warehouse_id: '12' }}           // forwarded to query-table POST
  customElement={<MyToolbarSlot />}          // React node above table
  // customElement="withdrawal_requests"      // or string key → customElements map
  controller={<MyCustomController />}      // replace default ApiTablesController
/>

Exported API

// Host setup
ApiTablesHostProvider, useApiTablesConfig, useApiTablesComponent
setExternalStoreGetter, getExternalState, createTableStore

// Page wiring
ReactApiTable, useTableStructure
// useLoadTableStructure — in bolesa-app-ui only; defers fetch during route transitions

// Lower-level (custom integrations)
ApiTablesProvider, ApiTablesController, ApiTablesComponent
useTableFetcher, useUtilsProvider

// Redux slice actions (advanced)
// tableCoreSlice, tableColumnsSlice, bulkActionsSlice, rowActionsSlice exports

Backend contract

Your API must expose:

| Endpoint | Method | Purpose | | --- | --- | --- | | /api-table/control-tables/load-table/{tableName} | GET | Columns, filters, actions JSON | | /api-table/control-tables/query-table/{tableName} | POST | Rows, pagination, optional bindings |

Row and bulk action URLs come from the structure payload. See the table structure docs for field shapes.

Structure GET

Headers: ln: en (or current locale) when useLocale is configured.

Response (top-level fields consumed by the frontend):

{
  "tableName": "products",
  "columns": [],
  "filters": [],
  "bulkActions": [],
  "rowActions": [],
  "filtersLayout": "inline",
  "filterLayoutSwitchable": true
}

Column schema:

{
  "label": "SKU",
  "data_src": "sku",
  "type": "text",
  "sortable": true,
  "showable": true
}

Column types: text, text_truncate, link, boolean, html, datalist, barcode, actions.

When rowActions includes general_actions, actions are nested per column name; per-row overrides may also appear on each item as row.actions.

Query POST response

{
  "items": [{ "id": "1", "sku": "ABC", "actions": {} }],
  "pagination": {
    "current_page": 1,
    "from": 1,
    "to": 25,
    "total": 100,
    "last_page": 4,
    "per_page": 25,
    "links": [],
    "next_page_url": null,
    "prev_page_url": null
  },
  "bindings": {}
}

bindings is optional aggregate metadata (status counts, summaries) for page-specific widgets — not row data. Stored in Redux as tableBindings.

Action URLs

Backend paths often include a leading /api. The frontend strips /api before calling axiosTable, because the axios instance baseURL already ends with /api:

action.action.api.replace(/^\/api/, '');

Document both the raw backend path and the effective URL your axios client will hit.

Row actions

| action_type | Frontend behavior | | --- | --- | | toggle | Switch control; POST on change | | redirect | Link to redirect_routes.api (requires ApiTablesConfig.Link) | | external_redirect | Opens redirect_routes.api in a new tab | | copy | Copies copy_value to clipboard | | default / custom_control | POST → confirmation, view modal, or custom control |

Bulk POST body:

{
  "filters": { /* same appliedFilters object as query-table */ },
  "selected_ids": ["row-id-1", "row-id-2"]
}

Non-custom row actions typically POST { "selected_ids": [] }; row IDs are usually embedded in the action URL from the backend.

Action success responses

The frontend checks response.data.success (nested under data), not top-level success:

{
  "data": {
    "success": true,
    "message": "Updated",
    "row": {},
    "url": "...",
    "filename": "...",
    "invoice_number": "..."
  },
  "items": [],
  "cart": { "total_items_count": 0 }
}

| onSuccess | Frontend behavior | | --- | --- | | reload / deleteRow | Re-fetch table | | refetchData | Merge response.items into current rows | | refetchRow | Replace single row from response.data.row | | downloadData / download_pdf | Download file | | generatePdf / downloadPdf | Client PDF generation | | OpenModalForm | Open custom control modal with prefetch response | | DisplayOnModal | Show view-row modal |

Excel bulk export: action_key === "export-excel-blob" uses responseType: "blob". Stream downloads: { success, type: "stream", data: { url }, file_name }.

Auth

Cookie session (withCredentials: true in the host axios client). Structure GET sends ln for localized labels. On 401, configure axiosAuth or onUnauthorized.

Client persistence (SecureLS)

| Key | Purpose | | --- | --- | | api_tables_page_size | Global page size (10 / 25 / 50 / 100) | | {tableName}_tb | Visible column keys (JSON array) | | {tableName}_filter_layout | "inline" or "sheet" when layout is switchable |

Troubleshooting

| Symptom | Fix | | --- | --- | | useApiTablesConfig must be used within ApiTablesHostProvider | Wrap page or layout with ApiTablesHostProvider | | components.X is required | Add missing key to config.components | | ApiTablesConfig.Link is required | Provide Link or avoid redirect row actions | | Table never loads rows | Confirm tableName in structure response; check axiosTable base URL | | Inline filter missing under column | Set filter_name equal to column data_src and ensure column is visible | | Filter only in popover, not in header | Filter name does not match a visible column — expected for unmapped filters | | created_at filter ignored | Use created_at_from / created_at_to instead of legacy created_at | | Scoped table returns wrong rows | Pass route context via <ReactApiTable params={{ id }} /> | | Custom control modal empty | Add customControls match for the action URL | | Action succeeds but UI shows error | Return data.success: true in the action response body | | 401 not handled | Set axiosAuth or onUnauthorized | | Filters untranslated | Provide useTranslations (namespace ApiTables in bolesa-app-ui) |

Author & links

| | | | --- | --- | | Author | Mohamed Hasan | | GitHub | github.com/mhmdhasan | | Package | npmjs.com/package/storage-apitables | | Documentation | apitablesdocs.bolesa.net |

License

MIT · © Mohamed Hasan