Support template variables in command menu item labels (#18707)
- Adds template variable interpolation (`${...}`) to command menu item
labels and short labels, enabling dynamic text like `Create new
${capitalize(objectMetadataItem.labelSingular)}` instead of static
`Create new record`.
- Supports `capitalize` and `lowercase` transform functions within
template expressions.
This commit is contained in:
+316
@@ -0,0 +1,316 @@
|
||||
import {
|
||||
CommandMenuContextApiPageType,
|
||||
type CommandMenuContextApi,
|
||||
} from '@/types';
|
||||
import { interpolateCommandMenuItemLabel } from '../interpolateCommandMenuItemLabel';
|
||||
|
||||
const buildContext = (
|
||||
overrides: Partial<CommandMenuContextApi> = {},
|
||||
): CommandMenuContextApi => ({
|
||||
pageType: CommandMenuContextApiPageType.INDEX_PAGE,
|
||||
isInSidePanel: false,
|
||||
isPageInEditMode: false,
|
||||
favoriteRecordIds: [],
|
||||
isSelectAll: false,
|
||||
hasAnySoftDeleteFilterOnView: false,
|
||||
numberOfSelectedRecords: 0,
|
||||
objectPermissions: {
|
||||
objectMetadataId: '',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
rowLevelPermissionPredicates: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
},
|
||||
selectedRecords: [],
|
||||
featureFlags: {},
|
||||
targetObjectReadPermissions: {},
|
||||
targetObjectWritePermissions: {},
|
||||
objectMetadataItem: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('interpolateCommandMenuItemLabel', () => {
|
||||
describe('sequential invocations (global regex lastIndex)', () => {
|
||||
it('should interpolate correctly when called multiple times in sequence', () => {
|
||||
const context = buildContext({
|
||||
numberOfSelectedRecords: 2,
|
||||
objectMetadataItem: { labelPlural: 'people' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'First: ${numberOfSelectedRecords}',
|
||||
context,
|
||||
}),
|
||||
).toBe('First: 2');
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Second: ${objectMetadataItem.labelPlural}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Second: people');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plain strings without template variables', () => {
|
||||
it('should return the label unchanged when no template variables are present', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({ label: 'Delete', context }),
|
||||
).toBe('Delete');
|
||||
});
|
||||
|
||||
it('should return null when label is null', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({ label: null, context }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when label is undefined', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({ label: undefined, context }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return an empty string for an empty label', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(interpolateCommandMenuItemLabel({ label: '', context })).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('simple template variable interpolation', () => {
|
||||
it('should interpolate a top-level context property', () => {
|
||||
const context = buildContext({ numberOfSelectedRecords: 5 });
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Selected: ${numberOfSelectedRecords}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Selected: 5');
|
||||
});
|
||||
|
||||
it('should interpolate objectMetadataItem.labelSingular', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelSingular: 'person' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Create new ${objectMetadataItem.labelSingular}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Create new person');
|
||||
});
|
||||
|
||||
it('should interpolate objectMetadataItem.labelPlural', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelPlural: 'companies' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Delete ${objectMetadataItem.labelPlural}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Delete companies');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple template variables', () => {
|
||||
it('should interpolate multiple variables in one label', () => {
|
||||
const context = buildContext({
|
||||
numberOfSelectedRecords: 3,
|
||||
objectMetadataItem: { labelPlural: 'people' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label:
|
||||
'${numberOfSelectedRecords} ${objectMetadataItem.labelPlural} selected',
|
||||
context,
|
||||
}),
|
||||
).toBe('3 people selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested property access', () => {
|
||||
it('should resolve deeply nested properties', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: {
|
||||
labelSingular: 'opportunity',
|
||||
nameSingular: 'opportunity',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'New ${objectMetadataItem.nameSingular}',
|
||||
context,
|
||||
}),
|
||||
).toBe('New opportunity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('missing context values', () => {
|
||||
it('should return empty string for undefined nested property', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: {},
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'New ${objectMetadataItem.labelSingular}',
|
||||
context,
|
||||
}),
|
||||
).toBe('New ');
|
||||
});
|
||||
|
||||
it('should return empty string for empty objectMetadataItem', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Create new ${objectMetadataItem.labelSingular}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Create new ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unresolvable property paths', () => {
|
||||
it('should return empty string for a path that cannot be resolved', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Label ${nonExistent.deep.path}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Label ');
|
||||
});
|
||||
|
||||
it('should not resolve inherited Object.prototype members', () => {
|
||||
const context = buildContext();
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Val: ${toString}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Val: ');
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Val: ${objectMetadataItem.hasOwnProperty}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Val: ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean context values', () => {
|
||||
it('should stringify boolean values', () => {
|
||||
const context = buildContext({ isInSidePanel: true });
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Side panel: ${isInSidePanel}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Side panel: true');
|
||||
});
|
||||
|
||||
it('should pass through string values as-is without forced lowercasing', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelSingular: 'Person' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Create new ${objectMetadataItem.labelSingular}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Create new Person');
|
||||
});
|
||||
});
|
||||
|
||||
describe('capitalize transform', () => {
|
||||
it('should capitalize the first character of a resolved value', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelSingular: 'person' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: '${capitalize(objectMetadataItem.labelSingular)} details',
|
||||
context,
|
||||
}),
|
||||
).toBe('Person details');
|
||||
});
|
||||
|
||||
it('should capitalize a mixed-case value', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelPlural: 'companyRecords' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: '${capitalize(objectMetadataItem.labelPlural)} selected',
|
||||
context,
|
||||
}),
|
||||
).toBe('CompanyRecords selected');
|
||||
});
|
||||
|
||||
it('should return empty string when the nested property is undefined', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: {},
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: '${capitalize(objectMetadataItem.labelSingular)} details',
|
||||
context,
|
||||
}),
|
||||
).toBe(' details');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lowercase transform', () => {
|
||||
it('should lowercase the resolved value', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelSingular: 'Person' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Create ${lowercase(objectMetadataItem.labelSingular)}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Create person');
|
||||
});
|
||||
|
||||
it('should lowercase all characters', () => {
|
||||
const context = buildContext({
|
||||
objectMetadataItem: { labelPlural: 'People' },
|
||||
});
|
||||
|
||||
expect(
|
||||
interpolateCommandMenuItemLabel({
|
||||
label: 'Delete ${lowercase(objectMetadataItem.labelPlural)}',
|
||||
context,
|
||||
}),
|
||||
).toBe('Delete people');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { Parser } from 'expr-eval-fork';
|
||||
|
||||
import { isDefined } from '../validation/isDefined';
|
||||
|
||||
import { safeGetNestedProperty } from './safeGetNestedProperty';
|
||||
|
||||
type ArrayMethod = 'every' | 'some';
|
||||
|
||||
const createArrayPropCheck = (
|
||||
method: ArrayMethod,
|
||||
predicate: (value: unknown) => boolean,
|
||||
) => {
|
||||
return (array: unknown, prop: string) => {
|
||||
if (!isNonEmptyArray(array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array[method]((item) =>
|
||||
predicate(safeGetNestedProperty(item, prop)),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const createArrayPropValueCheck = (
|
||||
method: ArrayMethod,
|
||||
predicate: (value: unknown, target: unknown) => boolean,
|
||||
) => {
|
||||
return (array: unknown, prop: string, value: unknown) => {
|
||||
if (!isNonEmptyArray(array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array[method]((item) =>
|
||||
predicate(safeGetNestedProperty(item, prop), value),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const conditionalAvailabilityParser = new Parser();
|
||||
|
||||
conditionalAvailabilityParser.functions.isDefined = (value: unknown) =>
|
||||
isDefined(value);
|
||||
|
||||
conditionalAvailabilityParser.functions.isNonEmptyString = (value: unknown) =>
|
||||
isNonEmptyString(value);
|
||||
|
||||
conditionalAvailabilityParser.functions.includes = (
|
||||
array: unknown,
|
||||
value: unknown,
|
||||
) => Array.isArray(array) && array.includes(value);
|
||||
|
||||
conditionalAvailabilityParser.functions.arrayLength = (value: unknown) =>
|
||||
Array.isArray(value) ? value.length : 0;
|
||||
|
||||
conditionalAvailabilityParser.functions.every = createArrayPropCheck(
|
||||
'every',
|
||||
Boolean,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.everyDefined = createArrayPropCheck(
|
||||
'every',
|
||||
isDefined,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.some = createArrayPropCheck(
|
||||
'some',
|
||||
Boolean,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.someDefined = createArrayPropCheck(
|
||||
'some',
|
||||
isDefined,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.someNonEmptyString =
|
||||
createArrayPropCheck('some', isNonEmptyString);
|
||||
|
||||
conditionalAvailabilityParser.functions.none = createArrayPropCheck(
|
||||
'every',
|
||||
(value) => !Boolean(value),
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.noneDefined = createArrayPropCheck(
|
||||
'every',
|
||||
(value) => !isDefined(value),
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.everyEquals = createArrayPropValueCheck(
|
||||
'every',
|
||||
(a, b) => a === b,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.someEquals = createArrayPropValueCheck(
|
||||
'some',
|
||||
(a, b) => a === b,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.noneEquals = createArrayPropValueCheck(
|
||||
'every',
|
||||
(a, b) => a !== b,
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.includesEvery =
|
||||
createArrayPropValueCheck(
|
||||
'every',
|
||||
(array, value) => Array.isArray(array) && array.includes(value),
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.includesSome =
|
||||
createArrayPropValueCheck(
|
||||
'some',
|
||||
(array, value) => Array.isArray(array) && array.includes(value),
|
||||
);
|
||||
|
||||
conditionalAvailabilityParser.functions.includesNone =
|
||||
createArrayPropValueCheck(
|
||||
'every',
|
||||
(array, value) => Array.isArray(array) && !array.includes(value),
|
||||
);
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type EvaluationContext } from 'expr-eval-fork';
|
||||
|
||||
import { conditionalAvailabilityParser } from './conditionalAvailabilityParser';
|
||||
|
||||
export const evaluateConditionalAvailabilityExpression = (
|
||||
expression: string | null | undefined,
|
||||
context: EvaluationContext,
|
||||
): boolean => {
|
||||
if (!isNonEmptyString(expression)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = conditionalAvailabilityParser.parse(expression);
|
||||
|
||||
return parsed.evaluate(context) === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { type Nullable } from '@/types';
|
||||
|
||||
import { capitalize } from '../strings/capitalize';
|
||||
import { isDefined } from '../validation/isDefined';
|
||||
|
||||
import { safeGetNestedProperty } from './safeGetNestedProperty';
|
||||
|
||||
const TEMPLATE_VARIABLE_REGEX = /\$\{([^{}]+)\}/g;
|
||||
const HAS_TEMPLATE_VARIABLE_REGEX = /\$\{[^{}]+\}/;
|
||||
const TRANSFORM_FUNCTION_CALL_REGEX = /^(\w+)\((.+)\)$/;
|
||||
|
||||
const LABEL_TRANSFORM_FUNCTIONS: Record<string, (value: string) => string> = {
|
||||
capitalize,
|
||||
lowercase: (value: string) => value.toLowerCase(),
|
||||
};
|
||||
|
||||
const resolveTemplateExpression = ({
|
||||
expression,
|
||||
context,
|
||||
}: {
|
||||
expression: string;
|
||||
context: Record<string, unknown>;
|
||||
}): string => {
|
||||
const trimmedExpression = expression.trim();
|
||||
const transformFunctionMatch = trimmedExpression.match(
|
||||
TRANSFORM_FUNCTION_CALL_REGEX,
|
||||
);
|
||||
|
||||
const expressionToEvaluate = transformFunctionMatch
|
||||
? transformFunctionMatch[2].trim()
|
||||
: trimmedExpression;
|
||||
|
||||
const transformFunction = transformFunctionMatch
|
||||
? LABEL_TRANSFORM_FUNCTIONS[transformFunctionMatch[1]]
|
||||
: undefined;
|
||||
|
||||
const resolvedPropertyValue = safeGetNestedProperty(
|
||||
context,
|
||||
expressionToEvaluate,
|
||||
);
|
||||
|
||||
if (!isDefined(resolvedPropertyValue)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const stringValue = String(resolvedPropertyValue);
|
||||
|
||||
return isDefined(transformFunction)
|
||||
? transformFunction(stringValue)
|
||||
: stringValue;
|
||||
};
|
||||
|
||||
export const interpolateCommandMenuItemLabel = ({
|
||||
label,
|
||||
context,
|
||||
}: {
|
||||
label: Nullable<string>;
|
||||
context: Record<string, unknown>;
|
||||
}): Nullable<string> => {
|
||||
if (!isDefined(label)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!HAS_TEMPLATE_VARIABLE_REGEX.test(label)) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return label.replace(TEMPLATE_VARIABLE_REGEX, (match, expression: string) => {
|
||||
try {
|
||||
return resolveTemplateExpression({ expression, context });
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { isObject, isString } from '@sniptt/guards';
|
||||
|
||||
import { isDefined } from '../validation/isDefined';
|
||||
|
||||
const BLOCKED_PROPERTY_NAMES = new Set([
|
||||
'__proto__',
|
||||
'constructor',
|
||||
'prototype',
|
||||
]);
|
||||
|
||||
export const safeGetNestedProperty = (
|
||||
objectToEvaluate: unknown,
|
||||
path: string,
|
||||
): unknown => {
|
||||
if (!isString(path)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parts = path.split('.');
|
||||
|
||||
let currentObject: unknown = objectToEvaluate;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!isDefined(currentObject) || !isObject(currentObject)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
BLOCKED_PROPERTY_NAMES.has(part) ||
|
||||
!Object.prototype.hasOwnProperty.call(currentObject, part)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentObject = (currentObject as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
return currentObject;
|
||||
};
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
import {
|
||||
isNonEmptyArray,
|
||||
isNonEmptyString,
|
||||
isObject,
|
||||
isString,
|
||||
} from '@sniptt/guards';
|
||||
import { type EvaluationContext, Parser } from 'expr-eval-fork';
|
||||
|
||||
import { isDefined } from '../validation/isDefined';
|
||||
|
||||
const BLOCKED_PROPERTY_NAMES = new Set([
|
||||
'__proto__',
|
||||
'constructor',
|
||||
'prototype',
|
||||
]);
|
||||
|
||||
const safeGetNestedProperty = (
|
||||
objectToEvaluate: unknown,
|
||||
path: string,
|
||||
): unknown => {
|
||||
if (!isString(path)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parts = path.split('.');
|
||||
|
||||
let currentObject: unknown = objectToEvaluate;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!isDefined(currentObject) || !isObject(currentObject)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (BLOCKED_PROPERTY_NAMES.has(part)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentObject = (currentObject as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
return currentObject;
|
||||
};
|
||||
|
||||
type ArrayMethod = 'every' | 'some';
|
||||
|
||||
const createArrayPropCheck = (
|
||||
method: ArrayMethod,
|
||||
predicate: (value: unknown) => boolean,
|
||||
) => {
|
||||
return (array: unknown, prop: string) => {
|
||||
if (!isNonEmptyArray(array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array[method]((item) =>
|
||||
predicate(safeGetNestedProperty(item, prop)),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const createArrayPropValueCheck = (
|
||||
method: ArrayMethod,
|
||||
predicate: (value: unknown, target: unknown) => boolean,
|
||||
) => {
|
||||
return (array: unknown, prop: string, value: unknown) => {
|
||||
if (!isNonEmptyArray(array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array[method]((item) =>
|
||||
predicate(safeGetNestedProperty(item, prop), value),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
parser.functions.isDefined = (value: unknown) => isDefined(value);
|
||||
parser.functions.isNonEmptyString = (value: unknown) => isNonEmptyString(value);
|
||||
parser.functions.includes = (array: unknown, value: unknown) =>
|
||||
Array.isArray(array) && array.includes(value);
|
||||
parser.functions.arrayLength = (value: unknown) =>
|
||||
Array.isArray(value) ? value.length : 0;
|
||||
|
||||
parser.functions.every = createArrayPropCheck('every', Boolean);
|
||||
parser.functions.everyDefined = createArrayPropCheck('every', isDefined);
|
||||
parser.functions.some = createArrayPropCheck('some', Boolean);
|
||||
parser.functions.someDefined = createArrayPropCheck('some', isDefined);
|
||||
parser.functions.someNonEmptyString = createArrayPropCheck(
|
||||
'some',
|
||||
isNonEmptyString,
|
||||
);
|
||||
parser.functions.none = createArrayPropCheck(
|
||||
'every',
|
||||
(value) => !Boolean(value),
|
||||
);
|
||||
parser.functions.noneDefined = createArrayPropCheck(
|
||||
'every',
|
||||
(value) => !isDefined(value),
|
||||
);
|
||||
|
||||
parser.functions.everyEquals = createArrayPropValueCheck(
|
||||
'every',
|
||||
(a, b) => a === b,
|
||||
);
|
||||
parser.functions.someEquals = createArrayPropValueCheck(
|
||||
'some',
|
||||
(a, b) => a === b,
|
||||
);
|
||||
parser.functions.noneEquals = createArrayPropValueCheck(
|
||||
'every',
|
||||
(a, b) => a !== b,
|
||||
);
|
||||
|
||||
parser.functions.includesEvery = createArrayPropValueCheck(
|
||||
'every',
|
||||
(array, value) => Array.isArray(array) && array.includes(value),
|
||||
);
|
||||
parser.functions.includesSome = createArrayPropValueCheck(
|
||||
'some',
|
||||
(array, value) => Array.isArray(array) && array.includes(value),
|
||||
);
|
||||
parser.functions.includesNone = createArrayPropValueCheck(
|
||||
'every',
|
||||
(array, value) => Array.isArray(array) && !array.includes(value),
|
||||
);
|
||||
|
||||
export const evaluateConditionalAvailabilityExpression = (
|
||||
expression: string | null | undefined,
|
||||
context: EvaluationContext,
|
||||
): boolean => {
|
||||
if (!isNonEmptyString(expression)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = parser.parse(expression);
|
||||
|
||||
return parsed.evaluate(context) === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -23,8 +23,11 @@ export { upsertIntoArrayOfObjectsComparingId } from './array/upsertIntoArrayOfOb
|
||||
export { upsertPropertiesOfItemIntoArrayOfObjectsComparingId } from './array/upsertPropertiesOfItemIntoArrayOfObjectsComparingId';
|
||||
export { assertUnreachable } from './assertUnreachable';
|
||||
export { base64UrlEncode } from './base64UrlEncode';
|
||||
export { conditionalAvailabilityParser } from './command-menu-items/conditionalAvailabilityParser';
|
||||
export { evaluateConditionalAvailabilityExpression } from './command-menu-items/evaluateConditionalAvailabilityExpression';
|
||||
export { interpolateCommandMenuItemLabel } from './command-menu-items/interpolateCommandMenuItemLabel';
|
||||
export { safeGetNestedProperty } from './command-menu-items/safeGetNestedProperty';
|
||||
export { computeDiffBetweenObjects } from './compute-diff-between-objects';
|
||||
export { evaluateConditionalAvailabilityExpression } from './conditional-availability/evaluateConditionalAvailabilityExpression';
|
||||
export { isPlainDateAfter } from './date/isPlainDateAfter';
|
||||
export { isPlainDateBefore } from './date/isPlainDateBefore';
|
||||
export { isPlainDateBeforeOrEqual } from './date/isPlainDateBeforeOrEqual';
|
||||
|
||||
Reference in New Issue
Block a user