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

fix: make back button navigate one route up #1545

Merged
merged 6 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/adminjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/components/app/action-header/action-header.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -31,6 +31,7 @@ const ActionHeader: React.FC<ActionHeaderProps> = (props) => {
const translateFunctions = useTranslation()
const { translateButton, translateAction } = translateFunctions
const navigate = useNavigate()
const location = useLocation()
const actionResponseHandler = useActionResponseHandler(actionPerformed)
const modalFunctions = useModal()

Expand All @@ -46,6 +47,7 @@ const ActionHeader: React.FC<ActionHeaderProps> = (props) => {
params,
actionResponseHandler,
navigate,
location,
translateFunctions,
modalFunctions,
})(event)
Expand Down Expand Up @@ -90,6 +92,7 @@ const ActionHeader: React.FC<ActionHeaderProps> = (props) => {
const cssActionsMB = action.showInDrawer ? 'xl' : 'default'
const CssHComponent = action.showInDrawer ? H3 : H2
const contentTag = getActionElementCss(resourceId, action.name, 'action-header')

return (
<Box className={cssClass('ActionHeader')} data-css={contentTag}>
{action.showInDrawer ? '' : (
Expand Down
26 changes: 8 additions & 18 deletions src/frontend/components/app/action-header/styled-back-button.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, { useMemo } from 'react'
import React from 'react'
import { Link as RouterLink } from 'react-router-dom'
import { useLocation } from 'react-router'
import {
ButtonCSS,
ButtonProps,
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 }) => <RouterLink to={to} {...rest} />)<ButtonProps>`${ButtonCSS}`
Expand All @@ -19,27 +18,18 @@ export type StyledBackButtonProps = {
}

const StyledBackButton: React.FC<StyledBackButtonProps> = (props) => {
const location = useLocation()
const { showInDrawer } = props
const { previousRoute } = useSelector<ReduxState, DrawerInState>((state) => state.drawer)
const { from = {} } = useSelector<ReduxState, RouterInState>((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 (
<StyledLink
size="icon"
to={backLink}
to={{
pathname: '..',
search: location.search,
}}
relative="route"
rounded
mr="lg"
type="button"
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/components/app/records-table/record-in-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router'
import { useNavigate, useLocation } from 'react-router'
import {
Placeholder, TableRow, TableCell, CheckBox, ButtonGroup,
} from '@adminjs/design-system'
Expand Down Expand Up @@ -30,6 +30,7 @@ const RecordInList: React.FC<RecordInListProps> = (props) => {
} = props
const [record, setRecord] = useState<RecordJSON>(recordFromProps)
const navigate = useNavigate()
const location = useLocation()
const translateFunctions = useTranslation()
const modalFunctions = useModal()

Expand Down Expand Up @@ -65,6 +66,7 @@ const RecordInList: React.FC<RecordInListProps> = (props) => {
params: { resourceId: resource.id, recordId: record.id },
actionResponseHandler,
navigate,
location,
translateFunctions,
modalFunctions,
})(event)
Expand All @@ -79,6 +81,7 @@ const RecordInList: React.FC<RecordInListProps> = (props) => {
params: actionParams,
actionResponseHandler,
navigate,
location,
translateFunctions,
modalFunctions,
})(event)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,6 +19,7 @@ const SelectedRecords: React.FC<SelectedRecordsProps> = (props) => {
const translateFunctions = useTranslation()
const { translateLabel } = translateFunctions
const navigate = useNavigate()
const location = useLocation()
const actionResponseHandler = useActionResponseHandler()
const modalFunctions = useModal()

Expand All @@ -37,6 +38,7 @@ const SelectedRecords: React.FC<SelectedRecordsProps> = (props) => {
params,
actionResponseHandler,
navigate,
location,
translateFunctions,
modalFunctions,
})(event)
Expand Down
58 changes: 30 additions & 28 deletions src/frontend/components/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import Notice from './app/notice.js'
import allowOverride from '../hoc/allow-override.js'
import { AdminModal as Modal } from './app/admin-modal.js'
import {
DashboardRoute, ResourceActionRoute, RecordActionRoute, PageRoute, BulkActionRoute, ResourceRoute,
DashboardRoute,
ResourceActionRoute,
RecordActionRoute,
PageRoute,
BulkActionRoute,
ResourceRoute,
} from './routes/index.js'
import useHistoryListen from '../hooks/use-history-listen.js'

Expand All @@ -24,37 +29,29 @@ const App: React.FC = () => {
useHistoryListen()

useEffect(() => {
if (sidebarVisible) { toggleSidebar(false) }
if (sidebarVisible) {
toggleSidebar(false)
}
}, [location])

const resourceId = ':resourceId'
const actionName = ':actionName'
const recordId = ':recordId'
const pageName = ':pageName'

// Note: replaces are required so that record/resource/bulk actions urls
// are relative to their parent route
const dashboardUrl = h.dashboardUrl()
const recordActionUrl = h.recordActionUrl({ resourceId, recordId, actionName })
const resourceUrl = h.resourceUrl({ resourceId })
const recordActionUrl = h
.recordActionUrl({ resourceId, recordId, actionName })
.replace(resourceUrl, '').substring(1)
const resourceActionUrl = h.resourceActionUrl({ resourceId, actionName })
.replace(resourceUrl, '').substring(1)
const bulkActionUrl = h.bulkActionUrl({ resourceId, actionName })
const resourceUrl = h.resourceUrl({ resourceId })
.replace(resourceUrl, '').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 (
<Box height="100%" flex data-css="app">
{sidebarVisible ? (
Expand All @@ -70,14 +67,19 @@ const App: React.FC = () => {
<Notice />
</Box>
<Routes>
<Route path={`${resourceUrl}/*`} element={<ResourceRoute />} />
<Route path={pageUrl} element={<PageRoute />} />
<Route path={dashboardUrl} element={<DashboardRoute />} />
</Routes>
<Routes>
<Route path={`${resourceActionUrl}/*`} element={<ResourceActionRoute />} />
<Route path={`${bulkActionUrl}/*`} element={<BulkActionRoute />} />
<Route path={`${recordActionUrl}/*`} element={<RecordActionRoute />} />
<Route path={dashboardUrl}>
<Route index element={<DashboardRoute />} />
</Route>
<Route path={resourceUrl}>
<Route index element={<ResourceRoute />} />
<Route path={bulkActionUrl} element={<BulkActionRoute />} />
<Route path={resourceActionUrl} element={<ResourceActionRoute />} />
<Route path={recordActionUrl} element={<RecordActionRoute />} />
</Route>
<Route path={pageUrl}>
<Route index element={<PageRoute />} />
</Route>
<Route path="*" element={<DashboardRoute />} />
</Routes>
</Box>
<Modal />
Expand Down
104 changes: 68 additions & 36 deletions src/frontend/components/routes/bulk-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,50 @@ const BulkAction: React.FC = () => {
const params = useParams<MatchParams>()
const [records, setRecords] = useState<Array<RecordJSON>>([])
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<void> => {
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(() => {
fetchRecords()
}, [params.resourceId, params.actionName])

if (!resource) {
return (<NoResourceError resourceId={resourceId!} />)
return <NoResourceError resourceId={resourceId!} />
}

if (!records && !loading) {
Expand All @@ -76,38 +84,62 @@ const BulkAction: React.FC = () => {

if (loading) {
const actionFromResource = resource.actions.find((r) => r.name === actionName)
return actionFromResource?.showInDrawer ? (<DrawerPortal><Loader /></DrawerPortal>) : <Loader />
return actionFromResource?.showInDrawer ? (
<DrawerPortal>
<Loader />
</DrawerPortal>
) : (
<Loader />
)
}

if (!action) {
return (<NoActionError resourceId={resourceId!} actionName={actionName!} />)
return <NoActionError resourceId={resourceId!} actionName={actionName!} />
}

if (action.showInDrawer) {
if (!listAction) {
return (
<DrawerPortal width={action.containerWidth}>
<BaseActionComponent
action={action as ActionJSON}
resource={resource}
records={records}
/>
</DrawerPortal>
)
}

const toggleFilter = listAction.showFilter
? (): void => setFilterVisible(!filterVisible)
: undefined

return (
<DrawerPortal width={action.containerWidth}>
<BaseActionComponent
action={action as ActionJSON}
resource={resource}
records={records}
/>
</DrawerPortal>
<>
<DrawerPortal width={action.containerWidth}>
<BaseActionComponent
action={action as ActionJSON}
resource={resource}
records={records}
/>
</DrawerPortal>
<Wrapper width={listAction.containerWidth}>
<ActionHeader
resource={resource}
action={listAction}
tag={tag}
toggleFilter={toggleFilter}
/>
<BaseActionComponent action={listAction} resource={resource} setTag={setTag} />
</Wrapper>
</>
)
}

return (
<Wrapper width={action.containerWidth}>
{!action?.showInDrawer ? (
<ActionHeader
resource={resource}
action={action}
/>
) : ''}
<BaseActionComponent
action={action as ActionJSON}
resource={resource}
records={records}
/>
{!action?.showInDrawer ? <ActionHeader resource={resource} action={action} /> : ''}
<BaseActionComponent action={action as ActionJSON} resource={resource} records={records} />
</Wrapper>
)
}
Expand Down
Loading
Loading