@open-mercato/ui 0.6.7-develop.6834.1.e76f4b8cbc → 0.6.7-develop.6851.1.c1680b00d8
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/backend/DataTable.js +8 -7
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +2 -1
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/crud/useGroupOrder.js +2 -4
- package/dist/backend/crud/useGroupOrder.js.map +2 -2
- package/dist/backend/utils/apiCall.js +21 -1
- package/dist/backend/utils/apiCall.js.map +2 -2
- package/dist/backend/utils/customFieldDefs.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +2 -1
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/package.json +4 -4
- package/src/backend/DataTable.tsx +20 -7
- package/src/backend/ProfileDropdown.tsx +1 -0
- package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
- package/src/backend/crud/__tests__/useGroupOrder.test.ts +10 -0
- package/src/backend/crud/useGroupOrder.ts +12 -4
- package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
- package/src/backend/utils/apiCall.ts +29 -1
- package/src/backend/utils/customFieldDefs.ts +1 -0
- package/src/frontend/LanguageSwitcher.tsx +1 -0
|
@@ -107,3 +107,64 @@ describe('readApiResultOrThrow', () => {
|
|
|
107
107
|
expect(result).toBeNull()
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
describe('aborted requests', () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
jest.resetAllMocks()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
function createAbortedBodyResponse(): Response {
|
|
117
|
+
const abortError = new Error('The operation was aborted.')
|
|
118
|
+
abortError.name = 'AbortError'
|
|
119
|
+
const response = {
|
|
120
|
+
ok: true,
|
|
121
|
+
status: 200,
|
|
122
|
+
headers: new Map<string, string>(),
|
|
123
|
+
text: jest.fn(async () => { throw abortError }),
|
|
124
|
+
clone: () => response,
|
|
125
|
+
} as unknown as Response
|
|
126
|
+
return response
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it('rejects with AbortError instead of reporting an empty payload', async () => {
|
|
130
|
+
;(apiFetch as jest.Mock).mockResolvedValue(createAbortedBodyResponse())
|
|
131
|
+
const controller = new AbortController()
|
|
132
|
+
controller.abort()
|
|
133
|
+
|
|
134
|
+
await expect(
|
|
135
|
+
readApiResultOrThrow('/api/interactions', { signal: controller.signal }),
|
|
136
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not mask a genuinely empty payload when the request was not aborted', async () => {
|
|
140
|
+
;(apiFetch as jest.Mock).mockResolvedValue(createMockResponse('', { status: 200 }))
|
|
141
|
+
const controller = new AbortController()
|
|
142
|
+
|
|
143
|
+
await expect(
|
|
144
|
+
readApiResultOrThrow('/api/interactions', { signal: controller.signal }),
|
|
145
|
+
).rejects.toThrow('Missing response payload (200)')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('keeps the caller-provided fallback when one is configured', async () => {
|
|
149
|
+
;(apiFetch as jest.Mock).mockResolvedValue(createAbortedBodyResponse())
|
|
150
|
+
const controller = new AbortController()
|
|
151
|
+
controller.abort()
|
|
152
|
+
|
|
153
|
+
const call = await apiCall<{ items: string[] }>(
|
|
154
|
+
'/api/interactions',
|
|
155
|
+
{ signal: controller.signal },
|
|
156
|
+
{ fallback: { items: [] } },
|
|
157
|
+
)
|
|
158
|
+
expect(call.result).toEqual({ items: [] })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('propagates AbortError raised by a custom parser', async () => {
|
|
162
|
+
;(apiFetch as jest.Mock).mockResolvedValue(createMockResponse('{}', { status: 200 }))
|
|
163
|
+
const abortError = new Error('The operation was aborted.')
|
|
164
|
+
abortError.name = 'AbortError'
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
apiCall('/api/interactions', undefined, { parse: async () => { throw abortError } }),
|
|
168
|
+
).rejects.toMatchObject({ name: 'AbortError' })
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -19,6 +19,27 @@ export type ApiCallResult<TReturn> = {
|
|
|
19
19
|
|
|
20
20
|
const scopedRequestHeaders = createScopedHeaderStack()
|
|
21
21
|
|
|
22
|
+
function isAbortError(error: unknown): boolean {
|
|
23
|
+
return typeof error === 'object'
|
|
24
|
+
&& error !== null
|
|
25
|
+
&& (error as { name?: unknown }).name === 'AbortError'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createAbortError(): Error {
|
|
29
|
+
if (typeof DOMException === 'function') {
|
|
30
|
+
return new DOMException('[internal] The operation was aborted.', 'AbortError')
|
|
31
|
+
}
|
|
32
|
+
const error = new Error('[internal] The operation was aborted.')
|
|
33
|
+
error.name = 'AbortError'
|
|
34
|
+
return error
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveAbortSignal(input: RequestInfo | URL, init?: RequestInit): AbortSignal | null {
|
|
38
|
+
if (init?.signal) return init.signal
|
|
39
|
+
if (typeof Request !== 'undefined' && input instanceof Request) return input.signal ?? null
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {
|
|
23
44
|
if (!base) return extra
|
|
24
45
|
if (typeof Headers !== 'undefined' && base instanceof Headers) {
|
|
@@ -63,9 +84,16 @@ export async function apiCall<TReturn = Record<string, unknown>>(
|
|
|
63
84
|
: response
|
|
64
85
|
if (parser) result = await parser(source)
|
|
65
86
|
else result = await readJsonSafe<TReturn>(source, fallback)
|
|
66
|
-
} catch {
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (isAbortError(err)) throw err
|
|
67
89
|
result = fallback
|
|
68
90
|
}
|
|
91
|
+
// `readJsonSafe` swallows the abort that cancels an in-flight body read, so a
|
|
92
|
+
// request aborted after its response headers arrived would otherwise look like
|
|
93
|
+
// a successful call that returned an empty payload.
|
|
94
|
+
if (result == null && resolveAbortSignal(input, init)?.aborted) {
|
|
95
|
+
throw createAbortError()
|
|
96
|
+
}
|
|
69
97
|
return {
|
|
70
98
|
ok: response.ok,
|
|
71
99
|
status: response.status,
|
|
@@ -24,6 +24,7 @@ export function LanguageSwitcher() {
|
|
|
24
24
|
pl: t('common.languages.polish', 'Polski'),
|
|
25
25
|
es: t('common.languages.spanish', 'Español'),
|
|
26
26
|
de: t('common.languages.german', 'Deutsch'),
|
|
27
|
+
ko: t('common.languages.korean', '한국어'),
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
async function setLocale(locale: Locale) {
|