You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/08/14 22:08:04 UTC

[incubator-superset] branch master updated: feat: sort card view by Alphabetical, Recently Modified, and Least Recently Modified (#10601)

This is an automated email from the ASF dual-hosted git repository.

tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 03a62f1  feat: sort card view by Alphabetical, Recently Modified, and Least Recently Modified (#10601)
03a62f1 is described below

commit 03a62f15d80ed323c4647a98e611b13f8eea4f66
Author: Lily Kuang <li...@preset.io>
AuthorDate: Fri Aug 14 15:07:37 2020 -0700

    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 +-
 .../views/CRUD/dashboard/DashboardList_spec.jsx    |  20 +++-
 .../src/components/ListView/CardSortSelect.tsx     | 114 +++++++++++++++++++++
 .../src/components/ListView/Filters.tsx            |  49 +++------
 .../src/components/ListView/ListView.tsx           |  19 +++-
 superset-frontend/src/components/ListView/types.ts |   7 ++
 superset-frontend/src/components/ListView/utils.ts |  19 +++-
 .../src/views/CRUD/chart/ChartList.tsx             |  22 ++++
 .../src/views/CRUD/dashboard/DashboardList.tsx     |  22 ++++
 10 files changed, 293 insertions(+), 40 deletions(-)

diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index 8625f1d..fee729e 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 2083b4a..213ac39 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 eef4ca0..37d5ca2 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 0000000..1dbad37
--- /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 a27a1d6..a205031 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 1a8313b..8a50c26 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 8a8b4f7..1161a88 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 89cd7a3..026d9e7 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 785a238..d1afb78 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 dc4194e..bdd4936 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}