Skip to content

Commit

Permalink
feat(native-filters): Re-arrange controls in FilterBar (#18784)
Browse files Browse the repository at this point in the history
* feat(native-filters): Re-arrange controls in FilterBar

* Typo fix

* Add tests

* Fix tests

* Address PR reviews

* Add 1px bottom padding to icon

* Fix test

* Tiny refactor

* Change buttons background

* Add hover state to Clear all button

* Fix Clear All button logic so it clears all selections

* Remove redundant useEffect

* Set zindex higher than loading icon

* Fix scrolling issue
  • Loading branch information
kgabryje committed Feb 22, 2022
1 parent 4c16586 commit 9d5c050
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export const nativeFilters = {
collapse: dataTestLocator('filter-bar__collapse-button'),
filterName: dataTestLocator('filter-control-name'),
filterContent: '.ant-select-selection-item',
createFilterButton: dataTestLocator('create-filter'),
createFilterButton: dataTestLocator('filter-bar__create-filter'),
timeRangeFilterContent: dataTestLocator('time-range-trigger'),
},
createFilterButton: dataTestLocator('filter-bar__create-filter'),
Expand Down
8 changes: 5 additions & 3 deletions superset-frontend/src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,16 @@ export default function Button(props: ButtonProps) {
},
'&[disabled], &[disabled]:hover': {
color: grayscale.base,
backgroundColor: backgroundColorDisabled,
borderColor: borderColorDisabled,
backgroundColor:
buttonStyle === 'link' ? 'transparent' : backgroundColorDisabled,
borderColor:
buttonStyle === 'link' ? 'transparent' : borderColorDisabled,
},
marginLeft: 0,
'& + .superset-button': {
marginLeft: theme.gridUnit * 2,
},
'& :first-of-type': {
'& > :first-of-type': {
marginRight: firstChildMargin,
},
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,20 @@ import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
import Loading from 'src/components/Loading';
import { EmptyStateBig } from 'src/components/EmptyState';
import { useUiConfig } from 'src/components/UiConfigContext';
import {
BUILDER_SIDEPANEL_WIDTH,
CLOSED_FILTER_BAR_WIDTH,
FILTER_BAR_HEADER_HEIGHT,
FILTER_BAR_TABS_HEIGHT,
HEADER_HEIGHT,
MAIN_HEADER_HEIGHT,
OPEN_FILTER_BAR_WIDTH,
TABS_HEIGHT,
} from 'src/dashboard/constants';
import { shouldFocusTabs, getRootLevelTabsComponent } from './utils';
import DashboardContainer from './DashboardContainer';
import { useNativeFilters } from './state';

const MAIN_HEADER_HEIGHT = 53;
const TABS_HEIGHT = 50;
const HEADER_HEIGHT = 72;
const CLOSED_FILTER_BAR_WIDTH = 32;
const OPEN_FILTER_BAR_WIDTH = 260;
const FILTER_BAR_HEADER_HEIGHT = 80;
const FILTER_BAR_TABS_HEIGHT = 46;
const BUILDER_SIDEPANEL_WIDTH = 374;

type DashboardBuilderProps = {};

const StyledDiv = styled.div`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from 'spec/helpers/testing-library';
import { ActionButtons } from './index';

const createProps = () => ({
onApply: jest.fn(),
onClearAll: jest.fn(),
dataMaskSelected: {
DefaultsID: {
filterState: {
value: null,
},
},
},
dataMaskApplied: {
DefaultsID: {
id: 'DefaultsID',
filterState: {
value: null,
},
},
},
isApplyDisabled: false,
});

test('should render the "Apply" button', () => {
const mockedProps = createProps();
render(<ActionButtons {...mockedProps} />, { useRedux: true });
expect(screen.getByText('Apply filters')).toBeInTheDocument();
expect(screen.getByText('Apply filters').parentElement).toBeEnabled();
});

test('should render the "Clear all" button as disabled', () => {
const mockedProps = createProps();
render(<ActionButtons {...mockedProps} />, { useRedux: true });
const clearBtn = screen.getByText('Clear all');
expect(clearBtn.parentElement).toBeDisabled();
});

test('should render the "Apply" button as disabled', () => {
const mockedProps = createProps();
const applyDisabledProps = {
...mockedProps,
isApplyDisabled: true,
};
render(<ActionButtons {...applyDisabledProps} />, { useRedux: true });
const applyBtn = screen.getByText('Apply filters');
expect(applyBtn.parentElement).toBeDisabled();
userEvent.click(applyBtn);
expect(mockedProps.onApply).not.toHaveBeenCalled();
});

test('should apply', () => {
const mockedProps = createProps();
render(<ActionButtons {...mockedProps} />, { useRedux: true });
const applyBtn = screen.getByText('Apply filters');
expect(mockedProps.onApply).not.toHaveBeenCalled();
userEvent.click(applyBtn);
expect(mockedProps.onApply).toHaveBeenCalled();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useMemo } from 'react';
import {
css,
DataMaskState,
DataMaskStateWithId,
styled,
t,
} from '@superset-ui/core';
import Button from 'src/components/Button';
import { isNullish } from 'src/utils/common';
import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
import { getFilterBarTestId } from '../index';

interface ActionButtonsProps {
onApply: () => void;
onClearAll: () => void;
dataMaskSelected: DataMaskState;
dataMaskApplied: DataMaskStateWithId;
isApplyDisabled: boolean;
}

const ActionButtonsContainer = styled.div`
${({ theme }) => css`
display: flex;
flex-direction: column;
align-items: center;
position: fixed;
z-index: 100;
// filter bar width minus 1px for border
width: ${OPEN_FILTER_BAR_WIDTH - 1}px;
bottom: 0;
padding: ${theme.gridUnit * 4}px;
padding-top: ${theme.gridUnit * 6}px;
background: linear-gradient(transparent, white 25%);
pointer-events: none;
& > button {
pointer-events: auto;
}
& > .filter-apply-button {
margin-bottom: ${theme.gridUnit * 3}px;
}
&& > .filter-clear-all-button {
color: ${theme.colors.grayscale.base};
margin-left: 0;
&:hover {
color: ${theme.colors.primary.dark1};
}
&[disabled],
&[disabled]:hover {
color: ${theme.colors.grayscale.light1};
}
}
`};
`;

export const ActionButtons = ({
onApply,
onClearAll,
dataMaskApplied,
dataMaskSelected,
isApplyDisabled,
}: ActionButtonsProps) => {
const isClearAllEnabled = useMemo(
() =>
Object.values(dataMaskApplied).some(
filter =>
!isNullish(dataMaskSelected[filter.id]?.filterState?.value) ||
(!dataMaskSelected[filter.id] &&
!isNullish(filter.filterState?.value)),
),
[dataMaskApplied, dataMaskSelected],
);

return (
<ActionButtonsContainer>
<Button
disabled={isApplyDisabled}
buttonStyle="primary"
htmlType="submit"
className="filter-apply-button"
onClick={onApply}
{...getFilterBarTestId('apply-button')}
>
{t('Apply filters')}
</Button>
<Button
disabled={!isClearAllEnabled}
buttonStyle="link"
buttonSize="small"
className="filter-clear-all-button"
onClick={onClearAll}
{...getFilterBarTestId('clear-button')}
>
{t('Clear all')}
</Button>
</ActionButtonsContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ describe('FilterBar', () => {
expect(screen.getByText('Clear all')).toBeInTheDocument();
});

it('should render the "Apply" option', () => {
it('should render the "Apply filters" option', () => {
renderWrapper();
expect(screen.getByText('Apply')).toBeInTheDocument();
expect(screen.getByText('Apply filters')).toBeInTheDocument();
});

it('should render the collapse icon', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import { buildCascadeFiltersTree } from './utils';

const Wrapper = styled.div`
padding: ${({ theme }) => theme.gridUnit * 4}px;
// 108px padding to make room for buttons with position: absolute
padding-bottom: ${({ theme }) => theme.gridUnit * 27}px;
&:hover {
cursor: pointer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ const FilterSetsWrapper = styled.div`
align-items: center;
justify-content: center;
grid-template-columns: 1fr;
// 108px padding to make room for buttons with position: absolute
padding-bottom: ${({ theme }) => theme.gridUnit * 27}px;
& button.superset-button {
margin-left: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,6 @@ import Header from './index';

const createProps = () => ({
toggleFiltersBar: jest.fn(),
onApply: jest.fn(),
onClearAll: jest.fn(),
dataMaskSelected: {
DefaultsID: {
filterState: {
value: null,
},
},
},
dataMaskApplied: {
DefaultsID: {
id: 'DefaultsID',
filterState: {
value: null,
},
},
},
isApplyDisabled: false,
});

test('should render', () => {
Expand All @@ -55,48 +37,6 @@ test('should render the "Filters" heading', () => {
expect(screen.getByText('Filters')).toBeInTheDocument();
});

test('should render the "Clear all" option', () => {
const mockedProps = createProps();
render(<Header {...mockedProps} />, { useRedux: true });
expect(screen.getByText('Clear all')).toBeInTheDocument();
});

test('should render the "Apply" button', () => {
const mockedProps = createProps();
render(<Header {...mockedProps} />, { useRedux: true });
expect(screen.getByText('Apply')).toBeInTheDocument();
expect(screen.getByText('Apply').parentElement).toBeEnabled();
});

test('should render the "Clear all" button as disabled', () => {
const mockedProps = createProps();
render(<Header {...mockedProps} />, { useRedux: true });
const clearBtn = screen.getByText('Clear all');
expect(clearBtn.parentElement).toBeDisabled();
});

test('should render the "Apply" button as disabled', () => {
const mockedProps = createProps();
const applyDisabledProps = {
...mockedProps,
isApplyDisabled: true,
};
render(<Header {...applyDisabledProps} />, { useRedux: true });
const applyBtn = screen.getByText('Apply');
expect(applyBtn.parentElement).toBeDisabled();
userEvent.click(applyBtn);
expect(mockedProps.onApply).not.toHaveBeenCalled();
});

test('should apply', () => {
const mockedProps = createProps();
render(<Header {...mockedProps} />, { useRedux: true });
const applyBtn = screen.getByText('Apply');
expect(mockedProps.onApply).not.toHaveBeenCalled();
userEvent.click(applyBtn);
expect(mockedProps.onApply).toHaveBeenCalled();
});

test('should render the expand button', () => {
const mockedProps = createProps();
render(<Header {...mockedProps} />, { useRedux: true });
Expand Down

0 comments on commit 9d5c050

Please sign in to comment.