@object-ui/core 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.
- package/CHANGELOG.md +7404 -0
- package/README.md +127 -26
- package/dist/actions/ActionEngine.d.ts +5 -3
- package/dist/actions/ActionEngine.js +14 -10
- package/dist/actions/ActionRunner.d.ts +363 -30
- package/dist/actions/ActionRunner.js +544 -58
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +75 -13
- package/dist/actions/actionKeys.js +131 -16
- package/dist/actions/actionResponse.js +3 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
- package/dist/evaluator/ExpressionEvaluator.js +226 -13
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +118 -17
- package/dist/evaluator/fieldRules.d.ts +107 -4
- package/dist/evaluator/fieldRules.js +231 -35
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +125 -9
- package/dist/evaluator/listConditional.js +167 -26
- package/dist/evaluator/optionRules.d.ts +126 -0
- package/dist/evaluator/optionRules.js +141 -4
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
- package/dist/evaluator/rowPredicateCanon.js +158 -0
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +97 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +397 -48
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/types/index.d.ts +92 -13
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +188 -0
- package/dist/utils/chart-presentation.js +345 -0
- package/dist/utils/chart-series.d.ts +271 -31
- package/dist/utils/chart-series.js +478 -55
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +407 -52
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/debug.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +171 -11
- package/dist/utils/expand-fields.js +171 -11
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -34
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +30 -5
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +131 -1
- package/dist/utils/normalize-list-view.js +248 -6
- package/dist/utils/number-display.d.ts +70 -17
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +62 -4
- package/dist/utils/predicate-fields.js +169 -4
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +128 -26
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +9 -4
- package/dist/validation/index.js +9 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +305 -82
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -5
- package/package.json +8 -9
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- 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 (
|
|
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
|
|
69
|
-
* in no vocabulary. Defaults to a
|
|
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}`
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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,18 +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
|
-
// eslint-disable-next-line no-console
|
|
120
145
|
console.warn(`[object-ui] ${message}`);
|
|
121
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.
|
|
122
153
|
const values = {
|
|
123
154
|
current_user_id: currentUserId,
|
|
124
155
|
current_org_id: currentOrgId,
|
|
125
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
|
+
};
|
|
126
164
|
const walk = (value) => {
|
|
127
165
|
if (value == null)
|
|
128
166
|
return value;
|
|
@@ -138,10 +176,36 @@ export function resolveContextTokens(filter, scope = {}) {
|
|
|
138
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.`);
|
|
139
177
|
return value;
|
|
140
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
|
+
}
|
|
141
197
|
// Not ours. Warn only for near-misses, which resolve in no vocabulary
|
|
142
198
|
// at all and would otherwise fail silently; genuine date macros and
|
|
143
199
|
// nav context-selector ids must pass through quietly.
|
|
144
|
-
|
|
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()];
|
|
145
209
|
if (suggestion) {
|
|
146
210
|
warn(`Filter placeholder "{${token}}" is not a recognised token — did you mean ` +
|
|
147
211
|
`"{${suggestion}}"? It is sent to the server as a literal string and will ` +
|
|
@@ -151,7 +215,14 @@ export function resolveContextTokens(filter, scope = {}) {
|
|
|
151
215
|
}
|
|
152
216
|
if (Array.isArray(value))
|
|
153
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).
|
|
154
222
|
if (typeof value === 'object') {
|
|
223
|
+
const proto = Object.getPrototypeOf(value);
|
|
224
|
+
if (proto !== Object.prototype && proto !== null)
|
|
225
|
+
return value;
|
|
155
226
|
const out = {};
|
|
156
227
|
for (const k of Object.keys(value))
|
|
157
228
|
out[k] = walk(value[k]);
|
|
@@ -166,7 +237,7 @@ export function resolveContextTokens(filter, scope = {}) {
|
|
|
166
237
|
* macros and context tokens — in one call.
|
|
167
238
|
*
|
|
168
239
|
* This is the function surfaces should call. Calling only one of the two
|
|
169
|
-
* resolvers is the defect behind
|
|
240
|
+
* resolvers is the defect behind objectstack-ai/objectstack#3574: dashboard widgets called
|
|
170
241
|
* `resolveDateMacros` alone, so `{today}` worked and `{current_user_id}`
|
|
171
242
|
* silently did not.
|
|
172
243
|
*
|
|
@@ -36,6 +36,33 @@ export declare const SYSTEM_VIEW_MARKER: unique symbol;
|
|
|
36
36
|
export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? {
|
|
37
37
|
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
38
38
|
} : T;
|
|
39
|
+
/**
|
|
40
|
+
* The inverse of `DeepReadonly<T>`: recursively strip `readonly` at every depth.
|
|
41
|
+
*
|
|
42
|
+
* Named `DeepMutable` rather than `Mutable` for the same reason the type above
|
|
43
|
+
* is not called `Readonly` — TS' built-in `Readonly<T>` is SHALLOW, so a bare
|
|
44
|
+
* `Mutable` would understate what this does by exactly the depth gap that made
|
|
45
|
+
* `cloneAsOverride` wrong in the first place. A type whose name misreports its
|
|
46
|
+
* own reach is the defect this pair exists to close, not a naming preference.
|
|
47
|
+
*
|
|
48
|
+
* Symmetric with `DeepReadonly` arm for arm, deliberately including its two
|
|
49
|
+
* limits: a tuple widens to an array (`DeepReadonly` widens it the same way, so
|
|
50
|
+
* an inverse that preserved tuples would not be an inverse), and `Date`,
|
|
51
|
+
* `RegExp`, `Map` and `Set` are mapped as plain objects even though
|
|
52
|
+
* `isFreezableObject` skips them at runtime.
|
|
53
|
+
*
|
|
54
|
+
* One thing it deliberately does NOT do: drop the `SYSTEM_VIEW_MARKER` key.
|
|
55
|
+
* `cloneAsOverride` never copies the symbol, so a clone's marker is always
|
|
56
|
+
* absent — but the key is declared optional (`?: true`), and carrying it
|
|
57
|
+
* therefore states "may be absent", which is true. Excluding it needs
|
|
58
|
+
* `Exclude<keyof T, typeof SYSTEM_VIEW_MARKER>`, a NON-homomorphic mapped type
|
|
59
|
+
* that drops the `?` modifier from every other property and turns optional keys
|
|
60
|
+
* required — a strictly worse type, and a real break for callers, traded for
|
|
61
|
+
* removing a key that already reads as optional.
|
|
62
|
+
*/
|
|
63
|
+
export type DeepMutable<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? DeepMutable<U>[] : T extends object ? {
|
|
64
|
+
-readonly [K in keyof T]: DeepMutable<T[K]>;
|
|
65
|
+
} : T;
|
|
39
66
|
/**
|
|
40
67
|
* A schema that has been frozen by `defineSystemView()`. The marker symbol is
|
|
41
68
|
* non-enumerable and therefore invisible to consumers, but its presence
|
|
@@ -87,8 +114,18 @@ export declare function isSystemView(value: unknown): boolean;
|
|
|
87
114
|
* Produce a deep, mutable clone of a System View so callers can apply
|
|
88
115
|
* Tenant/User overrides without touching the source schema.
|
|
89
116
|
*
|
|
117
|
+
* Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
|
|
118
|
+
* input type unchanged, which meant cloning a `SystemView<S>` returned
|
|
119
|
+
* something still typed deep-readonly even though the implementation had always
|
|
120
|
+
* produced a plain mutable object — so the override flow this function exists
|
|
121
|
+
* for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
|
|
122
|
+
* now relaxes TOWARD what the runtime already does, never away from it, which
|
|
123
|
+
* is why the change adds capability to a caller's type rather than removing it.
|
|
124
|
+
*
|
|
90
125
|
* Implementation note: uses `structuredClone` when available (Node 17+, all
|
|
91
126
|
* evergreen browsers) and falls back to a JSON round-trip. The marker
|
|
92
127
|
* symbol is intentionally NOT copied — the clone is no longer a System View.
|
|
128
|
+
* Neither path can copy it by accident: `structuredClone` ignores symbol keys,
|
|
129
|
+
* and `JSON.stringify` never sees them.
|
|
93
130
|
*/
|
|
94
|
-
export declare function cloneAsOverride<T>(view: T): T
|
|
131
|
+
export declare function cloneAsOverride<T>(view: T): DeepMutable<T>;
|
|
@@ -140,9 +140,19 @@ export function isSystemView(value) {
|
|
|
140
140
|
* Produce a deep, mutable clone of a System View so callers can apply
|
|
141
141
|
* Tenant/User overrides without touching the source schema.
|
|
142
142
|
*
|
|
143
|
+
* Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
|
|
144
|
+
* input type unchanged, which meant cloning a `SystemView<S>` returned
|
|
145
|
+
* something still typed deep-readonly even though the implementation had always
|
|
146
|
+
* produced a plain mutable object — so the override flow this function exists
|
|
147
|
+
* for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
|
|
148
|
+
* now relaxes TOWARD what the runtime already does, never away from it, which
|
|
149
|
+
* is why the change adds capability to a caller's type rather than removing it.
|
|
150
|
+
*
|
|
143
151
|
* Implementation note: uses `structuredClone` when available (Node 17+, all
|
|
144
152
|
* evergreen browsers) and falls back to a JSON round-trip. The marker
|
|
145
153
|
* symbol is intentionally NOT copied — the clone is no longer a System View.
|
|
154
|
+
* Neither path can copy it by accident: `structuredClone` ignores symbol keys,
|
|
155
|
+
* and `JSON.stringify` never sees them.
|
|
146
156
|
*/
|
|
147
157
|
export function cloneAsOverride(view) {
|
|
148
158
|
if (view == null || typeof view !== 'object')
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — value-fallback label prettifier
|
|
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
|
+
* humanize-label — the ONE home for the VALUE fallback: a stored value becomes
|
|
10
|
+
* a display string when nothing else resolves it (objectui#5444).
|
|
11
|
+
*
|
|
12
|
+
* Two byte-identical copies preceded this module: `@object-ui/fields` (read by
|
|
13
|
+
* `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
|
|
14
|
+
* that package itself) and a deliberate local copy in `plugin-charts`'
|
|
15
|
+
* `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
|
|
16
|
+
* `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
|
|
17
|
+
* already depend on, so the convention gets a single home without a new
|
|
18
|
+
* dependency edge and without React reaching core — objectui#4389's ruling for
|
|
19
|
+
* this family: core-canonical logic, plugins consume; never core importing
|
|
20
|
+
* react. Both former sites now re-export THIS function, so there is one
|
|
21
|
+
* implementation and two doorways.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
|
|
24
|
+
*
|
|
25
|
+
* The renderer carries a second prettifier — `humanizeFieldKey` in
|
|
26
|
+
* `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
|
|
27
|
+
* key becomes a column header). It is a DIFFERENT convention, not a stale copy
|
|
28
|
+
* of this one: it additionally splits camelCase.
|
|
29
|
+
*
|
|
30
|
+
* input humanizeFieldKey humanizeLabel (here)
|
|
31
|
+
* ────────────────── ────────────────── ──────────────────
|
|
32
|
+
* needs_analysis Needs Analysis Needs Analysis
|
|
33
|
+
* NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
|
|
34
|
+
* unitPrice Unit Price UnitPrice <- differ
|
|
35
|
+
* BestCase Best Case BestCase <- differ
|
|
36
|
+
* lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
|
|
37
|
+
*
|
|
38
|
+
* They are fed different inputs, which is why their outputs may differ. A field
|
|
39
|
+
* KEY is authored in the codebase and is expected to carry a machine spelling,
|
|
40
|
+
* so splitting camelCase recovers words its author meant. A stored VALUE is
|
|
41
|
+
* arbitrary tenant data — a picklist member, an object name, a code from
|
|
42
|
+
* another system — where a mid-token capital is not reliably a word boundary,
|
|
43
|
+
* and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
|
|
44
|
+
* Note also that the two conventions do not nest: on the last row above each
|
|
45
|
+
* leaves alone the separator the other rewrites, so "converge" would have to
|
|
46
|
+
* pick a winner per input class rather than adopt one side wholesale.
|
|
47
|
+
*
|
|
48
|
+
* Kept distinct DELIBERATELY, and this paragraph is the write-down whose
|
|
49
|
+
* absence made the split read as drift. Converging them is a decision, not a
|
|
50
|
+
* refactor: this function is read by grid, gantt, detail and charts, so any
|
|
51
|
+
* change to what it RETURNS moves rendered output in four packages at once.
|
|
52
|
+
* That change is explicitly out of scope for the convergence that created this
|
|
53
|
+
* module; it needs its own card.
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* "in_progress" -> "In Progress"
|
|
57
|
+
* "high-priority" -> "High Priority"
|
|
58
|
+
* "active" -> "Active"
|
|
59
|
+
*/
|
|
60
|
+
export declare function humanizeLabel(value: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — value-fallback label prettifier
|
|
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
|
+
* humanize-label — the ONE home for the VALUE fallback: a stored value becomes
|
|
10
|
+
* a display string when nothing else resolves it (objectui#5444).
|
|
11
|
+
*
|
|
12
|
+
* Two byte-identical copies preceded this module: `@object-ui/fields` (read by
|
|
13
|
+
* `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
|
|
14
|
+
* that package itself) and a deliberate local copy in `plugin-charts`'
|
|
15
|
+
* `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
|
|
16
|
+
* `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
|
|
17
|
+
* already depend on, so the convention gets a single home without a new
|
|
18
|
+
* dependency edge and without React reaching core — objectui#4389's ruling for
|
|
19
|
+
* this family: core-canonical logic, plugins consume; never core importing
|
|
20
|
+
* react. Both former sites now re-export THIS function, so there is one
|
|
21
|
+
* implementation and two doorways.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
|
|
24
|
+
*
|
|
25
|
+
* The renderer carries a second prettifier — `humanizeFieldKey` in
|
|
26
|
+
* `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
|
|
27
|
+
* key becomes a column header). It is a DIFFERENT convention, not a stale copy
|
|
28
|
+
* of this one: it additionally splits camelCase.
|
|
29
|
+
*
|
|
30
|
+
* input humanizeFieldKey humanizeLabel (here)
|
|
31
|
+
* ────────────────── ────────────────── ──────────────────
|
|
32
|
+
* needs_analysis Needs Analysis Needs Analysis
|
|
33
|
+
* NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
|
|
34
|
+
* unitPrice Unit Price UnitPrice <- differ
|
|
35
|
+
* BestCase Best Case BestCase <- differ
|
|
36
|
+
* lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
|
|
37
|
+
*
|
|
38
|
+
* They are fed different inputs, which is why their outputs may differ. A field
|
|
39
|
+
* KEY is authored in the codebase and is expected to carry a machine spelling,
|
|
40
|
+
* so splitting camelCase recovers words its author meant. A stored VALUE is
|
|
41
|
+
* arbitrary tenant data — a picklist member, an object name, a code from
|
|
42
|
+
* another system — where a mid-token capital is not reliably a word boundary,
|
|
43
|
+
* and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
|
|
44
|
+
* Note also that the two conventions do not nest: on the last row above each
|
|
45
|
+
* leaves alone the separator the other rewrites, so "converge" would have to
|
|
46
|
+
* pick a winner per input class rather than adopt one side wholesale.
|
|
47
|
+
*
|
|
48
|
+
* Kept distinct DELIBERATELY, and this paragraph is the write-down whose
|
|
49
|
+
* absence made the split read as drift. Converging them is a decision, not a
|
|
50
|
+
* refactor: this function is read by grid, gantt, detail and charts, so any
|
|
51
|
+
* change to what it RETURNS moves rendered output in four packages at once.
|
|
52
|
+
* That change is explicitly out of scope for the convergence that created this
|
|
53
|
+
* module; it needs its own card.
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* "in_progress" -> "In Progress"
|
|
57
|
+
* "high-priority" -> "High Priority"
|
|
58
|
+
* "active" -> "Active"
|
|
59
|
+
*/
|
|
60
|
+
export function humanizeLabel(value) {
|
|
61
|
+
return value.replace(/[_-]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
62
|
+
}
|