@agilant/toga-blox 1.0.167 → 1.0.170

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.
@@ -0,0 +1,5 @@
1
+ import { Options } from "../utils/queryHelpers";
2
+ export declare const apiGet: <TData = unknown>(route: string, options?: Options, params?: Record<string, any>) => Promise<TData>;
3
+ export declare const apiPost: <TData = unknown>(route: string, data: Record<string, any>, options?: Options, params?: Record<string, any>) => Promise<TData>;
4
+ export declare const apiPut: <TData = unknown>(route: string, data?: Record<string, any>, options?: Options, params?: Record<string, any>) => Promise<TData>;
5
+ export declare const apiDelete: <TData = unknown>(route: string, data?: Record<string, any>, options?: Options, params?: Record<string, any>) => Promise<TData>;
@@ -0,0 +1,46 @@
1
+ // src/api/apiFunctions.ts
2
+ import { assembleOptions } from "../utils/queryHelpers";
3
+ import axiosInstance from "./axiosInstance";
4
+ export const apiGet = async (route, options = {}, params = {}) => {
5
+ let url = route;
6
+ const optionsString = assembleOptions(options);
7
+ if (optionsString) {
8
+ url += route.includes("?") ? "&" : "?";
9
+ url += optionsString;
10
+ }
11
+ const response = await axiosInstance.get(url, { params });
12
+ return response.data;
13
+ };
14
+ export const apiPost = async (route, data, options = {}, params = {}) => {
15
+ let url = route;
16
+ const optionsString = assembleOptions(options);
17
+ if (optionsString) {
18
+ url += route.includes("?") ? "&" : "?";
19
+ url += optionsString;
20
+ }
21
+ const response = await axiosInstance.post(url, data, { params });
22
+ return response.data;
23
+ };
24
+ export const apiPut = async (route, data = {}, options = {}, params = {}) => {
25
+ let url = route;
26
+ const optionsString = assembleOptions(options);
27
+ if (optionsString) {
28
+ url += route.includes("?") ? "&" : "?";
29
+ url += optionsString;
30
+ }
31
+ const response = await axiosInstance.put(url, data, { params });
32
+ return response.data;
33
+ };
34
+ export const apiDelete = async (route, data = {}, options = {}, params = {}) => {
35
+ let url = route;
36
+ const optionsString = assembleOptions(options);
37
+ if (optionsString) {
38
+ url += route.includes("?") ? "&" : "?";
39
+ url += optionsString;
40
+ }
41
+ const response = await axiosInstance.delete(url, {
42
+ data,
43
+ params,
44
+ });
45
+ return response.data;
46
+ };
@@ -0,0 +1,3 @@
1
+ export declare const UserLoginRequiredEvent: Event;
2
+ declare const axiosInstance: import("axios").AxiosInstance;
3
+ export default axiosInstance;
@@ -0,0 +1,139 @@
1
+ import axios from "axios";
2
+ import * as Sentry from "@sentry/react";
3
+ // Event to indicate when user login is required
4
+ export const UserLoginRequiredEvent = new Event("UserLoginRequired");
5
+ // Assemble the base URL based on environment variables
6
+ const fullHostName = window.location.hostname.toUpperCase();
7
+ const hostName = fullHostName.split(".");
8
+ const host = "VITE_API_" + hostName[0];
9
+ const baseURL = import.meta.env[host] || import.meta.env.VITE_API;
10
+ // Initialize axios instance with base configurations
11
+ const axiosInstance = axios.create({
12
+ baseURL,
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ },
16
+ timeout: 10000, // Optional: Set a timeout for requests
17
+ });
18
+ // Function to fetch a public access token for unauthenticated requests
19
+ const fetchPublicToken = async () => {
20
+ try {
21
+ const response = await axios.post(`${baseURL}/auth/public`, null, {
22
+ headers: { "Content-Type": "application/json" },
23
+ });
24
+ localStorage.setItem("accessToken", response.data.data.tokens.access);
25
+ localStorage.setItem("refreshToken", response.data.data.tokens.refresh);
26
+ return response.data.data.tokens.access;
27
+ }
28
+ catch (error) {
29
+ console.error("Failed to fetch public token:", error);
30
+ Sentry.captureMessage("Failed to fetch public token Error", {
31
+ level: "error",
32
+ tags: {
33
+ errorType: "API",
34
+ authentication: "failed",
35
+ },
36
+ extra: {
37
+ message: "Failed to fetch public token",
38
+ details: error instanceof Error ? error.message : error,
39
+ },
40
+ });
41
+ }
42
+ return null;
43
+ };
44
+ // Function to refresh the access token using the refresh token
45
+ const refreshAccessToken = async () => {
46
+ const refreshToken = localStorage.getItem("refreshToken");
47
+ if (refreshToken) {
48
+ try {
49
+ const response = await axios.post(`${baseURL}/auth/refresh`, null, {
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ Authorization: `Bearer ${refreshToken}`,
53
+ },
54
+ });
55
+ if (response.data?.data?.tokens?.access) {
56
+ localStorage.setItem("accessToken", response.data.data.tokens.access);
57
+ return response.data.data.tokens.access;
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.error("Failed to refresh token:", error);
62
+ Sentry.captureMessage("Failed to fetch refresh token Error", {
63
+ level: "error",
64
+ tags: {
65
+ errorType: "API",
66
+ authentication: "failed",
67
+ },
68
+ extra: {
69
+ message: "Failed to fetch refresh token",
70
+ details: error instanceof Error ? error.message : error,
71
+ },
72
+ });
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+ // Request interceptor to add Authorization header
78
+ axiosInstance.interceptors.request.use(async (config) => {
79
+ let token;
80
+ const userString = localStorage.getItem("user");
81
+ if (userString) {
82
+ // If 'user' exists in localStorage, use the 'accessToken'
83
+ token = localStorage.getItem("accessToken");
84
+ }
85
+ else {
86
+ // If no 'user' exists, fetch the public token
87
+ token = await fetchPublicToken();
88
+ }
89
+ if (token) {
90
+ config.headers["Authorization"] = `Bearer ${token}`;
91
+ }
92
+ return config;
93
+ }, (error) => {
94
+ Sentry.captureMessage("Axios Instance request error", {
95
+ level: "error",
96
+ tags: {
97
+ errorType: "API",
98
+ authentication: "failed",
99
+ },
100
+ extra: {
101
+ message: "Axios Instance request error",
102
+ details: error instanceof Error ? error.message : error,
103
+ },
104
+ });
105
+ return Promise.reject(error);
106
+ });
107
+ // Response interceptor to handle 401 errors
108
+ axiosInstance.interceptors.response.use((response) => response, async (error) => {
109
+ const originalRequest = error.config;
110
+ // Handle 401 Unauthorized errors
111
+ if (error.response?.status === 401 && !originalRequest._retry) {
112
+ originalRequest._retry = true;
113
+ // Attempt to refresh the access token
114
+ const newToken = await refreshAccessToken();
115
+ if (newToken) {
116
+ // Set the new token in the headers and retry the request
117
+ originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
118
+ return axiosInstance(originalRequest);
119
+ }
120
+ else {
121
+ // If refresh fails, trigger login required event
122
+ window.dispatchEvent(UserLoginRequiredEvent);
123
+ }
124
+ }
125
+ Sentry.captureMessage("Axios Instance response error", {
126
+ level: "error",
127
+ tags: {
128
+ errorType: "API",
129
+ authentication: "failed",
130
+ },
131
+ extra: {
132
+ message: "Axios Instance response error",
133
+ status: error.response?.status,
134
+ details: error instanceof Error ? error.message : error,
135
+ },
136
+ });
137
+ return Promise.reject(error);
138
+ });
139
+ export default axiosInstance;
File without changes
File without changes
@@ -1,3 +1,12 @@
1
- import { DataWithUUID, TableCellProps } from "./types";
2
- declare const TableCell: <T extends DataWithUUID>({ cell, isLastCell, hasInfiniteScroll, linkText, }: TableCellProps<T>) => import("react/jsx-runtime").JSX.Element;
1
+ import { DataWithUUID } from "./types";
2
+ export interface TableCellProps<T extends DataWithUUID> {
3
+ cell: any;
4
+ isLastCell: boolean;
5
+ hasInfiniteScroll: boolean;
6
+ rowUuid?: string;
7
+ expanded: boolean;
8
+ onToggle: () => void;
9
+ linkText?: string;
10
+ }
11
+ declare const TableCell: <T extends DataWithUUID>({ cell, isLastCell, hasInfiniteScroll, expanded, onToggle, linkText, }: TableCellProps<T>) => import("react/jsx-runtime").JSX.Element;
3
12
  export default TableCell;
@@ -1,29 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // src/components/TableCell/TableCell.tsx
3
- import { useState } from "react";
4
2
  const MAX_CHARS = 30;
5
- const TableCell = ({ cell, isLastCell, hasInfiniteScroll, linkText = "text-purple-500 ml-1 underline", }) => {
6
- const [expanded, setExpanded] = useState(false);
7
- /* ---------- compute toggle state ---------- */
3
+ const TableCell = ({ cell, isLastCell, hasInfiniteScroll, expanded, onToggle, linkText = "text-purple-500 ml-1 underline", }) => {
4
+ /* decide if this cell even needs a toggle */
8
5
  const isString = typeof cell.value === "string";
9
6
  const fullText = isString ? cell.value : "";
10
7
  const isLong = isString && fullText.length > MAX_CHARS;
11
8
  const hasToggle = hasInfiniteScroll && isLong;
12
9
  const truncated = fullText.slice(0, MAX_CHARS);
13
- /* ---------- click only when toggling ---------- */
10
+ /* stop row click when toggling */
14
11
  const handleToggle = (e) => {
15
- e.stopPropagation();
16
- setExpanded((p) => !p);
12
+ e.stopPropagation(); // ← prevents bubbling to the <tr>
13
+ onToggle();
17
14
  };
18
- /* ---------- classes ---------- */
19
15
  const baseTd = "relative overflow-hidden font-light text-sm text-left px-5";
20
16
  const wrapper = "min-h-[40px] flex select-none" + (hasToggle ? " cursor-pointer" : "");
21
17
  return (_jsx("td", { ...cell.getCellProps(), className: isLastCell ? "" : baseTd, children: _jsx("div", { ...(hasToggle ? { onClick: handleToggle } : {}), className: wrapper, children: hasToggle ? (!expanded ? (
22
- /* collapsed: inline with ellipsis + hover‑only link */
23
- _jsxs("div", { className: "flex items-center group", children: [_jsxs("span", { children: [truncated, "\u2026", _jsx("span", { className: `hidden group-hover:inline ${linkText}`, children: "Show more" })] }), _jsx("span", { className: "inline group-hover:hidden" })] })) : (
24
- /* expanded: vertical stack so "Show less" is below */
18
+ /* collapsed view */
19
+ _jsx("div", { className: "flex items-center group", children: _jsxs("span", { children: [truncated, "\u2026", _jsx("span", { className: `hidden group-hover:inline ${linkText}`, children: "Show more" })] }) })) : (
20
+ /* expanded view */
25
21
  _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { children: fullText }), _jsx("span", { className: linkText, children: "Show less" })] }))) : (
26
- /* no toggle let row handle all clicks */
22
+ /* no toggle required */
27
23
  cell.render("Cell")) }) }));
28
24
  };
