From 03a62f15d80ed323c4647a98e611b13f8eea4f66 Mon Sep 17 00:00:00 2001 From: Lily Kuang <lily@preset.io> Date: Fri, 14 Aug 2020 15:07:37 -0700 Subject: [PATCH] feat: sort card view by Alphabetical, Recently Modified, and Least Recently Modified (#10601) --- .../components/ListView/ListView_spec.jsx | 53 +++++++- .../views/CRUD/chart/ChartList_spec.jsx | 8 +- .../CRUD/dashboard/DashboardList_spec.jsx | 20 ++- .../components/ListView/CardSortSelect.tsx | 114 ++++++++++++++++++ .../src/components/ListView/Filters.tsx | 49 +++----- .../src/components/ListView/ListView.tsx | 19 ++- .../src/components/ListView/types.ts | 7 ++ .../src/components/ListView/utils.ts | 19 ++- .../src/views/CRUD/chart/ChartList.tsx | 22 ++++ .../views/CRUD/dashboard/DashboardList.tsx | 22 ++++ 10 files changed, 293 insertions(+), 40 deletions(-) create mode 100644 superset-frontend/src/components/ListView/CardSortSelect.tsx diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx index 8625f1d06f249..fee729e117d28 100644 --- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx +++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx @@ -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(); + }); }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx index 2083b4a6bec14..213ac39a940e8 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx @@ -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(); + }); }); diff --git a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx index eef4ca03eb45b..37d5ca267284e 100644 --- a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx @@ -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(); + }); }); diff --git a/superset-frontend/src/components/ListView/CardSortSelect.tsx b/superset-frontend/src/components/ListView/CardSortSelect.tsx new file mode 100644 index 0000000000000..1dbad37ff9ef5 --- /dev/null +++ b/superset-frontend/src/components/ListView/CardSortSelect.tsx @@ -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> + ); +}; diff --git a/superset-frontend/src/components/ListView/Filters.tsx b/superset-frontend/src/components/ListView/Filters.tsx index a27a1d6baa623..a20503194d7e6 100644 --- a/superset-frontend/src/components/ListView/Filters.tsx +++ b/superset-frontend/src/components/ListView/Filters.tsx @@ -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 - key={id} - name={id} Header={Header} - selects={selects} emptyLabel={unfilteredLabel} - initialValue={initialValue} fetchSelects={fetchSelects} - paginate={paginate} + initialValue={initialValue} + key={id} + name={id} 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)} /> ); diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 1a8313b684692..8a50c26136b41 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -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 && ( diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 8a8b4f77e6f70..1161a888f0521 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -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; diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts index 89cd7a3c932e4..026d9e7a13ff3 100644 --- a/superset-frontend/src/components/ListView/utils.ts +++ b/superset-frontend/src/components/ListView/utils.ts @@ -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', + }), +}; diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 785a238bd8e30..d1afb788f40da 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -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} diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index dc4194e1c2f3b..bdd4936cc70c8 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -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}