@godxjp/ui 31.2.0 → 31.3.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/agent/START-HERE.md +5 -5
- package/agent/components/AuthExpiryProvider.json +40 -0
- package/agent/components/DataState.json +3 -1
- package/agent/components/InfiniteQueryState.json +1 -1
- package/agent/components-index.json +5 -0
- package/agent/components.json +44 -2
- package/agent/index.json +6 -6
- package/agent/llms.txt +6 -6
- package/agent/patterns/async-data-state.json +1 -1
- package/agent/patterns.json +1 -1
- package/agent/tokens.json +6 -0
- package/dist/components/admin/index.d.ts +2 -0
- package/dist/components/admin/index.js +2 -0
- package/dist/components/feedback/alert.d.ts +4 -0
- package/dist/components/feedback/alert.js +29 -8
- package/dist/components/feedback/auth-expiry.d.ts +30 -0
- package/dist/components/feedback/auth-expiry.js +64 -0
- package/dist/components/feedback/index.d.ts +2 -0
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/query/data-state.js +9 -0
- package/dist/components/query/index.d.ts +2 -0
- package/dist/components/query/index.js +2 -0
- package/dist/components/query/infinite-query-state.js +9 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +3 -0
- package/dist/i18n/messages/ja.json +3 -0
- package/dist/i18n/messages/vi.json +3 -0
- package/dist/props/components/feedback.prop.d.ts +13 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/registry.d.ts +10 -1
- package/dist/props/registry.js +12 -0
- package/dist/styles/alert-layout.css +4 -0
- package/dist/styles/layers.json +35 -1
- package/dist/tokens/components/feedback.css +2 -0
- package/docs/FRAME-COVERAGE-REPORT.md +4 -2
- package/docs/feedback/auth-expiry.tsx +104 -0
- package/docs/i18n/messages/en.json +6 -0
- package/docs/i18n/messages/ja.json +6 -0
- package/docs/i18n/messages/vi.json +6 -0
- package/docs/query/data-state.tsx +53 -5
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { AlertQueryError } from "../feedback/alert.js";
|
|
5
|
+
import { useAuthExpiry } from "../feedback/auth-expiry.js";
|
|
5
6
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
7
|
import { classifyQueryError } from "../../lib/query-error.js";
|
|
7
8
|
function defaultIsEmpty(data) {
|
|
@@ -34,10 +35,18 @@ function DataState({
|
|
|
34
35
|
}
|
|
35
36
|
void query.refetch();
|
|
36
37
|
}, [onRetry, query]);
|
|
38
|
+
const showingAuthError = query.isError && !query.isFetching && classifyQueryError(query.error).category === "auth";
|
|
39
|
+
const authExpiry = useAuthExpiry(showingAuthError, query.error);
|
|
37
40
|
if (query.isPending && query.fetchStatus === "idle") return /* @__PURE__ */ jsx(Fragment, { children: prerequisite });
|
|
38
41
|
if (query.isPending) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
39
42
|
if (query.isError) {
|
|
40
43
|
if (query.isFetching) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
44
|
+
if (authExpiry?.status === "redirecting") {
|
|
45
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
+
skeleton,
|
|
47
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("query.authExpiry.redirecting") })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
41
50
|
if (errorRenderer) return /* @__PURE__ */ jsx(Fragment, { children: errorRenderer(query.error, retry) });
|
|
42
51
|
const info = classifyQueryError(query.error);
|
|
43
52
|
const canRetry = (info.category === "transient" || info.category === "unknown") && (info.retryable || showRetry || Boolean(onRetry));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { DataState } from "./data-state.js";
|
|
2
2
|
export type { DataStateProp, DataStateProps } from "./data-state.js";
|
|
3
3
|
export { AlertMutationFeedback } from "./mutation-feedback.js";
|
|
4
|
+
export { AuthExpiryProvider } from "../feedback/auth-expiry.js";
|
|
5
|
+
export type { AuthExpiryProviderProp, AuthExpiryProviderProps } from "../feedback/auth-expiry.js";
|
|
4
6
|
export type { AlertMutationFeedbackProp, AlertMutationFeedbackProps } from "./mutation-feedback.js";
|
|
5
7
|
export { ButtonRefetch } from "./query-refetch-button.js";
|
|
6
8
|
export type { ButtonRefetchProp, ButtonRefetchProps } from "./query-refetch-button.js";
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { DataState } from "./data-state.js";
|
|
2
2
|
import { AlertMutationFeedback } from "./mutation-feedback.js";
|
|
3
|
+
import { AuthExpiryProvider } from "../feedback/auth-expiry.js";
|
|
3
4
|
import { ButtonRefetch } from "./query-refetch-button.js";
|
|
4
5
|
import { InfiniteQueryState, flattenItemPages } from "./infinite-query-state.js";
|
|
5
6
|
import { classifyQueryError, isRetryableQueryError } from "../../lib/query-error.js";
|
|
6
7
|
export {
|
|
7
8
|
AlertMutationFeedback,
|
|
9
|
+
AuthExpiryProvider,
|
|
8
10
|
ButtonRefetch,
|
|
9
11
|
DataState,
|
|
10
12
|
InfiniteQueryState,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { AlertQueryError } from "../feedback/alert.js";
|
|
5
|
+
import { useAuthExpiry } from "../feedback/auth-expiry.js";
|
|
5
6
|
import { Button } from "../general/button.js";
|
|
6
7
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
7
8
|
import { classifyQueryError } from "../../lib/query-error.js";
|
|
@@ -36,9 +37,17 @@ function InfiniteQueryState({
|
|
|
36
37
|
}
|
|
37
38
|
void query.refetch();
|
|
38
39
|
}, [onRetry, query]);
|
|
40
|
+
const showingAuthError = query.isError && !(query.isFetching && !query.isFetchingNextPage) && classifyQueryError(query.error).category === "auth";
|
|
41
|
+
const authExpiry = useAuthExpiry(showingAuthError, query.error);
|
|
39
42
|
if (query.isPending) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
40
43
|
if (query.isError) {
|
|
41
44
|
if (query.isFetching && !query.isFetchingNextPage) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
45
|
+
if (authExpiry?.status === "redirecting") {
|
|
46
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
47
|
+
skeleton,
|
|
48
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("query.authExpiry.redirecting") })
|
|
49
|
+
] });
|
|
50
|
+
}
|
|
42
51
|
if (errorRenderer) return /* @__PURE__ */ jsx(Fragment, { children: errorRenderer(query.error, retry) });
|
|
43
52
|
const info = classifyQueryError(query.error);
|
|
44
53
|
const canRetry = (info.category === "transient" || info.category === "unknown") && (info.retryable || showRetry || Boolean(onRetry));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.3.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -71,6 +71,19 @@ export type AlertQueryErrorProp = {
|
|
|
71
71
|
onAuthAction?: HandlerProp;
|
|
72
72
|
className?: ClassNameProp;
|
|
73
73
|
};
|
|
74
|
+
/** @see AuthExpiryProvider — central, deduplicated handling of an expired session (gh#1022). */
|
|
75
|
+
export type AuthExpiryProviderProp = {
|
|
76
|
+
/**
|
|
77
|
+
* Called ONCE when a query/mutation surface under the provider meets an auth-class error (401 /
|
|
78
|
+
* invalid or expired token); simultaneous 401s share one call, re-armed after every auth-errored
|
|
79
|
+
* view has gone. Typically redirects to the IdP with the current URL as the return target.
|
|
80
|
+
* Return a promise for a silent refresh: a rejection brings back the sign-in alert.
|
|
81
|
+
*/
|
|
82
|
+
onAuthExpired: (context: {
|
|
83
|
+
error: unknown;
|
|
84
|
+
}) => void | Promise<void>;
|
|
85
|
+
children?: ChildrenProp;
|
|
86
|
+
};
|
|
74
87
|
/** @see Alert */
|
|
75
88
|
export type AlertProp = React.HTMLAttributes<HTMLDivElement> & {
|
|
76
89
|
variant?: AlertVariantProp;
|
|
@@ -3,7 +3,7 @@ export type { ButtonProp } from "./general.prop.js";
|
|
|
3
3
|
export type { CommandProp, InputProp, TextareaProp, FormFieldProp, SearchInputProp, CheckboxProp, CheckboxGroupProp, ChoiceOptionProp, RadioProp, RadioGroupProp, SwitchProp, SliderProp, CalendarProp, DatePickerProp, TimePickerProp, ColorPickerProp, UploadProp, UploadFileItemProp, UploadVariantProp, TreeOptionProp, TreeFieldNamesProp, CascaderProp, TreeSelectProp, ShowCheckedStrategyProp, TransferProp, TransferItemProp, } from "./data-entry.prop.js";
|
|
4
4
|
export type { AvatarProp, EmptyStateProp, DescriptionsProp, DescriptionsItemProp, BadgeProp, DataTableProp, QrCodeProp, } from "./data-display.prop.js";
|
|
5
5
|
export type { ChartDatum, ChartSeriesProp, LineChartProp, BarChartProp, AreaChartProp, PieChartProp, } from "./charts.prop.js";
|
|
6
|
-
export type { AlertQueryErrorProp, AlertProp, DialogProp, DialogContentProp, AlertTitleProp, AlertContentProp, AlertDescriptionProp, AlertActionsProp, SheetResponsiveProp, SkeletonRowsProp, } from "./feedback.prop.js";
|
|
6
|
+
export type { AlertQueryErrorProp, AuthExpiryProviderProp, AlertProp, DialogProp, DialogContentProp, AlertTitleProp, AlertContentProp, AlertDescriptionProp, AlertActionsProp, SheetResponsiveProp, SkeletonRowsProp, } from "./feedback.prop.js";
|
|
7
7
|
export type { DataStateProp, InfiniteQueryStateProp, InfiniteQueryHelpers, PrefetchLinkProp, } from "./query.prop.js";
|
|
8
8
|
export type { DropdownMenuPlacementProp, DropdownMenuTriggerActionProp, PaginationProp, PaginationSizeProp, PaginationAlignProp, StepsProp, StepItemProp, StepStatusProp, StepsTypeProp, TabsProp, TabItemProp, TabsVariantProp, TabsPlacementProp, TabsExtraProp, TabsOnEditProp, } from "./navigation.prop.js";
|
|
9
9
|
export type { AppProviderProp, AppContextValue, AppSettingKind, AppSettingPickerProp, AppSettingToggleKind, AppSettingToggleProp, ThemeScopeProp, } from "./app.prop.js";
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -848,7 +848,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
848
848
|
readonly vocabulary: readonly ["OrientationProp", {
|
|
849
849
|
readonly field: "vertical";
|
|
850
850
|
readonly local: true;
|
|
851
|
-
readonly reason:
|
|
851
|
+
readonly reason: 'antd\'s boolean spelling of orientation="vertical"; orientation wins over it.';
|
|
852
852
|
}, {
|
|
853
853
|
readonly field: "fullWidth";
|
|
854
854
|
readonly local: true;
|
|
@@ -2415,6 +2415,15 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2415
2415
|
readonly file: "components/query.prop.ts";
|
|
2416
2416
|
readonly vocabulary: readonly [];
|
|
2417
2417
|
};
|
|
2418
|
+
readonly AuthExpiryProviderProp: {
|
|
2419
|
+
readonly group: "feedback";
|
|
2420
|
+
readonly file: "components/feedback.prop.ts";
|
|
2421
|
+
readonly vocabulary: readonly [{
|
|
2422
|
+
readonly field: "onAuthExpired";
|
|
2423
|
+
readonly local: true;
|
|
2424
|
+
readonly reason: "App-wide session-expiry callback receiving the triggering error; fired once per expiry (gh#1022).";
|
|
2425
|
+
}, "ChildrenProp"];
|
|
2426
|
+
};
|
|
2418
2427
|
readonly AlertQueryErrorProp: {
|
|
2419
2428
|
readonly group: "feedback";
|
|
2420
2429
|
readonly file: "components/feedback.prop.ts";
|
package/dist/props/registry.js
CHANGED
|
@@ -2833,6 +2833,18 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2833
2833
|
vocabulary: ["HandlerProp"]
|
|
2834
2834
|
},
|
|
2835
2835
|
PrefetchLinkProp: { group: "query", file: "components/query.prop.ts", vocabulary: [] },
|
|
2836
|
+
AuthExpiryProviderProp: {
|
|
2837
|
+
group: "feedback",
|
|
2838
|
+
file: "components/feedback.prop.ts",
|
|
2839
|
+
vocabulary: [
|
|
2840
|
+
{
|
|
2841
|
+
field: "onAuthExpired",
|
|
2842
|
+
local: true,
|
|
2843
|
+
reason: "App-wide session-expiry callback receiving the triggering error; fired once per expiry (gh#1022)."
|
|
2844
|
+
},
|
|
2845
|
+
"ChildrenProp"
|
|
2846
|
+
]
|
|
2847
|
+
},
|
|
2836
2848
|
AlertQueryErrorProp: {
|
|
2837
2849
|
group: "feedback",
|
|
2838
2850
|
file: "components/feedback.prop.ts",
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
color: hsl(var(--foreground));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
[data-slot="alert"][data-query-category="auth"] {
|
|
54
|
+
max-inline-size: var(--query-auth-alert-max-inline-size);
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
[data-slot="alert"][data-variant="banner"] {
|
|
54
58
|
border-radius: var(--banner-radius);
|
|
55
59
|
border-block-start-width: 0;
|
package/dist/styles/layers.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "31.
|
|
2
|
+
"version": "31.3.0",
|
|
3
3
|
"generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
|
|
4
4
|
"base": "base.css",
|
|
5
5
|
"fonts": "fonts.css",
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
"layers": [
|
|
68
68
|
"layout.css",
|
|
69
69
|
"control.css",
|
|
70
|
+
"text-layout.css",
|
|
71
|
+
"dialog-layout.css",
|
|
70
72
|
"alert-layout.css"
|
|
71
73
|
]
|
|
72
74
|
},
|
|
@@ -74,6 +76,7 @@
|
|
|
74
76
|
"layers": [
|
|
75
77
|
"layout.css",
|
|
76
78
|
"control.css",
|
|
79
|
+
"text-layout.css",
|
|
77
80
|
"dialog-layout.css",
|
|
78
81
|
"alert-layout.css",
|
|
79
82
|
"form-layout.css"
|
|
@@ -83,6 +86,7 @@
|
|
|
83
86
|
"layers": [
|
|
84
87
|
"layout.css",
|
|
85
88
|
"control.css",
|
|
89
|
+
"text-layout.css",
|
|
86
90
|
"dialog-layout.css",
|
|
87
91
|
"alert-layout.css",
|
|
88
92
|
"form-layout.css"
|
|
@@ -99,6 +103,7 @@
|
|
|
99
103
|
"shell-layout.css",
|
|
100
104
|
"layout.css",
|
|
101
105
|
"control.css",
|
|
106
|
+
"text-layout.css",
|
|
102
107
|
"dialog-layout.css",
|
|
103
108
|
"alert-layout.css",
|
|
104
109
|
"form-layout.css"
|
|
@@ -157,6 +162,9 @@
|
|
|
157
162
|
"layout.css"
|
|
158
163
|
]
|
|
159
164
|
},
|
|
165
|
+
"AuthExpiryProvider": {
|
|
166
|
+
"layers": []
|
|
167
|
+
},
|
|
160
168
|
"AuthFooter": {
|
|
161
169
|
"layers": [
|
|
162
170
|
"shell-layout.css"
|
|
@@ -195,6 +203,8 @@
|
|
|
195
203
|
"layers": [
|
|
196
204
|
"layout.css",
|
|
197
205
|
"control.css",
|
|
206
|
+
"text-layout.css",
|
|
207
|
+
"dialog-layout.css",
|
|
198
208
|
"alert-layout.css"
|
|
199
209
|
]
|
|
200
210
|
},
|
|
@@ -241,6 +251,8 @@
|
|
|
241
251
|
"layers": [
|
|
242
252
|
"layout.css",
|
|
243
253
|
"control.css",
|
|
254
|
+
"text-layout.css",
|
|
255
|
+
"dialog-layout.css",
|
|
244
256
|
"alert-layout.css"
|
|
245
257
|
]
|
|
246
258
|
},
|
|
@@ -357,6 +369,7 @@
|
|
|
357
369
|
"layers": [
|
|
358
370
|
"layout.css",
|
|
359
371
|
"control.css",
|
|
372
|
+
"text-layout.css",
|
|
360
373
|
"dialog-layout.css",
|
|
361
374
|
"alert-layout.css",
|
|
362
375
|
"form-layout.css"
|
|
@@ -390,6 +403,8 @@
|
|
|
390
403
|
"layers": [
|
|
391
404
|
"layout.css",
|
|
392
405
|
"control.css",
|
|
406
|
+
"text-layout.css",
|
|
407
|
+
"dialog-layout.css",
|
|
393
408
|
"alert-layout.css"
|
|
394
409
|
]
|
|
395
410
|
},
|
|
@@ -397,6 +412,7 @@
|
|
|
397
412
|
"layers": [
|
|
398
413
|
"layout.css",
|
|
399
414
|
"control.css",
|
|
415
|
+
"text-layout.css",
|
|
400
416
|
"table-layout.css",
|
|
401
417
|
"dialog-layout.css",
|
|
402
418
|
"alert-layout.css",
|
|
@@ -423,6 +439,7 @@
|
|
|
423
439
|
"layers": [
|
|
424
440
|
"layout.css",
|
|
425
441
|
"control.css",
|
|
442
|
+
"text-layout.css",
|
|
426
443
|
"dialog-layout.css",
|
|
427
444
|
"alert-layout.css",
|
|
428
445
|
"form-layout.css"
|
|
@@ -490,6 +507,8 @@
|
|
|
490
507
|
"layers": [
|
|
491
508
|
"layout.css",
|
|
492
509
|
"control.css",
|
|
510
|
+
"text-layout.css",
|
|
511
|
+
"dialog-layout.css",
|
|
493
512
|
"alert-layout.css",
|
|
494
513
|
"form-layout.css"
|
|
495
514
|
]
|
|
@@ -498,6 +517,8 @@
|
|
|
498
517
|
"layers": [
|
|
499
518
|
"layout.css",
|
|
500
519
|
"control.css",
|
|
520
|
+
"text-layout.css",
|
|
521
|
+
"dialog-layout.css",
|
|
501
522
|
"alert-layout.css"
|
|
502
523
|
]
|
|
503
524
|
},
|
|
@@ -505,6 +526,8 @@
|
|
|
505
526
|
"layers": [
|
|
506
527
|
"layout.css",
|
|
507
528
|
"control.css",
|
|
529
|
+
"text-layout.css",
|
|
530
|
+
"dialog-layout.css",
|
|
508
531
|
"alert-layout.css",
|
|
509
532
|
"form-layout.css"
|
|
510
533
|
]
|
|
@@ -516,6 +539,8 @@
|
|
|
516
539
|
"layers": [
|
|
517
540
|
"layout.css",
|
|
518
541
|
"control.css",
|
|
542
|
+
"text-layout.css",
|
|
543
|
+
"dialog-layout.css",
|
|
519
544
|
"alert-layout.css",
|
|
520
545
|
"form-layout.css"
|
|
521
546
|
]
|
|
@@ -524,6 +549,8 @@
|
|
|
524
549
|
"layers": [
|
|
525
550
|
"layout.css",
|
|
526
551
|
"control.css",
|
|
552
|
+
"text-layout.css",
|
|
553
|
+
"dialog-layout.css",
|
|
527
554
|
"alert-layout.css",
|
|
528
555
|
"form-layout.css"
|
|
529
556
|
]
|
|
@@ -549,6 +576,8 @@
|
|
|
549
576
|
"layers": [
|
|
550
577
|
"layout.css",
|
|
551
578
|
"control.css",
|
|
579
|
+
"text-layout.css",
|
|
580
|
+
"dialog-layout.css",
|
|
552
581
|
"alert-layout.css"
|
|
553
582
|
]
|
|
554
583
|
},
|
|
@@ -649,6 +678,7 @@
|
|
|
649
678
|
"shell-layout.css",
|
|
650
679
|
"layout.css",
|
|
651
680
|
"control.css",
|
|
681
|
+
"text-layout.css",
|
|
652
682
|
"dialog-layout.css",
|
|
653
683
|
"alert-layout.css",
|
|
654
684
|
"form-layout.css"
|
|
@@ -666,6 +696,7 @@
|
|
|
666
696
|
"layers": [
|
|
667
697
|
"layout.css",
|
|
668
698
|
"control.css",
|
|
699
|
+
"text-layout.css",
|
|
669
700
|
"dialog-layout.css",
|
|
670
701
|
"alert-layout.css",
|
|
671
702
|
"form-layout.css",
|
|
@@ -1103,6 +1134,7 @@
|
|
|
1103
1134
|
"layers": [
|
|
1104
1135
|
"layout.css",
|
|
1105
1136
|
"control.css",
|
|
1137
|
+
"text-layout.css",
|
|
1106
1138
|
"dialog-layout.css",
|
|
1107
1139
|
"alert-layout.css",
|
|
1108
1140
|
"data-display-layout.css",
|
|
@@ -1150,6 +1182,7 @@
|
|
|
1150
1182
|
"layers": [
|
|
1151
1183
|
"layout.css",
|
|
1152
1184
|
"control.css",
|
|
1185
|
+
"text-layout.css",
|
|
1153
1186
|
"dialog-layout.css",
|
|
1154
1187
|
"alert-layout.css",
|
|
1155
1188
|
"data-display-layout.css",
|
|
@@ -1162,6 +1195,7 @@
|
|
|
1162
1195
|
"layers": [
|
|
1163
1196
|
"layout.css",
|
|
1164
1197
|
"control.css",
|
|
1198
|
+
"text-layout.css",
|
|
1165
1199
|
"dialog-layout.css",
|
|
1166
1200
|
"alert-layout.css",
|
|
1167
1201
|
"form-layout.css"
|
|
@@ -133,6 +133,8 @@
|
|
|
133
133
|
--query-load-more-space-block-start: var(--space-stack-md);
|
|
134
134
|
--query-loading-more-space-block-start: var(--space-stack-sm);
|
|
135
135
|
|
|
136
|
+
--query-auth-alert-max-inline-size: 36rem;
|
|
137
|
+
|
|
136
138
|
--tooltip-max-width: 20rem;
|
|
137
139
|
--tooltip-space-inline: var(--space-2);
|
|
138
140
|
--tooltip-space-block: var(--space-1);
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
|
|
4
4
|
> Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
|
|
5
5
|
|
|
6
|
-
- Public components: **
|
|
7
|
-
- With ≥1 frame: **152** · zero-frame: **
|
|
6
|
+
- Public components: **176**
|
|
7
|
+
- With ≥1 frame: **152** · zero-frame: **24**
|
|
8
8
|
- Fully covered (every contract axis declared): **0**
|
|
9
9
|
|
|
10
10
|
## UNTESTED — no `/frame/**` route
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- [ ] `Paragraph`
|
|
21
21
|
- [ ] `Link`
|
|
22
22
|
- [ ] `ServiceCatalogCta`
|
|
23
|
+
- [ ] `AuthExpiryProvider`
|
|
23
24
|
- [ ] `SkeletonRows`
|
|
24
25
|
- [ ] `OverlayPortalProvider`
|
|
25
26
|
- [ ] `SkeletonDetail`
|
|
@@ -101,6 +102,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
101
102
|
| TimelineGrid | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
102
103
|
| Table | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
103
104
|
| DataState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
|
|
105
|
+
| AuthExpiryProvider | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
104
106
|
| InfiniteQueryState | ✓ | N/A | · | N/A | · | · | · | · | · | · |
|
|
105
107
|
| Form | ✓ | ✓ | · | N/A | · | · | · | · | · | · |
|
|
106
108
|
| FormField | ✓ | N/A | · | N/A | · | · | · | · | · | · |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { QueryClient, QueryClientProvider, useMutation, useQuery } from "@tanstack/react-query";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
DataTable,
|
|
11
|
+
type ColumnDef,
|
|
12
|
+
} from "@godxjp/ui/data-display";
|
|
13
|
+
import { SkeletonTable } from "@godxjp/ui/feedback";
|
|
14
|
+
import { Button, Text } from "@godxjp/ui/general";
|
|
15
|
+
import { useTranslation } from "@godxjp/ui/i18n";
|
|
16
|
+
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
17
|
+
import { AlertMutationFeedback, AuthExpiryProvider, DataState } from "@godxjp/ui/query";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* AuthExpiryProvider · the app root owns an expired session (gh#1022). Every DataState /
|
|
21
|
+
* InfiniteQueryState / AlertMutationFeedback / AlertQueryError underneath calls `onAuthExpired`
|
|
22
|
+
* automatically, once for any number of simultaneous 401s, and shows a neutral pending state
|
|
23
|
+
* instead of an error alert. A real app redirects to its IdP here:
|
|
24
|
+
* window.location.assign(`/auth/login?return_to=${encodeURIComponent(location.href)}`)
|
|
25
|
+
*/
|
|
26
|
+
const queryClient = new QueryClient({
|
|
27
|
+
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
type Member = { id: string; name: string };
|
|
31
|
+
|
|
32
|
+
const expired = () => Object.assign(new Error("Access token expired"), { status: 401 });
|
|
33
|
+
|
|
34
|
+
function MembersCard() {
|
|
35
|
+
const { t } = useTranslation();
|
|
36
|
+
const columns: ColumnDef<Member>[] = [
|
|
37
|
+
{ key: "id", header: "ID" },
|
|
38
|
+
{ key: "name", header: t("authExpiryDocs.nameColumn") },
|
|
39
|
+
];
|
|
40
|
+
const query = useQuery<Member[]>({
|
|
41
|
+
queryKey: ["auth-expiry-members"],
|
|
42
|
+
queryFn: async () => {
|
|
43
|
+
throw expired();
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return (
|
|
47
|
+
<Card>
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<CardTitle level={2}>{t("authExpiryDocs.membersTitle")}</CardTitle>
|
|
50
|
+
<CardDescription>A 401 keeps the skeleton and announces the redirect.</CardDescription>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardContent flush>
|
|
53
|
+
<DataState query={query} skeleton={<SkeletonTable rows={3} columns={2} />}>
|
|
54
|
+
{(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
|
|
55
|
+
</DataState>
|
|
56
|
+
</CardContent>
|
|
57
|
+
</Card>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function InviteCard() {
|
|
62
|
+
const { t } = useTranslation();
|
|
63
|
+
const mutation = useMutation({
|
|
64
|
+
mutationFn: async () => {
|
|
65
|
+
throw expired();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return (
|
|
69
|
+
<Card>
|
|
70
|
+
<CardHeader>
|
|
71
|
+
<CardTitle level={2}>{t("authExpiryDocs.inviteTitle")}</CardTitle>
|
|
72
|
+
<CardDescription>A 401 on submit shows a muted status line, not an alert.</CardDescription>
|
|
73
|
+
</CardHeader>
|
|
74
|
+
<CardContent>
|
|
75
|
+
<Flex direction="col" gap="md" align="start">
|
|
76
|
+
<Button onClick={() => mutation.mutate()}>{t("authExpiryDocs.send")}</Button>
|
|
77
|
+
<AlertMutationFeedback mutation={mutation} />
|
|
78
|
+
</Flex>
|
|
79
|
+
</CardContent>
|
|
80
|
+
</Card>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function Demo() {
|
|
85
|
+
const [calls, setCalls] = useState(0);
|
|
86
|
+
return (
|
|
87
|
+
<QueryClientProvider client={queryClient}>
|
|
88
|
+
<AuthExpiryProvider onAuthExpired={() => setCalls((n) => n + 1)}>
|
|
89
|
+
<PageContainer
|
|
90
|
+
title="AuthExpiryProvider"
|
|
91
|
+
subtitle="Expired session handled once at the app root, never as a page error"
|
|
92
|
+
>
|
|
93
|
+
<Flex direction="col" gap="lg">
|
|
94
|
+
<Text size="sm" tone="muted">
|
|
95
|
+
onAuthExpired calls: {calls}
|
|
96
|
+
</Text>
|
|
97
|
+
<MembersCard />
|
|
98
|
+
<InviteCard />
|
|
99
|
+
</Flex>
|
|
100
|
+
</PageContainer>
|
|
101
|
+
</AuthExpiryProvider>
|
|
102
|
+
</QueryClientProvider>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -1438,5 +1438,11 @@
|
|
|
1438
1438
|
"sample": "Mẫu",
|
|
1439
1439
|
"date": "Tháng 9/2026 · ngày dùng cho demo"
|
|
1440
1440
|
}
|
|
1441
|
+
},
|
|
1442
|
+
"authExpiryDocs": {
|
|
1443
|
+
"membersTitle": "Thành viên",
|
|
1444
|
+
"nameColumn": "Họ tên",
|
|
1445
|
+
"inviteTitle": "Gửi lời mời",
|
|
1446
|
+
"send": "Gửi"
|
|
1441
1447
|
}
|
|
1442
1448
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -12,8 +13,9 @@ import {
|
|
|
12
13
|
type ColumnDef,
|
|
13
14
|
} from "@godxjp/ui/data-display";
|
|
14
15
|
import { SkeletonTable } from "@godxjp/ui/feedback";
|
|
16
|
+
import { Text } from "@godxjp/ui/general";
|
|
15
17
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
16
|
-
import { DataState } from "@godxjp/ui/query";
|
|
18
|
+
import { AuthExpiryProvider, DataState } from "@godxjp/ui/query";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* DataState · drives skeleton / error / empty / success for ONE useQuery block.
|
|
@@ -99,8 +101,40 @@ function PrerequisiteBlock() {
|
|
|
99
101
|
);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function ExpiredSessionQuery({ queryKey }: { queryKey: string }) {
|
|
105
|
+
const query = useQuery<Invoice[]>({
|
|
106
|
+
queryKey: [queryKey],
|
|
107
|
+
queryFn: async () => {
|
|
108
|
+
throw Object.assign(new Error("Access token expired"), { status: 401 });
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return (
|
|
112
|
+
<DataState query={query} skeleton={<SkeletonTable rows={3} />}>
|
|
113
|
+
{(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
|
|
114
|
+
</DataState>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function AuthExpiryBlock() {
|
|
119
|
+
// The app root owns session expiry (gh#1022). A real app redirects to the IdP here:
|
|
120
|
+
// window.location.assign(`/auth/login?return_to=${encodeURIComponent(location.href)}`)
|
|
121
|
+
// Two queries 401 at once — the handler still runs exactly once, and no alert is painted.
|
|
122
|
+
const [calls, setCalls] = useState(0);
|
|
123
|
+
return (
|
|
124
|
+
<AuthExpiryProvider onAuthExpired={() => setCalls((n) => n + 1)}>
|
|
125
|
+
<Flex direction="col" gap="md">
|
|
126
|
+
<Text size="sm" tone="muted">
|
|
127
|
+
onAuthExpired calls: {calls}
|
|
128
|
+
</Text>
|
|
129
|
+
<ExpiredSessionQuery queryKey="ds-auth-expiry-a" />
|
|
130
|
+
<ExpiredSessionQuery queryKey="ds-auth-expiry-b" />
|
|
131
|
+
</Flex>
|
|
132
|
+
</AuthExpiryProvider>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
102
136
|
function AuthErrorBlock() {
|
|
103
|
-
//
|
|
137
|
+
// Without an AuthExpiryProvider: the proportionate fallback — a neutral sign-in prompt.
|
|
104
138
|
const query = useQuery<Invoice[]>({
|
|
105
139
|
queryKey: ["ds-auth-error"],
|
|
106
140
|
queryFn: async () => {
|
|
@@ -175,10 +209,24 @@ export default function Demo() {
|
|
|
175
209
|
|
|
176
210
|
<Card>
|
|
177
211
|
<CardHeader>
|
|
178
|
-
<CardTitle level={2}>
|
|
212
|
+
<CardTitle level={2}>Session expired · AuthExpiryProvider (recommended)</CardTitle>
|
|
213
|
+
<CardDescription>
|
|
214
|
+
Mount `AuthExpiryProvider` once at the app root. A 401 calls `onAuthExpired`
|
|
215
|
+
automatically, once for any number of simultaneous 401s, while each DataState keeps
|
|
216
|
+
its skeleton and announces the redirect to sign-in through a live region.
|
|
217
|
+
</CardDescription>
|
|
218
|
+
</CardHeader>
|
|
219
|
+
<CardContent>
|
|
220
|
+
<AuthExpiryBlock />
|
|
221
|
+
</CardContent>
|
|
222
|
+
</Card>
|
|
223
|
+
|
|
224
|
+
<Card>
|
|
225
|
+
<CardHeader>
|
|
226
|
+
<CardTitle level={2}>Session expired · fallback without a provider</CardTitle>
|
|
179
227
|
<CardDescription>
|
|
180
|
-
|
|
181
|
-
|
|
228
|
+
Without a provider a 401 renders a neutral, width-capped sign-in prompt whose button
|
|
229
|
+
calls `onAuthError`. It never offers a blind retry or shows the raw token message.
|
|
182
230
|
</CardDescription>
|
|
183
231
|
</CardHeader>
|
|
184
232
|
<CardContent>
|