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

Configurable components #8145

Merged
merged 87 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
02a7062
Proof of concept
fzaninotto Sep 5, 2022
9a22e2f
Improve DX
fzaninotto Sep 5, 2022
f742d34
UI tweaks
fzaninotto Sep 5, 2022
dae7b4f
Add transition
fzaninotto Sep 6, 2022
514d6a7
more complete story
fzaninotto Sep 6, 2022
a22557d
Add nested story
fzaninotto Sep 6, 2022
0efb4bd
Change hover style
fzaninotto Sep 6, 2022
aa6284d
UI tweaks
fzaninotto Sep 6, 2022
0eb46bc
inject Ref
fzaninotto Sep 6, 2022
26b2b94
label tweaking
fzaninotto Sep 6, 2022
7cce85c
support multiple Instances
fzaninotto Sep 6, 2022
aa8921f
untype ref
fzaninotto Sep 6, 2022
b33485a
export components
fzaninotto Sep 6, 2022
d36ae79
Enable InspectorContext by default
fzaninotto Sep 6, 2022
7d413fe
Fix configurable stories title
fzaninotto Sep 6, 2022
67b54f2
Work even when not in context
fzaninotto Sep 6, 2022
67fb84c
Change naming convention
fzaninotto Sep 6, 2022
20eb118
Use outline for accessibility
fzaninotto Sep 6, 2022
b166421
Add Reset£SettingsButton
fzaninotto Sep 6, 2022
5df71b2
Rename preferencesKey
fzaninotto Sep 6, 2022
8812a30
Fix warning
fzaninotto Sep 6, 2022
fade9cc
add renderTemplate
fzaninotto Sep 6, 2022
bd75bf5
Improve API
fzaninotto Sep 6, 2022
52b4d62
Make SimpleList configurable
fzaninotto Sep 6, 2022
0f04331
Add English translations
fzaninotto Sep 6, 2022
73571db
Move SimpleList to a dedicated folder
fzaninotto Sep 6, 2022
d840efb
Change naming strategy
fzaninotto Sep 6, 2022
6c9867c
Rename configurable to preferences
fzaninotto Sep 6, 2022
db6f57e
Fix linter warning
fzaninotto Sep 6, 2022
49c3c03
Fix typo
fzaninotto Sep 6, 2022
33c6f9d
Add French locale
fzaninotto Sep 6, 2022
67b60ed
Fix bad move
fzaninotto Sep 6, 2022
fce8a69
Document SimpleListConfigurable
fzaninotto Sep 6, 2022
e0d5406
Document Configurable
fzaninotto Sep 6, 2022
6c8129f
Move usePreferenceInput to core
fzaninotto Sep 7, 2022
e1801f1
rename preferencesKey to preferenceKey
fzaninotto Sep 7, 2022
aa01db2
Improve SimpleList doc
fzaninotto Sep 7, 2022
af5cecf
ui tweaks
fzaninotto Sep 7, 2022
fde4de5
Custom drag and drop
fzaninotto Sep 7, 2022
5b26683
Home made darg and drop
fzaninotto Sep 7, 2022
430fdc2
inspector tweaks
fzaninotto Sep 7, 2022
d85f9e7
Make page title configurable
fzaninotto Sep 7, 2022
74313a1
Fix preference key for page title and SimpleList
fzaninotto Sep 8, 2022
d036722
Fix inspector position on resize to make it always visible
fzaninotto Sep 8, 2022
331789c
Handle Enter key in inspector
fzaninotto Sep 9, 2022
7cc78f8
Fix inspector shows unmounted component editor
fzaninotto Sep 9, 2022
4575d13
Fix cannot select test in inspector inputs
fzaninotto Sep 12, 2022
f680934
add tests
fzaninotto Sep 12, 2022
0a59b3b
Add Inspector tests
fzaninotto Sep 12, 2022
db92a56
Add tests for Configurable
fzaninotto Sep 12, 2022
cd3ef4f
Fix confirm button icon size
fzaninotto Sep 12, 2022
c74b49b
Fix warnings
fzaninotto Sep 12, 2022
33cc606
Fix unmounting case
fzaninotto Sep 13, 2022
854d729
Fix story moves inspector
fzaninotto Sep 13, 2022
2b4d8b4
Switch to MUI Badge
fzaninotto Sep 13, 2022
23a213f
Fix SimpleList stories
fzaninotto Sep 14, 2022
2c46483
Fix tests
fzaninotto Sep 14, 2022
8167516
Fix edge case
fzaninotto Sep 14, 2022
db31b24
Fix title editor
fzaninotto Sep 14, 2022
020ddca
Add RemoveItemsFromStore
fzaninotto Sep 14, 2022
bcca22a
Add reset settings button to all editors
fzaninotto Sep 14, 2022
e4f46d9
Fix ConfigurableList style
fzaninotto Sep 14, 2022
dfa1825
Fix SimpleListConfigurable screencast
fzaninotto Sep 14, 2022
c54d92c
No need for a ref
fzaninotto Sep 14, 2022
23b6a53
Do not inject props, do not require to forward refs
fzaninotto Sep 14, 2022
c6aab3e
SimpleList doesn't need forwardRef
fzaninotto Sep 14, 2022
c7ca2b8
Fix typo
fzaninotto Sep 14, 2022
d1f4c9e
Fix regression in logout
fzaninotto Sep 14, 2022
d066e50
Make sure the Inspector works when no document or window is set
fzaninotto Sep 21, 2022
3284d76
Improve Configurable doc
fzaninotto Sep 21, 2022
7ad21c1
Make default i18nProvider smarter
fzaninotto Sep 24, 2022
c4a6ac6
use translate instead of lodash as template engine
fzaninotto Sep 24, 2022
e2f9c41
update SimpleList screencast
fzaninotto Sep 24, 2022
c8f77fd
Update SimpleList doc
fzaninotto Sep 24, 2022
fdf9994
Change SimpleListConfigurable approach
fzaninotto Sep 24, 2022
596727b
Add usePreference hook
fzaninotto Sep 24, 2022
2597807
Add jsDoc
fzaninotto Sep 24, 2022
252a70f
Fix customize isn't translated
fzaninotto Sep 24, 2022
9d17187
Update configurable stories to better reflect syntax
fzaninotto Sep 24, 2022
284df37
Fix typos
fzaninotto Sep 24, 2022
1b08872
Fix default title preference key
fzaninotto Sep 24, 2022
771e634
Fix edge case
fzaninotto Sep 24, 2022
b3492a6
Fix PageTitle
fzaninotto Sep 24, 2022
2009230
Update configurable documentation
fzaninotto Sep 24, 2022
8a4ce2a
Fix linter warning
fzaninotto Sep 24, 2022
27b15b1
Review
fzaninotto Sep 28, 2022
7315488
export substituteTokens
fzaninotto Sep 28, 2022
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
123 changes: 123 additions & 0 deletions docs/Configurable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
layout: default
title: "The Configurable Component"
---

