@object-ui/react 17.5.0 → 17.7.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +4100 -0
  2. package/README.md +303 -46
  3. package/dist/LazyPluginLoader.d.ts +14 -7
  4. package/dist/LazyPluginLoader.js +14 -7
  5. package/dist/SchemaRenderer.d.ts +38 -7
  6. package/dist/SchemaRenderer.js +1603 -60
  7. package/dist/context/ActionContext.js +40 -1
  8. package/dist/context/DiscussionContext.d.ts +12 -0
  9. package/dist/context/DiscussionContext.js +2 -0
  10. package/dist/context/DrillNavigationContext.d.ts +13 -0
  11. package/dist/context/HostNavigationContext.d.ts +82 -0
  12. package/dist/context/HostNavigationContext.js +65 -0
  13. package/dist/context/InlineEditContext.d.ts +81 -1
  14. package/dist/context/InlineEditContext.js +8 -2
  15. package/dist/context/RecordContext.d.ts +48 -5
  16. package/dist/context/RecordContext.js +10 -1
  17. package/dist/context/RelatedRecordActionsContext.d.ts +20 -1
  18. package/dist/context/SchemaRendererContext.d.ts +54 -2
  19. package/dist/context/SchemaRendererContext.js +38 -5
  20. package/dist/context/ThemeContext.js +1 -1
  21. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  22. package/dist/context/actionRunnerTranslator.js +37 -0
  23. package/dist/context/index.d.ts +13 -12
  24. package/dist/context/index.js +13 -12
  25. package/dist/element-data-source/ElementDataSourceGate.d.ts +163 -3
  26. package/dist/element-data-source/ElementDataSourceGate.js +407 -15
  27. package/dist/hooks/index.d.ts +37 -35
  28. package/dist/hooks/index.js +44 -35
  29. package/dist/hooks/useActionEngine.js +47 -4
  30. package/dist/hooks/useActionRunner.js +4 -0
  31. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  32. package/dist/hooks/useActionTextLocalizer.js +33 -8
  33. package/dist/hooks/useCapabilityGate.js +6 -5
  34. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  35. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  36. package/dist/hooks/useCrudShortcuts.js +1 -1
  37. package/dist/hooks/useDatasetDimensionLabels.js +1 -1
  38. package/dist/hooks/useDensityMode.d.ts +6 -2
  39. package/dist/hooks/useDensityMode.js +6 -2
  40. package/dist/hooks/useDiscovery.d.ts +1 -10
  41. package/dist/hooks/useDiscovery.js +1 -1
  42. package/dist/hooks/useETagCache.js +25 -4
  43. package/dist/hooks/useElementDataSource.d.ts +15 -2
  44. package/dist/hooks/useElementDataSource.js +29 -9
  45. package/dist/hooks/useExpression.d.ts +52 -38
  46. package/dist/hooks/useExpression.js +62 -42
  47. package/dist/hooks/useFilterScope.d.ts +22 -2
  48. package/dist/hooks/useFilterScope.js +29 -4
  49. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  50. package/dist/hooks/useGlobalUndo.js +112 -46
  51. package/dist/hooks/useNavigationOverlay.d.ts +77 -12
  52. package/dist/hooks/useNavigationOverlay.js +119 -9
  53. package/dist/hooks/useOffline.js +65 -11
  54. package/dist/hooks/usePageAssignment.d.ts +8 -5
  55. package/dist/hooks/usePageAssignment.js +38 -17
  56. package/dist/hooks/usePageTransition.js +1 -1
  57. package/dist/hooks/usePageVariableActionBridge.js +2 -2
  58. package/dist/hooks/useRecordSearch.d.ts +34 -1
  59. package/dist/hooks/useRecordSearch.js +42 -7
  60. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  61. package/dist/hooks/useResolvedFilter.js +130 -0
  62. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  63. package/dist/hooks/useSchemaPersistence.js +144 -5
  64. package/dist/hooks/useSettledSchema.d.ts +123 -0
  65. package/dist/hooks/useSettledSchema.js +186 -0
  66. package/dist/hooks/useViewData.d.ts +7 -2
  67. package/dist/hooks/useViewData.js +1 -1
  68. package/dist/hooks/useViewTransition.js +1 -1
  69. package/dist/index.d.ts +18 -11
  70. package/dist/index.js +54 -10
  71. package/dist/schema-input.d.ts +55 -14
  72. package/dist/schema-input.js +55 -12
  73. package/dist/utils/configBag.d.ts +68 -0
  74. package/dist/utils/configBag.js +70 -0
  75. package/dist/utils/error-message.d.ts +59 -0
  76. package/dist/utils/error-message.js +116 -1
  77. package/dist/utils/inlineAria.d.ts +61 -0
  78. package/dist/utils/inlineAria.js +70 -0
  79. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  80. package/dist/utils/nonGridRowCeiling.js +78 -0
  81. package/dist/utils/paramsBag.d.ts +28 -0
  82. package/dist/utils/paramsBag.js +146 -0
  83. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  84. package/dist/utils/propsBagDiagnostic.js +421 -0
  85. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  86. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  87. package/dist/utils/unevaluatedExpression.d.ts +110 -0
  88. package/dist/utils/unevaluatedExpression.js +257 -0
  89. package/dist/utils/visibilityDiagnostic.d.ts +371 -0
  90. package/dist/utils/visibilityDiagnostic.js +551 -0
  91. package/package.json +10 -10
  92. package/dist/hooks/useClientNotifications.d.ts +0 -47
  93. package/dist/hooks/useClientNotifications.js +0 -162
  94. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  95. package/dist/spec-bridge/SpecBridge.js +0 -56
  96. package/dist/spec-bridge/bridges/form-view.d.ts +0 -82
  97. package/dist/spec-bridge/bridges/form-view.js +0 -126
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -241
  100. package/dist/spec-bridge/index.d.ts +0 -11
  101. package/dist/spec-bridge/index.js +0 -10
  102. package/dist/spec-bridge/types.d.ts +0 -55
  103. package/dist/spec-bridge/types.js +0 -8
