[FRONT COMPONENTS] Introduce conditionalAvailabilityExpression to command menu items (#18319)
## PR Description - Uses `expr-eval` to enable front components (SDK plugins) to define conditional availability as declarative expressions. - Moves shared types and constants to `twenty-shared` - Introduces a `conditionalAvailabilityExpression` field on `CommandMenuItemEntity`, allowing command menu items to store an `expr-eval` compatible expression string that is evaluated against a CommandMenuContext to determine if the item should be shown. - Creates an esbuild transform plugin `conditional-availability-transform-plugin` in `twenty-sdk` that converts TypeScript conditional availability expressions into `expr-eval` compatible syntax at build time, so SDK developers can write natural TS expressions that get transformed to evaluable strings. - Removes deprecated `forceRegisteredActionsByKey` state and its usage. - Creates `useCommandMenuContext` hook that builds the full `CommandMenuContext` object from React state, which is then passed to `useCommandMenuItemFrontComponentActions` for evaluating conditional availability expressions.
This commit is contained in:
@@ -7,6 +7,7 @@ export type CommandMenuItemManifest = SyncableEntityOptions & {
|
||||
availabilityType?: 'GLOBAL' | 'SINGLE_RECORD' | 'BULK_RECORDS';
|
||||
availabilityObjectUniversalIdentifier?: string;
|
||||
frontComponentUniversalIdentifier: string;
|
||||
conditionalAvailabilityExpression?: string;
|
||||
};
|
||||
|
||||
export type FrontComponentCommandManifest = Omit<
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const BACKEND_BATCH_REQUEST_MAX_COUNT = 10000;
|
||||
@@ -7,6 +7,7 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { BACKEND_BATCH_REQUEST_MAX_COUNT } from './BackendBatchRequestMaxCount';
|
||||
export { CalendarStartDay } from './CalendarStartDay';
|
||||
export { COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES } from './CompositeFieldTypeSubFieldsNames';
|
||||
export { CurrencyCode } from './CurrencyCode';
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
declare module 'expr-eval' {
|
||||
export type Value =
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| Value[]
|
||||
| ((...args: Value[]) => Value)
|
||||
| { [propertyName: string]: Value };
|
||||
|
||||
export type EvaluationContext = Record<string, unknown>;
|
||||
|
||||
export interface Values {
|
||||
[propertyName: string]: Value;
|
||||
}
|
||||
|
||||
export interface ParserOptions {
|
||||
allowMemberAccess?: boolean;
|
||||
operators?: {
|
||||
add?: boolean;
|
||||
comparison?: boolean;
|
||||
concatenate?: boolean;
|
||||
conditional?: boolean;
|
||||
divide?: boolean;
|
||||
factorial?: boolean;
|
||||
logical?: boolean;
|
||||
multiply?: boolean;
|
||||
power?: boolean;
|
||||
remainder?: boolean;
|
||||
subtract?: boolean;
|
||||
sin?: boolean;
|
||||
cos?: boolean;
|
||||
tan?: boolean;
|
||||
asin?: boolean;
|
||||
acos?: boolean;
|
||||
atan?: boolean;
|
||||
sinh?: boolean;
|
||||
cosh?: boolean;
|
||||
tanh?: boolean;
|
||||
asinh?: boolean;
|
||||
acosh?: boolean;
|
||||
atanh?: boolean;
|
||||
sqrt?: boolean;
|
||||
log?: boolean;
|
||||
ln?: boolean;
|
||||
lg?: boolean;
|
||||
log10?: boolean;
|
||||
abs?: boolean;
|
||||
ceil?: boolean;
|
||||
floor?: boolean;
|
||||
round?: boolean;
|
||||
trunc?: boolean;
|
||||
exp?: boolean;
|
||||
length?: boolean;
|
||||
in?: boolean;
|
||||
random?: boolean;
|
||||
min?: boolean;
|
||||
max?: boolean;
|
||||
assignment?: boolean;
|
||||
fndef?: boolean;
|
||||
cbrt?: boolean;
|
||||
expm1?: boolean;
|
||||
log1p?: boolean;
|
||||
sign?: boolean;
|
||||
log2?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export class Parser {
|
||||
constructor(options?: ParserOptions);
|
||||
unaryOps: any;
|
||||
functions: any;
|
||||
consts: any;
|
||||
parse(expression: string): Expression;
|
||||
evaluate(expression: string, values?: EvaluationContext): number;
|
||||
static parse(expression: string): Expression;
|
||||
static evaluate(expression: string, values?: EvaluationContext): number;
|
||||
}
|
||||
|
||||
export interface Expression {
|
||||
simplify(values?: EvaluationContext): Expression;
|
||||
evaluate(values?: EvaluationContext): any;
|
||||
substitute(
|
||||
variable: string,
|
||||
value: Expression | string | number,
|
||||
): Expression;
|
||||
symbols(options?: { withMembers?: boolean }): string[];
|
||||
variables(options?: { withMembers?: boolean }): string[];
|
||||
toJSFunction(
|
||||
params: string,
|
||||
values?: EvaluationContext,
|
||||
): (...args: any[]) => number;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum ActionViewType {
|
||||
GLOBAL = 'GLOBAL',
|
||||
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
|
||||
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
|
||||
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',
|
||||
SHOW_PAGE = 'SHOW_PAGE',
|
||||
PAGE_EDIT_MODE = 'PAGE_EDIT_MODE',
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type ObjectPermissions } from './ObjectPermissions';
|
||||
import { type ObjectRecord } from './ObjectRecord';
|
||||
|
||||
export type CommandMenuContextApi = {
|
||||
isShowPage: boolean;
|
||||
isInRightDrawer: boolean;
|
||||
isFavorite: boolean;
|
||||
isRemote: boolean;
|
||||
isNoteOrTask: boolean;
|
||||
isSelectAll: boolean;
|
||||
hasAnySoftDeleteFilterOnView: boolean;
|
||||
numberOfSelectedRecords: number;
|
||||
objectPermissions: ObjectPermissions & { objectMetadataId: string };
|
||||
selectedRecord: ObjectRecord | undefined;
|
||||
featureFlags: Record<string, boolean>;
|
||||
targetObjectReadPermissions: Record<string, boolean>;
|
||||
targetObjectWritePermissions: Record<string, boolean>;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
export enum CoreObjectNameSingular {
|
||||
Activity = 'activity',
|
||||
ActivityTarget = 'activityTarget',
|
||||
ApiKey = 'apiKey',
|
||||
Attachment = 'attachment',
|
||||
Blocklist = 'blocklist',
|
||||
CalendarChannel = 'calendarChannel',
|
||||
CalendarEvent = 'calendarEvent',
|
||||
Comment = 'comment',
|
||||
Company = 'company',
|
||||
ConnectedAccount = 'connectedAccount',
|
||||
Dashboard = 'dashboard',
|
||||
TimelineActivity = 'timelineActivity',
|
||||
Favorite = 'favorite',
|
||||
FavoriteFolder = 'favoriteFolder',
|
||||
Message = 'message',
|
||||
MessageChannel = 'messageChannel',
|
||||
MessageParticipant = 'messageParticipant',
|
||||
MessageFolder = 'messageFolder',
|
||||
MessageThread = 'messageThread',
|
||||
Note = 'note',
|
||||
NoteTarget = 'noteTarget',
|
||||
Opportunity = 'opportunity',
|
||||
Person = 'person',
|
||||
Task = 'task',
|
||||
TaskTarget = 'taskTarget',
|
||||
Webhook = 'webhook',
|
||||
WorkspaceMember = 'workspaceMember',
|
||||
MessageThreadSubscriber = 'messageThreadSubscriber',
|
||||
Workflow = 'workflow',
|
||||
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
|
||||
WorkflowVersion = 'workflowVersion',
|
||||
WorkflowRun = 'workflowRun',
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export enum FeatureFlagKey {
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED',
|
||||
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
|
||||
IS_CORE_PICTURE_MIGRATED = 'IS_CORE_PICTURE_MIGRATED',
|
||||
IS_OTHER_FILE_MIGRATED = 'IS_OTHER_FILE_MIGRATED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
|
||||
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
|
||||
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { ActionViewType } from './ActionViewType';
|
||||
export type { AllowedAddressSubField } from './AddressFieldsType';
|
||||
export { ALLOWED_ADDRESS_SUBFIELDS } from './AddressFieldsType';
|
||||
export { AggregateOperations } from './AggregateOperations';
|
||||
@@ -14,6 +15,7 @@ export { AppBasePath } from './AppBasePath';
|
||||
export { AppPath } from './AppPath';
|
||||
export type { Arrayable } from './Arrayable';
|
||||
export type { ArraySortDirection } from './ArraySortDirection';
|
||||
export type { CommandMenuContextApi } from './CommandMenuContextApi';
|
||||
export { CommandMenuPages } from './CommandMenuPages';
|
||||
export type { ActorMetadata } from './composite-types/actor.composite-type';
|
||||
export {
|
||||
@@ -52,6 +54,7 @@ export {
|
||||
export type { CompositeFieldSubFieldName } from './CompositeFieldSubFieldNameType';
|
||||
export type { ConfigVariableValue } from './ConfigVariableValue';
|
||||
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
|
||||
export { CoreObjectNameSingular } from './CoreObjectNameSingular';
|
||||
export { CrudOperationType } from './CrudOperationType';
|
||||
export type {
|
||||
SnackBarVariant,
|
||||
@@ -63,6 +66,7 @@ export type { ExcludeFunctions } from './ExcludeFunctions';
|
||||
export type { ExtractPropertiesThatEndsWithId } from './ExtractPropertiesThatEndsWithId';
|
||||
export type { ExtractPropertiesThatEndsWithIds } from './ExtractPropertiesThatEndsWithIds';
|
||||
export type { ExtractSerializedRelationProperties } from './ExtractSerializedRelationProperties.type';
|
||||
export { FeatureFlagKey } from './FeatureFlagKey';
|
||||
export type {
|
||||
FieldMetadataDefaultValueFunctionNames,
|
||||
FieldMetadataDefaultValueUuidFunction,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type EvaluationContext, Parser } from 'expr-eval';
|
||||
|
||||
import { isDefined } from '../validation/isDefined';
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
parser.functions.isDefined = (value: unknown) => isDefined(value);
|
||||
parser.functions.isNonEmptyString = (value: unknown) => isNonEmptyString(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,6 +23,7 @@ export { upsertPropertiesOfItemIntoArrayOfObjectsComparingId } from './array/ups
|
||||
export { assertUnreachable } from './assertUnreachable';
|
||||
export { base64UrlEncode } from './base64UrlEncode';
|
||||
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