From cc7dafe60ac326081e62d37a4e6210b0789630ff Mon Sep 17 00:00:00 2001 From: Rafal Dziegielewski Date: Wed, 16 Aug 2023 09:42:03 +0200 Subject: [PATCH 1/6] fix: make back button navigate one route up --- .../app/action-header/action-header.tsx | 1 + .../app/action-header/styled-back-button.tsx | 21 +++---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/frontend/components/app/action-header/action-header.tsx b/src/frontend/components/app/action-header/action-header.tsx index 6c38b3506..50d2fd2aa 100644 --- a/src/frontend/components/app/action-header/action-header.tsx +++ b/src/frontend/components/app/action-header/action-header.tsx @@ -90,6 +90,7 @@ const ActionHeader: React.FC = (props) => { const cssActionsMB = action.showInDrawer ? 'xl' : 'default' const CssHComponent = action.showInDrawer ? H3 : H2 const contentTag = getActionElementCss(resourceId, action.name, 'action-header') + return ( {action.showInDrawer ? '' : ( diff --git a/src/frontend/components/app/action-header/styled-back-button.tsx b/src/frontend/components/app/action-header/styled-back-button.tsx index 360dca356..376b22afd 100644 --- a/src/frontend/components/app/action-header/styled-back-button.tsx +++ b/src/frontend/components/app/action-header/styled-back-button.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React from 'react' import { Link as RouterLink } from 'react-router-dom' import { ButtonCSS, @@ -6,10 +6,8 @@ import { Icon, } from '@adminjs/design-system' import { styled } from '@adminjs/design-system/styled-components' -import { useSelector } from 'react-redux' import allowOverride from '../../../hoc/allow-override.js' -import type { DrawerInState, ReduxState, RouterInState } from '../../../store/index.js' // eslint-disable-next-line @typescript-eslint/no-unused-vars const StyledLink = styled(({ rounded, to, ...rest }) => )`${ButtonCSS}` @@ -20,26 +18,13 @@ export type StyledBackButtonProps = { const StyledBackButton: React.FC = (props) => { const { showInDrawer } = props - const { previousRoute } = useSelector((state) => state.drawer) - const { from = {} } = useSelector((state) => state.router) const cssCloseIcon = showInDrawer ? 'ChevronRight' : 'ChevronLeft' - const backLink = useMemo(() => { - if (!showInDrawer) { - return from?.pathname - } - - if (previousRoute?.pathname) { - return `${previousRoute?.pathname}${previousRoute?.search}` - } - - return from?.pathname - }, [previousRoute, from]) - return ( Date: Thu, 17 Aug 2023 15:33:55 +0200 Subject: [PATCH 2/6] fix: refactor Routes --- src/adminjs.ts | 2 +- .../app/action-header/styled-back-button.tsx | 2 +- src/frontend/components/application.tsx | 62 ++++++++++--------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/adminjs.ts b/src/adminjs.ts index 3d0350dab..9fef940fd 100644 --- a/src/adminjs.ts +++ b/src/adminjs.ts @@ -16,7 +16,7 @@ import { ACTIONS } from './backend/actions/index.js' import loginTemplate from './frontend/login-template.js' import { ListActionResponse } from './backend/actions/list/list-action.js' -import { defaultLocale, Locale } from './locale/index.js' +import { Locale } from './locale/index.js' import { TranslateFunctions } from './utils/translate-functions.factory.js' import { relativeFilePathResolver } from './utils/file-resolver.js' import { Router } from './backend/utils/index.js' diff --git a/src/frontend/components/app/action-header/styled-back-button.tsx b/src/frontend/components/app/action-header/styled-back-button.tsx index 376b22afd..880c7cd78 100644 --- a/src/frontend/components/app/action-header/styled-back-button.tsx +++ b/src/frontend/components/app/action-header/styled-back-button.tsx @@ -24,7 +24,7 @@ const StyledBackButton: React.FC = (props) => { { useHistoryListen() useEffect(() => { - if (sidebarVisible) { toggleSidebar(false) } + if (sidebarVisible) { + toggleSidebar(false) + } }, [location]) const resourceId = ':resourceId' @@ -32,29 +39,21 @@ const App: React.FC = () => { const recordId = ':recordId' const pageName = ':pageName' + // TODO: Refactor .replace(...) mess const dashboardUrl = h.dashboardUrl() - const recordActionUrl = h.recordActionUrl({ resourceId, recordId, actionName }) + const resourceUrl = h.resourceUrl({ resourceId }) + const recordActionUrl = h + .recordActionUrl({ resourceId, recordId, actionName }) + .replace(dashboardUrl, '') + .replace(resourceUrl.replace(dashboardUrl, ''), '').substring(1) const resourceActionUrl = h.resourceActionUrl({ resourceId, actionName }) + .replace(dashboardUrl, '') + .replace(resourceUrl.replace(dashboardUrl, ''), '').substring(1) const bulkActionUrl = h.bulkActionUrl({ resourceId, actionName }) - const resourceUrl = h.resourceUrl({ resourceId }) + .replace(dashboardUrl, '') + .replace(resourceUrl.replace(dashboardUrl, ''), '').substring(1) const pageUrl = h.pageUrl(pageName) - /** - * When defining AdminJS routes, we use Routes component twice. - * This results in warnings appearing in console, for example about not being able to locate - * "/admin" route. They can be safely ignored though and should appear only - * in development environment. The warnings originate from the difference between - * "Switch" component that AdminJS had used in "react-router" v5 which was later replaced - * with "Routes" in "react-router" v6. "Switch" would use the first "Route" component - * that matched the provided path, while "Routes" searches for the best matching pattern. - * In AdminJS we use "DrawerPortal" to display actions in a drawer when - * "showInDrawer" option is set to true. The drawer should appear above the currently viewed - * page, but "Routes" broke this behavior because it instead showed a record action route with - * an empty background. - * The current flow is that first "Routes" component includes "Resource" route component - * for drawer-placed actions and the second "Routes" is entered for record actions - * on a separate page. - */ return ( {sidebarVisible ? ( @@ -70,14 +69,19 @@ const App: React.FC = () => { - } /> - } /> - } /> - - - } /> - } /> - } /> + + } /> + + + } /> + } /> + } /> + } /> + + + } /> + + } /> From 33e15edd874c0b1f18ee9f29c7de11cab6e3cf8a Mon Sep 17 00:00:00 2001 From: Rafal Dziegielewski Date: Thu, 17 Aug 2023 16:14:20 +0200 Subject: [PATCH 3/6] chore: fix Drawer not displaying list component underneath --- src/frontend/components/application.tsx | 4 +- .../components/routes/record-action.tsx | 127 +++++++++++------- .../components/routes/resource-action.tsx | 1 + 3 files changed, 81 insertions(+), 51 deletions(-) diff --git a/src/frontend/components/application.tsx b/src/frontend/components/application.tsx index 6ef4d36c3..4eb32cf83 100644 --- a/src/frontend/components/application.tsx +++ b/src/frontend/components/application.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-children-prop */ import React, { useEffect, useState } from 'react' -import { Routes, Route, Outlet } from 'react-router-dom' +import { Routes, Route } from 'react-router-dom' import { Box, Overlay } from '@adminjs/design-system' import { useLocation } from 'react-router' @@ -74,9 +74,9 @@ const App: React.FC = () => { } /> - } /> } /> } /> + } /> } /> diff --git a/src/frontend/components/routes/record-action.tsx b/src/frontend/components/routes/record-action.tsx index 0bb27599e..f0f4f1996 100644 --- a/src/frontend/components/routes/record-action.tsx +++ b/src/frontend/components/routes/record-action.tsx @@ -21,6 +21,8 @@ const api = new ApiClient() const RecordAction: React.FC = () => { const [record, setRecord] = useState() const [loading, setLoading] = useState(true) + const [tag, setTag] = useState('') + const [filterVisible, setFilterVisible] = useState(false) const params = useParams() const addNotice = useNotice() @@ -30,6 +32,9 @@ const RecordAction: React.FC = () => { const action = record && record.recordActions.find((r) => r.name === actionName) const actionFromResource = resource?.actions.find((a) => a.name === actionName) + const listActionName = 'list' + const listAction = resource?.resourceActions.find((r) => r.name === listActionName) + const fetchRecord = (): void => { // Do not call API on route enter if the action doesn't have a component if (actionFromResource && actionHasDisabledComponent(actionFromResource)) { @@ -38,46 +43,51 @@ const RecordAction: React.FC = () => { } setLoading(true) - api.recordAction(params as RecordActionParams).then((response) => { - if (response.data.notice && response.data.notice.type === 'error') { - addNotice(response.data.notice) - } - if ( - !response.data.record?.baseError?.type - || ![ - ErrorTypeEnum.App, - ErrorTypeEnum.NotFound, - ErrorTypeEnum.Forbidden, - ].includes(response.data.record?.baseError?.type as ErrorTypeEnum) - ) { - setRecord(response.data.record) - } - }).catch((error) => { - addNotice({ - message: 'errorFetchingRecord', - type: 'error', - resourceId, + api + .recordAction(params as RecordActionParams) + .then((response) => { + if (response.data.notice && response.data.notice.type === 'error') { + addNotice(response.data.notice) + } + if ( + !response.data.record?.baseError?.type + || ![ErrorTypeEnum.App, ErrorTypeEnum.NotFound, ErrorTypeEnum.Forbidden].includes( + response.data.record?.baseError?.type as ErrorTypeEnum, + ) + ) { + setRecord(response.data.record) + } + }) + .catch((error) => { + addNotice({ + message: 'errorFetchingRecord', + type: 'error', + resourceId, + }) + throw error + }) + .finally(() => { + setLoading(false) }) - throw error - }).finally(() => { - setLoading(false) - }) } useEffect(() => { fetchRecord() }, [actionName, recordId, resourceId]) - const handleActionPerformed = useCallback((oldRecord: RecordJSON, response: ActionResponse) => { - if (response.record) { - setRecord(mergeRecordResponse(oldRecord, response as RecordActionResponse)) - } else { - fetchRecord() - } - }, [fetchRecord]) + const handleActionPerformed = useCallback( + (oldRecord: RecordJSON, response: ActionResponse) => { + if (response.record) { + setRecord(mergeRecordResponse(oldRecord, response as RecordActionResponse)) + } else { + fetchRecord() + } + }, + [fetchRecord], + ) if (!resource) { - return () + return } // When the user visits this route (record action) from a different, than the current one, record. @@ -89,26 +99,51 @@ const RecordAction: React.FC = () => { const hasDifferentRecord = record && record.id && record.id.toString() !== recordId if (loading || hasDifferentRecord) { - return actionFromResource?.showInDrawer ? () : + return actionFromResource?.showInDrawer ? ( + + + + ) : ( + + ) } if (!action || (actionFromResource && actionHasDisabledComponent(actionFromResource))) { - return () + return } if (!record) { - return () + return } if (action.showInDrawer) { + if (!listAction) { + return ( + + + + ) + } + + const toggleFilter = listAction.showFilter + ? (): void => setFilterVisible(!filterVisible) + : undefined + return ( - - - + <> + + + + + + + + ) } @@ -118,15 +153,9 @@ const RecordAction: React.FC = () => { resource={resource} action={action} record={record} - actionPerformed={(response: ActionResponse): void => ( - handleActionPerformed(record, response) - )} - /> - handleActionPerformed(record, response)} /> + ) } diff --git a/src/frontend/components/routes/resource-action.tsx b/src/frontend/components/routes/resource-action.tsx index 94d43cbc8..074168599 100644 --- a/src/frontend/components/routes/resource-action.tsx +++ b/src/frontend/components/routes/resource-action.tsx @@ -30,6 +30,7 @@ const ResourceAction: React.FC = (props) => { if (!resource) { return () } + const action = resource.resourceActions.find((r) => r.name === actionName) if (!action || actionHasDisabledComponent(action)) { return () From a4e5513b90f10b0bbb9ccbde0266bdcb2ffb98d0 Mon Sep 17 00:00:00 2001 From: Rafal Dziegielewski Date: Thu, 17 Aug 2023 16:16:38 +0200 Subject: [PATCH 4/6] chore: fix replace mess --- src/frontend/components/application.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/frontend/components/application.tsx b/src/frontend/components/application.tsx index 4eb32cf83..a8ef24a13 100644 --- a/src/frontend/components/application.tsx +++ b/src/frontend/components/application.tsx @@ -39,19 +39,17 @@ const App: React.FC = () => { const recordId = ':recordId' const pageName = ':pageName' - // TODO: Refactor .replace(...) mess + // Note: replaces are required so that record/resource/bulk actions urls + // are relative to their parent route const dashboardUrl = h.dashboardUrl() const resourceUrl = h.resourceUrl({ resourceId }) const recordActionUrl = h .recordActionUrl({ resourceId, recordId, actionName }) - .replace(dashboardUrl, '') - .replace(resourceUrl.replace(dashboardUrl, ''), '').substring(1) + .replace(resourceUrl, '').substring(1) const resourceActionUrl = h.resourceActionUrl({ resourceId, actionName }) - .replace(dashboardUrl, '') - .replace(resourceUrl.replace(dashboardUrl, ''), '').substring(1) + .replace(resourceUrl, '').substring(1) const bulkActionUrl = h.bulkActionUrl({ resourceId, actionName }) - .replace(dashboardUrl, '') - .replace(resourceUrl.replace(dashboardUrl, ''), '').substring(1) + .replace(resourceUrl, '').substring(1) const pageUrl = h.pageUrl(pageName) return ( From b48a3d912cac081d4fabee85c784f897c1b44282 Mon Sep 17 00:00:00 2001 From: Rafal Dziegielewski Date: Thu, 17 Aug 2023 16:36:57 +0200 Subject: [PATCH 5/6] fix: retain filters when opening record in a drawer --- .../app/action-header/action-header.tsx | 4 ++- .../app/action-header/styled-back-button.tsx | 7 +++- .../app/records-table/record-in-list.tsx | 5 ++- .../app/records-table/selected-records.tsx | 4 ++- src/frontend/hooks/use-action/use-action.ts | 4 ++- .../action/build-action-click-handler.ts | 32 +++++++++++++------ 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/frontend/components/app/action-header/action-header.tsx b/src/frontend/components/app/action-header/action-header.tsx index 50d2fd2aa..1129a09e6 100644 --- a/src/frontend/components/app/action-header/action-header.tsx +++ b/src/frontend/components/app/action-header/action-header.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/anchor-is-valid */ import { Badge, Box, ButtonGroup, cssClass, H2, H3 } from '@adminjs/design-system' import React from 'react' -import { useNavigate } from 'react-router' +import { useNavigate, useLocation } from 'react-router' import allowOverride from '../../../hoc/allow-override.js' import { useActionResponseHandler, useTranslation, useModal } from '../../../hooks/index.js' @@ -31,6 +31,7 @@ const ActionHeader: React.FC = (props) => { const translateFunctions = useTranslation() const { translateButton, translateAction } = translateFunctions const navigate = useNavigate() + const location = useLocation() const actionResponseHandler = useActionResponseHandler(actionPerformed) const modalFunctions = useModal() @@ -46,6 +47,7 @@ const ActionHeader: React.FC = (props) => { params, actionResponseHandler, navigate, + location, translateFunctions, modalFunctions, })(event) diff --git a/src/frontend/components/app/action-header/styled-back-button.tsx b/src/frontend/components/app/action-header/styled-back-button.tsx index 880c7cd78..3ba6db66b 100644 --- a/src/frontend/components/app/action-header/styled-back-button.tsx +++ b/src/frontend/components/app/action-header/styled-back-button.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Link as RouterLink } from 'react-router-dom' +import { useLocation } from 'react-router' import { ButtonCSS, ButtonProps, @@ -17,13 +18,17 @@ export type StyledBackButtonProps = { } const StyledBackButton: React.FC = (props) => { + const location = useLocation() const { showInDrawer } = props const cssCloseIcon = showInDrawer ? 'ChevronRight' : 'ChevronLeft' return ( = (props) => { } = props const [record, setRecord] = useState(recordFromProps) const navigate = useNavigate() + const location = useLocation() const translateFunctions = useTranslation() const modalFunctions = useModal() @@ -65,6 +66,7 @@ const RecordInList: React.FC = (props) => { params: { resourceId: resource.id, recordId: record.id }, actionResponseHandler, navigate, + location, translateFunctions, modalFunctions, })(event) @@ -79,6 +81,7 @@ const RecordInList: React.FC = (props) => { params: actionParams, actionResponseHandler, navigate, + location, translateFunctions, modalFunctions, })(event) diff --git a/src/frontend/components/app/records-table/selected-records.tsx b/src/frontend/components/app/records-table/selected-records.tsx index 6df4c176f..15457719e 100644 --- a/src/frontend/components/app/records-table/selected-records.tsx +++ b/src/frontend/components/app/records-table/selected-records.tsx @@ -1,6 +1,6 @@ import React from 'react' import { TableCaption, Title, ButtonGroup, Box } from '@adminjs/design-system' -import { useNavigate } from 'react-router' +import { useNavigate, useLocation } from 'react-router' import { ActionJSON, buildActionClickHandler, RecordJSON, ResourceJSON } from '../../../interfaces/index.js' import getBulkActionsFromRecords from './utils/get-bulk-actions-from-records.js' @@ -19,6 +19,7 @@ const SelectedRecords: React.FC = (props) => { const translateFunctions = useTranslation() const { translateLabel } = translateFunctions const navigate = useNavigate() + const location = useLocation() const actionResponseHandler = useActionResponseHandler() const modalFunctions = useModal() @@ -37,6 +38,7 @@ const SelectedRecords: React.FC = (props) => { params, actionResponseHandler, navigate, + location, translateFunctions, modalFunctions, })(event) diff --git a/src/frontend/hooks/use-action/use-action.ts b/src/frontend/hooks/use-action/use-action.ts index 48f45c5b2..0ab42a612 100644 --- a/src/frontend/hooks/use-action/use-action.ts +++ b/src/frontend/hooks/use-action/use-action.ts @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router' +import { useNavigate, useLocation } from 'react-router' import { ActionResponse } from '../../../backend/actions/action.interface.js' import { ActionJSON, buildActionCallApiTrigger, buildActionClickHandler } from '../../interfaces/index.js' @@ -25,6 +25,7 @@ export function useAction( onActionCall?: ActionCallCallback, ): UseActionResult { const navigate = useNavigate() + const location = useLocation() const translateFunctions = useTranslation() const modalFunctions = useModal() const actionResponseHandler = useActionResponseHandler(onActionCall) @@ -44,6 +45,7 @@ export function useAction( navigate, translateFunctions, modalFunctions, + location, }) return { diff --git a/src/frontend/interfaces/action/build-action-click-handler.ts b/src/frontend/interfaces/action/build-action-click-handler.ts index 4f5556122..52975578b 100644 --- a/src/frontend/interfaces/action/build-action-click-handler.ts +++ b/src/frontend/interfaces/action/build-action-click-handler.ts @@ -1,7 +1,7 @@ /* eslint-disable no-restricted-globals */ /* eslint-disable no-undef */ /* eslint-disable no-alert */ -import { NavigateFunction } from 'react-router' +import { NavigateFunction, Location } from 'react-router' import { DifferentActionParams, useActionResponseHandler } from '../../hooks/index.js' import { actionHasDisabledComponent } from './action-has-component.js' @@ -12,12 +12,13 @@ import { TranslateFunctions } from '../../../utils/index.js' import { ModalData, ModalFunctions } from '../modal.interface.js' export type BuildActionClickOptions = { - action: ActionJSON; - params: DifferentActionParams; - actionResponseHandler: ReturnType; - navigate: NavigateFunction; - translateFunctions: TranslateFunctions; + action: ActionJSON + params: DifferentActionParams + actionResponseHandler: ReturnType + navigate: NavigateFunction + translateFunctions: TranslateFunctions modalFunctions: ModalFunctions + location?: Location } export type BuildActionClickReturn = (event: any) => any | Promise @@ -25,8 +26,7 @@ export type BuildActionClickReturn = (event: any) => any | Promise export const buildActionClickHandler = ( options: BuildActionClickOptions, ): BuildActionClickReturn => { - const { action, params, actionResponseHandler, navigate, - modalFunctions } = options + const { action, params, actionResponseHandler, navigate, modalFunctions, location } = options const { openModal } = modalFunctions const handleActionClick = (event: React.MouseEvent): Promise | any => { @@ -36,7 +36,9 @@ export const buildActionClickHandler = ( const href = actionHref(action, params) const callApi = buildActionCallApiTrigger({ - params, action, actionResponseHandler, + params, + action, + actionResponseHandler, }) // Action has "component" option set to "false" explicitly in it's configuration @@ -65,7 +67,17 @@ export const buildActionClickHandler = ( // Default behaviour - you're navigated to action URL and logic is performed on it's route if (href) { - navigate(href) + const url = new URL(`relative:${href}`) + const hrefParams = new URLSearchParams(url.search) + const currentParams = new URLSearchParams(action.showInDrawer ? location?.search ?? '' : '') + Object.entries(Object.fromEntries(currentParams.entries())).forEach(([key, value]) => { + hrefParams.append(key, value) + }) + + navigate({ + pathname: url.pathname, + search: hrefParams.toString(), + }) } } From f42acd38827c9a365754ac45bdfc67f5ba81c5ea Mon Sep 17 00:00:00 2001 From: Rafal Dziegielewski Date: Thu, 17 Aug 2023 16:42:15 +0200 Subject: [PATCH 6/6] chore: show list component under bulk actions opened in drawer --- .../components/routes/bulk-action.tsx | 104 ++++++++++++------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/src/frontend/components/routes/bulk-action.tsx b/src/frontend/components/routes/bulk-action.tsx index 5cb840c7e..11b5044f9 100644 --- a/src/frontend/components/routes/bulk-action.tsx +++ b/src/frontend/components/routes/bulk-action.tsx @@ -26,34 +26,42 @@ const BulkAction: React.FC = () => { const params = useParams() const [records, setRecords] = useState>([]) const [loading, setLoading] = useState(false) + const [tag, setTag] = useState('') + const [filterVisible, setFilterVisible] = useState(false) const { translateMessage } = useTranslation() const addNotice = useNotice() const location = useLocation() const { resourceId, actionName } = params + const resource = useResource(resourceId!) + const listActionName = 'list' + const listAction = resource?.resourceActions.find((r) => r.name === listActionName) const fetchRecords = (): Promise => { const recordIdsString = new URLSearchParams(location.search).get('recordIds') const recordIds = recordIdsString ? recordIdsString.split(',') : [] setLoading(true) - return api.bulkAction({ - resourceId: resourceId!, - recordIds, - actionName: actionName!, - }).then((response) => { - setLoading(false) - setRecords(response.data.records) - }).catch((error) => { - setLoading(false) - addNotice({ - message: 'errorFetchingRecords', - type: 'error', - resourceId, + return api + .bulkAction({ + resourceId: resourceId!, + recordIds, + actionName: actionName!, + }) + .then((response) => { + setLoading(false) + setRecords(response.data.records) + }) + .catch((error) => { + setLoading(false) + addNotice({ + message: 'errorFetchingRecords', + type: 'error', + resourceId, + }) + throw error }) - throw error - }) } useEffect(() => { @@ -61,7 +69,7 @@ const BulkAction: React.FC = () => { }, [params.resourceId, params.actionName]) if (!resource) { - return () + return } if (!records && !loading) { @@ -76,38 +84,62 @@ const BulkAction: React.FC = () => { if (loading) { const actionFromResource = resource.actions.find((r) => r.name === actionName) - return actionFromResource?.showInDrawer ? () : + return actionFromResource?.showInDrawer ? ( + + + + ) : ( + + ) } if (!action) { - return () + return } if (action.showInDrawer) { + if (!listAction) { + return ( + + + + ) + } + + const toggleFilter = listAction.showFilter + ? (): void => setFilterVisible(!filterVisible) + : undefined + return ( - - - + <> + + + + + + + + ) } return ( - {!action?.showInDrawer ? ( - - ) : ''} - + {!action?.showInDrawer ? : ''} + ) }