Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow undo/redo in form dialog schema builder #4328

Merged
merged 18 commits into from
Oct 6, 2020
Merged
34 changes: 26 additions & 8 deletions Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Licensed under the MIT License.

import * as React from 'react';
import { useRecoilValue } from 'recoil';
// eslint-disable-next-line @typescript-eslint/camelcase
import { RecoilRoot, useRecoilTransactionObserver_UNSTABLE } from 'recoil';
import { formDialogSchemaJsonSelector } from 'src/atoms/appState';
import { formDialogSchemaJsonSelector, trackedAtomsSelector } from 'src/atoms/appState';
import { useHandlers } from 'src/atoms/handlers';
import { FormDialogPropertiesEditor } from 'src/components/FormDialogPropertiesEditor';
import { UndoRoot } from 'src/undo/UndoRoot';

export type FormDialogSchemaEditorProps = {
/**
Expand All @@ -17,6 +19,10 @@ export type FormDialogSchemaEditorProps = {
* Initial json schema content.
*/
schema: { id: string; content: string };
/**
* Enables the undo/redo.
*/
allowUndo?: boolean;
/**
* Form dialog schema file extension.
*/
Expand All @@ -36,8 +42,17 @@ export type FormDialogSchemaEditorProps = {
};

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

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

React.useEffect(() => {
Expand All @@ -61,12 +76,15 @@ const InternalFormDialogSchemaEditor = React.memo((props: FormDialogSchemaEditor
});

return (
<FormDialogPropertiesEditor
key={editorId}
schemaExtension={schemaExtension}
onGenerateDialog={onGenerateDialog}
onReset={startOver}
/>
<UndoRoot key={schema.id} trackedAtoms={trackedAtoms}>
<FormDialogPropertiesEditor
key={editorId}
allowUndo={allowUndo}
schemaExtension={schemaExtension}
onGenerateDialog={onGenerateDialog}
onReset={startOver}
/>
</UndoRoot>
);
});

Expand Down
14 changes: 12 additions & 2 deletions Composer/packages/form-dialogs/src/atoms/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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

import { atom, atomFamily, selector, selectorFamily } from 'recoil';
import { atom, atomFamily, RecoilState, selector, selectorFamily } from 'recoil';
import { FormDialogProperty, FormDialogSchema } from 'src/atoms/types';
import { spreadSchemaPropertyStore, validateSchemaPropertyStore } from 'src/atoms/utils';

Expand Down Expand Up @@ -32,7 +32,7 @@ export const formDialogPropertyAtom = atomFamily<FormDialogProperty, string>({
name: '',
kind: 'string',
payload: { kind: 'string' },
required: false,
required: true,
array: false,
examples: [],
}),
Expand Down Expand Up @@ -151,3 +151,13 @@ export const activePropertyIdAtom = atom<string>({
key: 'ActivePropertyIdAtom',
default: '',
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
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))];
},
});
2 changes: 1 addition & 1 deletion Composer/packages/form-dialogs/src/atoms/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
formDialogSchemaPropertyNamesSelector,
formDialogTemplatesAtom,
} from 'src/atoms/appState';
import { FormDialogPropertyPayload, PropertyRequiredKind, FormDialogPropertyKind } from 'src/atoms/types';
import { FormDialogPropertyKind, FormDialogPropertyPayload, PropertyRequiredKind } from 'src/atoms/types';
import { createSchemaStoreFromJson, getDefaultPayload, getDuplicateName } from 'src/atoms/utils';
import { generateId } from 'src/utils/base';
import { readFileContent } from 'src/utils/file';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { useHandlers } from 'src/atoms/handlers';
import { CommandBarUploadButton } from 'src/components/common/CommandBarUpload';
import { FormDialogSchemaDetails } from 'src/components/property/FormDialogSchemaDetails';
import { useUndo } from 'src/undo/useUndo';
import { useUndoKeyBinding } from 'src/utils/hooks/useUndoKeyBinding';

