Skip to content

Commit

Permalink
feat: add Breadcrumb on SearchTreeView (#803)
Browse files Browse the repository at this point in the history
* RM#86490
  • Loading branch information
vhu-axelor authored Nov 29, 2024
1 parent 9ecd41a commit b45087c
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 6 deletions.
5 changes: 5 additions & 0 deletions changelogs/unreleased/86490_Breadcrumb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Breadcrumb: add new component",
"type": "feat",
"packages": "ui"
}
5 changes: 5 additions & 0 deletions changelogs/unreleased/86490_SearchTreeView.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "SearchTreeView: add Breadcrumb on view",
"type": "feat",
"packages": "core"
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ActionCardType,
ActionType,
AutoCompleteSearch,
Breadcrumb,
HeaderContainer,
TreeView,
} from '@axelor/aos-mobile-ui';
Expand Down Expand Up @@ -64,6 +65,7 @@ interface SearchTreeViewProps {
renderLeaf: (item: any) => any;
actionList?: ActionType[];
verticalActions?: boolean;
displayBreadcrumb?: boolean;
}

const SearchTreeView = ({
Expand Down Expand Up @@ -100,13 +102,28 @@ const SearchTreeView = ({
renderLeaf,
actionList,
verticalActions,
displayBreadcrumb = false,
}: SearchTreeViewProps) => {
const I18n = useTranslator();
const dispatch = useDispatch();
const isFocused = useIsFocused();

const [filter, setFilter] = useState(null);
const [parent, setParent] = useState(null);
const [parent, setParent] = useState([]);

const handleChangeParent = value => {
setParent(current => {
const _parent = [...current];

if (value) {
_parent.at(-1)?.id !== value?.id && _parent.push(value);
} else {
_parent.pop();
}

return _parent;
});
};

const fetchParentSearchAPI = useCallback(
({page = 0, searchValue}) => {
Expand All @@ -126,9 +143,9 @@ const SearchTreeView = ({
dispatch(
sliceFunction({
...(sliceFunctionData ?? {}),
[sliceFunctionDataParentIdName]: parent?.id,
[sliceFunctionDataParentIdName]: parent.at(-1)?.id,
[sliceFunctionDataNoParentName]:
parent == null && searchValue == null,
parent.length === 0 && searchValue == null,
searchValue: searchValue,
page: page,
}),
Expand Down Expand Up @@ -169,8 +186,8 @@ const SearchTreeView = ({
return (
<AutoCompleteSearch
objectList={parentList}
value={parent}
onChangeValue={setParent}
value={parent.at(-1)}
onChangeValue={handleChangeParent}
fetchData={fetchParentSearchAPI}
displayValue={displayParentSearchValue}
placeholder={searchParentPlaceholder}
Expand Down Expand Up @@ -233,6 +250,16 @@ const SearchTreeView = ({
{isHideableSearch && renderSearchBar()}
{headerChildren}
</HeaderContainer>
{displayBreadcrumb && (
<Breadcrumb
style={styles.breadcrumb}
items={parent.map((item, index) => ({
title: displayParentSearchValue(item),
onPress: () => setParent(current => current.slice(0, index + 1)),
}))}
onHomePress={() => setParent([])}
/>
)}
<TreeView
loadingList={loading}
data={list}
Expand All @@ -244,7 +271,7 @@ const SearchTreeView = ({
fetchData={fetchListAPI}
fetchBranchData={fetchBranchData}
branchCondition={branchCondition}
onBranchFilterPress={setParent}
onBranchFilterPress={handleChangeParent}
moreLoading={moreLoading}
isListEnd={isListEnd}
translator={I18n.t}
Expand All @@ -259,6 +286,9 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
breadcrumb: {
marginTop: 8,
},
});

export default SearchTreeView;
107 changes: 107 additions & 0 deletions packages/ui/__tests__/components/molecules/Breadcrumb.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Axelor Business Solutions
*
* Copyright (C) 2024 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import {TouchableOpacity, View} from 'react-native';
import {shallow} from 'enzyme';
import {Icon, Text, Breadcrumb} from '@axelor/aos-mobile-ui';
import {getGlobalStyles} from '../../tools';

describe('Breadcrumb Component', () => {
const mockOnHomePress = jest.fn();
const mockOnPress = jest.fn();
const props = {
items: [
{title: 'Home', onPress: mockOnPress},
{title: 'Products'},
{title: 'Electronics'},
],
onHomePress: mockOnHomePress,
};

it('renders without crashing', () => {
const wrapper = shallow(<Breadcrumb {...props} />);
expect(wrapper.exists()).toBe(true);
});

it('renders the home icon with correct props', () => {
const wrapper = shallow(<Breadcrumb {...props} />);
const homeIcon = wrapper.find(Icon).first();

expect(homeIcon.exists()).toBeTruthy();
expect(homeIcon.prop('name')).toBe('house-door-fill');
expect(homeIcon.prop('touchable')).toBe(true);
homeIcon.simulate('press');
expect(mockOnHomePress).toHaveBeenCalled();
});

it('renders the correct number of breadcrumb items', () => {
const wrapper = shallow(<Breadcrumb {...props} />);
const breadcrumbItems = wrapper.find(Text);

expect(breadcrumbItems.length).toBe(props.items.length);
});

it('renders the chevron icon between breadcrumb items', () => {
const wrapper = shallow(<Breadcrumb {...props} />);
const chevrons = wrapper.findWhere(
node => node.type() === Icon && node.prop('name') === 'chevron-right',
);

expect(chevrons.length).toBe(props.items.length);
});

it('renders breadcrumb items with onPress handlers when provided', () => {
const wrapper = shallow(<Breadcrumb {...props} />);
const clickableItem = wrapper.findWhere(
node =>
node.type() === TouchableOpacity &&
node.find(Text).prop('children') === 'Home',
);

expect(clickableItem.exists()).toBeTruthy();
clickableItem.simulate('press');
expect(mockOnPress).toHaveBeenCalled();
});

it('does not render onPress for items without handlers', () => {
const wrapper = shallow(<Breadcrumb {...props} />);
const nonClickableItem = wrapper.findWhere(
node => node.type() === Text && node.prop('children') === 'Products',
);

expect(nonClickableItem.exists()).toBeTruthy();
expect(nonClickableItem.prop('onPress')).toBeUndefined();
});

it('should apply custom styles to the container when style prop is provided', () => {
const customStyle = {
fontSize: 25,
width: '90%',
height: 25,
flexDirection: 'row',
alignSelf: 'center',
};

const wrapper = shallow(<Breadcrumb {...props} style={customStyle} />);

expect(getGlobalStyles(wrapper.find(View).at(0))).toMatchObject(
customStyle,
);
});
});
124 changes: 124 additions & 0 deletions packages/ui/src/components/molecules/Breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Axelor Business Solutions
*
* Copyright (C) 2024 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React, {useEffect, useState} from 'react';
import {StyleSheet, TouchableOpacity, View} from 'react-native';
import {useThemeColor} from '../../../theme/ThemeContext';
import {Icon, Text} from '../../atoms';

const ICON_WIDTH = 18;
const EMPTY_ITEM_WIDTH = 31;
const EMPTY_ITEM = {title: '...'};

interface Item {
title: string;
onPress?: () => void;
}

interface BreadcrumbProps {
style?: any;
items: Item[];
onHomePress: () => void;
}

const Breadcrumb = ({style, items, onHomePress}: BreadcrumbProps) => {
const Colors = useThemeColor();

const [visibleItems, setVisibleItems] = useState(items);
const [containerWidth, setContainerWidth] = useState(0);
const [itemsWidth, setItemsWidth] = useState<number[]>([]);

useEffect(() => {
setVisibleItems(items);
}, [items]);

const handleItemLayout = (event, index) => {
const {width} = event.nativeEvent.layout;
setItemsWidth(prevWidths => {
const newWidths = [...prevWidths];
newWidths[index] = width;
return newWidths;
});
};

useEffect(() => {
let totalWidth = itemsWidth.reduce((acc, width) => acc + width, ICON_WIDTH);

if (
items.length > 0 &&
itemsWidth.length === items.length &&
totalWidth > containerWidth
) {
let newVisibleItems = [...items];

let idx = 0;
while (totalWidth > containerWidth) {
if (idx === 0) {
newVisibleItems = [EMPTY_ITEM, ...newVisibleItems.slice(1)];
totalWidth += EMPTY_ITEM_WIDTH;
} else {
newVisibleItems = [EMPTY_ITEM, ...newVisibleItems.slice(2)];
}
totalWidth -= itemsWidth[idx];
idx++;
}

setVisibleItems(newVisibleItems);
}
}, [containerWidth, items, itemsWidth]);

return (
<View
style={[styles.container, style]}
onLayout={e => setContainerWidth(e?.nativeEvent?.layout?.width)}>
<Icon name="house-door-fill" touchable onPress={onHomePress} />
{visibleItems.map((item, index) => (
<View
style={styles.contentContainer}
onLayout={e => handleItemLayout(e, index)}
key={index}>
<Icon name="chevron-right" color={Colors.secondaryColor.background} />
<TouchableOpacity
style={styles.textContainer}
activeOpacity={0.7}
disabled={!item.onPress}
onPress={item.onPress}>
<Text>{item.title}</Text>
</TouchableOpacity>
</View>
))}
</View>
);
};

const styles = StyleSheet.create({
container: {
width: '90%',
height: 25,
flexDirection: 'row',
alignSelf: 'center',
},
contentContainer: {
flexDirection: 'row',
},
textContainer: {
justifyContent: 'center',
},
});

export default Breadcrumb;
1 change: 1 addition & 0 deletions packages/ui/src/components/molecules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {default as Alert} from './Alert/Alert';
export {default as AttachmentCard} from './AttachmentCard/AttachmentCard';
export {default as Badge} from './Badge/Badge';
export {default as BlockInteractionMessage} from './BlockInteractionMessage/BlockInteractionMessage';
export {default as Breadcrumb} from './Breadcrumb/Breadcrumb';
export {default as Button} from './Button/Button';
export {default as CardIconButton} from './CardIconButton/CardIconButton';
export {default as CardIndicator} from './CardIndicator/CardIndicator';
Expand Down
Loading

0 comments on commit b45087c

Please sign in to comment.