@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1
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/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3801
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1733
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -88
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-query.test.tsx +0 -149
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, jest, test } from "bun:test";
|
|
2
|
-
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
3
|
-
import { act, renderHook } from "@testing-library/react";
|
|
4
|
-
import type { ReactNode } from "react";
|
|
5
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
6
|
-
import { useQuery } from "../use-query";
|
|
7
|
-
|
|
8
|
-
type QueryFn = Dispatcher["query"];
|
|
9
|
-
|
|
10
|
-
function makeDispatcher(query: QueryFn): Dispatcher {
|
|
11
|
-
return {
|
|
12
|
-
write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
|
|
13
|
-
query,
|
|
14
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
15
|
-
statusStore: {
|
|
16
|
-
getState: () => "online",
|
|
17
|
-
subscribe: () => () => {},
|
|
18
|
-
} as unknown as Dispatcher["statusStore"],
|
|
19
|
-
async *stream() {},
|
|
20
|
-
pendingWrites: () => [],
|
|
21
|
-
pendingFiles: () => [],
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function wrapperFor(dispatcher: Dispatcher) {
|
|
26
|
-
return ({ children }: { readonly children: ReactNode }) => (
|
|
27
|
-
<DispatcherProvider dispatcher={dispatcher}>{children}</DispatcherProvider>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function countingDispatcher(): { dispatcher: Dispatcher; calls: () => number } {
|
|
32
|
-
let calls = 0;
|
|
33
|
-
const query = (async () => {
|
|
34
|
-
calls += 1;
|
|
35
|
-
return { isSuccess: true, data: { n: calls } };
|
|
36
|
-
}) as unknown as QueryFn;
|
|
37
|
-
return { dispatcher: makeDispatcher(query), calls: () => calls };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function advance(ms: number): Promise<void> {
|
|
41
|
-
await act(async () => {
|
|
42
|
-
jest.advanceTimersByTime(ms);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const QUERY_TYPE = "f:query:x:list";
|
|
47
|
-
|
|
48
|
-
describe("useQuery refetchIntervalMs", () => {
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
jest.useFakeTimers();
|
|
51
|
-
});
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
jest.useRealTimers();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("re-runs the query on every interval tick", async () => {
|
|
57
|
-
const { dispatcher, calls } = countingDispatcher();
|
|
58
|
-
const { result } = renderHook(
|
|
59
|
-
() => useQuery<{ n: number }>(QUERY_TYPE, {}, { refetchIntervalMs: 1000 }),
|
|
60
|
-
{
|
|
61
|
-
wrapper: wrapperFor(dispatcher),
|
|
62
|
-
},
|
|
63
|
-
);
|
|
64
|
-
await advance(0);
|
|
65
|
-
expect(calls()).toBe(1);
|
|
66
|
-
|
|
67
|
-
await advance(1000);
|
|
68
|
-
expect(calls()).toBe(2);
|
|
69
|
-
await advance(1000);
|
|
70
|
-
expect(calls()).toBe(3);
|
|
71
|
-
expect(result.current.data).toEqual({ n: 3 });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("unmount stops the ticks", async () => {
|
|
75
|
-
const { dispatcher, calls } = countingDispatcher();
|
|
76
|
-
const { unmount } = renderHook(() => useQuery(QUERY_TYPE, {}, { refetchIntervalMs: 1000 }), {
|
|
77
|
-
wrapper: wrapperFor(dispatcher),
|
|
78
|
-
});
|
|
79
|
-
await advance(1000);
|
|
80
|
-
const before = calls();
|
|
81
|
-
unmount();
|
|
82
|
-
await advance(5000);
|
|
83
|
-
expect(calls()).toBe(before);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("enabled: false never ticks", async () => {
|
|
87
|
-
const { dispatcher, calls } = countingDispatcher();
|
|
88
|
-
renderHook(() => useQuery(QUERY_TYPE, {}, { enabled: false, refetchIntervalMs: 1000 }), {
|
|
89
|
-
wrapper: wrapperFor(dispatcher),
|
|
90
|
-
});
|
|
91
|
-
await advance(5000);
|
|
92
|
-
expect(calls()).toBe(0);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("a tick while a fetch is in flight is skipped instead of aborting it", async () => {
|
|
96
|
-
let calls = 0;
|
|
97
|
-
let release: (() => void) | undefined;
|
|
98
|
-
const query = (async (_type: string, _payload: unknown, opts?: { signal?: AbortSignal }) => {
|
|
99
|
-
calls += 1;
|
|
100
|
-
await new Promise<void>((resolve) => {
|
|
101
|
-
release = resolve;
|
|
102
|
-
});
|
|
103
|
-
return opts?.signal?.aborted
|
|
104
|
-
? { isSuccess: false, error: { code: "aborted", message: "aborted", i18nKey: "x" } }
|
|
105
|
-
: { isSuccess: true, data: { done: true } };
|
|
106
|
-
}) as unknown as QueryFn;
|
|
107
|
-
const { result } = renderHook(
|
|
108
|
-
() => useQuery<{ done: boolean }>(QUERY_TYPE, {}, { refetchIntervalMs: 1000 }),
|
|
109
|
-
{
|
|
110
|
-
wrapper: wrapperFor(makeDispatcher(query)),
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
await advance(0);
|
|
114
|
-
expect(calls).toBe(1);
|
|
115
|
-
|
|
116
|
-
await advance(3000);
|
|
117
|
-
expect(calls).toBe(1);
|
|
118
|
-
|
|
119
|
-
await act(async () => {
|
|
120
|
-
release?.();
|
|
121
|
-
});
|
|
122
|
-
expect(result.current.data).toEqual({ done: true });
|
|
123
|
-
|
|
124
|
-
await advance(1000);
|
|
125
|
-
expect(calls).toBe(2);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("background ticks do not flip loading to true", async () => {
|
|
129
|
-
const { dispatcher, calls } = countingDispatcher();
|
|
130
|
-
const seenLoading: boolean[] = [];
|
|
131
|
-
const { result } = renderHook(
|
|
132
|
-
() => {
|
|
133
|
-
const r = useQuery<{ n: number }>(QUERY_TYPE, {}, { refetchIntervalMs: 1000 });
|
|
134
|
-
seenLoading.push(r.loading);
|
|
135
|
-
return r;
|
|
136
|
-
},
|
|
137
|
-
{ wrapper: wrapperFor(dispatcher) },
|
|
138
|
-
);
|
|
139
|
-
await advance(0);
|
|
140
|
-
expect(result.current.loading).toBe(false);
|
|
141
|
-
seenLoading.length = 0;
|
|
142
|
-
|
|
143
|
-
await advance(1000);
|
|
144
|
-
await advance(1000);
|
|
145
|
-
expect(calls()).toBe(3);
|
|
146
|
-
expect(result.current.data).toEqual({ n: 3 });
|
|
147
|
-
expect(seenLoading.every((l) => l === false)).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { Dispatcher, DispatcherError } from "@cosmicdrift/kumiko-headless";
|
|
3
|
-
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
-
import type { ReactNode } from "react";
|
|
5
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
6
|
-
import { useStreamHandler } from "../use-stream-handler";
|
|
7
|
-
|
|
8
|
-
function makeDispatcher(streamImpl: unknown): Dispatcher {
|
|
9
|
-
return {
|
|
10
|
-
write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
|
|
11
|
-
query: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["query"],
|
|
12
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
13
|
-
stream: streamImpl as Dispatcher["stream"],
|
|
14
|
-
statusStore: {
|
|
15
|
-
getState: () => "online",
|
|
16
|
-
subscribe: () => () => {},
|
|
17
|
-
} as unknown as Dispatcher["statusStore"],
|
|
18
|
-
pendingWrites: () => [],
|
|
19
|
-
pendingFiles: () => [],
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function wrapperFor(dispatcher: Dispatcher) {
|
|
24
|
-
return ({ children }: { readonly children: ReactNode }) => (
|
|
25
|
-
<DispatcherProvider dispatcher={dispatcher}>{children}</DispatcherProvider>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("useStreamHandler", () => {
|
|
30
|
-
test("start accumulates chunks then status=done", async () => {
|
|
31
|
-
const dispatcher = makeDispatcher(async function* () {
|
|
32
|
-
yield { i: 0 };
|
|
33
|
-
yield { i: 1 };
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const { result } = renderHook(() => useStreamHandler<{ i: number }>("f:stream:x:tail"), {
|
|
37
|
-
wrapper: wrapperFor(dispatcher),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await act(async () => {
|
|
41
|
-
await result.current.start({ count: 2 });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
await waitFor(() => expect(result.current.status).toBe("done"));
|
|
45
|
-
expect(result.current.chunks).toEqual([{ i: 0 }, { i: 1 }]);
|
|
46
|
-
expect(result.current.error).toBeNull();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("stream error sets status=error and error envelope", async () => {
|
|
50
|
-
const err: DispatcherError = {
|
|
51
|
-
code: "access_denied",
|
|
52
|
-
httpStatus: 403,
|
|
53
|
-
i18nKey: "errors.access",
|
|
54
|
-
message: "denied",
|
|
55
|
-
};
|
|
56
|
-
const dispatcher = makeDispatcher(async function* () {
|
|
57
|
-
yield* []; // satisfy generator shape; error is the only outcome
|
|
58
|
-
throw err;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const { result } = renderHook(() => useStreamHandler("f:stream:x:tail"), {
|
|
62
|
-
wrapper: wrapperFor(dispatcher),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
await act(async () => {
|
|
66
|
-
await result.current.start();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await waitFor(() => expect(result.current.status).toBe("error"));
|
|
70
|
-
expect(result.current.error?.code).toBe("access_denied");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("abort during stream resets status to idle (no late done)", async () => {
|
|
74
|
-
let release!: () => void;
|
|
75
|
-
const gate = new Promise<void>((r) => {
|
|
76
|
-
release = r;
|
|
77
|
-
});
|
|
78
|
-
const dispatcher = makeDispatcher(async function* (
|
|
79
|
-
_t: string,
|
|
80
|
-
_p: unknown,
|
|
81
|
-
opts?: { signal?: AbortSignal },
|
|
82
|
-
) {
|
|
83
|
-
yield { i: 0 };
|
|
84
|
-
await gate;
|
|
85
|
-
if (opts?.signal?.aborted) {
|
|
86
|
-
const e = { code: "aborted", httpStatus: 0, i18nKey: "x", message: "aborted" };
|
|
87
|
-
throw e;
|
|
88
|
-
}
|
|
89
|
-
yield { i: 1 };
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const { result } = renderHook(() => useStreamHandler<{ i: number }>("f:stream:x:tail"), {
|
|
93
|
-
wrapper: wrapperFor(dispatcher),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
let started!: Promise<void>;
|
|
97
|
-
act(() => {
|
|
98
|
-
started = result.current.start();
|
|
99
|
-
});
|
|
100
|
-
await waitFor(() => expect(result.current.chunks).toEqual([{ i: 0 }]));
|
|
101
|
-
act(() => result.current.abort());
|
|
102
|
-
release();
|
|
103
|
-
await started;
|
|
104
|
-
expect(result.current.chunks).toEqual([{ i: 0 }]);
|
|
105
|
-
expect(result.current.status).toBe("idle");
|
|
106
|
-
});
|
|
107
|
-
});
|
package/src/hooks/use-ai-text.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { useDispatcher } from "../context/dispatcher-context";
|
|
4
|
-
|
|
5
|
-
// AiTextField/AiTextArea's client-side surface. Deliberately duplicates the
|
|
6
|
-
// wire-contract shape from kumiko-enterprise's `ai-text` feature instead of
|
|
7
|
-
// importing it — this package is public NPM, ai-text is a private Enterprise
|
|
8
|
-
// package, and the widgets only ever talk HTTP (ai-text-primitive plan doc,
|
|
9
|
-
// Architecture Decision 1). Keep these in lockstep with
|
|
10
|
-
// kumiko-enterprise/packages/ai-text/src/{modes,feature}.ts by hand; there's
|
|
11
|
-
// no compile-time link between the two repos.
|
|
12
|
-
|
|
13
|
-
export const AI_TEXT_RUN_QN = "ai-text:query:run";
|
|
14
|
-
|
|
15
|
-
export type AiTextMode = "complete" | "correct" | "translate" | "rewrite";
|
|
16
|
-
export type AiTextRewriteStyle = "formal" | "casual" | "concise" | "expand";
|
|
17
|
-
|
|
18
|
-
export type AiTextRunPayload =
|
|
19
|
-
| { readonly mode: "complete"; readonly text: string }
|
|
20
|
-
| { readonly mode: "correct"; readonly text: string }
|
|
21
|
-
| { readonly mode: "translate"; readonly text: string; readonly targetLanguage: string }
|
|
22
|
-
| { readonly mode: "rewrite"; readonly text: string; readonly style?: AiTextRewriteStyle };
|
|
23
|
-
|
|
24
|
-
export type AiTextUsage = { readonly inputTokens: number; readonly outputTokens: number };
|
|
25
|
-
|
|
26
|
-
export type AiTextRunResult =
|
|
27
|
-
| { readonly type: "text"; readonly text: string; readonly usage: AiTextUsage }
|
|
28
|
-
| { readonly type: "error"; readonly reason: string; readonly usage: AiTextUsage };
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// useAiTextAction — one-shot request/response, any mode
|
|
32
|
-
// =============================================================================
|
|
33
|
-
//
|
|
34
|
-
// v1 has no streaming (ai-text-primitive plan doc, sequencing note —
|
|
35
|
-
// SSE-with-auth-reuse needs a framework-core `r.streamHandler` primitive,
|
|
36
|
-
// tracked separately). `complete` goes through this same request/response
|
|
37
|
-
// path as correct/translate/rewrite; `useCompletion` below adds debounce on
|
|
38
|
-
// top for the ghost-text use-case specifically.
|
|
39
|
-
|
|
40
|
-
export type AiTextActionState =
|
|
41
|
-
| "idle"
|
|
42
|
-
| "loading"
|
|
43
|
-
| "success"
|
|
44
|
-
| "error"
|
|
45
|
-
| "cap-exceeded"
|
|
46
|
-
| "unavailable";
|
|
47
|
-
|
|
48
|
-
export type UseAiTextActionResult = {
|
|
49
|
-
readonly run: (payload: AiTextRunPayload) => Promise<AiTextRunResult | null>;
|
|
50
|
-
readonly state: AiTextActionState;
|
|
51
|
-
readonly result: AiTextRunResult | null;
|
|
52
|
-
readonly error: DispatcherError | null;
|
|
53
|
-
readonly reset: () => void;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function stateForError(error: DispatcherError): AiTextActionState {
|
|
57
|
-
if (error.code === "cap_exceeded" || error.code === "rate_limited") return "cap-exceeded";
|
|
58
|
-
if (error.code === "feature_disabled") return "unavailable";
|
|
59
|
-
return "error";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function useAiTextAction(): UseAiTextActionResult {
|
|
63
|
-
const dispatcher = useDispatcher();
|
|
64
|
-
const [state, setState] = useState<AiTextActionState>("idle");
|
|
65
|
-
const [result, setResult] = useState<AiTextRunResult | null>(null);
|
|
66
|
-
const [error, setError] = useState<DispatcherError | null>(null);
|
|
67
|
-
|
|
68
|
-
// Track the in-flight call so a newer `run()` cancels an older one, and
|
|
69
|
-
// unmount doesn't set state after the component is gone — same pattern
|
|
70
|
-
// as useQuery's activeCtrl.
|
|
71
|
-
const activeCtrl = useRef<AbortController | null>(null);
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
return () => {
|
|
74
|
-
activeCtrl.current?.abort();
|
|
75
|
-
};
|
|
76
|
-
}, []);
|
|
77
|
-
|
|
78
|
-
const run = useCallback(
|
|
79
|
-
async (payload: AiTextRunPayload): Promise<AiTextRunResult | null> => {
|
|
80
|
-
activeCtrl.current?.abort();
|
|
81
|
-
const ctrl = new AbortController();
|
|
82
|
-
activeCtrl.current = ctrl;
|
|
83
|
-
|
|
84
|
-
setState("loading");
|
|
85
|
-
setError(null);
|
|
86
|
-
|
|
87
|
-
const res = await dispatcher.query<AiTextRunResult>(AI_TEXT_RUN_QN, payload, {
|
|
88
|
-
signal: ctrl.signal,
|
|
89
|
-
});
|
|
90
|
-
if (ctrl.signal.aborted) return null;
|
|
91
|
-
|
|
92
|
-
if (!res.isSuccess) {
|
|
93
|
-
if (res.error.code === "aborted") return null;
|
|
94
|
-
setError(res.error);
|
|
95
|
-
setState(stateForError(res.error));
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
setResult(res.data);
|
|
100
|
-
setState("success");
|
|
101
|
-
return res.data;
|
|
102
|
-
},
|
|
103
|
-
[dispatcher],
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const reset = useCallback(() => {
|
|
107
|
-
activeCtrl.current?.abort();
|
|
108
|
-
setState("idle");
|
|
109
|
-
setResult(null);
|
|
110
|
-
setError(null);
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
return { run, state, result, error, reset };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// =============================================================================
|
|
117
|
-
// useCompletion — debounced ghost-text for AiTextField/AiTextArea
|
|
118
|
-
// =============================================================================
|
|
119
|
-
//
|
|
120
|
-
// Debounce exists to keep the request-rate down against the monthly cap,
|
|
121
|
-
// not for UX polish — every keystroke would otherwise burn a request.
|
|
122
|
-
|
|
123
|
-
export type UseCompletionResult = {
|
|
124
|
-
readonly suggestion: string | null;
|
|
125
|
-
readonly state: AiTextActionState;
|
|
126
|
-
readonly error: DispatcherError | null;
|
|
127
|
-
/** Debounced — schedules a completion request `debounceMs` after the
|
|
128
|
-
* last call. Calling again before the timer fires replaces it. */
|
|
129
|
-
readonly requestCompletion: (text: string) => void;
|
|
130
|
-
/** Cancels any pending/in-flight request and clears the suggestion. */
|
|
131
|
-
readonly clear: () => void;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const DEFAULT_DEBOUNCE_MS = 500;
|
|
135
|
-
|
|
136
|
-
export function useCompletion(debounceMs: number = DEFAULT_DEBOUNCE_MS): UseCompletionResult {
|
|
137
|
-
const { run, state, result, error, reset } = useAiTextAction();
|
|
138
|
-
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
139
|
-
|
|
140
|
-
const clearTimer = useCallback(() => {
|
|
141
|
-
if (timerRef.current !== undefined) {
|
|
142
|
-
clearTimeout(timerRef.current);
|
|
143
|
-
timerRef.current = undefined;
|
|
144
|
-
}
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
const requestCompletion = useCallback(
|
|
148
|
-
(text: string) => {
|
|
149
|
-
clearTimer();
|
|
150
|
-
if (text.length === 0) {
|
|
151
|
-
reset();
|
|
152
|
-
// skip: empty text, state already reset above
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
timerRef.current = setTimeout(() => {
|
|
156
|
-
void run({ mode: "complete", text });
|
|
157
|
-
}, debounceMs);
|
|
158
|
-
},
|
|
159
|
-
[clearTimer, reset, run, debounceMs],
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const clear = useCallback(() => {
|
|
163
|
-
clearTimer();
|
|
164
|
-
reset();
|
|
165
|
-
}, [clearTimer, reset]);
|
|
166
|
-
|
|
167
|
-
useEffect(() => clearTimer, [clearTimer]);
|
|
168
|
-
|
|
169
|
-
const suggestion = result?.type === "text" ? result.text : null;
|
|
170
|
-
|
|
171
|
-
return { suggestion, state, error, requestCompletion, clear };
|
|
172
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from "react";
|
|
2
|
-
|
|
3
|
-
// Open/close state for dialogs, sheets and collapsibles — the standard
|
|
4
|
-
// replacement for the hand-rolled `useState(false)` + toggle-callback
|
|
5
|
-
// trio in app screens. All callbacks are referentially stable.
|
|
6
|
-
|
|
7
|
-
export type UseDisclosureResult = {
|
|
8
|
-
readonly open: boolean;
|
|
9
|
-
readonly onOpen: () => void;
|
|
10
|
-
readonly onClose: () => void;
|
|
11
|
-
readonly onToggle: () => void;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function useDisclosure(initialOpen = false): UseDisclosureResult {
|
|
15
|
-
const [open, setOpen] = useState(initialOpen);
|
|
16
|
-
const onOpen = useCallback(() => setOpen(true), []);
|
|
17
|
-
const onClose = useCallback(() => setOpen(false), []);
|
|
18
|
-
const onToggle = useCallback(() => setOpen((prev) => !prev), []);
|
|
19
|
-
return { open, onOpen, onClose, onToggle };
|
|
20
|
-
}
|
package/src/hooks/use-form.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
FormController,
|
|
3
|
-
FormControllerOptions,
|
|
4
|
-
FormSnapshot,
|
|
5
|
-
FormValues,
|
|
6
|
-
} from "@cosmicdrift/kumiko-headless";
|
|
7
|
-
import { createFormController } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import { useMemo, useSyncExternalStore } from "react";
|
|
9
|
-
import { useOptionalDispatcher } from "../context/dispatcher-context";
|
|
10
|
-
|
|
11
|
-
// Thin React wrapper around createFormController. Returns both the
|
|
12
|
-
// controller (imperative — setField, submit, reset) and the current
|
|
13
|
-
// snapshot (reactive — values, errors, isDirty). React subscribes via
|
|
14
|
-
// useSyncExternalStore so re-renders happen exactly when the snapshot
|
|
15
|
-
// reference changes, which the controller guarantees.
|
|
16
|
-
//
|
|
17
|
-
// The `submit.dispatcher` config is filled from context if omitted,
|
|
18
|
-
// so the typical call site is just:
|
|
19
|
-
// const { snapshot, controller } = useForm({
|
|
20
|
-
// initial: { ... },
|
|
21
|
-
// schema,
|
|
22
|
-
// submit: { type: "foo:create" },
|
|
23
|
-
// });
|
|
24
|
-
// and the hook wires the ambient DispatcherProvider's dispatcher in.
|
|
25
|
-
|
|
26
|
-
export type UseFormOptions<TValues extends FormValues, TCtx = unknown> = FormControllerOptions<
|
|
27
|
-
TValues,
|
|
28
|
-
TCtx
|
|
29
|
-
>;
|
|
30
|
-
|
|
31
|
-
export type UseFormResult<TValues extends FormValues> = {
|
|
32
|
-
readonly controller: FormController<TValues>;
|
|
33
|
-
readonly snapshot: FormSnapshot<TValues>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export function useForm<TValues extends FormValues, TCtx = unknown>(
|
|
37
|
-
options: UseFormOptions<TValues, TCtx>,
|
|
38
|
-
): UseFormResult<TValues> {
|
|
39
|
-
// Optional: a form with no `submit` config (or one with an explicit
|
|
40
|
-
// dispatcher) never needs the ambient one, and mounting without a
|
|
41
|
-
// DispatcherProvider must not crash just because useForm was called.
|
|
42
|
-
const contextDispatcher = useOptionalDispatcher();
|
|
43
|
-
|
|
44
|
-
// The controller is created once per mount. `options` mutates across
|
|
45
|
-
// renders in normal React usage (closures get new references), but
|
|
46
|
-
// the controller's behaviour depends only on the initial shape — we
|
|
47
|
-
// deliberately don't re-create on option-identity change, which
|
|
48
|
-
// would wipe in-flight state and "forget" dirty edits on every
|
|
49
|
-
// parent re-render. If the app needs a reset, call controller.reset()
|
|
50
|
-
// or re-mount the form (key prop).
|
|
51
|
-
// Controller lifetime = hook lifetime. Options deliberately NOT in
|
|
52
|
-
// the deps array; re-creating on option-identity change would wipe
|
|
53
|
-
// in-flight state and dirty edits on every parent re-render.
|
|
54
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — controller is lifetime-scoped, not render-scoped
|
|
55
|
-
const controller = useMemo<FormController<TValues>>(() => {
|
|
56
|
-
// Default submit.dispatcher to the ambient one when omitted.
|
|
57
|
-
type FullOptions = FormControllerOptions<TValues, TCtx>;
|
|
58
|
-
const rawSubmit = options.submit;
|
|
59
|
-
const submitConfig: FullOptions["submit"] = rawSubmit
|
|
60
|
-
? { ...rawSubmit, dispatcher: rawSubmit.dispatcher ?? contextDispatcher }
|
|
61
|
-
: undefined;
|
|
62
|
-
const controllerOptions: FullOptions = {
|
|
63
|
-
...(options as FullOptions),
|
|
64
|
-
...(submitConfig !== undefined && { submit: submitConfig }),
|
|
65
|
-
};
|
|
66
|
-
return createFormController<TValues, TCtx>(controllerOptions);
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const snapshot = useSyncExternalStore(
|
|
70
|
-
(notify) => controller.subscribe(notify),
|
|
71
|
-
() => controller.getSnapshot(),
|
|
72
|
-
// SSR snapshot: same as getSnapshot(). The controller's initial
|
|
73
|
-
// snapshot is deterministic from options.initial, safe for server.
|
|
74
|
-
() => controller.getSnapshot(),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
return { controller, snapshot };
|
|
78
|
-
}
|