Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): Let users re-arrange native filters #16154

Merged
merged 56 commits into from Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ed138e2
feat. flag: ON
m-ajay Jul 22, 2021
84b8de1
Merge branch 'master' into feat/native-filter-rearrage
m-ajay Jul 22, 2021
684d8c1
Merge branch 'master' of https://github.com/apache/superset into feat…
m-ajay Jul 27, 2021
1e6cb2f
refactor filter tabs
m-ajay Jul 27, 2021
8b0982a
WIP
m-ajay Aug 3, 2021
a0cdd6b
Drag and Rearrange, styling
m-ajay Aug 9, 2021
f74c57f
refactoring dnd code
m-ajay Aug 9, 2021
7e988e3
Add apache license header
m-ajay Aug 10, 2021
077e081
Fix bug reported during PR review
m-ajay Aug 10, 2021
2d2fa28
Minor fix on remove
m-ajay Aug 10, 2021
4e5d00b
turn off filters
m-ajay Aug 11, 2021
ec1559c
Fix status
m-ajay Aug 11, 2021
2967992
Merge branch 'master' into feat/native-filter-rearrage
m-ajay Aug 11, 2021
4645e8a
Fix a test
m-ajay Aug 16, 2021
daae259
Address PR comments
m-ajay Aug 24, 2021
d577128
Merge branch 'master' into feat/native-filter-rearrage
m-ajay Aug 24, 2021
016e270
iSort fixes
m-ajay Aug 31, 2021
8827468
Add type key to the new filters
m-ajay Sep 1, 2021
0e91825
Fix wrong attribute
m-ajay Sep 1, 2021
d3ebaa1
indent
m-ajay Sep 2, 2021
91b9061
PR comments
m-ajay Sep 3, 2021
45b3c5f
PR comments
m-ajay Sep 7, 2021
cea8a7d
Fix failing tests
m-ajay Sep 7, 2021
5ecd89a
Merge branch 'master' into feat/native-filter-rearrage
m-ajay Sep 7, 2021
1cdb8db
Merge branch 'feat/migration-add-type-to-native-filter' into feat/nat…
m-ajay Sep 8, 2021
d3ee2f7
Styling
m-ajay Sep 20, 2021
5849f82
Fix remove filter
m-ajay Sep 22, 2021
0838192
Fix the drag issue
m-ajay Sep 23, 2021
7a46859
Save works
m-ajay Sep 29, 2021
e152809
fix
m-ajay Sep 29, 2021
4db826e
Conflict resolution
m-ajay Sep 29, 2021
6cefa20
Write tests
m-ajay Sep 30, 2021
fee8b3a
Style changes
m-ajay Sep 30, 2021
4e4f576
Merge branch 'master' into feat/native-filter-rearrage
m-ajay Sep 30, 2021
390af33
New Icon
m-ajay Oct 1, 2021
575c201
Grab & Grabbing Cursor
m-ajay Oct 1, 2021
ec034ae
Commented out code
m-ajay Oct 1, 2021
a7602fd
Fix tests, fix CI
m-ajay Oct 1, 2021
fa5fc03
Fix failing tests
m-ajay Oct 1, 2021
33fa6f3
Fix test
m-ajay Oct 4, 2021
1b2a88f
Style fixes
m-ajay Oct 5, 2021
f02a395
Merge branch 'master' into feat/native-filter-rearrage
m-ajay Oct 5, 2021
0ff99c4
portal nodes dependency
m-ajay Oct 5, 2021
00c020c
More style fixes
m-ajay Oct 6, 2021
5ba6717
PR comments
m-ajay Oct 6, 2021
ca8bc43
add unique ids to collapse panels
m-ajay Oct 8, 2021
fb419b3
Filter removal bug fixed
m-ajay Oct 12, 2021
257ffa4
PR comments
m-ajay Oct 12, 2021
abdffb6
Fix test warnings
m-ajay Oct 12, 2021
09ac811
merge conflicts - error alert is not working
m-ajay Oct 13, 2021
0f832a0
delete filter tabs
m-ajay Oct 13, 2021
2b81f94
Fix the breaking test
m-ajay Oct 13, 2021
fce3de1
Fix warnings
m-ajay Oct 14, 2021
839cc77
Fix the weird bug on cancel
m-ajay Oct 14, 2021
5f83b7c
refactor
m-ajay Oct 14, 2021
06bf6eb
Fix broken scope
m-ajay Oct 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions superset-frontend/spec/fixtures/mockNativeFilters.ts
Expand Up @@ -448,3 +448,39 @@ export const mockQueryDataForCountries = [
{ country_name: 'Zambia', 'SUM(SP_POP_TOTL)': 438847085 },
{ country_name: 'Zimbabwe', 'SUM(SP_POP_TOTL)': 509866860 },
];

