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

Automatically wrap non-layout components in box containers #804

Merged
merged 11 commits into from
Aug 22, 2022
3 changes: 3 additions & 0 deletions packages/toolpad-app/src/appDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SecretAttrValue,
} from '@mui/toolpad-core';
import invariant from 'invariant';
import { BoxProps } from '@mui/material';
import { ConnectionStatus, AppTheme } from './types';
import { omit, update, updateOrCreate } from './utils/immutability';
import { camelCase, generateUniqueString, removeDiacritics } from './utils/strings';
Expand Down Expand Up @@ -91,6 +92,8 @@ export interface ElementNode<P = any> extends AppDomNodeBase {
};
readonly props?: BindableAttrValues<P>;
readonly layout?: {
readonly boxAlign?: ConstantAttrValue<BoxProps['alignItems']>;
readonly boxJustify?: ConstantAttrValue<BoxProps['justifyContent']>;
readonly columnSize?: ConstantAttrValue<number>;
};
}
Expand Down
41 changes: 39 additions & 2 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Stack,
CssBaseline,
Alert,
Box,
styled,
AlertTitle,
LinearProgress,
Expand Down Expand Up @@ -42,7 +43,12 @@ import { pick } from 'lodash-es';
import * as appDom from '../appDom';
import { VersionOrPreview } from '../types';
import { createProvidedContext } from '../utils/react';
import { getElementNodeComponentId, isPageRow, PAGE_ROW_COMPONENT_ID } from '../toolpadComponents';
import {
getElementNodeComponentId,
isPageLayoutComponent,
isPageRow,
PAGE_ROW_COMPONENT_ID,
} from '../toolpadComponents';
import AppOverview from './AppOverview';
import AppThemeProvider from './AppThemeProvider';
import evalJsBindings, {
Expand All @@ -57,6 +63,7 @@ import usePageTitle from '../utils/usePageTitle';
import ComponentsContext, { useComponents, useComponent } from './ComponentsContext';
import { AppModulesProvider, useAppModules } from './AppModulesProvider';
import Pre from '../components/Pre';
import { layoutBoxArgTypes } from '../toolpadComponents/layoutBox';

const USE_DATA_QUERY_CONFIG_KEYS: readonly (keyof UseDataQueryConfig)[] = [
'enabled',
Expand Down Expand Up @@ -292,9 +299,28 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
}
}

const isLayoutNode =
appDom.isPage(node) || (appDom.isElement(node) && isPageLayoutComponent(node));

return (
<NodeRuntimeWrapper nodeId={nodeId} componentConfig={Component[TOOLPAD_COMPONENT]}>
<Component {...props} />
{isLayoutNode ? (
<Component {...props} />
) : (
<Box
sx={{
display: 'flex',
alignItems:
(componentConfig.hasLayoutBoxAlign && node.layout?.boxAlign?.value) ||
layoutBoxArgTypes.boxAlign.defaultValue,
justifyContent:
(componentConfig.hasLayoutBoxJustify && node.layout?.boxJustify?.value) ||
layoutBoxArgTypes.boxJustify.defaultValue,
}}
>
<Component {...props} />
</Box>
)}
</NodeRuntimeWrapper>
);
}
Expand Down Expand Up @@ -437,6 +463,17 @@ function parseBindings(
}
}
}

if (!isPageLayoutComponent(elm)) {
for (const [propName, argType] of Object.entries(layoutBoxArgTypes)) {
const binding =
elm.layout?.[propName as keyof typeof layoutBoxArgTypes] ||
appDom.createConst(argType?.defaultValue ?? undefined);
const bindingId = `${elm.id}.layout.${propName}`;
const scopePath = `${elm.name}.${propName}`;
parsedBindingsMap.set(bindingId, parseBinding(binding, { scopePath }));
}
}
}

