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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
currentProjectIdState,
botDiagnosticsState,
botProjectIdsState,
formDialogSchemaIdsState,
} from '../../../src/recoilModel';
import mockProjectResponse from '../../../src/recoilModel/dispatchers/__tests__/mocks/mockProjectResponse.json';

Expand Down Expand Up @@ -95,6 +96,7 @@ const state = {
},
},
},
formDialogSchemas: [{ id: '1', content: '{}' }],
};

const initRecoilState = ({ set }) => {
Expand All @@ -106,6 +108,10 @@ const initRecoilState = ({ set }) => {
set(botDiagnosticsState(state.projectId), state.diagnostics);
set(settingsState(state.projectId), state.settings);
set(schemasState(state.projectId), mockProjectResponse.schemas);
set(
formDialogSchemaIdsState(state.projectId),
state.formDialogSchemas.map((fds) => fds.id)
);
};

describe('useNotification hooks', () => {
Expand Down
4 changes: 4 additions & 0 deletions Composer/packages/client/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
const getClientEnvironment = require('./env');
const paths = require('./paths');

new webpack.DefinePlugin({
'process.env.COMPOSER_ENABLE_FORMS': JSON.stringify(process.env.COMPOSER_ENABLE_FORMS),
});

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

Expand Down
3 changes: 2 additions & 1 deletion Composer/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@bfc/adaptive-form": "*",
"@bfc/code-editor": "*",
"@bfc/extension-client": "*",
"@bfc/form-dialogs": "*",
"@bfc/indexers": "*",
"@bfc/shared": "*",
"@bfc/ui-shared": "*",
Expand Down Expand Up @@ -51,13 +52,13 @@
"office-ui-fabric-react": "^7.121.11",
"prop-types": "^15.7.2",
"query-string": "^6.8.2",
"react-measure": "^2.3.0",
"re-resizable": "^6.3.2",
"react": "16.13.1",
"react-app-polyfill": "^0.2.1",
"react-dev-utils": "^7.0.3",
"react-dom": "16.13.1",
"react-frame-component": "^4.0.2",
"react-measure": "^2.3.0",
"react-timeago": "^4.4.0",
"recoil": "^0.0.13",
"styled-components": "^4.1.3",
Expand Down
6 changes: 4 additions & 2 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export const App: React.FC = () => {
loadLocale(appLocale);
}, [appLocale]);

const { fetchExtensions } = useRecoilValue(dispatcherState);
const { fetchExtensions, loadFormDialogSchemaTemplates } = useRecoilValue(dispatcherState);

useEffect(() => {
fetchExtensions();
});
loadFormDialogSchemaTemplates();
}, []);

