Skip to content

Commit

Permalink
Merge branch 'main' into lgerror
Browse files Browse the repository at this point in the history
  • Loading branch information
beyackle authored Mar 15, 2021
2 parents e760c96 + 833f786 commit 48e80ad
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('TextModalityEditor', () => {
it('should render the value if it is not a template reference', async () => {
const { findByText } = render(
<TextModalityEditor
focusOnMount={false}
lgOption={{ fileId: '', templateId: 'Activity' }}
removeModalityDisabled={false}
response={{ kind: 'Text', value: ['hello world'], valueType: 'direct' } as TextStructuredResponseItem}
Expand All @@ -30,6 +31,7 @@ describe('TextModalityEditor', () => {
it('should render items from template if the value is a template reference', async () => {
const { findByText, queryByText } = render(
<TextModalityEditor
focusOnMount={false}
lgOption={{ fileId: '', templateId: 'Activity' }}
lgTemplates={[{ name: 'Activity_text', body: '- variation1\n- variation2\n- variation3', parameters: [] }]}
removeModalityDisabled={false}
Expand Down
35 changes: 30 additions & 5 deletions Composer/packages/lib/code-editor/src/lg/hooks/useStringArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ArrayBasedStructuredResponseItem, PartialStructuredResponse } from '../
import { getTemplateId } from '../../utils/structuredResponse';
import { LGOption } from '../../utils/types';

const multiLineBlockSymbol = '```';

const getInitialItems = <T extends ArrayBasedStructuredResponseItem>(
response: T,
lgTemplates?: readonly LgTemplate[],
Expand All @@ -16,10 +18,30 @@ const getInitialItems = <T extends ArrayBasedStructuredResponseItem>(
const templateId = getTemplateId(response);
const template = lgTemplates?.find(({ name }) => name === templateId);
return response?.value && template?.body
? template?.body?.replace(/- /g, '').split(/\r?\n/g) || []
? template?.body
// Split by non-escaped -
// eslint-disable-next-line security/detect-unsafe-regex
?.split(/(?<!\\)- /g)
// Ignore empty or newline strings
.filter((s) => s !== '' && s !== '\n')
.map((s) => s.replace(/\r?\n$/g, ''))
// Remove LG template multiline block symbol
.map((s) => s.replace(/```/g, '')) || []
: response?.value || (focusOnMount ? [''] : []);
};

const fixMultilineItems = (items: string[]) => {
return items.map((item) => {
if (/\r?\n/g.test(item)) {
// Escape all un-escaped -
// eslint-disable-next-line security/detect-unsafe-regex
return `${multiLineBlockSymbol}${item.replace(/(?<!\\)-/g, '\\-')}${multiLineBlockSymbol}`;
}

return item;
});
};

export const useStringArray = <T extends ArrayBasedStructuredResponseItem>(
kind: 'Text' | 'Speak',
structuredResponse: T,
Expand All @@ -45,18 +67,21 @@ export const useStringArray = <T extends ArrayBasedStructuredResponseItem>(
const onChange = React.useCallback(
(newItems: string[]) => {
setItems(newItems);
// Fix variations that are multiline
// If only one item but it's multiline, still use helper LG template
const fixedNewItems = fixMultilineItems(newItems);
const id = templateId || `${lgOption?.templateId}_${newTemplateNameSuffix}`;
if (!newItems.length) {
if (!fixedNewItems.length) {
setTemplateId(id);
onUpdateResponseTemplate({ [kind]: { kind, value: [], valueType: 'direct' } });
onRemoveTemplate(id);
} else if (newItems.length === 1 && lgOption?.templateId) {
onUpdateResponseTemplate({ [kind]: { kind, value: newItems, valueType: 'direct' } });
} else if (fixedNewItems.length === 1 && !/\r?\n/g.test(fixedNewItems[0]) && lgOption?.templateId) {
onUpdateResponseTemplate({ [kind]: { kind, value: fixedNewItems, valueType: 'direct' } });
onTemplateChange(id, '');
} else {
setTemplateId(id);
onUpdateResponseTemplate({ [kind]: { kind, value: [`\${${id}()}`], valueType: 'template' } });
onTemplateChange(id, newItems.map((item) => `- ${item}`).join('\n'));
onTemplateChange(id, fixedNewItems.map((item) => `- ${item}`).join('\n'));
}
},
[kind, newTemplateNameSuffix, lgOption, templateId, onRemoveTemplate, onTemplateChange, onUpdateResponseTemplate]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,27 @@ export const StringArrayEditor = React.memo(
useEffect(() => {
const keydownHandler = (e: KeyboardEvent) => {
if (submitKeys.includes(e.key)) {
setCalloutTargetElement(null);

const filteredItems = items.filter(Boolean);
// Allow multiline via shift+Enter
if (e.key === 'Enter' && e.shiftKey) {
return;
}

setCalloutTargetElement(null);
// Filter our empty or newline strings
const filteredItems = items.filter((s) => s !== '' && s !== '\n');
if (e.key === 'Enter' && containerRef.current?.contains(e.target as Node)) {
onChange([...filteredItems, '']);
setCurrentIndex(filteredItems.length);
// If the value is not filtered, go to the next entry
// Otherwise cancel editing
if (items.length === filteredItems.length) {
e.preventDefault();
onChange([...filteredItems, '']);
setCurrentIndex(filteredItems.length);
} else {
onChange(filteredItems);
setCurrentIndex(null);
}
} else {
setCurrentIndex(null);

// Remove empty variations only if necessary
if (items.length !== filteredItems.length) {
onChange(filteredItems);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const TextViewItem = React.memo(
onFocus={focus}
>
<Text styles={displayTextStyles} variant="small">
{onRenderDisplayText?.() ?? value}
{onRenderDisplayText?.() ?? value.replace(/\r?\n/g, '↵')}
</Text>
</Stack>
<RemoveIcon className={removeIconClassName} iconProps={{ iconName: 'Trash' }} tabIndex={-1} onClick={remove} />
Expand Down Expand Up @@ -197,18 +197,6 @@ const TextFieldItem = React.memo(({ value, onShowCallout, onChange }: TextFieldI
[onShowCallout]
);

React.useEffect(() => {
if (inputRef.current && inputRef.current.value !== value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')
?.set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(inputRef.current, value);
const inputEvent = new Event('input', { bubbles: true });
inputRef.current.dispatchEvent(inputEvent);
}
}
}, [value]);

return (
<div ref={containerRef}>
<Input
Expand All @@ -218,6 +206,7 @@ const TextFieldItem = React.memo(({ value, onShowCallout, onChange }: TextFieldI
defaultValue={value}
resizable={false}
styles={textFieldStyles}
value={value}
onChange={onChange}
onClick={click}
onFocus={focus}
Expand Down

0 comments on commit 48e80ad

Please sign in to comment.