if (appDom.isQuery(elm)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ErrorAlert from './ErrorAlert';
import NodeNameEditor from '../NodeNameEditor';
import { useToolpadComponent } from '../toolpadComponents';
import { getElementNodeComponentId } from '../../../toolpadComponents';
import { layoutBoxArgTypes } from '../../../toolpadComponents/layoutBox';

const classes = {
control: 'Toolpad_Control',
Expand All @@ -35,8 +36,41 @@ interface ComponentPropsEditorProps<P> {
}

function ComponentPropsEditor<P>({ componentConfig, node }: ComponentPropsEditorProps<P>) {
const hasLayoutControls =
componentConfig.hasLayoutBoxAlign || componentConfig.hasLayoutBoxJustify;

return (
<ComponentPropsEditorRoot>
{hasLayoutControls ? (
<React.Fragment>
<Typography variant="subtitle2" sx={{ mt: 1 }}>
Layout:
</Typography>
{componentConfig.hasLayoutBoxJustify ? (
<div className={classes.control}>
<NodeAttributeEditor
node={node}
namespace="layout"
name="boxJustify"
argType={layoutBoxArgTypes.boxJustify}
/>
</div>
) : null}
{componentConfig.hasLayoutBoxAlign ? (
<div className={classes.control}>
<NodeAttributeEditor
node={node}
namespace="layout"
name="boxAlign"
argType={layoutBoxArgTypes.boxAlign}
/>
</div>
) : null}
</React.Fragment>
) : null}
<Typography variant="subtitle2" sx={{ mt: hasLayoutControls ? 2 : 1 }}>
Properties:
</Typography>
{(Object.entries(componentConfig.argTypes) as ExactEntriesOf<ArgTypeDefinitions<P>>).map(
([propName, propTypeDef]) =>
propTypeDef && shouldRenderControl(propTypeDef) ? (
Expand Down Expand Up @@ -78,9 +112,6 @@ function SelectedNodeEditor({ node }: SelectedNodeEditorProps) {
{nodeError ? <ErrorAlert error={nodeError} /> : null}
{node ? (
<React.Fragment>
<Typography variant="subtitle1" sx={{ mt: 2 }}>
Properties:
</Typography>
<ComponentPropsEditor componentConfig={componentConfig} node={node} />
</React.Fragment>
) : null}
Expand Down
27 changes: 27 additions & 0 deletions packages/toolpad-app/src/toolpadComponents/layoutBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BoxProps } from '@mui/material';
import { ArgTypeDefinition } from '@mui/toolpad-core';

const layoutBoxAlignArgTypeDef: ArgTypeDefinition<BoxProps['alignItems']> = {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Box vertical alignment',
control: { type: 'VerticalAlign' },
defaultValue: 'center',
};

const layoutBoxJustifyArgTypeDef: ArgTypeDefinition<BoxProps['justifyContent']> = {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Box horizontal alignment',
control: { type: 'HorizontalAlign' },
defaultValue: 'start',
};

export const layoutBoxArgTypes = {
boxAlign: layoutBoxAlignArgTypeDef,
boxJustify: layoutBoxJustifyArgTypeDef,
};
37 changes: 4 additions & 33 deletions packages/toolpad-components/src/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import * as React from 'react';
import { LoadingButton as MuiButton, LoadingButtonProps as MuiButtonProps } from '@mui/lab';
import { createComponent } from '@mui/toolpad-core';
import { Box, BoxProps } from '@mui/material';

interface ButtonProps extends Omit<MuiButtonProps, 'children'> {
content: string;
alignItems?: BoxProps['alignItems'];
justifyContent?: BoxProps['justifyContent'];
}

function Button({ content, alignItems, justifyContent, ...props }: ButtonProps) {
return (
<Box
sx={{
display: 'flex',
alignItems,
justifyContent,
}}
>
<MuiButton {...props}>{content}</MuiButton>
</Box>
);
function Button({ content, ...rest }: ButtonProps) {
return <MuiButton {...rest}>{content}</MuiButton>;
}

export default createComponent(Button, {
hasLayoutBoxAlign: true,
hasLayoutBoxJustify: true,
argTypes: {
content: {
typeDef: { type: 'string' },
Expand All @@ -39,24 +28,6 @@ export default createComponent(Button, {
disabled: {
typeDef: { type: 'boolean' },
},
alignItems: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Vertical alignment',
control: { type: 'VerticalAlign' },
defaultValue: 'center',
},
justifyContent: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Horizontal alignment',
control: { type: 'HorizontalAlign' },
defaultValue: 'start',
},
fullWidth: {
typeDef: { type: 'boolean' },
},
Expand Down
42 changes: 20 additions & 22 deletions packages/toolpad-components/src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,28 +252,26 @@ const DataGridComponent = React.forwardRef(function DataGridComponent(
const columns: GridColumns = columnsProp || EMPTY_COLUMNS;

return (
<Box>
<div ref={ref} style={{ height: heightProp, minHeight: '100%', width: '100%' }}>
<DataGridPro
components={{ Toolbar: GridToolbar, LoadingOverlay: SkeletonLoadingOverlay }}
onColumnResize={handleResize}
onColumnOrderChange={handleColumnOrderChange}
rows={rows}
columns={columns}
key={rowIdFieldProp}
getRowId={getRowId}
onSelectionModelChange={onSelectionModelChange}
selectionModel={selectionModel}
error={errorProp}
componentsProps={{
errorOverlay: {
message: typeof errorProp === 'string' ? errorProp : errorProp?.message,
},
}}
{...props}
/>
</div>
</Box>
<div ref={ref} style={{ height: heightProp, minHeight: '100%', width: '100%' }}>
<DataGridPro
components={{ Toolbar: GridToolbar, LoadingOverlay: SkeletonLoadingOverlay }}
onColumnResize={handleResize}
onColumnOrderChange={handleColumnOrderChange}
rows={rows}
columns={columns}
key={rowIdFieldProp}
getRowId={getRowId}
onSelectionModelChange={onSelectionModelChange}
selectionModel={selectionModel}
error={errorProp}
componentsProps={{
errorOverlay: {
message: typeof errorProp === 'string' ? errorProp : errorProp?.message,
},
}}
{...props}
/>
</div>
);
});

Expand Down
74 changes: 19 additions & 55 deletions packages/toolpad-components/src/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Skeleton, SxProps, BoxProps } from '@mui/material';
import { Box, Skeleton, SxProps } from '@mui/material';
import * as React from 'react';
import { createComponent } from '@mui/toolpad-core';

Expand All @@ -10,21 +10,9 @@ export interface ImageProps {
height: number;
loading?: boolean;
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
alignItems?: BoxProps['alignItems'];
justifyContent?: BoxProps['justifyContent'];
}

function Image({
sx: sxProp,
src,
width,
height,
alt,
loading: loadingProp,
fit,
alignItems,
justifyContent,
}: ImageProps) {
function Image({ sx: sxProp, src, width, height, alt, loading: loadingProp, fit }: ImageProps) {
const sx: SxProps = React.useMemo(
() => ({
...sxProp,
Expand All @@ -44,34 +32,28 @@ function Image({
const loading = loadingProp || imgLoading;

return (
<Box
sx={{
display: 'flex',
alignItems,
justifyContent,
}}
>
<Box sx={sx}>
{loading ? <Skeleton variant="rectangular" width={width} height={height} /> : null}
<Box
component="img"
src={src}
alt={alt}
sx={{
width: '100%',
height: '100%',
objectFit: fit,
position: 'absolute',
visibility: loading ? 'hidden' : 'visible',
}}
onLoad={handleLoad}
/>
</Box>
<Box sx={sx}>
{loading ? <Skeleton variant="rectangular" width={width} height={height} /> : null}
<Box
component="img"
src={src}
alt={alt}
sx={{
width: '100%',
height: '100%',
objectFit: fit,
position: 'absolute',
visibility: loading ? 'hidden' : 'visible',
}}
onLoad={handleLoad}
/>
</Box>
);
}

export default createComponent(Image, {
hasLayoutBoxAlign: true,
hasLayoutBoxJustify: true,
loadingPropSource: ['src'],
loadingProp: 'loading',
argTypes: {
Expand All @@ -98,24 +80,6 @@ export default createComponent(Image, {
typeDef: { type: 'boolean' },
defaultValue: false,
},
alignItems: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Vertical alignment',
control: { type: 'VerticalAlign' },
defaultValue: 'center',
},
justifyContent: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Horizontal alignment',
control: { type: 'HorizontalAlign' },
defaultValue: 'start',
},
sx: {
typeDef: { type: 'object' },
defaultValue: { maxWidth: '100%' },
Expand Down
Loading