Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
31 changes: 1 addition & 30 deletions .storybook/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,7 @@ const config = {
// lightweight mocks so components render without a Next.js runtime.
alias: {
...config.resolve?.alias,
// stub for mui-tiptap's uninstalled required peer @tiptap/extension-image
'@tiptap/extension-image': path.resolve(
dirname,
'../tests/mocks/tiptap-extension-image.js',
),
'next/dynamic': path.resolve(dirname, '../tests/mocks/next-dynamic.js'),
'next/dynamic': path.resolve(dirname, '../tests/mocks/next-dynamic.jsx'),
'next/router': path.resolve(dirname, '../tests/mocks/next-router.js'),
'next/navigation': path.resolve(dirname, '../tests/mocks/next-navigation.js'),
'next/head': path.resolve(dirname, '../tests/mocks/next-head.js'),
Expand All @@ -66,16 +61,6 @@ const config = {
'process.env': '{}',
global: 'window',
},
esbuild: {
...config.esbuild,
// The codebase uses .js files with JSX syntax. Vite's default esbuild loader
// only handles JSX in .jsx files, so we override the loader for all .js files.
jsx: 'automatic',
jsxImportSource: 'react',
loader: 'jsx',
include: /(src|tests|\.storybook)\/.*\.(js|jsx)$/,
exclude: [],
},
build: {
...config.build,
rollupOptions: {
Expand All @@ -89,20 +74,6 @@ const config = {
},
},
},
optimizeDeps: {
...config.optimizeDeps,
// Same JSX-in-.js fix as above, but for Vite's dependency pre-bundling step
// (esbuild runs separately for optimizeDeps vs transform).
esbuildOptions: {
...config.optimizeDeps?.esbuildOptions,
jsx: 'automatic',
jsxImportSource: 'react',
loader: {
'.js': 'jsx',
'.jsx': 'jsx',
},
},
},
}
},
staticDirs: ['../public'],
Expand Down
5 changes: 3 additions & 2 deletions .storybook/vitest.setup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { configure } from 'storybook/test'
import '../tests/mocks/require-context'

// coverage instrumentation slows lazy chunks and fetches past the 1s default
configure({ asyncUtilTimeout: 10000 })
// coverage instrumentation and vite 8's slower module serving push lazy chunks
// and fetches past the 1s default (and past 10s when the full suite runs)
configure({ asyncUtilTimeout: 30000 })
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const eslintConfig = defineConfig([
maximum: 10,
},
],
// JSX must live in .jsx files: vite 8's oxc transform (vitest/storybook)
// decides JSX handling purely by file extension
'react/jsx-filename-extension': ['error', { extensions: ['.jsx'] }],
},
},
{
Expand Down
51 changes: 29 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"license": "AGPL-3.0",
"engines": {
"node": "^22.22.0"
"node": "^22.22.2"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -38,36 +38,38 @@
"@emotion/react": "11.14.0",
"@emotion/server": "11.11.0",
"@emotion/styled": "11.14.1",
"@floating-ui/dom": "1.7.6",
"@heroicons/react": "2.2.0",
"@monaco-editor/react": "^4.6.0",
"@mui/icons-material": "7.3.10",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "7.3.10",
"@mui/system": "7.3.10",
"@mui/x-date-pickers": "^9.10.1",
"@mui/icons-material": "9.4.0",
"@mui/lab": "9.0.0-beta.9",
"@mui/material": "9.4.0",
"@mui/system": "9.4.0",
"@mui/x-date-pickers": "^9.12.0",
"@musement/iso-duration": "^1.0.0",
"@nivo/core": "^0.99.0",
"@nivo/sankey": "^0.99.0",
"@react-pdf/renderer": "^4.5.1",
"@react-pdf/renderer": "4.5.1",
"@reduxjs/toolkit": "^2.12.0",
"@tanstack/query-sync-storage-persister": "^5.90.25",
"@tanstack/query-sync-storage-persister": "^5.102.8",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-query-devtools": "^5.101.2",
"@tanstack/react-query-persist-client": "^5.101.2",
"@tanstack/react-table": "^8.19.2",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-heading": "^3.27.3",
"@tiptap/extension-table": "^3.20.5",
"@tiptap/pm": "^3.29.2",
"@tiptap/react": "^3.20.5",
"@tiptap/starter-kit": "^3.29.2",
"@tiptap/core": "^3.31.3",
"@tiptap/extension-heading": "^3.31.3",
"@tiptap/extension-image": "^3.31.3",
"@tiptap/extension-table": "^3.31.3",
"@tiptap/pm": "^3.31.3",
"@tiptap/react": "^3.31.3",
"@tiptap/starter-kit": "^3.31.3",
"@vvo/tzdb": "^6.198.0",
"apexcharts": "6.6.1",
"axios": "1.18.1",
"date-fns": "4.4.0",
"diff": "^9.0.0",
"dompurify": "^3.4.12",
"driver.js": "^1.4.0",
"driver.js": "^1.8.0",
"eml-parse-js": "^1.2.0-beta.0",
"export-to-csv": "^1.3.0",
"formik": "2.4.9",
Expand All @@ -94,14 +96,13 @@
"react-dropzone": "20.0.0",
"react-error-boundary": "^6.1.2",
"react-hook-form": "^7.76.1",
"react-hot-toast": "2.6.0",
"react-html-parser": "^2.0.2",
"react-is": "19.2.8",
"react-leaflet": "5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0",
"react-markdown": "10.1.0",
"react-media-hook": "^0.5.0",
"react-papaparse": "^4.4.0",
"react-quill": "^2.0.0",
"react-redux": "9.3.0",
"react-syntax-highlighter": "^16.1.0",
"react-time-ago": "^7.3.3",
Expand All @@ -114,6 +115,7 @@
"remark-parse": "^11.0.0",
"simplebar": "6.3.3",
"simplebar-react": "3.3.2",
"stylis": "4.2.0",
"stylis-plugin-rtl": "2.1.1",
"swagger-ui-dist": "5.32.12",
"unified": "^11.0.5",
Expand All @@ -130,19 +132,21 @@
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.5",
"@vitest/browser-playwright": "4.1.10",
"@vitest/coverage-v8": "4.1.10",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.10",
"eslint-config-prettier": "^10.1.8",
"jsdom": "29.0.1",
"msw": "2.12.14",
"jsdom": "30.0.1",
"msw": "2.15.0",
"msw-storybook-addon": "3.0.0",
"playwright": "1.59.1",
"prettier": "^3.9.6",
"storybook": "10.5.7",
"storybook": "10.5.10",
"typescript": "5.9.3",
"vite": "7.3.6",
"vite": "8.2.2",
"vitest": "4.1.10"
},
"msw": {
Expand All @@ -151,6 +155,9 @@
]
},
"resolutions": {
"vite": "7.3.6"
"vite": "8.2.2",
"postcss": "^8.5.23",
"sharp": "^0.35.0",
"monaco-editor/dompurify": "^3.4.14"
}
}
30 changes: 21 additions & 9 deletions public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* - Please do NOT modify this file.
*/

