diff --git a/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx
index 80c356209b6..1fc7089e0c6 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/annotation/AnnotationList_spec.jsx
@@ -23,9 +23,13 @@ import fetchMock from 'fetch-mock';
import { styledMount as mount } from 'spec/helpers/theming';
import AnnotationList from 'src/views/CRUD/annotation/AnnotationList';
-import SubMenu from 'src/components/Menu/SubMenu';
+import DeleteModal from 'src/components/DeleteModal';
+import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import ListView from 'src/components/ListView';
+import SubMenu from 'src/components/Menu/SubMenu';
+
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { act } from 'react-dom/test-utils';
// store needed for withToasts(AnnotationList)
const mockStore = configureStore([thunk]);
@@ -34,7 +38,9 @@ const store = mockStore({});
const annotationsEndpoint = 'glob:*/api/v1/annotation_layer/*/annotation*';
const annotationLayerEndpoint = 'glob:*/api/v1/annotation_layer/*';
-const mockannotation = [...new Array(3)].map((_, i) => ({
+fetchMock.delete(annotationsEndpoint, {});
+
+const mockannotations = [...new Array(3)].map((_, i) => ({
changed_on_delta_humanized: `${i} day(s) ago`,
created_by: {
first_name: `user`,
@@ -53,7 +59,7 @@ const mockannotation = [...new Array(3)].map((_, i) => ({
fetchMock.get(annotationsEndpoint, {
ids: [2, 0, 1],
- result: mockannotation,
+ result: mockannotations,
count: 3,
});
@@ -110,4 +116,44 @@ describe('AnnotationList', () => {
`"http://localhost/api/v1/annotation_layer/1/annotation/?q=(order_column:short_descr,order_direction:desc,page:0,page_size:25)"`,
);
});
+
+ it('renders a DeleteModal', () => {
+ expect(wrapper.find(DeleteModal)).toExist();
+ });
+
+ it('deletes', async () => {
+ act(() => {
+ wrapper.find('[data-test="delete-action"]').first().props().onClick();
+ });
+ await waitForComponentToPaint(wrapper);
+
+ expect(
+ wrapper.find(DeleteModal).first().props().description,
+ ).toMatchInlineSnapshot(
+ `"Are you sure you want to delete annotation 0 label?"`,
+ );
+
+ act(() => {
+ wrapper
+ .find('#delete')
+ .first()
+ .props()
+ .onChange({ target: { value: 'DELETE' } });
+ });
+ await waitForComponentToPaint(wrapper);
+ act(() => {
+ wrapper.find('button').last().props().onClick();
+ });
+ });
+
+ it('shows/hides bulk actions when bulk actions is clicked', async () => {
+ const button = wrapper.find('[data-test="annotation-bulk-select"]').first();
+ act(() => {
+ button.props().onClick();
+ });
+ await waitForComponentToPaint(wrapper);
+ expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
+ mockannotations.length + 1, // 1 for each row and 1 for select all
+ );
+ });
});
diff --git a/superset-frontend/src/common/components/common.stories.tsx b/superset-frontend/src/common/components/common.stories.tsx
index 19f597ea5b1..7bdae996495 100644
--- a/superset-frontend/src/common/components/common.stories.tsx
+++ b/superset-frontend/src/common/components/common.stories.tsx
@@ -27,6 +27,10 @@ import AntdTooltip from './Tooltip';
import { Menu } from '.';
import { Dropdown } from './Dropdown';
import InfoTooltip from './InfoTooltip';
+import {
+ DatePicker as AntdDatePicker,
+ RangePicker as AntdRangePicker,
+} from './DatePicker';
export default {
title: 'Common Components',
@@ -224,3 +228,12 @@ StyledInfoTooltip.argTypes = {
},
},
};
+
+export const DatePicker = () => ;
+export const DateRangePicker = () => (
+
+);
diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx
index cc6914d484b..00f3c85d073 100644
--- a/superset-frontend/src/components/Menu/SubMenu.tsx
+++ b/superset-frontend/src/components/Menu/SubMenu.tsx
@@ -86,6 +86,7 @@ type MenuChild = {
export interface ButtonProps {
name: ReactNode;
onClick: OnClickHandler;
+ 'data-test'?: string;
buttonStyle:
| 'primary'
| 'secondary'
@@ -159,6 +160,7 @@ const SubMenu: React.FunctionComponent = props => {
key={`${i}`}
buttonStyle={btn.buttonStyle}
onClick={btn.onClick}
+ data-test={btn['data-test']}
>
{btn.name}
diff --git a/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx b/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx
index ee9b623cd76..dba16e34659 100644
--- a/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx
+++ b/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx
@@ -20,15 +20,20 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { useParams, Link, useHistory } from 'react-router-dom';
import { t, styled, SupersetClient } from '@superset-ui/core';
-
import moment from 'moment';
+import rison from 'rison';
+
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
-import ListView from 'src/components/ListView';
+import Button from 'src/components/Button';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import DeleteModal from 'src/components/DeleteModal';
+import ListView, { ListViewProps } from 'src/components/ListView';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import getClientErrorObject from 'src/utils/getClientErrorObject';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import { IconName } from 'src/components/Icon';
import { useListViewResource } from 'src/views/CRUD/hooks';
+import { createErrorHandler } from 'src/views/CRUD/utils';
import { AnnotationObject } from './types';
import AnnotationModal from './AnnotationModal';
@@ -37,18 +42,24 @@ const PAGE_SIZE = 25;
interface AnnotationListProps {
addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
}
-function AnnotationList({ addDangerToast }: AnnotationListProps) {
+function AnnotationList({
+ addDangerToast,
+ addSuccessToast,
+}: AnnotationListProps) {
const { annotationLayerId }: any = useParams();
const {
state: {
loading,
resourceCount: annotationsCount,
resourceCollection: annotations,
+ bulkSelectEnabled,
},
fetchData,
refreshData,
+ toggleBulkSelect,
} = useListViewResource(
`annotation_layer/${annotationLayerId}/annotation`,
t('annotation'),
@@ -63,8 +74,11 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
currentAnnotation,
setCurrentAnnotation,
] = useState(null);
-
- const handleAnnotationEdit = (annotation: AnnotationObject) => {
+ const [
+ annotationCurrentlyDeleting,
+ setAnnotationCurrentlyDeleting,
+ ] = useState(null);
+ const handleAnnotationEdit = (annotation: AnnotationObject | null) => {
setCurrentAnnotation(annotation);
setAnnotationModalOpen(true);
};
@@ -85,7 +99,44 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
[annotationLayerId],
);
- // get the owners of this slice
+ const handleAnnotationDelete = ({ id, short_descr }: AnnotationObject) => {
+ SupersetClient.delete({
+ endpoint: `/api/v1/annotation_layer/${annotationLayerId}/annotation/${id}`,
+ }).then(
+ () => {
+ refreshData();
+ setAnnotationCurrentlyDeleting(null);
+ addSuccessToast(t('Deleted: %s', short_descr));
+ },
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t('There was an issue deleting %s: %s', short_descr, errMsg),
+ ),
+ ),
+ );
+ };
+
+ const handleBulkAnnotationsDelete = (
+ annotationsToDelete: AnnotationObject[],
+ ) => {
+ SupersetClient.delete({
+ endpoint: `/api/v1/annotation_layer/${annotationLayerId}/annotation/?q=${rison.encode(
+ annotationsToDelete.map(({ id }) => id),
+ )}`,
+ }).then(
+ ({ json = {} }) => {
+ refreshData();
+ addSuccessToast(json.message);
+ },
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t('There was an issue deleting the selected annotations: %s', errMsg),
+ ),
+ ),
+ );
+ };
+
+ // get the Annotation Layer
useEffect(() => {
fetchAnnotationLayer();
}, [fetchAnnotationLayer]);
@@ -122,7 +173,7 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleAnnotationEdit(original);
- const handleDelete = () => {}; // openDatabaseDeleteModal(original);
+ const handleDelete = () => setAnnotationCurrentlyDeleting(original);
const actions = [
{
label: 'edit-action',
@@ -159,11 +210,17 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
),
buttonStyle: 'primary',
onClick: () => {
- setCurrentAnnotation(null);
- setAnnotationModalOpen(true);
+ handleAnnotationEdit(null);
},
});
+ subMenuButtons.push({
+ name: t('Bulk Select'),
+ onClick: toggleBulkSelect,
+ buttonStyle: 'secondary',
+ 'data-test': 'annotation-bulk-select',
+ });
+
const StyledHeader = styled.div`
display: flex;
flex-direction: row;
@@ -186,6 +243,24 @@ function AnnotationList({ addDangerToast }: AnnotationListProps) {
hasHistory = false;
}
+ const EmptyStateButton = (
+
+ );
+
+ const emptyState = {
+ message: t('No annotation yet'),
+ slot: EmptyStateButton,
+ };
+
return (
<>
setAnnotationModalOpen(false)}
/>
-
- className="css-templates-list-view"
- columns={columns}
- count={annotationsCount}
- data={annotations}
- fetchData={fetchData}
- initialSort={initialSort}
- loading={loading}
- pageSize={PAGE_SIZE}
- />
+ {annotationCurrentlyDeleting && (
+ {
+ if (annotationCurrentlyDeleting) {
+ handleAnnotationDelete(annotationCurrentlyDeleting);
+ }
+ }}
+ onHide={() => setAnnotationCurrentlyDeleting(null)}
+ open
+ title={t('Delete Annotation?')}
+ />
+ )}
+
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = [
+ {
+ key: 'delete',
+ name: t('Delete'),
+ onSelect: confirmDelete,
+ type: 'danger',
+ },
+ ];
+
+ return (
+
+ className="annotations-list-view"
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ columns={columns}
+ count={annotationsCount}
+ data={annotations}
+ disableBulkSelect={toggleBulkSelect}
+ emptyState={emptyState}
+ fetchData={fetchData}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ />
+ );
+ }}
+
>
);
}
diff --git a/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx b/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx
index ddd19318891..2befe09f097 100644
--- a/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx
+++ b/superset-frontend/src/views/CRUD/annotation/AnnotationModal.tsx
@@ -287,9 +287,9 @@ const AnnotationModal: FunctionComponent = ({
*