# `<Configurable>`

This component makes another component configurable by the end user. When they enter the configuration mode, users can customize the component's settings via the inspector.

![SimpleListConfigurable](./img/SimpleListConfigurable.gif)

Some react-admin components are already configurable - or rather they have a configurable counterpart:

- [`<SimpleListConfigurable>`](./SimpleList.md#configurable)

## Usage

Wrap any component with `<Configurable>` and define its editor to let users customize it via a UI. Don't forget to pass down props to the inner component. Note that every configurable component needs a unique preference key, that is used to persist the user's preferences in the Store.

```jsx
import { Configurable } from 'react-admin';

const ConfigurableTextBlock = ({ preferenceKey = "textBlock", ...props }) => (
<Configurable editor={<TextBlockEditor />} preferenceKey={preferenceKey}>
<TextBlock {...props} />
</Configurable>
);
```

`<Configurable>` creates a context for the `preferenceKey`, so that both the child component and the editor can access it using `usePreferenceKey()`.

Then, use this component in your app:

```jsx
import { ConfigurableTextBlock } from './ConfigurableTextBlock';
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

export const Dashboard = () => (
<ConfigurableTextBlock
title="Welcome to the administration"
content="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
/>
);
```

## `children`

The wrapped component must use `usePreferenceKey` to get the preference key, and [`useStore`](./useStore.md) to access the configuration.

```jsx
import { useStore, usePreferenceKey } from 'react-admin';

const TextBlock = ({ title, content }) => {
const preferenceKey = usePreferenceKey();
const [color] = useStore(`${preferenceKey}.color`, '#ffffff');
return (
<Box bgcolor={color}>
<Typography variant="h6">{title}</Typography>
<Typography>{content}</Typography>
</Box>
);
};
```

## `editor`

The editor component must also use `usePreferenceKey` to get the preference key, and [`useStore`](./useStore.md) to read and write the configuration. When the user selects the configurable component, react-admin renders the `editor` component in the inspector.

```jsx
import { useStore, usePreferenceKey } from 'react-admin';

const TextBlockEditor = ({ preferenceKey }) => {
const preferenceKey = usePreferenceKey();
const [color, setColor] = useStore(`${preferenceKey}.color`, '#ffffff');
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
return (
<Box>
<Typography>Configure the text block</Typography>
<TextField
label="Color"
value={color}
onChange={e => setColor(e.target.value)}
/>
</Box>
);
};
```

## `preferenceKey`

This parameter lets you specify the key used to store the configuration in the user's preferences. This allows you to have more than one configurable component of the same type per page.

```jsx
import { Configurable } from 'react-admin';

const ConfigurableTextBlock = ({ preferenceKey, ...props }) => (
<Configurable editor={<TextBlockInspector />} preferenceKey={preferenceKey}>
<TextBlock {...props} />
</Configurable>
);
```

Then in your application, set the `preferenceKey` prop to a unique value for each component:

```jsx
import { ConfigurableTextBlock } from './ConfigurableTextBlock';

export const Dashboard = () => (
<>
<ConfigurableTextBlock
preferenceKey="textBlock1"
title="Welcome to the administration"
content="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
/>
<ConfigurableTextBlock
preferenceKey="textBlock2"
title="Security reminder"
content="Nullam bibendum orci tortor, a posuere arcu sollicitudin ac"
/>
</>
);
```

Users will be able to customize each component independently.

32 changes: 32 additions & 0 deletions docs/SimpleList.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,35 @@ export const PostList = props => {
);
}
```

## Configurable

You can let end users customize the fields displayed in the `<SimpleList>` by using the `<SimpleListConfigurable>` component instead.

![SimpleListConfigurable](./img/SimpleListConfigurable.gif)

```diff
import {
List,
- SimpleList,
+ SimpleListConfigurable,
} from 'react-admin';

