Skip to content

Commit

Permalink
feat: Add dynamic schema for build forms in form dialogs (#8046)
Browse files Browse the repository at this point in the history
* form dialog generators

* revert

* strings

* test

* chore: Update to the latest generator (#7664)

* Update to .template and new generate call.

* Update latest generator

Co-authored-by: Chris McConnell <chrimc>

* Remove debugger

* Make .template optional in $ref.

* Update to the npm version of bf-generate-library.

* #chore update to latest bf-generate-library

* Update to latest generator which fixes issues in
merge.

* Fix a bug in property grouping for forms.
Changed it so the first of:
* $designer.propertyGroups
* trigger.property
* action.expectedProperties
Is used for the property.
Enable multi-parenting and if not turned on
ensure the trigger does not disappear.

* strings

* yarn update

* lint issues

Co-authored-by: Soroush <sorgh@microsoft.com>
Co-authored-by: Chris McConnell <chrimc>
Co-authored-by: Chris McConnell <chrimc@microsoft.com>
Co-authored-by: Soroush <hatpick@fmail.com>
Co-authored-by: GeoffCoxMSFT <gcox@microsoft.com>
  • Loading branch information
5 people committed Jun 10, 2021
1 parent c01cd98 commit 38c521b
Show file tree
Hide file tree
Showing 47 changed files with 1,342 additions and 1,139 deletions.
2 changes: 1 addition & 1 deletion Composer/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
node_modules
dist
dist
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,10 @@ export const ProjectTree: React.FC<Props> = ({
const renderDialogTriggersByProperty = (dialog: DialogInfo, projectId: string, startDepth: number) => {
const jsonSchemaFiles = jsonSchemaFilesByProjectId[projectId];
const dialogSchemaProperties = extractSchemaProperties(dialog, jsonSchemaFiles);
const groupedTriggers = groupTriggersByPropertyReference(dialog, { validProperties: dialogSchemaProperties });
const groupedTriggers = groupTriggersByPropertyReference(dialog, {
validProperties: dialogSchemaProperties,
allowMultiParent: true,
});

const triggerGroups = Object.keys(groupedTriggers);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ const FormDialogPage: React.FC<Props> = React.memo((props: Props) => {
loadFormDialogSchemaTemplates();
}, []);

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { JsonEditor } from '@bfc/code-editor';
import { FormDialogSchemaEditor } from '@bfc/form-dialogs';
import { FileExtensions } from '@bfc/shared';
import { FileExtensions, FormDialogSchemaTemplate } from '@bfc/shared';
import styled from '@emotion/styled';
import { NeutralColors } from '@uifabric/fluent-theme';
import formatMessage from 'format-message';
Expand All @@ -13,7 +13,7 @@ import { classNamesFunction } from 'office-ui-fabric-react/lib/Utilities';
import * as React from 'react';
import { useRecoilValue } from 'recoil';

import { formDialogSchemaState } from '../../recoilModel';
import { formDialogSchemaState, localeState } from '../../recoilModel';
import TelemetryClient from '../../telemetry/TelemetryClient';

const Root = styled(Stack)<{
Expand Down Expand Up @@ -51,14 +51,15 @@ type Props = {
projectId: string;
schemaId: string;
generationInProgress?: boolean;
templates: string[];
templates: FormDialogSchemaTemplate[];
onChange: (id: string, content: string) => void;
onGenerate: (schemaId: string) => void;
};

export const VisualFormDialogSchemaEditor = React.memo((props: Props) => {
const { projectId, schemaId, templates, onChange, onGenerate, generationInProgress = false } = props;

const locale = useRecoilValue(localeState(projectId));
const schema = useRecoilValue(formDialogSchemaState({ projectId, schemaId }));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -118,6 +119,7 @@ export const VisualFormDialogSchemaEditor = React.memo((props: Props) => {
allowUndo
editorId={`${projectId}:${schema.id}`}
isGenerating={generationInProgress}
locale={locale}
schema={schema}
schemaExtension={FileExtensions.FormDialogSchema}
templates={templates}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ export const formDialogsDispatcher = () => {
}

try {
const { data } = await httpClient.get<FormDialogSchemaTemplate[]>('/formDialogs/templateSchemas');
const templates = Object.keys(data).map((key) => ({
name: key,
isGlobal: data[key].$global,
}));
const { data } = await httpClient.get<Record<string, Omit<FormDialogSchemaTemplate, 'id'>>>(
'/formDialogs/templateSchemas'
);
const templates = Object.keys(data).map<FormDialogSchemaTemplate>((id) => ({ id, ...data[id] }));

set(formDialogLibraryTemplatesState, templates);
} catch (ex) {
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/form-dialogs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"dependencies": {
"react-beautiful-dnd": "^13.0.0",
"@bfc/shared": "*",
"@bfc/ui-shared": "*"
},
"peerDependencies": {
Expand Down
24 changes: 17 additions & 7 deletions Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { FormDialogSchemaTemplate } from '@bfc/shared';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
// eslint-disable-next-line @typescript-eslint/camelcase
import { RecoilRoot, useRecoilTransactionObserver_UNSTABLE } from 'recoil';
import { RecoilRoot, useRecoilTransactionObserver_UNSTABLE, useRecoilValue } from 'recoil';

import { formDialogSchemaJsonSelector, trackedAtomsSelector } from './atoms/appState';
import { useHandlers } from './atoms/handlers';
import { FormDialogPropertiesEditor } from './components/FormDialogPropertiesEditor';
import { UndoRoot } from './undo/UndoRoot';

export type FormDialogSchemaEditorProps = {
locale: string;
/**
* Unique id for the visual editor.
*/
Expand All @@ -31,7 +32,7 @@ export type FormDialogSchemaEditorProps = {
/**
* Record of available schema templates.
*/
templates?: string[];
templates?: FormDialogSchemaTemplate[];
/**
* Indicates of caller is running generation logic.
*/
Expand All @@ -48,26 +49,35 @@ export type FormDialogSchemaEditorProps = {

const InternalFormDialogSchemaEditor = React.memo((props: FormDialogSchemaEditorProps) => {
const {
locale,
editorId,
schema,
templates = [],
schemaExtension = '.schema',
schemaExtension = '.template',
isGenerating = false,
onSchemaUpdated,
onGenerateDialog,
allowUndo = false,
} = props;

const trackedAtoms = useRecoilValue(trackedAtomsSelector);
const { setTemplates, reset, importSchemaString } = useHandlers();
const { setTemplates, reset, updateLocale, importSchemaString } = useHandlers();

React.useEffect(() => {
if (locale) {
updateLocale({ locale });
}
}, [locale]);

React.useEffect(() => {
setTemplates({ templates });
}, [templates]);

React.useEffect(() => {
importSchemaString(schema);
}, [editorId]);
if (templates.length) {
importSchemaString({ ...schema, templates });
}
}, [editorId, templates]);

const startOver = React.useCallback(() => {
reset({ name: schema.id });
Expand Down
78 changes: 38 additions & 40 deletions Composer/packages/form-dialogs/src/atoms/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@

/* eslint-disable @typescript-eslint/consistent-type-assertions */

import { FormDialogSchemaTemplate } from '@bfc/shared';
import { atom, atomFamily, RecoilState, selector, selectorFamily } from 'recoil';

import { FormDialogProperty, FormDialogSchema } from './types';
import { spreadSchemaPropertyStore, validateSchemaPropertyStore } from './utils';
import { PropertyCardData } from '../components/property/types';

import { FormDialogSchema } from './types';
import { spreadCardData, validateSchemaPropertyStore } from './utils';

const schemaDraftUrl = 'http://json-schema.org/draft-07/schema';

/**
* Locale
*/
export const formDialogLocale = atom<string>({
key: 'FormDialogLocale',
default: '',
});

/**
* This atom represents the list of the available templates.
*/
export const formDialogTemplatesAtom = atom<FormDialogSchemaTemplate[]>({
key: 'FormDialogTemplatesAtom',
default: [],
});

/**
* This atom represents a form dialog schema.
*/
Expand All @@ -24,18 +43,16 @@ export const formDialogSchemaAtom = atom<FormDialogSchema>({
});

/**
* This atom family represent a form dialog schema property.
* This atom family represent a form dialog schema property card data.
*/
export const formDialogPropertyAtom = atomFamily<FormDialogProperty, string>({
key: 'FormDialogPropertyAtom',
export const propertyCardDataAtom = atomFamily<PropertyCardData, string>({
key: 'PropertyCardDataAtom',
default: (id) => ({
id,
name: '',
kind: 'string',
payload: { kind: 'string' },
required: true,
array: false,
examples: [],
isArray: false,
isRequired: true,
propertyType: 'string',
}),
});

Expand All @@ -57,7 +74,7 @@ export const formDialogSchemaPropertyNamesSelector = selector<string[]>({
key: 'FormDialogSchemaPropertyNamesSelector',
get: ({ get }) => {
const propertyIds = get(allFormDialogPropertyIdsSelector);
return propertyIds.map((pId) => get(formDialogPropertyAtom(pId)).name);
return propertyIds.map((pId) => get(propertyCardDataAtom(pId)).name);
},
});

Expand All @@ -67,8 +84,8 @@ export const formDialogSchemaPropertyNamesSelector = selector<string[]>({
export const formDialogPropertyJsonSelector = selectorFamily<object, string>({
key: 'FormDialogPropertyJsonSelector',
get: (id) => ({ get }) => {
const schemaPropertyStore = get(formDialogPropertyAtom(id));
return spreadSchemaPropertyStore(schemaPropertyStore);
const cardData = get(propertyCardDataAtom(id));
return spreadCardData(cardData);
},
});

Expand All @@ -78,8 +95,9 @@ export const formDialogPropertyJsonSelector = selectorFamily<object, string>({
export const formDialogPropertyValidSelector = selectorFamily<boolean, string>({
key: 'FormDialogPropertyValidSelector',
get: (id) => ({ get }) => {
const schemaPropertyStore = get(formDialogPropertyAtom(id));
return validateSchemaPropertyStore(schemaPropertyStore);
const templates = get(formDialogTemplatesAtom);
const cardData = get(propertyCardDataAtom(id));
return validateSchemaPropertyStore(cardData, templates);
},
});

Expand All @@ -101,53 +119,34 @@ export const formDialogSchemaJsonSelector = selector({
key: 'FormDialogSchemaJsonSelector',
get: ({ get }) => {
const propertyIds = get(allFormDialogPropertyIdsSelector);
const schemaPropertyStores = propertyIds.map((pId) => get(formDialogPropertyAtom(pId)));
const propertyCards = propertyIds.map((pId) => get(propertyCardDataAtom(pId)));

let jsonObject: object = {
$schema: schemaDraftUrl,
type: 'object',
$requires: ['standard.schema'],
};

if (schemaPropertyStores.length) {
if (propertyCards.length) {
jsonObject = {
...jsonObject,
properties: propertyIds.reduce<Record<string, object>>((acc, propId, idx) => {
const property = schemaPropertyStores[idx];
const property = propertyCards[idx];
acc[property.name] = get(formDialogPropertyJsonSelector(propId));
return acc;
}, <Record<string, object>>{}),
};
}

const required = schemaPropertyStores.filter((property) => property.required).map((property) => property.name);
const examples = schemaPropertyStores.reduce<Record<string, string[]>>((acc, property) => {
if (property.examples?.length) {
acc[property.name] = property.examples;
}
return acc;
}, <Record<string, string[]>>{});
const required = propertyCards.filter((property) => property.isRequired).map((property) => property.name);

if (required.length) {
jsonObject = { ...jsonObject, required };
}

if (Object.keys(examples)?.length) {
jsonObject = { ...jsonObject, $examples: examples };
}

return JSON.stringify(jsonObject, null, 2);
},
});

/**
* This atom represents the list of the available templates.
*/
export const formDialogTemplatesAtom = atom<string[]>({
key: 'FormDialogTemplatesAtom',
default: [],
});

export const activePropertyIdAtom = atom<string>({
key: 'ActivePropertyIdAtom',
default: '',
Expand All @@ -158,7 +157,6 @@ export const trackedAtomsSelector = selector<RecoilState<any>[]>({
key: 'TrackedAtoms',
get: ({ get }) => {
const propIds = get(allFormDialogPropertyIdsSelector) || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return [formDialogSchemaAtom, activePropertyIdAtom, ...propIds.map((pId) => formDialogPropertyAtom(pId))];
return [formDialogSchemaAtom, activePropertyIdAtom, ...propIds.map((pId) => propertyCardDataAtom(pId))];
},
});

0 comments on commit 38c521b

Please sign in to comment.