@@ -5,46 +5,55 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- export * from './useExpression';
8
+ export * from './useExpression.js';
9
9
  // Session scope for filter placeholders ({current_user_id}/{current_org_id}).
10
- export * from './useFilterScope';
11
- export * from './useActionRunner';
12
- export * from './useNavigationOverlay';
13
- export * from './usePageVariables';
14
- export * from './usePageVariableActionBridge';
15
- export * from './useViewData';
10
+ export * from './useFilterScope.js';
11
+ // The ONE hold a data node resolves its own authored `filter` through
12
+ // (objectui#10666), fed by `useFilterScope()`.
13
+ export * from './useResolvedFilter.js';
14
+ export * from './useActionRunner.js';
15
+ export * from './useNavigationOverlay.js';
16
+ export * from './usePageVariables.js';
17
+ export * from './usePageVariableActionBridge.js';
18
+ export * from './useViewData.js';
16
19
  // PageComponentSchema.dataSource — the spec's per-element data binding.
17
- export * from './useElementDataSource';
18
- export * from './useDynamicApp';
19
- export * from './useDiscovery';
20
- export * from './useFocusTrap';
21
- export * from './useFocusManagement';
22
- export * from './useKeyboardShortcuts';
23
- export * from './useCrudShortcuts';
24
- export * from './useReducedMotion';
25
- export * from './useAnimation';
26
- export * from './useDensityMode';
27
- export * from './useViewSharing';
28
- export * from './useClientNotifications';
29
- export * from './useOffline';
30
- export * from './usePerformance';
31
- export * from './usePerformanceBudget';
32
- export * from './usePageTransition';
33
- export * from './useViewTransition';
34
- export * from './useETagCache';
35
- export * from './useSchemaPersistence';
36
- export * from './useGlobalUndo';
37
- export * from './useDebugMode';
38
- export * from './useActionEngine';
20
+ export * from './useElementDataSource.js';
21
+ export * from './useDynamicApp.js';
22
+ export * from './useDiscovery.js';
23
+ export * from './useFocusTrap.js';
24
+ export * from './useFocusManagement.js';
25
+ export * from './useKeyboardShortcuts.js';
26
+ export * from './useCrudShortcuts.js';
27
+ export * from './useReducedMotion.js';
28
+ export * from './useAnimation.js';
29
+ export * from './useDensityMode.js';
30
+ export * from './useViewSharing.js';
31
+ export * from './useOffline.js';
32
+ export * from './usePerformance.js';
33
+ export * from './usePerformanceBudget.js';
34
+ export * from './usePageTransition.js';
35
+ export * from './useViewTransition.js';
36
+ export * from './useETagCache.js';
37
+ export * from './useSchemaPersistence.js';
38
+ export * from './useGlobalUndo.js';
39
+ export * from './useDebugMode.js';
40
+ export * from './useActionEngine.js';
39
41
  // The ONE application of `useObjectLabel`'s action resolvers over the three
40
42
  // authored strings an action carries (label / confirmText / successMessage).
41
- export * from './useActionTextLocalizer';
42
- export * from './useCapabilityGate';
43
+ export * from './useActionTextLocalizer.js';
44
+ export * from './useCapabilityGate.js';
45
+ // The `SchemaRenderer` memo's `properties` evaluation, for an action container's
46
+ // members, which are drawn without `SchemaRenderer` (objectui#10290).
47
+ export * from './useConfigBagEvaluator.js';
43
48
  // The analytics label net's React glue, consumed by BOTH plugin-dashboard's
44
49
  // `DatasetWidget` and plugin-report's dataset block (objectui#4389). It lives
45
50
  // here rather than in `@object-ui/core` because it reads `SchemaRendererContext`
46
51
  // — see the file header for the measured dependency direction.
