From 9ecd41ae9c74f3d74de117b1c8dc901fb0eb67b1 Mon Sep 17 00:00:00 2001 From: vhu-axelor <146069039+vhu-axelor@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:35:31 +0100 Subject: [PATCH] feat: improve branch display on TreeView (#793) * RM#86488 --- .../unreleased/86488_feat_ActionCard.json | 5 + .../unreleased/86488_feat_TreeView.json | 5 + changelogs/unreleased/86488_refactor.json | 5 + .../SearchTreeView/SearchTreeView.tsx | 4 + .../components/organisms/ActionCard.test.js | 12 ++ .../organisms/ActionCard/ActionCard.tsx | 85 +++++++++---- .../ui/src/components/organisms/index.tsx | 5 +- .../components/templates/TreeView/Branch.tsx | 13 +- .../templates/TreeView/BranchCard.tsx | 46 +++---- .../templates/TreeView/TreeView.tsx | 10 +- .../stories/organisms/ActionCard.stories.tsx | 114 ++++++++++++------ 11 files changed, 211 insertions(+), 93 deletions(-) create mode 100644 changelogs/unreleased/86488_feat_ActionCard.json create mode 100644 changelogs/unreleased/86488_feat_TreeView.json create mode 100644 changelogs/unreleased/86488_refactor.json diff --git a/changelogs/unreleased/86488_feat_ActionCard.json b/changelogs/unreleased/86488_feat_ActionCard.json new file mode 100644 index 0000000000..5502c91b1b --- /dev/null +++ b/changelogs/unreleased/86488_feat_ActionCard.json @@ -0,0 +1,5 @@ +{ + "title": "ActionCard: add the possibility to define a quick action which is displayed above the button to see all the actions", + "type": "feat", + "packages": "ui" +} diff --git a/changelogs/unreleased/86488_feat_TreeView.json b/changelogs/unreleased/86488_feat_TreeView.json new file mode 100644 index 0000000000..fa91ab2181 --- /dev/null +++ b/changelogs/unreleased/86488_feat_TreeView.json @@ -0,0 +1,5 @@ +{ + "title": "TreeView: add the possibility to define additional actions on branch", + "type": "feat", + "packages": "ui" +} diff --git a/changelogs/unreleased/86488_refactor.json b/changelogs/unreleased/86488_refactor.json new file mode 100644 index 0000000000..6b829b627d --- /dev/null +++ b/changelogs/unreleased/86488_refactor.json @@ -0,0 +1,5 @@ +{ + "title": "TreeView: use ActionCard for branch display", + "type": "refactor", + "packages": "ui" +} diff --git a/packages/core/src/components/templates/SearchTreeView/SearchTreeView.tsx b/packages/core/src/components/templates/SearchTreeView/SearchTreeView.tsx index fbc2e25ad5..5b00261536 100644 --- a/packages/core/src/components/templates/SearchTreeView/SearchTreeView.tsx +++ b/packages/core/src/components/templates/SearchTreeView/SearchTreeView.tsx @@ -19,6 +19,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import { + ActionCardType, ActionType, AutoCompleteSearch, HeaderContainer, @@ -59,6 +60,7 @@ interface SearchTreeViewProps { headerTopChildren?: any; parentFieldName?: string; renderBranch?: (item: any) => any; + getBranchActions?: (branch: any) => ActionCardType[]; renderLeaf: (item: any) => any; actionList?: ActionType[]; verticalActions?: boolean; @@ -94,6 +96,7 @@ const SearchTreeView = ({ headerTopChildren, parentFieldName, renderBranch, + getBranchActions, renderLeaf, actionList, verticalActions, @@ -236,6 +239,7 @@ const SearchTreeView = ({ parentFieldName={parentFieldName} branchCardInfoButtonIndication={I18n.t('Base_Filter')} renderBranch={renderBranch} + getBranchActions={getBranchActions} renderLeaf={renderLeaf} fetchData={fetchListAPI} fetchBranchData={fetchBranchData} diff --git a/packages/ui/__tests__/components/organisms/ActionCard.test.js b/packages/ui/__tests__/components/organisms/ActionCard.test.js index c54184e0b7..f0fb9d0e5c 100644 --- a/packages/ui/__tests__/components/organisms/ActionCard.test.js +++ b/packages/ui/__tests__/components/organisms/ActionCard.test.js @@ -58,6 +58,18 @@ describe('ActionCard Component', () => { expect(actionList[0].onPress).toHaveBeenCalled(); }); + it('should render InfoButton components for quick action if present', () => { + const quickAction = {iconName: 'heart', onPress: jest.fn()}; + const wrapper = shallow( + , + ); + + expect(wrapper.find(InfoButton).length).toBe(actionList.length + 1); + + const quickActionWrapper = wrapper.find(InfoButton).at(actionList.length); + expect(quickActionWrapper.props()).toMatchObject(quickAction); + }); + it('should apply custom style to the container if provided', () => { const customStyle = {width: 200}; const wrapper = shallow(); diff --git a/packages/ui/src/components/organisms/ActionCard/ActionCard.tsx b/packages/ui/src/components/organisms/ActionCard/ActionCard.tsx index 25747e00bc..ea32bc3c0c 100644 --- a/packages/ui/src/components/organisms/ActionCard/ActionCard.tsx +++ b/packages/ui/src/components/organisms/ActionCard/ActionCard.tsx @@ -28,7 +28,7 @@ import { const ACTION_WIDTH = 40; const TWO_ACTIONS_HEIGHT = 84; -interface Action { +export interface Action { iconName: string; iconColor?: string; helper?: string; @@ -42,6 +42,7 @@ interface ActionCardProps { style?: any; children: any; actionList: Action[]; + quickAction?: Action; horizontal?: boolean; forceActionsDisplay?: boolean; translator: (key: string) => string; @@ -51,6 +52,7 @@ const ActionCard = ({ style, children, actionList, + quickAction, horizontal = false, forceActionsDisplay = false, translator, @@ -90,6 +92,12 @@ const ActionCard = ({ [actionList], ); + const _quickAction = useMemo( + () => + quickAction != null && !quickAction.hidden ? quickAction : undefined, + [quickAction], + ); + const isMoreThanOneAction = useMemo( () => _actionList.length > 1, [_actionList], @@ -110,13 +118,14 @@ const ActionCard = ({ useEffect(() => { const shouldDisplay = - (_actionList.length > 2 || - (_actionList[0]?.large && horizontal) || - _actionList[1]?.large) && - !forceActionsDisplay; + (_quickAction != null && !_quickAction.large + ? _actionList.length > 1 + : _actionList.length > 2 || + (_actionList[0]?.large && horizontal) || + _actionList[1]?.large) && !forceActionsDisplay; setDisplaySeeActionsButton(shouldDisplay); setIsActionsVisible(!shouldDisplay); - }, [_actionList, forceActionsDisplay, horizontal]); + }, [_actionList, forceActionsDisplay, horizontal, _quickAction]); const isCardMinHeight = useMemo( () => _actionList.length > 1 || _actionList[0]?.large, @@ -129,7 +138,7 @@ const ActionCard = ({ ); const getIconColor = (action: Action) => { - return action.iconColor ?? Colors.secondaryColor_dark.background; + return action?.iconColor ?? Colors.secondaryColor_dark.background; }; const renderHorizontalActions = (list, isLastList) => { @@ -168,9 +177,7 @@ const ActionCard = ({ verticalActions.push( {_actionList.length > 0 && - (displaySeeActionsButton && !isActionsVisible ? ( - setIsActionsVisible(true)} - /> - ) : ( + isActionsVisible && + (!_quickAction || isMoreThanOneAction) && ( {horizontal ? ( <> @@ -240,7 +240,42 @@ const ActionCard = ({ )} - ))} + )} + + {_quickAction != null && _actionList.length === 1 && ( + + )} + {displaySeeActionsButton && ( + setIsActionsVisible(current => !current)} + /> + )} + {_quickAction != null && ( + + )} + ); }; @@ -257,9 +292,6 @@ const getStyles = (isMoreThanOneAction: boolean) => cardContainer: { flex: 1, }, - seeActions: { - width: 40, - }, horizontalActionContainer: { flexDirection: 'row', flexWrap: 'wrap', @@ -269,13 +301,18 @@ const getStyles = (isMoreThanOneAction: boolean) => verticalActionContainer: { flexDirection: 'row', }, + quickActionContainer: { + flexDirection: 'column-reverse', + flexWrap: 'wrap', + }, }); const getVerticalActionStyle = (isLargeAction: boolean) => StyleSheet.create({ action: { + width: ACTION_WIDTH, height: isLargeAction ? '100%' : '50%', }, - }); + }).action; export default ActionCard; diff --git a/packages/ui/src/components/organisms/index.tsx b/packages/ui/src/components/organisms/index.tsx index ab8fc63dcc..913b4316dd 100644 --- a/packages/ui/src/components/organisms/index.tsx +++ b/packages/ui/src/components/organisms/index.tsx @@ -16,7 +16,10 @@ * along with this program. If not, see . */ -export {default as ActionCard} from './ActionCard/ActionCard'; +export { + default as ActionCard, + Action as ActionCardType, +} from './ActionCard/ActionCard'; export {default as AutoCompleteSearch} from './AutoCompleteSearch/AutoCompleteSearch'; export {default as CheckboxScrollList} from './CheckboxScrollList/CheckboxScrollList'; export {default as ChipSelect} from './ChipSelect/ChipSelect'; diff --git a/packages/ui/src/components/templates/TreeView/Branch.tsx b/packages/ui/src/components/templates/TreeView/Branch.tsx index c507b7a995..d82c127cb5 100644 --- a/packages/ui/src/components/templates/TreeView/Branch.tsx +++ b/packages/ui/src/components/templates/TreeView/Branch.tsx @@ -20,6 +20,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, StyleSheet, View} from 'react-native'; import {useThemeColor} from '../../../theme'; import {Label} from '../../molecules'; +import {ActionCardType} from '../../organisms'; import BranchCard from './BranchCard'; interface SubBranchViewProps { @@ -30,11 +31,12 @@ interface SubBranchViewProps { setOpenBranches: (current: any) => void; branchCardInfoButtonIndication: string; renderBranch: (renderParams: any) => any; + getBranchActions?: (renderParams: any) => ActionCardType[]; renderLeaf: (renderParams: any) => any; fetchBranchData: (idParent: number) => Promise; branchCondition: (item: any) => boolean; onBranchFilterPress: (branch: any) => void; - translator?: (translationKey: string) => string; + translator: (translationKey: string) => string; } const SubBranchView = ({ @@ -45,6 +47,7 @@ const SubBranchView = ({ setOpenBranches, branchCardInfoButtonIndication, renderBranch, + getBranchActions, renderLeaf, fetchBranchData, branchCondition, @@ -101,6 +104,7 @@ const SubBranchView = ({ setOpenBranches={setOpenBranches} renderBranch={renderBranch} branchCardInfoButtonIndication={branchCardInfoButtonIndication} + getBranchActions={getBranchActions} renderLeaf={renderLeaf} fetchBranchData={fetchBranchData} branchCondition={branchCondition} @@ -123,11 +127,12 @@ interface BranchProps { setOpenBranches: (current: any) => void; branchCardInfoButtonIndication: string; renderBranch: (renderParams: any) => any; + getBranchActions?: (renderParams: any) => ActionCardType[]; renderLeaf: (renderParams: any) => any; fetchBranchData: (idParent: number) => Promise; branchCondition: (item: any) => boolean; onBranchFilterPress: (branch: any) => void; - translator?: (translationKey: string) => string; + translator: (translationKey: string) => string; } const Branch = ({ @@ -138,6 +143,7 @@ const Branch = ({ setOpenBranches, branchCardInfoButtonIndication, renderBranch, + getBranchActions, renderLeaf, fetchBranchData, branchCondition, @@ -189,6 +195,8 @@ const Branch = ({ parent={branch.item} onFilterPress={onBranchFilterPress} infoButtonIndication={branchCardInfoButtonIndication} + actionList={getBranchActions?.(branch)} + translator={translator} /> {isBranchOpen && ( void; @@ -29,6 +28,8 @@ interface BranchCardProps { parent: any; onFilterPress: (branch: any) => void; infoButtonIndication: string; + actionList?: ActionCardType[]; + translator: (translationKey: string) => string; } const BranchCard = ({ @@ -38,42 +39,32 @@ const BranchCard = ({ parent, onFilterPress, infoButtonIndication, + actionList = [], + translator, }: BranchCardProps) => { - const Colors = useThemeColor(); - return ( - - + onFilterPress(parent), + }} + translator={translator}> + {children} - onFilterPress(parent)} - indication={infoButtonIndication} - /> - + ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - flexDirection: 'row', - marginVertical: 2, - marginHorizontal: 10, - }, - cardContainer: { - flex: 1, - }, card: { + flex: 1, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', @@ -81,9 +72,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 15, paddingRight: 15, }, - infoButton: { - width: 40, - }, }); export default BranchCard; diff --git a/packages/ui/src/components/templates/TreeView/TreeView.tsx b/packages/ui/src/components/templates/TreeView/TreeView.tsx index fb5a2be920..94e881c309 100644 --- a/packages/ui/src/components/templates/TreeView/TreeView.tsx +++ b/packages/ui/src/components/templates/TreeView/TreeView.tsx @@ -17,7 +17,7 @@ */ import React, {useEffect, useState} from 'react'; -import {ActionType, ScrollList} from '../../organisms'; +import {ActionCardType, ActionType, ScrollList} from '../../organisms'; import Branch from './Branch'; interface TreeViewProps { @@ -31,6 +31,10 @@ interface TreeViewProps { * Function used to display what is inside a branch card. */ renderBranch: (renderParams: any) => any; + /** + * Function used to get additional actions of a branch. + */ + getBranchActions?: (renderParams: any) => ActionCardType[]; /** * Function used to display a leaf card. */ @@ -51,7 +55,7 @@ interface TreeViewProps { moreLoading: boolean; isListEnd: boolean; filter?: boolean; - translator?: (translationKey: string) => string; + translator: (translationKey: string) => string; disabledRefresh?: boolean; actionList?: ActionType[]; verticalActions?: boolean; @@ -65,6 +69,7 @@ const TreeView = ({ parentFieldName, branchCardInfoButtonIndication, renderBranch, + getBranchActions, renderLeaf, fetchData = () => [], fetchBranchData, @@ -94,6 +99,7 @@ const TreeView = ({ setOpenBranches={setOpenBranches} renderBranch={renderBranch} branchCardInfoButtonIndication={branchCardInfoButtonIndication} + getBranchActions={getBranchActions} renderLeaf={renderLeaf} fetchBranchData={fetchBranchData} branchCondition={branchCondition} diff --git a/packages/ui/stories/organisms/ActionCard.stories.tsx b/packages/ui/stories/organisms/ActionCard.stories.tsx index 5b6c1918df..3047b700e0 100644 --- a/packages/ui/stories/organisms/ActionCard.stories.tsx +++ b/packages/ui/stories/organisms/ActionCard.stories.tsx @@ -17,6 +17,7 @@ */ import React from 'react'; +import {View} from 'react-native'; import type {Meta} from '@storybook/react'; import {ActionCard as Component, Card, Text} from '../../src/components'; import { @@ -35,47 +36,90 @@ export default meta; export const ActionCard: Story = { args: { horizontal: false, - carLarge: false, - busColor: 'primaryColor', - truckHidden: false, - truckDisabled: false, + forceActionsDisplay: false, + quickAction_visible: true, + quickAction_iconName: 'airplane', + quickAction_iconColor: 'primaryColor', + quickAction_helper: 'Plane', + quickAction_isLarge: false, + quickAction_disabled: false, + action1_visible: true, + action1_iconName: 'car-front', + action1_iconColor: 'infoColor', + action1_helper: 'Car', + action1_isLarge: false, + action1_disabled: false, + action2_visible: true, + action2_iconName: 'bus-front', + action2_iconColor: 'secondaryColor_dark', + action2_helper: 'Bus', + action2_isLarge: false, + action2_disabled: false, + action3_visible: true, + action3_iconName: 'truck', + action3_iconColor: 'plannedColor', + action3_helper: 'Truck', + action3_isLarge: false, + action3_disabled: false, }, argTypes: { - busColor: colorPicker, + quickAction_iconColor: colorPicker, + action1_iconColor: colorPicker, + action2_iconColor: colorPicker, + action3_iconColor: colorPicker, actionList: disabledControl, - forceActionsDisplay: disabledControl, + quickAction: disabledControl, translator: disabledControl, }, render: args => ( - - TEST - - } - translator={key => key} - {...args} - actionList={[ - { - iconName: 'car-front', - helper: 'Car', - large: args.carLarge, + + + TEST + + } + translator={key => key} + {...args} + quickAction={{ + iconName: args.quickAction_iconName, + iconColor: args.quickAction_iconColor.background, + helper: args.quickAction_helper, + large: args.quickAction_isLarge, + hidden: !args.quickAction_visible, + disabled: args.quickAction_disabled, onPress: () => {}, - }, - { - iconName: 'bus-front', - iconColor: args.busColor?.background, - helper: 'Bus', - onPress: () => {}, - }, - { - iconName: 'truck', - helper: 'Truck', - onPress: () => {}, - hidden: args.truckHidden, - disabled: args.truckDisabled, - }, - ]} - /> + }} + actionList={[ + { + iconName: args.action1_iconName, + iconColor: args.action1_iconColor.background, + helper: args.action1_helper, + large: args.action1_isLarge, + hidden: !args.action1_visible, + disabled: args.action1_disabled, + onPress: () => {}, + }, + { + iconName: args.action2_iconName, + iconColor: args.action2_iconColor.background, + helper: args.action2_helper, + large: args.action2_isLarge, + hidden: !args.action2_visible, + disabled: args.action2_disabled, + onPress: () => {}, + }, + { + iconName: args.action3_iconName, + iconColor: args.action3_iconColor.background, + helper: args.action3_helper, + large: args.action3_isLarge, + hidden: !args.action3_visible, + disabled: args.action3_disabled, + onPress: () => {}, + }, + ]} + /> + ), };