29
25
  export default TableCell;
@@ -10,7 +10,7 @@ const sampleData = {
10
10
  export default {
11
11
  title: "Table/TableCell",
12
12
  component: TableCell,
13
- tags: ["autodocs"], // 👈 enables automatic Docs tab
13
+ tags: ["autodocs"],
14
14
  argTypes: {
15
15
  hasInfiniteScroll: {
16
16
  control: "boolean",
@@ -35,7 +35,9 @@ export default {
35
35
  },
36
36
  };
37
37
  /* ---------- template ---------- */
38
- const Template = (args) => (_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { ...args }) }) }) }));
38
+ const Template = (args) => (_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { expanded: false, onToggle: function () {
39
+ throw new Error("Function not implemented.");
40
+ }, ...args }) }) }) }));
39
41
  /* ---------- stories ---------- */
40
42
  export const Default = Template.bind({});
41
43
  Default.args = {
@@ -55,18 +55,24 @@ const defaultProps = {
55
55
  };
56
56
  describe("TableCell Component", () => {
57
57
  it("trims the text when globalTrimActive is true", () => {
58
- render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { ...defaultProps }) }) }) }));
58
+ render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { expanded: false, onToggle: function () {
59
+ throw new Error("Function not implemented.");
60
+ }, ...defaultProps }) }) }) }));
59
61
  const trimmedText = `${sampleData.address.substring(0, 30)}...`;
