Skip to content

Commit

Permalink
Improve columns editor UX (#2570)
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Aug 30, 2023
1 parent 2d406b4 commit 72a59db
Showing 1 changed file with 194 additions and 116 deletions.
310 changes: 194 additions & 116 deletions packages/toolpad-app/src/toolpad/propertyControls/GridColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
Popover,
Stack,
TextField,
TextFieldProps,
TextFieldVariants,
Tooltip,
} from '@mui/material';
import * as React from 'react';
Expand Down Expand Up @@ -48,6 +50,193 @@ const COLUMN_TYPES: string[] = [
];
const ALIGNMENTS: GridAlignment[] = ['left', 'right', 'center'];

type ImmediateInputProps<V extends TextFieldVariants = TextFieldVariants> = TextFieldProps<V> & {
validate?: (input: string) => string | null;
};

interface ImmediateInputState {
input: string;
error: string | null;
}

function useImmediateTextField<V extends TextFieldVariants = TextFieldVariants>(
props: ImmediateInputProps<V>,
): TextFieldProps<V> {
const { value, onChange, error, helperText, required, onBlur, validate } = props;
const createInputState = React.useCallback(
(rawInput: unknown): ImmediateInputState => {
const input = String(rawInput);
let inputError = null;
if (required && !input) {
inputError = 'Input required';
} else if (validate) {
inputError = validate(input);
}
return { input, error: inputError };
},
[validate, required],
);
const [state, setState] = React.useState<ImmediateInputState>(createInputState(value));
React.useEffect(() => {
setState(createInputState(value));
}, [value, createInputState]);

return {
...props,
value: state.input,
error: !!state.error || error,
helperText: state.error || helperText,
required,
onBlur: (event) => {
if (state.input !== value) {
setState(createInputState(value));
}
onBlur?.(event);
},
onChange: (event) => {
const newState = createInputState(event.target.value);
setState(newState);
if (!newState.error) {
onChange?.(event);
}
},
};
}

interface GridColumnEditorProps {
value: SerializableGridColumn;
onChange: (newValue: SerializableGridColumn) => void;
disabled?: boolean;
}

function GridColumnEditor({
disabled,
value: editedColumn,
onChange: handleColumnChange,
}: GridColumnEditorProps) {
const { dom } = useDom();
const toolpadComponents = useToolpadComponents(dom);
const codeComponents: ToolpadComponentDefinition[] = React.useMemo(() => {
return Object.values(toolpadComponents)
.filter(Boolean)
.filter((definition) => !definition.builtIn);
}, [toolpadComponents]);

const fieldInput = useImmediateTextField({
label: 'field',
disabled,
required: true,
value: editedColumn.field,
onChange: (event) => {
handleColumnChange({ ...editedColumn, field: event.target.value });
},
});

return (
<Stack gap={1} py={1}>
<TextField {...fieldInput} />

<TextField
label="header"
value={editedColumn.headerName || ''}
disabled={disabled}
onChange={(event) =>
handleColumnChange({
...editedColumn,
headerName: event.target.value ? event.target.value : undefined,
})
}
/>

<TextField
select
fullWidth
label="align"
value={editedColumn.align ?? ''}
disabled={disabled}
onChange={(event) =>
handleColumnChange({
...editedColumn,
align: (event.target.value as GridAlignment) || undefined,
})
}
>
{ALIGNMENTS.map((alignment) => (
<MenuItem key={alignment} value={alignment}>
{alignment}
</MenuItem>
))}
</TextField>

<TextField
label="width"
type="number"
value={editedColumn.width}
disabled={disabled}
onChange={(event) =>
handleColumnChange({ ...editedColumn, width: Number(event.target.value) })
}
/>

<TextField
select
fullWidth
label="type"
value={editedColumn.type ?? ''}
disabled={disabled}
onChange={(event) =>
handleColumnChange({
...editedColumn,
type: event.target.value,
numberFormat: undefined,
})
}
>
{COLUMN_TYPES.map((type) => (
<MenuItem key={type} value={type}>
{type}
</MenuItem>
))}
</TextField>

<Box sx={{ ml: 1, pl: 1, borderLeft: 1, borderColor: 'divider' }}>
{editedColumn.type === 'number' ? (
<NumberFormatEditor
disabled={disabled}
value={editedColumn.numberFormat}
onChange={(numberFormat) => handleColumnChange({ ...editedColumn, numberFormat })}
/>
) : null}

{editedColumn.type === 'codeComponent' ? (
<TextField
select
required
fullWidth
label="Custom component"
value={editedColumn.codeComponent ?? ''}
disabled={disabled}
error={!editedColumn.codeComponent}
helperText={editedColumn.codeComponent ? undefined : 'Please select a component'}
onChange={(event) =>
handleColumnChange({
...editedColumn,
codeComponent: event.target.value,
})
}
>
{codeComponents.map(({ displayName }) => (
<MenuItem key={displayName} value={displayName}>
{displayName}
</MenuItem>
))}
</TextField>
) : null}
</Box>
</Stack>
);
}

