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: CSS Templates List Actions #11271

Merged
merged 13 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,21 @@ import { styledMount as mount } from 'spec/helpers/theming';
import CssTemplatesList from 'src/views/CRUD/csstemplates/CssTemplatesList';
import SubMenu from 'src/components/Menu/SubMenu';
import ListView from 'src/components/ListView';
// import Filters from 'src/components/ListView/Filters';
import Filters from 'src/components/ListView/Filters';
import DeleteModal from 'src/components/DeleteModal';
import Button from 'src/components/Button';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
// import { act } from 'react-dom/test-utils';
import { act } from 'react-dom/test-utils';

// store needed for withToasts(DatabaseList)
const mockStore = configureStore([thunk]);
const store = mockStore({});

const templatesInfoEndpoint = 'glob:*/api/v1/css_template/_info*';
const templatesEndpoint = 'glob:*/api/v1/css_template/?*';
const templateEndpoint = 'glob:*/api/v1/css_template/*';
const templatesRelatedEndpoint = 'glob:*/api/v1/css_template/related/*';

const mocktemplates = [...new Array(3)].map((_, i) => ({
changed_on_delta_humanized: `${i} day(s) ago`,
Expand All @@ -56,6 +61,16 @@ fetchMock.get(templatesEndpoint, {
templates_count: 3,
});

fetchMock.delete(templateEndpoint, {});
fetchMock.delete(templatesEndpoint, {});

fetchMock.get(templatesRelatedEndpoint, {
created_by: {
count: 0,
result: [],
},
});

describe('CssTemplatesList', () => {
const wrapper = mount(<CssTemplatesList />, { context: { store } });

Expand All @@ -74,4 +89,76 @@ describe('CssTemplatesList', () => {
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toExist();
});

it('fetches templates', () => {
const callsQ = fetchMock.calls(/css_template\/\?q/);
expect(callsQ).toHaveLength(1);
expect(callsQ[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/css_template/?q=(order_column:template_name,order_direction:desc,page:0,page_size:25)"`,
);
});

it('renders Filters', () => {
expect(wrapper.find(Filters)).toExist();
});

it('searches', async () => {
const filtersWrapper = wrapper.find(Filters);
act(() => {
filtersWrapper
.find('[name="template_name"]')
.first()
.props()
.onSubmit('fooo');
});
await waitForComponentToPaint(wrapper);

expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/css_template/?q=(filters:!((col:template_name,opr:ct,value:fooo)),order_column:template_name,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(
`"This action will permanently delete the template."`,
);

act(() => {
wrapper
.find('#delete')
.first()
.props()
.onChange({ target: { value: 'DELETE' } });
});
await waitForComponentToPaint(wrapper);
act(() => {
wrapper.find('button').last().props().onClick();
});

await waitForComponentToPaint(wrapper);

expect(fetchMock.calls(/css_template\/0/, 'DELETE')).toHaveLength(1);
});

it('shows/hides bulk actions when bulk actions is clicked', async () => {
const button = wrapper.find(Button).at(0);
act(() => {
button.props().onClick();
});
await waitForComponentToPaint(wrapper);
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
mocktemplates.length + 1, // 1 for each row and 1 for select all
);
});
});
203 changes: 177 additions & 26 deletions superset-frontend/src/views/CRUD/csstemplates/CssTemplatesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@
*/

import React, { useMemo, useState } from 'react';
import { t } from '@superset-ui/core';
import { t, SupersetClient } from '@superset-ui/core';

import rison from 'rison';
import moment from 'moment';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import DeleteModal from 'src/components/DeleteModal';
import TooltipWrapper from 'src/components/TooltipWrapper';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import { IconName } from 'src/components/Icon';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
// import ListView, { Filters } from 'src/components/ListView';
import ListView from 'src/components/ListView';
import ListView, { ListViewProps, Filters } from 'src/components/ListView';
import CssTemplateModal from './CssTemplateModal';
import { TemplateObject } from './types';

Expand All @@ -46,10 +51,12 @@ function CssTemplatesList({
loading,
resourceCount: templatesCount,
resourceCollection: templates,
bulkSelectEnabled,
},
hasPerm,
fetchData,
refreshData,
toggleBulkSelect,
} = useListViewResource<TemplateObject>(
'css_template',
t('css templates'),
Expand All @@ -67,6 +74,46 @@ function CssTemplatesList({
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');

const [
templateCurrentlyDeleting,
setTemplateCurrentlyDeleting,
] = useState<TemplateObject | null>(null);

const handleTemplateDelete = ({ id, template_name }: TemplateObject) => {
SupersetClient.delete({
endpoint: `/api/v1/css_template/${id}`,
}).then(
() => {
refreshData();
setTemplateCurrentlyDeleting(null);
addSuccessToast(t('Deleted: %s', template_name));
},
createErrorHandler(errMsg =>
addDangerToast(
t('There was an issue deleting %s: %s', template_name, errMsg),
),
),
);
};

const handleBulkTemplateDelete = (templatesToDelete: TemplateObject[]) => {
SupersetClient.delete({
endpoint: `/api/v1/css_template/?q=${rison.encode(
templatesToDelete.map(({ id }) => id),
)}`,
}).then(
({ json = {} }) => {
refreshData();
addSuccessToast(json.message);
},
createErrorHandler(errMsg =>
addDangerToast(
t('There was an issue deleting the selected templates: %s', errMsg),
),
),
);
};

function handleCssTemplateEdit(cssTemplate: TemplateObject) {
setCurrentCssTemplate(cssTemplate);
setCssTemplateModalOpen(true);
Expand All @@ -79,6 +126,36 @@ function CssTemplatesList({
accessor: 'template_name',
Header: t('Name'),
},
{
Cell: ({
row: {
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => {
let name = 'null';

if (changedBy) {
name = `${changedBy.first_name} ${changedBy.last_name}`;
}

return (
<TooltipWrapper
label="allow-run-async-header"
tooltip={t('Last modified by %s', name)}
placement="right"
>
<span>{changedOn}</span>
</TooltipWrapper>
);
},
Header: t('Last Modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
disableSortBy: true,
},
{
Cell: ({
row: {
Expand All @@ -103,6 +180,7 @@ function CssTemplatesList({
Header: t('Created On'),
accessor: 'created_on',
size: 'xl',
disableSortBy: true,
},
{
accessor: 'created_by',
Expand All @@ -116,20 +194,10 @@ function CssTemplatesList({
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
size: 'xl',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
},
}: any) => changedOn,
Header: t('Last Modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleCssTemplateEdit(original);
const handleDelete = () => {}; // openDatabaseDeleteModal(original);
const handleDelete = () => setTemplateCurrentlyDeleting(original);

const actions = [
canEdit
Expand Down Expand Up @@ -167,6 +235,10 @@ function CssTemplatesList({
[canDelete, canCreate],
);

const menuData: SubMenuProps = {
name: t('CSS Templates'),
};

const subMenuButtons: SubMenuProps['buttons'] = [];

if (canCreate) {
Expand All @@ -185,27 +257,106 @@ function CssTemplatesList({
});
}

if (canDelete) {
subMenuButtons.push({
name: t('Bulk Select'),
onClick: toggleBulkSelect,
buttonStyle: 'secondary',
});
}

menuData.buttons = subMenuButtons;

const filters: Filters = useMemo(
() => [
{
Header: t('Created By'),
id: 'created_by',
input: 'select',
operator: 'rel_o_m',
unfilteredLabel: 'All',
fetchSelects: createFetchRelated(
'css_template',
'created_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
errMsg,
),
),
),
paginate: true,
},
{
Header: t('Search'),
id: 'template_name',
input: 'search',
operator: 'ct',
},
],
[],
);

return (
<>
<SubMenu name={t('CSS Templates')} buttons={subMenuButtons} />
<SubMenu {...menuData} />
<CssTemplateModal
addDangerToast={addDangerToast}
cssTemplate={currentCssTemplate}
onCssTemplateAdd={() => refreshData()}
onHide={() => setCssTemplateModalOpen(false)}
show={cssTemplateModalOpen}
/>
<ListView<TemplateObject>
className="css-templates-list-view"
columns={columns}
count={templatesCount}
data={templates}
fetchData={fetchData}
// filters={filters}
initialSort={initialSort}
loading={loading}
pageSize={PAGE_SIZE}
/>
{templateCurrentlyDeleting && (
<DeleteModal
description={t('This action will permanently delete the template.')}
onConfirm={() => {
if (templateCurrentlyDeleting) {
handleTemplateDelete(templateCurrentlyDeleting);
}
}}
onHide={() => setTemplateCurrentlyDeleting(null)}
open
title={t('Delete Template?')}
/>
)}
<ConfirmStatusChange
title={t('Please confirm')}
description={t(
'Are you sure you want to delete the selected templates?',
)}
onConfirm={handleBulkTemplateDelete}
>
{confirmDelete => {
const bulkActions: ListViewProps['bulkActions'] = canDelete
? [
{
key: 'delete',
name: t('Delete'),
onSelect: confirmDelete,
type: 'danger',
},
]
: [];

return (
<ListView<TemplateObject>
className="css-templates-list-view"
columns={columns}
count={templatesCount}
data={templates}
fetchData={fetchData}
filters={filters}
initialSort={initialSort}
loading={loading}
pageSize={PAGE_SIZE}
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
disableBulkSelect={toggleBulkSelect}
/>
);
}}
</ConfirmStatusChange>
</>
);
}
Expand Down
Loading