47
- export * from './useDatasetDimensionLabels';
48
- export * from './useDataRefresh';
49
- export * from './usePageAssignment';
50
- export * from './useRecordSearch';
52
+ export * from './useDatasetDimensionLabels.js';
53
+ export * from './useDataRefresh.js';
54
+ export * from './usePageAssignment.js';
55
+ export * from './useRecordSearch.js';
56
+ // The settled-schema RESOLUTION half shared across ObjectKanban / ObjectView /
57
+ // ObjectCalendar's hand copies (objectui#6482, maintainer ruling Option A).
58
+ // Gate PLACEMENT stays per-component — see the hook's own doc comment.
59
+ export * from './useSettledSchema.js';
@@ -22,8 +22,9 @@
22
22
  * ```
23
23
  */
24
24
  import { useCallback, useContext, useMemo } from 'react';
25
- import { ActionEngine, } from '@object-ui/core';
26
- import { ActionCtxReact } from '../context/ActionContext';
25
+ import { ActionEngine, subjectPermissionsOf, } from '@object-ui/core';
26
+ import { ActionCtxReact } from '../context/ActionContext.js';
27
+ import { usePredicateScope } from './useExpression.js';
27
28
  export function useActionEngine(options = {}) {
28
29
  const { actions = [], context = {} } = options;
29
30
  // When wrapped in an <ActionProvider>, reuse its ActionRunner so that:
@@ -39,7 +40,42 @@ export function useActionEngine(options = {}) {
39
40
  // fall back to a self-contained runner constructed from the `context` arg.
40
41
  const providerCtx = useContext(ActionCtxReact);
41
42
  const sharedRunner = providerCtx?.runner ?? null;
43
+ // The acting subject the host's predicate scope binds as `current_user`
44
+ // (objectui#11212): the ONE object `ExpressionProvider` publishes under
45
+ // `current_user` / `user` / `ctx.user` / `os.user`, carrying the caller's
46
+ // effective object permissions once they have loaded. The engine filters
47
+ // `visible` against the RUNNER's bag, which a host seeds with its own `user`
48
+ // and never with `current_user` — so `current_user.can(…)`, and any other
49
+ // `current_user.*` predicate, faulted on this path in every state and the
50
+ // action was hidden even for a grant holder, while the same predicate
51
+ // answered on every predicate-scope surface. Binding the scope's subject
52
+ // itself — the same object, not a copy, because the engine answers `can`
53
+ // only for a receiver IDENTICAL to the bound `current_user` — is what makes
54
+ // the two paths one bag (objectui#6493). With no host scope (`{}`) nothing
55
+ // is bound and the path behaves as it always did.
56
+ //
57
+ // Only `current_user` is added. The runner's `user` / `ctx.user` / `os.user`
58
+ // stay the host's own object: it carries what the runner itself reads (the
59
+ // `systemPermissions` capability set), which the scope's subject does not.
60
+ //
61
+ // Since objectui#11242 `<ActionProvider>` binds the same subject on its own
62
+ // runner, so under a provider mounted INSIDE the predicate scope this write
63
+ // re-binds the object that is already there. It stays for the two runners
64
+ // no provider binds: this hook's own standalone runner (no provider at
65
+ // all), and a shared runner whose provider sits ABOVE the scope — the
66
+ // console's global provider does (`GlobalActionRuntimeProvider` wraps the
67
+ // routes, and `ExpressionProvider` is mounted inside them) — where the
68
+ // provider has no scope to read.
69
+ const subject = usePredicateScope().current_user;
70
+ // Keyed on what the binding READS, never on the subject's identity (a
71
+ // memoised value upstream — AGENTS.md #10): its serialisable fields, and the
72
+ // permissions map it carries, which the upstream adapter caches per payload
73
+ // outside React. A discarded-and-recomputed subject with the same content
74
+ // keeps the engine; the permissions arriving (or leaving) rebuilds it.
75
+ const subjectKey = subject === undefined ? '' : JSON.stringify(subject);
76
+ const subjectPermissions = subjectPermissionsOf(subject);
42
77
  const engine = useMemo(() => {
78
+ const bound = subject !== undefined ? { current_user: subject } : {};
43
79
  // When standalone (no surrounding `<ActionProvider>`), normalize the
44
80
  // context so predicates can use both `record`/`user` and `ctx.*`.
45
81
  const normalizedStandalone = (context && Object.keys(context).length > 0)
@@ -48,8 +84,9 @@ export function useActionEngine(options = {}) {
48
84
  ctx: (context.ctx && typeof context.ctx === 'object')
49
85
  ? { ...context, ...context.ctx }
50
86
  : { ...context },
87
+ ...bound,
51
88
  }
52
- : context;
89
+ : { ...context, ...bound };
53
90
  const e = sharedRunner ? new ActionEngine(sharedRunner) : new ActionEngine(normalizedStandalone);
54
91
  // When sharing a provider runner, MERGE per-render flat keys into the
55
92
  // existing `ctx` instead of overwriting it. The provider seeds
@@ -67,13 +104,19 @@ export function useActionEngine(options = {}) {
67
104
  const merged = {
68
105
  ...context,
69
106
  ctx: { ...existingCtx, ...callerCtx },
107
+ ...bound,
70
108
  };
71
109
  runner.updateContext(merged);
72
110
  }
111
+ else if (sharedRunner && subject !== undefined) {
112
+ // A caller that passes no per-render keys: the subject is still bound,
113
+ // onto the same shared runner the location filter reads.
114
+ e.getRunner().updateContext(bound);
115
+ }
73
116
  e.registerActions(actions);
74
117
  return e;
75
118
  // eslint-disable-next-line react-hooks/exhaustive-deps
76
- }, [sharedRunner, JSON.stringify(actions), JSON.stringify(context)]);
119
+ }, [sharedRunner, JSON.stringify(actions), JSON.stringify(context), subjectKey, subjectPermissions]);
77
120
  const getActionsForLocation = useCallback((location) => engine.getActionsForLocation(location), [engine]);
78
121
  const getBulkActions = useCallback(() => engine.getBulkActions(), [engine]);
79
122
  const executeAction = useCallback((name, contextOverride) => engine.executeAction(name, contextOverride), [engine]);
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { useState, useCallback, useMemo } from 'react';
9
9
  import { ActionRunner, } from '@object-ui/core';
10
+ import { useActionRunnerTranslator } from '../context/actionRunnerTranslator.js';
10
11
  /**
11
12
  * Hook for executing actions with loading state, typed with ActionDef.
12
13
  *
@@ -35,8 +36,11 @@ export function useActionRunner(optionsOrContext = {}) {
35
36
  const [loading, setLoading] = useState(false);
36
37
  const [error, setError] = useState(null);
37
38
  const [result, setResult] = useState(null);
39
+ // The runner's own copy (the generic success toast) in the session's language.
40
+ const translate = useActionRunnerTranslator();
38
41
  const runner = useMemo(() => {
39
42
  const r = new ActionRunner(context);
43
+ r.setTranslator(translate);
40
44
  if (onConfirm)
41
45
  r.setConfirmHandler(onConfirm);
42
46
  if (onToast)
@@ -47,11 +47,23 @@
47
47
  * Translation localizes what exists; it does not add behaviour. This mirrors
48
48
  * what the already-correct call sites (`RecordDetailView`, `ObjectView`,
49
49
  * `DeclaredActionsBar`) each spelled out by hand.
50
- * - **`label` is reduced through `pickLocalized` first**, because since rc.6 an
51
- * authored `label` may be an `I18nLabel` map rather than a string. The map is
52
- * collapsed to the active language BEFORE it is offered to the bundle as the
53
- * fallback, so a per-locale literal and a bundle entry cannot disagree about
54
- * what "the authored label" is.
50
+ * - **`outcomeMessages` follows the same rule, entry by entry**
51
+ * (`@objectstack/spec` 17.6.0, objectui#11344): each outcome the action
52
+ * declares resolves `_actions.<name>.outcomeMessages.<outcome>`; a bundle
53
+ * entry for an outcome the action does not declare is never read, so a
54
+ * bundle cannot add copy for an answer the author never wrote one for.
55
+ * - **`label`, `successMessage` and each `outcomeMessages` entry are reduced
56
+ * through `pickLocalized` first**, because each is an `I18nLabel` — a string
57
+ * or an inline per-locale map. The map is collapsed to the active language
58
+ * BEFORE it is offered to the bundle as the fallback, so a per-locale
59
+ * literal and a bundle entry cannot disagree about what "the authored text"
60
+ * is, and the action runner, which has no language, only ever receives a
61
+ * string.
62
+ * - **The success copy stays a template.** A `${result.*}` token, in a bundle
63
+ * entry or in the authored text, passes through this hook untouched: the
64
+ * action has not run yet, so there is no result to read. The runner fills
65
+ * the token in after the action succeeds, through the same `${result.*}`
66
+ * scope `onSuccess.navigate` reads — one interpolation, in one place.
55
67
  */
56
68
  export interface ActionTextLocalizerOptions {
57
69
  /**
@@ -61,9 +73,10 @@ export interface ActionTextLocalizerOptions {
61
73
  fallbackLabel?: string;
62
74
  }
63
75
  /**
64
- * Returns a copy of `action` with `label` — plus `confirmText` and
65
- * `successMessage` when the action declares them — resolved from the active
66
- * locale's bundle, falling back to the authored literals.
76
+ * Returns a copy of `action` with `label` — plus `confirmText`,
77
+ * `successMessage` and `outcomeMessages` when the action declares them —
78
+ * resolved from the active locale's bundle, falling back to the authored
79
+ * literals.
67
80
  */
68
81
  export type ActionTextLocalizer = <T extends Record<string, any>>(objectName: string | undefined, action: T, options?: ActionTextLocalizerOptions) => T;
69
82
  export declare function useActionTextLocalizer(): ActionTextLocalizer;
@@ -47,16 +47,29 @@
47
47
  * Translation localizes what exists; it does not add behaviour. This mirrors
48
48
  * what the already-correct call sites (`RecordDetailView`, `ObjectView`,
49
49
  * `DeclaredActionsBar`) each spelled out by hand.
50
- * - **`label` is reduced through `pickLocalized` first**, because since rc.6 an
51
- * authored `label` may be an `I18nLabel` map rather than a string. The map is
52
- * collapsed to the active language BEFORE it is offered to the bundle as the
53
- * fallback, so a per-locale literal and a bundle entry cannot disagree about
54
- * what "the authored label" is.
50
+ * - **`outcomeMessages` follows the same rule, entry by entry**
51
+ * (`@objectstack/spec` 17.6.0, objectui#11344): each outcome the action
52
+ * declares resolves `_actions.<name>.outcomeMessages.<outcome>`; a bundle
53
+ * entry for an outcome the action does not declare is never read, so a
54
+ * bundle cannot add copy for an answer the author never wrote one for.
55
+ * - **`label`, `successMessage` and each `outcomeMessages` entry are reduced
56
+ * through `pickLocalized` first**, because each is an `I18nLabel` — a string
57
+ * or an inline per-locale map. The map is collapsed to the active language
58
+ * BEFORE it is offered to the bundle as the fallback, so a per-locale
59
+ * literal and a bundle entry cannot disagree about what "the authored text"
60
+ * is, and the action runner, which has no language, only ever receives a
61
+ * string.
62
+ * - **The success copy stays a template.** A `${result.*}` token, in a bundle
63
+ * entry or in the authored text, passes through this hook untouched: the
64
+ * action has not run yet, so there is no result to read. The runner fills
65
+ * the token in after the action succeeds, through the same `${result.*}`
66
+ * scope `onSuccess.navigate` reads — one interpolation, in one place.
55
67
  */
56
68
  import { useMemo } from 'react';
57
69
  import { useObjectLabel, useObjectTranslation, pickLocalized } from '@object-ui/i18n';
70
+ import { isConfigBag } from '../utils/configBag.js';
58
71
  export function useActionTextLocalizer() {
59
- const { actionLabel, actionConfirm, actionSuccess } = useObjectLabel();
72
+ const { actionLabel, actionConfirm, actionSuccess, actionOutcome } = useObjectLabel();
60
73
  const { language } = useObjectTranslation();
61
74
  return useMemo(() => {
62
75
  return ((objectName, action, options) => {
@@ -83,9 +96,21 @@ export function useActionTextLocalizer() {
83
96
  out.confirmText = actionConfirm(objectName, name, action.confirmText);
84
97
  }
85
98
  if (action.successMessage !== undefined) {
86
- out.successMessage = actionSuccess(objectName, name, action.successMessage);
99
+ out.successMessage = actionSuccess(objectName, name, pickLocalized(action.successMessage, language));
100
+ }
101
+ // The map is an authored bag (outcome → copy), so the one "is this a
102
+ // real bag?" predicate in this package answers whether to read it.
103
+ const authoredOutcomes = action.outcomeMessages;
104
+ if (isConfigBag(authoredOutcomes)) {
105
+ const outcomeMessages = {};
106
+ for (const [outcome, copy] of Object.entries(authoredOutcomes)) {
107
+ const text = actionOutcome(objectName, name, outcome, pickLocalized(copy, language));
108
+ if (text !== undefined)
109
+ outcomeMessages[outcome] = text;
110
+ }
111
+ out.outcomeMessages = outcomeMessages;
87
112
  }
88
113
  return out;
89
114
  });
90
- }, [actionLabel, actionConfirm, actionSuccess, language]);
115
+ }, [actionLabel, actionConfirm, actionSuccess, actionOutcome, language]);
91
116
  }
@@ -23,13 +23,14 @@
23
23
  *
24
24
  * **Fail-OPEN when unknown.** No runner, no user, no `systemPermissions` array:
25
25
  * the action shows. Unknown is not denied, the server is the authority
26
- * (ADR-0057 D10), and hiding a permitted user's button on missing client data
27
- * is the worse failure. An EMPTY array is not unknown — it means "holds
28
- * nothing" and gates normally.
26
+ * (the framework's ADR-0124 D1 — server enforces, client is courtesy), and
27
+ * hiding a permitted user's button on missing client data is the worse
28
+ * failure. An EMPTY array is not unknown — it means "holds nothing" and gates
29
+ * normally.
29
30
  */
30
31
  import { useCallback, useContext } from 'react';
31
- import { ActionCtxReact } from '../context/ActionContext';
32
- import { usePredicateScope } from './useExpression';
32
+ import { ActionCtxReact } from '../context/ActionContext.js';
33
+ import { usePredicateScope } from './useExpression.js';
33
34
  /**
34
35
  * The caller's system capabilities, or `undefined` when the host never supplied
35
36
  * them (→ callers must fail open).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The `properties` evaluation of the `SchemaRenderer` memo, for a node that is
10
+ * rendered WITHOUT `SchemaRenderer` (objectui#10290).
11
+ *
12
+ * The action containers (`action:bar`, `action:group`, `action:menu` in
13
+ * `@object-ui/components`) draw their member actions themselves, so a member's
14
+ * `properties` never passes through the memo. Its static execution values ride
15
+ * `properties.params` (objectui#10289), and those values are templates
16
+ * evaluated where `properties` are (objectui#7867). This hook is that
17
+ * evaluation: the memo's per-key rule, its `ExpressionEvaluator`, and its
18
+ * scope (the host's ambient roots, `current_user`, the page's bound row as
19
+ * `record`, and page variables as `page`), read from the same contexts the memo
20
+ * reads. It is not a second template engine. See `evaluateConfigBagInScope`.
21
+ *
22
+ * The returned function takes a config bag and returns a new, evaluated bag. A
23
+ * value that is not a config bag is returned unchanged. Call it once per
24
+ * authored bag: the result is already evaluated, and evaluating it again would
25
+ * interpolate text that came out of the row.
26
+ *
27
+ * A fresh function each render. Nothing may depend on its identity
28
+ * (AGENTS.md #10).
29
+ */
30
+ export declare function useConfigBagEvaluator(): (bag: unknown) => unknown;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useRecordContext } from '../context/RecordContext.js';
9
+ import { evaluateConfigBagInScope } from '../SchemaRenderer.js';
10
+ import { usePredicateScope } from './useExpression.js';
11
+ import { usePageVariables } from './usePageVariables.js';
12
+ /**
13
+ * The `properties` evaluation of the `SchemaRenderer` memo, for a node that is
14
+ * rendered WITHOUT `SchemaRenderer` (objectui#10290).
15
+ *
16
+ * The action containers (`action:bar`, `action:group`, `action:menu` in
17
+ * `@object-ui/components`) draw their member actions themselves, so a member's
18
+ * `properties` never passes through the memo. Its static execution values ride
19
+ * `properties.params` (objectui#10289), and those values are templates
20
+ * evaluated where `properties` are (objectui#7867). This hook is that
21
+ * evaluation: the memo's per-key rule, its `ExpressionEvaluator`, and its
22
+ * scope (the host's ambient roots, `current_user`, the page's bound row as
23
+ * `record`, and page variables as `page`), read from the same contexts the memo
24
+ * reads. It is not a second template engine. See `evaluateConfigBagInScope`.
25
+ *
26
+ * The returned function takes a config bag and returns a new, evaluated bag. A
27
+ * value that is not a config bag is returned unchanged. Call it once per
28
+ * authored bag: the result is already evaluated, and evaluating it again would
29
+ * interpolate text that came out of the row.
30
+ *
31
+ * A fresh function each render. Nothing may depend on its identity
32
+ * (AGENTS.md #10).
33
+ */
34
+ export function useConfigBagEvaluator() {
35
+ const predicateScope = usePredicateScope();
36
+ const boundRecord = useRecordContext()?.data;
37
+ const { variables: pageVariables } = usePageVariables();
38
+ return (bag) => evaluateConfigBagInScope(bag, predicateScope, boundRecord, pageVariables);
39
+ }
@@ -6,7 +6,7 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import { useMemo } from 'react';
9
- import { useKeyboardShortcuts, } from './useKeyboardShortcuts';
9
+ import { useKeyboardShortcuts, } from './useKeyboardShortcuts.js';
10
10
  /**
11
11
  * Registers standard CRUD keyboard shortcuts using the useKeyboardShortcuts hook.
12
12
  *
@@ -61,7 +61,7 @@
61
61
  import { useContext, useEffect, useMemo, useState } from 'react';
62
62
  import { deriveDimensionLabelMaps, loadDimensionFieldMeta, } from '@object-ui/core';
63
63
  import { useSafeFieldLabel } from '@object-ui/i18n';
64
- import { SchemaRendererContext } from '../context/SchemaRendererContext';
64
+ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
65
65
  /**
66
66
  * Resolve one dataset query's dimension field metadata — the LOCALE-FREE half.
67
67
  *
@@ -50,8 +50,12 @@ export interface DensityResult {
50
50
  *
51
51
  * @example
52
52
  * ```tsx