const downloadFile = async (fileName: string, schemaExtension: string, content: string) => {
Expand Down Expand Up @@ -63,12 +64,13 @@ const SchemaName = styled(Stack)({

type Props = {
schemaExtension: string;
allowUndo: boolean;
onReset: () => void;
onGenerateDialog: (formDialogSchemaJson: string) => void;
};

export const FormDialogPropertiesEditor = React.memo((props: Props) => {
const { onReset, onGenerateDialog, schemaExtension } = props;
const { onReset, onGenerateDialog, schemaExtension, allowUndo } = props;

const schema = useRecoilValue(formDialogSchemaAtom);
const propertyIds = useRecoilValue(allFormDialogPropertyIdsSelector);
Expand All @@ -78,6 +80,7 @@ export const FormDialogPropertiesEditor = React.memo((props: Props) => {

const schemaIdRef = React.useRef<string>(schema.id);

const { undo, redo, canUndo, canRedo } = useUndo();
hatpick marked this conversation as resolved.
Show resolved Hide resolved
useUndoKeyBinding();

React.useEffect(() => {
Expand All @@ -99,6 +102,32 @@ export const FormDialogPropertiesEditor = React.memo((props: Props) => {
addProperty();
},
},
...(allowUndo
? [
{
key: 'undo',
text: formatMessage('Undo'),
iconProps: { iconName: 'Undo' },
title: formatMessage('Undo'),
ariaLabel: formatMessage('Undo'),
disabled: !canUndo(),
onClick: () => {
undo();
},
},
{
key: 'redo',
text: formatMessage('Redo'),
iconProps: { iconName: 'Redo' },
title: formatMessage('Redo'),
ariaLabel: formatMessage('Redo'),
disabled: !canRedo(),
onClick: () => {
redo();
},
},
]
: []),
{
key: 'import',
onRender: () => <CommandBarUploadButton accept={schemaExtension} onUpload={upload} />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const CommandBarUploadButton = (props: Props) => {
const onChange = () => {
if (inputFileRef.current.files) {
hatpick marked this conversation as resolved.
Show resolved Hide resolved
onUpload(inputFileRef.current.files.item(0));
inputFileRef.current.value = null;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ const PropertyListItemContent = React.memo((props: ContentProps) => {
onActivateItem(property.id);
}, [onActivateItem, property.id]);

const keyUp = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter') {
activateItem();
}
},
[activateItem]
);

const propertyTypeDisplayName = React.useMemo(() => getPropertyTypeDisplayName(property), [property]);

return (
Expand All @@ -85,6 +94,7 @@ const PropertyListItemContent = React.memo((props: ContentProps) => {
tokens={{ childrenGap: 8 }}
verticalAlign="center"
onClick={activateItem}
onKeyUp={keyUp}
>
<Icon {...dragHandleProps} iconName="GripperDotsVertical" />
<ErrorIcon>
Expand Down
35 changes: 23 additions & 12 deletions Composer/packages/form-dialogs/src/demo/DemoApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ const InternalDemoApp = () => {

return (
<Stack horizontal verticalFill styles={{ root: { height: 'calc(100vh)' } }}>
<Stack styles={{ root: { width: 400, padding: 16, borderRight: '1px solid' } }} tokens={{ childrenGap: 8 }}>
<Stack horizontal tokens={{ childrenGap: 8 }}>
<Stack styles={{ root: { width: 400, borderRight: '1px solid' } }} tokens={{ childrenGap: 8 }}>
<Stack horizontal tokens={{ childrenGap: 8, padding: 16 }}>
<TextField
styles={{ root: { flex: 1 } }}
value={newItemName}
Expand All @@ -224,16 +224,26 @@ const InternalDemoApp = () => {
></TextField>
<IconButton disabled={!newItemName} iconProps={{ iconName: 'Add' }} onClick={onAddItem} />
</Stack>
{items.map((item) => (
<Stack
key={item}
styles={{ root: { cursor: 'pointer', marginBottom: 8, height: 32 } }}
verticalAlign="center"
onClick={() => selectItem(item)}
>
{item}
</Stack>
))}
<div>
{items.map((item) => (
<Stack
key={item}
styles={{
root: {
cursor: 'pointer',
padding: '0 16px',
height: 48,
backgroundColor: item === selectedItemId ? '#ddd' : 'transparent',
borderBottom: '1px solid #444',
},
}}
verticalAlign="center"
onClick={() => selectItem(item)}
>
{item}
</Stack>
))}
</div>
</Stack>
{selectedItemId && (
<Stack
Expand All @@ -245,6 +255,7 @@ const InternalDemoApp = () => {
}}
>
<FormDialogSchemaEditor
allowUndo
editorId={selectedItemId}
schema={selectedItem}
schemaExtension=".form-dialog"
Expand Down
Loading