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 all 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/ui-plugins/select-dialog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-dom": "16.13.1"
},
"dependencies": {
"@emotion/core": "^10.0.27"
"@emotion/core": "^10.0.27",
"ajv": "7.2.3"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@
import React from 'react';
import styled from '@emotion/styled';
import { FieldProps, JSONSchema7, useShellApi } from '@bfc/extension-client';
import { FieldLabel, JsonField, SchemaField, IntellisenseTextField, WithTypeIcons } from '@bfc/adaptive-form';
import { FieldLabel, IntellisenseTextField, OpenObjectField, WithTypeIcons, SchemaField } from '@bfc/adaptive-form';
import Stack from 'office-ui-fabric-react/lib/components/Stack/Stack';
import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme';
import formatMessage from 'format-message';
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import Ajv, { AnySchemaObject } from 'ajv';

const loadSchema = async (uri: string) => {
const res = await fetch(uri);
return res.body as AnySchemaObject;
};

const ajv = new Ajv({
loadSchema,
strict: false,
});

const IntellisenseTextFieldWithIcon = WithTypeIcons(IntellisenseTextField);

Expand Down Expand Up @@ -42,16 +53,22 @@ const styles = {

const dropdownCalloutProps = { styles: { root: { minWidth: 140 } } };

const getInitialSelectedKey = (value?: string | Record<string, unknown>, schema?: JSONSchema7): string => {
if (typeof value !== 'string' && schema) {
const getSelectedKey = (
value?: string | Record<string, unknown>,
schema?: JSONSchema7,
validSchema = false
): string => {
if (typeof value !== 'string' && schema && validSchema) {
return 'form';
} else if (typeof value !== 'string' && !schema) {
return 'code';
} else if (typeof value !== 'string' && (!schema || !validSchema)) {
return 'object';
} else {
return 'expression';
}
};

type JSONValidationStatus = 'valid' | 'inValid' | 'validating';

const DialogOptionsField: React.FC<FieldProps> = ({
description,
uiOptions,
Expand All @@ -68,11 +85,39 @@ const DialogOptionsField: React.FC<FieldProps> = ({
[dialog, dialogSchemas]
);

const [selectedKey, setSelectedKey] = React.useState<string>(getInitialSelectedKey(options, schema));
const [selectedKey, setSelectedKey] = React.useState<string>();
const [validationStatus, setValidationStatus] = React.useState<JSONValidationStatus>('validating');

React.useLayoutEffect(() => {
setSelectedKey(getInitialSelectedKey(options, schema));
}, [dialog]);
const mountRef = React.useRef(false);

React.useEffect(() => {
mountRef.current = true;
if (schema && Object.keys(schema.properties || {}).length) {
(async () => {
setValidationStatus('validating');
try {
const validate = await ajv.compileAsync(schema, true);
const valid = validate(schema);

if (mountRef.current) {
setValidationStatus(valid ? 'valid' : 'inValid');
setSelectedKey(getSelectedKey(options, schema, true));
}
} catch (error) {
if (mountRef.current) {
setValidationStatus('inValid');
setSelectedKey(getSelectedKey(options, schema, false));
}
}
})();
} else {
setSelectedKey(getSelectedKey(options, schema, false));
}

return () => {
mountRef.current = false;
};
}, [schema]);

const change = React.useCallback(
(newOptions?: string | Record<string, any>) => {
Expand All @@ -84,38 +129,43 @@ const DialogOptionsField: React.FC<FieldProps> = ({
const onDropdownChange = React.useCallback(
(_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => {
if (option) {
setSelectedKey(option.key as string);
if (option.key === 'expression') {
// When the user switched between data types - either a string (expression) or an object (form or object) - we need to set
// options to undefined so we don't incorrectly pass a string to an object editor or pass an object to a string editor.

// If selectedKey is currently set to expression and the user is switching to form or object, set the value to undefined.
// If the user is switching to expression meaning the selectedKey is currently set to form or form, set the value to undefined.
if (option.key === 'expression' || selectedKey === 'expression') {
change();
}
setSelectedKey(option.key as string);
}
},
[change]
[change, selectedKey]
);

const typeOptions = React.useMemo<IDropdownOption[]>(() => {
return [
{
key: 'form',
text: formatMessage('form'),
disabled: !schema || !Object.keys(schema).length,
disabled: !schema || validationStatus !== 'valid',
},
{
key: 'code',
text: formatMessage('code editor'),
key: 'object',
text: formatMessage('object'),
},
{
key: 'expression',
text: 'expression',
},
];
}, [schema]);
}, [schema, validationStatus]);

let Field = IntellisenseTextFieldWithIcon;
if (selectedKey === 'form') {
Field = SchemaField;
} else if (selectedKey === 'code') {
Field = JsonField;
} else if (selectedKey === 'object') {
Field = OpenObjectField;
}

return (
Expand Down Expand Up @@ -144,9 +194,9 @@ const DialogOptionsField: React.FC<FieldProps> = ({
id={`${id}.options`}
label={false}
name={'options'}
schema={schema || {}}
schema={(selectedKey === 'form' ? schema : { type: 'object', additionalProperties: true }) || {}}
uiOptions={{}}
value={options || selectedKey === 'expression' ? '' : {}}
value={options}
onChange={change}
/>
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,28 @@ import { EditorExtension } from '@bfc/extension-client';
import { DialogOptionsField } from '../DialogOptionsField';

jest.mock('@bfc/adaptive-form', () => {
const AdaptiveForm = jest.requireActual('@bfc/adaptive-form');
const MockAdaptiveForm = jest.requireActual('@bfc/adaptive-form');

return {
...AdaptiveForm,
JsonField: () => <div>Json Field</div>,
...MockAdaptiveForm,
OpenObjectField: () => <div>Object Field</div>,
SchemaField: () => <div>Options Form</div>,
IntellisenseTextField: () => <div>Intellisense Text Field</div>,
};
});

jest.mock('office-ui-fabric-react/lib/Dropdown', () => {
const Dropdown = jest.requireActual('office-ui-fabric-react/lib/Dropdown');
const MockDropdown = jest.requireActual('office-ui-fabric-react/lib/Dropdown');

return {
...Dropdown,
...MockDropdown,
Dropdown: ({ onChange }) => (
<button
onClick={(e) => {
onChange(e, { key: 'code' });
onChange(e, { key: 'object' });
}}
>
Switch to Json Field
Switch to Object Field
</button>
),
};
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('DialogOptionsField', () => {
});
it('should render the JsonField if the dialog schema is undefined and options is not a string', async () => {
const { findByText } = renderDialogOptionsField({ value: { dialog: 'dialog2', options: {} } });
await findByText('Json Field');
await findByText('Object Field');
});
it('should render the IntellisenseTextField if options is a string', async () => {
const { findByText } = renderDialogOptionsField({ value: { dialog: 'dialog2', options: '=user.data' } });
Expand All @@ -102,10 +102,10 @@ describe('DialogOptionsField', () => {
// Should initially render Options Form
await findByText('Options Form');

// Switch to Json field
const button = await findByText('Switch to Json Field');
// Switch to Object field
const button = await findByText('Switch to Object Field');
fireEvent.click(button);

await findByText('Json Field');
await findByText('Object Field');
});
});
17 changes: 16 additions & 1 deletion Composer/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6027,6 +6027,16 @@ ajv-keywords@^3.4.1:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==

ajv@7.2.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.3.tgz#ca78d1cf458d7d36d1c3fa0794dd143406db5772"
integrity sha512-idv5WZvKVXDqKralOImQgPM9v6WOdLNa0IY3B3doOjw/YxRGT8I+allIJ6kd7Uaj+SF1xZUSU+nPM5aDNBVtnw==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"

ajv@^4.7.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
Expand Down Expand Up @@ -14995,6 +15005,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==

json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==

json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
Expand Down Expand Up @@ -19933,7 +19948,7 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=

require-from-string@^2.0.1:
require-from-string@^2.0.1, require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
Expand Down