@object-ui/core 17.6.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 (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -16,7 +16,7 @@ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
16
16
  *
17
17
  * { owner_id: '{current_user_id}' }
18
18
  *
19
- * ## Why this module exists (framework #3574)
19
+ * ## Why this module exists (objectstack-ai/objectstack#3574)
20
20
  *
21
21
  * There used to be no shared resolver. Three ad-hoc implementations had grown
22
22
  * up independently — one in `ObjectView` for list views, one in
@@ -40,6 +40,25 @@ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
40
40
  * Dropping a `{current_user_id}` filter widens a view; it must never widen
41
41
  * access. Never use a context token as an access control.
42
42
  *
43
+ * ## `{record_id}` — the record in view, not the session (objectui#7297)
44
+ *
45
+ * `@objectstack/spec` declares one sibling of the two session tokens:
46
+ * `RECORD_CONTEXT_TOKENS` = `{record_id}`, the id of the record a
47
+ * `type: 'record'` page is showing (objectstack-ai/objectstack#20003). It is a
48
+ * separate list, not a member of `CONTEXT_TOKENS`, because it resolves against
49
+ * the SURFACE rather than the session, and only here: the server never knows
50
+ * which record a page shows, so it refuses a filter still carrying the token
51
+ * by name (`FILTER_TOKEN_UNRESOLVED`).
52
+ *
53
+ * Its value is `FilterTokenScope.recordId`, which `useFilterScope()` in
54
+ * `@object-ui/react` fills from the mounted `RecordContextProvider` and from
55
+ * nothing else — never a URL parameter, never a page variable. With no record
56
+ * in scope the token is refused by name through `onUnresolved`, in the voice
57
+ * an unresolved session token gets, and left as written: it never becomes
58
+ * `null` (a count about nobody) and its condition is never dropped (a count
59
+ * about everybody). Like the session tokens it scopes what a record page
60
+ * SHOWS; which of those rows the caller may read is still RLS's decision.
61
+ *
43
62
  * ## Contract source
44
63
  *
45
64
  * The canonical vocabulary is published as part of the platform contract at
@@ -53,6 +72,20 @@ import { CONTEXT_TOKENS } from '@objectstack/spec/data';
53
72
  * copy was byte-identical, so every value comparison and every behavioural
54
73
  * test passed while it sat here. Reference identity is the one check that
55
74
  * distinguishes a re-export from a fork (objectui#3003).
75
+ *
76
+ * The same argument retired the two module-local copies that sat beside it
77
+ * until objectui#7265 — the near-miss suggestion map and the membership
78
+ * predicate are imported from `@objectstack/spec/data` now, so neither can
79
+ * drift either. Both were re-measured against the RESOLVED pin (17.4.0; the
80
+ * card had measured 17.2.0) before the swap, because "byte-identical" is a
81
+ * statement about a version, not a property: the map matched on nine keys, in
82
+ * the same order, with the same values, and the two predicates agreed on every
83
+ * one of 61 real vocabulary members — the token tuple, both suggestion key
84
+ * sets, the date-macro tokens and their aliases, plus casing, whitespace and
85
+ * prototype-key spellings. Each comparison ran a lit control of the same kind
86
+ * through the same comparator (the spec's own token-description map, and its
87
+ * date-macro predicate), so "no difference" came from an instrument shown able
88
+ * to report one.
56
89
  */
57
90
  /** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
58
91
  export { CONTEXT_TOKENS };
@@ -63,15 +96,26 @@ export interface FilterTokenScope {
63
96
  currentUserId?: string | null;
64
97
  /** The active organization id. */
65
98
  currentOrgId?: string | null;
99
+ /**
100
+ * The id of the record the surface is bound to, which resolves
101
+ * `{record_id}` (objectui#7297): the mounted record context of a
102
+ * `type: 'record'` page. `useFilterScope()` in `@object-ui/react` fills it
103
+ * from the nearest `RecordContextProvider`, and a host that renders no
104
+ * record leaves it unset, so the token is refused there. ⛔ Never fill it
105
+ * from a URL parameter or a page variable.
106
+ */
107
+ recordId?: string | null;
66
108
  /**
67
109
  * Called when a placeholder cannot be resolved — either a recognised token
68
- * with no value in scope (signed out), or a near-miss spelling that resolves
69
- * in no vocabulary. Defaults to a `console.warn`; pass `null` to silence.
110
+ * with no value in scope (signed out, or no record in context), or a
111
+ * near-miss spelling that resolves in no vocabulary. Defaults to a
112
+ * `console.warn`; pass `null` to silence.
70
113
  */
71
114
  onUnresolved?: ((message: string) => void) | null;
72
115
  }
73
116
  /**
74
- * Expand `{current_user_id}` / `{current_org_id}` inside a filter.
117
+ * Expand `{current_user_id}` / `{current_org_id}`, and `{record_id}` where a
118
+ * record is in scope, inside a filter.
75
119
  *
76
120
  * Walks arrays and plain objects recursively, which is what makes one
77
121
  * resolver cover both platform filter shapes: the MongoDB-style object a
@@ -98,7 +142,7 @@ export declare function resolveContextTokens<T = any>(filter: T, scope?: FilterT
98
142
  * macros and context tokens — in one call.
99
143
  *
100
144
  * This is the function surfaces should call. Calling only one of the two
101
- * resolvers is the defect behind framework #3574: dashboard widgets called
145
+ * resolvers is the defect behind objectstack-ai/objectstack#3574: dashboard widgets called
102
146
  * `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
103
147
  * silently did not.
104
148
  *
@@ -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 { CONTEXT_TOKENS } from '@objectstack/spec/data';
8
+ import { CONTEXT_TOKENS, CONTEXT_TOKEN_SUGGESTIONS, isContextToken, isRecordContextToken, } from '@objectstack/spec/data';
9
9
  import { resolveDateMacros } from './date-macros.js';
10
10
  /**
11
11
  * Session-scoped filter placeholders — the sibling vocabulary to
@@ -17,7 +17,7 @@ import { resolveDateMacros } from './date-macros.js';
17
17
  *
18
18
  * { owner_id: '{current_user_id}' }
19
19
  *
20
- * ## Why this module exists (framework #3574)
20
+ * ## Why this module exists (objectstack-ai/objectstack#3574)
21
21
  *
22
22
  * There used to be no shared resolver. Three ad-hoc implementations had grown
23
23
  * up independently — one in `ObjectView` for list views, one in
@@ -41,6 +41,25 @@ import { resolveDateMacros } from './date-macros.js';
41
41
  * Dropping a `{current_user_id}` filter widens a view; it must never widen
42
42
  * access. Never use a context token as an access control.
43
43
  *
44
+ * ## `{record_id}` — the record in view, not the session (objectui#7297)
45
+ *
46
+ * `@objectstack/spec` declares one sibling of the two session tokens:
47
+ * `RECORD_CONTEXT_TOKENS` = `{record_id}`, the id of the record a
48
+ * `type: 'record'` page is showing (objectstack-ai/objectstack#20003). It is a
49
+ * separate list, not a member of `CONTEXT_TOKENS`, because it resolves against
50
+ * the SURFACE rather than the session, and only here: the server never knows
51
+ * which record a page shows, so it refuses a filter still carrying the token
52
+ * by name (`FILTER_TOKEN_UNRESOLVED`).
53
+ *
54
+ * Its value is `FilterTokenScope.recordId`, which `useFilterScope()` in
55
+ * `@object-ui/react` fills from the mounted `RecordContextProvider` and from
56
+ * nothing else — never a URL parameter, never a page variable. With no record
57
+ * in scope the token is refused by name through `onUnresolved`, in the voice
58
+ * an unresolved session token gets, and left as written: it never becomes
59
+ * `null` (a count about nobody) and its condition is never dropped (a count
60
+ * about everybody). Like the session tokens it scopes what a record page
61
+ * SHOWS; which of those rows the caller may read is still RLS's decision.
62
+ *
44
63
  * ## Contract source
45
64
  *
46
65
  * The canonical vocabulary is published as part of the platform contract at
@@ -54,40 +73,47 @@ import { resolveDateMacros } from './date-macros.js';
54
73
  * copy was byte-identical, so every value comparison and every behavioural
55
74
  * test passed while it sat here. Reference identity is the one check that
56
75
  * distinguishes a re-export from a fork (objectui#3003).
76
+ *
77
+ * The same argument retired the two module-local copies that sat beside it
78
+ * until objectui#7265 — the near-miss suggestion map and the membership
79
+ * predicate are imported from `@objectstack/spec/data` now, so neither can
80
+ * drift either. Both were re-measured against the RESOLVED pin (17.4.0; the
81
+ * card had measured 17.2.0) before the swap, because "byte-identical" is a
82
+ * statement about a version, not a property: the map matched on nine keys, in
83
+ * the same order, with the same values, and the two predicates agreed on every
84
+ * one of 61 real vocabulary members — the token tuple, both suggestion key
85
+ * sets, the date-macro tokens and their aliases, plus casing, whitespace and
86
+ * prototype-key spellings. Each comparison ran a lit control of the same kind
87
+ * through the same comparator (the spec's own token-description map, and its
88
+ * date-macro predicate), so "no difference" came from an instrument shown able
89
+ * to report one.
57
90
  */
58
91
  /** The complete set of session-scoped filter tokens (spec-owned, re-exported). */
59
92
  export { CONTEXT_TOKENS };
60
- /**
61
- * Near-miss spellings → the token the author meant.
62
- *
63
- * Every entry is a real authoring mistake, and each is a correct spelling
64
- * *somewhere else* in the platform, which is exactly why authors reach for it:
65
- * `current_user` is the RLS expression root, `{user_id}` is valid
66
- * `titleFormat` field interpolation, `organization_id` is a real column name.
67
- *
68
- * Mirrors `CONTEXT_TOKEN_SUGGESTIONS` in `@objectstack/spec`. Used only to
69
- * make the runtime warning actionable; the authoring-time gate
70
- * (`validateFilterTokens` in `@objectstack/lint`) is what actually prevents
71
- * these from shipping.
72
- */
73
- const CONTEXT_TOKEN_SUGGESTIONS = {
74
- current_user: 'current_user_id',
75
- current_user_email: 'current_user_id',
76
- user_id: 'current_user_id',
77
- userid: 'current_user_id',
78
- me: 'current_user_id',
79
- current_organization_id: 'current_org_id',
80
- org_id: 'current_org_id',
81
- organization_id: 'current_org_id',
82
- current_tenant_id: 'current_org_id',
83
- };
84
93
  /** Whole-string placeholder: `{token}` or `${token}`, anchored. */
85
94
  const WHOLE_TOKEN_RE = /^\$?\{([a-zA-Z0-9_]+)\}$/;
86
- function isContextToken(token) {
87
- return CONTEXT_TOKENS.includes(token);
88
- }
89
95
  /**
90
- * Expand `{current_user_id}` / `{current_org_id}` inside a filter.
96
+ * A null-prototype copy of the spec's near-miss suggestion map (objectui#9129).
97
+ *
98
+ * `CONTEXT_TOKEN_SUGGESTIONS` is a plain object, so indexing it with an
99
+ * author-controlled, lower-cased string reaches `Object.prototype` for any
100
+ * spelling that happens to be an inherited member name — `constructor` and
101
+ * `__proto__` today, and, silently, whichever future member is added in
102
+ * lower case (see the lookup below). This is a read-only console hint, never
103
+ * an assignment, so no filter value is ever widened, narrowed or mismatched
104
+ * by it; the only externally visible effect of the underlying bug is a
105
+ * confusing suggestion string in a `warn()` call.
106
+ *
107
+ * Copying into an `Object.create(null)` base closes the whole class rather
108
+ * than special-casing the two spellings measured today: this object has no
109
+ * prototype at all, so *no* key — known or future — can resolve through it.
110
+ * `@objectstack/spec`'s own map is left untouched (out of scope here; the
111
+ * identical shape in its `classifyFilterToken` is the one objectstack `4342c9923` closed).
112
+ */
113
+ const NEAR_MISS_SUGGESTIONS = Object.assign(Object.create(null), CONTEXT_TOKEN_SUGGESTIONS);
114
+ /**
115
+ * Expand `{current_user_id}` / `{current_org_id}`, and `{record_id}` where a
116
+ * record is in scope, inside a filter.
91
117
  *
92
118
  * Walks arrays and plain objects recursively, which is what makes one
93
119
  * resolver cover both platform filter shapes: the MongoDB-style object a
@@ -111,17 +137,30 @@ function isContextToken(token) {
111
137
  export function resolveContextTokens(filter, scope = {}) {
112
138
  if (filter == null)
113
139
  return filter;
114
- const { currentUserId, currentOrgId } = scope;
140
+ const { currentUserId, currentOrgId, recordId } = scope;
115
141
  const warn = scope.onUnresolved === null
116
142
  ? () => { }
117
143
  : scope.onUnresolved ??
118
144
  ((message) => {
119
145
  console.warn(`[object-ui] ${message}`);
120
146
  });
147
+ // Annotated `Record<string, …>` because the spec's predicate returns a plain
148
+ // `boolean`, not the narrowing `token is ContextTokenName` the deleted local
149
+ // copy declared, so the narrowed branch below hands this lookup a `string`.
150
+ // The `satisfies` clause keeps the one thing that narrowing bought: a third
151
+ // context token in the spec reds this literal at compile time instead of
152
+ // resolving to `undefined` at runtime.
121
153
  const values = {
122
154
  current_user_id: currentUserId,
123
155
  current_org_id: currentOrgId,
124
156
  };
157
+ // The record-context sibling, kept apart for the reason the spec keeps the
158
+ // two lists apart: it is filled from the record in view, never from the
159
+ // session. The same `satisfies` ratchet: a second record-context token in
160
+ // the spec reds this literal at compile time.
161
+ const recordValues = {
162
+ record_id: recordId,
163
+ };
125
164
  const walk = (value) => {
126
165
  if (value == null)
127
166
  return value;
@@ -137,10 +176,36 @@ export function resolveContextTokens(filter, scope = {}) {
137
176
  warn(`Filter placeholder "{${token}}" could not be resolved — no ${token === 'current_user_id' ? 'signed-in user' : 'active organization'} in scope. The filter will match no records.`);
138
177
  return value;
139
178
  }
179
+ // `{record_id}` (objectui#7297). Resolved only from the record in scope.
180
+ // With none, it is refused BY NAME, in the voice of the unresolved
181
+ // session token above, and left as written: never `null` (a count about
182
+ // nobody), never dropped (a count about everybody), and never reported as
183
+ // an unknown spelling, because the spelling is right and the surface is
184
+ // not. The ObjectStack server then refuses the same filter by name
185
+ // (`FILTER_TOKEN_UNRESOLVED`), since no server path has a record in view.
186
+ if (isRecordContextToken(token)) {
187
+ const resolved = recordValues[token];
188
+ if (resolved != null && resolved !== '')
189
+ return resolved;
190
+ warn(`Filter placeholder "{${token}}" could not be resolved — no record in context ` +
191
+ `on this surface. It resolves only on a component of a \`type: 'record'\` page, ` +
192
+ `to the id of the record that page shows. It is left as written, so the filter ` +
193
+ `never widens: the server refuses it by name, and a backend with no resolver ` +
194
+ `matches no records.`);
195
+ return value;
196
+ }
140
197
  // Not ours. Warn only for near-misses, which resolve in no vocabulary
141
198
  // at all and would otherwise fail silently; genuine date macros and
142
199
  // nav context-selector ids must pass through quietly.
143
- const suggestion = CONTEXT_TOKEN_SUGGESTIONS[token.toLowerCase()];
200
+ //
201
+ // Every spelling in the imported map is a real authoring mistake, and
202
+ // each is a correct spelling *somewhere else* in the platform, which is
203
+ // exactly why authors reach for it: `current_user` is the RLS expression
204
+ // root, `{user_id}` is valid `titleFormat` field interpolation,
205
+ // `organization_id` is a real column name. The map only makes the runtime
206
+ // warning actionable; the authoring-time gate (`validateFilterTokens` in
207
+ // `@objectstack/lint`) is what actually prevents these from shipping.
208
+ const suggestion = NEAR_MISS_SUGGESTIONS[token.toLowerCase()];
144
209
  if (suggestion) {
145
210
  warn(`Filter placeholder "{${token}}" is not a recognised token — did you mean ` +
146
211
  `"{${suggestion}}"? It is sent to the server as a literal string and will ` +
@@ -150,7 +215,14 @@ export function resolveContextTokens(filter, scope = {}) {
150
215
  }
151
216
  if (Array.isArray(value))
152
217
  return value.map(walk);
218
+ // Only a PLAIN object (prototype `Object.prototype` or `null`) is a bag to
219
+ // walk. Anything else is a leaf, returned as the same instance: rebuilding
220
+ // a `Date` from its own keys — it has none — turned a comparand the spec
221
+ // admits (`ACCEPTED_FILTER_COMPARAND_TYPES`) into `{}` (objectui#10506).
153
222
  if (typeof value === 'object') {
223
+ const proto = Object.getPrototypeOf(value);
224
+ if (proto !== Object.prototype && proto !== null)
225
+ return value;
154
226
  const out = {};
155
227
  for (const k of Object.keys(value))
156
228
  out[k] = walk(value[k]);
@@ -165,7 +237,7 @@ export function resolveContextTokens(filter, scope = {}) {
165
237
  * macros and context tokens — in one call.
166
238
  *
167
239
  * This is the function surfaces should call. Calling only one of the two
168
- * resolvers is the defect behind framework #3574: dashboard widgets called
240
+ * resolvers is the defect behind objectstack-ai/objectstack#3574: dashboard widgets called
169
241
  * `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
170
242
  * silently did not.
171
243
  *
@@ -0,0 +1,61 @@
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 THIRD half of a view's field appetite (objectui#7179).
10
+ *
11
+ * A list view projects what it DISPLAYS — `$select` is built from `columns`.
12
+ * `predicate-fields.ts` covers the fields a view's PREDICATES read. This covers
13
+ * the fields it GROUPS BY, which is a third, independent source of demand: the
14
+ * spec's `grouping` block is a sibling of `columns`, not a subset of it, so a
15
+ * view may legitimately group by a field it never shows.
16
+ *
17
+ * ## Why it needed a fix rather than a gate
18
+ *
19
+ * `GroupingConfigSchema` accepts `grouping` with no matching column, and the
20
+ * neighbouring view kinds (kanban / gantt / timeline) already union their
21
+ * `groupByField` into the projection with no column required. Grouping by a
22
+ * field you do not want on screen is an ordinary thing to want. Refusing it at
23
+ * author time would make the grid the odd one out and reject working intent.
24
+ *
25
+ * ## The failure this closes
26
+ *
27
+ * With the grouping field absent from `$select` the server never returns it,
28
+ * so `useGroupedData` reads `undefined` on every row and `buildSegmentLabel`
29
+ * answers `(empty)` for all of them: ONE group holding every record, with no
30
+ * error, no warning and no empty state. It reads as "these records have no
31
+ * value for this field" — a plausible, wrong, actionable conclusion about the
32
+ * data rather than a visible bug in the view.
33
+ *
34
+ * ## ⛔ THE RESULT IS CANDIDATES, NOT VERIFIED FIELDS — the caller MUST gate it
35
+ *
36
+ * `GroupingFieldSchema.field` is a string that refuses a padded name
37
+ * (objectui#7347 — `grouping-field-padded-refused-7347.test.ts` in
38
+ * `@object-ui/types` pins it on every face declaring `grouping`; the trim below
39
+ * stays as defence for blocks that skipped that parse). Nothing in the schema
40
+ * requires it to name a field the object declares, and the whole premise of
41
+ * this harvest is that it has NOT been through column validation. Some backends
42
+ * answer an unknown `$select` key with an EMPTY RESULT SET rather than ignoring
43
+ * it — the cloud multi-tenant runtime does exactly that — so a single unknown
44
+ * grouping field put in the projection unguarded silently zeroes the whole
45
+ * list. That would convert this card's bug (one `(empty)` group holding every
46
+ * row) into a strictly worse one (no rows at all, still silent).
47
+ *
48
+ * So every caller intersects this result with the object's declared fields —
49
+ * {@link isProjectableField}, or the caller's equivalent known-field set —
50
+ * exactly as {@link collectPredicateFieldRefs}'s callers do, and for the same
51
+ * measured reason. Callers additionally FLS-gate it: a grouping field names a
52
+ * field just as capable of being denied as a column is, and the projection is
53
+ * what goes on the wire (objectui#6898).
54
+ *
55
+ * @param grouping - The view's `grouping` block in any authored state
56
+ * (`undefined`, malformed, or the spec shape). Anything that is not an entry
57
+ * carrying a non-empty string `field` contributes nothing, so a malformed
58
+ * block yields an empty harvest instead of a plausible wrong name.
59
+ * @returns Grouping field names in first-seen order, deduplicated.
60
+ */
61
+ export declare function collectGroupingFieldRefs(grouping: unknown): string[];
@@ -0,0 +1,84 @@
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 THIRD half of a view's field appetite (objectui#7179).
10
+ *
11
+ * A list view projects what it DISPLAYS — `$select` is built from `columns`.
12
+ * `predicate-fields.ts` covers the fields a view's PREDICATES read. This covers
13
+ * the fields it GROUPS BY, which is a third, independent source of demand: the
14
+ * spec's `grouping` block is a sibling of `columns`, not a subset of it, so a
15
+ * view may legitimately group by a field it never shows.
16
+ *
17
+ * ## Why it needed a fix rather than a gate
18
+ *
19
+ * `GroupingConfigSchema` accepts `grouping` with no matching column, and the
20
+ * neighbouring view kinds (kanban / gantt / timeline) already union their
21
+ * `groupByField` into the projection with no column required. Grouping by a
22
+ * field you do not want on screen is an ordinary thing to want. Refusing it at
23
+ * author time would make the grid the odd one out and reject working intent.
24
+ *
25
+ * ## The failure this closes
26
+ *
27
+ * With the grouping field absent from `$select` the server never returns it,
28
+ * so `useGroupedData` reads `undefined` on every row and `buildSegmentLabel`
29
+ * answers `(empty)` for all of them: ONE group holding every record, with no
30
+ * error, no warning and no empty state. It reads as "these records have no
31
+ * value for this field" — a plausible, wrong, actionable conclusion about the
32
+ * data rather than a visible bug in the view.
33
+ *
34
+ * ## ⛔ THE RESULT IS CANDIDATES, NOT VERIFIED FIELDS — the caller MUST gate it
35
+ *
36
+ * `GroupingFieldSchema.field` is a string that refuses a padded name
37
+ * (objectui#7347 — `grouping-field-padded-refused-7347.test.ts` in
38
+ * `@object-ui/types` pins it on every face declaring `grouping`; the trim below
39
+ * stays as defence for blocks that skipped that parse). Nothing in the schema
40
+ * requires it to name a field the object declares, and the whole premise of
41
+ * this harvest is that it has NOT been through column validation. Some backends
42
+ * answer an unknown `$select` key with an EMPTY RESULT SET rather than ignoring
43
+ * it — the cloud multi-tenant runtime does exactly that — so a single unknown
44
+ * grouping field put in the projection unguarded silently zeroes the whole
45
+ * list. That would convert this card's bug (one `(empty)` group holding every
46
+ * row) into a strictly worse one (no rows at all, still silent).
47
+ *
48
+ * So every caller intersects this result with the object's declared fields —
49
+ * {@link isProjectableField}, or the caller's equivalent known-field set —
50
+ * exactly as {@link collectPredicateFieldRefs}'s callers do, and for the same
51
+ * measured reason. Callers additionally FLS-gate it: a grouping field names a
52
+ * field just as capable of being denied as a column is, and the projection is
53
+ * what goes on the wire (objectui#6898).
54
+ *
55
+ * @param grouping - The view's `grouping` block in any authored state
56
+ * (`undefined`, malformed, or the spec shape). Anything that is not an entry
57
+ * carrying a non-empty string `field` contributes nothing, so a malformed
58
+ * block yields an empty harvest instead of a plausible wrong name.
59
+ * @returns Grouping field names in first-seen order, deduplicated.
60
+ */
61
+ export function collectGroupingFieldRefs(grouping) {
62
+ const fields = grouping?.fields;
63
+ if (!Array.isArray(fields))
64
+ return [];
65
+ const out = [];
66
+ const seen = new Set();
67
+ for (const entry of fields) {
68
+ // The spec shape is `{ field, order, collapsed }`. A bare string is NOT
69
+ // accepted here even though it would be a natural shorthand: `grouping` is
70
+ // a `$strict` object schema in `@objectstack/spec`, so a bare string is
71
+ // off-spec metadata, and reading it anyway would be exactly the lenient
72
+ // renderer-side alias AGENTS.md #0.1 forbids — it fossilizes a second
73
+ // de-facto contract instead of having the producer rejected at publish.
74
+ const name = entry?.field;
75
+ if (typeof name !== 'string')
76
+ continue;
77
+ const trimmed = name.trim();
78
+ if (!trimmed || seen.has(trimmed))
79
+ continue;
80
+ seen.add(trimmed);
81
+ out.push(trimmed);
82
+ }
83
+ return out;
84
+ }
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * What is genuinely objectui's and keeps a local name:
21
21
  * {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
22
- * INTERSECTED with the server-resolved effective API operation set (#3391), so
22
+ * INTERSECTED with the server-resolved effective API operation set (objectstack#3391), so
23
23
  * the UI never offers a button the server would 405. The spec's function has
24
24
  * no such notion; naming ours after it was the misdescription this burn-down
25
25
  * removes.
@@ -125,7 +125,7 @@ export declare function userActionPredicates(v: UserActionOverride | undefined |
125
125
  *
126
126
  * @param obj The object schema (managedBy bucket + userActions overrides).
127
127
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
128
- * set for this object (from `/me/permissions` `apiOperations`, #3391). When
128
+ * set for this object (from `/me/permissions` `apiOperations`, objectstack#3391). When
129
129
  * provided, each affordance bit is ANDed with the corresponding API operation
130
130
  * (create/import→create/import, edit→update, delete→delete, exportCsv→export),
131
131
  * so the UI never shows a button the server would 405. Passing `undefined`
@@ -163,7 +163,7 @@ export declare function isSystemWritable(obj: SchemaLike | null | undefined): bo
163
163
  * object-level editability, so only the resolved boolean matters here.)
164
164
  *
165
165
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
166
- * set for this object (`/me/permissions` `apiOperations`, #3391/#3546). When
166
+ * set for this object (`/me/permissions` `apiOperations`, objectstack#3391/objectstack#3546). When
167
167
  * provided, inline-edit is additionally ANDed with the server allowing
168
168
  * `update`, so a record page never offers double-click/pencil editing the
169
169
  * server would 405. `undefined` (old backend / unrestricted) leaves the
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * What is genuinely objectui's and keeps a local name:
21
21
  * {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
22
- * INTERSECTED with the server-resolved effective API operation set (#3391), so
22
+ * INTERSECTED with the server-resolved effective API operation set (objectstack#3391), so
23
23
  * the UI never offers a button the server would 405. The spec's function has
24
24
  * no such notion; naming ours after it was the misdescription this burn-down
25
25
  * removes.
@@ -73,7 +73,7 @@ export function userActionPredicates(v) {
73
73
  }
74
74
  /**
75
75
  * The affordance-bit → server API-operation mapping used to intersect the
76
- * UI-intent affordances with the server's effective API operation set (#3391).
76
+ * UI-intent affordances with the server's effective API operation set (objectstack#3391).
77
77
  * This is the objectui end of the framework's `API_METHOD_DERIVATION` contract:
78
78
  * the button predicate is `affordance(op) ∧ effective.api(op)`.
79
79
  */
@@ -97,7 +97,7 @@ const AFFORDANCE_TO_API_OPERATION = {
97
97
  *
98
98
  * @param obj The object schema (managedBy bucket + userActions overrides).
99
99
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
100
- * set for this object (from `/me/permissions` `apiOperations`, #3391). When
100
+ * set for this object (from `/me/permissions` `apiOperations`, objectstack#3391). When
101
101
  * provided, each affordance bit is ANDed with the corresponding API operation
102
102
  * (create/import→create/import, edit→update, delete→delete, exportCsv→export),
103
103
  * so the UI never shows a button the server would 405. Passing `undefined`
@@ -113,7 +113,7 @@ export function resolveEffectiveCrudAffordances(obj, effectiveApiOperations) {
113
113
  managedBy: obj?.managedBy ?? undefined,
114
114
  userActions: (obj?.userActions ?? undefined),
115
115
  });
116
- // [#3391] Intersect with the server's effective API operations when present.
116
+ // [objectstack#3391] Intersect with the server's effective API operations when present.
117
117
  // The frontend consumes the effective set the server hands down; it never
118
118
  // reads the raw `apiMethods` nor re-derives.
119
119
  if (Array.isArray(effectiveApiOperations)) {
@@ -159,7 +159,7 @@ export function isSystemWritable(obj) {
159
159
  * object-level editability, so only the resolved boolean matters here.)
160
160
  *
161
161
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
162
- * set for this object (`/me/permissions` `apiOperations`, #3391/#3546). When
162
+ * set for this object (`/me/permissions` `apiOperations`, objectstack#3391/objectstack#3546). When
163
163
  * provided, inline-edit is additionally ANDed with the server allowing
164
164
  * `update`, so a record page never offers double-click/pencil editing the
165
165
  * server would 405. `undefined` (old backend / unrestricted) leaves the
@@ -0,0 +1,48 @@
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
+ * Stored value → `<input type="date">` value (`YYYY-MM-DD`).
10
+ *
11
+ * A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
12
+ * date-only string is parsed as UTC midnight by `new Date()`, so reading local
13
+ * calendar components back out of it shifts the day by one everywhere west of
14
+ * Greenwich. `2026-06-17` must stay June 17th in every zone.
15
+ */
16
+ export declare function toDateInputValue(value: unknown): string;
17
+ /**
18
+ * Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
19
+ * expressed in the viewer's local wall clock — the same instant the detail
20
+ * page shows, so opening the editor never appears to move the time.
21
+ *
22
+ * A value that is ALREADY a zone-less local datetime is truncated to minutes
23
+ * in place instead of being round-tripped through `Date`, which would
24
+ * reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
25
+ */
26
+ export declare function toDateTimeInputValue(value: unknown): string;
27
+ /**
28
+ * `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
29
+ *
30
+ * The control hands us a zone-less local wall clock; `new Date()` reads that
31
+ * form as local time, which is the inverse of what `toDateTimeInputValue`
32
+ * wrote — the pair is what keeps read and write on one basis.
33
+ *
34
+ * An unparseable string is returned untouched rather than blanked: dropping a
35
+ * value the user can see in the box would be a data loss dressed up as a fix.
36
+ */
37
+ export declare function fromDateTimeInputValue(value: string): string;
38
+ /**
39
+ * True when a stored value is written on a calendar day that does not exist
40
+ * (`2026-02-30`, `2026-02-30T10:00:00Z`, `2026-02-30T10:00+08:00`) — the day
41
+ * read AS WRITTEN from the leading `YYYY-MM-DD`, by the one judgement the read
42
+ * faces use (`isRealCalendarDate`). A parsed `Date` always names a real day,
43
+ * so the check can only be made before any conversion.
44
+ *
45
+ * Anything without a leading `YYYY-MM-DD` (a `Date`, a number, free text) is
46
+ * not this case and answers `false`.
47
+ */
48
+ export declare function isImpossibleStoredDay(value: unknown): boolean;