Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Composer/packages/adaptive-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"dependencies": {
"@emotion/core": "^10.0.27",
"lodash": "^4.17.19",
"react-error-boundary": "^1.2.5"
"react-error-boundary": "^1.2.5",
"@bfc/built-in-functions": "*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ContextualMenu, DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu';
import React, { useCallback, useMemo } from 'react';
import { builtInFunctionsGrouping, getBuiltInFunctionInsertText } from '@bfc/built-in-functions';

import { expressionGroupingsToMenuItems } from './utils/expressionsListMenuUtils';

const componentMaxHeight = 400;

type ExpressionsListMenuProps = {
onExpressionSelected: (expression: string) => void;
onMenuMount: (menuContainerElms: HTMLDivElement[]) => void;
};
export const ExpressionsListMenu = (props: ExpressionsListMenuProps) => {
const { onExpressionSelected, onMenuMount } = props;

const containerRef = React.createRef<HTMLDivElement>();

const onExpressionKeySelected = useCallback(
(key) => {
const insertText = getBuiltInFunctionInsertText(key);
onExpressionSelected('= ' + insertText);
},
[onExpressionSelected]
);

const onLayerMounted = useCallback(() => {
const elms = document.querySelectorAll<HTMLDivElement>('.ms-ContextualMenu-Callout');
Comment thread
LouisEugeneMSFT marked this conversation as resolved.
onMenuMount(Array.prototype.slice.call(elms));
}, [onMenuMount]);

const menuItems = useMemo(
() =>
expressionGroupingsToMenuItems(
builtInFunctionsGrouping,
onExpressionKeySelected,
onLayerMounted,
componentMaxHeight
),
[onExpressionKeySelected, onLayerMounted]
);

return (
<div ref={containerRef}>
<ContextualMenu
calloutProps={{
onLayerMounted: onLayerMounted,
}}
directionalHint={DirectionalHint.bottomLeftEdge}
hidden={false}
items={menuItems}
shouldFocusOnMount={false}
styles={{
container: { maxHeight: componentMaxHeight },
}}
target={containerRef}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ContextualMenuItemType, IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';

type ExpressionGroupingType = {
key: string;
name: string;
children: string[];
};

export const expressionGroupingsToMenuItems = (
expressionGroupings: ExpressionGroupingType[],
menuItemSelectedHandler: (key: string) => void,
onLayerMounted: () => void,
maxHeight?: number
): IContextualMenuItem[] => {
const menuItems: IContextualMenuItem[] =
expressionGroupings?.map((grouping: ExpressionGroupingType) => {
return {
key: grouping.key,
text: grouping.name,
target: '_blank',
subMenuProps: {
calloutProps: { onLayerMounted: onLayerMounted },
items: grouping.children.map((key: string) => {
return {
key: key,
text: key,
onClick: () => menuItemSelectedHandler(key),
};
}),
styles: { container: { maxHeight } },
},
};
}) || [];

const header = {
key: 'header',
itemType: ContextualMenuItemType.Header,
text: 'Pre-built functions',
itemProps: { lang: 'en-us' },
};

menuItems.unshift(header);
Comment thread
LouisEugeneMSFT marked this conversation as resolved.

return menuItems;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

import { FieldProps } from '@bfc/extension-client';
import { Intellisense } from '@bfc/intellisense';
import React, { useRef } from 'react';
import React, { useRef, useState } from 'react';

import { getIntellisenseUrl } from '../../utils/getIntellisenseUrl';
import { ExpressionSwitchWindow } from '../ExpressionSwitchWindow';
import { ExpressionSwitchWindow } from '../expressions/ExpressionSwitchWindow';
import { ExpressionsListMenu } from '../expressions/ExpressionsListMenu';

import { JsonField } from './JsonField';
import { NumberField } from './NumberField';
Expand Down Expand Up @@ -58,8 +59,23 @@ export const IntellisenseExpressionField: React.FC<FieldProps<string>> = (props)
const scopes = ['expressions', 'user-variables'];
const intellisenseServerUrlRef = useRef(getIntellisenseUrl());

const [expressionsListContainerElements, setExpressionsListContainerElements] = useState<HTMLDivElement[]>([]);

const completionListOverrideResolver = (value: string) => {
return value === '=' ? (
<ExpressionsListMenu
onExpressionSelected={(expression: string) => onChange(expression)}
onMenuMount={(refs) => {
setExpressionsListContainerElements(refs);
}}
/>
) : null;
};

return (
<Intellisense
completionListOverrideContainerElements={expressionsListContainerElements}
completionListOverrideResolver={completionListOverrideResolver}
focused={defaultFocused}
id={`intellisense-${id}`}
scopes={scopes}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { CompletionElement } from './CompletionElement';
const styles = {
completionList: css`
position: absolute;
top: 32;
top: 32px;
left: 0;
max-height: 300px;
width: 100%;
Expand Down
11 changes: 10 additions & 1 deletion Composer/packages/intellisense/src/components/Intellisense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const Intellisense = React.memo(
id: string;
value?: any;
focused?: boolean;
completionListOverrideContainerElements?: HTMLDivElement[];
completionListOverrideResolver?: (value: any) => JSX.Element | null;
onChange: (newValue: string) => void;
onBlur?: (id: string) => void;
Expand All @@ -39,6 +40,7 @@ export const Intellisense = React.memo(
onChange,
onBlur,
children,
completionListOverrideContainerElements,
} = props;

const [textFieldValue, setTextFieldValue] = React.useState(value);
Expand Down Expand Up @@ -90,6 +92,13 @@ export const Intellisense = React.memo(
shouldBlur = false;
}

if (
completionListOverrideContainerElements &&
completionListOverrideContainerElements.some((item) => !checkIsOutside(x, y, item))
) {
shouldBlur = false;
}

if (shouldBlur) {
setShowCompletionList(false);
setCursorPosition(-1);
Expand All @@ -111,7 +120,7 @@ export const Intellisense = React.memo(
document.body.removeEventListener('click', outsideClickHandler);
document.body.removeEventListener('keydown', keydownHandler);
};
}, [focused, onBlur]);
}, [focused, onBlur, completionListOverrideContainerElements]);

// When textField value is changed
const onValueChanged = (newValue: string) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// This is the grouping of adaptive expression functions by type as seen on:
// https://docs.microsoft.com/en-us/azure/bot-service/adaptive-expressions/adaptive-expressions-prebuilt-functions?view=azure-bot-service-4.0

export const builtInFunctionsGrouping = [
Comment thread
LouisEugeneMSFT marked this conversation as resolved.
{
key: 'string',
name: 'String',
children: [
'length',
'replace',
'replaceIgnoreCase',
'split',
'substring',
'toLower',
'toUpper',
'trim',
'addOrdinal',
'endsWith',
'startsWith',
'countWord',
'concat',
'newGuid',
'indexOf',
'lastIndexOf',
'sentenceCase',
'titleCase',
],
},
{
key: 'collection',
name: 'Collection',
children: [
'contains',
'first',
'join',
'last',
'count',
'foreach',
'union',
'skip',
'take',
'intersection',
'subArray',
'select',
'where',
'sortBy',
'sortByDescending',
'indicesAndValues',
'flatten',
'unique',
],
},
{
key: 'logicalComparison',
name: 'Logical comparison',
children: [
'and',
'equals',
'empty',
'greater',
'greaterOrEquals',
'if',
'less',
'lessOrEquals',
'not',
'or',
'exists',
],
},
{
key: 'conversion',
name: 'Conversion',
children: [
'float',
'int',
'string',
'bool',
'createArray',
'json',
'base64',
'base64ToBinary',
'base64ToString',
'binary',
'dataUri',
'dataUriToBinary',
'dataUriToString',
'uriComponent',
'uriComponentToString',
'xml',
'formatNumber',
],
},
{
key: 'math',
name: 'Math',
children: [
'add',
'div',
'max',
'min',
'mod',
'mul',
'rand',
'sub',
'sum',
'range',
'exp',
'average',
'floor',
'ceiling',
'round',
],
},
{
key: 'dateAndTime',
name: 'Date and time',
children: [
'addDays',
'addHours',
'addMinutes',
'addSeconds',
'dayOfMonth',
'dayOfWeek',
'dayOfYear',
'formatDateTime',
'formatEpoch',
'formatTicks',
'subtractFromTime',
'utcNow',
'dateReadBack',
'month',
'date',
'year',
'getTimeOfDay',
'getFutureTime',
'getPastTime',
'addToTime',
'convertFromUTC',
'convertToUTC',
'startOfDay',
'startOfHour',
'startOfMonth',
'ticks',
'ticksToDays',
'ticksToHours',
'ticksToMinutes',
'dateTimeDiff',
'getPreviousViableDate',
'getNextViableDate',
'getPreviousViableTime',
'getNextViableTime',
],
},
{
key: 'timex',
name: 'Timex',
children: ['isPresent', 'isDuration', 'isTime', 'isDate', 'isTimeRange', 'isDateRange', 'isDefinite'],
},
{
key: 'uriParsing',
name: 'URI parsing',
children: ['uriHost', 'uriPath', 'uriPathAndQuery', 'uriPort', 'uriQuery', 'uriScheme'],
},
{
key: 'objectManipulationAndConstruction',
name: 'Object manipulation and construction',
children: [
'addProperty',
'removeProperty',
'setProperty',
'getProperty',
'coalesce',
'xPath',
'jPath',
'setPathToValue',
],
},
{
key: 'regularExpression',
name: 'Regular expression',
children: ['isMatch'],
},
{
key: 'typeChecking',
name: 'Type checking',
children: ['EOL', 'isInteger', 'isFloat', 'isBoolean', 'isArray', 'isObject', 'isDateTime', 'isString'],
},
];
Loading