return (
<Fragment key={appLocale}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { DialogTypes, DialogWrapper } from '@bfc/ui-shared';
import formatMessage from 'format-message';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import React, { useCallback } from 'react';
import { useRecoilValue } from 'recoil';

import { nameRegex } from '../../constants';
import { FieldConfig, useForm } from '../../hooks/useForm';
import { dialogsState } from '../../recoilModel';

type FormDialogSchemaFormData = {
name: string;
};

type Props = {
projectId: string;
isOpen: boolean;
onSubmit: (formDialogName: string) => void;
onDismiss: () => void;
};

const CreateFormDialogSchemaModal: React.FC<Props> = (props) => {
const { isOpen, projectId, onSubmit, onDismiss } = props;

const dialogs = useRecoilValue(dialogsState(projectId));

const formConfig: FieldConfig<FormDialogSchemaFormData> = {
name: {
required: true,
validate: (value) => {
if (!nameRegex.test(value)) {
return formatMessage('Spaces and special characters are not allowed. Use letters, numbers, -, or _.');
}
if (dialogs.some((dialog) => dialog.id === value)) {
return formatMessage('Dialog with the name: {value} already exists.', { value });
}
},
},
};

const { formData, formErrors, hasErrors, updateField } = useForm(formConfig);

const handleSubmit = useCallback(
(e) => {
e.preventDefault();
if (hasErrors) {
return;
}

onSubmit(formData.name);
},
[hasErrors, formData]
);

return (
<DialogWrapper
dialogType={DialogTypes.DesignFlow}
isOpen={isOpen}
subText={formatMessage('A form dialog enables your bot to collect pieces of information .')}
title={formatMessage('Create form dialog')}
onDismiss={onDismiss}
>
<form onSubmit={handleSubmit}>
<input style={{ display: 'none' }} type="submit" />
<Stack>
<TextField
required
errorMessage={formErrors.name}
label={formatMessage('Name')}
styles={name}
value={formData.name}
onChange={(_e, val) => updateField('name', val)}
/>
</Stack>

<DialogFooter>
<DefaultButton text={formatMessage('Cancel')} onClick={onDismiss} />
<PrimaryButton
disabled={hasErrors || formData.name === ''}
text={formatMessage('Create')}
onClick={handleSubmit}
/>
</DialogFooter>
</form>
</DialogWrapper>
);
};

export default CreateFormDialogSchemaModal;
160 changes: 160 additions & 0 deletions Composer/packages/client/src/pages/form-dialog/FormDialogPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import styled from '@emotion/styled';
import { navigate, RouteComponentProps } from '@reach/router';
import formatMessage from 'format-message';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { Text } from 'office-ui-fabric-react/lib/Text';
import * as React from 'react';
import { useRecoilValue } from 'recoil';

import { OpenConfirmModal } from '../../components/Modal/ConfirmDialog';
import { LeftRightSplit } from '../../components/Split/LeftRightSplit';
import {
dispatcherState,
formDialogGenerationProgressingState,
formDialogLibraryTemplatesState,
formDialogSchemaIdsState,
} from '../../recoilModel';

import CreateFormDialogSchemaModal from './CreateFormDialogSchemaModal';
import { FormDialogSchemaList } from './FormDialogSchemaList';
import { VisualFormDialogSchemaEditor } from './VisualFormDialogSchemaEditor';

const EmptyView = styled(Stack)({
width: '100%',
opacity: 0.5,
});

type Props = RouteComponentProps<{ projectId: string; schemaId: string }>;

const FormDialogPage: React.FC<Props> = React.memo((props: Props) => {
const { projectId = '', schemaId = '' } = props;
const formDialogSchemaIds = useRecoilValue(formDialogSchemaIdsState(projectId));
const formDialogLibraryTemplates = useRecoilValue(formDialogLibraryTemplatesState);
const formDialogGenerationProgressing = useRecoilValue(formDialogGenerationProgressingState);
const {
removeFormDialogSchema,
generateFormDialog,
createFormDialogSchema,
updateFormDialogSchema,
navigateToGeneratedDialog,
} = useRecoilValue(dispatcherState);

const { 0: createSchemaDialogOpen, 1: setCreateSchemaDialogOpen } = React.useState(false);

const availableTemplates = React.useMemo(
() => formDialogLibraryTemplates.filter((t) => !t.isGlobal).map((t) => t.name),
[formDialogLibraryTemplates]
);

const validSchemaId = React.useMemo(() => formDialogSchemaIds.includes(schemaId), [formDialogSchemaIds, schemaId]);

const createItemStart = React.useCallback(() => setCreateSchemaDialogOpen(true), [setCreateSchemaDialogOpen]);

const selectItem = React.useCallback((id: string) => {
navigate(`/bot/${projectId}/forms/${id}`);
}, []);

const deleteItem = React.useCallback(
async (id: string) => {
const res = await OpenConfirmModal(
formatMessage('Delete form dialog schema'),
formatMessage('Are you sure you want to remove form dialog schema "{id}"?', { id })
);
if (res) {
removeFormDialogSchema({ id, projectId });
if (schemaId === id) {
selectItem('');
}
}
},
[selectItem, removeFormDialogSchema, schemaId]
);

const generateDialog = React.useCallback(
(schemaId: string) => {
if (schemaId) {
generateFormDialog({ projectId, schemaId });
}
},
[generateFormDialog, projectId]
);

const viewDialog = React.useCallback(
(schemaId: string) => {
if (schemaId) {
navigateToGeneratedDialog({ projectId, schemaId });
}
},
[navigateToGeneratedDialog, projectId]
);

const updateItem = React.useCallback(
(id: string, content: string) => {
if (id === schemaId) {
updateFormDialogSchema({ id, content, projectId });
}
},
[updateFormDialogSchema, schemaId]
);

const createItem = React.useCallback(
(formDialogName: string) => {
createFormDialogSchema({ id: formDialogName, projectId });
setCreateSchemaDialogOpen(false);
},
[createFormDialogSchema, setCreateSchemaDialogOpen]
);

return (
<>
<Stack horizontal verticalFill>
<LeftRightSplit initialLeftGridWidth={320} minLeftPixels={320} minRightPixels={800}>
<FormDialogSchemaList
items={formDialogSchemaIds}
loading={formDialogGenerationProgressing}
projectId={projectId}
selectedId={schemaId}
onCreateItem={createItemStart}
onDeleteItem={deleteItem}
onGenerate={generateDialog}
onSelectItem={selectItem}
onViewDialog={viewDialog}
/>
{validSchemaId ? (
<VisualFormDialogSchemaEditor
generationInProgress={formDialogGenerationProgressing}
projectId={projectId}
schemaId={schemaId}
templates={availableTemplates}
onChange={updateItem}
onGenerate={generateDialog}
/>
) : (
<EmptyView verticalFill horizontalAlign="center" verticalAlign="center">
<Text variant="large">
{schemaId
? formatMessage(`{schemaId} doesn't exists, select an schema to edit or create a new one`, {
schemaId,
})
: formatMessage('Select an schema to edit or create a new one')}
</Text>
</EmptyView>
)}
</LeftRightSplit>
</Stack>
{createSchemaDialogOpen ? (
<CreateFormDialogSchemaModal
isOpen={createSchemaDialogOpen}
projectId={projectId}
onDismiss={() => setCreateSchemaDialogOpen(false)}
onSubmit={createItem}
/>
) : null}
</>
);
});

export default FormDialogPage;
Loading