@platforma-sdk/model 1.25.0 → 1.26.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/src/render/api.ts CHANGED
@@ -20,12 +20,19 @@ import type {
20
20
  AxisFilter,
21
21
  PValue,
22
22
  SUniversalPColumnId,
23
- AnchoredPColumnSelector } from '@milaboratories/pl-model-common';
23
+ AnyFunction,
24
+ DataInfo,
25
+ BinaryPartitionedDataInfoEntries,
26
+ JsonPartitionedDataInfoEntries,
27
+ PObjectId } from '@milaboratories/pl-model-common';
24
28
  import {
25
29
  AnchoredIdDeriver,
26
30
  getAxisId,
27
- AnyFunction,
31
+ isDataInfo,
32
+ mapDataInfo,
28
33
  resolveAnchors,
34
+ canonicalizeAxisId,
35
+ entriesToDataInfo,
29
36
  } from '@milaboratories/pl-model-common';
30
37
  import {
31
38
  ensurePColumn,
@@ -42,14 +49,15 @@ import type { Optional } from 'utility-types';
42
49
  import { getCfgRenderCtx } from '../internal';
43
50
  import { TreeNodeAccessor, ifDef } from './accessor';
44
51
  import type { FutureRef } from './future';
45
- import type { GlobalCfgRenderCtx } from './internal';
52
+ import type { AccessorHandle, GlobalCfgRenderCtx } from './internal';
46
53
  import { MainAccessorName, StagingAccessorName } from './internal';
47
54
  import type { LabelDerivationOps } from './util/label';
48
55
  import { deriveLabels } from './util/label';
49
56
  import type { APColumnSelectorWithSplit } from './split_selectors';
50
- import { getUniquePartitionKeys } from './util/pcolumn_data';
57
+ import { getUniquePartitionKeys, parsePColumnData } from './util/pcolumn_data';
51
58
  import type { TraceEntry } from './util/label';
52
- import { canonicalizeAxisId } from '@milaboratories/pl-model-common';
59
+ import { filterDataInfoEntries } from './util/axis_filtering';
60
+
53
61
  /**
54
62
  * Helper function to match domain objects
55
63
  * @param query Optional domain to match against
@@ -65,7 +73,74 @@ function matchDomain(query?: Record<string, string>, target?: Record<string, str
65
73
  return true;
66
74
  }
67
75
 
68
- export type UniversalOption = { label: string; value: SUniversalPColumnId };
76
+ export type UniversalColumnOption = { label: string; value: SUniversalPColumnId };
77
+
78
+ /**
79
+ * Transforms PColumn data into the internal representation expected by the platform
80
+ * @param data Data from a PColumn to transform
81
+ * @returns Transformed data compatible with platform API
82
+ */
83
+ function transformPColumnData(data: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>):
84
+ PColumn<PColumnValues | AccessorHandle | DataInfo<AccessorHandle>> {
85
+ return mapPObjectData(data, (d) => {
86
+ if (d instanceof TreeNodeAccessor) {
87
+ return d.handle;
88
+ } else if (isDataInfo(d)) {
89
+ return mapDataInfo(d, (accessor) => accessor.handle);
90
+ } else {
91
+ return d;
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Describes a single filter applied due to a split axis.
98
+ */
99
+ export type AxisFilterInfo = {
100
+ axisIdx: number;
101
+ axisId: AxisId;
102
+ value: PValue;
103
+ label: string;
104
+ };
105
+
106
+ /**
107
+ * Represents a column specification with potential split axis filtering information
108
+ * used in canonical options generation.
109
+ */
110
+ export type UniversalPColumnEntry = {
111
+ id: SUniversalPColumnId;
112
+ obj: PColumnSpec;
113
+ ref: PlRef;
114
+ axisFilters?: AxisFilterInfo[];
115
+ label: string;
116
+ };
117
+
118
+ /**
119
+ * Converts an array of SplitAxisFilter objects into an array of TraceEntry objects
120
+ * suitable for label generation.
121
+ */
122
+ function splitFiltersToTrace(splitFilters?: AxisFilterInfo[]): TraceEntry[] | undefined {
123
+ if (!splitFilters) return undefined;
124
+ return splitFilters.map((filter) => ({
125
+ type: `split:${canonicalizeAxisId(filter.axisId)}`,
126
+ label: filter.label,
127
+ importance: 1_000_000, // High importance for split filters in labels
128
+ }));
129
+ }
130
+
131
+ /**
132
+ * Converts an array of SplitAxisFilter objects into an array of AxisFilter tuples
133
+ * suitable for deriving anchored IDs.
134
+ */
135
+ function splitFiltersToAxisFilter(splitFilters?: AxisFilterInfo[]): AxisFilter[] | undefined {
136
+ if (!splitFilters) return undefined;
137
+ return splitFilters.map((filter) => [filter.axisIdx, filter.value]);
138
+ }
139
+
140
+ type UniversalPColumnOpts = {
141
+ labelOps?: LabelDerivationOps;
142
+ dontWaitAllData?: boolean;
143
+ };
69
144
 
70
145
  export class ResultPool {
71
146
  private readonly ctx: GlobalCfgRenderCtx = getCfgRenderCtx();
@@ -77,10 +152,6 @@ export class ResultPool {
77
152
  return this.ctx.calculateOptions(predicate);
78
153
  }
79
154
 
80
- // @TODO: unused, what is this for?
81
- private defaultLabelFn = (spec: PObjectSpec, _ref: PlRef) =>
82
- spec.annotations?.['pl7.app/label'] ?? `Unlabelled`;
83
-
84
155
  public getOptions(
85
156
  predicateOrSelector: ((spec: PObjectSpec) => boolean) | PColumnSelector | PColumnSelector[],
86
157
  label?: ((spec: PObjectSpec, ref: PlRef) => string) | LabelDerivationOps,
@@ -102,58 +173,14 @@ export class ResultPool {
102
173
  }
103
174
 
104
175
  /**
105
- * Calculates anchored identifier options for columns matching a given predicate and returns their
106
- * canonicalized representations.
107
- *
108
- * This function filters column specifications from the result pool that match the provided predicate,
109
- * creates a standardized AnchorCtx from the provided anchors, and generates a list of label-value
110
- * pairs for UI components (like dropdowns).
111
- *
112
- * @param anchorsOrCtx - Either:
113
- * - An existing AnchorCtx instance
114
- * - A record mapping anchor IDs to PColumnSpec objects
115
- * - A record mapping anchor IDs to PlRef objects (which will be resolved to PColumnSpec)
116
- * @param predicateOrSelector - Either:
117
- * - A predicate function that takes a PColumnSpec and returns a boolean.
118
- * Only specs that return true will be included.
119
- * - An APColumnSelector object for declarative filtering, which will be
120
- * resolved against the provided anchors and matched using matchPColumn.
121
- * - An array of APColumnSelector objects - columns matching ANY selector
122
- * in the array will be included (OR operation).
123
- * @param labelOps - Optional configuration for label generation:
124
- * - includeNativeLabel: Whether to include native column labels
125
- * - separator: String to use between label parts (defaults to " / ")
126
- * - addLabelAsSuffix: Whether to add labels as suffix instead of prefix
127
- * @returns An array of objects with `label` (display text) and `value` (anchored ID string) properties,
128
- * or undefined if any PlRef resolution fails.
176
+ * Internal implementation that generates UniversalPColumnEntry objects from the provided
177
+ * anchors and selectors.
129
178
  */
130
- // Overload for AnchorCtx - guaranteed to never return undefined
131
- getCanonicalOptions(
132
- anchorsOrCtx: AnchoredIdDeriver,
133
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | AnchoredPColumnSelector[],
134
- labelOps?: LabelDerivationOps,
135
- ): { label: string; value: SUniversalPColumnId }[];
136
-
137
- // Overload for Record<string, PColumnSpec> - guaranteed to never return undefined
138
- getCanonicalOptions(
139
- anchorsOrCtx: Record<string, PColumnSpec>,
140
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | AnchoredPColumnSelector[],
141
- labelOps?: LabelDerivationOps,
142
- ): { label: string; value: SUniversalPColumnId }[];
143
-
144
- // Overload for Record<string, PColumnSpec | PlRef> - may return undefined if PlRef resolution fails
145
- getCanonicalOptions(
146
- anchorsOrCtx: Record<string, PColumnSpec | PlRef>,
147
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | AnchoredPColumnSelector[],
148
- labelOps?: LabelDerivationOps,
149
- ): { label: string; value: SUniversalPColumnId }[] | undefined;
150
-
151
- // Implementation
152
- getCanonicalOptions(
179
+ public getUniversalPColumnEntries(
153
180
  anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
154
- predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | AnchoredPColumnSelector[],
155
- labelOps?: LabelDerivationOps,
156
- ): { label: string; value: SUniversalPColumnId }[] | undefined {
181
+ predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
182
+ opts?: UniversalPColumnOpts,
183
+ ): UniversalPColumnEntry[] | undefined {
157
184
  // Handle PlRef objects by resolving them to PColumnSpec
158
185
  const resolvedAnchors: Record<string, PColumnSpec> = {};
159
186
 
@@ -171,113 +198,272 @@ export class ResultPool {
171
198
  }
172
199
  }
173
200
 
174
- const predicate = typeof predicateOrSelectors === 'function'
175
- ? predicateOrSelectors
176
- : selectorsToPredicate(Array.isArray(predicateOrSelectors)
177
- ? predicateOrSelectors.map((selector) => resolveAnchors(resolvedAnchors, selector))
178
- : resolveAnchors(resolvedAnchors, predicateOrSelectors),
179
- );
180
-
181
- const filtered = this.getSpecs().entries.filter(({ obj: spec }) => {
182
- if (!isPColumnSpec(spec)) return false;
183
- return predicate(spec);
184
- });
185
-
186
- if (filtered.length === 0)
187
- return [];
201
+ const selectorsArray = typeof predicateOrSelectors === 'function'
202
+ ? [predicateOrSelectors]
203
+ : Array.isArray(predicateOrSelectors)
204
+ ? predicateOrSelectors
205
+ : [predicateOrSelectors];
188
206
 
189
207
  const anchorIdDeriver = anchorsOrCtx instanceof AnchoredIdDeriver
190
208
  ? anchorsOrCtx
191
209
  : new AnchoredIdDeriver(resolvedAnchors);
192
210
 
193
- const splitAxisIdxs = typeof predicateOrSelectors === 'object'
194
- && !Array.isArray(predicateOrSelectors)
195
- && 'axes' in predicateOrSelectors
196
- && predicateOrSelectors.axes !== undefined
197
- && predicateOrSelectors.partialAxesMatch === undefined
198
- ? predicateOrSelectors.axes
199
- .map((axis, index) => ('split' in axis && axis.split === true) ? index : -1)
200
- .filter((index) => index !== -1)
201
- : [];
202
- splitAxisIdxs.sort((a, b) => a - b);
211
+ const result: Omit<UniversalPColumnEntry, 'id' | 'label'>[] = [];
203
212
 
204
- if (splitAxisIdxs.length > 0) {
205
- const result: { obj: PColumnSpec; ref: PlRef; filteringTrace: TraceEntry[]; filters: AxisFilter[] }[] = [];
213
+ // Process each selector individually
214
+ for (const selector of selectorsArray) {
215
+ // Create predicate for this specific selector
216
+ const predicate = typeof selector === 'function'
217
+ ? selector
218
+ : selectorsToPredicate(resolveAnchors(resolvedAnchors, selector));
206
219
 
207
- const maxSplitIdx = splitAxisIdxs[splitAxisIdxs.length - 1]; // Last one is max since they're sorted
220
+ // Filter specs based on this specific predicate
221
+ const filtered = this.getSpecs().entries.filter(({ obj: spec }) => {
222
+ if (!isPColumnSpec(spec)) return false;
223
+ return predicate(spec);
224
+ });
208
225
 
209
- for (const { ref, obj: spec } of filtered) {
210
- if (!isPColumnSpec(spec)) continue;
211
-
212
- const columnData = this.getDataByRef(ref);
213
- if (!columnData || !isPColumn(columnData)) continue;
226
+ if (filtered.length === 0)
227
+ continue;
214
228
 
215
- const uniqueKeys = getUniquePartitionKeys(columnData.data);
216
- if (!uniqueKeys) continue; // data not fully initialized yet
229
+ // Check if this selector has any split axes
230
+ const splitAxisIdxs = typeof selector === 'object'
231
+ && 'axes' in selector
232
+ && selector.axes !== undefined
233
+ && selector.partialAxesMatch === undefined
234
+ ? selector.axes
235
+ .map((axis, index) => ('split' in axis && axis.split === true) ? index : -1)
236
+ .filter((index) => index !== -1)
237
+ : [];
238
+ splitAxisIdxs.sort((a, b) => a - b);
239
+
240
+ if (splitAxisIdxs.length > 0) { // Handle split axes
241
+ const maxSplitIdx = splitAxisIdxs[splitAxisIdxs.length - 1]; // Last one is max since they're sorted
242
+
243
+ for (const { ref, obj: spec } of filtered) {
244
+ if (!isPColumnSpec(spec)) throw new Error(`Assertion failed: expected PColumnSpec, got ${spec.kind}`);
245
+
246
+ const columnData = this.getDataByRef(ref);
247
+ if (!columnData) {
248
+ if (opts?.dontWaitAllData) continue;
249
+ return undefined;
250
+ }
251
+ if (!isPColumn(columnData)) throw new Error(`Assertion failed: expected PColumn, got ${columnData.spec.kind}`);
217
252
 
218
- if (maxSplitIdx >= uniqueKeys.length)
219
- throw new Error(`Not enough partition keys for the requested split axes in column ${spec.name}`);
253
+ const uniqueKeys = getUniquePartitionKeys(columnData.data);
254
+ if (!uniqueKeys) {
255
+ if (opts?.dontWaitAllData) continue;
256
+ return undefined;
257
+ }
220
258
 
221
- const axesLabels: (Record<string | number, string> | undefined)[] = splitAxisIdxs
222
- .map((idx) => this.findLabels(getAxisId(spec.axesSpec[idx])));
259
+ if (maxSplitIdx >= uniqueKeys.length)
260
+ throw new Error(`Not enough partition keys for the requested split axes in column ${spec.name}`);
223
261
 
224
- const keyCombinations: (string | number)[][] = [];
225
- const generateCombinations = (currentCombo: (string | number)[], sAxisIdx: number) => {
226
- if (sAxisIdx >= splitAxisIdxs.length) {
227
- keyCombinations.push([...currentCombo]);
228
- return;
229
- }
230
- const axisIdx = splitAxisIdxs[sAxisIdx];
231
- const axisValues = uniqueKeys[axisIdx];
232
- for (const val of axisValues) {
233
- currentCombo.push(val);
234
- generateCombinations(currentCombo, sAxisIdx + 1);
235
- currentCombo.pop();
236
- }
237
- };
238
- generateCombinations([], 0);
262
+ // Pre-fetch labels for all involved split axes
263
+ const axesLabels: (Record<string | number, string> | undefined)[] = splitAxisIdxs
264
+ .map((idx) => this.findLabels(getAxisId(spec.axesSpec[idx])));
239
265
 
240
- for (const keyCombo of keyCombinations) {
241
- const filteringTrace: TraceEntry[] = keyCombo.map((value, sAxisIdx) => {
266
+ const keyCombinations: (string | number)[][] = [];
267
+ const generateCombinations = (currentCombo: (string | number)[], sAxisIdx: number) => {
268
+ if (sAxisIdx >= splitAxisIdxs.length) {
269
+ keyCombinations.push([...currentCombo]);
270
+ return;
271
+ }
242
272
  const axisIdx = splitAxisIdxs[sAxisIdx];
243
- const canonicalAxisId = canonicalizeAxisId(getAxisId(spec.axesSpec[axisIdx]));
244
- const axisLabels = axesLabels[sAxisIdx];
245
- const label = axisLabels?.[value] ?? String(value);
246
- return {
247
- type: `split:${canonicalAxisId}`,
248
- label,
249
- importance: 1_000_000,
250
- };
251
- });
252
-
253
- const filters: AxisFilter[] = splitAxisIdxs.map((idx, i) => [idx, keyCombo[i] as PValue]);
273
+ const axisValues = uniqueKeys[axisIdx];
274
+ for (const val of axisValues) {
275
+ currentCombo.push(val);
276
+ generateCombinations(currentCombo, sAxisIdx + 1);
277
+ currentCombo.pop();
278
+ }
279
+ };
280
+ generateCombinations([], 0);
281
+
282
+ // Generate entries for each key combination
283
+ for (const keyCombo of keyCombinations) {
284
+ const splitFilters: AxisFilterInfo[] = keyCombo.map((value, sAxisIdx) => {
285
+ const axisIdx = splitAxisIdxs[sAxisIdx];
286
+ const axisId = getAxisId(spec.axesSpec[axisIdx]);
287
+ const axisLabelMap = axesLabels[sAxisIdx];
288
+ const label = axisLabelMap?.[value] ?? String(value);
289
+ return { axisIdx, axisId, value: value as PValue, label };
290
+ });
291
+
292
+ result.push({
293
+ obj: spec,
294
+ ref,
295
+ axisFilters: splitFilters,
296
+ });
297
+ }
298
+ }
299
+ } else {
300
+ // No split axes, simply add each filtered item without filters
301
+ for (const { ref, obj: spec } of filtered) {
302
+ if (!isPColumnSpec(spec)) continue;
254
303
  result.push({
255
304
  obj: spec,
256
305
  ref,
257
- filteringTrace: filteringTrace,
258
- filters,
306
+ // No splitFilters needed here
259
307
  });
260
308
  }
261
309
  }
310
+ }
262
311
 
263
- const labelResults = deriveLabels(
264
- result,
265
- (o) => ({
266
- spec: o.obj,
267
- suffixTrace: o.filteringTrace,
268
- }),
269
- labelOps ?? {},
270
- );
271
-
272
- return labelResults.map((item) => ({
273
- value: anchorIdDeriver.deriveS(item.value.obj as PColumnSpec, item.value.filters),
274
- label: item.label,
275
- }));
312
+ if (result.length === 0)
313
+ return [];
314
+
315
+ const labelResults = deriveLabels(
316
+ result,
317
+ (o) => ({
318
+ spec: o.obj,
319
+ suffixTrace: splitFiltersToTrace(o.axisFilters), // Use helper function
320
+ }),
321
+ opts?.labelOps ?? {},
322
+ );
323
+
324
+ return labelResults.map((item) => ({
325
+ id: anchorIdDeriver.deriveS(
326
+ item.value.obj,
327
+ splitFiltersToAxisFilter(item.value.axisFilters), // Use helper function
328
+ ),
329
+ obj: item.value.obj,
330
+ ref: item.value.ref,
331
+ axisFilters: item.value.axisFilters,
332
+ label: item.label,
333
+ }));
334
+ }
335
+
336
+ /**
337
+ * Returns columns that match the provided anchors and selectors. It applies axis filters and label derivation.
338
+ *
339
+ * @param anchorsOrCtx - Anchor context for column selection (same as in getCanonicalOptions)
340
+ * @param predicateOrSelectors - Predicate or selectors for filtering columns (same as in getCanonicalOptions)
341
+ * @param opts - Optional configuration for label generation and data waiting
342
+ * @returns A PFrameHandle for the created PFrame, or undefined if any required data is missing
343
+ */
344
+ public getAnchoredPColumns(
345
+ anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
346
+ predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
347
+ opts?: UniversalPColumnOpts,
348
+ ): PColumn<DataInfo<TreeNodeAccessor>>[] | undefined {
349
+ // Ensure includeNativeLabel is true in the labelOps
350
+ const enhancedOpts: UniversalPColumnOpts = {
351
+ ...opts,
352
+ labelOps: {
353
+ includeNativeLabel: true,
354
+ ...(opts?.labelOps || {}),
355
+ },
356
+ };
357
+
358
+ const entries = this.getUniversalPColumnEntries(
359
+ anchorsOrCtx,
360
+ predicateOrSelectors,
361
+ enhancedOpts,
362
+ );
363
+
364
+ if (!entries || entries.length === 0) return undefined;
365
+
366
+ const result: PColumn<DataInfo<TreeNodeAccessor>>[] = [];
367
+
368
+ for (const entry of entries) {
369
+ const columnData = this.getPColumnByRef(entry.ref);
370
+ if (!columnData) return undefined;
371
+
372
+ const parsedData = parsePColumnData(columnData.data);
373
+ if (!parsedData) return undefined;
374
+
375
+ let filteredEntries: JsonPartitionedDataInfoEntries<TreeNodeAccessor> | BinaryPartitionedDataInfoEntries<TreeNodeAccessor> = parsedData;
376
+ let spec = { ...columnData.spec };
377
+
378
+ if (entry.axisFilters && entry.axisFilters.length > 0) {
379
+ const axisFiltersByIdx = entry.axisFilters.map((filter) => [
380
+ filter.axisIdx,
381
+ filter.value,
382
+ ] as [number, PValue]);
383
+
384
+ filteredEntries = filterDataInfoEntries(parsedData, axisFiltersByIdx);
385
+
386
+ const axisIndicesToRemove = [...entry.axisFilters]
387
+ .map((filter) => filter.axisIdx)
388
+ .sort((a, b) => b - a);
389
+
390
+ const newAxesSpec = [...spec.axesSpec];
391
+ for (const idx of axisIndicesToRemove) {
392
+ newAxesSpec.splice(idx, 1);
393
+ }
394
+
395
+ spec = { ...spec, axesSpec: newAxesSpec };
396
+ }
397
+
398
+ const dataInfo = entriesToDataInfo(filteredEntries);
399
+
400
+ if (spec.annotations) {
401
+ spec = {
402
+ ...spec,
403
+ annotations: {
404
+ ...spec.annotations,
405
+ 'pl7.app/label': entry.label,
406
+ },
407
+ };
408
+ } else {
409
+ spec = {
410
+ ...spec,
411
+ annotations: {
412
+ 'pl7.app/label': entry.label,
413
+ },
414
+ };
415
+ }
416
+
417
+ result.push({
418
+ id: entry.id as unknown as PObjectId,
419
+ spec,
420
+ data: dataInfo,
421
+ });
276
422
  }
277
423
 
278
- return deriveLabels(filtered, (o) => o.obj, labelOps ?? {}).map(({ value: { obj: spec }, label }) => ({
279
- value: anchorIdDeriver.deriveS(spec as PColumnSpec),
280
- label,
424
+ return result;
425
+ }
426
+
427
+ /**
428
+ * Calculates anchored identifier options for columns matching a given predicate and returns their
429
+ * canonicalized representations.
430
+ *
431
+ * This function filters column specifications from the result pool that match the provided predicate,
432
+ * creates a standardized AnchorCtx from the provided anchors, and generates a list of label-value
433
+ * pairs for UI components (like dropdowns).
434
+ *
435
+ * @param anchorsOrCtx - Either:
436
+ * - An existing AnchorCtx instance
437
+ * - A record mapping anchor IDs to PColumnSpec objects
438
+ * - A record mapping anchor IDs to PlRef objects (which will be resolved to PColumnSpec)
439
+ * @param predicateOrSelectors - Either:
440
+ * - A predicate function that takes a PColumnSpec and returns a boolean.
441
+ * Only specs that return true will be included.
442
+ * - An APColumnSelector object for declarative filtering, which will be
443
+ * resolved against the provided anchors and matched using matchPColumn.
444
+ * - An array of APColumnSelector objects - columns matching ANY selector
445
+ * in the array will be included (OR operation).
446
+ * @param opts - Optional configuration for label generation:
447
+ * - labelOps: Optional configuration for label generation:
448
+ * - includeNativeLabel: Whether to include native column labels
449
+ * - separator: String to use between label parts (defaults to " / ")
450
+ * - addLabelAsSuffix: Whether to add labels as suffix instead of prefix
451
+ * - dontWaitAllData: Whether to skip columns that don't have all data (if not set, will return undefined,
452
+ * if at least one column that requires splitting is missing data)
453
+ * @returns An array of objects with `label` (display text) and `value` (anchored ID string) properties,
454
+ * or undefined if any PlRef resolution fails.
455
+ */
456
+ getCanonicalOptions(
457
+ anchorsOrCtx: AnchoredIdDeriver | Record<string, PColumnSpec | PlRef>,
458
+ predicateOrSelectors: ((spec: PColumnSpec) => boolean) | APColumnSelectorWithSplit | APColumnSelectorWithSplit[],
459
+ opts?: UniversalPColumnOpts,
460
+ ): { label: string; value: SUniversalPColumnId }[] | undefined {
461
+ const entries = this.getUniversalPColumnEntries(anchorsOrCtx, predicateOrSelectors, opts);
462
+ if (!entries) return undefined;
463
+ // Generate final options using the entries from the helper method
464
+ return entries.map((item) => ({
465
+ value: item.id,
466
+ label: item.label,
281
467
  }));
282
468
  }
283
469
 
@@ -351,8 +537,11 @@ export class ResultPool {
351
537
  return this.getData().entries.find(
352
538
  (f) => f.ref.blockId === ref.blockId && f.ref.name === ref.name,
353
539
  )?.obj;
540
+ const data = this.ctx.getDataFromResultPoolByRef(ref.blockId, ref.name); // Keep original call
541
+ // Need to handle undefined case before mapping
542
+ if (!data) return undefined;
354
543
  return mapPObjectData(
355
- this.ctx.getDataFromResultPoolByRef(ref.blockId, ref.name),
544
+ data,
356
545
  (handle) => new TreeNodeAccessor(handle, [ref.blockId, ref.name]),
357
546
  );
358
547
  }
@@ -532,37 +721,39 @@ export class RenderCtx<Args, UiState> {
532
721
  return this.resultPool.findLabels(axis);
533
722
  }
534
723
 
535
- private verifyInlineColumnsSupport(columns: PColumn<TreeNodeAccessor | PColumnValues>[]) {
536
- const hasInlineColumns = columns.some((c) => !(c.data instanceof TreeNodeAccessor));
724
+ private verifyInlineAndExplicitColumnsSupport(columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[]) {
725
+ const hasInlineColumns = columns.some((c) => !(c.data instanceof TreeNodeAccessor) || isDataInfo(c.data)); // Updated check for DataInfo
537
726
  const inlineColumnsSupport = this.ctx.featureFlags?.inlineColumnsSupport === true;
538
- if (hasInlineColumns && !inlineColumnsSupport) throw Error(`inline columns not supported`);
727
+ if (hasInlineColumns && !inlineColumnsSupport) throw Error(`Inline or explicit columns not supported`); // Combined check
728
+
729
+ // Removed redundant explicitColumns check
539
730
  }
540
731
 
541
- public createPFrame(def: PFrameDef<TreeNodeAccessor | PColumnValues>): PFrameHandle {
542
- this.verifyInlineColumnsSupport(def);
732
+ public createPFrame(def: PFrameDef<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>): PFrameHandle {
733
+ this.verifyInlineAndExplicitColumnsSupport(def);
543
734
  return this.ctx.createPFrame(
544
- def.map((c) => mapPObjectData(c, (d) => (d instanceof TreeNodeAccessor ? d.handle : d))),
735
+ def.map((c) => transformPColumnData(c)),
545
736
  );
546
737
  }
547
738
 
548
- public createPTable(def: PTableDef<PColumn<TreeNodeAccessor | PColumnValues>>): PTableHandle;
739
+ public createPTable(def: PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>): PTableHandle;
549
740
  public createPTable(def: {
550
- columns: PColumn<TreeNodeAccessor | PColumnValues>[];
741
+ columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[];
551
742
  filters?: PTableRecordFilter[];
552
743
  /** Table sorting */
553
744
  sorting?: PTableSorting[];
554
745
  }): PTableHandle;
555
746
  public createPTable(
556
747
  def:
557
- | PTableDef<PColumn<TreeNodeAccessor | PColumnValues>>
748
+ | PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>
558
749
  | {
559
- columns: PColumn<TreeNodeAccessor | PColumnValues>[];
750
+ columns: PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>[];
560
751
  filters?: PTableRecordFilter[];
561
752
  /** Table sorting */
562
753
  sorting?: PTableSorting[];
563
754
  },
564
755
  ): PTableHandle {
565
- let rawDef: PTableDef<PColumn<TreeNodeAccessor | PColumnValues>>;
756
+ let rawDef: PTableDef<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>>;
566
757
  if ('columns' in def) {
567
758
  rawDef = {
568
759
  src: {
@@ -575,11 +766,9 @@ export class RenderCtx<Args, UiState> {
575
766
  } else {
576
767
  rawDef = def;
577
768
  }
578
- this.verifyInlineColumnsSupport(extractAllColumns(rawDef.src));
769
+ this.verifyInlineAndExplicitColumnsSupport(extractAllColumns(rawDef.src));
579
770
  return this.ctx.createPTable(
580
- mapPTableDef(rawDef, (po) =>
581
- mapPObjectData(po, (d) => (d instanceof TreeNodeAccessor ? d.handle : d)),
582
- ),
771
+ mapPTableDef(rawDef, (po) => transformPColumnData(po)),
583
772
  );
584
773
  }
585
774
 
@@ -16,6 +16,7 @@ import type {
16
16
  PTableHandle,
17
17
  ResultCollection,
18
18
  ValueOrError,
19
+ DataInfo,
19
20
  } from '@milaboratories/pl-model-common';
20
21
 
21
22
  export const StagingAccessorName = 'staging';
@@ -140,9 +141,9 @@ export interface GlobalCfgRenderCtxMethods<AHandle = AccessorHandle, FHandle = F
140
141
  // PFrame / PTable
141
142
  //
142
143
 
143
- createPFrame(def: PFrameDef<AHandle | PColumnValues>): PFrameHandle;
144
+ createPFrame(def: PFrameDef<AHandle | PColumnValues | DataInfo<AHandle>>): PFrameHandle;
144
145
 
145
- createPTable(def: PTableDef<PColumn<AHandle | PColumnValues>>): PTableHandle;
146
+ createPTable(def: PTableDef<PColumn<AHandle | PColumnValues | DataInfo<AHandle>>>): PTableHandle;
146
147
 
147
148
  //
148
149
  // Computable
@@ -152,6 +153,7 @@ export interface GlobalCfgRenderCtxMethods<AHandle = AccessorHandle, FHandle = F
152
153
  }
153
154
 
154
155
  export const GlobalCfgRenderCtxFeatureFlags = {
156
+ explicitColumnsSupport: true as const,
155
157
  inlineColumnsSupport: true as const,
156
158
  activeArgs: true as const,
157
159
  };