diff --git a/app/src/App.tsx b/app/src/App.tsx index 5029e65..2eb6e07 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -22,7 +22,7 @@ export const App = () => ( schemaService={schemaService} schemaProviders={defaultSchemaProviders} schemaDecorators={defaultSchemaDecorators} - editorTabs={[ + previewTabs={[ { name: 'Preview (React)', Component: ReactMaterialPreview }, { name: 'Preview (Angular)', Component: AngularMaterialPreview }, ]} diff --git a/jsonforms-editor/src/JsonFormsEditor.tsx b/jsonforms-editor/src/JsonFormsEditor.tsx index 2d18c43..3b23b73 100644 --- a/jsonforms-editor/src/JsonFormsEditor.tsx +++ b/jsonforms-editor/src/JsonFormsEditor.tsx @@ -27,15 +27,12 @@ import { SelectedElement } from './core/selection'; import { tryFindByUUID } from './core/util/schemasUtil'; import { defaultEditorRenderers, - defaultEditorTabs, + defaultPreviewTabs, EditorPanel, -} from './editor'; -import { EditorTab } from './editor/components/EditorPanel'; -import { - defaultPalettePanelTabs, - PalettePanel, PaletteTab, -} from './palette-panel'; +} from './editor'; +import { PreviewTab } from './editor'; +import { defaultPalettePanelTabs, PalettePanel } from './palette-panel'; import { defaultPropertyRenderers, PropertiesPanel } from './properties'; import { PropertiesService, @@ -58,6 +55,7 @@ const useStyles = makeStyles((theme) => ({ reflexContainer: { flex: '1', alignItems: 'stretch', + overflow: 'auto', }, })); @@ -65,7 +63,7 @@ interface JsonFormsEditorProps { schemaService?: SchemaService; schemaProviders: PropertySchemasProvider[]; schemaDecorators: PropertySchemasDecorator[]; - editorTabs?: EditorTab[] | null; + previewTabs?: PreviewTab[] | null; paletteService?: PaletteService; paletteTabs?: PaletteTab[] | null; editorRenderers?: JsonFormsRendererRegistryEntry[]; @@ -92,7 +90,7 @@ export const JsonFormsEditor: React.FC = ({ schemaProviders, schemaDecorators, editorRenderers = defaultEditorRenderers, - editorTabs: editorTabsProp = defaultEditorTabs, + previewTabs: previewTabsProp = defaultPreviewTabs, paletteTabs = defaultPalettePanelTabs, propertyRenderers = defaultPropertyRenderers, header = Header, @@ -103,7 +101,7 @@ export const JsonFormsEditor: React.FC = ({ const [propertiesService] = useState( propertiesServiceProvider(schemaProviders, schemaDecorators) ); - const editorTabs = editorTabsProp ?? undefined; + const previewTabs = previewTabsProp ?? undefined; const headerComponent = header ?? undefined; const footerComponent = footer ?? undefined; @@ -139,13 +137,13 @@ export const JsonFormsEditor: React.FC = ({ schemaService, paletteService, propertiesService, + propertyRenderers, }} > = ({ }; interface JsonFormsEditorUiProps { - editorTabs?: EditorTab[]; + previewTabs?: PreviewTab[]; editorRenderers: JsonFormsRendererRegistryEntry[]; - propertyRenderers: JsonFormsRendererRegistryEntry[]; header?: ComponentType; footer?: ComponentType; paletteTabs?: PaletteTab[]; } const JsonFormsEditorUi: React.FC = ({ - editorTabs, + previewTabs, editorRenderers, - propertyRenderers, header, footer, paletteTabs, }) => { const classes = useStyles(); return ( - + - -
- -
-
-
- +
- +
diff --git a/jsonforms-editor/src/core/api/paletteService.tsx b/jsonforms-editor/src/core/api/paletteService.tsx index 6076110..56ddb8b 100644 --- a/jsonforms-editor/src/core/api/paletteService.tsx +++ b/jsonforms-editor/src/core/api/paletteService.tsx @@ -8,9 +8,19 @@ import React from 'react'; -import { GroupIcon, HorizontalIcon, LabelIcon, VerticalIcon } from '../icons'; +import { + ControlIcon, + GroupIcon, + HorizontalIcon, + LabelIcon, + VerticalIcon, +} from '../icons'; import { EditorUISchemaElement } from '../model/uischema'; -import { createLabel, createLayout } from '../util/generators/uiSchema'; +import { + createEmptyControl, + createLabel, + createLayout, +} from '../util/generators/uiSchema'; export interface PaletteService { getPaletteElements(): PaletteElement[]; @@ -48,6 +58,12 @@ const paletteElements: PaletteElement[] = [ icon: , uiSchemaElementProvider: () => createLabel(), }, + { + type: 'Control', + label: 'Control', + icon: , + uiSchemaElementProvider: () => createEmptyControl(), + }, ]; export class DefaultPaletteService implements PaletteService { diff --git a/jsonforms-editor/src/core/components/EditableControl.tsx b/jsonforms-editor/src/core/components/EditableControl.tsx new file mode 100644 index 0000000..b3856ce --- /dev/null +++ b/jsonforms-editor/src/core/components/EditableControl.tsx @@ -0,0 +1,95 @@ +/** + * --------------------------------------------------------------------- + * Copyright (c) 2020 EclipseSource Munich + * Licensed under MIT + * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE + * --------------------------------------------------------------------- + */ + +import { Grid, TextField } from '@material-ui/core'; +import MenuItem from '@material-ui/core/MenuItem'; +import React from 'react'; + +import { useDispatch, useSchema } from '../context'; +import { + Actions, + getChildren, + getScope, + isArrayElement, + isObjectElement, + SchemaElement, +} from '../model'; +import { EditorControl } from '../model/uischema'; + +interface EditableControlProps { + uischema: EditorControl; +} +export const EditableControl: React.FC = ({ + uischema, +}) => { + const dispatch = useDispatch(); + const baseSchema = useSchema() as SchemaElement; + const handleLabelChange = (event: React.ChangeEvent) => { + dispatch( + Actions.updateUISchemaElement(uischema.uuid, { + label: event.target.value, + }) + ); + }; + const handleScopeChange = (event: React.ChangeEvent) => { + dispatch( + Actions.changeControlScope( + uischema.uuid, + (event.target.value as any).scope, + (event.target.value as any).uuid + ) + ); + }; + const scopes = getChildren(baseSchema) + .flatMap((child) => { + // if the child is the only item of an array, use its children instead + if ( + (isObjectElement(child) && + isArrayElement(child.parent) && + child.parent.items === child) || + isObjectElement(child) + ) { + return getChildren(child); + } + return [child]; + }) + .map((e) => ({ scope: `#${getScope(e)}`, uuid: e.uuid })); + const scopeValues = scopes.filter((s) => s.scope.endsWith(uischema.scope)); + const scopeValue = + scopeValues && scopeValues.length === 1 ? scopeValues[0] : ''; + return ( + + + + + + + {scopes.map((scope) => ( + + {scope.scope} + + ))} + + + + ); +}; diff --git a/jsonforms-editor/src/core/components/Header.tsx b/jsonforms-editor/src/core/components/Header.tsx index 59e94e3..1b9ccce 100644 --- a/jsonforms-editor/src/core/components/Header.tsx +++ b/jsonforms-editor/src/core/components/Header.tsx @@ -33,7 +33,7 @@ export const Header: React.FC = () => { const openDownloadDialog = () => setOpen(true); return ( - + <> { uiSchema={uiSchema} /> )} - + ); }; diff --git a/jsonforms-editor/src/core/components/Layout.tsx b/jsonforms-editor/src/core/components/Layout.tsx index 45d8e0d..32e4d7a 100644 --- a/jsonforms-editor/src/core/components/Layout.tsx +++ b/jsonforms-editor/src/core/components/Layout.tsx @@ -5,46 +5,153 @@ * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE * --------------------------------------------------------------------- */ -import { makeStyles } from '@material-ui/core'; -import React from 'react'; +import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; +import AppBar from '@material-ui/core/AppBar'; +import Divider from '@material-ui/core/Divider'; +import Drawer from '@material-ui/core/Drawer'; +import IconButton from '@material-ui/core/IconButton'; +import { makeStyles } from '@material-ui/core/styles'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import React, { useCallback } from 'react'; + +import { PaletteTab } from '../../editor'; +import { PalettePanel } from '../../palette-panel'; + +const footerHeight = '40px'; +const drawerWidth = '400px'; const useStyles = makeStyles((theme) => ({ + appBar: { + zIndex: theme.zIndex.drawer + 1, + }, main: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), minHeight: 0, + flexGrow: 1, + display: 'flex', + flexDirection: 'column', }, container: { - display: 'grid', height: '100vh', - gridTemplateAreas: 'header content footer', - gridTemplateColumns: '1fr', - gridTemplateRows: 'auto 1fr auto', + display: 'flex', + overflow: 'hidden', }, footer: { - padding: theme.spacing(2, 2), + zIndex: theme.zIndex.drawer + 1, + padding: theme.spacing(1, 1), backgroundColor: theme.palette.type === 'light' ? theme.palette.grey[200] : theme.palette.grey[800], + height: footerHeight, + bottom: 0, + left: 'auto', + right: 0, + position: 'fixed', + width: '100%', + }, + fakeFooter: { + marginBottom: footerHeight, + }, + drawer: { + width: drawerWidth, + flexShrink: 0, + whiteSpace: 'nowrap', }, + drawerOpen: { + width: drawerWidth, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + drawerClose: { + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + overflowX: 'hidden', + width: theme.spacing(7) + 1, + [theme.breakpoints.up('sm')]: { + width: theme.spacing(9) + 1, + }, + }, + drawerContainer: { + overflow: 'auto', + }, + drawerTitle: (props: { isOpen: boolean }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.isOpen ? 'flex-start' : 'center', + }), })); interface LayoutProps { HeaderComponent?: React.ComponentType; FooterComponent?: React.ComponentType; + paletteTabs?: PaletteTab[]; } export const Layout: React.FC = ({ HeaderComponent, FooterComponent, + paletteTabs, children, }) => { - const classes = useStyles(); + const [open, setOpen] = React.useState(false); + const [selectedTabName, setSelectedTabName] = React.useState( + paletteTabs ? paletteTabs[0].name : '' + ); + const toggleDrawerClose = () => { + setOpen(!open); + }; + const openDrawer = () => { + setOpen(true); + }; + const onSelected = useCallback((tabName) => setSelectedTabName(tabName), []); + const classes = useStyles({ isOpen: open }); + const classNameOpen = open ? classes.drawerOpen : classes.drawerClose; + return (
-
{HeaderComponent ? : null}
-
{children}
+ + {HeaderComponent ? : null} + + + +
+
+ + {open ? : } + + {open ? ( + {selectedTabName} + ) : null} +
+ + +
+
+ +
+ + {children} +
+
{FooterComponent ? : null}
diff --git a/jsonforms-editor/src/core/components/index.ts b/jsonforms-editor/src/core/components/index.ts index d454b75..8472164 100644 --- a/jsonforms-editor/src/core/components/index.ts +++ b/jsonforms-editor/src/core/components/index.ts @@ -14,3 +14,4 @@ export * from './ErrorDialog'; export * from './OkCancelDialog'; export * from './ExportDialog'; export * from './ShowMoreLess'; +export * from './EditableControl'; diff --git a/jsonforms-editor/src/core/context/context.ts b/jsonforms-editor/src/core/context/context.ts index 31ce140..eb7b125 100644 --- a/jsonforms-editor/src/core/context/context.ts +++ b/jsonforms-editor/src/core/context/context.ts @@ -5,6 +5,7 @@ * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE * --------------------------------------------------------------------- */ +import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; import React, { useContext } from 'react'; import { PropertiesService } from '../../properties/propertiesService'; @@ -24,6 +25,7 @@ export interface EditorContext { dispatch: (action: EditorAction) => void; selection: SelectedElement; setSelection: (selection: SelectedElement) => void; + propertyRenderers: JsonFormsRendererRegistryEntry[]; } /**We always use a provider so default can be undefined*/ @@ -73,3 +75,8 @@ export const usePropertiesService = (): PropertiesService => { const { propertiesService } = useEditorContext(); return propertiesService; }; + +export const usePropertyRenderers = (): JsonFormsRendererRegistryEntry[] => { + const { propertyRenderers } = useEditorContext(); + return propertyRenderers; +}; diff --git a/jsonforms-editor/src/core/dnd/types.ts b/jsonforms-editor/src/core/dnd/types.ts index f09dbbf..2d09970 100644 --- a/jsonforms-editor/src/core/dnd/types.ts +++ b/jsonforms-editor/src/core/dnd/types.ts @@ -39,7 +39,7 @@ const newUISchemaElement = ( export interface MoveUISchemaElement { type: 'moveUiSchemaElement'; uiSchemaElement: EditorUISchemaElement; - schema?: SchemaElement; + // schema?: SchemaElement; } const moveUISchemaElement = ( diff --git a/jsonforms-editor/src/core/model/actions.ts b/jsonforms-editor/src/core/model/actions.ts index c72c2f4..6dfa957 100644 --- a/jsonforms-editor/src/core/model/actions.ts +++ b/jsonforms-editor/src/core/model/actions.ts @@ -16,7 +16,8 @@ export type CombinedAction = | AddScopedElementToLayout | MoveUiSchemaElement | RemoveUiSchemaElement - | AddDetail; + | AddDetail + | ChangeControlScope; export type EditorAction = UiSchemaAction | CombinedAction; @@ -38,6 +39,8 @@ export const UPDATE_UISCHEMA_ELEMENT: 'jsonforms-editor/UPDATE_UISCHEMA_ELEMENT' 'jsonforms-editor/UPDATE_UISCHEMA_ELEMENT'; export const ADD_DETAIL: 'jsonforms-editor/ADD_DETAIL' = 'jsonforms-editor/ADD_DETAIL'; +export const CHANGE_CONTROL_SCOPE: 'jsonforms-editor/CHANGE_CONTROL_SCOPE' = + 'jsonforms-editor/CHANGE_CONTROL_SCOPE'; export interface SetSchemaAction { type: 'jsonforms-editor/SET_SCHEMA'; @@ -89,6 +92,13 @@ export interface UpdateUiSchemaElement { changedProperties: { [key: string]: any }; } +export interface ChangeControlScope { + type: 'jsonforms-editor/CHANGE_CONTROL_SCOPE'; + elementUUID: string; + scope: string; + schemaUUID: string; +} + export interface AddDetail { type: 'jsonforms-editor/ADD_DETAIL'; uiSchemaElementId: string; @@ -167,6 +177,12 @@ const addDetail = ( detail, }); +const changeControlScope = ( + elementUUID: string, + scope: string, + schemaUUID: string +) => ({ type: CHANGE_CONTROL_SCOPE, elementUUID, scope, schemaUUID }); + export const Actions = { setSchema, setUiSchema, @@ -177,4 +193,5 @@ export const Actions = { removeUiSchemaElement, updateUISchemaElement, addDetail, + changeControlScope, }; diff --git a/jsonforms-editor/src/core/model/reducer.ts b/jsonforms-editor/src/core/model/reducer.ts index ce4fb56..1c90b17 100644 --- a/jsonforms-editor/src/core/model/reducer.ts +++ b/jsonforms-editor/src/core/model/reducer.ts @@ -20,6 +20,7 @@ import { ADD_DETAIL, ADD_SCOPED_ELEMENT_TO_LAYOUT, ADD_UNSCOPED_ELEMENT_TO_LAYOUT, + CHANGE_CONTROL_SCOPE, CombinedAction, EditorAction, MOVE_UISCHEMA_ELEMENT, @@ -118,6 +119,24 @@ export const combinedReducer = (state: EditorState, action: CombinedAction) => { buildSchemaTree(action.schema), buildEditorUiSchemaTree(action.uiSchema) ); + case CHANGE_CONTROL_SCOPE: + return withCloneTrees( + state.uiSchema, + action.elementUUID, + state.schema, + action.schemaUUID, + state, + (newUiSchema, newSchema) => { + if (!newSchema || !newUiSchema || !isEditorControl(newUiSchema)) + return state; + linkElements(newUiSchema, newSchema); + newUiSchema.scope = action.scope; + return { + schema: getRoot(newSchema), + uiSchema: getRoot(newUiSchema as EditorUISchemaElement), + }; + } + ); case ADD_SCOPED_ELEMENT_TO_LAYOUT: return withCloneTrees( state.uiSchema, @@ -361,6 +380,7 @@ export const editorReducer = (state: EditorState, action: EditorAction) => { schema: state.schema, uiSchema: uiSchemaReducer(state.uiSchema, action), }; + case CHANGE_CONTROL_SCOPE: case SET_SCHEMA: case SET_UISCHEMA: case SET_SCHEMAS: diff --git a/jsonforms-editor/src/core/renderers/DroppableArrayControl.tsx b/jsonforms-editor/src/core/renderers/DroppableArrayControl.tsx index 1c8c8ad..e7daa46 100644 --- a/jsonforms-editor/src/core/renderers/DroppableArrayControl.tsx +++ b/jsonforms-editor/src/core/renderers/DroppableArrayControl.tsx @@ -15,10 +15,11 @@ import { JsonFormsDispatch, withJsonFormsArrayControlProps, } from '@jsonforms/react'; -import { makeStyles, Typography } from '@material-ui/core'; +import { Grid, makeStyles, Typography } from '@material-ui/core'; import React, { useMemo } from 'react'; import { useDrop } from 'react-dnd'; +import { EditableControl } from '../components'; import { useDispatch, useSchema } from '../context'; import { canDropIntoScope, @@ -96,22 +97,28 @@ const DroppableArrayControl: React.FC = ({ return renderers && [...renderers, DroppableElementRegistration]; }, [renderers]); - if (!uischema.options?.detail) { - return ( - - Default array layout. Drag and drop an item here to customize array - layout. - - ); - } return ( - + + + + + + {!uischema.options?.detail ? ( + + Default array layout. Drag and drop an item here to customize array + layout. + + ) : ( + + )} + + ); }; diff --git a/jsonforms-editor/src/core/renderers/DroppableControl.tsx b/jsonforms-editor/src/core/renderers/DroppableControl.tsx new file mode 100644 index 0000000..644adc3 --- /dev/null +++ b/jsonforms-editor/src/core/renderers/DroppableControl.tsx @@ -0,0 +1,28 @@ +/** + * --------------------------------------------------------------------- + * Copyright (c) 2020 EclipseSource Munich + * Licensed under MIT + * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE + * --------------------------------------------------------------------- + */ + +import { ControlProps, isControl, rankWith } from '@jsonforms/core'; +import { withJsonFormsControlProps } from '@jsonforms/react'; +import React from 'react'; + +import { EditableControl } from '../components'; +import { EditorControl } from '../model/uischema'; + +interface DroppableControlProps extends ControlProps { + uischema: EditorControl; +} +const DroppableControl: React.FC = ({ uischema }) => { + return ; +}; + +export const DroppableControlRegistration = { + tester: rankWith(40, isControl), // less than DroppableElement + renderer: withJsonFormsControlProps( + DroppableControl as React.FC + ), +}; diff --git a/jsonforms-editor/src/core/renderers/DroppableGroupLayout.tsx b/jsonforms-editor/src/core/renderers/DroppableGroupLayout.tsx index 1530c81..d39d626 100644 --- a/jsonforms-editor/src/core/renderers/DroppableGroupLayout.tsx +++ b/jsonforms-editor/src/core/renderers/DroppableGroupLayout.tsx @@ -13,10 +13,13 @@ import { CardHeader, Grid, makeStyles, + TextField, Typography, } from '@material-ui/core'; import React from 'react'; +import { useDispatch } from '../context'; +import { Actions } from '../model'; import { EditorLayout } from '../model/uischema'; import { DroppableLayout } from './DroppableLayout'; @@ -39,6 +42,14 @@ const Group: React.FC = (props) => { const { uischema } = props; const groupLayout = uischema as GroupLayout & EditorLayout; const classes = useStyles(); + const dispatch = useDispatch(); + const handleLabelChange = (event: React.ChangeEvent) => { + dispatch( + Actions.updateUISchemaElement(groupLayout.uuid, { + label: event.target.value, + }) + ); + }; return ( = (props) => { )} > + diff --git a/jsonforms-editor/src/core/renderers/DroppableLabel.tsx b/jsonforms-editor/src/core/renderers/DroppableLabel.tsx new file mode 100644 index 0000000..8cbd679 --- /dev/null +++ b/jsonforms-editor/src/core/renderers/DroppableLabel.tsx @@ -0,0 +1,42 @@ +/** + * --------------------------------------------------------------------- + * Copyright (c) 2020 EclipseSource Munich + * Licensed under MIT + * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE + * --------------------------------------------------------------------- + */ +import { LabelElement, LayoutProps, rankWith, uiTypeIs } from '@jsonforms/core'; +import { withJsonFormsLayoutProps } from '@jsonforms/react'; +import { TextField } from '@material-ui/core'; +import React from 'react'; + +import { useDispatch } from '../context'; +import { Actions } from '../model'; +import { EditorUISchemaElement } from '../model/uischema'; + +const DroppableLabel: React.FC = (props) => { + const { uischema } = props; + const labelElement = uischema as LabelElement & EditorUISchemaElement; + const dispatch = useDispatch(); + const handleLabelChange = (event: React.ChangeEvent) => { + dispatch( + Actions.updateUISchemaElement(labelElement.uuid, { + text: event.target.value, + }) + ); + }; + return ( + + ); +}; + +export const DroppableLabelRegistration = { + tester: rankWith(45, uiTypeIs('Label')), + renderer: withJsonFormsLayoutProps(DroppableLabel), +}; diff --git a/jsonforms-editor/src/core/renderers/DroppableLayout.tsx b/jsonforms-editor/src/core/renderers/DroppableLayout.tsx index b8434dc..75f3228 100644 --- a/jsonforms-editor/src/core/renderers/DroppableLayout.tsx +++ b/jsonforms-editor/src/core/renderers/DroppableLayout.tsx @@ -18,10 +18,14 @@ import { withJsonFormsLayoutProps, } from '@jsonforms/react'; import { Grid, makeStyles } from '@material-ui/core'; +import EditIcon from '@material-ui/icons/Edit'; +import SpeedDial from '@material-ui/lab/SpeedDial'; +import SpeedDialAction from '@material-ui/lab/SpeedDialAction'; +import SpeedDialIcon from '@material-ui/lab/SpeedDialIcon'; import React from 'react'; import { useDrop } from 'react-dnd'; -import { useDispatch, useSchema } from '../context'; +import { useDispatch, usePaletteService, useSchema } from '../context'; import { canDropIntoLayout, canMoveSchemaElementTo, @@ -73,12 +77,12 @@ export const DroppableLayout: React.FC = ({ spacing={direction === 'row' ? 2 : 0} wrap='nowrap' > - + {layout.elements.map((child, index) => ( - + @@ -95,7 +99,7 @@ export const DroppableLayout: React.FC = ({ ))} @@ -103,6 +107,62 @@ export const DroppableLayout: React.FC = ({ ); }; +const useActionBarStyles = makeStyles((theme) => ({ + speedDial: { + bottom: theme.spacing(2), + right: theme.spacing(2), + }, +})); +interface InlineActionBarProps { + layout: EditorLayout; + index: number; +} +const InlineActionBar = ({ layout, index }: InlineActionBarProps) => { + const classes = useActionBarStyles(); + const paletteService = usePaletteService(); + const dispatch = useDispatch(); + const [open, setOpen] = React.useState(false); + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + return ( + } />} + onClose={handleClose} + onOpen={handleOpen} + open={open} + direction={layout.type === 'HorizontalLayout' ? 'down' : 'right'} + > + {paletteService + .getPaletteElements() + .map(({ type, label, icon, uiSchemaElementProvider }) => ( + { + dispatch( + Actions.addUnscopedElementToLayout( + uiSchemaElementProvider(), + layout.uuid, + index + ) + ); + handleClose(); + }} + /> + ))} + + ); +}; + interface DropPointProps { layout: EditorLayout; index: number; @@ -116,10 +176,16 @@ const useDropPointStyles = makeStyles((theme) => ({ : 'none', backgroundSize: 'calc(10 * 1px) calc(10 * 1px)', backgroundClip: 'content-box', - minWidth: '2em', - minHeight: props.isOver ? '8em' : '2em', - maxWidth: props.fillWidth || props.isOver ? 'inherit' : '2em', + minWidth: '3em', + minHeight: props.isOver ? '8em' : '3em', + maxWidth: props.fillWidth ? 'inherit' : '3em', }), + actions: { + opacity: 0, + '&:hover': { + opacity: 1, + }, + }, })); const DropPoint: React.FC = ({ layout, index }) => { @@ -195,8 +261,13 @@ const DropPoint: React.FC = ({ layout, index }) => { ref={drop} className={classes.dropPointGridItem} data-cy={`${getDataPath(layout)}-drop-${index}`} + alignItems='stretch' xs - > + > + + + + ); }; diff --git a/jsonforms-editor/src/core/util/generators/uiSchema.ts b/jsonforms-editor/src/core/util/generators/uiSchema.ts index b0f55f9..6191520 100644 --- a/jsonforms-editor/src/core/util/generators/uiSchema.ts +++ b/jsonforms-editor/src/core/util/generators/uiSchema.ts @@ -44,3 +44,12 @@ export const createLabel = ( uuid: uuid(), } as LabelElement & EditorUISchemaElement; }; + +export const createEmptyControl = (): ControlElement & + EditorUISchemaElement => { + return { + type: 'Control', + scope: '', + uuid: uuid(), + } as ControlElement & EditorUISchemaElement; +}; diff --git a/jsonforms-editor/src/core/util/hooks.ts b/jsonforms-editor/src/core/util/hooks.ts index 7d1e079..80547fc 100644 --- a/jsonforms-editor/src/core/util/hooks.ts +++ b/jsonforms-editor/src/core/util/hooks.ts @@ -22,7 +22,8 @@ const doBuildUiSchema = (uiSchema: EditorUISchemaElement | undefined) => */ export const useExportSchema = () => { const schema = useSchema(); - return useTransform(schema, doBuildJsonSchema); + // return useTransform(schema, doBuildJsonSchema); + return doBuildJsonSchema(schema); }; /** @@ -30,40 +31,8 @@ export const useExportSchema = () => { */ export const useExportUiSchema = () => { const uiSchema = useUiSchema(); - return useTransform(uiSchema, doBuildUiSchema); -}; - -/** - * Transforms the given element whenever it changes. - */ -export const useTransform = ( - element: T1, - transform: (el: T1) => T2 -) => { - const [transformedElement, setTransformedElement] = useState( - transform(element) - ); - useEffectAfterInit(() => setTransformedElement(transform(element)), [ - element, - transform, - ]); - return transformedElement; -}; - -/** - * Hook similar to `useEffect` with the difference that the effect - * is only executed from the second call onwards. - */ -const useEffectAfterInit = (effect: () => void, dependencies: Array) => { - const firstExecution = useRef(true); - useEffect(() => { - if (firstExecution.current) { - firstExecution.current = false; - return; - } - effect(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [...dependencies]); + // return useTransform(uiSchema, doBuildUiSchema); + return doBuildUiSchema(uiSchema); }; /** Force a rerender */ diff --git a/jsonforms-editor/src/editor/components/EditorElement.tsx b/jsonforms-editor/src/editor/components/EditorElement.tsx index 9f7dfea..a39c24a 100644 --- a/jsonforms-editor/src/editor/components/EditorElement.tsx +++ b/jsonforms-editor/src/editor/components/EditorElement.tsx @@ -79,7 +79,7 @@ export const EditorElement: React.FC = ({ wrappedElement.linkedSchemaElement ); const [{ isDragging }, drag] = useDrag({ - item: DndItems.moveUISchemaElement(wrappedElement, elementSchema), + item: DndItems.moveUISchemaElement(wrappedElement), collect: (monitor) => ({ isDragging: !!monitor.isDragging(), }), diff --git a/jsonforms-editor/src/editor/components/EditorPanel.tsx b/jsonforms-editor/src/editor/components/EditorPanel.tsx index 41ab3a0..65accf9 100644 --- a/jsonforms-editor/src/editor/components/EditorPanel.tsx +++ b/jsonforms-editor/src/editor/components/EditorPanel.tsx @@ -6,63 +6,23 @@ * --------------------------------------------------------------------- */ import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; -import { makeStyles, Tab, Tabs } from '@material-ui/core'; -import React, { useState } from 'react'; +import Typography from '@material-ui/core/Typography'; +import React from 'react'; -import { TabContent } from '../../core/components'; import { Editor } from './Editor'; -const useStyles = makeStyles(() => ({ - editorPanel: { - height: '100%', - display: 'grid', - gridTemplateColumns: '1fr', - gridTemplateRows: 'auto 1fr ', - }, -})); - -export interface EditorTab { - name: string; - Component: React.ComponentType; -} - interface EditorPanelProps { - editorTabs?: EditorTab[]; editorRenderers: JsonFormsRendererRegistryEntry[]; } export const EditorPanel: React.FC = ({ - editorTabs, editorRenderers, }) => { - const [selectedTab, setSelectedTab] = useState(0); - const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setSelectedTab(newValue); - }; - const classes = useStyles(); return ( -
- - - {editorTabs - ? editorTabs.map((tab) => ( - - )) - : null} - - - - - {editorTabs - ? editorTabs.map((tab, index) => ( - - - - )) - : null} -
+ <> + + Editor + + + ); }; diff --git a/jsonforms-editor/src/editor/components/EmptyEditor.tsx b/jsonforms-editor/src/editor/components/EmptyEditor.tsx index 81caee6..6ffff1b 100644 --- a/jsonforms-editor/src/editor/components/EmptyEditor.tsx +++ b/jsonforms-editor/src/editor/components/EmptyEditor.tsx @@ -7,21 +7,29 @@ */ import { Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; +import EditIcon from '@material-ui/icons/Edit'; +import SpeedDial from '@material-ui/lab/SpeedDial'; +import SpeedDialAction from '@material-ui/lab/SpeedDialAction'; +import SpeedDialIcon from '@material-ui/lab/SpeedDialIcon'; import React from 'react'; import { useDrop } from 'react-dnd'; -import { useDispatch } from '../../core/context'; +import { useDispatch, usePaletteService } from '../../core/context'; import { NEW_UI_SCHEMA_ELEMENT } from '../../core/dnd'; import { Actions } from '../../core/model'; -const useStyles = makeStyles({ +const useStyles = makeStyles((theme) => ({ root: (props: any) => ({ padding: 10, fontSize: props.isOver ? '1.1em' : '1em', border: props.isOver ? '1px solid #D3D3D3' : 'none', height: '100%', }), -}); + speedDial: { + bottom: theme.spacing(2), + right: theme.spacing(2), + }, +})); export const EmptyEditor: React.FC = () => { const dispatch = useDispatch(); @@ -35,12 +43,45 @@ export const EmptyEditor: React.FC = () => { dispatch(Actions.setUiSchema(uiSchemaElement)); }, }); + const paletteService = usePaletteService(); + const [open, setOpen] = React.useState(false); + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; const classes = useStyles({ isOver }); return (
Drag and drop an element from the Palette to begin. + } />} + onClose={handleClose} + onOpen={handleOpen} + open={open} + direction={'right'} + > + {paletteService + .getPaletteElements() + .map(({ type, label, icon, uiSchemaElementProvider }) => ( + { + dispatch(Actions.setUiSchema(uiSchemaElementProvider())); + handleClose(); + }} + /> + ))} +
); }; diff --git a/jsonforms-editor/src/editor/index.ts b/jsonforms-editor/src/editor/index.ts index 221a0a6..6d94016 100644 --- a/jsonforms-editor/src/editor/index.ts +++ b/jsonforms-editor/src/editor/index.ts @@ -9,19 +9,22 @@ import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; import { materialRenderers } from '@jsonforms/material-renderers'; import { DroppableArrayControlRegistration } from '../core/renderers/DroppableArrayControl'; +import { DroppableControlRegistration } from '../core/renderers/DroppableControl'; import { DroppableElementRegistration } from '../core/renderers/DroppableElement'; import { DroppableGroupLayoutRegistration } from '../core/renderers/DroppableGroupLayout'; +import { DroppableLabelRegistration } from '../core/renderers/DroppableLabel'; import { DroppableHorizontalLayoutRegistration, DroppableVerticalLayoutRegistration, } from '../core/renderers/DroppableLayout'; -import { EditorTab } from './components/EditorPanel'; import { ReactMaterialPreview } from './components/preview'; +import { PreviewTab } from './interface'; export * from './components/EditorPanel'; export { EditorElement } from './components/EditorElement'; -export const defaultEditorTabs: EditorTab[] = [ +export * from './interface'; +export const defaultPreviewTabs: PreviewTab[] = [ { name: 'Preview', Component: ReactMaterialPreview }, ]; @@ -32,4 +35,6 @@ export const defaultEditorRenderers: JsonFormsRendererRegistryEntry[] = [ DroppableElementRegistration, DroppableGroupLayoutRegistration, DroppableArrayControlRegistration, + DroppableControlRegistration, + DroppableLabelRegistration, ]; diff --git a/jsonforms-editor/src/editor/interface.ts b/jsonforms-editor/src/editor/interface.ts new file mode 100644 index 0000000..99961f3 --- /dev/null +++ b/jsonforms-editor/src/editor/interface.ts @@ -0,0 +1,16 @@ +/** + * --------------------------------------------------------------------- + * Copyright (c) 2020 EclipseSource Munich + * Licensed under MIT + * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE + * --------------------------------------------------------------------- + */ +export interface PreviewTab { + name: string; + Component: React.ComponentType; +} +export interface PaletteTab { + name: string; + Component: React.ComponentType; + icon: React.ReactElement; +} diff --git a/jsonforms-editor/src/env.ts b/jsonforms-editor/src/env.ts index aa886af..b903e6a 100644 --- a/jsonforms-editor/src/env.ts +++ b/jsonforms-editor/src/env.ts @@ -7,5 +7,5 @@ */ export const env = () => { const { REACT_APP_DEBUG: DEBUG = 'false', NODE_ENV } = process.env; - return { NODE_ENV, DEBUG }; + return { NODE_ENV, DEBUG, IS_DEBUG: DEBUG === 'true' }; }; diff --git a/jsonforms-editor/src/index.ts b/jsonforms-editor/src/index.ts index 9503bbd..58bde55 100644 --- a/jsonforms-editor/src/index.ts +++ b/jsonforms-editor/src/index.ts @@ -20,5 +20,4 @@ export * from './core/util'; export * from './editor/components/preview'; export * from './editor'; export * from './text-editor'; -export * from './palette-panel'; export default JsonFormsEditor; diff --git a/jsonforms-editor/src/palette-panel/components/JsonSchemaPanel.tsx b/jsonforms-editor/src/palette-panel/components/JsonSchemaPanel.tsx index c0c1579..612033a 100644 --- a/jsonforms-editor/src/palette-panel/components/JsonSchemaPanel.tsx +++ b/jsonforms-editor/src/palette-panel/components/JsonSchemaPanel.tsx @@ -28,7 +28,7 @@ export const JsonSchemaPanel: React.FC = ({ const dispatch = useDispatch(); const exportSchema = useExportSchema(); const schema: SchemaElement | undefined = useSchema(); - const showDebugSchema = env().DEBUG === 'true'; + const showDebugSchema = env().IS_DEBUG; const handleSchemaUpdate = (newSchema: string): UpdateResult => { try { const newSchemaObject = JSON.parse(newSchema); diff --git a/jsonforms-editor/src/palette-panel/components/PalletePanel.tsx b/jsonforms-editor/src/palette-panel/components/PalletePanel.tsx index 342186d..e431a4d 100644 --- a/jsonforms-editor/src/palette-panel/components/PalletePanel.tsx +++ b/jsonforms-editor/src/palette-panel/components/PalletePanel.tsx @@ -5,85 +5,149 @@ * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE * --------------------------------------------------------------------- */ -import { makeStyles, Tab, Tabs } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; +import Grid from '@material-ui/core/Grid'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import Tooltip from '@material-ui/core/Tooltip'; +import BallotIcon from '@material-ui/icons/Ballot'; +import ChatIcon from '@material-ui/icons/Chat'; +import CodeIcon from '@material-ui/icons/Code'; +import SettingsIcon from '@material-ui/icons/Settings'; import React, { useState } from 'react'; -import { TabContent } from '../../core/components'; -import { usePaletteService, useSchema } from '../../core/context'; +import { + usePaletteService, + usePropertyRenderers, + useSchema, +} from '../../core/context'; import { SchemaElement } from '../../core/model'; +import { PaletteTab } from '../../editor'; +import { Properties } from '../../properties/components/Properties'; import { JsonSchemaPanel } from './JsonSchemaPanel'; import { SchemaTreeView } from './SchemaTree'; import { UIElementsTree } from './UIElementsTree'; import { UISchemaPanel } from './UISchemaPanel'; const useStyles = makeStyles((theme) => ({ - uiElementsTree: { - marginBottom: theme.spacing(1), + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: 'flex', + height: 224, }, palettePanel: { height: '100%', display: 'flex', flexDirection: 'column', }, + menu: { + margin: theme.spacing(1), + width: theme.spacing(7) + 1, + maxWidth: theme.spacing(7) + 1, + [theme.breakpoints.up('sm')]: { + width: theme.spacing(9) + 1, + maxWidth: theme.spacing(9) + 1, + }, + }, })); - -export interface PaletteTab { - name: string; - Component: React.ReactElement; -} - -export interface PalettePanelProps { +const usePaletteStyles = makeStyles((theme) => ({ + uiElementsTree: { + marginBottom: theme.spacing(1), + }, +})); +interface PalettePanelProps { paletteTabs?: PaletteTab[]; + open: boolean; + openDrawer: () => void; + onSelected: (tabName: string) => void; } - export const defaultPalettePanelTabs: PaletteTab[] = [ + { + name: 'Palette', + Component: () => { + const classes = usePaletteStyles(); + const schema: SchemaElement | undefined = useSchema(); + const paletteService = usePaletteService(); + return ( + <> + + {' '} + + ); + }, + icon: , + }, + { + name: 'Properties', + Component: () => { + const propertyRenderers = usePropertyRenderers(); + return ; + }, + icon: , + }, { name: 'JSON Schema', - Component: , + Component: () => , + icon: , + }, + { + name: 'UI Schema', + Component: () => , + icon: , }, - { name: 'UI Schema', Component: }, ]; - -export const PalettePanel: React.FC = ({ paletteTabs }) => { +export const PalettePanel = ({ + paletteTabs, + open, + openDrawer, + onSelected, +}: PalettePanelProps) => { + const classes = useStyles(); const [selectedTab, setSelectedTab] = useState(0); - const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => { + const handleTabChange = (newValue: number, name: string) => { setSelectedTab(newValue); + if (!open) { + openDrawer(); + } + onSelected(name); }; - const schema: SchemaElement | undefined = useSchema(); - const paletteService = usePaletteService(); - const classes = useStyles(); + return ( -
- - - {paletteTabs - ? paletteTabs.map((tab) => ( - - )) - : null} - - - - - - {paletteTabs - ? paletteTabs.map((tab, index) => ( - - {tab.Component} - - )) - : null} -
+ + + + {paletteTabs?.map((pt, i) => { + return ( + handleTabChange(i, pt.name)} + selected={selectedTab === i} + key={`${pt.name.toLowerCase()}_tab`} + > + + {pt.icon} + + + ); + })} + + + + {open && ( + + {paletteTabs?.map((pt, i) => { + if (selectedTab === i) + return ( + + ); + return undefined; + })} + + )} + ); }; diff --git a/jsonforms-editor/src/palette-panel/components/UISchemaPanel.tsx b/jsonforms-editor/src/palette-panel/components/UISchemaPanel.tsx index 2d0add0..e692c9e 100644 --- a/jsonforms-editor/src/palette-panel/components/UISchemaPanel.tsx +++ b/jsonforms-editor/src/palette-panel/components/UISchemaPanel.tsx @@ -27,7 +27,7 @@ export const UISchemaPanel: React.FC = ({ const dispatch = useDispatch(); const exportUiSchema = useExportUiSchema(); const uiSchema = useUiSchema(); - const showDebugSchema = env().DEBUG === 'true'; + const showDebugSchema = env().IS_DEBUG; const handleUiSchemaUpdate = (newUiSchema: string): UpdateResult => { try { const newUiSchemaObject = JSON.parse(newUiSchema); diff --git a/jsonforms-editor/src/palette-panel/index.ts b/jsonforms-editor/src/palette-panel/index.ts index ea2b5b7..f627018 100644 --- a/jsonforms-editor/src/palette-panel/index.ts +++ b/jsonforms-editor/src/palette-panel/index.ts @@ -6,7 +6,6 @@ * --------------------------------------------------------------------- */ -export type { PaletteTab } from './components/PalletePanel'; export * from './components/PalletePanel'; export { JsonSchemaPanel } from './components/JsonSchemaPanel'; export * from './components/UISchemaPanel'; diff --git a/jsonforms-editor/src/properties/components/PropertiesPanel.tsx b/jsonforms-editor/src/properties/components/PropertiesPanel.tsx index 419623e..9f14ccd 100644 --- a/jsonforms-editor/src/properties/components/PropertiesPanel.tsx +++ b/jsonforms-editor/src/properties/components/PropertiesPanel.tsx @@ -5,24 +5,52 @@ * https://github.com/eclipsesource/jsonforms-editor/blob/master/LICENSE * --------------------------------------------------------------------- */ -import { JsonFormsRendererRegistryEntry } from '@jsonforms/core'; -import { Typography } from '@material-ui/core'; -import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Tab from '@material-ui/core/Tab'; +import Tabs from '@material-ui/core/Tabs'; +import React, { useState } from 'react'; -import { Properties } from './Properties'; +import { TabContent } from '../../core/components'; +import { PreviewTab } from '../../editor'; +const useStyles = makeStyles((theme) => ({ + palettePanel: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, +})); export interface PropertiesPanelProps { - propertyRenderers: JsonFormsRendererRegistryEntry[]; + previewTabs?: PreviewTab[]; } export const PropertiesPanel: React.FC = ({ - propertyRenderers, + previewTabs, }) => { + const [selectedTab, setSelectedTab] = useState(0); + const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setSelectedTab(newValue); + }; + const classes = useStyles(); return ( - <> - - Properties - - - +
+ + {previewTabs + ? previewTabs.map((tab) => ( + + )) + : null} + + {previewTabs + ? previewTabs.map((tab, index) => ( + + + + )) + : null} +
); };