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
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();
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) {
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