@inertiaui/modal-react 2.0.0-beta.1 → 3.0.0
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/ModalLink.d.ts +2 -2
- package/dist/cache.d.ts +12 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/inertiaui-modal.js +92 -75
- package/dist/inertiaui-modal.js.map +1 -1
- package/dist/inertiaui-modal.umd.cjs +92 -74
- package/dist/inertiaui-modal.umd.cjs.map +1 -1
- package/dist/inertiauiModal.d.ts +1 -1
- package/dist/types.d.ts +12 -12
- package/package.json +11 -8
- package/src/ModalLink.tsx +2 -2
- package/src/ModalRoot.tsx +92 -126
- package/src/cache.ts +64 -0
- package/src/helpers.ts +3 -0
- package/src/inertiauiModal.ts +1 -0
- package/src/types.ts +13 -13
package/dist/inertiauiModal.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { default as Modal } from './Modal';
|
|
|
8
8
|
import { default as ModalLink } from './ModalLink';
|
|
9
9
|
import { default as WhenVisible } from './WhenVisible';
|
|
10
10
|
import * as dialogUtils from '@inertiaui/vanilla';
|
|
11
|
-
export type { Modal as ModalInstance, ModalConfig, ModalResponseData, ModalStackContextValue, VisitOptions, ReloadOptions, EventCallback, ComponentResolver, PageProps, ModalRootProps, ModalRendererProps, LocalModal, PrefetchOption, PrefetchOptions, } from './types';
|
|
11
|
+
export type { Modal as ModalInstance, ModalConfig, ModalResponseData, ModalStackContextValue, VisitOptions, ReloadOptions, EventCallback, ComponentResolver, HttpMethod, PageProps, ModalRootProps, ModalRendererProps, LocalModal, PrefetchOption, PrefetchOptions, } from './types';
|
|
12
12
|
export type { ModalTypeConfig } from './config';
|
|
13
13
|
export type { CleanupFunction, FocusTrapOptions, EscapeKeyOptions } from '@inertiaui/vanilla';
|
|
14
14
|
declare const setPageLayout: <T extends {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AxiosResponse } from 'axios';
|
|
2
1
|
import { ComponentType, ReactNode } from 'react';
|
|
3
|
-
import { RequestPayload } from '@inertiajs/core';
|
|
2
|
+
import { RequestPayload, HttpResponse, Method } from '@inertiajs/core';
|
|
3
|
+
import { ModalTypeConfig } from './config';
|
|
4
|
+
export type HttpMethod = Method;
|
|
4
5
|
export interface ModalResponseData {
|
|
5
6
|
id?: string;
|
|
6
7
|
component: string;
|
|
@@ -12,22 +13,22 @@ export interface ModalResponseData {
|
|
|
12
13
|
};
|
|
13
14
|
baseUrl?: string;
|
|
14
15
|
}
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
}
|
|
16
|
+
export type ModalConfig = Partial<ModalTypeConfig & {
|
|
17
|
+
slideover: boolean;
|
|
18
|
+
}>;
|
|
18
19
|
export interface ReloadOptions {
|
|
19
20
|
only?: string[];
|
|
20
21
|
except?: string[];
|
|
21
|
-
method?:
|
|
22
|
+
method?: HttpMethod;
|
|
22
23
|
data?: Record<string, unknown>;
|
|
23
24
|
headers?: Record<string, string>;
|
|
24
25
|
onStart?: () => void;
|
|
25
|
-
onSuccess?: (response:
|
|
26
|
+
onSuccess?: (response: HttpResponse) => void;
|
|
26
27
|
onError?: (error: unknown) => void;
|
|
27
28
|
onFinish?: () => void;
|
|
28
29
|
}
|
|
29
30
|
export interface VisitOptions {
|
|
30
|
-
method?:
|
|
31
|
+
method?: HttpMethod;
|
|
31
32
|
data?: RequestPayload;
|
|
32
33
|
headers?: Record<string, string>;
|
|
33
34
|
config?: ModalConfig;
|
|
@@ -36,14 +37,14 @@ export interface VisitOptions {
|
|
|
36
37
|
queryStringArrayFormat?: 'brackets' | 'indices';
|
|
37
38
|
navigate?: boolean;
|
|
38
39
|
onStart?: () => void;
|
|
39
|
-
onSuccess?: (response?:
|
|
40
|
+
onSuccess?: (response?: HttpResponse) => void;
|
|
40
41
|
onError?: (...args: unknown[]) => void;
|
|
41
42
|
listeners?: Record<string, (...args: unknown[]) => void>;
|
|
42
43
|
props?: Record<string, unknown>;
|
|
43
44
|
}
|
|
44
45
|
export type PrefetchOption = boolean | 'hover' | 'click' | 'mount' | Array<'hover' | 'click' | 'mount'>;
|
|
45
46
|
export interface PrefetchOptions {
|
|
46
|
-
method?:
|
|
47
|
+
method?: HttpMethod;
|
|
47
48
|
data?: RequestPayload;
|
|
48
49
|
headers?: Record<string, string>;
|
|
49
50
|
queryStringArrayFormat?: 'brackets' | 'indices';
|
|
@@ -92,7 +93,7 @@ export interface ModalStackContextValue {
|
|
|
92
93
|
length: () => number;
|
|
93
94
|
closeAll: (force?: boolean) => void;
|
|
94
95
|
reset: () => void;
|
|
95
|
-
visit: (href: string, method:
|
|
96
|
+
visit: (href: string, method: HttpMethod, payload?: RequestPayload, headers?: Record<string, string>, config?: ModalConfig, onClose?: (() => void) | null, onAfterLeave?: (() => void) | null, queryStringArrayFormat?: 'brackets' | 'indices', useBrowserHistory?: boolean, onStart?: (() => void) | null, onSuccess?: ((response?: HttpResponse) => void) | null, onError?: ((...args: unknown[]) => void) | null) => Promise<Modal>;
|
|
96
97
|
visitModal: (url: string, options?: VisitOptions) => Promise<Modal>;
|
|
97
98
|
registerLocalModal: (name: string, callback: (modal: Modal) => void) => void;
|
|
98
99
|
removeLocalModal: (name: string) => void;
|
|
@@ -101,7 +102,6 @@ export interface PageProps {
|
|
|
101
102
|
initialPage?: {
|
|
102
103
|
version?: string;
|
|
103
104
|
};
|
|
104
|
-
resolveComponent?: ComponentResolver;
|
|
105
105
|
}
|
|
106
106
|
export interface ModalRootProps {
|
|
107
107
|
children?: ReactNode;
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inertiaui/modal-react",
|
|
3
3
|
"author": "Pascal Baljet <pascal@protone.media>",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/inertiaui/modal.git",
|
|
10
|
+
"directory": "react"
|
|
11
|
+
},
|
|
7
12
|
"type": "module",
|
|
8
13
|
"files": [
|
|
9
14
|
"dist",
|
|
@@ -30,8 +35,8 @@
|
|
|
30
35
|
},
|
|
31
36
|
"devDependencies": {
|
|
32
37
|
"@heroicons/react": "^2.1.4",
|
|
33
|
-
"@inertiajs/react": "^
|
|
34
|
-
"@inertiaui/vanilla": "^0.
|
|
38
|
+
"@inertiajs/react": "^3.0.0",
|
|
39
|
+
"@inertiaui/vanilla": "^0.3.0",
|
|
35
40
|
"@testing-library/react": "^16.0.0",
|
|
36
41
|
"@types/react": "^19.0.0",
|
|
37
42
|
"@types/react-dom": "^19.0.0",
|
|
@@ -40,7 +45,6 @@
|
|
|
40
45
|
"@vitejs/plugin-react": "^4.3.1",
|
|
41
46
|
"@vitest/coverage-v8": "^4.0.0",
|
|
42
47
|
"@vitest/ui": "^4.0.0",
|
|
43
|
-
"axios": "^1.6.0",
|
|
44
48
|
"clsx": "^2.1.1",
|
|
45
49
|
"eslint": "^9.0.0",
|
|
46
50
|
"happy-dom": "^20.0.0",
|
|
@@ -53,12 +57,11 @@
|
|
|
53
57
|
"vitest": "^4.0.0"
|
|
54
58
|
},
|
|
55
59
|
"dependencies": {
|
|
56
|
-
"@inertiaui/vanilla": "^0.
|
|
60
|
+
"@inertiaui/vanilla": "^0.3.0"
|
|
57
61
|
},
|
|
58
62
|
"peerDependencies": {
|
|
59
|
-
"@inertiajs/react": "^
|
|
60
|
-
"axios": "^1.6.0",
|
|
63
|
+
"@inertiajs/react": "^3.0.0",
|
|
61
64
|
"react": "^19.0.0",
|
|
62
65
|
"react-dom": "^19.0.0"
|
|
63
66
|
}
|
|
64
|
-
}
|
|
67
|
+
}
|
package/src/ModalLink.tsx
CHANGED
|
@@ -2,12 +2,12 @@ import { useCallback, useState, useEffect, useMemo, useRef, ReactNode, ElementTy
|
|
|
2
2
|
import { useModalStack, modalPropNames, prefetch as prefetchModal } from './ModalRoot'
|
|
3
3
|
import { only, rejectNullValues, isStandardDomEvent } from './helpers'
|
|
4
4
|
import { getConfig } from './config'
|
|
5
|
-
import type { Modal, PrefetchOption } from './types'
|
|
5
|
+
import type { Modal, PrefetchOption, HttpMethod } from './types'
|
|
6
6
|
import type { RequestPayload } from '@inertiajs/core'
|
|
7
7
|
|
|
8
8
|
interface ModalLinkProps {
|
|
9
9
|
href: string
|
|
10
|
-
method?:
|
|
10
|
+
method?: HttpMethod
|
|
11
11
|
data?: RequestPayload
|
|
12
12
|
as?: ElementType
|
|
13
13
|
headers?: Record<string, string>
|
package/src/ModalRoot.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createElement, useEffect, useState, useRef, useReducer, ReactNode, ComponentType } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { router, usePage, progress } from '@inertiajs/react'
|
|
5
|
-
import { mergeDataIntoQueryString, type RequestPayload } from '@inertiajs/core'
|
|
1
|
+
import { createElement, useEffect, useLayoutEffect, useState, useRef, useReducer, ReactNode, ComponentType } from 'react'
|
|
2
|
+
import { except, kebabCase, generateId, sameUrlPath, parseResponseData } from './helpers'
|
|
3
|
+
import { ResponseCache } from './cache'
|
|
4
|
+
import { router, usePage, progress, http } from '@inertiajs/react'
|
|
5
|
+
import { mergeDataIntoQueryString, type RequestPayload, type HttpResponse, type HttpRequestConfig } from '@inertiajs/core'
|
|
6
6
|
import { createContext, useContext } from 'react'
|
|
7
7
|
import ModalRenderer from './ModalRenderer'
|
|
8
8
|
import { getConfig } from './config'
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
VisitOptions,
|
|
15
15
|
ReloadOptions,
|
|
16
16
|
EventCallback,
|
|
17
|
-
|
|
17
|
+
HttpMethod,
|
|
18
18
|
PageProps,
|
|
19
19
|
ModalRootProps,
|
|
20
20
|
LocalModal,
|
|
@@ -24,75 +24,38 @@ import type {
|
|
|
24
24
|
const ModalStackContext = createContext<ModalStackContextValue | null>(null)
|
|
25
25
|
ModalStackContext.displayName = 'ModalStackContext'
|
|
26
26
|
|
|
27
|
-
let pageVersion: string | null = null
|
|
28
|
-
let resolveComponent: ComponentResolver | null = null
|
|
29
27
|
let baseUrl: string | null = null
|
|
28
|
+
let currentPageVersion: string | null = null
|
|
30
29
|
|
|
31
30
|
// Track the URL we're closing to (prevents navigate handler from re-setting baseUrl)
|
|
32
31
|
// Only suppresses if navigate event URL matches this URL
|
|
33
32
|
let closingToBaseUrlTarget: string | null = null
|
|
34
33
|
|
|
35
34
|
// Prefetch cache (#146)
|
|
36
|
-
|
|
37
|
-
response: AxiosResponse
|
|
38
|
-
timestamp: number
|
|
39
|
-
expiresAt: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const prefetchCache = new Map<string, PrefetchCacheEntry>()
|
|
43
|
-
const prefetchInFlight = new Map<string, Promise<AxiosResponse>>()
|
|
44
|
-
|
|
45
|
-
function getPrefetchCacheKey(url: string, method: string, data: RequestPayload): string {
|
|
46
|
-
return `${method}:${url}:${JSON.stringify(data)}`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getCachedResponse(url: string, method: string, data: RequestPayload): AxiosResponse | null {
|
|
50
|
-
const key = getPrefetchCacheKey(url, method, data)
|
|
51
|
-
const cached = prefetchCache.get(key)
|
|
52
|
-
|
|
53
|
-
if (!cached) {
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (Date.now() > cached.expiresAt) {
|
|
58
|
-
prefetchCache.delete(key)
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return cached.response
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function setCachedResponse(url: string, method: string, data: RequestPayload, response: AxiosResponse, cacheFor: number): void {
|
|
66
|
-
const key = getPrefetchCacheKey(url, method, data)
|
|
67
|
-
prefetchCache.set(key, {
|
|
68
|
-
response,
|
|
69
|
-
timestamp: Date.now(),
|
|
70
|
-
expiresAt: Date.now() + cacheFor,
|
|
71
|
-
})
|
|
72
|
-
}
|
|
35
|
+
const prefetchCache = new ResponseCache<HttpResponse>()
|
|
73
36
|
|
|
74
37
|
export function prefetch(href: string, options: PrefetchOptions = {}): Promise<void> {
|
|
75
38
|
if (href.startsWith('#')) {
|
|
76
39
|
return Promise.resolve()
|
|
77
40
|
}
|
|
78
41
|
|
|
79
|
-
const method =
|
|
42
|
+
const method = options.method ?? 'get'
|
|
80
43
|
const data = options.data ?? ({} as RequestPayload)
|
|
81
44
|
const headers = options.headers ?? {}
|
|
82
45
|
const queryStringArrayFormat = options.queryStringArrayFormat ?? 'brackets'
|
|
83
46
|
const cacheFor = options.cacheFor ?? 30000
|
|
84
47
|
|
|
85
|
-
const [url, mergedData] = mergeDataIntoQueryString(method
|
|
48
|
+
const [url, mergedData] = mergeDataIntoQueryString(method, href || '', data, queryStringArrayFormat)
|
|
49
|
+
|
|
50
|
+
const cacheKey = ResponseCache.key(method, url, mergedData)
|
|
86
51
|
|
|
87
52
|
// Check if already cached
|
|
88
|
-
|
|
89
|
-
if (cached) {
|
|
53
|
+
if (prefetchCache.get(cacheKey)) {
|
|
90
54
|
return Promise.resolve()
|
|
91
55
|
}
|
|
92
56
|
|
|
93
57
|
// Check if already in flight
|
|
94
|
-
const
|
|
95
|
-
const inFlight = prefetchInFlight.get(cacheKey)
|
|
58
|
+
const inFlight = prefetchCache.getInFlight(cacheKey)
|
|
96
59
|
if (inFlight) {
|
|
97
60
|
return inFlight.then(() => {})
|
|
98
61
|
}
|
|
@@ -104,27 +67,29 @@ export function prefetch(href: string, options: PrefetchOptions = {}): Promise<v
|
|
|
104
67
|
Accept: 'text/html, application/xhtml+xml',
|
|
105
68
|
'X-Requested-With': 'XMLHttpRequest',
|
|
106
69
|
'X-Inertia': 'true',
|
|
107
|
-
'X-Inertia-Version':
|
|
70
|
+
'X-Inertia-Version': currentPageVersion ?? '',
|
|
108
71
|
'X-InertiaUI-Modal': generateId(),
|
|
109
72
|
'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
|
|
110
73
|
}
|
|
111
74
|
|
|
112
|
-
const request =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
const request = http
|
|
76
|
+
.getClient()
|
|
77
|
+
.request({
|
|
78
|
+
url,
|
|
79
|
+
method,
|
|
80
|
+
data: mergedData,
|
|
81
|
+
headers: requestHeaders,
|
|
82
|
+
})
|
|
118
83
|
.then((response) => {
|
|
119
|
-
|
|
84
|
+
prefetchCache.set(cacheKey, response, cacheFor)
|
|
120
85
|
options.onPrefetched?.()
|
|
121
86
|
return response
|
|
122
87
|
})
|
|
123
88
|
.finally(() => {
|
|
124
|
-
|
|
89
|
+
prefetchCache.deleteInFlight(cacheKey)
|
|
125
90
|
})
|
|
126
91
|
|
|
127
|
-
|
|
92
|
+
prefetchCache.setInFlight(cacheKey, request)
|
|
128
93
|
|
|
129
94
|
return request.then(() => {})
|
|
130
95
|
}
|
|
@@ -365,29 +330,30 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
365
330
|
return
|
|
366
331
|
}
|
|
367
332
|
|
|
368
|
-
const method =
|
|
333
|
+
const method = options.method ?? 'get'
|
|
369
334
|
const data = options.data ?? {}
|
|
370
335
|
|
|
371
336
|
options.onStart?.()
|
|
372
337
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
338
|
+
http.getClient()
|
|
339
|
+
.request({
|
|
340
|
+
url: this.response.url,
|
|
341
|
+
method,
|
|
342
|
+
data: method === 'get' ? undefined : data,
|
|
343
|
+
params: method === 'get' ? data : undefined,
|
|
344
|
+
headers: {
|
|
345
|
+
...(options.headers ?? {}),
|
|
346
|
+
Accept: 'text/html, application/xhtml+xml',
|
|
347
|
+
'X-Inertia': 'true',
|
|
348
|
+
'X-Inertia-Partial-Component': this.response.component,
|
|
349
|
+
'X-Inertia-Version': this.response.version ?? '',
|
|
350
|
+
'X-Inertia-Partial-Data': keys.join(','),
|
|
351
|
+
'X-InertiaUI-Modal': generateId(),
|
|
352
|
+
'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
|
|
353
|
+
},
|
|
354
|
+
})
|
|
389
355
|
.then((response) => {
|
|
390
|
-
this.updateProps(response.data.props)
|
|
356
|
+
this.updateProps((parseResponseData(response.data) as ModalResponseData).props)
|
|
391
357
|
|
|
392
358
|
options.onSuccess?.(response)
|
|
393
359
|
})
|
|
@@ -420,10 +386,6 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
420
386
|
onClose: (() => void) | null = null,
|
|
421
387
|
onAfterLeave: (() => void) | null = null,
|
|
422
388
|
): Promise<Modal> => {
|
|
423
|
-
if (!resolveComponent) {
|
|
424
|
-
return Promise.reject(new Error('resolveComponent not set'))
|
|
425
|
-
}
|
|
426
|
-
|
|
427
389
|
if (!isValidModalResponse(responseData)) {
|
|
428
390
|
return Promise.reject(
|
|
429
391
|
new Error(
|
|
@@ -433,8 +395,8 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
433
395
|
)
|
|
434
396
|
}
|
|
435
397
|
|
|
436
|
-
return resolveComponent(responseData.component).then((component) =>
|
|
437
|
-
push(component, responseData, config, onClose, onAfterLeave),
|
|
398
|
+
return router.resolveComponent(responseData.component).then((component) =>
|
|
399
|
+
push(component as ComponentType, responseData, config, onClose, onAfterLeave),
|
|
438
400
|
)
|
|
439
401
|
}
|
|
440
402
|
|
|
@@ -537,7 +499,7 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
537
499
|
|
|
538
500
|
const visit = (
|
|
539
501
|
href: string,
|
|
540
|
-
method:
|
|
502
|
+
method: HttpMethod,
|
|
541
503
|
payload: RequestPayload = {},
|
|
542
504
|
headers: Record<string, string> = {},
|
|
543
505
|
config: ModalConfig = {},
|
|
@@ -546,7 +508,7 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
546
508
|
queryStringArrayFormat: 'brackets' | 'indices' = 'brackets',
|
|
547
509
|
useBrowserHistory = false,
|
|
548
510
|
onStart: (() => void) | null = null,
|
|
549
|
-
onSuccess: ((response?:
|
|
511
|
+
onSuccess: ((response?: HttpResponse) => void) | null = null,
|
|
550
512
|
onError: ((...args: unknown[]) => void) | null = null,
|
|
551
513
|
props: Record<string, unknown> | null = null,
|
|
552
514
|
): Promise<Modal> => {
|
|
@@ -558,15 +520,16 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
558
520
|
return
|
|
559
521
|
}
|
|
560
522
|
|
|
561
|
-
const [url, data] = mergeDataIntoQueryString(method
|
|
523
|
+
const [url, data] = mergeDataIntoQueryString(method, href || '', payload, queryStringArrayFormat)
|
|
562
524
|
|
|
563
525
|
// Check for cached prefetch response (#146)
|
|
564
|
-
const cachedResponse =
|
|
526
|
+
const cachedResponse = prefetchCache.get(ResponseCache.key(method, url, data))
|
|
565
527
|
if (cachedResponse) {
|
|
528
|
+
const cachedData = parseResponseData(cachedResponse.data) as ModalResponseData
|
|
566
529
|
onSuccess?.(cachedResponse)
|
|
567
|
-
pushFromResponseData(
|
|
530
|
+
pushFromResponseData(cachedData, config, onClose, onAfterLeave)
|
|
568
531
|
.then((modal) => {
|
|
569
|
-
updateBrowserUrl(
|
|
532
|
+
updateBrowserUrl(cachedData.url, useBrowserHistory, cachedData)
|
|
570
533
|
resolve(modal)
|
|
571
534
|
})
|
|
572
535
|
.catch(reject)
|
|
@@ -582,7 +545,7 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
582
545
|
Accept: 'text/html, application/xhtml+xml',
|
|
583
546
|
'X-Requested-With': 'XMLHttpRequest',
|
|
584
547
|
'X-Inertia': 'true',
|
|
585
|
-
'X-Inertia-Version':
|
|
548
|
+
'X-Inertia-Version': currentPageVersion ?? '',
|
|
586
549
|
'X-InertiaUI-Modal': modalId,
|
|
587
550
|
'X-InertiaUI-Modal-Base-Url': baseUrl ?? '',
|
|
588
551
|
}
|
|
@@ -591,17 +554,19 @@ export const ModalStackProvider = ({ children }: ModalStackProviderProps) => {
|
|
|
591
554
|
|
|
592
555
|
progress?.start()
|
|
593
556
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
557
|
+
http.getClient()
|
|
558
|
+
.request({
|
|
559
|
+
url,
|
|
560
|
+
method,
|
|
561
|
+
data,
|
|
562
|
+
headers: requestHeaders,
|
|
563
|
+
})
|
|
600
564
|
.then((response) => {
|
|
565
|
+
const responseData = parseResponseData(response.data) as ModalResponseData
|
|
601
566
|
onSuccess?.(response)
|
|
602
|
-
pushFromResponseData(
|
|
567
|
+
pushFromResponseData(responseData, config, onClose, onAfterLeave)
|
|
603
568
|
.then((modal) => {
|
|
604
|
-
updateBrowserUrl(
|
|
569
|
+
updateBrowserUrl(responseData.url, useBrowserHistory, responseData)
|
|
605
570
|
resolve(modal)
|
|
606
571
|
})
|
|
607
572
|
.catch(reject)
|
|
@@ -671,11 +636,7 @@ export const modalPropNames = ['closeButton', 'closeExplicitly', 'closeOnClickOu
|
|
|
671
636
|
|
|
672
637
|
export const initFromPageProps = (pageProps: PageProps) => {
|
|
673
638
|
if (pageProps.initialPage) {
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (pageProps.resolveComponent) {
|
|
678
|
-
resolveComponent = pageProps.resolveComponent
|
|
639
|
+
currentPageVersion = pageProps.initialPage.version ?? null
|
|
679
640
|
}
|
|
680
641
|
}
|
|
681
642
|
|
|
@@ -734,11 +695,30 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
734
695
|
const $page = usePage<InertiaUIModalPageProps>()
|
|
735
696
|
const pendingModalKeysRef = useRef(new Set<string>())
|
|
736
697
|
|
|
698
|
+
// Keep module-level pageVersion in sync for use by prefetch/visit functions.
|
|
699
|
+
// Set during render (not useEffect) because prefetch() needs it synchronously on mount.
|
|
700
|
+
currentPageVersion = $page.version ?? null
|
|
701
|
+
|
|
737
702
|
// Generate a unique key for deduplication (handles case when modal has no id)
|
|
738
703
|
const getModalKey = (modalData: ModalResponseData) => modalData.id || `${modalData.component}:${modalData.url}`
|
|
739
704
|
|
|
740
705
|
const isNavigatingRef = useRef(false)
|
|
741
|
-
|
|
706
|
+
|
|
707
|
+
// Use a ref so the interceptor always reads the latest page props
|
|
708
|
+
const pageRef = useRef($page)
|
|
709
|
+
pageRef.current = $page
|
|
710
|
+
|
|
711
|
+
// Register interceptor in useLayoutEffect (fires during commit, before microtasks).
|
|
712
|
+
// Inertia 3 loads deferred props during page.set() microtasks which fire after commit
|
|
713
|
+
// but before useEffect — useLayoutEffect ensures the interceptor is registered in time.
|
|
714
|
+
useLayoutEffect(() => http.onRequest((config: HttpRequestConfig) => {
|
|
715
|
+
const baseUrlValue = baseUrl ?? pageRef.current.props._inertiaui_modal?.baseUrl ?? null
|
|
716
|
+
if (baseUrlValue) {
|
|
717
|
+
config.headers = config.headers ?? {}
|
|
718
|
+
config.headers['X-InertiaUI-Modal-Base-Url'] = baseUrlValue
|
|
719
|
+
}
|
|
720
|
+
return config
|
|
721
|
+
}), [])
|
|
742
722
|
|
|
743
723
|
useEffect(() => router.on('start', () => (isNavigatingRef.current = true)), [])
|
|
744
724
|
useEffect(() => router.on('finish', () => (isNavigatingRef.current = false)), [])
|
|
@@ -759,7 +739,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
759
739
|
closingToBaseUrlTarget = null
|
|
760
740
|
context?.closeAll(true)
|
|
761
741
|
baseUrl = null
|
|
762
|
-
initialModalStillOpenedRef.current = false
|
|
763
742
|
return
|
|
764
743
|
}
|
|
765
744
|
closingToBaseUrlTarget = null
|
|
@@ -769,7 +748,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
769
748
|
// No modal data - close any open modals (force close without transition)
|
|
770
749
|
context?.closeAll(true)
|
|
771
750
|
baseUrl = null
|
|
772
|
-
initialModalStillOpenedRef.current = false
|
|
773
751
|
return
|
|
774
752
|
}
|
|
775
753
|
|
|
@@ -777,7 +755,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
777
755
|
if (!sameUrlPath(pageUrl, modalOnBase.url)) {
|
|
778
756
|
context?.closeAll(true)
|
|
779
757
|
baseUrl = null
|
|
780
|
-
initialModalStillOpenedRef.current = false
|
|
781
758
|
return
|
|
782
759
|
}
|
|
783
760
|
|
|
@@ -809,6 +786,11 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
809
786
|
console.error('No base url in modal response data so cannot navigate back')
|
|
810
787
|
return
|
|
811
788
|
}
|
|
789
|
+
// Clear baseUrl before navigating so the interceptor doesn't add
|
|
790
|
+
// the modal header to the base page request (deferred props should
|
|
791
|
+
// load without the modal context after closing)
|
|
792
|
+
baseUrl = null
|
|
793
|
+
|
|
812
794
|
if (!isNavigatingRef.current && typeof window !== 'undefined' && window.location.href !== modalOnBase.baseUrl) {
|
|
813
795
|
router.visit(modalOnBase.baseUrl, {
|
|
814
796
|
preserveScroll: true,
|
|
@@ -823,23 +805,6 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
823
805
|
[],
|
|
824
806
|
)
|
|
825
807
|
|
|
826
|
-
const axiosRequestInterceptor = (config: InternalAxiosRequestConfig) => {
|
|
827
|
-
// A Modal is opened on top of a base route, so we need to pass this base route
|
|
828
|
-
// so it can redirect back with the back() helper method...
|
|
829
|
-
// Only send the header when we have an actual base URL value
|
|
830
|
-
const baseUrlValue = baseUrl ?? (initialModalStillOpenedRef.current ? $page.props._inertiaui_modal?.baseUrl : null)
|
|
831
|
-
if (baseUrlValue) {
|
|
832
|
-
config.headers['X-InertiaUI-Modal-Base-Url'] = baseUrlValue
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
return config
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
useEffect(() => {
|
|
839
|
-
const interceptorId = Axios.interceptors.request.use(axiosRequestInterceptor)
|
|
840
|
-
return () => Axios.interceptors.request.eject(interceptorId)
|
|
841
|
-
}, [])
|
|
842
|
-
|
|
843
808
|
const previousModalRef = useRef<(ModalResponseData & { baseUrl: string }) | undefined>(undefined)
|
|
844
809
|
|
|
845
810
|
useEffect(() => {
|
|
@@ -878,3 +843,4 @@ export const ModalRoot = ({ children }: ModalRootProps) => {
|
|
|
878
843
|
</>
|
|
879
844
|
)
|
|
880
845
|
}
|
|
846
|
+
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
interface CacheEntry<T> {
|
|
2
|
+
response: T
|
|
3
|
+
expiresAt: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class ResponseCache<T> {
|
|
7
|
+
private cache = new Map<string, CacheEntry<T>>()
|
|
8
|
+
private timers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
9
|
+
private inFlight = new Map<string, Promise<T>>()
|
|
10
|
+
|
|
11
|
+
static key(method: string, url: string, data: unknown): string {
|
|
12
|
+
return `${method}:${url}:${JSON.stringify(data)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(key: string): T | null {
|
|
16
|
+
const cached = this.cache.get(key)
|
|
17
|
+
|
|
18
|
+
if (!cached) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (Date.now() > cached.expiresAt) {
|
|
23
|
+
this.delete(key)
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return cached.response
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(key: string, response: T, cacheFor: number): void {
|
|
31
|
+
this.delete(key)
|
|
32
|
+
|
|
33
|
+
this.cache.set(key, {
|
|
34
|
+
response,
|
|
35
|
+
expiresAt: Date.now() + cacheFor,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (cacheFor > 0) {
|
|
39
|
+
this.timers.set(key, setTimeout(() => this.delete(key), cacheFor))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
delete(key: string): void {
|
|
44
|
+
this.cache.delete(key)
|
|
45
|
+
|
|
46
|
+
const timer = this.timers.get(key)
|
|
47
|
+
if (timer) {
|
|
48
|
+
clearTimeout(timer)
|
|
49
|
+
this.timers.delete(key)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getInFlight(key: string): Promise<T> | undefined {
|
|
54
|
+
return this.inFlight.get(key)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setInFlight(key: string, promise: Promise<T>): void {
|
|
58
|
+
this.inFlight.set(key, promise)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
deleteInFlight(key: string): void {
|
|
62
|
+
this.inFlight.delete(key)
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/helpers.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Re-export helper utilities from vanilla
|
|
2
2
|
export { sameUrlPath, except, only, rejectNullValues, kebabCase, isStandardDomEvent } from '@inertiaui/vanilla'
|
|
3
|
+
export function parseResponseData(data: unknown): unknown {
|
|
4
|
+
return typeof data === 'string' ? JSON.parse(data) : data
|
|
5
|
+
}
|
|
3
6
|
import { generateId as vanillaGenerateId } from '@inertiaui/vanilla'
|
|
4
7
|
|
|
5
8
|
// Wrap generateId with custom callback support for testing
|