@isrd-isi-edu/ermrestjs 2.6.1 → 2.8.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/js/ag_reference.js +2 -1
- package/js/core.js +11 -5
- package/js/utils/helpers.js +2 -460
- package/js/utils/pseudocolumn_helpers.js +37 -5
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/models/active-list-condition.ts +72 -29
- package/src/models/reference/reference.ts +8 -2
- package/src/models/reference/tuple.ts +14 -1
- package/src/models/reference-column/reference-column.ts +14 -5
- package/src/models/source-object-wrapper.ts +4 -4
- package/src/services/active-list.ts +6 -1
- package/src/services/handlebars.ts +69 -10
- package/src/utils/column-utils.ts +1 -1
- package/src/utils/constants.ts +84 -0
- package/src/utils/format-utils.ts +755 -0
- package/src/utils/reference-utils.ts +29 -2
- package/tsconfig.json +1 -2
|
@@ -16,11 +16,22 @@ import { _processWaitForList } from '@isrd-isi-edu/ermrestjs/js/utils/pseudocolu
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Encapsulates condition evaluation logic for conditionally visible
|
|
19
|
-
* columns and related entities
|
|
19
|
+
* columns and related entities.
|
|
20
|
+
*
|
|
21
|
+
* Two forms:
|
|
22
|
+
* - **With source**: `condDef.source` or `condDef.sourcekey` drives a fetch
|
|
23
|
+
* whose data feeds the condition. Only honored in `detailed` context.
|
|
24
|
+
* `column` is a PseudoColumn used by chaise's record-page flow.
|
|
25
|
+
* - **No source**: only `condDef.condition_pattern` is set. Evaluated
|
|
26
|
+
* synchronously against template globals (`$session`, `$catalog`,
|
|
27
|
+
* `isUserInAcl`, etc.). Honored in every context. `column` is `null`.
|
|
20
28
|
*/
|
|
21
29
|
export default class ActiveListCondition {
|
|
22
|
-
/**
|
|
23
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The PseudoColumn for fetching condition data.
|
|
32
|
+
* `null` for no-source conditions (pattern-only).
|
|
33
|
+
*/
|
|
34
|
+
column: VisibleColumn | null;
|
|
24
35
|
|
|
25
36
|
/** Whether the condition source requires a secondary request (has inbound path or aggregate) */
|
|
26
37
|
isAsync: boolean;
|
|
@@ -47,10 +58,25 @@ export default class ActiveListCondition {
|
|
|
47
58
|
|
|
48
59
|
const wm = _warningMessages;
|
|
49
60
|
|
|
50
|
-
if (typeof condDef !== 'object' ||
|
|
61
|
+
if (typeof condDef !== 'object' || condDef === null) {
|
|
51
62
|
throw new Error(wm.CONDITION.INVALID_SOURCE);
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
const hasSource = !!condDef.source || isStringAndNotEmpty(condDef.sourcekey);
|
|
66
|
+
|
|
67
|
+
// no-source branch: pattern-only, evaluated synchronously against template globals
|
|
68
|
+
if (!hasSource) {
|
|
69
|
+
if (!isStringAndNotEmpty(condDef.condition_pattern)) {
|
|
70
|
+
throw new Error(wm.CONDITION.MISSING_PATTERN);
|
|
71
|
+
}
|
|
72
|
+
if (condDef.wait_for !== undefined) {
|
|
73
|
+
throw new Error(wm.CONDITION.NO_SOURCE_WAIT_FOR);
|
|
74
|
+
}
|
|
75
|
+
this.column = null;
|
|
76
|
+
this.isAsync = false;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
let condSourceWrapper: SourceObjectWrapper;
|
|
55
81
|
try {
|
|
56
82
|
const sds = this._reference.table.sourceDefinitions;
|
|
@@ -101,10 +127,17 @@ export default class ActiveListCondition {
|
|
|
101
127
|
*/
|
|
102
128
|
get waitFor(): ReferenceColumn[] {
|
|
103
129
|
if (this._waitFor === undefined) {
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
// no-source conditions don't have a source fetch to wait on
|
|
131
|
+
if (this.column === null) {
|
|
132
|
+
this._waitFor = [];
|
|
133
|
+
this._hasWaitFor = false;
|
|
134
|
+
this._hasWaitForAggregate = false;
|
|
135
|
+
} else {
|
|
136
|
+
const res = _processWaitForList(this._condDef.wait_for, this._reference, this._reference.table, null, this._tuple, 'condition');
|
|
137
|
+
this._waitFor = res.waitForList;
|
|
138
|
+
this._hasWaitFor = res.hasWaitFor;
|
|
139
|
+
this._hasWaitForAggregate = res.hasWaitForAggregate;
|
|
140
|
+
}
|
|
108
141
|
}
|
|
109
142
|
return this._waitFor!;
|
|
110
143
|
}
|
|
@@ -112,25 +145,29 @@ export default class ActiveListCondition {
|
|
|
112
145
|
/**
|
|
113
146
|
* Decide whether the conditioned content should be shown.
|
|
114
147
|
*
|
|
115
|
-
*
|
|
148
|
+
* Four internal paths, dispatched on `this.column`, `condition_pattern`, and `this.isAsync`:
|
|
116
149
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
150
|
+
* 0. **No-source (`this.column === null`).** `condition_pattern` is rendered
|
|
151
|
+
* against the caller's `templateVariables` plus the catalog's global
|
|
152
|
+
* template environment (`$session`, `$catalog`, `isUserInAcl`, etc.).
|
|
153
|
+
* `conditionValue` and `mainTuple` are unused.
|
|
154
|
+
* 1. **With-source, `condition_pattern` set.** Render the pattern. `$self` /
|
|
155
|
+
* `$_self` come from {@link buildSelfTemplateVariables}, which sources from
|
|
156
|
+
* `mainTuple` for sync sources and from `conditionValue` for async sources.
|
|
157
|
+
* Blank rendered output ⇒ empty.
|
|
158
|
+
* 2. **With-source, no pattern + `isAsync === true`.** Caller already fetched
|
|
159
|
+
* the data; emptiness is computed from `conditionValue` by {@link _isConditionValueEmpty}.
|
|
160
|
+
* 3. **With-source, no pattern + `isAsync === false`.** `conditionValue` is
|
|
161
|
+
* ignored (sync sources have no fetched value). The raw value is pulled
|
|
162
|
+
* from `mainTuple` via {@link buildSelfTemplateVariables}: scalar sources
|
|
163
|
+
* are empty when `$_self` is `null`/`undefined`/`''`; entity-mode sources
|
|
164
|
+
* are empty when `$self` is undefined (the joined row didn't load).
|
|
128
165
|
*
|
|
129
166
|
* The `on_empty` flag then maps `isEmpty` to "show": `"hide"` (default) shows
|
|
130
167
|
* when not empty; `"show"` shows when empty.
|
|
131
168
|
*
|
|
132
169
|
* @param templateVariables - accumulated `$x`/`$_x` template variables for
|
|
133
|
-
* every other source (
|
|
170
|
+
* every other source (consulted by the pattern branches).
|
|
134
171
|
* @param conditionValue - the fetched value for an async source.
|
|
135
172
|
* - **entityset** (Reference.read result): a {@link Page} object — has
|
|
136
173
|
* `length` (used by `_isConditionValueEmpty`) and `templateVariables`
|
|
@@ -140,18 +177,24 @@ export default class ActiveListCondition {
|
|
|
140
177
|
* scalar; `templateVariables` carries `$self`/`$_self`.
|
|
141
178
|
* - **null** if the request failed or there's no fetched value (treated
|
|
142
179
|
* as empty).
|
|
143
|
-
* Ignored
|
|
144
|
-
* @param mainTuple - the main record tuple.
|
|
145
|
-
*
|
|
146
|
-
* from it.
|
|
180
|
+
* Ignored for no-source (path 0) and sync sources (path 3).
|
|
181
|
+
* @param mainTuple - the main record tuple. Required for paths 1 and 3
|
|
182
|
+
* (both look up `$self`/`$_self` from it). Unused for paths 0 and 2.
|
|
147
183
|
* @returns whether the conditioned content should be shown.
|
|
148
184
|
*/
|
|
149
|
-
evaluateCondition(templateVariables: any, conditionValue: any, mainTuple
|
|
185
|
+
evaluateCondition(templateVariables: any, conditionValue: any, mainTuple?: Tuple): { shouldShow: boolean } {
|
|
150
186
|
let isEmpty: boolean;
|
|
151
187
|
const condDef = this._condDef;
|
|
152
188
|
|
|
153
|
-
if (
|
|
154
|
-
|
|
189
|
+
if (this.column === null) {
|
|
190
|
+
// no-source: pattern-only, evaluated against caller's templateVariables
|
|
191
|
+
// plus the catalog's global template environment ($session, $catalog, etc.)
|
|
192
|
+
const rendered = _renderTemplate(condDef.condition_pattern as string, templateVariables ?? {}, this._reference.table.schema.catalog, {
|
|
193
|
+
templateEngine: condDef.template_engine,
|
|
194
|
+
});
|
|
195
|
+
isEmpty = !rendered || rendered.trim() === '';
|
|
196
|
+
} else if (condDef.condition_pattern) {
|
|
197
|
+
const selfTemplateVariables = buildSelfTemplateVariables(this.column as ReferenceColumn, mainTuple!, conditionValue);
|
|
155
198
|
const keyValues: any = {};
|
|
156
199
|
Object.assign(keyValues, templateVariables, selfTemplateVariables);
|
|
157
200
|
|
|
@@ -166,7 +209,7 @@ export default class ActiveListCondition {
|
|
|
166
209
|
} else {
|
|
167
210
|
// no pattern, sync: derive the raw value from the tuple via the same
|
|
168
211
|
// helper sourceFormatPresentation uses, then decide emptiness directly.
|
|
169
|
-
const selfVars = buildSelfTemplateVariables(this.column as ReferenceColumn, mainTuple
|
|
212
|
+
const selfVars = buildSelfTemplateVariables(this.column as ReferenceColumn, mainTuple!, null);
|
|
170
213
|
if ('$_self' in selfVars) {
|
|
171
214
|
// scalar source (local column or all-outbound scalar)
|
|
172
215
|
const v = selfVars.$_self;
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
computeReferenceDisplay,
|
|
50
50
|
type ReferenceDisplay,
|
|
51
51
|
generateColumnsList,
|
|
52
|
+
applyNoSourceConditions,
|
|
52
53
|
} from '@isrd-isi-edu/ermrestjs/src/utils/reference-utils';
|
|
53
54
|
import { isObject, isObjectAndNotNull, isStringAndNotEmpty, verify } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
54
55
|
import { fixedEncodeURIComponent, simpleDeepCopy } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
@@ -817,7 +818,7 @@ export class Reference {
|
|
|
817
818
|
}
|
|
818
819
|
|
|
819
820
|
if (notSorted && this._related.length !== 0) {
|
|
820
|
-
|
|
821
|
+
this._related.sort((a, b) => {
|
|
821
822
|
// displayname
|
|
822
823
|
if (a.displayname.value !== b.displayname.value) {
|
|
823
824
|
return (a.displayname.value as string).localeCompare(b.displayname.value as string);
|
|
@@ -835,6 +836,8 @@ export class Reference {
|
|
|
835
836
|
});
|
|
836
837
|
}
|
|
837
838
|
|
|
839
|
+
applyNoSourceConditions(this._related, (rel) => rel.pseudoColumn?.resolvedCondition);
|
|
840
|
+
|
|
838
841
|
return this._related;
|
|
839
842
|
}
|
|
840
843
|
|
|
@@ -888,7 +891,10 @@ export class Reference {
|
|
|
888
891
|
conditionedItem: { column?: boolean; inline?: boolean; related?: boolean; index: number },
|
|
889
892
|
addToDeps: (deps: Array<ActiveListRequest | ActiveListRelatedEntityRequest>) => void,
|
|
890
893
|
): boolean => {
|
|
891
|
-
|
|
894
|
+
// !condition.column is the no-source marker: those should already be
|
|
895
|
+
// filtered out by generateColumnsList / generateRelatedList, but this is
|
|
896
|
+
// a safety net since ActiveList's processConditionedItem assumes a column.
|
|
897
|
+
if (!isDetailed || !condition || !condition.column) return false;
|
|
892
898
|
builder.processConditionedItem(condition, conditionedItem, addToDeps);
|
|
893
899
|
return true;
|
|
894
900
|
};
|
|
@@ -4,7 +4,7 @@ import { DisplayName } from '@isrd-isi-edu/ermrestjs/src/models/display-name';
|
|
|
4
4
|
|
|
5
5
|
// utils
|
|
6
6
|
import { fixedEncodeURIComponent, shallowCopy, simpleDeepCopy } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
7
|
-
import { isObjectAndNotNull } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
7
|
+
import { isObjectAndNotNull, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
8
8
|
import { _ERMrestACLs, _contexts, _permissionMessages } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
9
9
|
|
|
10
10
|
import { isAllOutboundColumn } from '@isrd-isi-edu/ermrestjs/src/utils/column-utils';
|
|
@@ -154,6 +154,13 @@ export class Tuple {
|
|
|
154
154
|
return this._data;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
*
|
|
159
|
+
* @param permission the ermrest permission that should be checked (refer to _ERMrestACLs constant for more details)
|
|
160
|
+
* @param colName the name of the column for which the permission should be checked
|
|
161
|
+
* @param isAssoc whether the permission check is for an association
|
|
162
|
+
* @returns a boolean indicating whether the permission is granted
|
|
163
|
+
*/
|
|
157
164
|
checkPermissions(permission: string, colName?: string, isAssoc = false): boolean {
|
|
158
165
|
let sum = this._rightsSummary[permission];
|
|
159
166
|
if (isAssoc) {
|
|
@@ -161,6 +168,12 @@ export class Tuple {
|
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
if (permission === _ERMrestACLs.COLUMN_UPDATE) {
|
|
171
|
+
// if the column is passed but doesn't exist in the table, return false
|
|
172
|
+
if (isStringAndNotEmpty(colName) && !this._pageRef.table.columns.has(colName!)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// if there aren't any column-level permissions, then allow it
|
|
164
177
|
if (!isObjectAndNotNull(sum) || typeof sum[colName!] !== 'boolean') return true;
|
|
165
178
|
return sum[colName!];
|
|
166
179
|
}
|
|
@@ -461,6 +461,8 @@ export class ReferenceColumn {
|
|
|
461
461
|
|
|
462
462
|
/**
|
|
463
463
|
* The resolved condition for this column, or null if no condition or not applicable.
|
|
464
|
+
* Note: no-source conditions are returned here even after `applyNoSourceConditions`
|
|
465
|
+
* has already handled them. Downstream consumers guard via `!condition.column`.
|
|
464
466
|
*/
|
|
465
467
|
get resolvedCondition(): ActiveListCondition | null {
|
|
466
468
|
if (this._resolvedCondition === undefined) {
|
|
@@ -472,13 +474,13 @@ export class ReferenceColumn {
|
|
|
472
474
|
|
|
473
475
|
/**
|
|
474
476
|
* Resolve the condition definition and source for this column.
|
|
477
|
+
*
|
|
478
|
+
* With-source conditions (drive a secondary fetch tied to the main entity)
|
|
479
|
+
* are only honored in `detailed` context. No-source conditions (pattern-only)
|
|
480
|
+
* are honored in every context — they're evaluated synchronously against
|
|
481
|
+
* the catalog's global template environment.
|
|
475
482
|
*/
|
|
476
483
|
protected _resolveCondition(): ActiveListCondition | null {
|
|
477
|
-
// only applicable in detailed context
|
|
478
|
-
if (this._context !== _contexts.DETAILED) {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
484
|
if (!this.sourceObjectWrapper) {
|
|
483
485
|
return null;
|
|
484
486
|
}
|
|
@@ -499,6 +501,13 @@ export class ReferenceColumn {
|
|
|
499
501
|
return null;
|
|
500
502
|
}
|
|
501
503
|
|
|
504
|
+
// gate with-source conditions to detailed context (no-source falls through)
|
|
505
|
+
const hasSource = !!condDef.source || isStringAndNotEmpty(condDef.sourcekey as string | undefined);
|
|
506
|
+
if (hasSource && this._context !== _contexts.DETAILED) {
|
|
507
|
+
$log.info('condition with `source`/`sourcekey` only honored in `detailed` context; ignoring on column `' + this.name + '`.');
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
502
511
|
let condition: ActiveListCondition;
|
|
503
512
|
try {
|
|
504
513
|
condition = new ActiveListCondition(condDef, this._baseReference, this._mainTuple, condKey);
|
|
@@ -693,12 +693,12 @@ export class FacetObjectGroupWrapper {
|
|
|
693
693
|
* Will throw an error if there was any issues in processing the source object.
|
|
694
694
|
*
|
|
695
695
|
* @param sourceObject The source object representing the facet group.
|
|
696
|
-
* @param
|
|
696
|
+
* @param reference The reference to which the facet group belongs.
|
|
697
697
|
* @param hasFilterOrFacet Indicates if the group has any filters or facets.
|
|
698
698
|
*
|
|
699
699
|
* @throws Will throw an error if the source object is invalid or has no valid children.
|
|
700
700
|
*/
|
|
701
|
-
constructor(sourceObject: Record<string, unknown>,
|
|
701
|
+
constructor(sourceObject: Record<string, unknown>, reference: Reference, hasFilterOrFacet: boolean) {
|
|
702
702
|
if (!Array.isArray(sourceObject.and) || sourceObject.and.length === 0) {
|
|
703
703
|
throw new Error('valid array of children is required');
|
|
704
704
|
}
|
|
@@ -727,10 +727,10 @@ export class FacetObjectGroupWrapper {
|
|
|
727
727
|
const children: SourceObjectWrapper[] = [];
|
|
728
728
|
for (const child of sourceObject.and) {
|
|
729
729
|
try {
|
|
730
|
-
const wrapper = _facetColumnHelpers.sourceDefToFacetObjectWrapper(child,
|
|
730
|
+
const wrapper = _facetColumnHelpers.sourceDefToFacetObjectWrapper(child, reference, hasFilterOrFacet);
|
|
731
731
|
children.push(wrapper);
|
|
732
732
|
} catch (exp: unknown) {
|
|
733
|
-
$log.info(`child of facet group "${sourceObject.markdown_name}", index: ${children.length} is invalid:`);
|
|
733
|
+
$log.info(`child of facet group "${sourceObject.markdown_name}", index: ${children.length} is invalid/ignored:`);
|
|
734
734
|
$log.info((exp as Error).message);
|
|
735
735
|
}
|
|
736
736
|
}
|
|
@@ -379,6 +379,11 @@ export class ActiveListBuilder {
|
|
|
379
379
|
conditionedItem: ActiveListConditionedItem,
|
|
380
380
|
addToDependent: (dependentRequests: Array<ActiveListRequest | ActiveListRelatedEntityRequest>) => void,
|
|
381
381
|
): void {
|
|
382
|
+
// Defense: no-source conditions (column === null) are evaluated synchronously
|
|
383
|
+
// at column-build time and should never reach the ActiveList. Caller
|
|
384
|
+
// (`tryCondition`) already guards this; bail safely if something slipped past.
|
|
385
|
+
if (!condition.column) return;
|
|
386
|
+
|
|
382
387
|
// dedup: if this condition was already grouped (via condition_key), reuse
|
|
383
388
|
// that group's dependent list and add this item to its conditionedItems list.
|
|
384
389
|
if (condition.conditionKey) {
|
|
@@ -396,7 +401,7 @@ export class ActiveListBuilder {
|
|
|
396
401
|
// this populates `requests` (with `condition: true, index: conditionIndex`
|
|
397
402
|
// so chaise routes the completion to evaluateCondition); for sync sources
|
|
398
403
|
// it's a no-op on `requests` but still populates `allOutBounds` / `selfLinks`.
|
|
399
|
-
this.addCol(condition.column
|
|
404
|
+
this.addCol(condition.column!, false, ActiveListRequestTypes.CONDITION, conditionIndex);
|
|
400
405
|
condition.waitFor.forEach((wf) => {
|
|
401
406
|
this.addCol(wf, true, ActiveListRequestTypes.CONDITION, conditionIndex);
|
|
402
407
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-useless-escape */
|
|
2
2
|
import Handlebars from 'handlebars';
|
|
3
|
+
import { map as lodashMap } from 'lodash-es';
|
|
3
4
|
import moment from 'moment-timezone';
|
|
4
5
|
|
|
5
6
|
import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
@@ -11,11 +12,11 @@ import {
|
|
|
11
12
|
_addErmrestVarsToTemplate,
|
|
12
13
|
_addTemplateVars,
|
|
13
14
|
_escapeMarkdownCharacters,
|
|
14
|
-
_formatUtils,
|
|
15
15
|
_getPath,
|
|
16
16
|
encodeFacet,
|
|
17
17
|
encodeFacetString,
|
|
18
18
|
} from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
19
|
+
import { _formatUtils } from '@isrd-isi-edu/ermrestjs/src/utils/format-utils';
|
|
19
20
|
import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
|
|
20
21
|
import { isObjectAndNotNull } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
21
22
|
import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
@@ -402,6 +403,28 @@ export default class HandlebarsService {
|
|
|
402
403
|
return _formatUtils.humanizeBytes(value, mode, precision, tooltip);
|
|
403
404
|
},
|
|
404
405
|
|
|
406
|
+
/**
|
|
407
|
+
* {{datetimeDuration start end}}
|
|
408
|
+
* {{datetimeDuration start end unit="month"}}
|
|
409
|
+
* {{datetimeDuration start end unit="auto" fraction=2}}
|
|
410
|
+
* {{datetimeDuration start end unit="multi"}}
|
|
411
|
+
* {{datetimeDuration start end unit="calendar"}}
|
|
412
|
+
* {{datetimeDuration start end direction="before/after"}}
|
|
413
|
+
* {{datetimeDuration start end unit="month" tooltip=true}}
|
|
414
|
+
* @ignore
|
|
415
|
+
* @returns human-readable duration between `start` and `end`
|
|
416
|
+
*/
|
|
417
|
+
datetimeDuration: function (start: any, end: any, options: Handlebars.HelperOptions) {
|
|
418
|
+
let unit, fraction, direction, tooltip;
|
|
419
|
+
if (options && isObjectAndNotNull(options.hash)) {
|
|
420
|
+
unit = options.hash.unit;
|
|
421
|
+
fraction = options.hash.fraction;
|
|
422
|
+
direction = options.hash.direction;
|
|
423
|
+
tooltip = options.hash.tooltip;
|
|
424
|
+
}
|
|
425
|
+
return _formatUtils.datetimeDuration(start, end, unit, fraction, direction, tooltip);
|
|
426
|
+
},
|
|
427
|
+
|
|
405
428
|
/**
|
|
406
429
|
* {{stringLength value }}
|
|
407
430
|
* @ignore
|
|
@@ -419,15 +442,7 @@ export default class HandlebarsService {
|
|
|
419
442
|
* @returns a boolean indicating if the user is in any of the given groups
|
|
420
443
|
*/
|
|
421
444
|
isUserInAcl: function (...args) {
|
|
422
|
-
const groups = args.
|
|
423
|
-
if (Array.isArray(arg)) {
|
|
424
|
-
return acc.concat(arg);
|
|
425
|
-
} else if (typeof arg === 'string') {
|
|
426
|
-
return acc.concat([arg]);
|
|
427
|
-
}
|
|
428
|
-
return acc;
|
|
429
|
-
}, []);
|
|
430
|
-
|
|
445
|
+
const groups = flattenVariadicArgs(args).filter((g): g is string => typeof g === 'string');
|
|
431
446
|
return AuthnService.isUserInAcl(groups);
|
|
432
447
|
},
|
|
433
448
|
|
|
@@ -591,6 +606,40 @@ export default class HandlebarsService {
|
|
|
591
606
|
return Number(arg1) - Number(arg2);
|
|
592
607
|
},
|
|
593
608
|
});
|
|
609
|
+
|
|
610
|
+
// list/array helpers
|
|
611
|
+
Handlebars.registerHelper({
|
|
612
|
+
/**
|
|
613
|
+
* {{#if (memberOf scalarCol "a" "b" "c")}}
|
|
614
|
+
* {{#if (memberOf scalarCol someArray)}}
|
|
615
|
+
* {{#if (memberOf scalarCol (pluck $self "values.type"))}}
|
|
616
|
+
*
|
|
617
|
+
* Variadic args; any array argument is flattened one level.
|
|
618
|
+
* @returns true if `value` equals any of the given args
|
|
619
|
+
*/
|
|
620
|
+
memberOf: function (value: unknown, ...args: unknown[]) {
|
|
621
|
+
return flattenVariadicArgs(args).includes(value);
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* {{#if (hasMember arrayCol "non-polymer")}}
|
|
626
|
+
* @returns true if `array` is an array containing `value`
|
|
627
|
+
*/
|
|
628
|
+
hasMember: function (array: unknown, value: unknown) {
|
|
629
|
+
return Array.isArray(array) && array.includes(value);
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* {{pluck $self "values.type"}}
|
|
634
|
+
*
|
|
635
|
+
* Preserves length and order; missing paths resolve to undefined.
|
|
636
|
+
* @returns array of values from `array` at the given lodash path
|
|
637
|
+
*/
|
|
638
|
+
pluck: function (array: unknown, path: string): unknown[] {
|
|
639
|
+
if (!Array.isArray(array) || typeof path !== 'string' || !path) return [];
|
|
640
|
+
return lodashMap(array, path);
|
|
641
|
+
},
|
|
642
|
+
});
|
|
594
643
|
}
|
|
595
644
|
}
|
|
596
645
|
|
|
@@ -602,6 +651,16 @@ const reduceOp = function (args: any[], reducer: (a: boolean, b: boolean) => boo
|
|
|
602
651
|
return args.reduce(reducer, first);
|
|
603
652
|
};
|
|
604
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Drops the trailing Handlebars `options` arg, then flattens any array args
|
|
656
|
+
* one level into a single list. Used by variadic helpers that accept either
|
|
657
|
+
* loose values or arrays (e.g. `(memberOf col "a" "b")` vs `(memberOf col arr)`).
|
|
658
|
+
*/
|
|
659
|
+
const flattenVariadicArgs = function (args: unknown[]): unknown[] {
|
|
660
|
+
const rest = args.slice(0, -1); // drop options
|
|
661
|
+
return rest.reduce<unknown[]>((acc, a) => acc.concat(Array.isArray(a) ? a : [a]), []);
|
|
662
|
+
};
|
|
663
|
+
|
|
605
664
|
const regexpFindAll = function (value: string, regexp: string, flags: string) {
|
|
606
665
|
const regexpObj = new RegExp(regexp, flags);
|
|
607
666
|
const matches = value.match(regexpObj);
|
|
@@ -18,12 +18,12 @@ import { isDefinedAndNotNull } from '@isrd-isi-edu/ermrestjs/src/utils/type-util
|
|
|
18
18
|
|
|
19
19
|
// legacy
|
|
20
20
|
import {
|
|
21
|
-
_formatUtils,
|
|
22
21
|
_generateRowPresentation,
|
|
23
22
|
_getRowTemplateVariables,
|
|
24
23
|
_processColumnOrderList,
|
|
25
24
|
_renderTemplate,
|
|
26
25
|
} from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
26
|
+
import { _formatUtils } from '@isrd-isi-edu/ermrestjs/src/utils/format-utils';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Convert the raw value of an aggregate column to a formatted value.
|
package/src/utils/constants.ts
CHANGED
|
@@ -286,6 +286,7 @@ export const _handlebarsHelpersList = [
|
|
|
286
286
|
'toTitleCase',
|
|
287
287
|
'replace',
|
|
288
288
|
'humanizeBytes',
|
|
289
|
+
'datetimeDuration',
|
|
289
290
|
'printf',
|
|
290
291
|
'stringLength',
|
|
291
292
|
'isUserInAcl',
|
|
@@ -294,6 +295,10 @@ export const _handlebarsHelpersList = [
|
|
|
294
295
|
// math helpers
|
|
295
296
|
'add',
|
|
296
297
|
'subtract',
|
|
298
|
+
// list/array helpers
|
|
299
|
+
'memberOf',
|
|
300
|
+
'hasMember',
|
|
301
|
+
'pluck',
|
|
297
302
|
];
|
|
298
303
|
|
|
299
304
|
export const _operationsFlag = Object.freeze({
|
|
@@ -407,6 +412,8 @@ export const _warningMessages = Object.freeze({
|
|
|
407
412
|
USED_IN_IFRAME_INPUT: 'the column already used in another column mapping.',
|
|
408
413
|
CONDITION: {
|
|
409
414
|
INVALID_SOURCE: 'condition definition is invalid. `source` or `sourcekey` is required and must be valid.',
|
|
415
|
+
MISSING_PATTERN: 'condition without `source`/`sourcekey` must define `condition_pattern`.',
|
|
416
|
+
NO_SOURCE_WAIT_FOR: 'condition without `source`/`sourcekey` cannot use `wait_for`.',
|
|
410
417
|
},
|
|
411
418
|
});
|
|
412
419
|
|
|
@@ -523,6 +530,83 @@ export const _sourceProperties = Object.freeze({
|
|
|
523
530
|
|
|
524
531
|
export const _exportKnownAPIs = ['entity', 'attribute', 'attributegroup', 'aggregate'];
|
|
525
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Named constants for the `unit` argument of `datetimeDuration`. Source of
|
|
535
|
+
* truth for valid unit values — `_datetimeDuration.VALID_UNITS` and
|
|
536
|
+
* `UNIT_ORDER` are derived from this object below.
|
|
537
|
+
*
|
|
538
|
+
* Includes the three pseudo-units (`AUTO`, `MULTI`, `CALENDAR`) alongside the
|
|
539
|
+
* seven real time units. Use `DurationRealUnit` for the real-unit subset.
|
|
540
|
+
*/
|
|
541
|
+
export const DURATION_UNIT = Object.freeze({
|
|
542
|
+
AUTO: 'auto',
|
|
543
|
+
MULTI: 'multi',
|
|
544
|
+
CALENDAR: 'calendar',
|
|
545
|
+
YEAR: 'year',
|
|
546
|
+
MONTH: 'month',
|
|
547
|
+
DAY: 'day',
|
|
548
|
+
HOUR: 'hour',
|
|
549
|
+
MINUTE: 'minute',
|
|
550
|
+
SECOND: 'second',
|
|
551
|
+
MILLISECOND: 'millisecond',
|
|
552
|
+
} as const);
|
|
553
|
+
export type DurationUnit = (typeof DURATION_UNIT)[keyof typeof DURATION_UNIT];
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Named constants for the `direction` argument of `datetimeDuration`. Source
|
|
557
|
+
* of truth for valid direction values — `_datetimeDuration.VALID_DIRECTIONS`
|
|
558
|
+
* is derived from this object below.
|
|
559
|
+
*/
|
|
560
|
+
export const DURATION_DIRECTION = Object.freeze({
|
|
561
|
+
SIGN: 'sign',
|
|
562
|
+
BEFORE_AFTER: 'before/after',
|
|
563
|
+
EARLIER_LATER: 'earlier/later',
|
|
564
|
+
UNSIGNED: 'unsigned',
|
|
565
|
+
} as const);
|
|
566
|
+
export type DurationDirection = (typeof DURATION_DIRECTION)[keyof typeof DURATION_DIRECTION];
|
|
567
|
+
|
|
568
|
+
export const _datetimeDuration = Object.freeze({
|
|
569
|
+
// Real units in walking order (largest first). The source of truth for what
|
|
570
|
+
// counts as a "real" (non-pseudo) duration unit.
|
|
571
|
+
UNIT_ORDER: [
|
|
572
|
+
DURATION_UNIT.YEAR,
|
|
573
|
+
DURATION_UNIT.MONTH,
|
|
574
|
+
DURATION_UNIT.DAY,
|
|
575
|
+
DURATION_UNIT.HOUR,
|
|
576
|
+
DURATION_UNIT.MINUTE,
|
|
577
|
+
DURATION_UNIT.SECOND,
|
|
578
|
+
DURATION_UNIT.MILLISECOND,
|
|
579
|
+
] as const,
|
|
580
|
+
ABBREVIATIONS: {
|
|
581
|
+
[DURATION_UNIT.YEAR]: 'Y',
|
|
582
|
+
[DURATION_UNIT.MONTH]: 'M',
|
|
583
|
+
[DURATION_UNIT.DAY]: 'D',
|
|
584
|
+
[DURATION_UNIT.HOUR]: 'h',
|
|
585
|
+
[DURATION_UNIT.MINUTE]: 'm',
|
|
586
|
+
[DURATION_UNIT.SECOND]: 's',
|
|
587
|
+
[DURATION_UNIT.MILLISECOND]: 'ms',
|
|
588
|
+
},
|
|
589
|
+
// Julian fixed-math constants (1Y = 365.25d, 1M = 365.25/12d).
|
|
590
|
+
// Self-consistent (12 * MONTH === YEAR), matches moment.js / date-fns / astronomy.
|
|
591
|
+
CONVERSION_RATES: {
|
|
592
|
+
[DURATION_UNIT.MILLISECOND]: 1,
|
|
593
|
+
[DURATION_UNIT.SECOND]: 1000,
|
|
594
|
+
[DURATION_UNIT.MINUTE]: 60 * 1000,
|
|
595
|
+
[DURATION_UNIT.HOUR]: 60 * 60 * 1000,
|
|
596
|
+
[DURATION_UNIT.DAY]: 24 * 60 * 60 * 1000,
|
|
597
|
+
[DURATION_UNIT.MONTH]: (365.25 / 12) * 24 * 60 * 60 * 1000, // 2,629,800,000
|
|
598
|
+
[DURATION_UNIT.YEAR]: 365.25 * 24 * 60 * 60 * 1000, // 31,557,600,000
|
|
599
|
+
},
|
|
600
|
+
VALID_UNITS: Object.values(DURATION_UNIT) as readonly DurationUnit[],
|
|
601
|
+
VALID_DIRECTIONS: Object.values(DURATION_DIRECTION) as readonly DurationDirection[],
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* The real (non-pseudo) subset of `DurationUnit`, derived from `UNIT_ORDER`
|
|
606
|
+
* so it stays in sync automatically.
|
|
607
|
+
*/
|
|
608
|
+
export type DurationRealUnit = (typeof _datetimeDuration.UNIT_ORDER)[number];
|
|
609
|
+
|
|
526
610
|
export const FILTER_TYPES = Object.freeze({
|
|
527
611
|
BINARYPREDICATE: 'BinaryPredicate',
|
|
528
612
|
CONJUNCTION: 'Conjunction',
|