From 43c134a79d0e64bfe3a6ea7adc53c53f0982150b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 8 Aug 2023 14:01:17 +0100 Subject: [PATCH 1/2] Dynamic component #2 --- admin-js/src/App.js | 16 ++++++++++++++-- examples/custom-clone.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 examples/custom-clone.js diff --git a/admin-js/src/App.js b/admin-js/src/App.js index efc8d383..a7263ad5 100644 --- a/admin-js/src/App.js +++ b/admin-js/src/App.js @@ -19,10 +19,18 @@ import { // Filters email, maxLength, maxValue, minLength, minValue, regex, required, // Misc - AutocompleteInput, EditButton, HttpError, WithRecord + AutocompleteInput, EditButton, HttpError, WithRecord, + // For custom components... + useCreatePath, useRecordContext, Button, } from "react-admin"; +import { createElement } from "react"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; + +import Queue from '@mui/icons-material/Queue'; +import { Link } from 'react-router-dom'; +import { stringify } from 'query-string'; + // Hacked TimeField/TimeInput to actually work with times. // TODO: Replace once new components are introduced using Temporal API. @@ -66,7 +74,9 @@ const COMPONENTS = { ReferenceOneField, SelectField, TextField, TimeField, BooleanInput, DateInput, DateTimeInput, NumberInput, ReferenceInput, SelectInput, - TextInput, TimeInput + TextInput, TimeInput, + + useRecordContext, useCreatePath, Button, Queue, Link, stringify, createElement }; const FUNCTIONS = {email, maxLength, maxValue, minLength, minValue, regex, required}; const _body = document.querySelector("body"); @@ -77,6 +87,8 @@ if (STATE["js_module"]) { // The inline comment skips the webpack import() and allows us to use the native // browser's import() function. Needed to dynamically import a module. MODULE_LOADER = import(/* webpackIgnore: true */ STATE["js_module"]).then((mod) => { + for (const k of Object.keys(mod.g)) + mod.g[k] = COMPONENTS[k] Object.assign(COMPONENTS, mod.components); Object.assign(FUNCTIONS, mod.functions); }); diff --git a/examples/custom-clone.js b/examples/custom-clone.js new file mode 100644 index 00000000..f01f9fca --- /dev/null +++ b/examples/custom-clone.js @@ -0,0 +1,37 @@ +export const g = {"Queue": null, "Link": null, "stringify": null, "Button": null, + "createElement": null, "useRecordContext": null, "useCreatePath": null} + +const CopyUSButton = (props) => { + const { + label = "Copy to US", + scrollToTop = true, + icon = g.createElement(g.Queue), + ...rest + } = props; + const record = g.useRecordContext(props); + const createPath = g.useCreatePath(); + const pathname = createPath({resource: "rhymes_us", type: "create"}); + props = { + component: g.Link, + to: ( + record + ? { + pathname, + search: g.stringify({source: JSON.stringify(record)}), + state: {_scrollToTop: scrollToTop}, + } + : pathname + ), + label: label, + onClick: stopPropagation, + ...sanitizeRestProps(rest) + }; + return g.createElement(g.Button, props, icon); +}; + +// useful to prevent click bubbling in a datagrid with rowClick +const stopPropagation = e => e.stopPropagation(); + +const sanitizeRestProps = ({resource, record, ...rest}) => rest; + +export const components = {CopyUSButton: CopyUSButton}; From dac1e53d95f8261c2816c5454fb25385a553c82c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Wed, 9 Aug 2023 12:27:27 +0100 Subject: [PATCH 2/2] Update simple example to use custom button --- admin-js/src/App.js | 4 ++-- examples/custom-clone.js | 12 +++++++----- examples/simple.py | 13 +++++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/admin-js/src/App.js b/admin-js/src/App.js index a7263ad5..37ff27e9 100644 --- a/admin-js/src/App.js +++ b/admin-js/src/App.js @@ -21,7 +21,7 @@ import { // Misc AutocompleteInput, EditButton, HttpError, WithRecord, // For custom components... - useCreatePath, useRecordContext, Button, + useCreatePath, useRecordContext, useResourceContext, Button, } from "react-admin"; import { createElement } from "react"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; @@ -76,7 +76,7 @@ const COMPONENTS = { BooleanInput, DateInput, DateTimeInput, NumberInput, ReferenceInput, SelectInput, TextInput, TimeInput, - useRecordContext, useCreatePath, Button, Queue, Link, stringify, createElement + useRecordContext, useResourceContext, useCreatePath, Button, Queue, Link, stringify, createElement }; const FUNCTIONS = {email, maxLength, maxValue, minLength, minValue, regex, required}; const _body = document.querySelector("body"); diff --git a/examples/custom-clone.js b/examples/custom-clone.js index f01f9fca..4f270276 100644 --- a/examples/custom-clone.js +++ b/examples/custom-clone.js @@ -1,16 +1,18 @@ export const g = {"Queue": null, "Link": null, "stringify": null, "Button": null, - "createElement": null, "useRecordContext": null, "useCreatePath": null} + "createElement": null, "useRecordContext": null, "useCreatePath": null, + "useResourceContext": null} -const CopyUSButton = (props) => { +const CustomCloneButton = (props) => { const { - label = "Copy to US", + label = "My custom clone", scrollToTop = true, icon = g.createElement(g.Queue), ...rest } = props; + const resource = g.useResourceContext(props); const record = g.useRecordContext(props); const createPath = g.useCreatePath(); - const pathname = createPath({resource: "rhymes_us", type: "create"}); + const pathname = createPath({resource, type: "create"}); props = { component: g.Link, to: ( @@ -34,4 +36,4 @@ const stopPropagation = e => e.stopPropagation(); const sanitizeRestProps = ({resource, record, ...rest}) => rest; -export const components = {CopyUSButton: CopyUSButton}; +export const components = {CustomCloneButton: CustomCloneButton}; diff --git a/examples/simple.py b/examples/simple.py index e9e893d2..70b4cf20 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -5,6 +5,7 @@ from datetime import datetime from enum import Enum +from pathlib import Path import sqlalchemy as sa from aiohttp import web @@ -13,6 +14,7 @@ import aiohttp_admin from aiohttp_admin.backends.sqlalchemy import SAResource +from aiohttp_admin.types import comp # Example DB models @@ -49,6 +51,11 @@ async def check_credentials(username: str, password: str) -> bool: return username == "admin" and password == "admin" +async def serve_js(request): + js = Path(__file__).with_name("custom-clone.js").read_text() + return web.Response(text=js, content_type="text/javascript") + + async def create_app() -> web.Application: engine = create_async_engine("sqlite+aiosqlite:///:memory:") session = async_sessionmaker(engine, expire_on_commit=False) @@ -64,6 +71,7 @@ async def create_app() -> web.Application: sess.add(SimpleParent(id=p.id, date=datetime(2023, 2, 13, 19, 4))) app = web.Application() + app.router.add_get("/js", serve_js, name="js") # This is the setup required for aiohttp-admin. schema: aiohttp_admin.Schema = { @@ -72,9 +80,10 @@ async def create_app() -> web.Application: "secure": False }, "resources": ( - {"model": SAResource(engine, Simple)}, + {"model": SAResource(engine, Simple), "show_actions": (comp("CustomCloneButton"),)}, {"model": SAResource(engine, SimpleParent)} - ) + ), + "js_module": str(app.router["js"].url_for()) } aiohttp_admin.setup(app, schema)