export const BookList = () => (
<List>
- <SimpleList
+ <SimpleListConfigurable
primaryText={record => record.title}
secondaryText={record => record.author}
tertiaryText={record => record.date}
/>
</List>
);
```

When users enter the configuration mode and select the `<SimpleList>`, they can set the `primaryText`, `secondaryText`, and `tertiaryText` fields via the inspector. `<SimpleList>` uses a simple templating engine (based on [`lodash.template()`](https://lodash.com/docs/4.17.15#template)) to render the fields. The template receives the current record as parameter. This means users can access the record field using the `${field}` syntax, e.g.:

```
Title: ${title} (by ${author})
```

Binary file added docs/img/SimpleListConfigurable.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,12 @@

<ul><div>The Store</div>
<li {% if page.path == 'Store.md' %} class="active" {% endif %}><a class="nav-link" href="./Store.html">Introduction</a></li>
<li {% if page.path == 'LocalesMenuButton.md' %} class="active" {% endif %}><a class="nav-link" href="./LocalesMenuButton.html"><code>&lt;LocalesMenuButton&gt;</code></a></li>
WiXSL marked this conversation as resolved.
Show resolved Hide resolved
<li {% if page.path == 'ToggleThemeButton.md' %} class="active" {% endif %}><a class="nav-link" href="./ToggleThemeButton.html"><code>&lt;ToggleThemeButton&gt;</code></a></li>
<li {% if page.path == 'useStore.md' %} class="active" {% endif %}><a class="nav-link" href="./useStore.html"><code>useStore</code></a></li>
<li {% if page.path == 'useRemoveFromStore.md' %} class="active" {% endif %}><a class="nav-link" href="./useRemoveFromStore.html"><code>useRemoveFromStore</code></a></li>
<li {% if page.path == 'useResetStore.md' %} class="active" {% endif %}><a class="nav-link" href="./useResetStore.html"><code>useResetStore</code></a></li>
<li {% if page.path == 'useStoreContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useStoreContext.html"><code>useStoreContext</code></a></li>
<li {% if page.path == 'Configurable.md' %} class="active" {% endif %}><a class="nav-link" href="./Configurable.html"><code>&lt;Configurable&gt;</code></a></li>
<li {% if page.path == 'ToggleThemeButton.md' %} class="active" {% endif %}><a class="nav-link" href="./ToggleThemeButton.html"><code>&lt;ToggleThemeButton&gt;</code></a></li>
</ul>

<ul><div>I18N Provider and Translations</div>
Expand Down
15 changes: 11 additions & 4 deletions examples/simple/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import * as React from 'react';

import { memo } from 'react';
import { ReactQueryDevtools } from 'react-query/devtools';
import { Layout } from 'react-admin';
import { CssBaseline } from '@mui/material';
import { AppBar, Layout, InspectorButton } from 'react-admin';
import { CssBaseline, Typography } from '@mui/material';

const MyAppBar = memo(props => (
<AppBar {...props}>
<Typography flex="1" variant="h6" id="react-admin-title" />
<InspectorButton />
</AppBar>
));

export default props => (
<>
<CssBaseline />
<Layout {...props} />
<Layout {...props} appBar={MyAppBar} />
<ReactQueryDevtools
initialIsOpen={false}
toggleButtonProps={{ style: { width: 20, height: 30 } }}
Expand Down
25 changes: 14 additions & 11 deletions packages/ra-core/src/core/CoreAdminContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
defaultDataProvider,
} from '../dataProvider';
import { StoreContextProvider, Store, memoryStore } from '../store';
import { PreferencesEditorContextProvider } from '../preferences/PreferencesEditorContextProvider';
import { I18nContextProvider } from '../i18n';
import { ResourceDefinitionContextProvider } from './ResourceDefinitionContext';
import { NotificationContextProvider } from '../notification';
Expand Down Expand Up @@ -81,17 +82,19 @@ React-admin requires a valid dataProvider function to work.`);
<AuthContext.Provider value={finalAuthProvider}>
<DataProviderContext.Provider value={finalDataProvider}>
<StoreContextProvider value={store}>
<QueryClientProvider client={finalQueryClient}>
<AdminRouter history={history} basename={basename}>
<I18nContextProvider value={i18nProvider}>
<NotificationContextProvider>
<ResourceDefinitionContextProvider>
{children}
</ResourceDefinitionContextProvider>
</NotificationContextProvider>
</I18nContextProvider>
</AdminRouter>
</QueryClientProvider>
<PreferencesEditorContextProvider>
<QueryClientProvider client={finalQueryClient}>
<AdminRouter history={history} basename={basename}>
<I18nContextProvider value={i18nProvider}>
<NotificationContextProvider>
<ResourceDefinitionContextProvider>
{children}
</ResourceDefinitionContextProvider>
</NotificationContextProvider>
</I18nContextProvider>
</AdminRouter>
</QueryClientProvider>
</PreferencesEditorContextProvider>
</StoreContextProvider>
</DataProviderContext.Provider>
</AuthContext.Provider>
Expand Down
15 changes: 15 additions & 0 deletions packages/ra-core/src/i18n/TranslationMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,20 @@ export interface TranslationMessages extends StringMap {
remove_message: string;
help: string;
};
configurable?: {
customize: string;
templateError: string;
configureMode: string;
inspector: {
title: string;
content: string;
reset: string;
};
SimpleList: {
primaryText: string;
secondaryText: string;
tertiaryText: string;
};
};
};
}
11 changes: 6 additions & 5 deletions packages/ra-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export * from './core';
export * from './auth';
export * from './controller';
export * from './core';
export * from './preferences';
export * from './dataProvider';
export * from './export';
export * from './form';
export * from './i18n';
export * from './inference';
export * from './util';
export * from './controller';
export * from './form';
export * from './notification';
export * from './store';
export * from './routing';
export * from './store';
export * from './types';
export * from './util';
20 changes: 20 additions & 0 deletions packages/ra-core/src/preferences/PreferenceKeyContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { createContext, useContext } from 'react';