53
- * const density = useDensityMode(activeView?.densityMode ?? 'comfortable', {
54
- * onChange: (m) => dataSource.updateViewConfig(obj, vid, { densityMode: m }),
53
+ * import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
54
+ *
55
+ * // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
56
+ * // retired on `NamedListView` (objectui#7924), so persist the spec value.
57
+ * const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
58
+ * onChange: (m) => dataSource.updateViewConfig(obj, vid, { rowHeight: DENSITY_MODE_TO_ROW_HEIGHT[m] }),
55
59
  * });
56
60
  * ```
57
61
  */
@@ -35,8 +35,12 @@ const DENSITY_ORDER = ['compact', 'comfortable', 'spacious'];
35
35
  *
36
36
  * @example
37
37
  * ```tsx
38
- * const density = useDensityMode(activeView?.densityMode ?? 'comfortable', {
39
- * onChange: (m) => dataSource.updateViewConfig(obj, vid, { densityMode: m }),
38
+ * import { DENSITY_MODE_TO_ROW_HEIGHT, rowHeightToDensityMode } from '@object-ui/core';
39
+ *
40
+ * // A view stores the protocol's `rowHeight`; the legacy `densityMode` key is
41
+ * // retired on `NamedListView` (objectui#7924), so persist the spec value.
42
+ * const density = useDensityMode(rowHeightToDensityMode(activeView?.rowHeight) ?? 'comfortable', {
43
+ * onChange: (m) => dataSource.updateViewConfig(obj, vid, { rowHeight: DENSITY_MODE_TO_ROW_HEIGHT[m] }),
40
44
  * });
41
45
  * ```
42
46
  */
@@ -40,17 +40,8 @@ export interface DiscoveryInfo {
40
40
  /** Server name and version */
41
41
  name?: string;
42
42
  version?: string;
43
- /** Runtime mode (e.g., 'development', 'production', 'preview') */
43
+ /** Runtime mode the server reports (`RuntimeMode` in `@objectstack/spec`) */
44
44
  mode?: string;
45
- /** Preview mode configuration from the kernel (present when mode is 'preview') */
46
- previewMode?: {
47
- autoLogin?: boolean;
48
- simulatedRole?: 'admin' | 'user' | 'viewer';
49
- simulatedUserName?: string;
50
- readOnly?: boolean;
51
- expiresInSeconds?: number;
52
- bannerMessage?: string;
53
- };
54
45
  /** Service availability status */
55
46
  services?: {
56
47
  /** Authentication service status */
@@ -6,7 +6,7 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import { useState, useEffect, useContext } from 'react';
9
- import { SchemaRendererContext } from '../context/SchemaRendererContext';
9
+ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
10
10
  /**
11
11
  * The backward-compatible "can I actually use this service?" check
12
12
  * (ADR-0076 D12 console slice):
@@ -5,7 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
+ import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from 'react';
9
9
  // ---------------------------------------------------------------------------
10
10
  // Defaults
11
11
  // ---------------------------------------------------------------------------
@@ -141,9 +141,30 @@ export function useETagCache(userConfig = {}) {
141
141
  const swUrl = userConfig.serviceWorkerUrl ?? DEFAULTS.serviceWorkerUrl;
142
142
  const [cacheSize, setCacheSize] = useState(0);
143
143
  const [serviceWorkerActive, setServiceWorkerActive] = useState(false);
144
- // Keep config in a ref so callbacks always see latest values
145
- const configRef = useRef({ enabled, storage, storagePrefix, maxEntries, ttl });
146
- configRef.current = { enabled, storage, storagePrefix, maxEntries, ttl };
144
+ // Keep config in a ref so callbacks always see latest values. Every reader
145
+ // below is a `useCallback` with `[]` deps whose identity consumers may key
146
+ // effects on, so the config has to reach them without changing it — that is
147
+ // the ref's job. Only the WRITE moves: done in the render body it also ran
148
+ // for renders React discards or replays, publishing config from a tree that
149
+ // never committed. `useInsertionEffect` runs in the mutation phase, ahead of
150
+ // every layout effect, ref attachment and paint, so the sole window deferred
151
+ // is the render phase — where `fetchWithETag` / `clearCache` and the other
152
+ // side effects are not callable anyway.
153
+ //
154
+ // The object itself is built by `useMemo` rather than inline at the two
155
+ // places that consume it. `useRef({ ... })` evaluates its argument on EVERY
156
+ // render and keeps only the first result, so every later render allocated a
157
+ // five-key object that was thrown away (objectui#6817) — the same shape PR
158
+ // objectui#6796 repaired in `useSchemaPersistence`. Its identity is
159
+ // unobservable from outside the hook: the ref is private and every reader
160
+ // only reads fields off `.current`, so a memo React chooses to discard is
161
+ // harmless — it rebuilds an equal object, which is what every render used
162
+ // to do unconditionally.
163
+ const config = useMemo(() => ({ enabled, storage, storagePrefix, maxEntries, ttl }), [enabled, storage, storagePrefix, maxEntries, ttl]);
164
+ const configRef = useRef(config);
165
+ useInsertionEffect(() => {
166
+ configRef.current = config;
167
+ });
147
168
  // Hydrate memory cache from localStorage on mount
148
169
  useEffect(() => {
149
170
  if (storage === 'localStorage') {
@@ -19,7 +19,7 @@
19
19
  * still ignores `view`), and the remaining object-bound blocks will need the same
20
20
  * resolution. One resolver in the runtime layer, not one per plugin.
21
21
  */
22
- import { type ComposedElementDataSource, type ElementDataSourceConfig, type ElementSavedView } from '@object-ui/core';
22
+ import { FilterOperatorError, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementSavedView } from '@object-ui/core';
23
23
  /**
24
24
  * Where a resolution attempt stands. Distinguishing `loading` from `missing`
25
25
  * matters: a component that treated "not resolved yet" as "does not exist" would
@@ -34,7 +34,12 @@ export type ElementDataSourceStatus =
34
34
  | 'loading'
35
35
  /** The named `view` was found and composed in. */
36
36
  | 'resolved'
37
- /** The named `view` does not exist on the object (or could not be read). */
37
+ /**
38
+ * The binding cannot be applied: the named `view` does not exist on the
39
+ * object (or could not be read), or a filter it combines is REFUSED by the
40
+ * filter converter — then `filterRefusal` carries the refusal (objectui#10789).
41
+ * Either way `composed` is withheld, so no query runs without the filter.
42
+ */
38
43
  | 'missing';
39
44
  export interface UseElementDataSourceResult {
40
45
  status: ElementDataSourceStatus;
@@ -50,6 +55,14 @@ export interface UseElementDataSourceResult {
50
55
  view?: ElementSavedView;
51
56
  /** Author-facing explanation, set only for `missing`. */
52
57
  error?: string;
58
+ /**
59
+ * Why the binding could not be composed, when the reason is a refused
60
+ * filter — set only with `missing`, and `error` then carries its message
61
+ * (objectui#10789). A caller that draws the malformed-filter notice reads the
62
+ * refused operator or field off it (`filterRefusalSubject`) rather than
63
+ * scraping `error`.
64
+ */
65
+ filterRefusal?: FilterOperatorError;
53
66
  }
54
67
  /**
55
68
  * Resolve a schema node's `dataSource` binding, fetching the named saved view.
@@ -20,8 +20,8 @@
20
20
  * resolution. One resolver in the runtime layer, not one per plugin.
21
21
  */
22
22
  import { useContext, useEffect, useMemo, useState } from 'react';
23
- import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
24
- import { SchemaRendererContext } from '../context/SchemaRendererContext';
23
+ import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, FilterOperatorError, isElementDataSourceConfig, resolveSavedView, } from '@object-ui/core';
24
+ import { SchemaRendererContext } from '../context/SchemaRendererContext.js';
25
25
  const isRecord = (v) => !!v && typeof v === 'object';
26
26
  /**
27
27
  * Read every saved view for `object` from the two places they live, in the same
@@ -45,7 +45,10 @@ async function fetchSavedViews(dataSource, object) {
45
45
  if (!canReadObjectDef && !canReadOverlay)
46
46
  return { capable: false, views: {} };
47
47
  const embedded = canReadObjectDef
48
- ? dataSource.getObjectSchema(object).then((def) => (isRecord(def) ? (def.listViews ?? def.list_views) : undefined), () => undefined)
48
+ ? dataSource.getObjectSchema(object).then(
49
+ // `listViews` is canonical (#5362); the snake leg is a compatibility READ for
50
+ // stored pre-settlement documents (never censused: objectstack#7917).
51
+ (def) => (isRecord(def) ? (def.listViews ?? def.list_views) : undefined), () => undefined)
49
52
  : Promise.resolve(undefined);
50
53
  const overlay = canReadOverlay
51
54
  ? dataSource.listViews(object).then((rows) => rows, () => undefined)
@@ -68,6 +71,9 @@ async function fetchSavedViews(dataSource, object) {
68
71
  * ```
69
72
  */
70
73
  export function useElementDataSource(schema, dataSource) {
74
+ // Read AS DECLARED (objectui#7209) — no cast between the hook and the value,
75
+ // so a member the context does not declare is a compile error here. Pinned by
76
+ // `useElementDataSource.schemaRendererContextRead-7209.test.ts`.
71
77
  const context = useContext(SchemaRendererContext);
72
78
  const adapter = (dataSource ?? context?.dataSource ?? null);
73
79
  const config = useMemo(() => {
@@ -128,11 +134,25 @@ export function useElementDataSource(schema, dataSource) {
128
134
  return { status: 'loading', config };
129
135
  if (resolved.view === null)
130
136
  return { status: 'missing', config, error: resolved.error };
131
- return {
132
- status: 'resolved',
133
- config,
134
- view: resolved.view,
135
- composed: composeElementDataSource(config, resolved.view),
136
- };
137
+ // objectui#10789 — composing the view's filter with the binding's own is a
138
+ // MERGE, and the merge lowers both through the throwing converter form.
139
+ // This is a render-time `useMemo`, so an uncaught refusal was a render
140
+ // error: the block's error boundary instead of a sentence naming the
141
+ // operator. The refusal is kept as a VALUE and reported as `missing`,
142
+ // which every caller already answers by withholding the query — never as
143
+ // `undefined`, which would read as "no filter" and widen the read. Only a
144
+ // `FilterOperatorError` is caught, the rule `toFilterNodeSafely` states:
145
+ // anything else is a defect, not a statement about the author's filter.
146
+ // (The `ready` branch above composes no view, so nothing is merged there.)
147
+ let composed;
148
+ try {
149
+ composed = composeElementDataSource(config, resolved.view);
150
+ }
151
+ catch (error) {
152
+ if (!(error instanceof FilterOperatorError))
153
+ throw error;
154
+ return { status: 'missing', config, error: error.message, filterRefusal: error };
155
+ }
156
+ return { status: 'resolved', config, view: resolved.view, composed };
137
157
  }, [config, resolved, requestKey]);
138
158
  }