const PACKAGE_VERSION = '2.12.14'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const PACKAGE_VERSION = '2.15.0'
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

Expand Down Expand Up @@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)

// Omit the body of server-sent event stream responses.
// Cloning such responses would prevent client-side stream cancelations
// from reaching the original stream (a teed stream only cancels its
// source once both of its branches cancel) and would buffer the
// entire stream into the unconsumed clone indefinitely.
const isEventStreamResponse = response.headers
.get('content-type')
?.toLowerCase()
.startsWith('text/event-stream')

// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
const responseClone = isEventStreamResponse ? null : response.clone()

sendToClient(
client,
Expand All @@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
type: response.type,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: responseClone ? responseClone.body : null,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
responseClone && responseClone.body
? [serializedRequest.body, responseClone.body]
: [],
)
}

Expand Down
4 changes: 2 additions & 2 deletions public/version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "10.9.1"
}
"version": "10.10.0"
}
22 changes: 21 additions & 1 deletion src/api/ApiCall.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ const wildcardToRegExp = (pattern) =>
new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
const matchesWildcardPattern = (queryKey, pattern) => wildcardToRegExp(pattern).test(queryKey);

// The server's Retry-After (seconds) as ms, capped so a large hint can't hang a request indefinitely.
const getRetryAfterMs = (error) => {
if (!isAxiosError(error)) return null;
const headers = error.response?.headers;
const raw = headers?.get?.("retry-after") ?? headers?.["retry-after"];
const seconds = Number(raw);
return Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds * 1000, 60000) : null;
};

// react-query's default exponential backoff, but honouring the server's Retry-After when present so a
// throttled retry lands after the limit clears instead of hammering inside the window.
const retryDelayWithRetryAfter = (failureCount, error) =>
getRetryAfterMs(error) ?? Math.min(1000 * 2 ** failureCount, 30000);

export function ApiGetCall(props) {
const {
url,
Expand Down Expand Up @@ -74,6 +88,7 @@ export function ApiGetCall(props) {
signal: signal,
params: { ...element, ...impersonationCacheParams() },
headers: await buildVersionedHeaders(),
cippQueryKey: queryKey,
});
results.push(response.data);
if (onResult) {
Expand Down Expand Up @@ -113,6 +128,7 @@ export function ApiGetCall(props) {
params: { ...data, ...impersonationCacheParams() },
headers: await buildVersionedHeaders(),
responseType: responseType,
cippQueryKey: queryKey,
});

let responseData = response.data;
Expand Down Expand Up @@ -165,6 +181,7 @@ export function ApiGetCall(props) {
keepPreviousData: keepPreviousData,
refetchInterval: refetchInterval,
retry: retryFn,
retryDelay: retryDelayWithRetryAfter,
});
return queryInfo;
}
Expand Down Expand Up @@ -295,14 +312,16 @@ export function ApiGetCallWithPagination({
signal: signal,
params: { ...data, ...pageParam, ...impersonationCacheParams() },
headers: await buildVersionedHeaders(),
cippQueryKey: queryKey,
});
return response.data;
},
getNextPageParam: (lastPage) => {
// AllTenants pages only when the page opted into manualPagination.
if (
data?.noPagination ||
data?.manualPagination === false ||
data?.tenantFilter === "AllTenants"
(data?.tenantFilter === "AllTenants" && data?.manualPagination !== true)
) {
return undefined;
}
Expand All @@ -311,6 +330,7 @@ export function ApiGetCallWithPagination({
staleTime: 300000,
refetchOnWindowFocus: false,
retry: retryFn,
retryDelay: retryDelayWithRetryAfter,
});

return queryInfo;
Expand Down
Loading
Loading