@object-ui/core 4.0.7 → 4.0.8
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 +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/utils/date-macros.d.ts +28 -0
- package/dist/utils/date-macros.js +102 -0
- package/dist/utils/drill-down.d.ts +91 -0
- package/dist/utils/drill-down.js +139 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
* Resolve well-known relative-date placeholders inside a filter object so
|
|
10
|
+
* both server-side aggregation and client-side `find()` calls see a real
|
|
11
|
+
* ISO date string instead of a literal `{current_quarter_start}`.
|
|
12
|
+
*
|
|
13
|
+
* Supported tokens (case-sensitive, both `{token}` and `${token}` forms):
|
|
14
|
+
* - today → start of today (YYYY-MM-DD)
|
|
15
|
+
* - now → current timestamp (full ISO)
|
|
16
|
+
* - current_week_start → Monday 00:00 of this week
|
|
17
|
+
* - current_month_start → 1st 00:00 of this month
|
|
18
|
+
* - current_quarter_start → 1st 00:00 of this quarter
|
|
19
|
+
* - current_year_start → Jan 1 00:00 of this year
|
|
20
|
+
* - last_week_start → Monday 00:00 of previous week
|
|
21
|
+
* - last_month_start → 1st 00:00 of previous month
|
|
22
|
+
* - last_quarter_start → 1st 00:00 of previous quarter
|
|
23
|
+
* - last_year_start → Jan 1 00:00 of previous year
|
|
24
|
+
*
|
|
25
|
+
* Walks any plain-object / array structure recursively. Non-string values
|
|
26
|
+
* (and unknown tokens) are passed through untouched.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveDateMacros<T = any>(filter: T, now?: Date): T;
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
* Resolve well-known relative-date placeholders inside a filter object so
|
|
10
|
+
* both server-side aggregation and client-side `find()` calls see a real
|
|
11
|
+
* ISO date string instead of a literal `{current_quarter_start}`.
|
|
12
|
+
*
|
|
13
|
+
* Supported tokens (case-sensitive, both `{token}` and `${token}` forms):
|
|
14
|
+
* - today → start of today (YYYY-MM-DD)
|
|
15
|
+
* - now → current timestamp (full ISO)
|
|
16
|
+
* - current_week_start → Monday 00:00 of this week
|
|
17
|
+
* - current_month_start → 1st 00:00 of this month
|
|
18
|
+
* - current_quarter_start → 1st 00:00 of this quarter
|
|
19
|
+
* - current_year_start → Jan 1 00:00 of this year
|
|
20
|
+
* - last_week_start → Monday 00:00 of previous week
|
|
21
|
+
* - last_month_start → 1st 00:00 of previous month
|
|
22
|
+
* - last_quarter_start → 1st 00:00 of previous quarter
|
|
23
|
+
* - last_year_start → Jan 1 00:00 of previous year
|
|
24
|
+
*
|
|
25
|
+
* Walks any plain-object / array structure recursively. Non-string values
|
|
26
|
+
* (and unknown tokens) are passed through untouched.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveDateMacros(filter, now = new Date()) {
|
|
29
|
+
if (filter == null)
|
|
30
|
+
return filter;
|
|
31
|
+
const startOfDay = (d) => {
|
|
32
|
+
const x = new Date(d);
|
|
33
|
+
x.setHours(0, 0, 0, 0);
|
|
34
|
+
return x;
|
|
35
|
+
};
|
|
36
|
+
const isoDate = (d) => {
|
|
37
|
+
const x = startOfDay(d);
|
|
38
|
+
const yyyy = x.getFullYear();
|
|
39
|
+
const mm = String(x.getMonth() + 1).padStart(2, '0');
|
|
40
|
+
const dd = String(x.getDate()).padStart(2, '0');
|
|
41
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
42
|
+
};
|
|
43
|
+
const startOfWeek = (d) => {
|
|
44
|
+
const x = startOfDay(d);
|
|
45
|
+
const day = x.getDay(); // 0=Sun, 1=Mon, ...
|
|
46
|
+
const diff = (day + 6) % 7; // distance back to Monday
|
|
47
|
+
x.setDate(x.getDate() - diff);
|
|
48
|
+
return x;
|
|
49
|
+
};
|
|
50
|
+
const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
|
|
51
|
+
const startOfQuarter = (d) => {
|
|
52
|
+
const q = Math.floor(d.getMonth() / 3);
|
|
53
|
+
return new Date(d.getFullYear(), q * 3, 1);
|
|
54
|
+
};
|
|
55
|
+
const startOfYear = (d) => new Date(d.getFullYear(), 0, 1);
|
|
56
|
+
const addMonths = (d, n) => new Date(d.getFullYear(), d.getMonth() + n, 1);
|
|
57
|
+
const macros = {
|
|
58
|
+
today: () => isoDate(now),
|
|
59
|
+
now: () => now.toISOString(),
|
|
60
|
+
current_week_start: () => isoDate(startOfWeek(now)),
|
|
61
|
+
current_month_start: () => isoDate(startOfMonth(now)),
|
|
62
|
+
current_quarter_start: () => isoDate(startOfQuarter(now)),
|
|
63
|
+
current_year_start: () => isoDate(startOfYear(now)),
|
|
64
|
+
last_week_start: () => {
|
|
65
|
+
const w = startOfWeek(now);
|
|
66
|
+
w.setDate(w.getDate() - 7);
|
|
67
|
+
return isoDate(w);
|
|
68
|
+
},
|
|
69
|
+
last_month_start: () => isoDate(addMonths(startOfMonth(now), -1)),
|
|
70
|
+
last_quarter_start: () => isoDate(addMonths(startOfQuarter(now), -3)),
|
|
71
|
+
last_year_start: () => isoDate(new Date(now.getFullYear() - 1, 0, 1)),
|
|
72
|
+
};
|
|
73
|
+
const tokenRe = /\$?\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
74
|
+
const walk = (value) => {
|
|
75
|
+
if (value == null)
|
|
76
|
+
return value;
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
const wholeMatch = value.match(/^\$?\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/);
|
|
79
|
+
if (wholeMatch && macros[wholeMatch[1]])
|
|
80
|
+
return macros[wholeMatch[1]]();
|
|
81
|
+
let touched = false;
|
|
82
|
+
const replaced = value.replace(tokenRe, (m, tok) => {
|
|
83
|
+
if (macros[tok]) {
|
|
84
|
+
touched = true;
|
|
85
|
+
return macros[tok]();
|
|
86
|
+
}
|
|
87
|
+
return m;
|
|
88
|
+
});
|
|
89
|
+
return touched ? replaced : value;
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
return value.map(walk);
|
|
93
|
+
if (typeof value === 'object') {
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const k of Object.keys(value))
|
|
96
|
+
out[k] = walk(value[k]);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
return walk(filter);
|
|
102
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* Drill-down helpers shared by PivotTable and chart widgets.
|
|
6
|
+
*
|
|
7
|
+
* The functions here implement the **Style A (declarative)** drill-down
|
|
8
|
+
* protocol: a widget schema may carry an optional `drillDown` block, and
|
|
9
|
+
* when a user clicks a cell / bar / segment the widget builds a click
|
|
10
|
+
* payload (`event`) and asks these helpers to derive a filter + title for
|
|
11
|
+
* the drill-down drawer.
|
|
12
|
+
*
|
|
13
|
+
* ```jsonc
|
|
14
|
+
* "drillDown": {
|
|
15
|
+
* "enabled": true,
|
|
16
|
+
* "filter": { "stage": "${event.rowKey}", "lead_source": "${event.colKey}" },
|
|
17
|
+
* "title": "${event.rowLabel} × ${event.colLabel}"
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The helpers are intentionally protocol-agnostic — any caller can
|
|
22
|
+
* synthesize an `event` object and rely on the same defaults / templating.
|
|
23
|
+
*/
|
|
24
|
+
import type { DrillDownConfig } from '@object-ui/types';
|
|
25
|
+
/**
|
|
26
|
+
* Generic click payload. Pivots provide row/col, charts provide
|
|
27
|
+
* category/series. Extra fields are passed through verbatim so callers
|
|
28
|
+
* can reference them in templates.
|
|
29
|
+
*/
|
|
30
|
+
export interface DrillEvent {
|
|
31
|
+
/** Pivot: row field raw value. */
|
|
32
|
+
rowKey?: string;
|
|
33
|
+
/** Pivot: column field raw value. */
|
|
34
|
+
colKey?: string;
|
|
35
|
+
/** Pivot: human-readable row label. */
|
|
36
|
+
rowLabel?: string;
|
|
37
|
+
/** Pivot: human-readable column label. */
|
|
38
|
+
colLabel?: string;
|
|
39
|
+
/** Chart: x-axis / category value. */
|
|
40
|
+
category?: string;
|
|
41
|
+
/** Chart: human-readable label for the category (when raw differs from display). */
|
|
42
|
+
categoryLabel?: string;
|
|
43
|
+
/** Chart: series name (multi-series charts). */
|
|
44
|
+
series?: string;
|
|
45
|
+
/** Aggregated value at the click point. */
|
|
46
|
+
value?: number;
|
|
47
|
+
/** Pivot scope: which cell type was clicked. */
|
|
48
|
+
scope?: 'cell' | 'row' | 'column' | 'total';
|
|
49
|
+
/** Free-form pass-through (e.g. raw record reference). */
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
/** Default field hints used to derive a filter when `config.filter` is omitted. */
|
|
53
|
+
export interface DrillDefaults {
|
|
54
|
+
rowField?: string;
|
|
55
|
+
columnField?: string;
|
|
56
|
+
/** Chart group-by field (xAxisKey / aggregate.groupBy). */
|
|
57
|
+
groupByField?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Substitute `${event.x}` placeholders in a string with values from the
|
|
61
|
+
* event payload. Unknown keys resolve to an empty string.
|
|
62
|
+
*
|
|
63
|
+
* Non-string values are coerced via `String(...)`. Returns the input
|
|
64
|
+
* unchanged when no placeholder is present.
|
|
65
|
+
*/
|
|
66
|
+
export declare function interpolate(template: string, event: DrillEvent): string;
|
|
67
|
+
/**
|
|
68
|
+
* Compute the filter object passed to the drilled list view.
|
|
69
|
+
*
|
|
70
|
+
* Resolution order:
|
|
71
|
+
* 1. `config.filter` (interpolated against the event)
|
|
72
|
+
* 2. Defaults derived from `defaults`:
|
|
73
|
+
* - `rowField` + event.rowKey (pivot)
|
|
74
|
+
* - `columnField` + event.colKey (pivot)
|
|
75
|
+
* - `groupByField` + event.category (chart)
|
|
76
|
+
*
|
|
77
|
+
* Pivot scope shortcuts:
|
|
78
|
+
* - scope === 'row' → only rowField filter
|
|
79
|
+
* - scope === 'column' → only columnField filter
|
|
80
|
+
* - scope === 'total' → empty filter (drill-through to the full set)
|
|
81
|
+
*/
|
|
82
|
+
export declare function computeDrillFilter(config: DrillDownConfig | undefined, event: DrillEvent, defaults?: DrillDefaults): Record<string, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Compute the drill-down title (for drawer/dialog header).
|
|
85
|
+
*
|
|
86
|
+
* Falls back to the most descriptive non-empty thing we know about the
|
|
87
|
+
* click point. Always returns a non-empty string.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveDrillTitle(config: DrillDownConfig | undefined, event: DrillEvent, fallback?: string): string;
|
|
90
|
+
/** Whether drill-down is enabled on a config (treats `{}` as enabled too). */
|
|
91
|
+
export declare function isDrillEnabled(config: DrillDownConfig | undefined): boolean;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* Drill-down helpers shared by PivotTable and chart widgets.
|
|
6
|
+
*
|
|
7
|
+
* The functions here implement the **Style A (declarative)** drill-down
|
|
8
|
+
* protocol: a widget schema may carry an optional `drillDown` block, and
|
|
9
|
+
* when a user clicks a cell / bar / segment the widget builds a click
|
|
10
|
+
* payload (`event`) and asks these helpers to derive a filter + title for
|
|
11
|
+
* the drill-down drawer.
|
|
12
|
+
*
|
|
13
|
+
* ```jsonc
|
|
14
|
+
* "drillDown": {
|
|
15
|
+
* "enabled": true,
|
|
16
|
+
* "filter": { "stage": "${event.rowKey}", "lead_source": "${event.colKey}" },
|
|
17
|
+
* "title": "${event.rowLabel} × ${event.colLabel}"
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The helpers are intentionally protocol-agnostic — any caller can
|
|
22
|
+
* synthesize an `event` object and rely on the same defaults / templating.
|
|
23
|
+
*/
|
|
24
|
+
const TEMPLATE = /\$\{event\.([a-zA-Z0-9_]+)\}/g;
|
|
25
|
+
/**
|
|
26
|
+
* Substitute `${event.x}` placeholders in a string with values from the
|
|
27
|
+
* event payload. Unknown keys resolve to an empty string.
|
|
28
|
+
*
|
|
29
|
+
* Non-string values are coerced via `String(...)`. Returns the input
|
|
30
|
+
* unchanged when no placeholder is present.
|
|
31
|
+
*/
|
|
32
|
+
export function interpolate(template, event) {
|
|
33
|
+
if (typeof template !== 'string' || template.indexOf('${event.') === -1) {
|
|
34
|
+
return template;
|
|
35
|
+
}
|
|
36
|
+
return template.replace(TEMPLATE, (_match, key) => {
|
|
37
|
+
const v = event[key];
|
|
38
|
+
if (v === undefined || v === null)
|
|
39
|
+
return '';
|
|
40
|
+
return String(v);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Recursively walk a filter object/value and replace `${event.*}` strings
|
|
45
|
+
* with the event values. Whole-string templates that resolve to a single
|
|
46
|
+
* event field preserve the original (typed) value — e.g. `"${event.value}"`
|
|
47
|
+
* stays a number — so backend filters can compare against the right type.
|
|
48
|
+
*/
|
|
49
|
+
function interpolateValue(value, event) {
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
// Whole-string template (e.g. "${event.rowKey}") → keep typed value
|
|
52
|
+
const m = value.match(/^\$\{event\.([a-zA-Z0-9_]+)\}$/);
|
|
53
|
+
if (m) {
|
|
54
|
+
const raw = event[m[1]];
|
|
55
|
+
// Convert empty string sentinel back to null for SQL filter friendliness
|
|
56
|
+
if (raw === '')
|
|
57
|
+
return null;
|
|
58
|
+
return raw === undefined ? null : raw;
|
|
59
|
+
}
|
|
60
|
+
return interpolate(value, event);
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map(v => interpolateValue(v, event));
|
|
64
|
+
}
|
|
65
|
+
if (value && typeof value === 'object') {
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [k, v] of Object.entries(value)) {
|
|
68
|
+
out[k] = interpolateValue(v, event);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Compute the filter object passed to the drilled list view.
|
|
76
|
+
*
|
|
77
|
+
* Resolution order:
|
|
78
|
+
* 1. `config.filter` (interpolated against the event)
|
|
79
|
+
* 2. Defaults derived from `defaults`:
|
|
80
|
+
* - `rowField` + event.rowKey (pivot)
|
|
81
|
+
* - `columnField` + event.colKey (pivot)
|
|
82
|
+
* - `groupByField` + event.category (chart)
|
|
83
|
+
*
|
|
84
|
+
* Pivot scope shortcuts:
|
|
85
|
+
* - scope === 'row' → only rowField filter
|
|
86
|
+
* - scope === 'column' → only columnField filter
|
|
87
|
+
* - scope === 'total' → empty filter (drill-through to the full set)
|
|
88
|
+
*/
|
|
89
|
+
export function computeDrillFilter(config, event, defaults = {}) {
|
|
90
|
+
if (config?.filter) {
|
|
91
|
+
return interpolateValue(config.filter, event);
|
|
92
|
+
}
|
|
93
|
+
const out = {};
|
|
94
|
+
const { rowField, columnField, groupByField } = defaults;
|
|
95
|
+
const scope = event.scope ?? 'cell';
|
|
96
|
+
if (scope === 'total')
|
|
97
|
+
return out;
|
|
98
|
+
if ((scope === 'cell' || scope === 'row') && rowField && 'rowKey' in event) {
|
|
99
|
+
out[rowField] = event.rowKey === '' ? null : event.rowKey;
|
|
100
|
+
}
|
|
101
|
+
if ((scope === 'cell' || scope === 'column') && columnField && 'colKey' in event) {
|
|
102
|
+
out[columnField] = event.colKey === '' ? null : event.colKey;
|
|
103
|
+
}
|
|
104
|
+
if (groupByField && 'category' in event) {
|
|
105
|
+
out[groupByField] = event.category === '' ? null : event.category;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Compute the drill-down title (for drawer/dialog header).
|
|
111
|
+
*
|
|
112
|
+
* Falls back to the most descriptive non-empty thing we know about the
|
|
113
|
+
* click point. Always returns a non-empty string.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveDrillTitle(config, event, fallback = 'Details') {
|
|
116
|
+
if (config?.title) {
|
|
117
|
+
const t = interpolate(config.title, event).trim();
|
|
118
|
+
if (t)
|
|
119
|
+
return t;
|
|
120
|
+
}
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (event.rowLabel)
|
|
123
|
+
parts.push(event.rowLabel);
|
|
124
|
+
if (event.colLabel)
|
|
125
|
+
parts.push(event.colLabel);
|
|
126
|
+
if (event.categoryLabel)
|
|
127
|
+
parts.push(event.categoryLabel);
|
|
128
|
+
else if (event.category)
|
|
129
|
+
parts.push(event.category);
|
|
130
|
+
if (event.series)
|
|
131
|
+
parts.push(event.series);
|
|
132
|
+
return parts.length > 0 ? parts.join(' × ') : fallback;
|
|
133
|
+
}
|
|
134
|
+
/** Whether drill-down is enabled on a config (treats `{}` as enabled too). */
|
|
135
|
+
export function isDrillEnabled(config) {
|
|
136
|
+
if (!config)
|
|
137
|
+
return false;
|
|
138
|
+
return config.enabled !== false;
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/spec": "^4.0.
|
|
28
|
+
"@objectstack/spec": "^4.0.5",
|
|
29
29
|
"lodash": "^4.18.1",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
|
-
"@object-ui/types": "4.0.
|
|
31
|
+
"@object-ui/types": "4.0.8"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^6.0.3",
|
|
35
|
-
"vitest": "^4.1.
|
|
35
|
+
"vitest": "^4.1.6"
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"objectui",
|