feat: sort card view by Alphabetical, Recently Modified, and Least Recently Modified (#10601)

This commit is contained in:
Lily Kuang 2020-08-14 15:07:37 -07:00 committed by GitHub
parent acb00f509c
commit 03a62f15d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 294 additions and 41 deletions

View File

@ -22,14 +22,17 @@ import { act } from 'react-dom/test-utils';
import { QueryParamProvider } from 'use-query-params';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import Button from 'src/components/Button';
import CardCollection from 'src/components/ListView/CardCollection';
import { CardSortSelect } from 'src/components/ListView/CardSortSelect';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import ListView from 'src/components/ListView/ListView';
import ListViewFilters from 'src/components/ListView/Filters';
import ListViewPagination from 'src/components/ListView/Pagination';
import Pagination from 'src/components/Pagination';
import Button from 'src/components/Button';
import TableCollection from 'src/components/ListView/TableCollection';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
function makeMockLocation(query) {
const queryStr = encodeURIComponent(query);
@ -100,6 +103,14 @@ const mockedProps = {
onSelect: jest.fn(),
},
],
cardSortSelectOptions: [
{
desc: false,
id: 'something',
label: 'Alphabetical',
value: 'alphabetical',
},
],
};
const factory = (props = mockedProps) =>
@ -281,6 +292,24 @@ describe('ListView', () => {
);
});
it('disable card view based on prop', async () => {
expect(wrapper.find(CardCollection).exists()).toBe(false);
expect(wrapper.find(CardSortSelect).exists()).toBe(false);
expect(wrapper.find(TableCollection).exists()).toBe(true);
});
it('enable card view based on prop', async () => {
const wrapper2 = factory({
...mockedProps,
renderCard: jest.fn(),
initialSort: [{ id: 'something' }],
});
await waitForComponentToPaint(wrapper2);
expect(wrapper2.find(CardCollection).exists()).toBe(true);
expect(wrapper2.find(CardSortSelect).exists()).toBe(true);
expect(wrapper2.find(TableCollection).exists()).toBe(false);
});
it('Throws an exception if filter missing in columns', () => {
expect.assertions(1);
const props = {
@ -377,4 +406,24 @@ describe('ListView', () => {
]
`);
});
it('calls fetchData on card view sort', async () => {
const wrapper2 = factory({
...mockedProps,
renderCard: jest.fn(),
initialSort: [{ id: 'something' }],
});
act(() => {
wrapper2.find('[data-test="card-sort-select"]').first().props().onChange({
desc: false,
id: 'something',
label: 'Alphabetical',
value: 'alphabetical',
});
});
wrapper2.update();
expect(mockedProps.fetchData).toHaveBeenCalled();
});
});

View File

@ -24,6 +24,7 @@ import fetchMock from 'fetch-mock';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import ChartList from 'src/views/CRUD/chart/ChartList';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ListView from 'src/components/ListView';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import ListViewCard from 'src/components/ListViewCard';
@ -49,7 +50,7 @@ const mockCharts = [...new Array(3)].map((_, i) => ({
}));
fetchMock.get(chartsInfoEndpoint, {
permissions: ['can_list', 'can_edit'],
permissions: ['can_list', 'can_edit', 'can_delete'],
});
fetchMock.get(chartssOwnersEndpoint, {
result: [],
@ -113,4 +114,9 @@ describe('ChartList', () => {
wrapper.find('[data-test="pencil"]').first().simulate('click');
expect(wrapper.find(PropertiesModal)).toExist();
});
it('delete', () => {
wrapper.find('[data-test="trash"]').first().simulate('click');
expect(wrapper.find(ConfirmStatusChange)).toExist();
});
});

View File

@ -23,10 +23,11 @@ import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
import ListView from 'src/components/ListView';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import ListViewCard from 'src/components/ListViewCard';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
// store needed for withToasts(DashboardTable)
const mockStore = configureStore([thunk]);
@ -50,7 +51,7 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({
}));
fetchMock.get(dashboardsInfoEndpoint, {
permissions: ['can_list', 'can_edit'],
permissions: ['can_list', 'can_edit', 'can_delete'],
});
fetchMock.get(dashboardOwnersEndpoint, {
result: [],
@ -104,4 +105,19 @@ describe('DashboardList', () => {
wrapper.find('[data-test="pencil"]').first().simulate('click');
expect(wrapper.find(PropertiesModal)).toExist();
});
it('card view edits', () => {
wrapper.find('[data-test="pencil"]').last().simulate('click');
expect(wrapper.find(PropertiesModal)).toExist();
});
it('delete', () => {
wrapper.find('[data-test="trash"]').first().simulate('click');
expect(wrapper.find(ConfirmStatusChange)).toExist();
});
it('card view delete', () => {
wrapper.find('[data-test="trash"]').last().simulate('click');
expect(wrapper.find(ConfirmStatusChange)).toExist();
});
});

View File

@ -0,0 +1,114 @@
/**
* 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, { useState } from 'react';
import { styled, withTheme, SupersetThemeProps } from '@superset-ui/style';
import { PartialThemeConfig, Select } from 'src/components/Select';
import { CardSortSelectOption, FetchDataConfig, SortColumn } from './types';
import { filterSelectStyles } from './utils';
const SortTitle = styled.label`
font-weight: bold;
line-height: 27px;
margin: 0 0.4em 0 0;
`;
const SortContainer = styled.div`
display: inline-flex;
float: right;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
padding: 24px 24px 0 0;
position: relative;
top: 8px;
`;
interface CardViewSelectSortProps {
onChange: (conf: FetchDataConfig) => any;
options: Array<CardSortSelectOption>;
initialSort?: SortColumn[];
pageIndex: number;
pageSize: number;
}
interface StyledSelectProps {
onChange: (value: CardSortSelectOption) => void;
options: CardSortSelectOption[];
selectStyles: any;
theme: SupersetThemeProps['theme'];
value: CardSortSelectOption;
}
function StyledSelect({
onChange,
options,
selectStyles,
theme,
value,
}: StyledSelectProps) {
const filterSelectTheme: PartialThemeConfig = {
spacing: {
baseUnit: 1,
fontSize: theme.typography.sizes.s,
minWidth: '5em',
},
};
return (
<Select
data-test="card-sort-select"
clearable={false}
onChange={onChange}
options={options}
stylesConfig={selectStyles}
themeConfig={filterSelectTheme}
value={value}
/>
);
}
const StyledCardSortSelect = withTheme(StyledSelect);
export const CardSortSelect = ({
initialSort,
onChange,
options,
pageIndex,
pageSize,
}: CardViewSelectSortProps) => {
const defaultSort =
initialSort && options.find(({ id }) => id === initialSort[0].id);
const [selectedOption, setSelectedOption] = useState<CardSortSelectOption>(
defaultSort || options[0],
);
const handleOnChange = (selected: CardSortSelectOption) => {
setSelectedOption(selected);
const sortBy = [{ id: selected.id, desc: selected.desc }];
onChange({ pageIndex, pageSize, sortBy, filters: [] });
};
return (
<SortContainer>
<SortTitle>Sort:</SortTitle>
<StyledCardSortSelect
onChange={(value: CardSortSelectOption) => handleOnChange(value)}
options={options}
selectStyles={filterSelectStyles}
value={selectedOption}
/>
</SortContainer>
);
};

View File

@ -23,29 +23,29 @@ import {
Select,
PaginatedSelect,
PartialThemeConfig,
PartialStylesConfig,
} from 'src/components/Select';
import SearchInput from 'src/components/SearchInput';
import {
Filter,
Filters,
FilterValue,
Filters,
InternalFilter,
SelectOption,
} from './types';
import { filterSelectStyles } from './utils';
interface BaseFilter {
Header: string;
initialValue: any;
}
interface SelectFilterProps extends BaseFilter {
name?: string;
onSelect: (selected: any) => any;
selects: Filter['selects'];
emptyLabel?: string;
fetchSelects?: Filter['fetchSelects'];
name?: string;
onSelect: (selected: any) => any;
paginate?: boolean;
selects: Filter['selects'];
theme: SupersetThemeProps['theme'];
}
@ -61,40 +61,23 @@ const FilterTitle = styled.label`
margin: 0 0.4em 0 0;
`;
const filterSelectStyles: PartialStylesConfig = {
container: (provider, { getValue }) => ({
...provider,
// dynamic width based on label string length
minWidth: `${Math.min(
12,
Math.max(5, 3 + getValue()[0].label.length / 2),
)}em`,
}),
control: provider => ({
...provider,
borderWidth: 0,
boxShadow: 'none',
cursor: 'pointer',
}),
};
const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
function SelectFilter({
Header,
selects = [],
emptyLabel = 'None',
fetchSelects,
initialValue,
onSelect,
fetchSelects,
paginate = false,
selects = [],
theme,
}: SelectFilterProps) {
const filterSelectTheme: PartialThemeConfig = {
spacing: {
baseUnit: 2,
minWidth: '5em',
fontSize: theme.typography.sizes.s,
minWidth: '5em',
},
};
@ -235,12 +218,12 @@ function UIFilters({
(
{
Header,
fetchSelects,
id,
input,
paginate,
selects,
unfilteredLabel,
fetchSelects,
paginate,
},
index,
) => {
@ -249,24 +232,24 @@ function UIFilters({
if (input === 'select') {
return (
<StyledSelectFilter
Header={Header}
emptyLabel={unfilteredLabel}
fetchSelects={fetchSelects}
initialValue={initialValue}
key={id}
name={id}
Header={Header}
selects={selects}
emptyLabel={unfilteredLabel}
initialValue={initialValue}
fetchSelects={fetchSelects}
paginate={paginate}
onSelect={(value: any) => updateFilterValue(index, value)}
paginate={paginate}
selects={selects}
/>
);
}
if (input === 'search') {
return (
<SearchFilter
key={id}
Header={Header}
initialValue={initialValue}
key={id}
onSubmit={(value: string) => updateFilterValue(index, value)}
/>
);

View File

@ -28,7 +28,13 @@ import TableCollection from './TableCollection';
import CardCollection from './CardCollection';
import Pagination from './Pagination';
import FilterControls from './Filters';
import { FetchDataConfig, Filters, SortColumn } from './types';
import { CardSortSelect } from './CardSortSelect';
import {
FetchDataConfig,
Filters,
SortColumn,
CardSortSelectOption,
} from './types';
import { ListViewError, useListViewState } from './utils';
const ListViewStyles = styled.div`
@ -188,6 +194,7 @@ export interface ListViewProps<T = any> {
disableBulkSelect?: () => void;
renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
renderCard?: (row: T) => React.ReactNode;
cardSortSelectOptions?: Array<CardSortSelectOption>;
}
const ListView: FunctionComponent<ListViewProps> = ({
@ -205,6 +212,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
disableBulkSelect = () => {},
renderBulkSelectCopy = selected => t('%s Selected', selected.length),
renderCard,
cardSortSelectOptions,
}) => {
const {
getTableProps,
@ -263,6 +271,15 @@ const ListView: FunctionComponent<ListViewProps> = ({
updateFilterValue={applyFilterValue}
/>
)}
{viewingMode === 'card' && cardSortSelectOptions && (
<CardSortSelect
initialSort={initialSort}
onChange={fetchData}
options={cardSortSelectOptions}
pageIndex={pageIndex}
pageSize={pageSize}
/>
)}
</div>
<div className="body">
{bulkSelectEnabled && (

View File

@ -28,6 +28,13 @@ export interface SelectOption {
value: any;
}
export interface CardSortSelectOption {
desc: boolean;
id: any;
label: string;
value: any;
}
export interface Filter {
Header: string;
id: string;

View File

@ -34,7 +34,7 @@ import {
} from 'use-query-params';
import { isEqual } from 'lodash';
import { PartialStylesConfig } from 'src/components/Select';
import {
FetchDataConfig,
Filter,
@ -255,3 +255,20 @@ export function useListViewState({
applyFilterValue,
};
}
export const filterSelectStyles: PartialStylesConfig = {
container: (provider, { getValue }) => ({
...provider,
// dynamic width based on label string length
minWidth: `${Math.min(
12,
Math.max(5, 3 + getValue()[0].label.length / 2),
)}em`,
}),
control: provider => ({
...provider,
borderWidth: 0,
boxShadow: 'none',
cursor: 'pointer',
}),
};

View File

@ -342,6 +342,27 @@ class ChartList extends React.PureComponent<Props, State> {
},
];
sortTypes = [
{
desc: false,
id: 'slice_name',
label: 'Alphabetical',
value: 'alphabetical',
},
{
desc: true,
id: 'changed_on_delta_humanized',
label: 'Recently Modified',
value: 'recently_modified',
},
{
desc: false,
id: 'changed_on_delta_humanized',
label: 'Least Recently Modified',
value: 'least_recently_modified',
},
];
hasPerm = (perm: string) => {
if (!this.state.permissions.length) {
return false;
@ -592,6 +613,7 @@ class ChartList extends React.PureComponent<Props, State> {
<ListView
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
cardSortSelectOptions={this.sortTypes}
className="chart-list-view"
columns={this.columns}
count={chartCount}

View File

@ -266,6 +266,27 @@ class DashboardList extends React.PureComponent<Props, State> {
},
];
sortTypes = [
{
desc: false,
id: 'dashboard_title',
label: 'Alphabetical',
value: 'alphabetical',
},
{
desc: true,
id: 'changed_on_delta_humanized',
label: 'Recently Modified',
value: 'recently_modified',
},
{
desc: false,
id: 'changed_on_delta_humanized',
label: 'Least Recently Modified',
value: 'least_recently_modified',
},
];
hasPerm = (perm: string) => {
if (!this.state.permissions.length) {
return false;
@ -601,6 +622,7 @@ class DashboardList extends React.PureComponent<Props, State> {
<ListView
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
cardSortSelectOptions={this.sortTypes}
className="dashboard-list-view"
columns={this.columns}
count={dashboardCount}