feat(ListBoxHeader): add icon for cyclic dimension and new drilldown icon (#1504)

* feat: add cyclic icon and new drilldown icon

* fix: failed unit test

* fix: update snapshot

* test: add rendering test for cyclic

* refactor: data-title-utils.js

* test: add unit test

* fix: update snapshot

* fix: pr comments

* fix: failed test

* fix: typo

* fix: remove unnecessary things
This commit is contained in:
linhnguyen-qlik
2024-03-06 11:18:27 +01:00
committed by GitHub
parent 40ccb6aa8e
commit f35a5c44c7
12 changed files with 182 additions and 17 deletions

View File

@@ -115,6 +115,10 @@
"value": "Drill-down dimension", "value": "Drill-down dimension",
"comment": "Tooltip for the drill-down icons on the dimension titles in charts. (pde 160210)" "comment": "Tooltip for the drill-down icons on the dimension titles in charts. (pde 160210)"
}, },
"Listbox.Cyclic": {
"value": "Cyclic dimension",
"comment": "Tooltip for the cyclic icons on the dimension titles in charts. (pde 160210)"
},
"Menu.More": { "Menu.More": {
"value": "More", "value": "More",
"comment": "Menu option to display list of additional actions to choose from (ffd 20200520)" "comment": "Menu option to display list of additional actions to choose from (ffd 20200520)"

View File

@@ -207,7 +207,6 @@ function ListBoxInline({ options, layout }) {
const { wildCardSearch, searchEnabled, autoConfirm = false, layoutOptions = {} } = layout ?? {}; const { wildCardSearch, searchEnabled, autoConfirm = false, layoutOptions = {} } = layout ?? {};
const isLocked = layout?.qListObject?.qDimensionInfo?.qLocked; const isLocked = layout?.qListObject?.qDimensionInfo?.qLocked;
const showSearchIcon = searchEnabled !== false && search === 'toggle' && !isLocked; const showSearchIcon = searchEnabled !== false && search === 'toggle' && !isLocked;
const isDrillDown = layout?.qListObject?.qDimensionInfo?.qGrouping === 'H';
const canShowTitle = layout?.title?.length && layout?.showTitle !== false; const canShowTitle = layout?.title?.length && layout?.showTitle !== false;
const showDetachedToolbarOnly = toolbar && !canShowTitle && !isPopover; const showDetachedToolbarOnly = toolbar && !canShowTitle && !isPopover;
@@ -257,8 +256,8 @@ function ListBoxInline({ options, layout }) {
const listBoxHeader = ( const listBoxHeader = (
<ListBoxHeader <ListBoxHeader
app={app}
showSearchIcon={showSearchIcon} showSearchIcon={showSearchIcon}
isDrillDown={isDrillDown}
onShowSearch={handleShowSearch} onShowSearch={handleShowSearch}
isPopover={isPopover} isPopover={isPopover}
showToolbar={showToolbar} showToolbar={showToolbar}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Button, styled } from '@mui/material';
import { ICON_PADDING } from '../../constants';
const StyledButton = styled(Button)(() => ({
borderRadius: 4,
width: 24,
height: 24,
minWidth: 22,
padding: 0,
}));
function DimensionIcon({ iconData, translator, iconStyle }) {
if (!iconData) return undefined;
const { icon, tooltip, onClick } = iconData;
const Icon = icon;
const title = translator.get(tooltip);
const isButton = onClick;
return isButton ? (
<StyledButton variant="outlined" onClick={onClick} tabIndex={-1} title={title} size="large" disableRipple>
<Icon style={iconStyle} />
</StyledButton>
) : (
<Icon title={title} size="large" style={{ ...iconStyle, padding: `${ICON_PADDING}px` }} />
);
}
export default DimensionIcon;

View File

@@ -4,7 +4,6 @@ import { Grid, IconButton } from '@mui/material';
import Lock from '@nebula.js/ui/icons/lock'; import Lock from '@nebula.js/ui/icons/lock';
import { unlock } from '@nebula.js/ui/icons/unlock'; import { unlock } from '@nebula.js/ui/icons/unlock';
import SearchIcon from '@nebula.js/ui/icons/search'; import SearchIcon from '@nebula.js/ui/icons/search';
import DrillDownIcon from '@nebula.js/ui/icons/drill-down';
import ActionsToolbar from '../../../ActionsToolbar'; import ActionsToolbar from '../../../ActionsToolbar';
import showToolbarDetached from '../../interactions/listbox-show-toolbar-detached'; import showToolbarDetached from '../../interactions/listbox-show-toolbar-detached';
import getListboxActionProps from '../../interactions/listbox-action-props'; import getListboxActionProps from '../../interactions/listbox-action-props';
@@ -12,6 +11,8 @@ import createListboxSelectionToolbar from '../../interactions/listbox-selection-
import { BUTTON_ICON_WIDTH, CELL_PADDING_LEFT, HEADER_PADDING_RIGHT, ICON_PADDING } from '../../constants'; import { BUTTON_ICON_WIDTH, CELL_PADDING_LEFT, HEADER_PADDING_RIGHT, ICON_PADDING } from '../../constants';
import hasSelections from '../../assets/has-selections'; import hasSelections from '../../assets/has-selections';
import { HeaderTitle, StyledGridHeader, UnlockCoverButton, iconStyle } from './ListBoxHeaderComponents'; import { HeaderTitle, StyledGridHeader, UnlockCoverButton, iconStyle } from './ListBoxHeaderComponents';
import iconUtils from './icon-utils';
import DimensionIcon from './DimensionIcon';
// ms that needs to pass before the lock button can be toggled again // ms that needs to pass before the lock button can be toggled again
const lockTimeFrameMs = 500; const lockTimeFrameMs = 500;
@@ -51,7 +52,6 @@ export default function ListBoxHeader({
isRtl, isRtl,
showLock, showLock,
showSearchIcon, showSearchIcon,
isDrillDown,
constraints, constraints,
onShowSearch, onShowSearch,
classes, classes,
@@ -66,6 +66,7 @@ export default function ListBoxHeader({
selections, selections,
keyboard, keyboard,
autoConfirm, autoConfirm,
app,
}) { }) {
const [isToolbarDetached, setIsToolbarDetached] = useState(showDetachedToolbarOnly); const [isToolbarDetached, setIsToolbarDetached] = useState(showDetachedToolbarOnly);
const [isLocked, setLocked] = useState(layout?.qListObject?.qDimensionInfo?.qLocked); const [isLocked, setLocked] = useState(layout?.qListObject?.qDimensionInfo?.qLocked);
@@ -76,10 +77,10 @@ export default function ListBoxHeader({
}, [layout?.qListObject?.qDimensionInfo?.qLocked]); }, [layout?.qListObject?.qDimensionInfo?.qLocked]);
const titleRef = useRef(null); const titleRef = useRef(null);
const iconData = iconUtils.createDimensionIconData(layout?.qListObject?.qDimensionInfo, app);
const showUnlock = showLock && isLocked; const showUnlock = showLock && isLocked;
const showLockIcon = !showLock && isLocked; // shows instead of the cover button when field/dim is locked. const showLockIcon = !showLock && isLocked; // shows instead of the cover button when field/dim is locked.
const showLeftIcon = showSearchIcon || showLockIcon || isDrillDown; // the left-most icon outside of the actions/selections toolbar. const showLeftIcon = showSearchIcon || showLockIcon || iconData; // the left-most icon outside of the actions/selections toolbar.
const paddingLeft = CELL_PADDING_LEFT - (showLeftIcon ? ICON_PADDING : 0); const paddingLeft = CELL_PADDING_LEFT - (showLeftIcon ? ICON_PADDING : 0);
const paddingRight = isRtl ? CELL_PADDING_LEFT - (showLeftIcon ? ICON_PADDING : 0) : HEADER_PADDING_RIGHT; const paddingRight = isRtl ? CELL_PADDING_LEFT - (showLeftIcon ? ICON_PADDING : 0) : HEADER_PADDING_RIGHT;
@@ -88,7 +89,7 @@ export default function ListBoxHeader({
const iconsWidth = const iconsWidth =
(showSearchIcon ? BUTTON_ICON_WIDTH : 0) + (showSearchIcon ? BUTTON_ICON_WIDTH : 0) +
(showLockIcon ? BUTTON_ICON_WIDTH : 0) + (showLockIcon ? BUTTON_ICON_WIDTH : 0) +
(isDrillDown ? BUTTON_ICON_WIDTH : 0); (iconData ? BUTTON_ICON_WIDTH : 0);
const toggleLock = getToggleLock({ isLocked, setLocked, settingLockedState, setSettingLockedState, model }); const toggleLock = getToggleLock({ isLocked, setLocked, settingLockedState, setSettingLockedState, model });
@@ -203,13 +204,7 @@ export default function ListBoxHeader({
{showLeftIcon && ( {showLeftIcon && (
<Grid item container alignItems="center" width={iconsWidth} className="header-action-container"> <Grid item container alignItems="center" width={iconsWidth} className="header-action-container">
{lockedIconComp || (showSearchIcon && searchIconComp)} {lockedIconComp || (showSearchIcon && searchIconComp)}
{isDrillDown && ( <DimensionIcon iconData={iconData} iconStyle={iconStyle} translator={translator} />
<DrillDownIcon
tabIndex={-1}
title={translator.get('Listbox.DrillDown')}
style={{ ...iconStyle, padding: `${ICON_PADDING}px` }}
/>
)}
</Grid> </Grid>
)} )}
<Grid <Grid

View File

@@ -0,0 +1,36 @@
import DrillDownIcon from '@nebula.js/ui/icons/drill-down';
import CyclicIcon from '@nebula.js/ui/icons/reload';
const dimensionTypes = {
single: 'N',
drillDown: 'H',
cyclic: 'C',
};
const createDimensionIconData = (dimInfo, app) => {
switch (dimInfo.qGrouping) {
case dimensionTypes.drillDown:
return {
icon: DrillDownIcon,
tooltip: 'Listbox.DrillDown',
onClick: undefined,
};
case dimensionTypes.cyclic:
return {
icon: CyclicIcon,
tooltip: 'Listbox.Cyclic',
onClick: () => {
app
.getDimension(dimInfo.qLibraryId)
.then((dimensionModel) => {
dimensionModel.stepCycle(1);
})
.catch(() => null);
},
};
default:
return undefined;
}
};
export default { createDimensionIconData };

View File

@@ -0,0 +1,32 @@
import DrillDownIcon from '@nebula.js/ui/icons/drill-down';
import CyclicIcon from '@nebula.js/ui/icons/reload';
import utils from '../ListBoxHeader/icon-utils';
describe('icon-utils', () => {
it('should return no icon data for single dimension', () => {
const dimInfo = { qGrouping: 'N' };
const result = utils.createDimensionIconData(dimInfo, undefined);
expect(result).toEqual(undefined);
});
it('should return icon data for drilldown dimension', () => {
const dimInfo = { qGrouping: 'H' };
const result = utils.createDimensionIconData(dimInfo, undefined);
expect(result).toMatchObject({
icon: DrillDownIcon,
tooltip: 'Listbox.DrillDown',
onClick: undefined,
});
});
it('should return no icon data for cyclic dimension', () => {
const dimInfo = { qGrouping: 'C' };
const app = {};
const result = utils.createDimensionIconData(dimInfo, app);
expect(result).toMatchObject({
icon: CyclicIcon,
tooltip: 'Listbox.Cyclic',
onClick: expect.any(Function),
});
});
});

View File

@@ -38,7 +38,7 @@ const model = {
function getDefaultProps() { function getDefaultProps() {
const containerRef = React.createRef(); const containerRef = React.createRef();
const defaultProps = { const defaultProps = {
layout: { title: 'The title', qListObject: { qDimensionInfo: { qLocked: false } } }, layout: { title: 'The title', qListObject: { qDimensionInfo: { qLocked: false, qGrouping: 'N' } } },
translator, translator,
styles, styles,
isRtl: false, isRtl: false,
@@ -216,7 +216,7 @@ describe('<ListBoxHeader />', () => {
}); });
test('There should be an unlock cover button on top of the actions toolbar', async () => { test('There should be an unlock cover button on top of the actions toolbar', async () => {
const layout = { title: 'The title', qListObject: { qDimensionInfo: { qLocked: true } } }; const layout = { title: 'The title', qListObject: { qDimensionInfo: { qLocked: true, qGrouping: 'N' } } };
hasSelections.mockReturnValue(false); hasSelections.mockReturnValue(false);
const testRenderer = await render({ const testRenderer = await render({
layout, layout,

View File

@@ -6,7 +6,7 @@ const drillDown = (props) => ({
{ {
type: 'path', type: 'path',
attrs: { attrs: {
d: 'M15.9993744,3 L15.9993744,5 L4.99937445,5 L4.99937445,3 L15.9993744,3 Z M8.99937445,7 L15.9993744,7 L15.9993744,9 L8.99937445,9 L8.99937445,7 Z M11.9993744,11 L15.9993744,11 L15.9993744,13 L11.9993744,13 L11.9993744,11 Z M2.2,11 L7,11 L7,9.00369263 C7,8.89323568 7.08954305,8.80369263 7.2,8.80369263 C7.2549016,8.80369263 7.30738916,8.82626165 7.34515712,8.86610844 L10.1852182,11.8624926 C10.2583124,11.93961 10.258346,12.0604218 10.1852948,12.13758 L7.34523376,15.1373102 C7.2692928,15.2175206 7.14270711,15.2209817 7.06249671,15.1450407 C7.02260076,15.1072683 7,15.0547472 7,14.9998069 L7,12.9043747 C4.79351111,12.9043747 2.8018683,12.9266213 1.02507156,12.9711145 L1.02507252,12.9711526 C0.472939773,12.9849787 0.014139487,12.5485949 0.000313396522,11.9964622 C0.000104473692,11.988119 -1.32268838e-12,11.9797736 -1.3231638e-12,11.9714278 L-1.83320026e-12,3 L2,3 L2,10.8 C2,10.9104569 2.08954305,11 2.2,11 Z', d: 'M7 4h9v1H7zm2 4h7v1H9zm2 4h5v1h-5zM1 4v3.526a3.5 3.5 0 0 0 3.5 3.5h2.31L5.22 9.402l.715-.7L8.7 11.527 5.935 14.35l-.714-.7 1.59-1.624H4.5a4.5 4.5 0 0 1-4.5-4.5V4z',
}, },
}, },
], ],

View File

@@ -0,0 +1,15 @@
import SvgIcon from './SvgIcon';
const reload = (props) => ({
...props,
shapes: [
{
type: 'path',
attrs: {
d: 'M1 8a7 7 0 1 1 10 6.326V11h-1v5h5v-1h-3.124A8 8 0 1 0 8 16v-1a7 7 0 0 1-7-7Z',
},
},
],
});
export default (props) => SvgIcon(reload(props));
export { reload };

View File

@@ -0,0 +1,55 @@
const fixture = {
getLayout: () => ({
title: 'Field title with truncated text that is very long',
qInfo: {
qId: 'qId',
},
visualization: 'listbox',
qListObject: {
qDimensionInfo: {
qGrouping: 'C',
},
qSize: {
qcy: 2,
},
qInitialDataFetch: [{ qLeft: 0, qWidth: 0, qTop: 0, qHeight: 0 }],
},
qSelectionInfo: {
qInSelections: false,
},
layoutOptions: {
layoutOrder: 'row',
},
}),
getListObjectData: () => [
{
qMatrix: [
[
{
qText: 'A',
qNum: 'NaN',
qElemNumber: 0,
qState: 'O',
},
],
[
{
qText: 'B',
qNum: 'NaN',
qElemNumber: 1,
qState: 'O',
},
],
],
qTails: [],
qArea: {
qLeft: 0,
qTop: 0,
qWidth: 1,
qHeight: 2,
},
},
],
};
export default fixture;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB