mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
fix: add filters info in footnote (#1710)
This commit is contained in:
@@ -47,6 +47,22 @@
|
|||||||
"value": "Excluded lock",
|
"value": "Excluded lock",
|
||||||
"comment": "Excluded lock state of an item in listbox(buy20180905)"
|
"comment": "Excluded lock state of an item in listbox(buy20180905)"
|
||||||
},
|
},
|
||||||
|
"Object.FilterLabel.All": {
|
||||||
|
"value": "ALL",
|
||||||
|
"comment": "Label for when a chart filter has been set to exclude global selections in a field, meaning all field values will be included. (tew 220411)"
|
||||||
|
},
|
||||||
|
"Object.FilterLabel.Exclude": {
|
||||||
|
"value": "NOT",
|
||||||
|
"comment": "Label for when a chart filter has been set to exclude the filter values. (tew 220411)"
|
||||||
|
},
|
||||||
|
"Object.FilterLabel.Unknown": {
|
||||||
|
"value": "Unknown",
|
||||||
|
"comment": "Label for when a chart filter could not be labeled. (tew 220411)"
|
||||||
|
},
|
||||||
|
"Object.FiltersApplied": {
|
||||||
|
"value": "Filters applied:",
|
||||||
|
"comment": "Title for applied filters on a chart. (tew 220411)"
|
||||||
|
},
|
||||||
"Listbox.Search": {
|
"Listbox.Search": {
|
||||||
"value": "Search in listbox",
|
"value": "Search in listbox",
|
||||||
"comment": "Action text to search in listbox"
|
"comment": "Action text to search in listbox"
|
||||||
|
|||||||
@@ -584,6 +584,7 @@ const Cell = forwardRef(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCardTheme = !!halo.public.theme?.getStyle('', '', '_cards');
|
||||||
const flags = halo.public.galaxy?.flags;
|
const flags = halo.public.galaxy?.flags;
|
||||||
let useOldCellPadding;
|
let useOldCellPadding;
|
||||||
let bodyPadding;
|
let bodyPadding;
|
||||||
@@ -594,13 +595,13 @@ const Cell = forwardRef(
|
|||||||
useOldCellPadding = true;
|
useOldCellPadding = true;
|
||||||
bodyPadding = undefined;
|
bodyPadding = undefined;
|
||||||
} else {
|
} else {
|
||||||
const senseTheme = halo.public.theme;
|
|
||||||
useOldCellPadding = false;
|
useOldCellPadding = false;
|
||||||
bodyPadding = getPadding({
|
bodyPadding = getPadding({
|
||||||
layout,
|
layout,
|
||||||
isError: state.error,
|
isError: state.error,
|
||||||
senseTheme,
|
isCardTheme,
|
||||||
titleStyles,
|
titleStyles,
|
||||||
|
translator,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,7 +668,15 @@ const Cell = forwardRef(
|
|||||||
>
|
>
|
||||||
{Content}
|
{Content}
|
||||||
</Grid>
|
</Grid>
|
||||||
{cellNode && layout && state.sn && <Footer layout={layout} titleStyles={titleStyles} />}
|
{cellNode && layout && state.sn && (
|
||||||
|
<Footer
|
||||||
|
layout={layout}
|
||||||
|
titleStyles={titleStyles}
|
||||||
|
translator={translator}
|
||||||
|
flags={flags}
|
||||||
|
isCardTheme={isCardTheme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
{state.longRunningQuery && <LongRunningQuery canCancel={canCancel} canRetry={canRetry} api={longrunning} />}
|
{state.longRunningQuery && <LongRunningQuery canCancel={canCancel} canRetry={canRetry} api={longrunning} />}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
54
apis/nucleus/src/components/FiltersFooter.jsx
Normal file
54
apis/nucleus/src/components/FiltersFooter.jsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Typography, Grid, Tooltip } from '@mui/material';
|
||||||
|
import FilterIcon from '@nebula.js/ui/icons/filter';
|
||||||
|
import { generateFiltersLabels } from '../utils/generateFiltersInfo';
|
||||||
|
|
||||||
|
function ItalicText({ styles, children }) {
|
||||||
|
return (
|
||||||
|
<Typography noWrap variant="body2" style={{ ...styles, fontStyle: 'italic' }}>
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FiltersFooter({ layout, translator, filtersFootnoteString, footerStyle }) {
|
||||||
|
const filtersFootnoteLabels = generateFiltersLabels(layout?.filters ?? [], translator);
|
||||||
|
const styles = {
|
||||||
|
color: footerStyle?.color,
|
||||||
|
fontFamily: footerStyle?.fontFamily,
|
||||||
|
fontSize: footerStyle?.fontSize,
|
||||||
|
fontWeight: footerStyle?.fontWeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={filtersFootnoteString}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
wrap="nowrap"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: footerStyle?.backgroundColor,
|
||||||
|
padding: footerStyle?.padding,
|
||||||
|
borderTop: footerStyle?.borderTop,
|
||||||
|
}}
|
||||||
|
data-testid="filters-footnote"
|
||||||
|
>
|
||||||
|
<Grid item display="flex">
|
||||||
|
<FilterIcon style={{ fontSize: '12px', color: footerStyle.color, margin: 'auto' }} />
|
||||||
|
<ItalicText styles={{ ...styles, marginLeft: '2px' }}>
|
||||||
|
{translator.get('Object.FiltersApplied')}
|
||||||
|
</ItalicText>
|
||||||
|
</Grid>
|
||||||
|
<Grid item display="flex">
|
||||||
|
{filtersFootnoteLabels.map((filter) => (
|
||||||
|
<Grid container wrap="nowrap" key={`${filter.field}-${filter.label}`}>
|
||||||
|
<ItalicText styles={{ ...styles, fontWeight: 'bold' }}>{`${filter.field}:`}</ItalicText>
|
||||||
|
<ItalicText styles={styles}> {filter.label} </ItalicText>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FiltersFooter;
|
||||||
@@ -3,6 +3,8 @@ import React from 'react';
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
import { Typography, Grid, Tooltip } from '@mui/material';
|
import { Typography, Grid, Tooltip } from '@mui/material';
|
||||||
|
import { generateFiltersString } from '../utils/generateFiltersInfo';
|
||||||
|
import FiltersFooter from './FiltersFooter';
|
||||||
|
|
||||||
const PREFIX = 'Footer';
|
const PREFIX = 'Footer';
|
||||||
|
|
||||||
@@ -10,10 +12,9 @@ const classes = {
|
|||||||
itemStyle: `${PREFIX}-itemStyle`,
|
itemStyle: `${PREFIX}-itemStyle`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledGrid = styled(Grid)(({ theme }) => ({
|
const StyledGrid = styled(Grid)(() => ({
|
||||||
[`& .${classes.itemStyle}`]: {
|
[`& .${classes.itemStyle}`]: {
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -28,15 +29,37 @@ const CellFooter = {
|
|||||||
className: 'njs-cell-footer',
|
className: 'njs-cell-footer',
|
||||||
};
|
};
|
||||||
|
|
||||||
function Footer({ layout, titleStyles = {} }) {
|
function Footer({ layout, titleStyles = {}, translator, flags, isCardTheme }) {
|
||||||
return layout && layout.showTitles && layout.footnote ? (
|
const footerStyle = titleStyles.footer;
|
||||||
|
const hasFilters = layout?.filters?.length > 0 && layout?.qHyperCube?.qMeasureInfo?.length > 0;
|
||||||
|
const filtersFootnoteString = generateFiltersString(layout?.filters ?? [], translator);
|
||||||
|
const showFilters = !layout?.footnote && hasFilters && filtersFootnoteString;
|
||||||
|
const themePaddingEnabled = flags?.isEnabled('VNA-13_CELLPADDING_FROM_THEME');
|
||||||
|
const paddingTop = isCardTheme ? '1px' : '6px';
|
||||||
|
|
||||||
|
return layout && layout.showTitles && (layout.footnote || showFilters) ? (
|
||||||
<StyledGrid container>
|
<StyledGrid container>
|
||||||
<Grid item className={classes.itemStyle}>
|
<Grid
|
||||||
<Tooltip title={layout.footnote}>
|
item
|
||||||
<Typography noWrap variant="body2" className={CellFooter.className} style={titleStyles.footer}>
|
className={classes.itemStyle}
|
||||||
{layout.footnote}
|
data-testid={CellFooter.className}
|
||||||
</Typography>
|
sx={{ paddingTop: (theme) => (themePaddingEnabled ? paddingTop : theme.spacing(1)) }}
|
||||||
</Tooltip>
|
>
|
||||||
|
{layout.footnote && (
|
||||||
|
<Tooltip title={layout.footnote}>
|
||||||
|
<Typography noWrap variant="body2" className={CellFooter.className} style={footerStyle}>
|
||||||
|
{layout.footnote}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{showFilters && (
|
||||||
|
<FiltersFooter
|
||||||
|
layout={layout}
|
||||||
|
translator={translator}
|
||||||
|
filtersFootnoteString={filtersFootnoteString}
|
||||||
|
footerStyle={footerStyle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</StyledGrid>
|
</StyledGrid>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { create, act } from 'react-test-renderer';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import * as generateFiltersInfo from '../../utils/generateFiltersInfo';
|
||||||
|
import FiltersFooter from '../FiltersFooter';
|
||||||
|
|
||||||
|
describe('<FiltersFooter />', () => {
|
||||||
|
let renderer;
|
||||||
|
let render;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(generateFiltersInfo, 'generateFiltersLabels').mockReturnValue([
|
||||||
|
{
|
||||||
|
field: 'Alpha',
|
||||||
|
label: 'B, C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'Dim1',
|
||||||
|
label: 'B',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
render = async ({ layout, footerStyle, translator }) => {
|
||||||
|
await act(async () => {
|
||||||
|
renderer = create(<FiltersFooter layout={layout} footerStyle={footerStyle} translator={translator} />);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
renderer.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render filters footnote', async () => {
|
||||||
|
const translator = { get: (s) => s };
|
||||||
|
const layout = {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
type: 'values',
|
||||||
|
field: 'Alpha',
|
||||||
|
options: {
|
||||||
|
values: ['B', 'C'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'values',
|
||||||
|
field: 'Dim1',
|
||||||
|
options: {
|
||||||
|
values: ['B'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const footerStyle = { backgroundColor: 'red', color: 'blue' };
|
||||||
|
await render({ layout, footerStyle, translator });
|
||||||
|
const types = renderer.root.findAllByType(Typography);
|
||||||
|
expect(types).toHaveLength(5);
|
||||||
|
expect(types[0].props.children.map((child) => (typeof child === 'string' ? child.trim() : child))).toEqual([
|
||||||
|
'Object.FiltersApplied',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
expect(types[1].props.children).toBe('Alpha:');
|
||||||
|
expect(types[2].props.children.map((child) => (typeof child === 'string' ? child.trim() : child))).toEqual([
|
||||||
|
'',
|
||||||
|
'B, C',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
expect(types[3].props.children).toBe('Dim1:');
|
||||||
|
expect(types[4].props.children.map((child) => (typeof child === 'string' ? child.trim() : child))).toEqual([
|
||||||
|
'',
|
||||||
|
'B',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,11 +2,16 @@ import React from 'react';
|
|||||||
import { create, act } from 'react-test-renderer';
|
import { create, act } from 'react-test-renderer';
|
||||||
import { Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
import Footer from '../Footer';
|
import Footer from '../Footer';
|
||||||
|
import * as generateFiltersInfo from '../../utils/generateFiltersInfo';
|
||||||
|
import * as FiltersFooter from '../FiltersFooter';
|
||||||
|
|
||||||
describe('<Footer />', () => {
|
describe('<Footer />', () => {
|
||||||
let renderer;
|
let renderer;
|
||||||
let render;
|
let render;
|
||||||
|
let FiltersFootnote;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.spyOn(generateFiltersInfo, 'generateFiltersString').mockReturnValue('filters string');
|
||||||
|
FiltersFootnote = jest.spyOn(FiltersFooter, 'default').mockReturnValue('FiltersFooter');
|
||||||
render = async (layout) => {
|
render = async (layout) => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
renderer = create(<Footer layout={layout} />);
|
renderer = create(<Footer layout={layout} />);
|
||||||
@@ -20,10 +25,19 @@ describe('<Footer />', () => {
|
|||||||
await render();
|
await render();
|
||||||
expect(renderer.root.props.layout).toBe(undefined);
|
expect(renderer.root.props.layout).toBe(undefined);
|
||||||
});
|
});
|
||||||
it('should render', async () => {
|
it('should render footnote when has footnote', async () => {
|
||||||
await render({ showTitles: true, footnote: 'foo' });
|
await render({ showTitles: true, footnote: 'foo' });
|
||||||
const types = renderer.root.findAllByType(Typography);
|
const types = renderer.root.findAllByType(Typography);
|
||||||
expect(types).toHaveLength(1);
|
expect(types).toHaveLength(1);
|
||||||
expect(types[0].props.children).toBe('foo');
|
expect(types[0].props.children).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render filters footnote when has filters and no footnote', async () => {
|
||||||
|
jest.mock('../FiltersFooter', () => ({
|
||||||
|
FiltersFooter: jest.fn(() => <div>Filters footNote</div>),
|
||||||
|
}));
|
||||||
|
await render({ showTitles: true, footnote: '', filters: ['A'], qHyperCube: { qMeasureInfo: [{ cId: 'chart' }] } });
|
||||||
|
const types = renderer.root.findAllByType(FiltersFootnote);
|
||||||
|
expect(types).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
const escapeField = (field) => {
|
import escapeField from '../../../utils/escape-field';
|
||||||
if (!field) {
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
if (/^[A-Za-z][A-Za-z0-9_]*$/.test(field)) {
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
return `[${field.replace(/\]/g, ']]')}]`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const needToFetchFrequencyMax = (layout) => layout?.frequencyMax === 'fetch';
|
export const needToFetchFrequencyMax = (layout) => layout?.frequencyMax === 'fetch';
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,53 @@
|
|||||||
import getPadding from '../cell-padding';
|
import getPadding from '../cell-padding';
|
||||||
|
import * as generateFiltersInfo from '../generateFiltersInfo';
|
||||||
|
|
||||||
describe('cell padding', () => {
|
describe('cell padding', () => {
|
||||||
let testFn;
|
let testFn;
|
||||||
let titleStyles;
|
let titleStyles;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.spyOn(generateFiltersInfo, 'generateFiltersString').mockReturnValue('filters string');
|
||||||
titleStyles = {
|
titleStyles = {
|
||||||
main: {},
|
main: {},
|
||||||
subTitle: {},
|
subTitle: {},
|
||||||
footer: {},
|
footer: {},
|
||||||
};
|
};
|
||||||
testFn = ({
|
testFn = ({
|
||||||
cardTheme = false,
|
isCardTheme = true,
|
||||||
isError = false,
|
isError = false,
|
||||||
showTitles = false,
|
showTitles = false,
|
||||||
title = false,
|
title = false,
|
||||||
subtitle = false,
|
subtitle = false,
|
||||||
footer = false,
|
footer = false,
|
||||||
visualization = 'barchart',
|
visualization = 'barchart',
|
||||||
|
footerFilter = false,
|
||||||
}) => {
|
}) => {
|
||||||
const senseTheme = {
|
|
||||||
getStyle: () => (cardTheme ? true : undefined),
|
|
||||||
};
|
|
||||||
const layout = {
|
const layout = {
|
||||||
showTitles,
|
showTitles,
|
||||||
title: title ? 'yes' : '',
|
title: title ? 'yes' : '',
|
||||||
subtitle: subtitle ? 'yes' : '',
|
subtitle: subtitle ? 'yes' : '',
|
||||||
footnote: footer ? 'yes' : '',
|
footnote: footer ? 'yes' : '',
|
||||||
|
filters: footerFilter ? [{}] : [],
|
||||||
|
qHyperCube: { qMeasureInfo: [{}] },
|
||||||
visualization,
|
visualization,
|
||||||
};
|
};
|
||||||
return getPadding({ layout, isError, senseTheme, titleStyles });
|
return getPadding({ layout, isError, isCardTheme, titleStyles });
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('not card theme return undefined', () => {
|
test('not card theme return undefined', () => {
|
||||||
const bodyPadding = testFn({ cardTheme: false });
|
const bodyPadding = testFn({ isCardTheme: false });
|
||||||
expect(bodyPadding).toBeUndefined();
|
expect(bodyPadding).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card theme return bodyPadding', () => {
|
test('card theme return bodyPadding', () => {
|
||||||
const bodyPadding = testFn({ cardTheme: true });
|
const bodyPadding = testFn({ isCardTheme: true });
|
||||||
expect(bodyPadding).toBe('10px 10px 5px');
|
expect(bodyPadding).toBe('10px 10px 5px');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card theme with title, subtitel & footer should update titleStyles', () => {
|
test('card theme with title, subtitel & footer should update titleStyles', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
title: true,
|
title: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
@@ -58,9 +60,9 @@ describe('cell padding', () => {
|
|||||||
expect(titleStyles.footer.borderTop).toBe('1px solid #d9d9d9');
|
expect(titleStyles.footer.borderTop).toBe('1px solid #d9d9d9');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card theme with only subtitel', () => {
|
test('card theme with only subtitle', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
});
|
});
|
||||||
@@ -73,7 +75,7 @@ describe('cell padding', () => {
|
|||||||
|
|
||||||
test('card theme with only footer', () => {
|
test('card theme with only footer', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
footer: true,
|
footer: true,
|
||||||
});
|
});
|
||||||
@@ -84,9 +86,23 @@ describe('cell padding', () => {
|
|||||||
expect(titleStyles.footer.borderTop).toBe('1px solid #d9d9d9');
|
expect(titleStyles.footer.borderTop).toBe('1px solid #d9d9d9');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('card theme with only filters info in footer', () => {
|
||||||
|
const bodyPadding = testFn({
|
||||||
|
isCardTheme: true,
|
||||||
|
showTitles: true,
|
||||||
|
footer: false,
|
||||||
|
footerFilter: true,
|
||||||
|
});
|
||||||
|
expect(bodyPadding).toBe('10px 10px 0');
|
||||||
|
expect(titleStyles.main.padding).toBeUndefined();
|
||||||
|
expect(titleStyles.subTitle.padding).toBeUndefined();
|
||||||
|
expect(titleStyles.footer.padding).toBe('6px 10px');
|
||||||
|
expect(titleStyles.footer.borderTop).toBe('1px solid #d9d9d9');
|
||||||
|
});
|
||||||
|
|
||||||
test('card theme with sn-filter-pane visualization type the do not include padding', () => {
|
test('card theme with sn-filter-pane visualization type the do not include padding', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
visualization: 'sn-filter-pane',
|
visualization: 'sn-filter-pane',
|
||||||
});
|
});
|
||||||
expect(bodyPadding).toBeUndefined();
|
expect(bodyPadding).toBeUndefined();
|
||||||
@@ -94,7 +110,7 @@ describe('cell padding', () => {
|
|||||||
|
|
||||||
test('card theme with action-button visualization do not include padding', () => {
|
test('card theme with action-button visualization do not include padding', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
visualization: 'action-button',
|
visualization: 'action-button',
|
||||||
});
|
});
|
||||||
expect(bodyPadding).toBeUndefined();
|
expect(bodyPadding).toBeUndefined();
|
||||||
@@ -102,7 +118,7 @@ describe('cell padding', () => {
|
|||||||
|
|
||||||
test('card theme with action-button visualization do include padding if showTitels is true', () => {
|
test('card theme with action-button visualization do include padding if showTitels is true', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
visualization: 'action-button',
|
visualization: 'action-button',
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
});
|
});
|
||||||
@@ -111,7 +127,7 @@ describe('cell padding', () => {
|
|||||||
|
|
||||||
test('card theme with sn-filter-pane visualization type the do include footer styling', () => {
|
test('card theme with sn-filter-pane visualization type the do include footer styling', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: true,
|
isCardTheme: true,
|
||||||
visualization: 'sn-filter-pane',
|
visualization: 'sn-filter-pane',
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
footer: true,
|
footer: true,
|
||||||
@@ -123,7 +139,7 @@ describe('cell padding', () => {
|
|||||||
|
|
||||||
test('not card theme with sn-filter-pane visualization', () => {
|
test('not card theme with sn-filter-pane visualization', () => {
|
||||||
const bodyPadding = testFn({
|
const bodyPadding = testFn({
|
||||||
cardTheme: false,
|
isCardTheme: false,
|
||||||
visualization: 'sn-filter-pane',
|
visualization: 'sn-filter-pane',
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
footer: true,
|
footer: true,
|
||||||
|
|||||||
25
apis/nucleus/src/utils/__tests__/escape-field.test.js
Normal file
25
apis/nucleus/src/utils/__tests__/escape-field.test.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import escapeField from '../escape-field';
|
||||||
|
|
||||||
|
describe('escaping of fields', () => {
|
||||||
|
test('Should escape non-allowed characters', () => {
|
||||||
|
expect(escapeField('A B')).toEqual('[A B]');
|
||||||
|
expect(escapeField('A[B')).toEqual('[A[B]');
|
||||||
|
expect(escapeField('A]B')).toEqual('[A]]B]');
|
||||||
|
expect(escapeField('123')).toEqual('[123]');
|
||||||
|
expect(escapeField('a-1')).toEqual('[a-1]');
|
||||||
|
expect(escapeField('_a')).toEqual('[_a]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should leave allowed characters unescaped', () => {
|
||||||
|
expect(escapeField('A')).toEqual('A');
|
||||||
|
expect(escapeField('ABC')).toEqual('ABC');
|
||||||
|
expect(escapeField('ABC123')).toEqual('ABC123');
|
||||||
|
expect(escapeField('abc123')).toEqual('abc123');
|
||||||
|
expect(escapeField('a1B2c3')).toEqual('a1B2c3');
|
||||||
|
expect(escapeField('a_b_c')).toEqual('a_b_c');
|
||||||
|
expect(escapeField(']')).toEqual(']');
|
||||||
|
expect(escapeField('')).toEqual('');
|
||||||
|
expect(escapeField(null)).toEqual(null);
|
||||||
|
expect(escapeField(undefined)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
279
apis/nucleus/src/utils/__tests__/generateFiltersInfo.test.js
Normal file
279
apis/nucleus/src/utils/__tests__/generateFiltersInfo.test.js
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { generateFiltersString, FilterType, SearchMode, ConditionMode, ModifierType } from '../generateFiltersInfo';
|
||||||
|
|
||||||
|
describe('generateFiltersString', () => {
|
||||||
|
const translator = { get: (s) => s };
|
||||||
|
describe('Value filter', () => {
|
||||||
|
let filter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
filter = {
|
||||||
|
type: FilterType.VALUES,
|
||||||
|
field: 'fieldName',
|
||||||
|
exclude: true,
|
||||||
|
id: 'filter1',
|
||||||
|
options: {
|
||||||
|
values: ['value1', 'value2'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when exclude mode is true', async () => {
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: Object.FilterLabel.Exclude value1, value2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when exclude mode is false', async () => {
|
||||||
|
filter.exclude = false;
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: value1, value2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when no field values are selected', async () => {
|
||||||
|
filter.options.values = [];
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Search filter', () => {
|
||||||
|
let filter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
filter = {
|
||||||
|
type: FilterType.SEARCH,
|
||||||
|
field: 'fieldName',
|
||||||
|
exclude: false,
|
||||||
|
id: 'filter1',
|
||||||
|
options: {
|
||||||
|
values: ['searchInput'],
|
||||||
|
mode: SearchMode.CONTAINS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label for search', async () => {
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: *searchInput*');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when exclude mode is true and searchMode is CONTAINS', async () => {
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: *searchInput*');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when exclude is false and searchMode is CONTAINS', async () => {
|
||||||
|
filter.exclude = false;
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: *searchInput*');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when searchMode is MATCHES_EXACTLY', async () => {
|
||||||
|
filter.options.mode = SearchMode.MATCHES_EXACTLY;
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: searchInput');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when searchMode is STARTS_WITH', async () => {
|
||||||
|
filter.options.mode = SearchMode.STARTS_WITH;
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: searchInput*');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when searchMode is ENDS_WITH', async () => {
|
||||||
|
filter.options.mode = SearchMode.ENDS_WITH;
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: *searchInput');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when searchMode is BEGINNING_OF_WORD', async () => {
|
||||||
|
filter.options.mode = SearchMode.BEGINNING_OF_WORD;
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: *^searchInput*');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Condition filter', () => {
|
||||||
|
let filter;
|
||||||
|
beforeEach(() => {
|
||||||
|
filter = {
|
||||||
|
type: FilterType.CONDITION,
|
||||||
|
field: 'fieldName',
|
||||||
|
exclude: false,
|
||||||
|
id: 'filter1',
|
||||||
|
options: {
|
||||||
|
modifier: {
|
||||||
|
type: ModifierType.FIXED_VALUE,
|
||||||
|
operator: '<',
|
||||||
|
},
|
||||||
|
mode: 'compare',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
test('should generate filter label when the filter is invalid', async () => {
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has COMPARE type and is FIXED_VALUE', async () => {
|
||||||
|
filter.options.firstValue = { fixedValue: '10' };
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: <10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has COMPARE type and is CALCULATED_VALUE', async () => {
|
||||||
|
filter.options.modifier = { type: 'calculated_value', operator: '<' };
|
||||||
|
filter.options.firstValue = { aggregator: 'sum', field: 'fieldName2' };
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: =fieldName<sum(total fieldName2)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has COMPARE type and is CALCULATED_VALUE and is between', async () => {
|
||||||
|
filter.options.modifier = { type: 'calculated_value', operator: '>= <=' };
|
||||||
|
filter.options.firstValue = { aggregator: 'sum', field: 'fieldName2' };
|
||||||
|
filter.options.secondValue = {
|
||||||
|
aggregator: 'count',
|
||||||
|
field: 'fieldName3',
|
||||||
|
};
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: =fieldName>=sum(total fieldName2) and fieldName<=count(total fieldName3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has COMPARE type and missing secondValue on between', async () => {
|
||||||
|
filter.options.modifier = { type: 'calculated_value', operator: '>= <=' };
|
||||||
|
filter.options.firstValue = { aggregator: 'sum', field: 'fieldName2' };
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has GENERAL type and filter is invalid', async () => {
|
||||||
|
filter.options.mode = ConditionMode.GENERAL;
|
||||||
|
filter.options.firstValue = { fixedValue: '10' };
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has GENERAL type and is FIXED_VALUE', async () => {
|
||||||
|
filter.options.mode = ConditionMode.GENERAL;
|
||||||
|
filter.options.firstValue = { fixedValue: '10' };
|
||||||
|
filter.options.conditionField = {
|
||||||
|
aggregator: 'avg',
|
||||||
|
field: 'conditionFieldName1',
|
||||||
|
};
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: =avg(conditionFieldName1)<10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has GENERAL type and is FIXED_VALUE and is between', async () => {
|
||||||
|
filter.options.modifier = { type: ModifierType.FIXED_VALUE, operator: '>= <=' };
|
||||||
|
filter.options.mode = ConditionMode.GENERAL;
|
||||||
|
filter.options.firstValue = { fixedValue: '10' };
|
||||||
|
filter.options.secondValue = { fixedValue: '100' };
|
||||||
|
filter.options.conditionField = {
|
||||||
|
aggregator: 'avg',
|
||||||
|
field: 'conditionFieldName1',
|
||||||
|
};
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: =avg(conditionFieldName1)>=10 and avg(conditionFieldName1)<=100');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has GENERAL type and is CALCULATED_VALUE', async () => {
|
||||||
|
filter.options.modifier = { type: 'calculated_value', operator: '<' };
|
||||||
|
filter.options.mode = ConditionMode.GENERAL;
|
||||||
|
filter.options.firstValue = { aggregator: 'sum', field: 'fieldName2' };
|
||||||
|
filter.options.conditionField = {
|
||||||
|
aggregator: 'avg',
|
||||||
|
field: 'conditionFieldName1',
|
||||||
|
};
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: =avg(conditionFieldName1)<sum(fieldName2)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label when the filter has GENERAL type and is CALCULATED_VALUE and is between', async () => {
|
||||||
|
filter.options.modifier = { type: 'calculated_value', operator: '>= <=' };
|
||||||
|
filter.options.mode = ConditionMode.GENERAL;
|
||||||
|
filter.options.firstValue = { aggregator: 'sum', field: 'fieldName2' };
|
||||||
|
filter.options.secondValue = {
|
||||||
|
aggregator: 'count',
|
||||||
|
field: 'fieldName3',
|
||||||
|
};
|
||||||
|
filter.options.conditionField = {
|
||||||
|
aggregator: 'avg',
|
||||||
|
field: 'conditionFieldName1',
|
||||||
|
};
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual(
|
||||||
|
'fieldName: =avg(conditionFieldName1)>=sum(fieldName2) and avg(conditionFieldName1)<=count(fieldName3)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle fields that contains multiple words', async () => {
|
||||||
|
filter.options.modifier = { type: 'calculated_value', operator: '>= <=' };
|
||||||
|
filter.options.mode = ConditionMode.GENERAL;
|
||||||
|
filter.options.firstValue = {
|
||||||
|
aggregator: 'sum',
|
||||||
|
field: 'field Name2',
|
||||||
|
};
|
||||||
|
filter.options.secondValue = {
|
||||||
|
aggregator: 'count',
|
||||||
|
field: 'field Name3',
|
||||||
|
};
|
||||||
|
filter.options.conditionField = {
|
||||||
|
aggregator: 'avg',
|
||||||
|
field: 'conditionField Name1',
|
||||||
|
};
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual(
|
||||||
|
'fieldName: =avg([conditionField Name1])>=sum([field Name2]) and avg([conditionField Name1])<=count([field Name3])'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Clear selection filter', () => {
|
||||||
|
let filter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
filter = {
|
||||||
|
type: FilterType.CLEAR_SELECTION,
|
||||||
|
field: 'fieldName',
|
||||||
|
exclude: false,
|
||||||
|
id: 'filter1',
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate correct filter label for clear selection', async () => {
|
||||||
|
const label = generateFiltersString([filter], translator);
|
||||||
|
expect(label).toEqual('fieldName: Object.FilterLabel.All');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multiple filters', () => {
|
||||||
|
test('should generate correct filter label for multiple filters', async () => {
|
||||||
|
const label = generateFiltersString(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: FilterType.VALUES,
|
||||||
|
field: 'fieldName',
|
||||||
|
exclude: true,
|
||||||
|
id: 'filter1',
|
||||||
|
options: {
|
||||||
|
values: ['value1', 'value2'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FilterType.SEARCH,
|
||||||
|
field: 'fieldName',
|
||||||
|
exclude: false,
|
||||||
|
id: 'filter1',
|
||||||
|
options: {
|
||||||
|
values: ['searchInput'],
|
||||||
|
mode: SearchMode.CONTAINS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
translator
|
||||||
|
);
|
||||||
|
expect(label).toEqual('fieldName: Object.FilterLabel.Exclude value1, value2; fieldName: *searchInput*');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { generateFiltersString } from './generateFiltersInfo';
|
||||||
|
|
||||||
const NP_PADDINH_IN_CARDS_WITHOUT_TITLE = ['action-button', 'sn-nav-menu'];
|
const NP_PADDINH_IN_CARDS_WITHOUT_TITLE = ['action-button', 'sn-nav-menu'];
|
||||||
const NO_PADDING_IN_CARDS = [
|
const NO_PADDING_IN_CARDS = [
|
||||||
'pivot-table',
|
'pivot-table',
|
||||||
@@ -35,13 +37,15 @@ const shouldUseCardPadding = ({ isCardTheme, layout, isError }) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPadding = ({ layout, isError, senseTheme, titleStyles }) => {
|
const getPadding = ({ layout, isError, isCardTheme, titleStyles, translator }) => {
|
||||||
const isCardTheme = senseTheme?.getStyle('', '', '_cards');
|
|
||||||
const cardPadding = shouldUseCardPadding({ isCardTheme, layout, isError });
|
const cardPadding = shouldUseCardPadding({ isCardTheme, layout, isError });
|
||||||
if (isCardTheme) {
|
if (isCardTheme) {
|
||||||
const showTitle = layout?.showTitles && !!layout?.title;
|
const showTitle = layout?.showTitles && !!layout?.title;
|
||||||
const showSubtitle = layout?.showTitles && !!layout?.subtitle;
|
const showSubtitle = layout?.showTitles && !!layout?.subtitle;
|
||||||
const showFootnote = layout?.showTitles && !!layout?.footnote;
|
const hasFilters = layout?.filters?.length > 0 && layout?.qHyperCube?.qMeasureInfo?.length > 0;
|
||||||
|
const filtersFootnoteString = generateFiltersString(layout?.filters ?? [], translator);
|
||||||
|
const showFilters = !layout?.footnote && hasFilters && filtersFootnoteString;
|
||||||
|
const showFootnote = layout?.showTitles && (!!layout?.footnote || showFilters);
|
||||||
|
|
||||||
if (showTitle && cardPadding) {
|
if (showTitle && cardPadding) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
|||||||
21
apis/nucleus/src/utils/escape-field.js
Normal file
21
apis/nucleus/src/utils/escape-field.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Escape a script field name. Will add surrounding brackets if the field name contains special characters.
|
||||||
|
* Examples:
|
||||||
|
* Field1 -> Field1
|
||||||
|
* My field -> [My field]
|
||||||
|
* My] field -> [My]] field]
|
||||||
|
*
|
||||||
|
* @param field
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const escapeField = (field) => {
|
||||||
|
if (!field || field === ']') {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
if (/^[A-Za-z][A-Za-z0-9_]*$/.test(field)) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
return `[${field.replace(/\]/g, ']]')}]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default escapeField;
|
||||||
172
apis/nucleus/src/utils/generateFiltersInfo.js
Normal file
172
apis/nucleus/src/utils/generateFiltersInfo.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import escapeField from './escape-field';
|
||||||
|
|
||||||
|
export const FilterType = {
|
||||||
|
VALUES: 'values',
|
||||||
|
CONDITION: 'condition',
|
||||||
|
SEARCH: 'search',
|
||||||
|
CLEAR_SELECTION: 'clear_selection',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchMode = {
|
||||||
|
CONTAINS: 'contains',
|
||||||
|
MATCHES_EXACTLY: 'matches_exactly',
|
||||||
|
STARTS_WITH: 'starts_with',
|
||||||
|
ENDS_WITH: 'ends_with',
|
||||||
|
BEGINNING_OF_WORD: 'beginning_of_word',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConditionMode = {
|
||||||
|
GENERAL: 'general',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModifierType = {
|
||||||
|
FIXED_VALUE: 'fixed_value',
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSearchValue = (filter) => {
|
||||||
|
let prefix = '';
|
||||||
|
let postfix = '';
|
||||||
|
const mode = filter.options?.mode;
|
||||||
|
switch (mode) {
|
||||||
|
case SearchMode.BEGINNING_OF_WORD:
|
||||||
|
prefix = '*^';
|
||||||
|
postfix = '*';
|
||||||
|
break;
|
||||||
|
case SearchMode.ENDS_WITH:
|
||||||
|
prefix = '*';
|
||||||
|
break;
|
||||||
|
case SearchMode.STARTS_WITH:
|
||||||
|
postfix = '*';
|
||||||
|
break;
|
||||||
|
case SearchMode.MATCHES_EXACTLY:
|
||||||
|
break;
|
||||||
|
case SearchMode.CONTAINS:
|
||||||
|
default:
|
||||||
|
prefix = '*';
|
||||||
|
postfix = '*';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const value = filter.options.values?.[0] ?? '';
|
||||||
|
return value !== '' ? `"${prefix}${value}${postfix}"` : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateConditionValue = (filter) => {
|
||||||
|
const { options } = filter;
|
||||||
|
|
||||||
|
const isGeneralMode = options?.mode === ConditionMode.GENERAL;
|
||||||
|
const isFixedModifier = options?.modifier?.type === ModifierType.FIXED_VALUE;
|
||||||
|
const modifierOperators = options?.modifier?.operator ? options.modifier.operator.split(' ') : [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
modifierOperators.length === 0 ||
|
||||||
|
(!isFixedModifier && (!options.firstValue?.aggregator || !options.firstValue?.field)) ||
|
||||||
|
(isFixedModifier && !options.firstValue?.fixedValue) ||
|
||||||
|
(isGeneralMode && (!options.conditionField?.aggregator || !options.conditionField?.field))
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let conditionField;
|
||||||
|
let firstValue;
|
||||||
|
let conditionPrefix = '=';
|
||||||
|
if (isGeneralMode && isFixedModifier) {
|
||||||
|
// general mode with fixed modifiers
|
||||||
|
conditionField = `${options.conditionField?.aggregator}(${escapeField(options.conditionField?.field)})`;
|
||||||
|
firstValue = `${options.firstValue?.fixedValue}`;
|
||||||
|
} else if (!isGeneralMode && isFixedModifier) {
|
||||||
|
// comparing mode with fixed modifiers
|
||||||
|
conditionField = ``;
|
||||||
|
firstValue = `${options.firstValue?.fixedValue}`;
|
||||||
|
conditionPrefix = '';
|
||||||
|
} else if (!isGeneralMode && !isFixedModifier) {
|
||||||
|
// comparing mode without fixed modifiers
|
||||||
|
conditionField = `${escapeField(filter.field)}`;
|
||||||
|
firstValue = `${options.firstValue?.aggregator}(total ${escapeField(options.firstValue?.field)})`;
|
||||||
|
} else {
|
||||||
|
// general mode without fixed modifiers
|
||||||
|
conditionField = `${options.conditionField?.aggregator}(${escapeField(options.conditionField?.field)})`;
|
||||||
|
firstValue = `${options.firstValue?.aggregator}(${escapeField(options.firstValue?.field)})`;
|
||||||
|
}
|
||||||
|
const leftConditionResult = `${conditionPrefix}${conditionField}${modifierOperators[0]}${firstValue}`;
|
||||||
|
|
||||||
|
let rightConditionResult = '';
|
||||||
|
if (modifierOperators.length > 1) {
|
||||||
|
if (
|
||||||
|
(!isFixedModifier && (!options.secondValue?.aggregator || !options.secondValue?.field)) ||
|
||||||
|
(isFixedModifier && !options.secondValue?.fixedValue)
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let secondValue;
|
||||||
|
let rightConditionField;
|
||||||
|
if (isGeneralMode && isFixedModifier) {
|
||||||
|
// general mode with fixed modifiers
|
||||||
|
secondValue = `${options.secondValue?.fixedValue}`;
|
||||||
|
rightConditionField = ` and ${conditionField}`;
|
||||||
|
} else if (!isGeneralMode && isFixedModifier) {
|
||||||
|
// comparing mode with fixed modifiers
|
||||||
|
secondValue = `${options.secondValue?.fixedValue}`;
|
||||||
|
rightConditionField = '';
|
||||||
|
} else if (!isGeneralMode && !isFixedModifier) {
|
||||||
|
// comparing mode without fixed modifiers
|
||||||
|
secondValue = `${options.secondValue?.aggregator}(total ${escapeField(options.secondValue?.field)})`;
|
||||||
|
rightConditionField = ` and ${conditionField}`;
|
||||||
|
} else {
|
||||||
|
// general mode without fixed modifiers
|
||||||
|
secondValue = `${options.secondValue?.aggregator}(${escapeField(options.secondValue?.field)})`;
|
||||||
|
rightConditionField = ` and ${conditionField}`;
|
||||||
|
}
|
||||||
|
rightConditionResult = `${rightConditionField}${modifierOperators[1]}${secondValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `"${leftConditionResult}${rightConditionResult}"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateFiltersLabels = (filters, translator) => {
|
||||||
|
if (!Array.isArray(filters)) {
|
||||||
|
// QB-15075: when the property `filters` was already used by older apps
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const EXCLUDE = translator.get('Object.FilterLabel.Exclude');
|
||||||
|
const filtersToShow = filters.filter((filter) => filter.showInFooter !== false);
|
||||||
|
return filtersToShow
|
||||||
|
.map((filter) => {
|
||||||
|
let label = '';
|
||||||
|
switch (filter.type) {
|
||||||
|
case FilterType.SEARCH:
|
||||||
|
label += generateSearchValue(filter);
|
||||||
|
break;
|
||||||
|
case FilterType.CONDITION:
|
||||||
|
label += generateConditionValue(filter);
|
||||||
|
break;
|
||||||
|
case FilterType.CLEAR_SELECTION:
|
||||||
|
label += translator.get('Object.FilterLabel.All');
|
||||||
|
break;
|
||||||
|
case FilterType.VALUES:
|
||||||
|
default:
|
||||||
|
label += filter.options?.values
|
||||||
|
? filter.options.values.join(', ')
|
||||||
|
: translator.get('Object.FilterLabel.Unknown');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label !== '') {
|
||||||
|
// Trim quotes
|
||||||
|
label = label.replace(/^"|"$/g, '');
|
||||||
|
// Prefix with exclude if filter inverted
|
||||||
|
if (filter.exclude) label = `${EXCLUDE} ${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
field: filter.field,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((filter) => filter.label !== '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateFiltersString = (filters, translator) =>
|
||||||
|
generateFiltersLabels(filters, translator)
|
||||||
|
.map((f) => `${f.field}: ${f.label}`)
|
||||||
|
.join('; ');
|
||||||
15
packages/ui/icons/filter.js
Normal file
15
packages/ui/icons/filter.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import SvgIcon from './SvgIcon';
|
||||||
|
|
||||||
|
const filter = (props) => ({
|
||||||
|
...props,
|
||||||
|
shapes: [
|
||||||
|
{
|
||||||
|
type: 'path',
|
||||||
|
attrs: {
|
||||||
|
d: 'M14.07 0a1 1 0 0 1 .816 1.577L10 8.5v4.398a1 1 0 0 1-.532.884l-2.734 1.445A.5.5 0 0 1 6 14.785V8.5L1.112 1.577A1 1 0 0 1 1.93 0zm0 1H1.93L7 8.183v5.772l2-1.057V8.183z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
export default (props) => SvgIcon(filter(props));
|
||||||
|
export { filter };
|
||||||
Reference in New Issue
Block a user