60
62
  expect(screen.getByText(trimmedText)).toBeInTheDocument();
61
63
  });
62
64
  it("does not trim the text when globalTrimActive is false", () => {
63
65
  const props = { ...defaultProps, globalTrimActive: false };
64
- render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { ...props }) }) }) }));
66
+ render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { expanded: false, onToggle: function () {
67
+ throw new Error("Function not implemented.");
68
+ }, ...props }) }) }) }));
65
69
  expect(screen.getByText(sampleData.address)).toBeInTheDocument();
66
70
  });
67
71
  it("applies different styles when isLastCell is true", () => {
68
72
  const props = { ...defaultProps, isLastCell: true };
69
- render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { ...props }) }) }) }));
73
+ render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { expanded: false, onToggle: function () {
74
+ throw new Error("Function not implemented.");
75
+ }, ...props }) }) }) }));
70
76
  const cell = screen.getByRole("cell");
71
77
  expect(cell).not.toHaveClass("relative overflow-hidden font-light text-sm text-left z-0 px-5");
72
78
  });
@@ -79,7 +85,9 @@ describe("TableCell Component", () => {
79
85
  render: (type) => (type === "Cell" ? "42" : null),
80
86
  },
81
87
  };
82
- render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { ...numericValueProps }) }) }) }));
88
+ render(_jsx("table", { children: _jsx("tbody", { children: _jsx("tr", { children: _jsx(TableCell, { expanded: false, onToggle: function () {
89
+ throw new Error("Function not implemented.");
90
+ }, ...numericValueProps }) }) }) }));
83
91
  expect(screen.getByText("42")).toBeInTheDocument();
84
92
  });
85
93
  });
@@ -7,11 +7,11 @@ export interface TableRowProps<T extends DataWithUUID> {
7
7
  activeIndex?: number;
8
8
  activeRowColor?: string;
9
9
  rowHoverClasses?: string;
10
- globalTrimActive?: boolean;
11
10
  hasInfiniteScroll: boolean;
12
11
  rowUuid?: string;
13
- columnInputs?: string[];
14
12
  onRowClick: (index: number, rowUuid: string, event: React.MouseEvent<HTMLTableRowElement, MouseEvent>) => void;
13
+ expandedCells: Record<string, boolean>;
14
+ toggleCell: (key: string) => void;
15
15
  hasDropDown?: boolean;
16
16
  onFetchRowData?: (uuid: string) => Promise<void> | void;
17
17
  loadingIndicator?: ReactNode;
@@ -19,5 +19,5 @@ export interface TableRowProps<T extends DataWithUUID> {
19
19
  expandedContent?: ReactNode;
20
20
  linkText?: string;
21
21
  }
22
- declare const TableRow: <T extends DataWithUUID>({ row, prepareRow, activeIndex, activeRowColor, rowHoverClasses, globalTrimActive, hasInfiniteScroll, rowUuid, columnInputs, onRowClick, hasDropDown, onFetchRowData, loadingIndicator, errorIndicator, expandedContent, linkText, }: TableRowProps<T>) => import("react/jsx-runtime").JSX.Element;
22
+ declare const TableRow: <T extends DataWithUUID>({ row, prepareRow, activeIndex, activeRowColor, rowHoverClasses, hasInfiniteScroll, rowUuid, onRowClick, expandedCells, toggleCell, hasDropDown, onFetchRowData, loadingIndicator, errorIndicator, expandedContent, linkText, }: TableRowProps<T>) => import("react/jsx-runtime").JSX.Element;
23
23
  export default TableRow;
@@ -7,29 +7,24 @@ import TableCell from "../TableCell";
7
7
  /* COMPONENT */
8
8
  /* ------------------------------------------------------------------ */
9
9
  const TableRow = ({
10
- /* reacttable */
10
+ /* react-table */
11
11
  row, prepareRow,
12
12
  /* visual */
13
13
  activeIndex, activeRowColor = "bg-pink-100", rowHoverClasses = "hover:bg-navy-100 hover:cursor-pointer",
14
- /* cell props */
15
- globalTrimActive = false, hasInfiniteScroll,
16
- /* meta */
17
- rowUuid, columnInputs,
18
- /* click handler */
19
- onRowClick,
14
+ /* behaviour / meta */
15
+ hasInfiniteScroll, rowUuid, onRowClick,
16
+ /* expansion store */
17
+ expandedCells, toggleCell,
20
18
  /* dropdown */
21
19
  hasDropDown = false, onFetchRowData, loadingIndicator, errorIndicator, expandedContent, linkText = "text-blue-500", }) => {
22
- /* prepare react-table row */
23
- prepareRow(row);
24
- const isActive = activeIndex === row.index;
25
- let rowClasses = "border-primary";
26
- if (isActive)
27
- rowClasses += ` activeRow ${activeRowColor}`;
20
+ prepareRow(row); // react-table must be prepared before render
21
+ /* mark active row */
22
+ const rowClasses = `border-primary${activeIndex === row.index ? ` activeRow ${activeRowColor}` : ""}`;
28
23
  /* local dropdown state */
29
24
  const [isExpanded, setIsExpanded] = useState(false);
30
25
  const [isLoading, setIsLoading] = useState(false);
31
26
  const [error, setError] = useState(null);
32
- /* rowclick: call your passed‑in handler, then dropdown logic */
27
+ /* handle row click + dropdown logic */
33
28
  const handleRowClick = async (e) => {
34
29
  onRowClick(row.index, rowUuid ?? "", e);
35
30
  if (hasDropDown) {
@@ -52,8 +47,8 @@ hasDropDown = false, onFetchRowData, loadingIndicator, errorIndicator, expandedC
52
47
  };
53
48
  return (_jsxs(Fragment, { children: [_jsx("tr", { "data-testid": "table-row", className: `border-b border-b-navy-200 ${rowHoverClasses} ${rowClasses}`, ...(row.getRowProps ? row.getRowProps() : {}), onClick: handleRowClick, children: row.cells.map((cell, idx) => {
54
49
  const isLastCell = idx === row.cells.length - 1;
55
- const cellProps = cell.getCellProps();
56
- return (_jsx(Fragment, { children: _jsx(TableCell, { cell: cell, isLastCell: isLastCell, globalTrimActive: globalTrimActive, columnInputs: columnInputs, rowUuid: rowUuid, hasInfiniteScroll: hasInfiniteScroll, linkText: linkText }) }, cellProps.key));
50
+ const cellKey = `${rowUuid ?? "no-uuid"}-${cell.column.id}`;
51
+ return (_jsx(TableCell, { cell: cell, isLastCell: isLastCell, hasInfiniteScroll: hasInfiniteScroll, rowUuid: rowUuid, expanded: expandedCells[cellKey] ?? false, onToggle: () => toggleCell(cellKey), linkText: linkText }, cellKey));
57
52
  }) }, rowUuid), hasDropDown && (_jsx(AnimatePresence, { children: isExpanded && (_jsx("tr", { "data-testid": "expanded-row", children: _jsx("td", { colSpan: row.cells.length, className: "p-0", children: _jsx(motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { duration: 0.3 }, className: "overflow-hidden w-full", children: isLoading ? (loadingIndicator ?? (_jsx("span", { children: "Loading..." }))) : error ? (errorIndicator ? (errorIndicator(error)) : (_jsxs("span", { className: "text-red-700", children: ["Error: ", error.message] }))) : (expandedContent ?? (_jsxs("span", { children: ["drop down \u2013 ", rowUuid] }))) }, "expanded-dropdown-content") }) })) }))] }));
58
53
  };
59
54
  export default TableRow;
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // stories/TableRow.stories.tsx
3
+ import React from "react";
2
4
  import TableRow from "./TableRow";
3
5
  const sampleData = {
4
6
  uuid: "12345",
5
7
  name: "John Doe",
6
8
  age: 30,
7
- address: "123 Main St, Springfield, IL 62701, United States of America – apartment 4B",
9
+ address: "123 Main St, Springfield, IL 62701, USA – apartment 4B",
8
10
  email: "john.doe@example.com",
9
- phone: "+1 5551234567",
11
+ phone: "+1 555-123-4567",
10
12
  status: "Active",
11
13
  expandedContent: (_jsx("div", { className: "size-96 bg-blue-300 flex items-center justify-center", children: "Custom element passed in \u2013 12345" })),
12
14
  };
@@ -14,41 +16,44 @@ const sampleData2 = {
14
16
  uuid: "67890",
15
17
  name: "Jane Smith",
16
18
  age: 25,
17
- address: "456 Another St, Springfield, IL 62702, United States of America",
19
+ address: "456 Another St, Springfield, IL 62702, USA",
18
20
  email: "jane.smith@example.com",
19
- phone: "+1 5559876543",
21
+ phone: "+1 555-987-6543",
20
22
  status: "Inactive",
21
23
  };
22
- /* ------------------------------------------------------------------ */
23
- /* MOCK REACT‑TABLE CELLS */
24
- /* ------------------------------------------------------------------ */
25
24
  const makeCells = (d) => [
26
25
  {
26
+ column: { id: "name" },
27
27
  value: d.name,
28
28
  render: (t) => (t === "Cell" ? d.name : null),
29
29
  getCellProps: () => ({}),
30
30
  },
31
31
  {
32
+ column: { id: "age" },
32
33
  value: d.age,
33
34
  render: (t) => (t === "Cell" ? d.age.toString() : null),
34
35
  getCellProps: () => ({}),
35
36
  },
36
37
  {
38
+ column: { id: "address" },
37
39
  value: d.address,
38
40
  render: (t) => (t === "Cell" ? d.address : null),
39
41
  getCellProps: () => ({}),
40
42
  },
41
43
  {
44
+ column: { id: "email" },
42
45
  value: d.email,
43
46
  render: (t) => (t === "Cell" ? d.email : null),
44
47
  getCellProps: () => ({}),
45
48
  },
46
49
  {
50
+ column: { id: "phone" },
47
51
  value: d.phone,
48
52
  render: (t) => (t === "Cell" ? d.phone : null),
49
53
  getCellProps: () => ({}),
50
54
  },
51
55
  {
56
+ column: { id: "status" },
52
57
  value: d.status,
53
58
  render: (t) => t === "Cell" ? (_jsx("span", { className: d.status === "Active"
54
59
  ? "text-green-500"
@@ -70,7 +75,7 @@ const makeMockRow = (d, idx, cells) => ({
70
75
  });
71
76
  const mockRow = makeMockRow(sampleData, 0, cellsForRow1);
72
77
  const mockRow2 = makeMockRow(sampleData2, 1, cellsForRow2);
73
- const prepareRow = () => { }; // noop for Storybook
78
+ const prepareRow = () => { }; // no-op for Storybook
74
79
  /* ------------------------------------------------------------------ */
75
80
  /* STORYBOOK CONFIG */
76
81
  /* ------------------------------------------------------------------ */
@@ -78,28 +83,19 @@ const meta = {
78
83
  title: "Table/TableRow",
79
84
  component: TableRow,
80
85
  tags: ["autodocs"],
81
- decorators: [
82
- (Story) => (_jsx("div", { onClickCapture: (e) => {
83
- const t = e.target.innerText;
84
- if (t === "Show more" || t === "Show less") {
85
- alert("from cell");
86
- }
87
- }, children: _jsx(Story, {}) })),
88
- ],
89
- argTypes: {
90
- hasInfiniteScroll: { control: "boolean" },
91
- hasDropDown: { control: "boolean" },
92
- onRowClick: { action: "row clicked" },
93
- },
94
- parameters: {
95
- layout: "centered",
96
- },
86
+ parameters: { layout: "centered" },
97
87
  };
98
88
  export default meta;
99
89
  /* ------------------------------------------------------------------ */
100
90
  /* TEMPLATE */
101
91
  /* ------------------------------------------------------------------ */
102
- const Template = (args) => (_jsx("table", { className: "min-w-[700px]", children: _jsx("tbody", { children: _jsx(TableRow, { ...args, hasInfiniteScroll: args.hasInfiniteScroll ?? true, onRowClick: args.onRowClick ?? (() => alert("from row")) }) }) }));
92
+ const Template = (args) => {
93
+ const [expandedCells, setExpandedCells] = React.useState({});
94
+ const toggleCell = React.useCallback((key) => setExpandedCells((s) => ({ ...s, [key]: !s[key] })), []);
95
+ return (_jsx("table", { className: "min-w-[700px]", children: _jsx("tbody", { children: _jsx(TableRow, { row: undefined, prepareRow: function (row) {
96
+ throw new Error("Function not implemented.");
97
+ }, ...args, expandedCells: expandedCells, toggleCell: toggleCell, hasInfiniteScroll: args.hasInfiniteScroll ?? true, onRowClick: args.onRowClick ?? (() => alert("from row")) }) }) }));
98
+ };
103
99
  /* ------------------------------------------------------------------ */
104
100
  /* STORIES */
105
101
  /* ------------------------------------------------------------------ */
@@ -107,12 +103,11 @@ export const Default = Template.bind({});
107
103
  Default.args = {
108
104
  row: mockRow,
109
105
  prepareRow,
110
- hasInfiniteScroll: true, // ✅ required
111
- onRowClick: () => alert("from row"), // ✅ required
112
- globalTrimActive: false, // optional
113
- hasDropDown: false, // optional
114
- rowUuid: sampleData.uuid, // optional
115
- // (you can omit activeIndex, columnInputs, rowHoverClasses—they’re optional)
106
+ hasInfiniteScroll: true,
107
+ onRowClick: () => alert("from row"),
108
+ globalTrimActive: false,
109
+ hasDropDown: false,
110
+ rowUuid: sampleData.uuid,
116
111
  };
117
112
  export const ExpandableRow = Template.bind({});
118
113
  ExpandableRow.args = {
@@ -120,4 +115,8 @@ ExpandableRow.args = {
120
115
  hasDropDown: true,
121
116
  expandedContent: sampleData.expandedContent,
122
117
  };
123
- export const MultipleRows = () => (_jsx("table", { className: "min-w-[700px]", children: _jsxs("tbody", { children: [_jsx(TableRow, { row: mockRow, prepareRow: prepareRow, hasInfiniteScroll: true, onRowClick: () => alert("from row"), globalTrimActive: false, hasDropDown: true, rowUuid: sampleData.uuid, expandedContent: sampleData.expandedContent }), _jsx(TableRow, { row: mockRow2, prepareRow: prepareRow, hasInfiniteScroll: true, onRowClick: () => alert("from row"), globalTrimActive: false, hasDropDown: false, rowUuid: sampleData2.uuid })] }) }));
118
+ export const MultipleRows = () => {
119
+ const [expandedCells, setExpandedCells] = React.useState({});
120
+ const toggleCell = React.useCallback((key) => setExpandedCells((s) => ({ ...s, [key]: !s[key] })), []);
121
+ return (_jsx("table", { className: "min-w-[700px]", children: _jsxs("tbody", { children: [_jsx(TableRow, { row: mockRow, prepareRow: prepareRow, hasInfiniteScroll: true, onRowClick: () => alert("from row"), hasDropDown: true, rowUuid: "1", expandedContent: sampleData.expandedContent, expandedCells: expandedCells, toggleCell: toggleCell }), _jsx(TableRow, { row: mockRow2, prepareRow: prepareRow, hasInfiniteScroll: true, onRowClick: () => alert("from row"), hasDropDown: false, rowUuid: "2", expandedCells: expandedCells, toggleCell: toggleCell })] }) }));
122
+ };
@@ -23,18 +23,24 @@ describe("TableRow - Branch Coverage Tests", () => {
23
23
  it("covers isActive = true (row is active)", () => {
24
24
  render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: { ...baseMockRow, index: 5 }, prepareRow: mockPrepareRow, activeIndex: 5, hasInfiniteScroll: false, onRowClick: function (index, rowUuid, event) {
25
25
  throw new Error("Function not implemented.");
26
+ }, expandedCells: undefined, toggleCell: function (key) {
27
+ throw new Error("Function not implemented.");
26
28
  } }) }) }));
27
29
  expect(screen.getByTestId("table-row")).toHaveClass("activeRow");
28
30
  });
29
31
  it("covers isActive = false (row is NOT active)", () => {
30
32
  render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: { ...baseMockRow, index: 2 }, prepareRow: mockPrepareRow, activeIndex: 5, hasInfiniteScroll: false, onRowClick: function (index, rowUuid, event) {
31
33
  throw new Error("Function not implemented.");
34
+ }, expandedCells: undefined, toggleCell: function (key) {
35
+ throw new Error("Function not implemented.");
32
36
  } }) }) }));
