diff --git a/.eslintrc.js b/.eslintrc.js
index bf91b82ec..b51f24f24 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -51,6 +51,7 @@ module.exports = {
'func-names': 'off',
'prefer-arrow-callback': 'off',
'import/no-extraneous-dependencies': 'off',
+ 'mocha/no-mocha-arrows': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
diff --git a/example-app/src/admin.options.js b/example-app/src/admin.options.js
index 6c0260acd..d576e225f 100644
--- a/example-app/src/admin.options.js
+++ b/example-app/src/admin.options.js
@@ -14,8 +14,8 @@ const AdminTool = require('./tools/tool.admin')
const AdminPage = require('./pages/page.admin')
const AdminNested = require('./nested/nested.admin')
-AdminBro.bundle('./components/sidebar-footer', 'SidebarFooter')
-AdminBro.bundle('./components/no-records', 'NoRecords')
+// AdminBro.bundle('./components/sidebar-footer', 'SidebarFooter')
+// AdminBro.bundle('./components/no-records', 'NoRecords')
/** @type {import('admin-bro').AdminBroOptions} */
const options = {
@@ -34,8 +34,16 @@ const options = {
app: process.env.npm_package_version,
},
branding: currentUser => ({
+ logo: false,
companyName: currentUser ? currentUser.email : 'something',
}),
+ pages: {
+ aboutUs: {
+ handler: async () => { console.log('clicked') },
+ component: AdminBro.bundle('./components/no-records'),
+ icon: 'Add',
+ },
+ },
locale: {
language: 'en',
translations: {
diff --git a/src/admin-bro-options.interface.ts b/src/admin-bro-options.interface.ts
index ae9d5ddad..9bb6d151f 100644
--- a/src/admin-bro-options.interface.ts
+++ b/src/admin-bro-options.interface.ts
@@ -362,6 +362,11 @@ export type AdminPage = {
* Component defined by using {@link AdminBro.bundle}
*/
component: string;
+
+ /**
+ * Page icon
+ */
+ icon?: string;
}
/**
diff --git a/src/backend/decorators/page-json.interface.ts b/src/backend/decorators/page-json.interface.ts
index e9a2fb194..10ce31378 100644
--- a/src/backend/decorators/page-json.interface.ts
+++ b/src/backend/decorators/page-json.interface.ts
@@ -11,4 +11,9 @@ export default interface PageJSON {
* Page component. Bundled with {@link AdminBro.bundle}
*/
component: string;
+
+ /**
+ * Page icon
+ */
+ icon?: string;
}
diff --git a/src/backend/utils/options-parser.ts b/src/backend/utils/options-parser.ts
index 75eb49895..1d428c115 100644
--- a/src/backend/utils/options-parser.ts
+++ b/src/backend/utils/options-parser.ts
@@ -34,7 +34,7 @@ export const getBranding = async (
const { branding } = admin.options
const defaultLogo = slash(path.join(
admin.options.rootPath,
- '/frontend/assets/logo-mini.svg',
+ '/frontend/assets/logo.svg',
))
const computed = typeof branding === 'function'
diff --git a/src/frontend/assets/images/logo.svg b/src/frontend/assets/images/logo.svg
index 7a94ce318..5d4f77fba 100644
--- a/src/frontend/assets/images/logo.svg
+++ b/src/frontend/assets/images/logo.svg
@@ -1,13 +1,17 @@
-
+
+
\ No newline at end of file
diff --git a/src/frontend/components/app/sidebar/index.ts b/src/frontend/components/app/sidebar/index.ts
index 530d5dc60..1642cdb60 100644
--- a/src/frontend/components/app/sidebar/index.ts
+++ b/src/frontend/components/app/sidebar/index.ts
@@ -1,7 +1,5 @@
import Sidebar from './sidebar'
-import SidebarParent from './sidebar-parent'
import groupResources from './utils/group-resources'
-import SidebarResource from './sidebar-resource'
export * from './sidebar-resource-section'
-export { SidebarParent, Sidebar, groupResources, SidebarResource }
+export { Sidebar, groupResources }
diff --git a/src/frontend/components/app/sidebar/sidebar-branding.tsx b/src/frontend/components/app/sidebar/sidebar-branding.tsx
index 5334ae248..b09b0e4b5 100644
--- a/src/frontend/components/app/sidebar/sidebar-branding.tsx
+++ b/src/frontend/components/app/sidebar/sidebar-branding.tsx
@@ -1,44 +1,57 @@
import React from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
-import { H5 } from '@admin-bro/design-system'
+import { cssClass, themeGet, Text } from '@admin-bro/design-system'
import ViewHelpers from '../../../../backend/utils/view-helpers'
import { BrandingOptions } from '../../../../admin-bro-options.interface'
+import allowOverride from '../../../hoc/allow-override'
-const LogoLink = styled(Link)`
+type Props = {
+ branding: BrandingOptions;
+}
+
+const StyledLogo = styled(Link)`
+ text-align: center;
display: flex;
- align-items: center;
+ align-content: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding: ${themeGet('space', 'lg')} ${themeGet('space', 'xxl')} ${themeGet('space', 'xxl')};
text-decoration: none;
- color: ${({ theme }): string => theme.colors.grey80};
- & > img {
- margin-right: 12px;
+ & > h1 {
+ text-decoration: none;
+ font-weight: ${themeGet('fontWeights', 'bolder')};
+ font-size: ${themeGet('fontWeights', 'bolder')};
+ color: ${themeGet('colors', 'grey80')};
+ font-size: ${themeGet('fontSizes', 'xl')};
+ line-height: ${themeGet('lineHeights', 'xl')};
+ }
+
+ &:hover h1 {
+ color: ${themeGet('colors', 'primary100')};
}
`
-type Props = {
- branding: BrandingOptions;
-}
+const h = new ViewHelpers()
const SidebarBranding: React.FC = (props) => {
const { branding } = props
const { logo, companyName } = branding
- const h = new ViewHelpers()
return (
-
-
- {logo && (
-
- )}
- {companyName}
-
-
+
+ {logo ? (
+
+ ) : companyName
}
+
)
}
-export default SidebarBranding
+export default allowOverride(SidebarBranding, 'SidebarBranding')
diff --git a/src/frontend/components/app/sidebar/sidebar-footer.tsx b/src/frontend/components/app/sidebar/sidebar-footer.tsx
index c17b20e9a..2029300bb 100644
--- a/src/frontend/components/app/sidebar/sidebar-footer.tsx
+++ b/src/frontend/components/app/sidebar/sidebar-footer.tsx
@@ -1,23 +1,11 @@
import React from 'react'
-import { Box, Text, Icon, Link } from '@admin-bro/design-system'
+import { Box, SoftwareBrothers } from '@admin-bro/design-system'
import allowOverride from '../../../hoc/allow-override'
const SidebarFooter: React.FC = () => (
-
- With
-
- by
-
- SoftwareBrothers
-
-
+
)
diff --git a/src/frontend/components/app/sidebar/sidebar-pages.tsx b/src/frontend/components/app/sidebar/sidebar-pages.tsx
index 04df4859f..a277328ed 100644
--- a/src/frontend/components/app/sidebar/sidebar-pages.tsx
+++ b/src/frontend/components/app/sidebar/sidebar-pages.tsx
@@ -1,44 +1,51 @@
import React from 'react'
-import { Box, Label, Text } from '@admin-bro/design-system'
+import { Navigation, NavigationElementProps } from '@admin-bro/design-system'
-import { ReduxState } from '../../../store/store'
-import SidebarLink from './styled/sidebar-link.styled'
+import { useHistory, useLocation } from 'react-router'
import ViewHelpers from '../../../../backend/utils/view-helpers'
import { useTranslation } from '../../../hooks/use-translation'
+import { ReduxState } from '../../../store/store'
type Props = {
pages?: ReduxState['pages'];
}
+const h = new ViewHelpers()
+
const SidebarPages: React.FC = (props) => {
const { pages } = props
const { translateLabel } = useTranslation()
-
- const h = new ViewHelpers()
+ const location = useLocation()
+ const history = useHistory()
if (!pages || !pages.length) {
return (<>>)
}
- const isActive = (page, location): boolean => (
+ const isActive = (page): boolean => (
!!location.pathname.match(`/pages/${page.name}`)
)
+ const elements: Array = pages.map(page => ({
+ id: page.name,
+ label: page.name,
+ isSelected: isActive(page),
+ icon: page.icon,
+ href: h.pageUrl(page.name),
+ onClick: (event, element): void => {
+ event.preventDefault()
+ if (element.href) {
+ history.push(element.href)
+ }
+ },
+ }))
+
return (
-
-
- {pages.map(page => (
- isActive(page, location)}
- data-testid="sidebar-page-link"
- >
- {translateLabel(page.name)}
-
- ))}
-
+
)
}
diff --git a/src/frontend/components/app/sidebar/sidebar-parent.tsx b/src/frontend/components/app/sidebar/sidebar-parent.tsx
deleted file mode 100644
index 3ce9cf06b..000000000
--- a/src/frontend/components/app/sidebar/sidebar-parent.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react'
-import { NavGroup, Box, cssClass } from '@admin-bro/design-system'
-
-import SidebarResource from './sidebar-resource'
-import ResourceJSON from '../../../../backend/decorators/resource-json.interface'
-import { useTranslation } from '../../../hooks'
-
-
-type Props = {
- parent: {
- icon: string;
- name: string;
- resources: Array;
- };
-}
-
-const SidebarParent: React.FC = (props) => {
- const { parent } = props
- const { icon, name, resources } = parent
- const { translateLabel } = useTranslation()
-
- if (!parent.name) {
- return (
-
- {resources.map(resource => (
-
- ))}
-
- )
- }
-
- return (
-
- {resources.map(resource => (
-
- ))}
-
- )
-}
-export default SidebarParent
diff --git a/src/frontend/components/app/sidebar/sidebar-resource-section.tsx b/src/frontend/components/app/sidebar/sidebar-resource-section.tsx
index 60742d07d..33749dcaa 100644
--- a/src/frontend/components/app/sidebar/sidebar-resource-section.tsx
+++ b/src/frontend/components/app/sidebar/sidebar-resource-section.tsx
@@ -1,8 +1,11 @@
import React, { FC } from 'react'
+import { Box, cssClass, Navigation } from '@admin-bro/design-system'
+import { useHistory, useLocation } from 'react-router'
+import { useTranslation } from '../../../hooks/use-translation'
import groupResources from './utils/group-resources'
-import SidebarParent from './sidebar-parent'
import ResourceJSON from '../../../../backend/decorators/resource-json.interface'
import allowOverride from '../../../hoc/allow-override'
+import { useLocalStorage } from '../../../hooks'
/**
* @alias SidebarResourceSectionProps
@@ -13,6 +16,11 @@ export type SidebarResourceSectionProps = {
resources: Array;
}
+const isSelected = (href, location): boolean => {
+ const regExp = new RegExp(`${href}($|/)`)
+ return !!location.pathname.match(regExp)
+}
+
/**
* Groups resources by sections and renders the list in {@link Sidebar}
*
@@ -27,17 +35,37 @@ export type SidebarResourceSectionProps = {
* @name SidebarResourceSection
*/
const SidebarResourceSectionOriginal: FC = ({ resources }) => {
- const groupedResources = groupResources(resources)
+ const [openElements, setOpenElements] = useLocalStorage>(
+ 'sidebarElements', {},
+ )
+ const history = useHistory()
+ const location = useLocation()
+
+ const elements = groupResources(resources).map((element, index) => ({
+ ...element,
+ onClick: (): void => setOpenElements({
+ ...openElements,
+ [index]: !openElements[index],
+ }),
+ isOpen: !!openElements[index],
+ elements: element.elements?.map(subElement => ({
+ ...subElement,
+ onClick: (event): void => {
+ if (subElement.href) {
+ event.preventDefault()
+ history.push(subElement.href)
+ }
+ },
+ isSelected: isSelected(subElement.href, location),
+ })),
+ }))
+ const { translateLabel } = useTranslation()
return (
- <>
- {
- groupedResources
- .map(parent => (
-
- ))
- }
- >
+
)
}
diff --git a/src/frontend/components/app/sidebar/sidebar-resource.tsx b/src/frontend/components/app/sidebar/sidebar-resource.tsx
deleted file mode 100644
index 6e9fce27c..000000000
--- a/src/frontend/components/app/sidebar/sidebar-resource.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react'
-import { withRouter } from 'react-router-dom'
-import { RouteComponentProps } from 'react-router'
-import { Text } from '@admin-bro/design-system'
-
-import ResourceJSON from '../../../../backend/decorators/resource-json.interface'
-import SidebarLink from './styled/sidebar-link.styled'
-
-
-type Props = {
- resource: ResourceJSON;
-}
-
-const SidebarResource: React.FC = (props) => {
- const { resource } = props
- const regExp = new RegExp(`/resources/${resource.id}($|/)`)
- const isActive = (match, location): boolean => !!location.pathname.match(regExp)
- if (!resource.href) {
- return null
- }
- return (
-
- {resource.name}
-
- )
-}
-
-export default withRouter(SidebarResource)
diff --git a/src/frontend/components/app/sidebar/sidebar.tsx b/src/frontend/components/app/sidebar/sidebar.tsx
index 740a60d6b..c14ce1b83 100644
--- a/src/frontend/components/app/sidebar/sidebar.tsx
+++ b/src/frontend/components/app/sidebar/sidebar.tsx
@@ -1,13 +1,14 @@
import React from 'react'
import { useSelector } from 'react-redux'
-import { Navigation, Box, Label, cssClass } from '@admin-bro/design-system'
+import { Box, cssClass } from '@admin-bro/design-system'
import { BrandingOptions } from 'src/admin-bro-options.interface'
+import ResourceJSON from 'src/backend/decorators/resource-json.interface'
+import PageJSON from 'src/backend/decorators/page-json.interface'
import SidebarBranding from './sidebar-branding'
import SidebarPages from './sidebar-pages'
import { ReduxState } from '../../../store/store'
import SidebarFooter from './sidebar-footer'
-import { useTranslation } from '../../../hooks/use-translation'
import SidebarResourceSection from './sidebar-resource-section'
@@ -15,35 +16,29 @@ type Props = {
isVisible: boolean;
}
-//
-
-//
-//
-//
-//
-//
-// {branding.softwareBrothers && }
-//
-
const Sidebar: React.FC = (props) => {
const { isVisible } = props
- const [branding, resources, pages] = useSelector((state: ReduxState) => [
- state.branding, state.resources, state.pages,
- ])
-
- const { translateLabel } = useTranslation()
-
- // console.log(resources, pages)
+ const [branding, resources, pages]: [BrandingOptions, ResourceJSON[], PageJSON[]] = useSelector(
+ (state: ReduxState) => [
+ state.branding, state.resources, state.pages,
+ ],
+ )
return (
-
-
-
+
+
+
+
- Hello
+
+ {branding?.softwareBrothers && }
)
}
diff --git a/src/frontend/components/app/sidebar/utils/group-resources.spec.ts b/src/frontend/components/app/sidebar/utils/group-resources.spec.ts
new file mode 100644
index 000000000..8c546cdec
--- /dev/null
+++ b/src/frontend/components/app/sidebar/utils/group-resources.spec.ts
@@ -0,0 +1,38 @@
+import { NavigationProps } from '@admin-bro/design-system'
+import { expect } from 'chai'
+import ResourceJSON from '../../../../../backend/decorators/resource-json.interface'
+import factory from '../../../spec/factory'
+
+import groupResources from './group-resources'
+
+describe.only('groupResources', () => {
+ let resources: Array
+ let grouped: NavigationProps['elements']
+ let parent1: ResourceJSON['parent']
+ let parent2: ResourceJSON['parent']
+
+ beforeEach(async () => {
+ parent1 = { name: 'Volvo', icon: 'volvo' }
+ parent2 = { name: 'Audi', icon: 'audi' }
+
+ resources = [
+ await factory.build('ResourceJSON', { parent: parent1 }),
+ await factory.build('ResourceJSON', { parent: parent1 }),
+ await factory.build('ResourceJSON', { parent: parent1 }),
+ await factory.build('ResourceJSON', { parent: parent2 }),
+ ]
+
+ grouped = groupResources(resources)
+ })
+
+ it('groups nested resources into parents', async () => {
+ expect(grouped.length).to.equal(2)
+ expect(grouped[0].label).to.eq(parent1?.name)
+ expect(grouped[1].label).to.eq(parent2?.name)
+ })
+
+ it('moves all nested elements to given parent', () => {
+ expect(grouped[0].elements?.length).to.eq(3)
+ expect(grouped[0].elements?.[0].label).to.eq(resources[0].name)
+ })
+})
diff --git a/src/frontend/components/app/sidebar/utils/group-resources.ts b/src/frontend/components/app/sidebar/utils/group-resources.ts
index bf20f76a3..bef622c01 100644
--- a/src/frontend/components/app/sidebar/utils/group-resources.ts
+++ b/src/frontend/components/app/sidebar/utils/group-resources.ts
@@ -1,11 +1,15 @@
+import { NavigationElementProps, NavigationProps } from '@admin-bro/design-system'
import ResourceJSON from '../../../../../backend/decorators/resource-json.interface'
+const resourceToNavigationElement = (
+ resource: ResourceJSON,
+): NavigationElementProps => ({
+ href: resource.href || undefined,
+ label: resource.name,
+})
+
/* eslint-disable no-param-reassign */
-export default (resources: Array): Array<{
- name: string;
- icon: string;
- resources: Array;
-}> => {
+export default (resources: Array): NavigationProps['elements'] => {
const visibleResources = resources.filter(res => res.href)
const map = visibleResources.reduce((memo, resource) => {
const key = resource.parent?.name || ''
@@ -18,8 +22,8 @@ export default (resources: Array): Array<{
return memo
}, {})
return Object.keys(map).map(parentName => ({
- name: parentName,
+ label: parentName,
icon: map[parentName].icon,
- resources: map[parentName],
+ elements: map[parentName].map(resourceToNavigationElement),
}))
}
diff --git a/src/frontend/components/application.tsx b/src/frontend/components/application.tsx
index 9da38ef59..0e7b7258f 100644
--- a/src/frontend/components/application.tsx
+++ b/src/frontend/components/application.tsx
@@ -2,7 +2,7 @@
import React, { useState } from 'react'
import { Switch, Route } from 'react-router-dom'
import { createGlobalStyle } from 'styled-components'
-import { Box, Overlay } from '@admin-bro/design-system'
+import { Box, Overlay, Reset } from '@admin-bro/design-system'
import ViewHelpers from '../../backend/utils/view-helpers'
import Sidebar from './app/sidebar/sidebar'
@@ -40,6 +40,7 @@ const App: React.FC = () => {
return (
+
{sidebarVisible ? (
diff --git a/src/frontend/components/property-type/reference/reference-value.tsx b/src/frontend/components/property-type/reference/reference-value.tsx
index 1303c0898..62c2b9a55 100644
--- a/src/frontend/components/property-type/reference/reference-value.tsx
+++ b/src/frontend/components/property-type/reference/reference-value.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import styled, { DefaultTheme, ThemedStyledProps } from 'styled-components'
+import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { ButtonCSS, ButtonProps } from '@admin-bro/design-system'
@@ -12,7 +12,7 @@ interface Props {
record: RecordJSON;
}
-const StyledLink = styled(Link)`
+const StyledLink = styled(Link)`
${ButtonCSS};
padding-left: ${({ theme }): string => theme.space.xs};
padding-right: ${({ theme }): string => theme.space.xs};
diff --git a/src/frontend/hooks/index.ts b/src/frontend/hooks/index.ts
index b7581c51b..00217ddab 100644
--- a/src/frontend/hooks/index.ts
+++ b/src/frontend/hooks/index.ts
@@ -4,5 +4,6 @@ export * from './use-translation'
export * from './use-record/use-record'
export * from './use-records'
export * from './use-current-admin'
+export * from './use-local-storage'
export { default as updateRecord } from './use-record/update-record'
diff --git a/src/frontend/hooks/use-local-storage.ts b/src/frontend/hooks/use-local-storage.ts
new file mode 100644
index 000000000..545911f38
--- /dev/null
+++ b/src/frontend/hooks/use-local-storage.ts
@@ -0,0 +1,44 @@
+/* eslint-disable no-console */
+import React, { useState } from 'react'
+
+export type UseLocalStorageResult = [
+ T,
+ React.Dispatch>
+]
+
+// Hook
+// https://usehooks.com/useLocalStorage/
+export function useLocalStorage(key: string, initialValue: T): UseLocalStorageResult {
+ // State to store our value
+ // Pass initial state function to useState so logic is only executed once
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ // Get from local storage by key
+ const item = window.localStorage.getItem(key)
+ // Parse stored json or if none return initialValue
+ return item ? JSON.parse(item) : initialValue
+ } catch (error) {
+ // If error also return initialValue
+ console.log(error)
+ return initialValue
+ }
+ })
+
+ // Return a wrapped version of useState's setter function that ...
+ // ... persists the new value to localStorage.
+ const setValue: React.Dispatch> = (value) => {
+ try {
+ // Allow value to be a function so we have same API as useState
+ const valueToStore = value instanceof Function ? value(storedValue) : value
+ // Save state
+ setStoredValue(valueToStore as any)
+ // Save to local storage
+ window.localStorage.setItem(key, JSON.stringify(valueToStore))
+ } catch (error) {
+ // A more advanced implementation would handle the error case
+ console.log(error)
+ }
+ }
+
+ return [storedValue, setValue]
+}
diff --git a/src/frontend/store/pages-to-store.ts b/src/frontend/store/pages-to-store.ts
index ce8c10df0..f39866890 100644
--- a/src/frontend/store/pages-to-store.ts
+++ b/src/frontend/store/pages-to-store.ts
@@ -5,6 +5,7 @@ const pagesToStore = (pages: Record): Array => {
const pagesArray = Object.entries(pages).map(([key, adminPage]) => ({
name: key,
component: adminPage.component,
+ icon: adminPage.icon,
}))
return pagesArray
}
diff --git a/src/frontend/utils/overridable-component.ts b/src/frontend/utils/overridable-component.ts
index 6674e2d8a..075d060bb 100644
--- a/src/frontend/utils/overridable-component.ts
+++ b/src/frontend/utils/overridable-component.ts
@@ -1,4 +1,8 @@
/**
* Name of the components which can be overridden by AdminBro.bundle
*/
-export type OverridableComponent = 'LoggedIn' | 'NoRecords' | 'SidebarResourceSection' | 'SidebarFooter'
+export type OverridableComponent = 'LoggedIn'
+ | 'NoRecords'
+ | 'SidebarResourceSection'
+ | 'SidebarFooter'
+ | 'SidebarBranding'