@isrd-isi-edu/ermrestjs 2.5.0 → 2.6.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.
@@ -33,6 +33,7 @@ import {
33
33
  RelatedReference,
34
34
  BulkCreateForeignKeyObject,
35
35
  } from '@isrd-isi-edu/ermrestjs/src/models/reference';
36
+ import ActiveListCondition from '@isrd-isi-edu/ermrestjs/src/models/active-list-condition';
36
37
 
37
38
  // services
38
39
  import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
@@ -41,7 +42,7 @@ import ErrorService from '@isrd-isi-edu/ermrestjs/src/services/error';
41
42
  import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
42
43
 
43
44
  // utils
44
- import { createPseudoColumn, isAllOutboundColumn, isRelatedColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
45
+ import { createPseudoColumn, isRelatedColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
45
46
  import {
46
47
  computeReadPath,
47
48
  generateFacetColumns,
@@ -71,7 +72,6 @@ import { onload } from '@isrd-isi-edu/ermrestjs/js/setup/node';
71
72
  import {
72
73
  _getPagingValues,
73
74
  _getRecursiveAnnotationValue,
74
- _isEntryContext,
75
75
  _isValidForeignKeyName,
76
76
  _isValidSortElement,
77
77
  compareColumnPositions,
@@ -81,6 +81,14 @@ import { _compressFacetObject, _sourceColumnHelpers } from '@isrd-isi-edu/ermres
81
81
 
82
82
  import type { CommentType } from '@isrd-isi-edu/ermrestjs/src/models/comment';
83
83
  import type { DisplayName } from '@isrd-isi-edu/ermrestjs/src/models/display-name';
84
+ import {
85
+ ActiveListBuilder,
86
+ ActiveListRequestTypes,
87
+ type ActiveList,
88
+ type ActiveListRequest,
89
+ type ActiveListRelatedEntityRequest,
90
+ } from '@isrd-isi-edu/ermrestjs/src/services/active-list';
91
+
84
92
  import { _exportHelpers, _getDefaultExportTemplate, _referenceExportOutput, validateExportTemplate } from '@isrd-isi-edu/ermrestjs/js/export';
85
93
 
86
94
  /**
@@ -117,7 +125,7 @@ import { _exportHelpers, _getDefaultExportTemplate, _referenceExportOutput, vali
117
125
  * @return Promise when resolved passes the
118
126
  * {@link Reference} object. If rejected, passes one of the ermrest errors
119
127
  */
120
- export const resolve = async (uri: string, contextHeaderParams?: any): Promise<Reference> => {
128
+ export const resolve = async (uri: string, contextHeaderParams?: unknown): Promise<Reference> => {
121
129
  verify(uri, "'uri' must be specified");
122
130
  // make sure all the dependencies are loaded
123
131
  await onload();
@@ -148,22 +156,6 @@ export const _createPage = (reference: Reference, etag: string, data: any, hasPr
148
156
  return new Page(reference, etag, data, hasPrevious, hasNext);
149
157
  };
150
158
 
151
- type ActiveList = {
152
- // TODO
153
- // requests: {
154
- // column: ReferenceColumn;
155
- // objects: Array<{ index: number; column?: boolean; related?: boolean; inline?: boolean; citation?: boolean }>;
156
- // type: 'column' | 'related' | 'inline' | 'citation';
157
- // // TODO backwards compatibility:
158
- // inline?: boolean;
159
- // related?: boolean;
160
- // citation?: boolean;
161
- // };
162
- requests: any[];
163
- allOutBounds: Array<ForeignKeyPseudoColumn | PseudoColumn>;
164
- selfLinks: Array<KeyPseudoColumn>;
165
- };
166
-
167
159
  export type VisibleColumn =
168
160
  | ReferenceColumn
169
161
  | PseudoColumn
@@ -229,9 +221,9 @@ export class Reference {
229
221
  private _canUseTRS?: boolean;
230
222
  private _canUseTCRS?: boolean;
231
223
  private _readPath?: string;
232
- private _readAttributeGroupPathProps_cached?: any;
224
+ private _readAttributeGroupPathProps_cached?: unknown;
233
225
  private _display?: ReferenceDisplay;
234
- private _defaultExportTemplate?: any;
226
+ private _defaultExportTemplate?: unknown;
235
227
  private _csvDownloadLink?: string | null;
236
228
  private _searchColumns?: Array<VisibleColumn> | false;
237
229
  private _cascadingDeletedItems?: Array<Table | RelatedReference | Reference>;
@@ -239,7 +231,7 @@ export class Reference {
239
231
  private _related?: Array<RelatedReference>;
240
232
  private _facetColumns?: Array<FacetColumn>;
241
233
  private _facetColumnsStructure?: Array<number | FacetGroup>;
242
- private _activeList?: any;
234
+ private _activeList?: ActiveList;
243
235
  private _citation?: Citation | null;
244
236
  private _googleDatasetMetadata?: GoogleDatasetMetadata | null;
245
237
 
@@ -328,6 +320,10 @@ export class Reference {
328
320
  return this._pseudoColumn;
329
321
  }
330
322
 
323
+ /**
324
+ * The column-directive that is used for creating this reference.
325
+ * Allows access to props defined on the column-directive (display props, condition, waitFor, etc)
326
+ */
331
327
  setPseudoColumn(value: VisibleColumn) {
332
328
  this._pseudoColumn = value;
333
329
  }
@@ -604,7 +600,7 @@ export class Reference {
604
600
  /**
605
601
  * Generate the list of columns for this reference based on context and annotations
606
602
  */
607
- generateColumnsList(tuple?: Tuple, columnsList?: any[], dontChangeReference?: boolean, skipLog?: boolean): ReferenceColumn[] {
603
+ generateColumnsList(tuple?: Tuple, columnsList?: Array<unknown>, dontChangeReference?: boolean, skipLog?: boolean): ReferenceColumn[] {
608
604
  const resultColumns = generateColumnsList(this, tuple, columnsList, skipLog);
609
605
 
610
606
  if (!dontChangeReference) {
@@ -849,7 +845,7 @@ export class Reference {
849
845
  if (this._activeList === undefined) {
850
846
  this.generateActiveList();
851
847
  }
852
- return this._activeList;
848
+ return this._activeList!;
853
849
  }
854
850
 
855
851
  /**
@@ -878,182 +874,80 @@ export class Reference {
878
874
  * @private
879
875
  */
880
876
  generateActiveList(tuple?: Tuple): ActiveList {
881
- // VARIABLES:
882
- const allOutBounds: Array<PseudoColumn | ForeignKeyPseudoColumn> = [];
883
- const requests: any[] = [];
884
- const selfLinks: Array<KeyPseudoColumn> = [];
885
- const consideredUniqueFiltered: { [key: string]: number } = {};
886
- const consideredSets: { [key: string]: number } = {};
887
- const consideredOutbounds: { [key: string]: boolean } = {};
888
- const consideredAggregates: { [key: string]: number } = {};
889
- const consideredSelfLinks: { [key: string]: boolean } = {};
890
- const consideredEntryWaitFors: { [key: string]: number } = {};
891
-
892
- const sds = this.table.sourceDefinitions;
893
-
894
- // in detailed, we want related and citation
895
877
  const isDetailed = this._context === _contexts.DETAILED;
896
- // in entry, don't include waitfors in the activelist used for loading the page.
897
- // the waitfors will be used in chaise instead prior to submission.
898
- const isEntry = _isEntryContext(this._context);
899
-
900
- const COLUMN_TYPE = 'column';
901
- const RELATED_TYPE = 'related';
902
- const CITATION_TYPE = 'citation';
903
- const INLINE_TYPE = 'inline';
904
-
905
- // FUNCTIONS:
906
- const hasAggregate = (col: VisibleColumn) => {
907
- return col.hasWaitForAggregate || ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate);
908
- };
909
-
910
- // col: the column that we need its data
911
- // isWaitFor: whether it was part of waitFor or just visible
912
- // type: where in the page it belongs to
913
- // index: the container index (column index, or related index) (optional)
914
- const addColToActiveList = (col: VisibleColumn, isWaitFor: boolean, type: string, index?: number) => {
915
- const obj: any = { isWaitFor: isWaitFor };
916
-
917
- // add the type
918
- obj[type] = true;
919
-
920
- // add index if available (not available in citation)
921
- if (Number.isInteger(index)) {
922
- obj.index = index;
923
- }
924
-
925
- if (isWaitFor && isEntry) {
926
- if (col.name in consideredEntryWaitFors) {
927
- requests[consideredEntryWaitFors[col.name]].objects.push(obj);
928
- return;
929
- }
930
- consideredEntryWaitFors[col.name] = requests.length;
931
- requests.push({ firstOutbound: true, column: col, objects: [obj] });
932
- return;
933
- }
934
-
935
- // unique filtered
936
- // TODO FILTER_IN_SOURCE chaise should use this type of column as well?
937
- // TODO FILTER_IN_SOURCE should be added to documentation as well
938
- if ((col as PseudoColumn).sourceObjectWrapper?.isUniqueFiltered) {
939
- // duplicate
940
- if (col.name in consideredUniqueFiltered) {
941
- requests[consideredUniqueFiltered[col.name]].objects.push(obj);
942
- return;
943
- }
944
-
945
- // new
946
- consideredUniqueFiltered[col.name] = requests.length;
947
- requests.push({ entity: true, column: col, objects: [obj] });
948
- return;
949
- }
950
-
951
- // aggregates
952
- if ((col as PseudoColumn).isPathColumn && (col as PseudoColumn).hasAggregate) {
953
- // duplicate
954
- if (col.name in consideredAggregates) {
955
- requests[consideredAggregates[col.name]].objects.push(obj);
956
- return;
957
- }
958
-
959
- // new
960
- consideredAggregates[col.name] = requests.length;
961
- requests.push({ aggregate: true, column: col, objects: [obj] });
962
- return;
963
- }
878
+ const builder = new ActiveListBuilder(this, tuple, isDetailed);
964
879
 
965
- //entitysets
966
- if (isRelatedColumn(col)) {
967
- if (!isDetailed) {
968
- return; // only acceptable in detailed
969
- }
970
-
971
- if (col.name in consideredSets) {
972
- requests[consideredSets[col.name]].objects.push(obj);
973
- return;
974
- }
975
-
976
- consideredSets[col.name] = requests.length;
977
- requests.push({ entityset: true, column: col, objects: [obj] });
978
- }
979
-
980
- // all outbounds
981
- if (isAllOutboundColumn(col)) {
982
- if (col.name in consideredOutbounds) return;
983
- consideredOutbounds[col.name] = true;
984
- allOutBounds.push(col as PseudoColumn | ForeignKeyPseudoColumn);
985
- }
986
-
987
- // self-links
988
- if ((col as KeyPseudoColumn).isKey) {
989
- if (col.name in consideredSelfLinks) return;
990
- consideredSelfLinks[col.name] = true;
991
- selfLinks.push(col as KeyPseudoColumn);
992
- }
993
- };
994
-
995
- const addInlineColumn = (col: VisibleColumn, i: number) => {
996
- if (isRelatedColumn(col)) {
997
- requests.push({ inline: true, index: i });
998
- } else {
999
- addColToActiveList(col, false, COLUMN_TYPE, i);
1000
- }
1001
-
1002
- col.waitFor.forEach((wf: any) => {
1003
- addColToActiveList(wf, true, isRelatedColumn(col) ? INLINE_TYPE : COLUMN_TYPE, i);
1004
- });
880
+ /**
881
+ * Helper for the condition branch shared across the columns / related loops.
882
+ * Returns true if the column was routed through a conditional group (so the
883
+ * caller should NOT add the column directly), false if there was no
884
+ * applicable condition (caller adds normally).
885
+ */
886
+ const tryCondition = (
887
+ condition: ActiveListCondition | null | undefined,
888
+ conditionedItem: { column?: boolean; inline?: boolean; related?: boolean; index: number },
889
+ addToDeps: (deps: Array<ActiveListRequest | ActiveListRelatedEntityRequest>) => void,
890
+ ): boolean => {
891
+ if (!isDetailed || !condition) return false;
892
+ builder.processConditionedItem(condition, conditionedItem, addToDeps);
893
+ return true;
1005
894
  };
1006
895
 
1007
- // THE CODE STARTS HERE:
1008
896
  const columns = this.generateColumnsList(tuple);
1009
897
 
1010
898
  // citation
1011
899
  if (isDetailed && this.citation) {
1012
900
  this.citation.waitFor.forEach((col) => {
1013
- addColToActiveList(col, true, CITATION_TYPE);
901
+ builder.addCol(col, true, ActiveListRequestTypes.CITATION);
1014
902
  });
1015
903
  }
1016
904
 
1017
905
  // columns without aggregate
1018
906
  columns.forEach((col, i: number) => {
1019
- if (!hasAggregate(col)) {
1020
- addInlineColumn(col, i);
1021
- }
907
+ if (builder.hasAggregate(col)) return;
908
+ const rc = col.resolvedCondition;
909
+ const item = isRelatedColumn(col) ? { inline: true, index: i } : { column: true, index: i };
910
+ if (tryCondition(rc, item, (deps) => builder.addInlineToDependent(deps, col, i))) return;
911
+ builder.addInline(col, i);
1022
912
  });
1023
913
 
1024
914
  // columns with aggregate
1025
915
  columns.forEach((col, i: number) => {
1026
- if (hasAggregate(col)) {
1027
- addInlineColumn(col, i);
1028
- }
916
+ if (!builder.hasAggregate(col)) return;
917
+ const rc = col.resolvedCondition;
918
+ const item = isRelatedColumn(col) ? { inline: true, index: i } : { column: true, index: i };
919
+ if (tryCondition(rc, item, (deps) => builder.addInlineToDependent(deps, col, i))) return;
920
+ builder.addInline(col, i);
1029
921
  });
1030
922
 
1031
923
  // related tables
1032
924
  if (isDetailed) {
1033
925
  this.generateRelatedList(tuple).forEach((rel, i) => {
1034
- requests.push({ related: true, index: i });
1035
-
1036
- if (rel.pseudoColumn) {
1037
- rel.pseudoColumn.waitFor.forEach((wf) => {
1038
- addColToActiveList(wf, true, RELATED_TYPE, i);
926
+ const rc = rel.pseudoColumn?.resolvedCondition;
927
+ /**
928
+ * if there's a condition, we have to capture the requests generated for the related entity as its dependencies.
929
+ * So when the condition is evaludated, we can decide whether to run those requests or not.
930
+ */
931
+ const addRelatedToDeps = (deps: Array<ActiveListRequest | ActiveListRelatedEntityRequest>) => {
932
+ deps.push({ related: true, index: i });
933
+ rel.pseudoColumn?.waitFor.forEach((wf) => {
934
+ builder.addColToDependent(deps, wf, true, ActiveListRequestTypes.RELATED, i);
1039
935
  });
1040
- }
936
+ };
937
+ if (tryCondition(rc, { related: true, index: i }, addRelatedToDeps)) return;
938
+ builder.requests.push({ related: true, index: i });
939
+ rel.pseudoColumn?.waitFor.forEach((wf) => {
940
+ builder.addCol(wf, true, ActiveListRequestTypes.RELATED, i);
941
+ });
1041
942
  });
1042
943
  }
1043
944
 
1044
- //fkeys
1045
- sds.fkeys.forEach((fk) => {
1046
- if (fk.name in consideredOutbounds) return;
1047
- consideredOutbounds[fk.name] = true;
1048
- allOutBounds.push(new ForeignKeyPseudoColumn(this, fk));
945
+ // fkeys
946
+ this.table.sourceDefinitions.fkeys.forEach((fk) => {
947
+ builder.addAllOutBound(new ForeignKeyPseudoColumn(this, fk));
1049
948
  });
1050
949
 
1051
- this._activeList = {
1052
- requests: requests,
1053
- allOutBounds: allOutBounds,
1054
- selfLinks: selfLinks,
1055
- };
1056
-
950
+ this._activeList = builder.build();
1057
951
  return this._activeList;
1058
952
  }
1059
953
 
@@ -1074,7 +968,7 @@ export class Reference {
1074
968
  }
1075
969
 
1076
970
  /**
1077
- * Will return the expor templates that are available for this reference.
971
+ * Will return the export templates that are available for this reference.
1078
972
  * It will validate the templates that are defined in annotations.
1079
973
  * If its `detailed` context and annotation was missing,
1080
974
  * it will return the default export template.
@@ -1088,6 +982,7 @@ export class Reference {
1088
982
 
1089
983
  // annotation is missing
1090
984
  if (!Array.isArray(templates)) {
985
+ // eslint-disable-next-line eqeqeq
1091
986
  const canUseDefault = useDefault && this.context === _contexts.DETAILED && this.defaultExportTemplate != null;
1092
987
 
1093
988
  return canUseDefault ? [this.defaultExportTemplate] : [];
@@ -1128,7 +1023,7 @@ export class Reference {
1128
1023
  */
1129
1024
  getColumnByName(name: string): VisibleColumn {
1130
1025
  // given an array of columns, find column by name
1131
- const findCol = (list: any[]): any => {
1026
+ const findCol = (list: Array<VisibleColumn | Column>): VisibleColumn | Column | false => {
1132
1027
  for (let i = 0; i < list.length; i++) {
1133
1028
  if (list[i].name === name) {
1134
1029
  return list[i];
@@ -1140,13 +1035,13 @@ export class Reference {
1140
1035
  // search in visible columns
1141
1036
  let c = findCol(this.columns);
1142
1037
  if (c) {
1143
- return c;
1038
+ return c as VisibleColumn;
1144
1039
  }
1145
1040
 
1146
1041
  // search in table columns
1147
1042
  c = findCol(this.table.columns.all());
1148
1043
  if (c) {
1149
- return new ReferenceColumn(this, [c]);
1044
+ return new ReferenceColumn(this, [c as Column]);
1150
1045
  }
1151
1046
 
1152
1047
  // backward compatibility, look at fks and keys using constraint name
@@ -232,7 +232,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
232
232
  return { isHTML: false, value: data[this._baseCol.name], unformatted: data[this._baseCol.name] };
233
233
  }
234
234
 
235
- if (this.hasWaitFor && !options.skipWaitFor) {
235
+ if ((this.hasWaitFor || this.hasCondition) && !options.skipWaitFor) {
236
236
  return nullValue;
237
237
  }
238
238
 
@@ -14,6 +14,7 @@ import { isObjectAndNotNull, isStringAndNotEmpty, isObjectAndKeyExists } from '@
14
14
  import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
15
15
  import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
16
16
  import { _annotations, _foreignKeyInputModes } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
17
+ import { buildSelfTemplateVariables } from '@isrd-isi-edu/ermrestjs/src/utils/template-utils';
17
18
  import { Reference } from '@isrd-isi-edu/ermrestjs/src/models/reference';
18
19
 
19
20
  // legacy
@@ -323,8 +324,8 @@ export class ForeignKeyPseudoColumn extends ReferenceColumn {
323
324
  unformatted: this._getNullValue(context!),
324
325
  };
325
326
 
326
- // if there's wait_for, this should return null.
327
- if (this.hasWaitFor && !options.skipWaitFor) {
327
+ // if there's wait_for or async condition, this should return null.
328
+ if ((this.hasWaitFor || this.hasCondition) && !options.skipWaitFor) {
328
329
  return nullValue;
329
330
  }
330
331
 
@@ -347,12 +348,7 @@ export class ForeignKeyPseudoColumn extends ReferenceColumn {
347
348
  const context = this._context;
348
349
 
349
350
  if (this.display.sourceMarkdownPattern) {
350
- let selfTemplateVariables = {};
351
- if (mainTuple.linkedData[this.name]) {
352
- selfTemplateVariables = {
353
- $self: _getRowTemplateVariables(this.table, context, mainTuple.linkedData[this.name]),
354
- };
355
- }
351
+ const selfTemplateVariables = buildSelfTemplateVariables(this, mainTuple, columnValue);
356
352
 
357
353
  const keyValues = {};
358
354
  Object.assign(keyValues, templateVariables, selfTemplateVariables);
@@ -8,6 +8,7 @@ import { Reference, Tuple } from '@isrd-isi-edu/ermrestjs/src/models/reference';
8
8
  // utils
9
9
  import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
10
10
  import { isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
11
+ import { buildSelfTemplateVariables } from '@isrd-isi-edu/ermrestjs/src/utils/template-utils';
11
12
 
12
13
  // legacy
13
14
  import { Key, Table } from '@isrd-isi-edu/ermrestjs/js/core';
@@ -95,7 +96,7 @@ export class KeyPseudoColumn extends ReferenceColumn {
95
96
  unformatted: this._getNullValue(context!),
96
97
  };
97
98
 
98
- if (this.hasWaitFor && !options.skipWaitFor) {
99
+ if ((this.hasWaitFor || this.hasCondition) && !options.skipWaitFor) {
99
100
  return nullValue;
100
101
  }
101
102
 
@@ -118,9 +119,7 @@ export class KeyPseudoColumn extends ReferenceColumn {
118
119
  const context = this._context;
119
120
 
120
121
  if (this.display.sourceMarkdownPattern) {
121
- const selfTemplateVariables = {
122
- $self: _getRowTemplateVariables(this.table, context, mainTuple.data),
123
- };
122
+ const selfTemplateVariables = buildSelfTemplateVariables(this, mainTuple, columnValue);
124
123
 
125
124
  const keyValues = {};
126
125
  Object.assign(keyValues, templateVariables, selfTemplateVariables);
@@ -15,6 +15,7 @@ import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils
15
15
  import { isDefinedAndNotNull, isObject, isObjectAndNotNull, isStringAndNotEmpty, verify } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
16
16
  import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
17
17
  import { processAggregateValue } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
18
+ import { buildSelfTemplateVariables } from '@isrd-isi-edu/ermrestjs/src/utils/template-utils';
18
19
  import {
19
20
  _pseudoColAggregateFns,
20
21
  _pseudoColEntityAggregateFns,
@@ -148,7 +149,7 @@ export class PseudoColumn extends ReferenceColumn {
148
149
  return nullValue;
149
150
  }
150
151
 
151
- if (this.hasWaitFor && !options.skipWaitFor) {
152
+ if ((this.hasWaitFor || this.hasCondition) && !options.skipWaitFor) {
152
153
  return nullValue;
153
154
  }
154
155
 
@@ -193,42 +194,10 @@ export class PseudoColumn extends ReferenceColumn {
193
194
  }
194
195
 
195
196
  sourceFormatPresentation(templateVariables: any, columnValue: any, mainTuple: Tuple) {
196
- const baseCol = this.baseColumn;
197
197
  const context = this._context;
198
- let selfTemplateVariables: any = {};
199
198
 
200
199
  if (this.display.sourceMarkdownPattern) {
201
- // for aggregate, the columnValue has the value precomputed
202
- if (this.hasAggregate && columnValue) {
203
- selfTemplateVariables = columnValue.templateVariables;
204
- }
205
- // all-outbound paths
206
- else if (this.hasPath && this.isUnique) {
207
- // use the linked data if exists
208
- if (!mainTuple.linkedData[this.name]) {
209
- selfTemplateVariables = {};
210
- }
211
- // scalar default
212
- else if (!this.isEntityMode) {
213
- selfTemplateVariables = {
214
- $self: baseCol.formatvalue(mainTuple.linkedData[this.name][baseCol.name], context),
215
- $_self: mainTuple.linkedData[this.name][baseCol.name],
216
- };
217
- }
218
- // entity default
219
- else {
220
- selfTemplateVariables = {
221
- $self: _getRowTemplateVariables(this.table, context, mainTuple.linkedData[this.name]),
222
- };
223
- }
224
- }
225
- // any other paths
226
- else if (baseCol) {
227
- selfTemplateVariables = {
228
- $self: baseCol.formatvalue(mainTuple.data[baseCol.name], context),
229
- $_self: mainTuple.data[baseCol.name],
230
- };
231
- }
200
+ const selfTemplateVariables = buildSelfTemplateVariables(this, mainTuple, columnValue);
232
201
 
233
202
  const keyValues = {};
234
203
  Object.assign(keyValues, templateVariables, selfTemplateVariables);
@@ -5,13 +5,17 @@ import { CommentType } from '@isrd-isi-edu/ermrestjs/src/models/comment';
5
5
  import { DisplayName } from '@isrd-isi-edu/ermrestjs/src/models/display-name';
6
6
  import { ColumnAggregateFn, ColumnGroupAggregateFn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
7
7
  import { Tuple, Reference } from '@isrd-isi-edu/ermrestjs/src/models/reference';
8
+ import type { ConditionDefinition } from '@isrd-isi-edu/ermrestjs/src/models/table-source-definitions';
8
9
 
9
10
  // services
11
+ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
10
12
 
11
13
  // utils
12
14
  import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
13
15
  import { isObjectAndKeyExists, isObjectAndNotNull, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
14
16
  import { _annotations, _contexts } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
17
+ import { buildSelfTemplateVariables } from '@isrd-isi-edu/ermrestjs/src/utils/template-utils';
18
+ import ActiveListCondition from '@isrd-isi-edu/ermrestjs/src/models/active-list-condition';
15
19
 
16
20
  // legacy
17
21
  import { Column, Table, Type } from '@isrd-isi-edu/ermrestjs/js/core';
@@ -122,6 +126,8 @@ export class ReferenceColumn {
122
126
  protected _hasWaitFor?: boolean;
123
127
  protected _hasWaitForAggregate?: boolean;
124
128
  protected _waitFor?: ReferenceColumn[];
129
+ protected _hasCondition?: boolean;
130
+ protected _resolvedCondition?: ActiveListCondition | null;
125
131
  /**
126
132
  * the reference that this column is defined on
127
133
  */
@@ -441,6 +447,69 @@ export class ReferenceColumn {
441
447
  return this._waitFor!;
442
448
  }
443
449
 
450
+ /**
451
+ * Whether the column has an async condition that requires a secondary request.
452
+ * Only true in detailed context for conditions whose source has an inbound path or aggregate.
453
+ */
454
+ get hasCondition(): boolean {
455
+ if (this._hasCondition === undefined) {
456
+ // triggers resolution
457
+ void this.resolvedCondition;
458
+ }
459
+ return this._hasCondition!;
460
+ }
461
+
462
+ /**
463
+ * The resolved condition for this column, or null if no condition or not applicable.
464
+ */
465
+ get resolvedCondition(): ActiveListCondition | null {
466
+ if (this._resolvedCondition === undefined) {
467
+ this._resolvedCondition = this._resolveCondition();
468
+ this._hasCondition = this._resolvedCondition?.isAsync ?? false;
469
+ }
470
+ return this._resolvedCondition;
471
+ }
472
+
473
+ /**
474
+ * Resolve the condition definition and source for this column.
475
+ */
476
+ protected _resolveCondition(): ActiveListCondition | null {
477
+ // only applicable in detailed context
478
+ if (this._context !== _contexts.DETAILED) {
479
+ return null;
480
+ }
481
+
482
+ if (!this.sourceObjectWrapper) {
483
+ return null;
484
+ }
485
+
486
+ // get condition def: inline condition or condition_key
487
+ let condDef: ConditionDefinition | undefined, condKey: string | undefined;
488
+ const sourceObject = this.sourceObjectWrapper.sourceObject;
489
+ if (isStringAndNotEmpty(sourceObject.condition_key)) {
490
+ condKey = sourceObject.condition_key as string;
491
+ condDef = this._currentTable.sourceDefinitions.getCondition(condKey);
492
+ if (!condDef) {
493
+ $log.info('condition_key `' + condKey + '` not found in source-definitions conditions.');
494
+ return null;
495
+ }
496
+ } else if (isObjectAndNotNull(sourceObject.condition)) {
497
+ condDef = sourceObject.condition as Record<string, unknown>;
498
+ } else if (!condDef) {
499
+ return null;
500
+ }
501
+
502
+ let condition: ActiveListCondition;
503
+ try {
504
+ condition = new ActiveListCondition(condDef, this._baseReference, this._mainTuple, condKey);
505
+ } catch (e: unknown) {
506
+ $log.warn(e instanceof Error ? e.message : String(e));
507
+ return null;
508
+ }
509
+
510
+ return condition;
511
+ }
512
+
444
513
  /**
445
514
  * Formats a value corresponding to this reference-column definition.
446
515
  */
@@ -465,7 +534,13 @@ export class ReferenceColumn {
465
534
  context = this._context;
466
535
  }
467
536
 
468
- // if there's wait_for, this should return null.
537
+ // wait_for placeholder: the column depends on data that isn't ready, so
538
+ // return a null placeholder. Note: hasCondition is intentionally NOT in
539
+ // this check — the column's value is independent of the condition; the
540
+ // condition only controls *visibility* (chaise's `conditionHide` flag).
541
+ // Returning null here for an async-conditioned local column would
542
+ // permanently blank it, since there's no secondary fetch to flip the
543
+ // value back in chaise.
469
544
  if (this.hasWaitFor && !options.skipWaitFor) {
470
545
  return {
471
546
  isHTML: false,
@@ -608,17 +683,13 @@ export class ReferenceColumn {
608
683
  }
609
684
 
610
685
  sourceFormatPresentation(templateVariables: any, columnValue: any, mainTuple: Tuple): any {
611
- const baseCol = this.baseColumns[0];
612
686
  const context = this._context;
613
687
 
614
688
  if (this.display.sourceMarkdownPattern) {
615
689
  let selfTemplateVariables = {};
616
- // children pseudo-column have already set the $self and $_self
617
- if (!this.isPseudo && baseCol) {
618
- selfTemplateVariables = {
619
- $self: baseCol.formatvalue(mainTuple.data[baseCol.name], context),
620
- $_self: mainTuple.data[baseCol.name],
621
- };
690
+ // children pseudo-columns build $self in their own override via buildSelfTemplateVariables
691
+ if (!this.isPseudo) {
692
+ selfTemplateVariables = buildSelfTemplateVariables(this, mainTuple, columnValue);
622
693
  }
623
694
 
624
695
  const keyValues = {};
@@ -77,7 +77,7 @@ export type InputIframePropsType = {
77
77
  */
78
78
  export default class SourceObjectWrapper {
79
79
  /**
80
- * the source object
80
+ * the raw source object (column-directive object)
81
81
  */
82
82
  public sourceObject: Record<string, unknown>;
83
83
  /**