diff --git a/admin-js/src/App.js b/admin-js/src/App.js index efc8d383..37ff27e9 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, useResourceContext, 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, useResourceContext, 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..4f270276 --- /dev/null +++ b/examples/custom-clone.js @@ -0,0 +1,39 @@ +export const g = {"Queue": null, "Link": null, "stringify": null, "Button": null, + "createElement": null, "useRecordContext": null, "useCreatePath": null, + "useResourceContext": null} + +const CustomCloneButton = (props) => { + const { + 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, 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 = {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)