@cratis/scene.components 3.3.1 → 3.5.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/README.md +51 -2
- package/bindings/ArcBoundComponents.tsx +40 -0
- package/bindings/BoundConstructor.ts +5 -5
- package/bindings/for_requireArcProxy/NativeProxies.ts +28 -0
- package/bindings/for_requireArcProxy/when_mounting_bound_adapters.tsx +71 -0
- package/bindings/for_requireArcProxy/when_previewing_without_arc.tsx +37 -0
- package/bindings/for_requireArcProxy/when_validating_a_proxy.ts +44 -0
- package/bindings/requireArcProxy.ts +24 -0
- package/cratisComponents.ts +2 -1
- package/cratisComponentsPackage.ts +2 -1
- package/data/SceneDataTable.tsx +12 -2
- package/data/SceneObservableDataTable.tsx +1 -1
- package/data/SceneSingleResult.tsx +29 -0
- package/data/SingleResultRuntime.tsx +89 -0
- package/data/SingleResultStatus.tsx +13 -0
- package/data/for_SceneDataTable/when_forwarding_query_arguments.tsx +104 -0
- package/data/for_SceneSingleResult/when_executing_a_native_query.tsx +290 -0
- package/data/for_SceneSingleResult/when_gating_the_lazy_runtime.tsx +60 -0
- package/data/index.ts +1 -0
- package/dialogs/SceneCommandDialog.tsx +1 -1
- package/dialogs/SceneStepperCommandDialog.tsx +1 -1
- package/dist/cjs/bindings/ArcBoundComponents.js +55 -0
- package/dist/cjs/bindings/ArcBoundComponents.js.map +1 -0
- package/dist/cjs/bindings/requireArcProxy.js +25 -0
- package/dist/cjs/bindings/requireArcProxy.js.map +1 -0
- package/dist/cjs/cratisComponents.js +2 -0
- package/dist/cjs/cratisComponents.js.map +1 -1
- package/dist/cjs/cratisComponentsPackage.js +2 -1
- package/dist/cjs/cratisComponentsPackage.js.map +1 -1
- package/dist/cjs/data/SceneDataTable.js +15 -1
- package/dist/cjs/data/SceneDataTable.js.map +1 -1
- package/dist/cjs/data/SceneObservableDataTable.js +1 -1
- package/dist/cjs/data/SceneObservableDataTable.js.map +1 -1
- package/dist/cjs/data/SceneSingleResult.js +41 -0
- package/dist/cjs/data/SceneSingleResult.js.map +1 -0
- package/dist/cjs/data/SingleResultRuntime.js +122 -0
- package/dist/cjs/data/SingleResultRuntime.js.map +1 -0
- package/dist/cjs/data/SingleResultStatus.js +23 -0
- package/dist/cjs/data/SingleResultStatus.js.map +1 -0
- package/dist/cjs/dialogs/SceneCommandDialog.js +1 -1
- package/dist/cjs/dialogs/SceneCommandDialog.js.map +1 -1
- package/dist/cjs/dialogs/SceneStepperCommandDialog.js +1 -1
- package/dist/cjs/dialogs/SceneStepperCommandDialog.js.map +1 -1
- package/dist/cjs/forms/fields/SceneRadioButtonField.js +4 -1
- package/dist/cjs/forms/fields/SceneRadioButtonField.js.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/pages/SceneDataPage.js +1 -1
- package/dist/cjs/pages/SceneDataPage.js.map +1 -1
- package/dist/esm/bindings/ArcBoundComponents.d.ts +18 -0
- package/dist/esm/bindings/ArcBoundComponents.d.ts.map +1 -0
- package/dist/esm/bindings/ArcBoundComponents.js +49 -0
- package/dist/esm/bindings/ArcBoundComponents.js.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/NativeProxies.d.ts +25 -0
- package/dist/esm/bindings/for_requireArcProxy/NativeProxies.d.ts.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/NativeProxies.js +23 -0
- package/dist/esm/bindings/for_requireArcProxy/NativeProxies.js.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/when_mounting_bound_adapters.d.ts +2 -0
- package/dist/esm/bindings/for_requireArcProxy/when_mounting_bound_adapters.d.ts.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/when_mounting_bound_adapters.js +56 -0
- package/dist/esm/bindings/for_requireArcProxy/when_mounting_bound_adapters.js.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/when_previewing_without_arc.d.ts +2 -0
- package/dist/esm/bindings/for_requireArcProxy/when_previewing_without_arc.d.ts.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/when_previewing_without_arc.js +32 -0
- package/dist/esm/bindings/for_requireArcProxy/when_previewing_without_arc.js.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/when_validating_a_proxy.d.ts +2 -0
- package/dist/esm/bindings/for_requireArcProxy/when_validating_a_proxy.d.ts.map +1 -0
- package/dist/esm/bindings/for_requireArcProxy/when_validating_a_proxy.js +45 -0
- package/dist/esm/bindings/for_requireArcProxy/when_validating_a_proxy.js.map +1 -0
- package/dist/esm/bindings/requireArcProxy.d.ts +5 -0
- package/dist/esm/bindings/requireArcProxy.d.ts.map +1 -0
- package/dist/esm/bindings/requireArcProxy.js +23 -0
- package/dist/esm/bindings/requireArcProxy.js.map +1 -0
- package/dist/esm/cratisComponents.d.ts.map +1 -1
- package/dist/esm/cratisComponents.js +2 -0
- package/dist/esm/cratisComponents.js.map +1 -1
- package/dist/esm/cratisComponentsPackage.d.ts.map +1 -1
- package/dist/esm/cratisComponentsPackage.js +2 -1
- package/dist/esm/cratisComponentsPackage.js.map +1 -1
- package/dist/esm/data/SceneDataTable.d.ts.map +1 -1
- package/dist/esm/data/SceneDataTable.js +16 -2
- package/dist/esm/data/SceneDataTable.js.map +1 -1
- package/dist/esm/data/SceneObservableDataTable.js +1 -1
- package/dist/esm/data/SceneObservableDataTable.js.map +1 -1
- package/dist/esm/data/SceneSingleResult.d.ts +3 -0
- package/dist/esm/data/SceneSingleResult.d.ts.map +1 -0
- package/dist/esm/data/SceneSingleResult.js +39 -0
- package/dist/esm/data/SceneSingleResult.js.map +1 -0
- package/dist/esm/data/SingleResultRuntime.d.ts +9 -0
- package/dist/esm/data/SingleResultRuntime.d.ts.map +1 -0
- package/dist/esm/data/SingleResultRuntime.js +118 -0
- package/dist/esm/data/SingleResultRuntime.js.map +1 -0
- package/dist/esm/data/SingleResultStatus.d.ts +7 -0
- package/dist/esm/data/SingleResultStatus.d.ts.map +1 -0
- package/dist/esm/data/SingleResultStatus.js +21 -0
- package/dist/esm/data/SingleResultStatus.js.map +1 -0
- package/dist/esm/data/for_SceneDataTable/when_forwarding_query_arguments.d.ts +2 -0
- package/dist/esm/data/for_SceneDataTable/when_forwarding_query_arguments.d.ts.map +1 -0
- package/dist/esm/data/for_SceneDataTable/when_forwarding_query_arguments.js +90 -0
- package/dist/esm/data/for_SceneDataTable/when_forwarding_query_arguments.js.map +1 -0
- package/dist/esm/data/for_SceneSingleResult/when_executing_a_native_query.d.ts +2 -0
- package/dist/esm/data/for_SceneSingleResult/when_executing_a_native_query.d.ts.map +1 -0
- package/dist/esm/data/for_SceneSingleResult/when_executing_a_native_query.js +255 -0
- package/dist/esm/data/for_SceneSingleResult/when_executing_a_native_query.js.map +1 -0
- package/dist/esm/data/for_SceneSingleResult/when_gating_the_lazy_runtime.d.ts +2 -0
- package/dist/esm/data/for_SceneSingleResult/when_gating_the_lazy_runtime.d.ts.map +1 -0
- package/dist/esm/data/for_SceneSingleResult/when_gating_the_lazy_runtime.js +54 -0
- package/dist/esm/data/for_SceneSingleResult/when_gating_the_lazy_runtime.js.map +1 -0
- package/dist/esm/data/index.d.ts +1 -0
- package/dist/esm/data/index.d.ts.map +1 -1
- package/dist/esm/data/index.js +1 -0
- package/dist/esm/data/index.js.map +1 -1
- package/dist/esm/dialogs/SceneCommandDialog.js +1 -1
- package/dist/esm/dialogs/SceneCommandDialog.js.map +1 -1
- package/dist/esm/dialogs/SceneStepperCommandDialog.js +1 -1
- package/dist/esm/dialogs/SceneStepperCommandDialog.js.map +1 -1
- package/dist/esm/for_cratisComponentsPackage/when_describing_query_arguments.d.ts +2 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_query_arguments.d.ts.map +1 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_query_arguments.js +11 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_query_arguments.js.map +1 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_single_result.d.ts +2 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_single_result.d.ts.map +1 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_single_result.js +16 -0
- package/dist/esm/for_cratisComponentsPackage/when_describing_single_result.js.map +1 -0
- package/dist/esm/forms/fields/SceneRadioButtonField.d.ts.map +1 -1
- package/dist/esm/forms/fields/SceneRadioButtonField.js +4 -1
- package/dist/esm/forms/fields/SceneRadioButtonField.js.map +1 -1
- package/dist/esm/forms/fields/for_SceneRadioButtonField/when_mapping_the_group_name.d.ts +2 -0
- package/dist/esm/forms/fields/for_SceneRadioButtonField/when_mapping_the_group_name.d.ts.map +1 -0
- package/dist/esm/forms/fields/for_SceneRadioButtonField/when_mapping_the_group_name.js +38 -0
- package/dist/esm/forms/fields/for_SceneRadioButtonField/when_mapping_the_group_name.js.map +1 -0
- package/dist/esm/forms/fields/for_resolveFieldBinding/when_resolving_a_field_binding.js +4 -0
- package/dist/esm/forms/fields/for_resolveFieldBinding/when_resolving_a_field_binding.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/pages/SceneDataPage.js +1 -1
- package/dist/esm/pages/SceneDataPage.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/for_cratisComponentsPackage/when_describing_query_arguments.ts +15 -0
- package/for_cratisComponentsPackage/when_describing_single_result.ts +20 -0
- package/forms/fields/SceneRadioButtonField.tsx +4 -1
- package/forms/fields/for_SceneRadioButtonField/when_mapping_the_group_name.tsx +45 -0
- package/forms/fields/for_resolveFieldBinding/when_resolving_a_field_binding.ts +6 -0
- package/package.json +20 -8
- package/pages/SceneDataPage.tsx +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,8 @@ unbound table costs one dashed box, not the whole screen.
|
|
|
72
72
|
### Arc is loaded lazily
|
|
73
73
|
|
|
74
74
|
`@cratis/arc`, `@cratis/arc.react` and `@cratis/fundamentals` are peer dependencies of `@cratis/components`
|
|
75
|
-
that the *host* supplies.
|
|
75
|
+
that the *host* supplies. This package also declares Arc and Arc React as optional peers and pins
|
|
76
|
+
22.16.1 as development dependencies for native integration tests. A design surface is not a host.
|
|
76
77
|
|
|
77
78
|
Every adapter that reaches them does so through a dynamic `import()` inside `React.lazy`, wrapped in
|
|
78
79
|
`ArcRuntimeBoundary` (`Suspense` + the library's own `ErrorBoundary`). A screen built only from the library's
|
|
@@ -87,6 +88,54 @@ This splits the library cleanly, and the split is the library's own:
|
|
|
87
88
|
The Storybook build externalizes the Arc packages (`.storybook/main.ts`) so the lazy chunk can be emitted
|
|
88
89
|
with its imports left bare. No story loads that chunk, because no story registers a binding.
|
|
89
90
|
|
|
91
|
+
## Optional single-result view (read-only)
|
|
92
|
+
|
|
93
|
+
`Cratis.Components:singleResult` is a narrow adapter for an optional **single model**, not a collection.
|
|
94
|
+
For example, a host registers its generated `ProjectName` proxy and supplies these ExternalComponent
|
|
95
|
+
properties (the identifier comes from the host's committed input, never a guessed project):
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
registerQuery('ProjectName', ProjectName);
|
|
99
|
+
const properties = {
|
|
100
|
+
query: 'ProjectName',
|
|
101
|
+
queryArguments: Object.freeze({ projectId: committedProjectId }),
|
|
102
|
+
enabled: true,
|
|
103
|
+
resultField: 'name',
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use the actual own-field name of the generated model. `resultField` selects exactly one own data field:
|
|
108
|
+
no dotted-path traversal, getters, inherited members, expressions, JSON display or result-binding DSL.
|
|
109
|
+
Strings, finite numbers and booleans render as escaped text, including `0`, `false` and empty strings.
|
|
110
|
+
An absent/non-scalar field is a visible failure, not a missing model. A successful null/undefined model
|
|
111
|
+
is `Not found`; unsuccessful, unauthorized, invalid and exception envelopes are `Unable to load result`.
|
|
112
|
+
Idle, loading and failure states never display server exception messages or stacks.
|
|
113
|
+
|
|
114
|
+
`enabled` defaults to false and must be exactly true; the element must also be enabled. The host owns
|
|
115
|
+
input controls, validation feedback and committing/replacing immutable `queryArguments` objects. Even
|
|
116
|
+
parameterless queries require an explicit `{}`. Missing bindings take precedence over all other checks
|
|
117
|
+
and never import Arc. Disabled or malformed adapter properties do not import the lazy runtime either.
|
|
118
|
+
Native Arc `perform` owns required-parameter and generated-validator checks before HTTP; Scene adds no
|
|
119
|
+
business validation and forwards the argument object by identity.
|
|
120
|
+
|
|
121
|
+
The private lazy runtime creates a fresh native proxy per request generation, configures all four Arc
|
|
122
|
+
context connection/header values, and aborts on replacement or unmount. It also ignores late completion
|
|
123
|
+
when cancellation is ignored, masks previous results on the input-changing render, and responds to
|
|
124
|
+
header-callback identity and `queryVersion` changes. Hosts must replace the callback or bump the version
|
|
125
|
+
when credentials change invisibly inside a stable callback. There is no shared cache, command-triggered
|
|
126
|
+
refresh, polling, retry button, or observable-query subscription.
|
|
127
|
+
|
|
128
|
+
Published Arc 22.16.1 itself normalizes top-level falsy primitive payloads to null in `QueryResult`.
|
|
129
|
+
This adapter therefore promises scalar **fields of models**, not top-level primitive queries; it does
|
|
130
|
+
not fork Arc deserialization to recover those payloads. `isReady: false` is a failure for this one-shot
|
|
131
|
+
view rather than an invented retry protocol. Enumerable proxies are rejected before execution.
|
|
132
|
+
|
|
133
|
+
Tests use a real `QueryFor` subclass with unmodified `perform`, native validation and only the HTTP
|
|
134
|
+
boundary replaced. Separate lazy-boundary doubles test Arc-free gating, not native client correctness.
|
|
135
|
+
They do not prove a generated server route, backend persistence or end-to-end editable input. Scene #39
|
|
136
|
+
must remain open: editable Scene parameter binding, input/commit semantics, cross-component binding and
|
|
137
|
+
a general result-binding model are still unimplemented. No default profile, generator or schema changed.
|
|
138
|
+
|
|
90
139
|
## Naming and shadowing
|
|
91
140
|
|
|
92
141
|
Bare names are `lowerCamelCase` and abstract — `dataTable`, not `DataTableForQuery` — registered under
|
|
@@ -149,7 +198,7 @@ both packages write themes in one language: `primary.color`, `primary.contrastCo
|
|
|
149
198
|
```
|
|
150
199
|
bindings/ the registry, BindingKind, BoundConstructor, MissingBinding, ArcRuntimeBoundary
|
|
151
200
|
pages/ page, dataPage, formElement
|
|
152
|
-
data/ dataTable, table, observableDataTable
|
|
201
|
+
data/ dataTable, table, observableDataTable, singleResult
|
|
153
202
|
forms/ commandForm; forms/fields/ the twelve field types
|
|
154
203
|
dialogs/ dialog, confirmationDialog, busyIndicatorDialog, commandDialog, stepperCommandDialog
|
|
155
204
|
common/ icon, tooltip, dropdown, errorBoundary
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
// Runtime-only module: import dynamically beneath ArcRuntimeBoundary, never through the barrel.
|
|
5
|
+
import type { ComponentProps } from 'react';
|
|
6
|
+
import { QueryFor, ObservableQueryFor } from '@cratis/arc/queries';
|
|
7
|
+
import { Command } from '@cratis/arc/commands';
|
|
8
|
+
import { DataTableForQuery, DataTableForObservableQuery } from '@cratis/components/DataTables';
|
|
9
|
+
import { DataPage } from '@cratis/components/DataPage';
|
|
10
|
+
import { CommandDialog, StepperCommandDialog } from '@cratis/components/CommandDialog';
|
|
11
|
+
import type { BoundConstructor } from './BoundConstructor';
|
|
12
|
+
import { requireArcProxy } from './requireArcProxy';
|
|
13
|
+
|
|
14
|
+
type QueryProps<T> = Omit<T, 'query'> & { query: BoundConstructor };
|
|
15
|
+
type CommandProps<T> = Omit<T, 'command'> & { command: BoundConstructor };
|
|
16
|
+
|
|
17
|
+
// Scene carries property names and argument objects, not compile-time row schemas. These adapters
|
|
18
|
+
// support generated object/collection queries, as required by the Components public prop contracts.
|
|
19
|
+
export function BoundDataTable({ query, ...props }: QueryProps<ComponentProps<typeof DataTableForQuery>>) {
|
|
20
|
+
return <DataTableForQuery {...props} query={requireArcProxy(query, QueryFor<object, object>, 'query')} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function BoundObservableDataTable({ query, ...props }: QueryProps<ComponentProps<typeof DataTableForObservableQuery>>) {
|
|
24
|
+
return <DataTableForObservableQuery {...props} query={requireArcProxy(query, ObservableQueryFor<object, object>, 'observable query')} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function BoundDataPage({ query, ...props }: QueryProps<ComponentProps<typeof DataPage>>) {
|
|
28
|
+
const base = query.prototype instanceof ObservableQueryFor ? ObservableQueryFor<object, object> : QueryFor<object, object>;
|
|
29
|
+
// This chooses only the kind of the supplied constructor, never another registered binding.
|
|
30
|
+
const proxy = requireArcProxy<QueryFor<object, object> | ObservableQueryFor<object, object>>(query, base, 'query');
|
|
31
|
+
return <DataPage {...props} query={proxy} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function BoundCommandDialog({ command, ...props }: CommandProps<ComponentProps<typeof CommandDialog>>) {
|
|
35
|
+
return <CommandDialog {...props} command={requireArcProxy(command, Command<object, object>, 'command')} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function BoundStepperCommandDialog({ command, ...props }: CommandProps<ComponentProps<typeof StepperCommandDialog>>) {
|
|
39
|
+
return <StepperCommandDialog {...props} command={requireArcProxy(command, Command<object, object>, 'command')} />;
|
|
40
|
+
}
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* `[ReadModel]` and `[Command]` types.
|
|
7
7
|
*
|
|
8
8
|
* `@cratis/components` types these as `Constructor<T>` from `@cratis/fundamentals`, constrained to
|
|
9
|
-
* `IQueryFor<T>` / a command shape. This
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* registry is that Scene never needs to know what an Arc proxy is.
|
|
9
|
+
* `IQueryFor<T>` / a command shape. This registry intentionally does not use those types: optional
|
|
10
|
+
* Arc peers must not make an Arc-free preview load Arc. The lazy Components bridge validates native
|
|
11
|
+
* proxy inheritance and the zero-argument constructor convention before passing a binding on.
|
|
13
12
|
*
|
|
14
13
|
* So the registry stores the widest honest shape: something that can be constructed. Whether a
|
|
15
14
|
* registered class really is a query or a command is the host's responsibility, and it is checkable
|
|
16
|
-
* where the host registers it, with the real Arc types in scope
|
|
15
|
+
* where the host registers it, with the real Arc types in scope. Arbitrary registrations remain usable
|
|
16
|
+
* by other consumers; the lazy Arc bridge rejects unsupported constructors visibly.
|
|
17
17
|
*
|
|
18
18
|
* `never[]` rather than `unknown[]` for the parameters so that any constructor is assignable, regardless
|
|
19
19
|
* of what it takes; `unknown[]` would only accept constructors whose parameters accept anything.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { QueryFor, ObservableQueryFor } from '@cratis/arc/queries';
|
|
5
|
+
import { Command } from '@cratis/arc/commands';
|
|
6
|
+
|
|
7
|
+
export class Snapshot extends QueryFor<object[], { projectId: string }> {
|
|
8
|
+
readonly route = '/projects';
|
|
9
|
+
readonly parameterDescriptors = [];
|
|
10
|
+
readonly requiredRequestParameters = ['projectId'];
|
|
11
|
+
defaultValue = [];
|
|
12
|
+
constructor() { super(Object, true); throw new Error('Must not construct during binding'); }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Live extends ObservableQueryFor<object[], object> {
|
|
16
|
+
readonly route = '/projects';
|
|
17
|
+
readonly parameterDescriptors = [];
|
|
18
|
+
readonly requiredRequestParameters = [];
|
|
19
|
+
readonly defaultValue = [];
|
|
20
|
+
constructor() { super(Object, true); throw new Error('Must not construct during binding'); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Save extends Command {
|
|
24
|
+
readonly route = '/save';
|
|
25
|
+
readonly propertyDescriptors = [];
|
|
26
|
+
readonly requestParameters = [];
|
|
27
|
+
constructor() { super(Object, false); throw new Error('Must not construct during binding'); }
|
|
28
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
5
|
+
import { expect, vi } from 'vitest';
|
|
6
|
+
import { externalComponent } from '../../given';
|
|
7
|
+
|
|
8
|
+
// Keep the real Arc base classes; replace only the final Components renderers.
|
|
9
|
+
describe('when mounting bound adapters', () => {
|
|
10
|
+
const received = vi.fn<(props: Record<string, unknown>) => void>();
|
|
11
|
+
let bindings: typeof import('../bindingRegistry');
|
|
12
|
+
let proxies: typeof import('./NativeProxies');
|
|
13
|
+
let adapters: typeof import('../ArcBoundComponents');
|
|
14
|
+
let Boundary: typeof import('../ArcRuntimeBoundary').ArcRuntimeBoundary;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
received.mockClear();
|
|
19
|
+
const component = (props: Record<string, unknown>) => { received(props); return <span>Mounted native adapter</span>; };
|
|
20
|
+
vi.doMock('@cratis/components/DataTables', () => ({ DataTableForQuery: component, DataTableForObservableQuery: component }));
|
|
21
|
+
vi.doMock('@cratis/components/DataPage', () => ({ DataPage: component }));
|
|
22
|
+
vi.doMock('@cratis/components/CommandDialog', () => ({ CommandDialog: component, StepperCommandDialog: component }));
|
|
23
|
+
bindings = await import('../bindingRegistry');
|
|
24
|
+
bindings.clearBindings();
|
|
25
|
+
proxies = await import('./NativeProxies');
|
|
26
|
+
adapters = await import('../ArcBoundComponents');
|
|
27
|
+
({ ArcRuntimeBoundary: Boundary } = await import('../ArcRuntimeBoundary'));
|
|
28
|
+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
cleanup();
|
|
33
|
+
bindings.clearBindings();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
vi.doUnmock('@cratis/components/DataTables');
|
|
36
|
+
vi.doUnmock('@cratis/components/DataPage');
|
|
37
|
+
vi.doUnmock('@cratis/components/CommandDialog');
|
|
38
|
+
vi.resetModules();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should pass both supported page kinds and both command dialog constructors unchanged', () => {
|
|
42
|
+
const { BoundDataPage, BoundObservableDataTable, BoundCommandDialog, BoundStepperCommandDialog } = adapters;
|
|
43
|
+
render(<><BoundDataPage query={proxies.Snapshot} title='' emptyMessage='' children={undefined} />
|
|
44
|
+
<BoundDataPage query={proxies.Live} title='' emptyMessage='' children={undefined} />
|
|
45
|
+
<BoundObservableDataTable query={proxies.Live} emptyMessage='' />
|
|
46
|
+
<BoundCommandDialog command={proxies.Save} title='' visible />
|
|
47
|
+
<BoundStepperCommandDialog command={proxies.Save} title='' visible /></>);
|
|
48
|
+
expect(received.mock.calls.map(([props]) => props.query ?? props.command)).toEqual([proxies.Snapshot, proxies.Live, proxies.Live, proxies.Save, proxies.Save]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should visibly reject a wrong-kind registered query without selecting another query', async () => {
|
|
52
|
+
bindings.registerQuery('wrong', proxies.Save);
|
|
53
|
+
bindings.registerQuery('valid', proxies.Snapshot);
|
|
54
|
+
const { SceneDataTable } = await import('../../data/SceneDataTable');
|
|
55
|
+
await act(async () => {
|
|
56
|
+
render(<SceneDataTable element={externalComponent('Cratis.Components:dataTable', { query: 'wrong' })} slots={{}} />);
|
|
57
|
+
});
|
|
58
|
+
expect(screen.getByText("Invalid query binding 'Save': expected a zero-argument Arc query proxy")).toBeTruthy();
|
|
59
|
+
expect(received).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should visibly reject snapshot bindings in observable tables and query bindings in dialogs', () => {
|
|
63
|
+
const { BoundObservableDataTable, BoundCommandDialog, BoundStepperCommandDialog, BoundDataPage } = adapters;
|
|
64
|
+
render(<><Boundary><BoundObservableDataTable query={proxies.Snapshot} emptyMessage='' /></Boundary>
|
|
65
|
+
<Boundary><BoundCommandDialog command={proxies.Snapshot} title='' visible /></Boundary>
|
|
66
|
+
<Boundary><BoundStepperCommandDialog command={proxies.Live} title='' visible /></Boundary>
|
|
67
|
+
<Boundary><BoundDataPage query={proxies.Save} title='' emptyMessage='' children={undefined} /></Boundary></>);
|
|
68
|
+
expect(screen.getAllByText('Error')).toHaveLength(4);
|
|
69
|
+
expect(received).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
5
|
+
import { expect, vi } from 'vitest';
|
|
6
|
+
import { externalComponent } from '../../given';
|
|
7
|
+
|
|
8
|
+
describe('when previewing bound adapters without Arc', () => {
|
|
9
|
+
const loaded = vi.fn();
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
vi.doUnmock('../ArcBoundComponents');
|
|
14
|
+
vi.resetModules();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should show missing bindings without importing the Arc runtime module', async () => {
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
vi.doMock('../ArcBoundComponents', () => {
|
|
20
|
+
loaded();
|
|
21
|
+
throw new Error('Arc is unavailable in preview');
|
|
22
|
+
});
|
|
23
|
+
const { clearBindings } = await import('../bindingRegistry');
|
|
24
|
+
clearBindings();
|
|
25
|
+
const { SceneDataTable } = await import('../../data/SceneDataTable');
|
|
26
|
+
const { SceneObservableDataTable } = await import('../../data/SceneObservableDataTable');
|
|
27
|
+
const { SceneDataPage } = await import('../../pages/SceneDataPage');
|
|
28
|
+
const { SceneCommandDialog } = await import('../../dialogs/SceneCommandDialog');
|
|
29
|
+
const { SceneStepperCommandDialog } = await import('../../dialogs/SceneStepperCommandDialog');
|
|
30
|
+
await act(async () => {
|
|
31
|
+
render(<>{[SceneDataTable, SceneObservableDataTable, SceneDataPage, SceneCommandDialog, SceneStepperCommandDialog].map((Adapter, index) =>
|
|
32
|
+
<Adapter key={index} element={externalComponent('Cratis.Components:preview', { query: 'missing', command: 'missing' })} slots={{}} />)}</>);
|
|
33
|
+
});
|
|
34
|
+
expect(screen.getAllByText(/Unresolved .* binding 'missing'/)).toHaveLength(5);
|
|
35
|
+
expect(loaded).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { expect } from 'vitest';
|
|
5
|
+
import { QueryFor, ObservableQueryFor } from '@cratis/arc/queries';
|
|
6
|
+
import { Command } from '@cratis/arc/commands';
|
|
7
|
+
import type { ComponentProps } from 'react';
|
|
8
|
+
import type { DataTableForQuery, DataTableForObservableQuery } from '@cratis/components/DataTables';
|
|
9
|
+
import type { CommandDialog } from '@cratis/components/CommandDialog';
|
|
10
|
+
import { requireArcProxy } from '../requireArcProxy';
|
|
11
|
+
import { registerQuery, resolveQuery, clearBindings } from '../bindingRegistry';
|
|
12
|
+
import { Snapshot, Live, Save } from './NativeProxies';
|
|
13
|
+
|
|
14
|
+
describe('when validating a proxy at the lazy Arc boundary', () => {
|
|
15
|
+
afterEach(clearBindings);
|
|
16
|
+
|
|
17
|
+
it('should preserve native constructors without constructing them and satisfy public component props', () => {
|
|
18
|
+
const query: ComponentProps<typeof DataTableForQuery>['query'] = requireArcProxy(Snapshot, QueryFor<object, object>, 'query');
|
|
19
|
+
const observable: ComponentProps<typeof DataTableForObservableQuery>['query'] = requireArcProxy(Live, ObservableQueryFor<object, object>, 'observable query');
|
|
20
|
+
const command: ComponentProps<typeof CommandDialog>['command'] = requireArcProxy(Save, Command<object, object>, 'command');
|
|
21
|
+
expect(query).toBe(Snapshot);
|
|
22
|
+
expect(observable).toBe(Live);
|
|
23
|
+
expect(command).toBe(Save);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should reject wrong kinds rather than perform an observable query as a snapshot', () => {
|
|
27
|
+
expect(() => requireArcProxy(Live, QueryFor<object, object>, 'query')).toThrow('Invalid query binding');
|
|
28
|
+
expect(() => requireArcProxy(Snapshot, ObservableQueryFor<object, object>, 'observable query')).toThrow('Invalid observable query binding');
|
|
29
|
+
expect(() => requireArcProxy(Snapshot, Command<object, object>, 'command')).toThrow('Invalid command binding');
|
|
30
|
+
expect(() => requireArcProxy(Save, QueryFor<object, object>, 'query')).toThrow('Invalid query binding');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should preserve arbitrary constructor registration but reject it at an Arc adapter', () => {
|
|
34
|
+
class Arbitrary { constructor(readonly input: string) {} }
|
|
35
|
+
registerQuery('arbitrary', Arbitrary);
|
|
36
|
+
expect(resolveQuery('arbitrary')).toBe(Arbitrary);
|
|
37
|
+
expect(() => requireArcProxy(Arbitrary, QueryFor<object, object>, 'query')).toThrow('Invalid query binding');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject a native subclass requiring constructor arguments without invoking it', () => {
|
|
41
|
+
class NeedsArguments extends Snapshot { constructor(readonly input: string) { super(); } }
|
|
42
|
+
expect(() => requireArcProxy(NeedsArguments, QueryFor<object, object>, 'query')).toThrow('zero-argument Arc');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { BoundConstructor } from './BoundConstructor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bridge for the generated, zero-argument Arc proxies consumed by Components. The registry remains
|
|
8
|
+
* open to arbitrary constructors; only mounting an Arc adapter requires this narrower contract.
|
|
9
|
+
* Check inheritance without constructing a proxy (constructors can have side effects).
|
|
10
|
+
*
|
|
11
|
+
* This is not structural interface validation: the host still owns the generated subclass's data,
|
|
12
|
+
* argument and implementation correctness. Prototype identity deliberately rejects duck-typed proxies
|
|
13
|
+
* and proxies from a second Arc installation. The arity check enforces the generated convention,
|
|
14
|
+
* not general JavaScript constructor safety (defaults/rest parameters can hide requirements).
|
|
15
|
+
* The single assertion bridges the registry under that host-owned generated-proxy contract; it is
|
|
16
|
+
* not a proof of arbitrary subclass behavior or of erased row/argument types.
|
|
17
|
+
*/
|
|
18
|
+
export function requireArcProxy<T extends object>(target: BoundConstructor, base: { prototype: T }, kind: string): new () => T {
|
|
19
|
+
const prototype: unknown = target.prototype;
|
|
20
|
+
if (target.length !== 0 || typeof prototype !== 'object' || prototype === null || !Object.prototype.isPrototypeOf.call(base.prototype, prototype)) {
|
|
21
|
+
throw new Error(`Invalid ${kind} binding '${target.name}': expected a zero-argument Arc ${kind} proxy`);
|
|
22
|
+
}
|
|
23
|
+
return target as new () => T;
|
|
24
|
+
}
|
package/cratisComponents.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { ComponentRegistry, componentRegistryKey } from '@cratis/scene.react';
|
|
5
5
|
import { SceneDataPage, SceneFormElement, ScenePage } from './pages';
|
|
6
|
-
import { SceneDataTable, SceneObservableDataTable } from './data';
|
|
6
|
+
import { SceneDataTable, SceneObservableDataTable, SceneSingleResult } from './data';
|
|
7
7
|
import {
|
|
8
8
|
SceneCalendarField,
|
|
9
9
|
SceneCheckboxField,
|
|
@@ -49,6 +49,7 @@ export const cratisComponents: ComponentRegistry = {
|
|
|
49
49
|
[componentRegistryKey(cratisComponentsPackageName, 'dataPage')]: SceneDataPage,
|
|
50
50
|
[componentRegistryKey(cratisComponentsPackageName, 'formElement')]: SceneFormElement,
|
|
51
51
|
|
|
52
|
+
[componentRegistryKey(cratisComponentsPackageName, 'singleResult')]: SceneSingleResult,
|
|
52
53
|
[componentRegistryKey(cratisComponentsPackageName, 'dataTable')]: SceneDataTable,
|
|
53
54
|
[componentRegistryKey(cratisComponentsPackageName, 'table')]: SceneDataTable,
|
|
54
55
|
[componentRegistryKey(cratisComponentsPackageName, 'observableDataTable')]: SceneObservableDataTable,
|
|
@@ -36,6 +36,7 @@ export const cratisComponentsPackageManifest: ScenePackage = {
|
|
|
36
36
|
'page',
|
|
37
37
|
'dataPage',
|
|
38
38
|
'formElement',
|
|
39
|
+
'singleResult',
|
|
39
40
|
'dataTable',
|
|
40
41
|
'table',
|
|
41
42
|
'observableDataTable',
|
|
@@ -88,7 +89,7 @@ export const cratisComponentsPackageManifest: ScenePackage = {
|
|
|
88
89
|
themes: [],
|
|
89
90
|
|
|
90
91
|
displayName: 'Cratis Components',
|
|
91
|
-
description: "Cratis' Arc-bound data, form and dialog composites, built on PrimeReact and Tailwind.",
|
|
92
|
+
description: "Cratis' Arc-bound data, form and dialog composites, built on PrimeReact and Tailwind. dataTable and table accept optional object-valued queryArguments for collection queries. singleResult displays one read-only scalar own resultField of an optional model using an explicit query, object queryArguments and enabled: true.",
|
|
92
93
|
module: '@cratis/scene.components',
|
|
93
94
|
license: 'MIT',
|
|
94
95
|
licenseUrl: 'https://github.com/Cratis/Scene/blob/main/LICENSE',
|
package/data/SceneDataTable.tsx
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { lazy } from 'react';
|
|
5
5
|
import { RegisteredComponentProps } from '@cratis/scene.react';
|
|
6
6
|
import { ArcRuntimeBoundary, BindingKind, MissingBinding, resolveElementBinding } from '../bindings';
|
|
7
|
-
import { stringArrayProperty, stringProperty } from '../properties';
|
|
7
|
+
import { objectProperty, stringArrayProperty, stringProperty } from '../properties';
|
|
8
|
+
import { Placeholder } from '../bindings/Placeholder';
|
|
8
9
|
|
|
9
|
-
const DataTableForQuery = lazy(async () => ({ default: (await import('
|
|
10
|
+
const DataTableForQuery = lazy(async () => ({ default: (await import('../bindings/ArcBoundComponents')).BoundDataTable }));
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The `Cratis.Components:dataTable` component, also registered as `table` - `DataTableForQuery` from
|
|
@@ -19,6 +20,9 @@ const DataTableForQuery = lazy(async () => ({ default: (await import('@cratis/co
|
|
|
19
20
|
* back into it, where PrimeReact's `DataTable` is handed rows and knows nothing about where they came from.
|
|
20
21
|
*
|
|
21
22
|
* The `query` property names an Arc query proxy; the columns come from the `content` slot.
|
|
23
|
+
* Optional `queryArguments` is an explicit object forwarded unchanged to that proxy. Invalid values
|
|
24
|
+
* render a placeholder rather than accidentally executing a query without its arguments. This remains
|
|
25
|
+
* a collection table; forwarding arguments does not make optional single-result queries compatible.
|
|
22
26
|
*
|
|
23
27
|
* There is deliberately no `clientFiltering` property. `@cratis/components` 3.0.0 still accepts the prop
|
|
24
28
|
* so existing call sites compile, but it no longer does anything - filtering is always applied to the
|
|
@@ -30,10 +34,16 @@ export function SceneDataTable({ element, slots }: RegisteredComponentProps) {
|
|
|
30
34
|
const { name, target } = resolveElementBinding(element, BindingKind.Query);
|
|
31
35
|
if (!target) return <MissingBinding element={element} kind={BindingKind.Query} name={name} />;
|
|
32
36
|
|
|
37
|
+
const queryArguments = objectProperty(element.properties, 'queryArguments');
|
|
38
|
+
if (element.properties.queryArguments !== undefined && queryArguments === undefined) {
|
|
39
|
+
return <Placeholder element={element} problem='Invalid queryArguments: expected an object' />;
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
return (
|
|
34
43
|
<ArcRuntimeBoundary>
|
|
35
44
|
<DataTableForQuery
|
|
36
45
|
query={target}
|
|
46
|
+
{...(queryArguments === undefined ? {} : { queryArguments })}
|
|
37
47
|
emptyMessage={stringProperty(element.properties, 'emptyMessage') ?? ''}
|
|
38
48
|
dataKey={stringProperty(element.properties, 'dataKey')}
|
|
39
49
|
globalFilterFields={stringArrayProperty(element.properties, 'globalFilterFields')}
|
|
@@ -7,7 +7,7 @@ import { ArcRuntimeBoundary, BindingKind, MissingBinding, resolveElementBinding
|
|
|
7
7
|
import { stringArrayProperty, stringProperty } from '../properties';
|
|
8
8
|
|
|
9
9
|
const DataTableForObservableQuery = lazy(async () => ({
|
|
10
|
-
default: (await import('
|
|
10
|
+
default: (await import('../bindings/ArcBoundComponents')).BoundObservableDataTable,
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { lazy } from 'react';
|
|
5
|
+
import { RegisteredComponentProps } from '@cratis/scene.react';
|
|
6
|
+
import { ArcRuntimeBoundary, BindingKind, MissingBinding, resolveElementBinding } from '../bindings';
|
|
7
|
+
import { booleanProperty, objectProperty, stringProperty } from '../properties';
|
|
8
|
+
import { SingleResultStatus } from './SingleResultStatus';
|
|
9
|
+
|
|
10
|
+
const SingleResultRuntime = lazy(() => import('./SingleResultRuntime'));
|
|
11
|
+
|
|
12
|
+
/** Read-only scalar field of an optional single model; the host owns and commits the input. */
|
|
13
|
+
export function SceneSingleResult({ element }: RegisteredComponentProps) {
|
|
14
|
+
const { name, target } = resolveElementBinding(element, BindingKind.Query);
|
|
15
|
+
if (!target) return <MissingBinding element={element} kind={BindingKind.Query} name={name} />;
|
|
16
|
+
|
|
17
|
+
// Nothing, including loading Arc, happens until the host explicitly commits its input.
|
|
18
|
+
if (!element.isEnabled || booleanProperty(element.properties, 'enabled') !== true) {
|
|
19
|
+
return <SingleResultStatus state='idle' />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const queryArguments = objectProperty(element.properties, 'queryArguments');
|
|
23
|
+
const resultField = stringProperty(element.properties, 'resultField');
|
|
24
|
+
if (!queryArguments || !resultField) return <SingleResultStatus state='failure' />;
|
|
25
|
+
|
|
26
|
+
return <ArcRuntimeBoundary>
|
|
27
|
+
<SingleResultRuntime query={target} queryArguments={queryArguments} resultField={resultField} />
|
|
28
|
+
</ArcRuntimeBoundary>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { ArcContext } from '@cratis/arc.react';
|
|
6
|
+
import type { QueryFor } from '@cratis/arc/queries';
|
|
7
|
+
import type { BoundConstructor } from '../bindings/BoundConstructor';
|
|
8
|
+
import { SingleResultStatus } from './SingleResultStatus';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
query: BoundConstructor;
|
|
12
|
+
queryArguments: Record<string, unknown>;
|
|
13
|
+
resultField: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Outcome = { state: 'failure' | 'notFound' } | { state: 'success'; value: string };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Private execution owner. Arc owns validation, routing, HTTP and deserialization; this component
|
|
20
|
+
* owns only request lifetime and presentation. No shared cache can hand B the result belonging to A.
|
|
21
|
+
*/
|
|
22
|
+
export default function SingleResultRuntime({ query, queryArguments, resultField }: Props) {
|
|
23
|
+
const arc = useContext(ArcContext);
|
|
24
|
+
const { microservice, apiBasePath, origin, httpHeadersCallback, queryVersion } = arc;
|
|
25
|
+
// Identity is deliberate: hosts replace immutable committed arguments rather than mutating them.
|
|
26
|
+
const request = useMemo(() => ({ query, queryArguments, resultField, microservice, apiBasePath, origin, httpHeadersCallback, queryVersion }),
|
|
27
|
+
[query, queryArguments, resultField, microservice, apiBasePath, origin, httpHeadersCallback, queryVersion]);
|
|
28
|
+
const [settled, setSettled] = useState<{ request: typeof request; outcome: Outcome }>();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
// Each effect setup is a generation, including StrictMode's setup/cleanup/setup replay.
|
|
32
|
+
let current = true;
|
|
33
|
+
let instance: QueryFor<unknown, Record<string, unknown>> | undefined;
|
|
34
|
+
const publish = (outcome: Outcome) => {
|
|
35
|
+
if (current) setSettled({ request, outcome });
|
|
36
|
+
};
|
|
37
|
+
const execute = async () => {
|
|
38
|
+
try {
|
|
39
|
+
// The registry intentionally stores constructors without an eager Arc dependency.
|
|
40
|
+
instance = new request.query() as QueryFor<unknown, Record<string, unknown>>;
|
|
41
|
+
if (instance.enumerable !== false) {
|
|
42
|
+
publish({ state: 'failure' });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
instance.setMicroservice(request.microservice);
|
|
46
|
+
instance.setApiBasePath(request.apiBasePath ?? '');
|
|
47
|
+
instance.setOrigin(request.origin ?? '');
|
|
48
|
+
instance.setHttpHeadersCallback(request.httpHeadersCallback ?? (() => ({})));
|
|
49
|
+
// Native perform validates required arguments and proxy rules before HTTP. No copying,
|
|
50
|
+
// defaults, business validation, route construction or array wrapping happens here.
|
|
51
|
+
const result = await instance.perform(request.queryArguments);
|
|
52
|
+
if (!current) return;
|
|
53
|
+
if (result.isSuccess !== true || result.isAuthorized !== true || result.isValid !== true || result.hasExceptions !== false || result.isReady === false) {
|
|
54
|
+
publish({ state: 'failure' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (result.data === null || result.data === undefined) {
|
|
58
|
+
publish({ state: 'notFound' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof result.data !== 'object' || Array.isArray(result.data)) {
|
|
62
|
+
publish({ state: 'failure' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Only one own data field, never a path, inherited member, getter or expression.
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(result.data, request.resultField);
|
|
67
|
+
const value: unknown = descriptor && 'value' in descriptor ? descriptor.value : undefined;
|
|
68
|
+
if (typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) {
|
|
69
|
+
publish({ state: 'success', value: String(value) });
|
|
70
|
+
} else {
|
|
71
|
+
publish({ state: 'failure' });
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Includes native AbortError rejections. Disposed generations cannot publish or leak
|
|
75
|
+
// an unhandled promise, even when the HTTP boundary ignores cancellation.
|
|
76
|
+
publish({ state: 'failure' });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
void execute();
|
|
80
|
+
return () => {
|
|
81
|
+
current = false;
|
|
82
|
+
instance?.abortController?.abort();
|
|
83
|
+
};
|
|
84
|
+
}, [request]);
|
|
85
|
+
|
|
86
|
+
// Mask old data during the render that changes inputs, before effect cleanup/setup runs.
|
|
87
|
+
if (settled?.request !== request) return <SingleResultStatus state='loading' />;
|
|
88
|
+
return <SingleResultStatus {...settled.outcome} />;
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
type State = 'idle' | 'loading' | 'notFound' | 'failure' | 'success';
|
|
5
|
+
|
|
6
|
+
const messages = { idle: 'Idle', loading: 'Loading', notFound: 'Not found', failure: 'Unable to load result', success: '' };
|
|
7
|
+
|
|
8
|
+
/** Internal shared presentation; never exposes query exception messages or server stack traces. */
|
|
9
|
+
export function SingleResultStatus({ state, value }: { state: State; value?: string }) {
|
|
10
|
+
return <span role={state === 'failure' ? 'alert' : 'status'} data-scene-single-result={state}>
|
|
11
|
+
{state === 'success' ? value : messages[state]}
|
|
12
|
+
</span>;
|
|
13
|
+
}
|