Skip to content

Commit

Permalink
Support dynamically loaded custom components (#804)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dreamsorcerer authored Dec 3, 2023
1 parent a7336fc commit 22049d5
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 30 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ __pycache__/
# From yarn install
admin-js/yarn.lock
admin-js/node_modules/
examples/demo/admin-js/yarn.lock
examples/demo/admin-js/node_modules/
# Generated by yarn build
aiohttp_admin/static/admin.js
aiohttp_admin/static/*.js.map
examples/demo/static/admin.js
examples/demo/static/*.js.map
# coverage (when running pytest)
.coverage
73 changes: 45 additions & 28 deletions admin-js/src/App.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,66 @@
import {
// App
Admin, AppBar, InspectorButton, Layout, Resource, TitlePortal,
// Create/Edit
Create, DeleteButton, Edit, SaveButton, SimpleForm, Toolbar,
// List
Datagrid, DatagridConfigurable, List,
// Show
SimpleShowLayout, Show,
// Actions
BulkDeleteButton, BulkExportButton, BulkUpdateButton, CloneButton, CreateButton,
ExportButton, FilterButton, ListButton, SelectColumnsButton, ShowButton, TopToolbar,
// Fields
BooleanField, DateField, NumberField, ReferenceField, ReferenceManyField,
ReferenceOneField, SelectField, TextField,
// Inputs
BooleanInput, DateInput, DateTimeInput, NullableBooleanInput, NumberInput,
SelectInput, TextInput,
TimeInput as _TimeInput, ReferenceInput as _ReferenceInput,
// Filters
Admin, AppBar, AutocompleteInput,
BooleanField, BooleanInput, BulkDeleteButton, Button, BulkExportButton, BulkUpdateButton,
CloneButton, Create, CreateButton,
Datagrid, DatagridConfigurable, DateField, DateInput, DateTimeInput, DeleteButton,
Edit, EditButton, ExportButton,
FilterButton, HttpError, InspectorButton,
Layout, List, ListButton,
NullableBooleanInput, NumberInput, NumberField,
ReferenceField, ReferenceInput, ReferenceManyField, ReferenceOneField, Resource,
SaveButton, SelectColumnsButton, SelectField, SelectInput, Show, ShowButton,
SimpleForm, SimpleShowLayout,
TextField, TextInput, TimeInput, TitlePortal, Toolbar, TopToolbar,
WithRecord,
email, maxLength, maxValue, minLength, minValue, regex, required,
// Misc
AutocompleteInput, EditButton, HttpError, WithRecord
useCreate, useCreatePath, useDelete, useDeleteMany, useGetList, useGetMany,
useGetOne, useInfiniteGetList, useGetRecordId, useInput, useNotify,
useRecordContext, useRedirect, useRefresh, useResourceContext, useUnselect,
useUnselectAll, useUpdate, useUpdateMany,
} from "react-admin";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";

window.ReactAdmin = {
Admin, AppBar, AutocompleteInput,
BooleanField, BooleanInput, BulkDeleteButton, Button, BulkExportButton, BulkUpdateButton,
CloneButton, Create, CreateButton,
Datagrid, DatagridConfigurable, DateField, DateInput, DateTimeInput, DeleteButton,
Edit, EditButton, ExportButton,
FilterButton, HttpError, InspectorButton,
Layout, List, ListButton,
NullableBooleanInput, NumberInput, NumberField,
ReferenceField, ReferenceInput, ReferenceManyField, ReferenceOneField, Resource,
SaveButton, SelectColumnsButton, SelectField, SelectInput, Show, ShowButton,
SimpleForm, SimpleShowLayout,
TextField, TextInput, TimeInput, TitlePortal, Toolbar, TopToolbar,
WithRecord,
email, maxLength, maxValue, minLength, minValue, regex, required,
useCreate, useCreatePath, useDelete, useDeleteMany, useGetList, useGetMany,
useGetOne, useInfiniteGetList, useGetRecordId, useInput, useNotify,
useRecordContext, useRedirect, useRefresh, useResourceContext, useUnselect,
useUnselectAll, useUpdate, useUpdateMany,
};

// Hacked TimeField/TimeInput to actually work with times.
// TODO: Replace once new components are introduced using Temporal API.

const TimeField = (props) => (
const _TimeField = (props) => (
<WithRecord {...props} render={
(record) => <DateField {...props} showDate={false} showTime={true}
record={{...record, [props["source"]]: record[props["source"]] === null ? null : "2020-01-01T" + record[props["source"]]}} />
} />
);

const TimeInput = (props) => (<_TimeInput format={(v) => v} parse={(v) => v} {...props} />);
const _TimeInput = (props) => (<TimeInput format={(v) => v} parse={(v) => v} {...props} />);

/** Reconfigure ReferenceInput to filter by the displayed repr field. */
const ReferenceInput = (props) => {
const _ReferenceInput = (props) => {
const ref = props["reference"];
const repr = STATE["resources"][ref]["repr"];
return (
<_ReferenceInput sort={{"field": repr, "order": "ASC"}} {...props}>
<ReferenceInput sort={{"field": repr, "order": "ASC"}} {...props}>
<AutocompleteInput filterToQuery={s => ({[repr]: s})} />
</_ReferenceInput>
</ReferenceInput>
);
};

Expand All @@ -64,10 +81,10 @@ const COMPONENTS = {
ExportButton, FilterButton, ListButton, ShowButton,

BooleanField, DateField, NumberField, ReferenceField, ReferenceManyField,
ReferenceOneField, SelectField, TextField, TimeField,
ReferenceOneField, SelectField, TextField, TimeField: _TimeField,

BooleanInput, DateInput, DateTimeInput, NullableBooleanInput, NumberInput,
ReferenceInput, SelectInput, TextInput, TimeInput
ReferenceInput: _ReferenceInput, SelectInput, TextInput, TimeInput: _TimeInput
};
const FUNCTIONS = {email, maxLength, maxValue, minLength, minValue, regex, required};
const _body = document.querySelector("body");
Expand Down
16 changes: 14 additions & 2 deletions admin-js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import React from "react";
import ReactDOM from "react-dom/client";
import ReactJSXRuntime from "react/jsx-runtime";
import ReactDOM from "react-dom";
import ReactDOMClient from "react-dom/client";
import {Link, Route, useLocation, useNavigate, useParams} from 'react-router-dom';
import QueryString from 'query-string';
import {App, MODULE_LOADER} from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
// Copy libraries to global location for shim.
window.React = React;
window.ReactJSXRuntime = ReactJSXRuntime;
window.ReactDOM = ReactDOM;
window.ReactDOMClient = ReactDOMClient;
window.ReactRouterDOM = {Link, Route, useLocation, useNavigate, useParams};
window.QueryString = QueryString;

const root = ReactDOMClient.createRoot(document.getElementById("root"));
MODULE_LOADER.then(() => {
root.render(
<React.StrictMode>
Expand Down
61 changes: 61 additions & 0 deletions examples/demo/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
To build a custom component:

First we need to replace some of our dependencies with a shim (ensure shim/ is copied
to your project directory).
In package.json, update the dependencies which are available in the shim/ directory:

"react": "file:./shim/react",
"react-admin": "file:./shim/react-admin",
"react-dom": "file:./shim/react-dom",

Also repeat these in a 'resolutions' config:

"resolutions": {
"react": "file:./shim/react",
"react-admin": "file:./shim/react-admin",
"react-dom": "file:./shim/react-dom",
"react-router-dom": "file:./shim/react-router-dom",
"query-string": "file:./shim/query-string"
},

Using the shim for atleast react-admin is required, otherwise the components will
end up using different contexts to the application and will fail to function.
Using the shim for other libraries is recommended as it will significantly reduce
the size of your compiled module.



Second, we need to ensure that it is built as an ES6 module. To achieve this, add
craco to the dependencies:

"@craco/craco": "^7.1.0",

Then create a craco.config.js file:

module.exports = {
webpack: {
configure: {
output: {
library: {
type: "module"
}
},
experiments: {outputModule: true}
}
}
}

And replace `react-scripts` with `craco` in the 'scripts' config:

"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
},


Then the components can be built as normal:

yarn install
yarn build
12 changes: 12 additions & 0 deletions examples/demo/admin-js/craco.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
webpack: {
configure: {
output: {
library: {
type: 'module'
}
},
experiments: {outputModule: true}
}
}
}
47 changes: 47 additions & 0 deletions examples/demo/admin-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "admin-js",
"version": "0.1.0",
"private": true,
"dependencies": {
"@craco/craco": "^7.1.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.14",
"@mui/material": "^5.14.14",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"query-string": "file:./shim/query-string",
"react": "file:./shim/react",
"react-admin": "file:./shim/react-admin",
"react-dom": "file:./shim/react-dom",
"react-router-dom": "file:./shim/react-router-dom",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.0"
},
"resolutions": {
"react": "file:./shim/react",
"react-admin": "file:./shim/react-admin",
"react-dom": "file:./shim/react-dom",
"react-router-dom": "file:./shim/react-router-dom",
"query-string": "file:./shim/query-string"
},
"scripts": {
"start": "craco start",
"build": "craco build && (rm ../static/*.js.map || true) && mv build/static/js/main.*.js ../static/admin.js && mv build/static/js/main.*.js.map ../static/ && rm -rf build/",
"test": "craco test",
"eject": "craco eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Empty file.
1 change: 1 addition & 0 deletions examples/demo/admin-js/shim/query-string/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = window.QueryString;
1 change: 1 addition & 0 deletions examples/demo/admin-js/shim/react-admin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = window.ReactAdmin;
1 change: 1 addition & 0 deletions examples/demo/admin-js/shim/react-dom/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = window.ReactDOM;
1 change: 1 addition & 0 deletions examples/demo/admin-js/shim/react-router-dom/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = window.ReactRouterDOM;
1 change: 1 addition & 0 deletions examples/demo/admin-js/shim/react/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = window.React;
1 change: 1 addition & 0 deletions examples/demo/admin-js/shim/react/jsx-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = window.ReactJSXRuntime;
53 changes: 53 additions & 0 deletions examples/demo/admin-js/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { memo } from 'react';
import Queue from '@mui/icons-material/Queue';
import { Link } from 'react-router-dom';
import { stringify } from 'query-string';
import { useResourceContext, useRecordContext, useCreatePath, Button } from 'react-admin';

export const CustomCloneButton = (props: CloneButtonProps) => {
const {
label = 'CUSTOM CLONE',
scrollToTop = true,
icon = defaultIcon,
...rest
} = props;
const resource = useResourceContext(props);
const record = useRecordContext(props);
const createPath = useCreatePath();
const pathname = createPath({ resource, type: 'create' });
return (
<Button
component={Link}
to={
record
? {
pathname,
search: stringify({
source: JSON.stringify(omitId(record)),
}),
state: { _scrollToTop: scrollToTop },
}
: pathname
}
label={label}
onClick={stopPropagation}
{...sanitizeRestProps(rest)}
>
{icon}
</Button>
);
};

const defaultIcon = <Queue />;

const stopPropagation = e => e.stopPropagation();

const omitId = ({ id, ...rest }) => rest;

const sanitizeRestProps = ({
resource,
record,
...rest
}) => rest;

export const components = {CustomCloneButton: memo(CustomCloneButton)};
Loading

0 comments on commit 22049d5

Please sign in to comment.