@malloy-publisher/sdk 0.0.181 → 0.0.183-dev
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/dist/ServerProvider-lOQXDlqB.cjs.js +1 -0
- package/dist/{ServerProvider-DN2wSIAZ.es.js → ServerProvider-on-8FH5Z.es.js} +1259 -764
- package/dist/client/api.d.ts +674 -20
- package/dist/client/index.cjs.js +1 -1
- package/dist/client/index.es.js +57 -44
- package/dist/core-CjeTkq8O.es.js +7515 -0
- package/dist/core-YNpOLuB1.cjs.js +148 -0
- package/dist/engine-oniguruma-BkproSVE.cjs.js +1 -0
- package/dist/engine-oniguruma-C4vnmooL.es.js +272 -0
- package/dist/github-light-BFTOhCbz.cjs.js +1 -0
- package/dist/github-light-JYsPkUQd.es.js +4 -0
- package/dist/hooks/useDimensionFilters.d.ts +2 -0
- package/dist/hooks/useDimensionalFilterRangeData.d.ts +19 -5
- package/dist/index-BOLBP6_i.cjs.js +233 -0
- package/dist/index-DRDu9kIV.es.js +53627 -0
- package/dist/index.cjs.js +1 -244
- package/dist/index.es.js +48 -58039
- package/dist/json-71t8ZF9g.es.js +6 -0
- package/dist/json-y-J1j5EW.cjs.js +1 -0
- package/dist/sql-BqWZrLHB.cjs.js +1 -0
- package/dist/sql-DCkt643-.es.js +6 -0
- package/dist/typescript-BqvpT6pB.cjs.js +1 -0
- package/dist/typescript-buWNZFwO.es.js +6 -0
- package/package.json +6 -8
- package/src/components/Notebook/Notebook.tsx +162 -108
- package/src/components/ServerProvider.tsx +8 -0
- package/src/components/filter/DimensionFilter.tsx +68 -39
- package/src/components/highlighter.ts +54 -32
- package/src/hooks/useDimensionFilters.ts +2 -0
- package/src/hooks/useDimensionalFilterRangeData.ts +27 -13
- package/src/index.ts +0 -5
- package/dist/ServerProvider-DSeOLGfV.cjs.js +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/sdk",
|
|
3
3
|
"description": "Malloy Publisher SDK",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.183-dev",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs.js",
|
|
7
7
|
"module": "dist/index.es.js",
|
|
@@ -47,9 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"style": "./dist/malloy-explorer.css",
|
|
49
49
|
"sideEffects": [
|
|
50
|
-
"**/*.css"
|
|
51
|
-
"./src/index.ts",
|
|
52
|
-
"./dist/index.*.js"
|
|
50
|
+
"**/*.css"
|
|
53
51
|
],
|
|
54
52
|
"repository": {
|
|
55
53
|
"type": "git",
|
|
@@ -72,10 +70,10 @@
|
|
|
72
70
|
"react-dom": ">=19.1.0",
|
|
73
71
|
"react-router-dom": ">=7.6.2",
|
|
74
72
|
"@tanstack/react-query": ">=5.59.16",
|
|
75
|
-
"@malloydata/malloy-explorer": "
|
|
76
|
-
"@malloydata/malloy-interfaces": "
|
|
77
|
-
"@malloydata/malloy-query-builder": "
|
|
78
|
-
"@malloydata/render": "
|
|
73
|
+
"@malloydata/malloy-explorer": ">=0.0.338-dev260215172810",
|
|
74
|
+
"@malloydata/malloy-interfaces": ">=0.0.374",
|
|
75
|
+
"@malloydata/malloy-query-builder": ">=0.0.374",
|
|
76
|
+
"@malloydata/render": ">=0.0.374",
|
|
79
77
|
"@mui/icons-material": ">=7.1.1",
|
|
80
78
|
"@mui/x-date-pickers": ">=7.1.1",
|
|
81
79
|
"@mui/material": ">=7.1.1",
|
|
@@ -2,7 +2,7 @@ import "@malloydata/malloy-explorer/styles.css";
|
|
|
2
2
|
import * as Malloy from "@malloydata/malloy-interfaces";
|
|
3
3
|
import { Box, Paper, Stack, Typography } from "@mui/material";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
-
import { RawNotebook } from "../../client";
|
|
5
|
+
import { RawNotebook, Source } from "../../client";
|
|
6
6
|
import {
|
|
7
7
|
getDimensionKey,
|
|
8
8
|
useDimensionalFilterRangeData,
|
|
@@ -16,10 +16,6 @@ import { ApiErrorDisplay } from "../ApiErrorDisplay";
|
|
|
16
16
|
import { DimensionFilter, RetrievalFunction } from "../filter/DimensionFilter";
|
|
17
17
|
import {
|
|
18
18
|
extractDimensionSpecs,
|
|
19
|
-
extractSourceFromQuery,
|
|
20
|
-
generateFilterClause,
|
|
21
|
-
getJoinedSources,
|
|
22
|
-
injectWhereClause,
|
|
23
19
|
parseAllSourceInfos,
|
|
24
20
|
parseNotebookFilterAnnotation,
|
|
25
21
|
} from "../filter/utils";
|
|
@@ -81,7 +77,7 @@ export default function Notebook({
|
|
|
81
77
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
82
78
|
const [executionError, setExecutionError] = useState<Error | null>(null);
|
|
83
79
|
|
|
84
|
-
// Parse filter configuration from notebook annotations
|
|
80
|
+
// Parse filter configuration from notebook annotations (legacy ##(filters) approach)
|
|
85
81
|
const filterConfig = useMemo(() => {
|
|
86
82
|
if (!notebook) return null;
|
|
87
83
|
return parseNotebookFilterAnnotation(notebook.annotations);
|
|
@@ -99,16 +95,112 @@ export default function Notebook({
|
|
|
99
95
|
);
|
|
100
96
|
const modelPath = sourceData?.modelPath ?? null;
|
|
101
97
|
|
|
98
|
+
// Extract server-side filter definitions from notebook sources
|
|
99
|
+
// These come from #(filter) annotations parsed by the server
|
|
100
|
+
const serverFilters = useMemo(() => {
|
|
101
|
+
const result = new Map<string, Source["filters"]>();
|
|
102
|
+
if (!notebook?.sources) return result;
|
|
103
|
+
for (const source of notebook.sources as Source[]) {
|
|
104
|
+
if (source.name && source.filters && source.filters.length > 0) {
|
|
105
|
+
// Exclude implicit filters from the UI
|
|
106
|
+
const visibleFilters = source.filters.filter((f) => !f.implicit);
|
|
107
|
+
if (visibleFilters.length > 0) {
|
|
108
|
+
result.set(source.name, visibleFilters);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}, [notebook]);
|
|
114
|
+
|
|
115
|
+
// Determine if we're using server-driven filters (#(filter)) or legacy (##(filters))
|
|
116
|
+
const useServerFilters = serverFilters.size > 0;
|
|
117
|
+
|
|
102
118
|
// Build dimension specs from filter config and source info map
|
|
103
119
|
// Each spec includes source and model for proper query routing
|
|
104
120
|
const dimensionSpecs = useMemo(() => {
|
|
121
|
+
if (useServerFilters && modelPath) {
|
|
122
|
+
// Server-driven: build specs from #(filter) metadata
|
|
123
|
+
const specs: import("../../hooks/useDimensionalFilterRangeData").DimensionSpec[] =
|
|
124
|
+
[];
|
|
125
|
+
for (const [sourceName, filters] of serverFilters) {
|
|
126
|
+
for (const filter of filters ?? []) {
|
|
127
|
+
if (!filter.dimension || !filter.type) continue;
|
|
128
|
+
|
|
129
|
+
// Choose widget type based on the dimension's data type first,
|
|
130
|
+
// then fall back to the annotation's comparator type
|
|
131
|
+
type FT =
|
|
132
|
+
import("../../hooks/useDimensionalFilterRangeData").FilterType;
|
|
133
|
+
let filterType: FT;
|
|
134
|
+
const dimType = filter.dimensionType;
|
|
135
|
+
if (dimType === "boolean") {
|
|
136
|
+
filterType = "Boolean";
|
|
137
|
+
} else if (
|
|
138
|
+
dimType === "date" ||
|
|
139
|
+
dimType === "timestamp" ||
|
|
140
|
+
dimType === "timestamptz"
|
|
141
|
+
) {
|
|
142
|
+
filterType = "DateMinMax";
|
|
143
|
+
} else if (dimType === "number") {
|
|
144
|
+
filterType =
|
|
145
|
+
filter.type === "equal" || filter.type === "in"
|
|
146
|
+
? "Star"
|
|
147
|
+
: "MinMax";
|
|
148
|
+
} else {
|
|
149
|
+
const filterTypeMap: Record<string, FT> = {
|
|
150
|
+
equal: "Star",
|
|
151
|
+
in: "Star",
|
|
152
|
+
like: "Star",
|
|
153
|
+
greater_than: "MinMax",
|
|
154
|
+
less_than: "MinMax",
|
|
155
|
+
};
|
|
156
|
+
filterType = filterTypeMap[filter.type] ?? "Star";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Derive the match type from the #(filter) annotation type so
|
|
160
|
+
// the UI never needs to show a match-type dropdown.
|
|
161
|
+
type MT = import("../../hooks/useDimensionFilters").MatchType;
|
|
162
|
+
const matchTypeMap: Record<string, MT> = {
|
|
163
|
+
equal: "Equals",
|
|
164
|
+
in: "Equals",
|
|
165
|
+
like: "Contains",
|
|
166
|
+
greater_than:
|
|
167
|
+
filterType === "DateMinMax" ? "After" : "Greater Than",
|
|
168
|
+
less_than:
|
|
169
|
+
filterType === "DateMinMax" ? "Before" : "Less Than",
|
|
170
|
+
};
|
|
171
|
+
const defaultMatchType: MT | undefined =
|
|
172
|
+
matchTypeMap[filter.type!];
|
|
173
|
+
|
|
174
|
+
const filterLabel =
|
|
175
|
+
filter.name !== filter.dimension ? filter.name : undefined;
|
|
176
|
+
specs.push({
|
|
177
|
+
source: sourceName,
|
|
178
|
+
model: modelPath,
|
|
179
|
+
dimensionName: filter.dimension!,
|
|
180
|
+
filterType,
|
|
181
|
+
label: filterLabel,
|
|
182
|
+
filterName: filter.name ?? filter.dimension!,
|
|
183
|
+
defaultMatchType,
|
|
184
|
+
required: filter.required ?? false,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return specs;
|
|
189
|
+
}
|
|
190
|
+
// Legacy: use ##(filters) + #(filter) annotation approach
|
|
105
191
|
if (!filterConfig || sourceInfoMap.size === 0 || !modelPath) return [];
|
|
106
192
|
return extractDimensionSpecs(
|
|
107
193
|
sourceInfoMap,
|
|
108
194
|
filterConfig.filters,
|
|
109
195
|
modelPath,
|
|
110
196
|
);
|
|
111
|
-
}, [
|
|
197
|
+
}, [
|
|
198
|
+
useServerFilters,
|
|
199
|
+
serverFilters,
|
|
200
|
+
filterConfig,
|
|
201
|
+
sourceInfoMap,
|
|
202
|
+
modelPath,
|
|
203
|
+
]);
|
|
112
204
|
|
|
113
205
|
// Initialize dimension filters hook
|
|
114
206
|
const { filterStates, updateFilter, getActiveFilters } = useDimensionFilters(
|
|
@@ -124,9 +216,8 @@ export default function Notebook({
|
|
|
124
216
|
[filterStates, getActiveFilters],
|
|
125
217
|
);
|
|
126
218
|
|
|
127
|
-
// Create a map of dimension key -> source name for quick lookup
|
|
128
|
-
|
|
129
|
-
const dimensionToSourceMap = useMemo(() => {
|
|
219
|
+
// Create a map of dimension key -> source name for quick lookup (used by filter UI)
|
|
220
|
+
const _dimensionToSourceMap = useMemo(() => {
|
|
130
221
|
const map = new Map<string, string>();
|
|
131
222
|
for (const spec of dimensionSpecs) {
|
|
132
223
|
const key = getDimensionKey(spec);
|
|
@@ -135,28 +226,55 @@ export default function Notebook({
|
|
|
135
226
|
return map;
|
|
136
227
|
}, [dimensionSpecs]);
|
|
137
228
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
return map;
|
|
145
|
-
}, [sourceInfoMap]);
|
|
146
|
-
|
|
147
|
-
// Fetch filter range data when we have dimension specs
|
|
148
|
-
// The hook now handles multiple source/model combos internally
|
|
229
|
+
// Fetch filter range data when we have dimension specs.
|
|
230
|
+
// Do NOT pass activeFilters here — the index query should return all
|
|
231
|
+
// possible values for each dimension, not just those matching the
|
|
232
|
+
// current selection. Otherwise selecting "FORD" would hide every other
|
|
233
|
+
// manufacturer from the dropdown.
|
|
149
234
|
const { data: filterValuesData } = useDimensionalFilterRangeData({
|
|
150
235
|
project: projectName,
|
|
151
236
|
package: packageName,
|
|
152
237
|
dimensionSpecs,
|
|
153
238
|
versionId,
|
|
154
239
|
enabled: dimensionSpecs.length > 0,
|
|
155
|
-
activeFilters,
|
|
156
240
|
});
|
|
157
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Convert active FilterSelections into a flat { filterName: value } map
|
|
244
|
+
* suitable for the server's filter_params parameter.
|
|
245
|
+
* Uses filterName from the selection (propagated from the spec) as the
|
|
246
|
+
* API param key, falling back to dimensionName.
|
|
247
|
+
*/
|
|
248
|
+
const buildFilterParams = useCallback(
|
|
249
|
+
(filtersToApply: FilterSelection[]): string | undefined => {
|
|
250
|
+
if (filtersToApply.length === 0) return undefined;
|
|
251
|
+
|
|
252
|
+
const toParamString = (v: unknown): string => {
|
|
253
|
+
if (v instanceof Date) {
|
|
254
|
+
return v.toISOString().slice(0, 10);
|
|
255
|
+
}
|
|
256
|
+
return String(v);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const params: { [key: string]: string | string[] } = {};
|
|
260
|
+
for (const f of filtersToApply) {
|
|
261
|
+
const paramName = f.filterName ?? f.dimensionName;
|
|
262
|
+
const val = f.value;
|
|
263
|
+
if (Array.isArray(val)) {
|
|
264
|
+
params[paramName] = val.map(toParamString);
|
|
265
|
+
} else if (val !== undefined && val !== null) {
|
|
266
|
+
params[paramName] = toParamString(val);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return Object.keys(params).length > 0
|
|
270
|
+
? JSON.stringify(params)
|
|
271
|
+
: undefined;
|
|
272
|
+
},
|
|
273
|
+
[],
|
|
274
|
+
);
|
|
275
|
+
|
|
158
276
|
// Unified cell execution function
|
|
159
|
-
// Executes all notebook cells,
|
|
277
|
+
// Executes all notebook cells, passing server-side filter params when available
|
|
160
278
|
// Runs up to 4 requests in parallel for better performance
|
|
161
279
|
const executeCells = useCallback(
|
|
162
280
|
async (filtersToApply: FilterSelection[] = []) => {
|
|
@@ -176,6 +294,10 @@ export default function Notebook({
|
|
|
176
294
|
setIsExecuting(true);
|
|
177
295
|
setExecutionError(null);
|
|
178
296
|
|
|
297
|
+
const filterParams = useServerFilters
|
|
298
|
+
? buildFilterParams(filtersToApply)
|
|
299
|
+
: undefined;
|
|
300
|
+
|
|
179
301
|
try {
|
|
180
302
|
// Build execution tasks for code cells
|
|
181
303
|
const executionTasks: Array<() => Promise<void>> = [];
|
|
@@ -186,95 +308,30 @@ export default function Notebook({
|
|
|
186
308
|
// Markdown cells don't need execution
|
|
187
309
|
if (rawCell.type === "markdown") continue;
|
|
188
310
|
|
|
189
|
-
// Execute code cells
|
|
190
|
-
const cellText = rawCell.text || "";
|
|
191
|
-
const hasQuery =
|
|
192
|
-
cellText.includes("run:") ||
|
|
193
|
-
cellText.includes("->") ||
|
|
194
|
-
/^\s*(run|query)\s*:/m.test(cellText);
|
|
195
|
-
|
|
196
311
|
// Capture cell index for closure
|
|
197
312
|
const cellIndex = i;
|
|
198
313
|
|
|
199
314
|
const executeCell = async () => {
|
|
200
315
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
sourceJoinsMap.get(querySourceName)) ||
|
|
217
|
-
new Set<string>();
|
|
218
|
-
|
|
219
|
-
// Filter to only include those matching this query's source or joined sources
|
|
220
|
-
// FilterSelection now includes source, so we can check directly
|
|
221
|
-
const filtersForSource = querySourceName
|
|
222
|
-
? filtersToApply.filter((filter) => {
|
|
223
|
-
return (
|
|
224
|
-
filter.source === querySourceName ||
|
|
225
|
-
joinedSources.has(filter.source)
|
|
226
|
-
);
|
|
227
|
-
})
|
|
228
|
-
: [];
|
|
229
|
-
|
|
230
|
-
if (filtersForSource.length > 0) {
|
|
231
|
-
const filterClause = generateFilterClause(
|
|
232
|
-
filtersForSource,
|
|
233
|
-
dimensionToSourceMap,
|
|
234
|
-
querySourceName,
|
|
235
|
-
);
|
|
236
|
-
if (filterClause) {
|
|
237
|
-
queryToExecute = injectWhereClause(
|
|
238
|
-
cellText,
|
|
239
|
-
filterClause,
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Execute using models API
|
|
246
|
-
const response =
|
|
247
|
-
await apiClients.models.executeQueryModel(
|
|
248
|
-
projectName,
|
|
249
|
-
packageName,
|
|
250
|
-
modelPath,
|
|
251
|
-
{
|
|
252
|
-
query: queryToExecute,
|
|
253
|
-
versionId,
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
result = response.data.result;
|
|
257
|
-
} else {
|
|
258
|
-
// Non-query code cell (or no filters applied) - use notebook cell execution API
|
|
259
|
-
const response =
|
|
260
|
-
await apiClients.notebooks.executeNotebookCell(
|
|
261
|
-
projectName,
|
|
262
|
-
packageName,
|
|
263
|
-
notebookPath,
|
|
264
|
-
cellIndex,
|
|
265
|
-
versionId,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const executedCell = response.data;
|
|
269
|
-
result = executedCell.result;
|
|
270
|
-
newSources =
|
|
271
|
-
rawCell.newSources || executedCell.newSources;
|
|
272
|
-
}
|
|
316
|
+
// Use notebook cell execution API with optional filter_params
|
|
317
|
+
const response =
|
|
318
|
+
await apiClients.notebooks.executeNotebookCell(
|
|
319
|
+
projectName,
|
|
320
|
+
packageName,
|
|
321
|
+
notebookPath,
|
|
322
|
+
cellIndex,
|
|
323
|
+
versionId,
|
|
324
|
+
filterParams,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const executedCell = response.data;
|
|
328
|
+
const result = executedCell.result;
|
|
329
|
+
const newSources =
|
|
330
|
+
rawCell.newSources || executedCell.newSources;
|
|
273
331
|
|
|
274
332
|
// Update state incrementally
|
|
275
333
|
setEnhancedCells((prev) => {
|
|
276
334
|
const next = [...prev];
|
|
277
|
-
// Ensure we have a cell to update (in case state was reset externally, though unlikely)
|
|
278
335
|
if (!next[cellIndex]) {
|
|
279
336
|
next[cellIndex] = { ...rawCell };
|
|
280
337
|
}
|
|
@@ -290,7 +347,6 @@ export default function Notebook({
|
|
|
290
347
|
`Error executing cell ${cellIndex}:`,
|
|
291
348
|
cellError,
|
|
292
349
|
);
|
|
293
|
-
// Don't update result on error, leave as is (undefined)
|
|
294
350
|
}
|
|
295
351
|
};
|
|
296
352
|
|
|
@@ -323,14 +379,12 @@ export default function Notebook({
|
|
|
323
379
|
[
|
|
324
380
|
isSuccess,
|
|
325
381
|
notebook,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
sourceJoinsMap,
|
|
382
|
+
useServerFilters,
|
|
383
|
+
buildFilterParams,
|
|
329
384
|
projectName,
|
|
330
385
|
packageName,
|
|
331
386
|
notebookPath,
|
|
332
387
|
versionId,
|
|
333
|
-
apiClients.models,
|
|
334
388
|
apiClients.notebooks,
|
|
335
389
|
],
|
|
336
390
|
);
|
|
@@ -21,6 +21,14 @@ import {
|
|
|
21
21
|
import { Configuration } from "../client/configuration";
|
|
22
22
|
import { globalQueryClient } from "../utils/queryClient";
|
|
23
23
|
|
|
24
|
+
// There's a bug in the OpenAPI generator that causes it to ignore baseURL in
|
|
25
|
+
// the axios request if axios.defaults.baseURL is not set. The per-instance
|
|
26
|
+
// baseURL on the custom axios instance below is the real value we use; this
|
|
27
|
+
// sentinel exists only to satisfy the generated client's code path.
|
|
28
|
+
if (!axios.defaults.baseURL) {
|
|
29
|
+
axios.defaults.baseURL = "IfYouAreSeeingThis_baseURL_IsNotSet";
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export interface ServerContextValue {
|
|
25
33
|
server: string;
|
|
26
34
|
getAccessToken?: () => Promise<string>;
|
|
@@ -115,9 +115,9 @@ export function DimensionFilter({
|
|
|
115
115
|
onChange,
|
|
116
116
|
retrievalFn,
|
|
117
117
|
}: DimensionFilterProps) {
|
|
118
|
-
// Default to "Between" for date filters, otherwise use first available match type
|
|
119
118
|
const getDefaultMatchType = () => {
|
|
120
119
|
if (selection?.matchType) return selection.matchType;
|
|
120
|
+
if (spec.defaultMatchType) return spec.defaultMatchType;
|
|
121
121
|
if (spec.filterType === "DateMinMax") return "Between";
|
|
122
122
|
return getAvailableMatchTypes(spec.filterType)[0] || "Equals";
|
|
123
123
|
};
|
|
@@ -203,11 +203,11 @@ export function DimensionFilter({
|
|
|
203
203
|
// Clear internal state when selection is cleared externally
|
|
204
204
|
setValue1("");
|
|
205
205
|
setValue2("");
|
|
206
|
-
// Reset to default match type (Between for dates, first available for others)
|
|
207
206
|
setMatchType(
|
|
208
|
-
spec.
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
spec.defaultMatchType ??
|
|
208
|
+
(spec.filterType === "DateMinMax"
|
|
209
|
+
? "Between"
|
|
210
|
+
: getAvailableMatchTypes(spec.filterType)[0] || "Equals"),
|
|
211
211
|
);
|
|
212
212
|
} else if (selection) {
|
|
213
213
|
// Update internal state when selection changes externally
|
|
@@ -216,7 +216,7 @@ export function DimensionFilter({
|
|
|
216
216
|
setValue1(selection.value ?? "");
|
|
217
217
|
setValue2(selection.value2 ?? "");
|
|
218
218
|
}
|
|
219
|
-
}, [selection, spec.filterType]);
|
|
219
|
+
}, [selection, spec.filterType, spec.defaultMatchType]);
|
|
220
220
|
|
|
221
221
|
const availableMatchTypes = getAvailableMatchTypes(spec.filterType);
|
|
222
222
|
const isDate = isDateFilter(spec.filterType);
|
|
@@ -266,6 +266,7 @@ export function DimensionFilter({
|
|
|
266
266
|
onChange({
|
|
267
267
|
dimensionName: spec.dimensionName,
|
|
268
268
|
source: spec.source,
|
|
269
|
+
filterName: spec.filterName,
|
|
269
270
|
matchType: newMatchType,
|
|
270
271
|
value: value1,
|
|
271
272
|
...(requiresTwoValues(newMatchType) && value2 && { value2 }),
|
|
@@ -296,6 +297,7 @@ export function DimensionFilter({
|
|
|
296
297
|
onChange({
|
|
297
298
|
dimensionName: spec.dimensionName,
|
|
298
299
|
source: spec.source,
|
|
300
|
+
filterName: spec.filterName,
|
|
299
301
|
matchType,
|
|
300
302
|
value: newValue1,
|
|
301
303
|
...(needsTwoValues && newValue2 && { value2: newValue2 }),
|
|
@@ -326,9 +328,8 @@ export function DimensionFilter({
|
|
|
326
328
|
"& .MuiAutocomplete-root .MuiInputBase-root": {
|
|
327
329
|
padding: "5px 10px",
|
|
328
330
|
minHeight: "29.5px",
|
|
329
|
-
maxHeight: "29.5px",
|
|
330
331
|
boxSizing: "border-box",
|
|
331
|
-
|
|
332
|
+
flexWrap: "wrap",
|
|
332
333
|
},
|
|
333
334
|
"& .MuiChip-root": { height: "16px", margin: "0 2px 0 0" },
|
|
334
335
|
"& .MuiChip-label": { fontSize: "0.7rem", padding: "0 6px" },
|
|
@@ -354,38 +355,62 @@ export function DimensionFilter({
|
|
|
354
355
|
"& .MuiSvgIcon-root": { fontSize: "1.25rem" },
|
|
355
356
|
}}
|
|
356
357
|
>
|
|
357
|
-
{/* Dimension Label/Name */}
|
|
358
|
-
<Box
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
minHeight: "auto",
|
|
375
|
-
padding: "4px 10px",
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
},
|
|
358
|
+
{/* Dimension Label/Name — red when required and not yet set */}
|
|
359
|
+
<Box
|
|
360
|
+
sx={{
|
|
361
|
+
fontWeight: 600,
|
|
362
|
+
color:
|
|
363
|
+
spec.required && !selection ? "error.main" : "text.primary",
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
{spec.label ?? spec.dimensionName}
|
|
367
|
+
{spec.required && (
|
|
368
|
+
<Box
|
|
369
|
+
component="span"
|
|
370
|
+
sx={{
|
|
371
|
+
color: !selection ? "error.main" : "text.secondary",
|
|
372
|
+
fontWeight: 400,
|
|
373
|
+
fontSize: "0.7rem",
|
|
374
|
+
ml: 0.5,
|
|
379
375
|
}}
|
|
380
376
|
>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
377
|
+
(required)
|
|
378
|
+
</Box>
|
|
379
|
+
)}
|
|
380
|
+
</Box>
|
|
381
|
+
|
|
382
|
+
{/* Match Type Selector — hidden when the filter annotation already
|
|
383
|
+
determines the match type (defaultMatchType is set). Only shown
|
|
384
|
+
for legacy/untyped filters where the user needs to choose. */}
|
|
385
|
+
{spec.filterType !== "Boolean" &&
|
|
386
|
+
!spec.defaultMatchType &&
|
|
387
|
+
availableMatchTypes.length > 1 && (
|
|
388
|
+
<FormControl size="small" fullWidth>
|
|
389
|
+
<InputLabel>Match Type</InputLabel>
|
|
390
|
+
<Select
|
|
391
|
+
value={matchType}
|
|
392
|
+
label="Match Type"
|
|
393
|
+
onChange={handleMatchTypeChange}
|
|
394
|
+
MenuProps={{
|
|
395
|
+
PaperProps: {
|
|
396
|
+
sx: {
|
|
397
|
+
"& .MuiMenuItem-root": {
|
|
398
|
+
fontSize: "0.75rem",
|
|
399
|
+
minHeight: "auto",
|
|
400
|
+
padding: "4px 10px",
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
}}
|
|
405
|
+
>
|
|
406
|
+
{availableMatchTypes.map((type) => (
|
|
407
|
+
<MenuItem key={type} value={type}>
|
|
408
|
+
{type}
|
|
409
|
+
</MenuItem>
|
|
410
|
+
))}
|
|
411
|
+
</Select>
|
|
412
|
+
</FormControl>
|
|
413
|
+
)}
|
|
389
414
|
|
|
390
415
|
{/* Value Input - varies by filter type */}
|
|
391
416
|
{spec.filterType === "Star" && matchType === "Equals" && (
|
|
@@ -455,7 +480,11 @@ export function DimensionFilter({
|
|
|
455
480
|
size="small"
|
|
456
481
|
label="Search Text"
|
|
457
482
|
value={value1}
|
|
458
|
-
onChange={(e) =>
|
|
483
|
+
onChange={(e) => setValue1(e.target.value)}
|
|
484
|
+
onBlur={() => handleValueChange(value1)}
|
|
485
|
+
onKeyDown={(e) => {
|
|
486
|
+
if (e.key === "Enter") handleValueChange(value1);
|
|
487
|
+
}}
|
|
459
488
|
placeholder="Enter text to search..."
|
|
460
489
|
fullWidth
|
|
461
490
|
/>
|