33
37
  expect(screen.getByTestId("table-row")).not.toHaveClass("activeRow");
34
38
  });
35
39
  it("covers onRowClick = defined + rowUuid = provided", () => {
36
40
  const mockOnRowClick = vi.fn();
37
- render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: baseMockRow, prepareRow: mockPrepareRow, onRowClick: mockOnRowClick, rowUuid: "some-uuid", hasInfiniteScroll: false }) }) }));
41
+ render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: baseMockRow, prepareRow: mockPrepareRow, onRowClick: mockOnRowClick, rowUuid: "some-uuid", hasInfiniteScroll: false, expandedCells: undefined, toggleCell: function (key) {
42
+ throw new Error("Function not implemented.");
43
+ } }) }) }));
38
44
  fireEvent.click(screen.getByTestId("table-row"));
39
45
  expect(mockOnRowClick).toHaveBeenCalledWith(baseMockRow.index, // 0
40
46
  "some-uuid", expect.anything() // Synthetic event
@@ -42,7 +48,9 @@ describe("TableRow - Branch Coverage Tests", () => {
42
48
  });
43
49
  it("covers onRowClick = defined + rowUuid = undefined (so calls empty string)", () => {
44
50
  const mockOnRowClick = vi.fn();
45
- render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: baseMockRow, prepareRow: mockPrepareRow, onRowClick: mockOnRowClick, hasInfiniteScroll: false }) }) }));
51
+ render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: baseMockRow, prepareRow: mockPrepareRow, onRowClick: mockOnRowClick, hasInfiniteScroll: false, expandedCells: undefined, toggleCell: function (key) {
52
+ throw new Error("Function not implemented.");
53
+ } }) }) }));
46
54
  fireEvent.click(screen.getByTestId("table-row"));