function GridColumnsPropEditor({
propType,
label,
Expand All @@ -58,13 +247,6 @@ function GridColumnsPropEditor({
}: EditorProps<SerializableGridColumns>) {
const { bindings } = usePageEditorState();
const [editedIndex, setEditedIndex] = React.useState<number | null>(null);
const { dom } = useDom();
const toolpadComponents = useToolpadComponents(dom);
const codeComponents: ToolpadComponentDefinition[] = React.useMemo(() => {
return Object.values(toolpadComponents)
.filter(Boolean)
.filter((definition) => !definition.builtIn);
}, [toolpadComponents]);

const editedColumn = typeof editedIndex === 'number' ? value[editedIndex] : null;

Expand Down Expand Up @@ -177,115 +359,11 @@ function GridColumnsPropEditor({
<IconButton aria-label="Back" onClick={() => setEditedIndex(null)}>
<ArrowBackIcon />
</IconButton>
<Stack gap={1} py={1}>
<TextField
label="field"
value={editedColumn.field}
disabled={disabled}
onChange={(event) =>
handleColumnChange({ ...editedColumn, field: event.target.value })
}
/>

<TextField
label="header"
value={editedColumn.headerName}
disabled={disabled}
onChange={(event) =>
handleColumnChange({ ...editedColumn, headerName: event.target.value })
}
/>

<TextField
select
fullWidth
label="align"
value={editedColumn.align ?? ''}
disabled={disabled}
onChange={(event) =>
handleColumnChange({
...editedColumn,
align: (event.target.value as GridAlignment) || undefined,
})
}
>
{ALIGNMENTS.map((alignment) => (
<MenuItem key={alignment} value={alignment}>
{alignment}
</MenuItem>
))}
</TextField>

<TextField
label="width"
type="number"
value={editedColumn.width}
disabled={disabled}
onChange={(event) =>
handleColumnChange({ ...editedColumn, width: Number(event.target.value) })
}
/>

<TextField
select
fullWidth
label="type"
value={editedColumn.type ?? ''}
disabled={disabled}
onChange={(event) =>
handleColumnChange({
...editedColumn,
type: event.target.value,
numberFormat: undefined,
})
}
>
{COLUMN_TYPES.map((type) => (
<MenuItem key={type} value={type}>
{type}
</MenuItem>
))}
</TextField>

<Box sx={{ ml: 1, pl: 1, borderLeft: 1, borderColor: 'divider' }}>
{editedColumn.type === 'number' ? (
<NumberFormatEditor
disabled={disabled}
value={editedColumn.numberFormat}
onChange={(numberFormat) =>
handleColumnChange({ ...editedColumn, numberFormat })
}
/>
) : null}

{editedColumn.type === 'codeComponent' ? (
<TextField
select
required
fullWidth
label="Custom component"
value={editedColumn.codeComponent ?? ''}
disabled={disabled}
error={!editedColumn.codeComponent}
helperText={
editedColumn.codeComponent ? undefined : 'Please select a component'
}
onChange={(event) =>
handleColumnChange({
...editedColumn,
codeComponent: event.target.value,
})
}
>
{codeComponents.map(({ displayName }) => (
<MenuItem key={displayName} value={displayName}>
{displayName}
</MenuItem>
))}
</TextField>
) : null}
</Box>
</Stack>
<GridColumnEditor
value={editedColumn}
onChange={handleColumnChange}
disabled={disabled}
/>
</React.Fragment>
) : (
<React.Fragment>
Expand Down

0 comments on commit 72a59db

Please sign in to comment.