From 7597159da259cbf7458cb98217f8110eead75cac Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Fri, 16 Apr 2021 16:05:10 +0300 Subject: [PATCH] feat(core-blocks): CB-872 metadata editor properies groups --- .../src/Containers/ILayoutSizeProps.ts | 5 + .../src/FormControls/Checkboxes/Checkbox.tsx | 9 +- .../Checkboxes/FieldCheckboxNew.tsx | 19 +++- .../src/FormControls/Checkboxes/Switch.tsx | 9 +- .../src/FormControls/Checkboxes/SwitchNew.tsx | 9 +- .../Checkboxes/useCheckboxState.ts | 7 +- .../src/FormControls/InputField.tsx | 10 +- .../src/FormControls/InputFieldNew.tsx | 10 +- .../ObjectPropertyInfoForm.tsx | 14 +-- .../ObjectPropertyInfoFormNew.tsx | 86 +++++++++++++++ .../ObjectPropertyInfoForm/RenderField.tsx} | 103 ++++++------------ .../ObjectPropertyInfoForm/formStyles.ts | 0 .../useObjectPropertyCategories.tsx | 46 ++++++++ webapp/packages/core-blocks/src/index.ts | 5 +- .../ObjectInfoTab/ObjectProperties.tsx | 37 +++++-- 15 files changed, 273 insertions(+), 96 deletions(-) rename webapp/packages/core-blocks/src/{ => ObjectPropertyInfo}/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx (91%) create mode 100644 webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx rename webapp/packages/core-blocks/src/{ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx => ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx} (59%) rename webapp/packages/core-blocks/src/{ => ObjectPropertyInfo}/ObjectPropertyInfoForm/formStyles.ts (100%) create mode 100644 webapp/packages/core-blocks/src/ObjectPropertyInfo/useObjectPropertyCategories.tsx diff --git a/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts b/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts index f81363ab72..4e1f97023e 100644 --- a/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts +++ b/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts @@ -6,11 +6,16 @@ * you may not use this file except in compliance with the License. */ +export type Layout = 'mixedControls'; export interface ILayoutSizeProps { keepSize?: boolean; tiny?: boolean; small?: boolean; medium?: boolean; large?: boolean; + /** + * Used by components that want to change their size or other styles due to diffrent layout + */ + layout?: Layout; fill?: boolean; } diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index f7b575057a..239d12db35 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -61,7 +61,14 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ onChange, ...rest }: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { - const checkboxState = useCheckboxState({ value, checked, state, name, onChange }); + const checkboxState = useCheckboxState({ + value, + checked, + defaultChecked: rest.defaultChecked, + state, + name, + onChange, + }); if (autoHide && !isControlPresented(name, state)) { return null; diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx index 82c135094e..40d3a815bb 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import styled, { css } from 'reshadow'; +import styled, { css, use } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -22,6 +22,14 @@ const fieldCheckboxStyles = css` display: flex; align-items: flex-end; white-space: pre-wrap; + &[|layout='mixedControls'] layout { + /* same as input height */ + height: 32px; + } + } + layout { + display: flex; + align-items: center; } field-label { composes: theme-typography--body2 from global; @@ -35,6 +43,7 @@ const fieldCheckboxStyles = css` export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ children, className, + layout, ...rest }: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { const styles = useStyles(baseFormControlStylesNew, fieldCheckboxStyles); @@ -44,9 +53,11 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ } return styled(styles)( - - - {children} + + + + {children} + ); }; diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx index 75e8698515..3ab94b7e6d 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx @@ -103,7 +103,14 @@ export const Switch: SwitchType = observer(function Switch({ disabled, ...rest }: IBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { - const checkboxState = useCheckboxState({ value, checked, state, name, onChange }); + const checkboxState = useCheckboxState({ + value, + checked, + defaultChecked: rest.defaultChecked, + state, + name, + onChange, + }); const styles = useStyles( baseFormControlStyles, switchStyles, diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/SwitchNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/SwitchNew.tsx index 02a797cb9e..07b7e68946 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/SwitchNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/SwitchNew.tsx @@ -106,7 +106,14 @@ export const SwitchNew: SwitchType = observer(function SwitchNew({ disabled, ...rest }: IBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { - const checkboxState = useCheckboxState({ value, checked, state, name, onChange }); + const checkboxState = useCheckboxState({ + value, + checked, + defaultChecked: rest.defaultChecked, + state, + name, + onChange, + }); const styles = useStyles( baseFormControlStylesNew, switchStyles, diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts b/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts index a2b3396b12..9ecf383417 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts @@ -11,11 +11,12 @@ import { useContext, useState } from 'react'; import { useObjectRef } from '../../useObjectRef'; import { FormContext } from '../FormContext'; -export type CheckboxOnChangeEvent =(value: boolean, name: T) => void; +export type CheckboxOnChangeEvent = (value: boolean, name: T) => void; export type CheckboxStateOptions = { value: string | undefined; checked: boolean | undefined; + defaultChecked: boolean | undefined; } & ( { state: undefined; @@ -42,7 +43,7 @@ export function useCheckboxState(options: CheckboxStateOpti let checked = optionsRef.checked ?? false; - if (state !== undefined && name !== undefined) { + if (state !== undefined && name !== undefined && name in state) { const currentState = state[name as TKey]; if (typeof value === 'string' && Array.isArray(currentState)) { @@ -52,6 +53,8 @@ export function useCheckboxState(options: CheckboxStateOpti } else { checked = !!currentState; } + } else if (optionsRef.defaultChecked !== undefined) { + checked = optionsRef.defaultChecked; } return useObjectRef({ diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.tsx b/webapp/packages/core-blocks/src/FormControls/InputField.tsx index 5cda6bd2c6..5b63ae2c49 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputField.tsx @@ -76,7 +76,15 @@ export const InputField: InputFieldType = observer(function InputField({ } }, [state, name, context, onChange]); - const value = state ? state[name] : valueControlled; + let value: any = valueControlled; + + if (state) { + if (name in state) { + value = state[name]; + } else if (rest.defaultValue !== undefined) { + value = rest.defaultValue; + } + } if (autoHide && !isControlPresented(name, state)) { return null; diff --git a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx index eab775a1ec..8a587ef907 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx @@ -100,7 +100,15 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ return null; } - let value = state ? state[name] : valueControlled; + let value: any = valueControlled; + + if (state) { + if (name in state) { + value = state[name]; + } else if (rest.defaultValue !== undefined) { + value = rest.defaultValue; + } + } if (mapState) { value = mapState(value); diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx similarity index 91% rename from webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx rename to webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx index 5d465f85c0..d20b577a99 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm.tsx @@ -14,13 +14,13 @@ import { InputField } from '@cloudbeaver/core-blocks'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; -import { FieldCheckbox } from '../FormControls/Checkboxes/FieldCheckbox'; -import { Combobox } from '../FormControls/Combobox'; -import { FormFieldDescription } from '../FormControls/FormFieldDescription'; -import { FormGroup } from '../FormControls/FormGroup'; -import { isControlPresented } from '../FormControls/isControlPresented'; -import { Link } from '../Link'; -import { TextPlaceholder } from '../TextPlaceholder'; +import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox'; +import { Combobox } from '../../FormControls/Combobox'; +import { FormFieldDescription } from '../../FormControls/FormFieldDescription'; +import { FormGroup } from '../../FormControls/FormGroup'; +import { isControlPresented } from '../../FormControls/isControlPresented'; +import { Link } from '../../Link'; +import { TextPlaceholder } from '../../TextPlaceholder'; import { formStyles } from './formStyles'; const RESERVED_KEYWORDS = ['no', 'off', 'new-password']; diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx new file mode 100644 index 0000000000..d6b2b20bc7 --- /dev/null +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx @@ -0,0 +1,86 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import React, { useCallback } from 'react'; + +import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; + +import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps'; +import { TextPlaceholder } from '../../TextPlaceholder'; +import { RenderField } from './RenderField'; + +interface ObjectPropertyFormProps extends ILayoutSizeProps { + properties: ObjectPropertyInfo[] | undefined; + state: Record; + category?: string | null; + editable?: boolean; + autofillToken?: string; + className?: string; + disabled?: boolean; + readOnly?: boolean; + autoHide?: boolean; + showRememberTip?: boolean; + onFocus?: (name: string) => void; +} + +export const ObjectPropertyInfoFormNew: React.FC = observer(function ObjectPropertyInfoFormNew({ + properties, + state, + category, + editable = true, + className, + autofillToken = '', + disabled, + readOnly, + autoHide, + showRememberTip, + layout, + onFocus, +}) { + const handleFocus = useCallback((e: React.FocusEvent) => { + if (onFocus) { + onFocus(e.target.name); + } + }, [onFocus]); + + if (!properties || properties.length === 0) { + return Properties empty; + } + + let filteredProperties: ObjectPropertyInfo[] | null = null; + + if (category !== undefined) { + if (category === null) { + filteredProperties = properties.filter(property => !property.category); + } else { + filteredProperties = properties.filter(property => property.category === category); + } + } + + return ( + <> + {(filteredProperties || properties).map(property => ( + + ))} + + ); +}); diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx similarity index 59% rename from webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx rename to webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index 0947969e50..5c7fcf4e59 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -7,18 +7,16 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback } from 'react'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; -import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; -import { FieldCheckboxNew } from '../FormControls/Checkboxes/FieldCheckboxNew'; -import { ComboboxNew } from '../FormControls/ComboboxNew'; -import { FormFieldDescriptionNew } from '../FormControls/FormFieldDescriptionNew'; -import { InputFieldNew } from '../FormControls/InputFieldNew'; -import { isControlPresented } from '../FormControls/isControlPresented'; -import { Link } from '../Link'; -import { TextPlaceholder } from '../TextPlaceholder'; +import type { Layout } from '../../Containers/ILayoutSizeProps'; +import { FieldCheckboxNew } from '../../FormControls/Checkboxes/FieldCheckboxNew'; +import { ComboboxNew } from '../../FormControls/ComboboxNew'; +import { FormFieldDescriptionNew } from '../../FormControls/FormFieldDescriptionNew'; +import { InputFieldNew } from '../../FormControls/InputFieldNew'; +import { isControlPresented } from '../../FormControls/isControlPresented'; +import { Link } from '../../Link'; const RESERVED_KEYWORDS = ['no', 'off', 'new-password']; @@ -32,10 +30,30 @@ interface RenderFieldProps { autoHide?: boolean; showRememberTip?: boolean; onFocus?: (event: React.FocusEvent) => void; + layout?: Layout; className?: string; } -const RenderField: React.FC = observer(function RenderField({ +function isCheckbox(property: ObjectPropertyInfo) { + return property.dataType?.toLowerCase() === 'boolean'; +} + +function getDefaultValueFor(property: ObjectPropertyInfo) { + const checkbox = isCheckbox(property); + const value = property.value; + + if (value === null || value === undefined) { + return checkbox ? false : ''; + } + + if (typeof value === 'string') { + return checkbox ? value === 'true' : value; + } + + return value.displayName || value.value || JSON.stringify(value); +} + +export const RenderField: React.FC = observer(function RenderField({ property, state, editable = true, @@ -45,12 +63,14 @@ const RenderField: React.FC = observer(function RenderField({ autoHide, showRememberTip, onFocus, + layout, className, }) { const href = property.features.includes('href'); const password = property.features.includes('password'); - const checkbox = property.dataType === 'Boolean'; + const checkbox = isCheckbox(property); const combobox = property.validValues && property.validValues.length > 0; + const defaultValue = getDefaultValueFor(property); let description: string | undefined; if (href) { @@ -81,8 +101,10 @@ const RenderField: React.FC = observer(function RenderField({ {property.displayName ?? ''} @@ -101,6 +123,7 @@ const RenderField: React.FC = observer(function RenderField({ defaultValue={property.defaultValue} title={property.description} disabled={disabled} + layout={layout} className={className} > {property.displayName ?? ''} @@ -114,6 +137,7 @@ const RenderField: React.FC = observer(function RenderField({ title={property.description} name={property.id!} state={state} + defaultValue={defaultValue} description={description} disabled={disabled} readOnly={readOnly} @@ -121,65 +145,10 @@ const RenderField: React.FC = observer(function RenderField({ autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`} mod='surface' className={className} + layout={layout} onFocus={onFocus} > {property.displayName} ); }); - -interface ObjectPropertyFormProps extends ILayoutSizeProps { - properties: ObjectPropertyInfo[] | undefined; - state: Record; - editable?: boolean; - autofillToken?: string; - className?: string; - disabled?: boolean; - readOnly?: boolean; - autoHide?: boolean; - showRememberTip?: boolean; - onFocus?: (name: string) => void; -} - -export const ObjectPropertyInfoFormNew: React.FC = observer(function ObjectPropertyInfoFormNew({ - properties, - state, - editable = true, - className, - autofillToken = '', - disabled, - readOnly, - autoHide, - showRememberTip, - onFocus, -}) { - const handleFocus = useCallback((e: React.FocusEvent) => { - if (onFocus) { - onFocus(e.target.name); - } - }, [onFocus]); - - if (!properties || properties.length === 0) { - return Properties empty; - } - - return ( - <> - {properties.map(property => ( - - ))} - - ); -}); diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfoForm/formStyles.ts b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/formStyles.ts similarity index 100% rename from webapp/packages/core-blocks/src/ObjectPropertyInfoForm/formStyles.ts rename to webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/formStyles.ts diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/useObjectPropertyCategories.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/useObjectPropertyCategories.tsx new file mode 100644 index 0000000000..92e101e04a --- /dev/null +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/useObjectPropertyCategories.tsx @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed, IComputedValue } from 'mobx'; +import { useMemo } from 'react'; + +import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; + +interface ICategoriesInfo { + categories: string[]; + isUncategorizedExists: boolean; +} + +export function useObjectPropertyCategories(properties: ObjectPropertyInfo[] | undefined) { + const categories: IComputedValue = useMemo(() => computed(() => { + const result: ICategoriesInfo = { + categories: [], + isUncategorizedExists: false, + }; + + if (!properties) { + return result; + } + + for (const property of properties) { + const category = property.category; + if (!category) { + result.isUncategorizedExists = true; + continue; + } + + if (!result.categories.includes(category)) { + result.categories.push(category); + } + } + + return result; + }), [properties]); + + return categories.get(); +} diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 5098008ea8..e2cbae0663 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -4,8 +4,9 @@ export * from './ItemList'; export * from './layout/AppLogo'; export * from './layout/TopAppBar'; export * from './Loader/Loader'; -export * from './ObjectPropertyInfoForm/ObjectPropertyInfoForm'; -export * from './ObjectPropertyInfoForm/ObjectPropertyInfoFormNew'; +export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm'; +export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormNew'; +export * from './ObjectPropertyInfo/useObjectPropertyCategories'; export * from './Placeholder/Placeholder'; export * from './Placeholder/PlaceholderContainer'; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx index 7a267970b9..aa95f6e553 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx @@ -11,13 +11,11 @@ import type { PropsWithChildren } from 'react'; import styled from 'reshadow'; import { useDatabaseObjectInfo } from '@cloudbeaver/core-app'; -import { ColoredContainer, Group, Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoFormNew, Group } from '@cloudbeaver/core-blocks'; import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; -import { ObjectProperty } from './ObjectProperty'; - type ObjectPropertiesProps = PropsWithChildren<{ objectId: string; }>; @@ -28,6 +26,7 @@ export const ObjectProperties = observer(function ObjectProperties({ const translate = useTranslate(); const { dbObject, isLoading } = useDatabaseObjectInfo(objectId); const styles = useStyles(BASE_CONTAINERS_STYLES); + const { categories, isUncategorizedExists } = useObjectPropertyCategories(dbObject?.properties); if (!dbObject?.properties && isLoading) { return ; @@ -38,12 +37,32 @@ export const ObjectProperties = observer(function ObjectProperties({ } return styled(styles)( - - - {dbObject.properties.map(v => ( - - ))} - + + {isUncategorizedExists && ( + + + + )} + {categories.map(category => ( + + {category} + + + ))} ); });