47
55
  expect(mockOnRowClick).toHaveBeenCalledWith(baseMockRow.index, // 0
48
56
  "", expect.anything());
@@ -50,6 +58,8 @@ describe("TableRow - Branch Coverage Tests", () => {
50
58
  it("covers onRowClick = NOT defined", () => {
51
59
  render(_jsx("table", { children: _jsx("tbody", { children: _jsx(TableRow, { row: baseMockRow, prepareRow: mockPrepareRow, hasInfiniteScroll: false, onRowClick: function (index, rowUuid, event) {
52
60
  throw new Error("Function not implemented.");
61
+ }, expandedCells: undefined, toggleCell: function (key) {
62
+ throw new Error("Function not implemented.");
53
63
  } }) }) }));
54
64
  // onRowClick not provided; just ensure no crash
55
65
  fireEvent.click(screen.getByTestId("table-row"));
@@ -1,2 +1,3 @@
1
1
  export { useTableInfiniteScroll } from "./useTableInfiniteScroll";
2
2
  export type { useTableInfiniteScrollOptions } from "./useTableInfiniteScroll";
3
+ export { useApiQuery } from "./useApiQuery";
@@ -1 +1,2 @@
1
1
  export { useTableInfiniteScroll } from "./useTableInfiniteScroll";
2
+ export { useApiQuery } from "./useApiQuery";
@@ -0,0 +1,2 @@
1
+ import { UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
2
+ export declare const useApiMutation: <TData = unknown, TError = unknown, TVariables extends Record<string, any> = {}, TContext = unknown>(method: "POST" | "PUT" | "DELETE", route: string, options?: Record<string, any>, mutationOptions?: Omit<UseMutationOptions<TData, TError, TVariables, TContext>, "mutationFn">) => UseMutationResult<TData, TError, TVariables, TContext>;
@@ -0,0 +1,25 @@
1
+ import { useMutation, useQueryClient, } from "@tanstack/react-query";
2
+ import { apiDelete, apiPost, apiPut } from "../api/apiFunctions";
3
+ export const useApiMutation = (method, route, options = {}, mutationOptions) => {
4
+ const queryClient = useQueryClient();
5
+ const mutationFn = (payload) => {
6
+ switch (method) {
7
+ case "POST":
8
+ return apiPost(route, payload, options);
9
+ case "PUT":
10
+ return apiPut(route, payload, options);
11
+ case "DELETE":
12
+ return apiDelete(route, payload, options);
13
+ default:
14
+ throw new Error(`Unsupported method: ${method}`);
15
+ }
16
+ };
17
+ return useMutation({
18
+ mutationFn,
19
+ onSuccess: () => {
20
+ // Invalidate all queries (or scope it to specific keys)
21
+ queryClient.invalidateQueries();
22
+ },
23
+ ...mutationOptions,
24
+ });
25
+ };
@@ -0,0 +1,2 @@
1
+ import { UseQueryOptions, QueryKey } from "@tanstack/react-query";
2
+ export declare const useApiQuery: <TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(key: TQueryKey, route: string, options?: {}, payload?: {}, queryOptions?: Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<TData, TError>;
@@ -0,0 +1,9 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { apiGet } from "../api/apiFunctions";
3
+ export const useApiQuery = (key, route, options = {}, payload = {}, queryOptions) => {
4
+ return useQuery({
5
+ queryKey: key,
6
+ queryFn: () => apiGet(route, options, payload),
7
+ ...queryOptions,
8
+ });
9
+ };
@@ -0,0 +1,67 @@
1
+ export interface Options {
2
+ fields?: any;
3
+ where?: any;
4
+ join?: any;
5
+ ojoin?: any;
6
+ sort?: any;
7
+ [key: string]: any;
8
+ }
9
+ export interface OptionsType {
10
+ transactionId?: string;
11
+ fields?: (string | Field | {
12
+ location: string[];
13
+ shipToAddress: string[];
14
+ })[];
15
+ sort?: any;
16
+ slug?: string;
17
+ id?: string;
18
+ join?: Array<{
19
+ [key: string]: {
20
+ [key: string]: string;
21
+ };
22
+ }>;
23
+ where?: {
24
+ [key: string]: any;
25
+ and?: any[];
26
+ or?: any[];
27
+ };
28
+ recordsPerPage?: number;
29
+ page?: number;
30
+ }
31
+ export interface ExtendedOptionsType extends Omit<OptionsType, "fields"> {
32
+ depth?: number;
33
+ fields?: (string | Field | {
34
+ location: string[];
35
+ shipToAddress: string[];
36
+ })[];
37
+ where?: any;
38
+ join?: any;
39
+ ojoin?: any;
40
+ sort?: any;
41
+ recordsPerPage?: number;
42
+ page?: number;
43
+ }
44
+ export interface Field {
45
+ [key: string]: string | string[] | Field;
46
+ }
47
+ export interface ExtendedOptionsType extends Omit<OptionsType, "fields"> {
48
+ depth?: number;
49
+ fields?: (string | Field | {
50
+ location: string[];
51
+ shipToAddress: string[];
52
+ })[];
53
+ where?: any;
54
+ join?: any;
55
+ ojoin?: any;
56
+ sort?: any;
57
+ recordsPerPage?: number;
58
+ page?: number;
59
+ [key: string]: any;
60
+ }
61
+ export declare function assembleOptions(options: Options): string;
62
+ export declare function assembleOptionsFields(fields: any, prefix?: string): string[];
63
+ export declare function assembleOptionsWhere(input: Record<string, any>): string;
64
+ export declare function assembleOptionsJoin(input: Record<string, any>): string;
65
+ export declare function assembleOptionsSort(input: any): string;
66
+ export declare function urlEncode(input: string, encodeSpecialChars?: boolean): string;
67
+ export declare function toCamelCase(slug: string): string;
@@ -0,0 +1,132 @@
1
+ export function assembleOptions(options) {
2
+ const queryString = new URLSearchParams();
3
+ for (const optionKey in options) {
4
+ const optionVal = options[optionKey];
5
+ switch (optionKey.toLowerCase()) {
6
+ case "fields":
7
+ queryString.set("fields", assembleOptionsFields(optionVal).join(","));
8
+ break;
9
+ case "where":
10
+ queryString.set("where", assembleOptionsWhere(optionVal));
11
+ break;
12
+ case "join":
13
+ queryString.set("join", assembleOptionsJoin(optionVal));
14
+ break;
15
+ case "ojoin":
16
+ queryString.set("ojoin", assembleOptionsJoin(optionVal));
17
+ break;
18
+ case "sort":
19
+ queryString.set("sort", assembleOptionsSort(optionVal));
20
+ break;
21
+ default:
22
+ queryString.set(optionKey, optionVal);
23
+ }
24
+ }
25
+ // Decode URI because URLSearchParams automatically encodes URI components
26
+ return decodeURIComponent(queryString.toString());
27
+ }
28
+ export function assembleOptionsFields(fields, prefix = "") {
29
+ let output = [];
30
+ for (const key in fields) {
31
+ const value = fields[key];
32
+ if (typeof value === "object" && value !== null) {
33
+ let newPrefix = prefix + (isNaN(Number(key)) ? key : "") + ".";
34
+ if (newPrefix === ".") {
35
+ newPrefix = "";
36
+ }
37
+ if (newPrefix.slice(-2) === "..") {
38
+ newPrefix = newPrefix.slice(0, -1);
39
+ }
40
+ output = output.concat(assembleOptionsFields(value, newPrefix));
41
+ }
42
+ else {
43
+ output.push(prefix + value);
44
+ }
45
+ }
46
+ return output;
47
+ }
48
+ export function assembleOptionsWhere(input) {
49
+ const output = [];
50
+ let operator = "and";
51
+ for (const key in input) {
52
+ const value = input[key];
53
+ operator = key.toUpperCase();
54
+ for (const fieldConditions of value) {
55
+ for (const field in fieldConditions) {
56
+ const conditions = fieldConditions[field];
57
+ const childOperator = field.toUpperCase();
58
+ if (childOperator === "AND" || childOperator === "OR") {
59
+ output.push(assembleOptionsWhere({ [childOperator]: conditions }));
60
+ }
61
+ else {
62
+ for (let comparison in conditions) {
63
+ let conditionValue = conditions[comparison];
64
+ comparison = comparison.replace(/<>/g, "ne");
65
+ comparison = comparison.replace(/>=/g, "ge");
66
+ comparison = comparison.replace(/>/g, "gt");
67
+ comparison = comparison.replace(/<=/g, "le");
68
+ comparison = comparison.replace(/</g, "lt");
69
+ comparison = comparison.replace(/==/g, "eq");
70
+ comparison = comparison.replace(/!=/g, "ne");
71
+ comparison = comparison.replace(/=/g, "eq");
72
+ comparison = comparison.replace(/not/g, "not");
73
+ comparison = comparison.replace(/like/g, "like");
74
+ comparison = comparison.replace(/contains/g, "contains");
75
+ comparison = comparison.replace(/starts/g, "starts");
76
+ comparison = comparison.replace(/ends/g, "ends");
77
+ comparison = comparison.replace(/excludes/g, "excludes");
78
+ conditionValue = urlEncode(conditionValue);
79
+ output.push(`${field}:${comparison}:${conditionValue}`);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return `(${output.join(`,${operator},`)})`;
86
+ }
87
+ export function assembleOptionsJoin(input) {
88
+ const out = [];
89
+ for (const item of Object.values(input)) {
90
+ let thisJoin = "";
91
+ for (const table in item) {
92
+ const value = item[table];
93
+ thisJoin += table + ":";
94
+ const thisJoinConditions = [];
95
+ for (const fieldA in value) {
96
+ const fieldB = value[fieldA];
97
+ thisJoinConditions.push(`${fieldA}=${fieldB}`);
98
+ }
99
+ thisJoin += thisJoinConditions.join(";");
100
+ }
101
+ out.push(thisJoin);
102
+ }
103
+ return out.join(",");
104
+ }
105
+ export function assembleOptionsSort(input) {
106
+ if (Array.isArray(input)) {
107
+ return input.join(",");
108
+ }
109
+ else if (typeof input === "string") {
110
+ return input;
111
+ }
112
+ else {
113
+ return "";
114
+ }
115
+ }
116
+ export function urlEncode(input, encodeSpecialChars = true) {
117
+ if (encodeSpecialChars && input) {
118
+ input = input.replace(/[%&?]/g, (match) => {
119
+ return encodeURIComponent(match);
120
+ });
121
+ input = input.replace(/[!'()*\-._~]/g, (c) => {
122
+ return "%" + c.charCodeAt(0).toString(16).toUpperCase();
123
+ });
124
+ return input;
125
+ }
126
+ else {
127
+ return encodeURIComponent(input);
128
+ }
129
+ }
130
+ export function toCamelCase(slug) {
131
+ return slug.replace(/-([a-z])/g, (letter) => letter.toUpperCase());
132
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agilant/toga-blox",
3
3
  "private": false,
4
- "version": "1.0.167",
4
+ "version": "1.0.170",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -31,6 +31,7 @@
31
31
  "@storybook/addon-viewport": "^7.6.10",
32
32
  "@tanstack/react-virtual": "^3.1.3",
33
33
  "@tanstack/virtual-core": "^3.1.3",
34
+ "axios": "^1.8.4",
34
35
  "classnames": "^2.5.1",
35
36
  "esbuild-wasm": "^0.23.0",
36
37
  "framer-motion": "^11.0.8",
@@ -50,6 +51,7 @@
50
51
  "@fortawesome/fontawesome-svg-core": "^6.5.1",
51
52
  "@fortawesome/free-solid-svg-icons": "^6.5.2",
52
53
  "@fortawesome/react-fontawesome": "^0.2.2",
54
+ "@sentry/react": "^8.55.0",
53
55
  "@storybook/addon-a11y": "^7.6.8",
54
56
  "@storybook/addon-essentials": "^7.6.6",
55
57
  "@storybook/addon-interactions": "^7.6.6",
@@ -79,13 +81,23 @@
79
81
  "typescript": "^5.7.3",
80
82
  "vite": "^4.5.1",
81
83
  "vite-tsconfig-paths": "^4.2.2",
82
- "vitest": "^1.6.0"
84
+ "vitest": "^1.6.0",
85
+ "@tanstack/query-sync-storage-persister": "^5.59.20",
86
+ "@tanstack/react-query": "^5.59.19",
87
+ "@tanstack/react-query-devtools": "^5.59.19",
88
+ "@tanstack/react-query-persist-client": "^5.59.20"
83
89
  },
84
90
  "peerDependencies": {
85
91
  "@fortawesome/fontawesome-svg-core": "^6.5.1",
86
92
  "@fortawesome/free-solid-svg-icons": "^6.5.2",
87
93
  "@fortawesome/react-fontawesome": "^0.2.2",
94
+ "@sentry/react": "^8.50.0",
95
+ "axios": "^1.8.4",
88
96
  "react-hook-form": "^7.43.9",
89
- "react-router-dom": "^6.16.0"
97
+ "react-router-dom": "^6.16.0",
98
+ "@tanstack/query-sync-storage-persister": "^5.59.20",
99
+ "@tanstack/react-query": "^5.59.19",
100
+ "@tanstack/react-query-devtools": "^5.59.19",
101
+ "@tanstack/react-query-persist-client": "^5.59.20"
90
102
  }
91
103
  }