export const PreferenceKeyContext = createContext<string>('');

export const PreferenceKeyContextProvider = ({
value = '',
children,
}: {
value?: string;
children: React.ReactNode;
}) => (
<PreferenceKeyContext.Provider value={value}>
{children}
</PreferenceKeyContext.Provider>
);

export const usePreferenceKey = () => {
return useContext(PreferenceKeyContext);
};
21 changes: 21 additions & 0 deletions packages/ra-core/src/preferences/PreferencesEditorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';
import { createContext, ReactElement } from 'react';

export const PreferencesEditorContext = createContext<
PreferencesEditorContextValue
>(undefined);

export type PreferencesEditorContextValue = {
editor: ReactElement | null;
setEditor: React.Dispatch<React.SetStateAction<ReactElement>>;
preferenceKey?: string;
setPreferenceKey: React.Dispatch<React.SetStateAction<string>>;
title: string | null;
titleOptions?: any;
setTitle: (title: string, titleOptions?: any) => void;
isEnabled: boolean;
enable: () => void;
disable: () => void;
path: string | null;
setPath: (path: string) => void;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { ReactElement, useCallback, useMemo, useState } from 'react';
import {
PreferencesEditorContext,
PreferencesEditorContextValue,
} from './PreferencesEditorContext';

export const PreferencesEditorContextProvider = ({ children }) => {
const [isEnabled, setIsEnabled] = useState(false);
const [editor, setEditor] = useState<ReactElement>(null);
const [preferenceKey, setPreferenceKey] = useState<string>();
const [path, setPath] = useState<string>(null);
const [title, setTitleString] = useState<string>(null);
const [titleOptions, setTitleOptions] = useState<any>();
const enable = useCallback(() => setIsEnabled(true), []);
const disable = useCallback(() => {
setIsEnabled(false);
setEditor(null);
}, []);

const setTitle = useCallback((title: string, titleOptions?: any) => {
setTitleString(title);
setTitleOptions(titleOptions);
}, []);

const context = useMemo<PreferencesEditorContextValue>(() => {
return {
editor,
setEditor,
preferenceKey,
setPreferenceKey,
title,
titleOptions,
setTitle,
isEnabled,
disable,
enable,
path,
setPath,
};
}, [
disable,
enable,
editor,
preferenceKey,
isEnabled,
path,
setPath,
title,
titleOptions,
setTitle,
]);

return (
<PreferencesEditorContext.Provider value={context}>
{children}
</PreferencesEditorContext.Provider>
);
};
7 changes: 7 additions & 0 deletions packages/ra-core/src/preferences/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './PreferencesEditorContext';
export * from './PreferencesEditorContextProvider';
export * from './useRenderTemplate';
export * from './usePreferencesEditor';
export * from './usePreferenceInput';
export * from './useSetInspectorTitle';
export * from './PreferenceKeyContext';
Loading