export const buildNativeFilter = (
id: string,
name: string,
parents: string[],
) => ({
id,
controlValues: {
multiSelect: true,
enableEmptyFilter: false,
defaultToFirstItem: false,
inverseSelection: false,
searchAllOptions: false,
},
name,
filterType: 'filter_select',
targets: [
{
datasetId: 1,
column: {
name,
},
},
],
defaultDataMask: {
extraFormData: {},
filterState: {},
ownState: {},
},
cascadeParentIds: parents,
scope: {
rootPath: ['ROOT_ID'],
excluded: [],
},
type: 'NATIVE_FILTER',
});
Expand Up @@ -16,14 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactWrapper } from 'enzyme';
import React from 'react';
import { styledMount as mount } from 'spec/helpers/theming';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
import { Provider } from 'react-redux';
import Alert from 'src/components/Alert';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { mockStore } from 'spec/fixtures/mockStore';
import { styledMount as mount } from 'spec/helpers/theming';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import Alert from 'src/components/Alert';
import { FiltersConfigModal } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';

Object.defineProperty(window, 'matchMedia', {
Expand Down Expand Up @@ -66,7 +68,9 @@ describe('FiltersConfigModal', () => {
function setup(overridesProps?: any) {
return mount(
<Provider store={mockStore}>
<FiltersConfigModal {...mockedProps} {...overridesProps} />
<DndProvider backend={HTML5Backend}>
<FiltersConfigModal {...mockedProps} {...overridesProps} />
</DndProvider>
</Provider>,
);
}
Expand Down
Expand Up @@ -220,8 +220,9 @@ describe('FilterBar', () => {

const renderWrapper = (props = closedBarProps, state?: object) =>
render(<FilterBar {...props} width={280} height={400} offset={0} />, {
useRedux: true,
initialState: state,
useDnd: true,
useRedux: true,
useRouter: true,
});

Expand Down
Expand Up @@ -27,6 +27,7 @@ import { getFilterBarTestId } from '..';

export interface FCBProps {
createNewOnOpen?: boolean;
dashboardId?: number;
}

const HeaderButton = styled(Button)`
Expand All @@ -35,6 +36,7 @@ const HeaderButton = styled(Button)`

export const FilterConfigurationLink: React.FC<FCBProps> = ({
createNewOnOpen,
dashboardId,
children,
}) => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -65,6 +67,7 @@ export const FilterConfigurationLink: React.FC<FCBProps> = ({
onSave={submit}
onCancel={close}
createNewOnOpen={createNewOnOpen}
key={`filters-for-${dashboardId}`}
/>
</>
);
Expand Down
Expand Up @@ -17,16 +17,20 @@
* under the License.
*/
import React, { FC, useMemo, useState } from 'react';
import { DataMask, styled, t } from '@superset-ui/core';
import { css } from '@emotion/react';
import * as portals from 'react-reverse-portal';
import { DataMaskStateWithId } from 'src/dataMask/types';
import { DataMask, styled, t } from '@superset-ui/core';
import {
createHtmlPortalNode,
InPortal,
OutPortal,
} from 'react-reverse-portal';
import { Collapse } from 'src/common/components';
import { DataMaskStateWithId } from 'src/dataMask/types';
import { useDashboardHasTabs, useSelectFiltersInScope } from '../../state';
import { Filter } from '../../types';
michael-s-molina marked this conversation as resolved.
Show resolved Hide resolved
import CascadePopover from '../CascadeFilters/CascadePopover';
import { buildCascadeFiltersTree } from './utils';
import { useFilters } from '../state';
import { Filter } from '../../types';
import { useDashboardHasTabs, useSelectFiltersInScope } from '../../state';
import { buildCascadeFiltersTree } from './utils';

const Wrapper = styled.div`
padding: ${({ theme }) => theme.gridUnit * 4}px;
Expand All @@ -52,7 +56,7 @@ const FilterControls: FC<FilterControlsProps> = ({
const portalNodes = React.useMemo(() => {
const nodes = new Array(filterValues.length);
for (let i = 0; i < filterValues.length; i += 1) {
nodes[i] = portals.createHtmlPortalNode();
nodes[i] = createHtmlPortalNode();
}
return nodes;
}, [filterValues.length]);
Expand All @@ -78,7 +82,7 @@ const FilterControls: FC<FilterControlsProps> = ({
{portalNodes
.filter((node, index) => cascadeFilterIds.has(filterValues[index].id))
.map((node, index) => (
<portals.InPortal node={node}>
<InPortal node={node}>
<CascadePopover
data-test="cascade-filters-control"
key={cascadeFilters[index].id}
Expand All @@ -92,11 +96,17 @@ const FilterControls: FC<FilterControlsProps> = ({
directPathToChild={directPathToChild}
inView={false}
/>
</portals.InPortal>
</InPortal>
))}
{filtersInScope.map(filter => {
const index = filterValues.findIndex(f => f.id === filter.id);
return <portals.OutPortal node={portalNodes[index]} inView />;
return (
<OutPortal
key={`filters-inscope-${index}`}
node={portalNodes[index]}
inView
/>
);
})}
{showCollapsePanel && (
<Collapse
Expand Down Expand Up @@ -134,7 +144,13 @@ const FilterControls: FC<FilterControlsProps> = ({
>
{filtersOutOfScope.map(filter => {
const index = cascadeFilters.findIndex(f => f.id === filter.id);
return <portals.OutPortal node={portalNodes[index]} inView />;
return (
<OutPortal
key={`filters-outofscope-${index}`}
node={portalNodes[index]}
inView
/>
);
})}
</Collapse.Panel>
</Collapse>
Expand Down
Expand Up @@ -87,6 +87,9 @@ const Header: FC<HeaderProps> = ({
const canEdit = useSelector<RootState, boolean>(
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
);
const dashboardId = useSelector<RootState, number>(
({ dashboardInfo }) => dashboardInfo.id,
);

const isClearAllDisabled = Object.values(dataMaskApplied).every(
filter =>
Expand All @@ -99,7 +102,10 @@ const Header: FC<HeaderProps> = ({
<TitleArea>
<span>{t('Filters')}</span>
{canEdit && (
<FilterConfigurationLink createNewOnOpen={filterValues.length === 0}>
<FilterConfigurationLink
dashboardId={dashboardId}
createNewOnOpen={filterValues.length === 0}
>
<Icons.Edit
data-test="create-filter"
iconColor={theme.colors.grayscale.base}
Expand Down
@@ -0,0 +1,138 @@
/**
* 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 { styled } from '@superset-ui/core';
import React, { useRef } from 'react';
import {
DragSourceMonitor,
DropTargetMonitor,
useDrag,
useDrop,
XYCoord,
} from 'react-dnd';
import Icons, { IconType } from 'src/components/Icons';

interface TitleContainerProps {
readonly isDragging: boolean;
}
const FILTER_TYPE = 'FILTER';

const Container = styled.div<TitleContainerProps>`
${({ isDragging, theme }) => `
opacity: ${isDragging ? 0.3 : 1};
cursor: ${isDragging ? 'grabbing' : 'pointer'};
width: 100%;
display: flex;
padding: ${theme.gridUnit}px
`}
michael-s-molina marked this conversation as resolved.
Show resolved Hide resolved
`;

const DragIcon = styled(Icons.Drag)<IconType & { isDragging: boolean }>`
${({ isDragging, theme }) => `
font-size: ${theme.typography.sizes.m}px;
margin-top: 15px;
cursor: ${isDragging ? 'grabbing' : 'grab'};
padding-left: ${theme.gridUnit}px;
`}
michael-s-molina marked this conversation as resolved.
Show resolved Hide resolved
`;

interface FilterTabTitleProps {
index: number;
filterIds: string[];
onRearrage: (dragItemIndex: number, targetIndex: number) => void;
}

interface DragItem {
index: number;
filterIds: string[];
type: string;
}

export const DraggableFilter: React.FC<FilterTabTitleProps> = ({
index,
onRearrage,
filterIds,
children,
}) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({
item: { filterIds, type: FILTER_TYPE, index },
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [, drop] = useDrop({
accept: FILTER_TYPE,
hover: (item: DragItem, monitor: DropTargetMonitor) => {
if (!ref.current) {
return;
}

const dragIndex = item.index;
const hoverIndex = index;

// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();

// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;

// Determine mouse position
const clientOffset = monitor.getClientOffset();

// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;

// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%

// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}

// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}

onRearrage(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here.
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
// eslint-disable-next-line no-param-reassign
item.index = hoverIndex;
},
});
drag(drop(ref));
return (
<Container ref={ref} isDragging={isDragging}>
<DragIcon isDragging={isDragging} alt="Move icon" className="dragIcon" />
<div css={{ flexGrow: 4 }}>{children}</div>
</Container>
);
};

export default DraggableFilter;