Merge pull request #8780 from mabashian/7879-notif-copy-search

Add support for notification template copy and advanced search

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-12-16 20:39:52 +00:00
committed by GitHub
5 changed files with 113 additions and 12 deletions

View File

@@ -34,7 +34,7 @@ function CopyButton({
<> <>
<Tooltip content={helperText.tooltip} position="top"> <Tooltip content={helperText.tooltip} position="top">
<Button <Button
isDisabled={isDisabled} isDisabled={isLoading || isDisabled}
aria-label={i18n._(t`Copy`)} aria-label={i18n._(t`Copy`)}
variant="plain" variant="plain"
onClick={copyItemToAPI} onClick={copyItemToAPI}

View File

@@ -187,7 +187,7 @@ function NotificationList({
key: 'description__icontains', key: 'description__icontains',
}, },
{ {
name: i18n._(t`Type`), name: i18n._(t`Notification type`),
key: 'or__notification_type', key: 'or__notification_type',
options: [ options: [
['email', i18n._(t`Email`)], ['email', i18n._(t`Email`)],

View File

@@ -29,27 +29,41 @@ function NotificationTemplatesList({ i18n }) {
const addUrl = `${match.url}/add`; const addUrl = `${match.url}/add`;
const { const {
result: { templates, count, actions }, result: {
templates,
count,
actions,
relatedSearchableKeys,
searchableKeys,
},
error: contentError, error: contentError,
isLoading: isTemplatesLoading, isLoading: isTemplatesLoading,
request: fetchTemplates, request: fetchTemplates,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
const responses = await Promise.all([ const [response, actionsResponse] = await Promise.all([
NotificationTemplatesAPI.read(params), NotificationTemplatesAPI.read(params),
NotificationTemplatesAPI.readOptions(), NotificationTemplatesAPI.readOptions(),
]); ]);
return { return {
templates: responses[0].data.results, templates: response.data.results,
count: responses[0].data.count, count: response.data.count,
actions: responses[1].data.actions, actions: actionsResponse.data.actions,
relatedSearchableKeys: (
actionsResponse.data?.related_search_fields || []
).map(val => val.slice(0, -8)),
searchableKeys: Object.keys(
actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
}; };
}, [location]), }, [location]),
{ {
templates: [], templates: [],
count: 0, count: 0,
actions: {}, actions: {},
relatedSearchableKeys: [],
searchableKeys: [],
} }
); );
@@ -109,7 +123,7 @@ function NotificationTemplatesList({ i18n }) {
key: 'description__icontains', key: 'description__icontains',
}, },
{ {
name: i18n._(t`Type`), name: i18n._(t`Notification type`),
key: 'or__notification_type', key: 'or__notification_type',
options: [ options: [
['email', i18n._(t`Email`)], ['email', i18n._(t`Email`)],
@@ -125,14 +139,16 @@ function NotificationTemplatesList({ i18n }) {
], ],
}, },
{ {
name: i18n._(t`Created By (Username)`), name: i18n._(t`Created by (username)`),
key: 'created_by__username__icontains', key: 'created_by__username__icontains',
}, },
{ {
name: i18n._(t`Modified By (Username)`), name: i18n._(t`Modified by (username)`),
key: 'modified_by__username__icontains', key: 'modified_by__username__icontains',
}, },
]} ]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSortColumns={[ toolbarSortColumns={[
{ {
name: i18n._(t`Name`), name: i18n._(t`Name`),
@@ -166,6 +182,7 @@ function NotificationTemplatesList({ i18n }) {
renderItem={template => ( renderItem={template => (
<NotificationTemplateListItem <NotificationTemplateListItem
key={template.id} key={template.id}
fetchTemplates={fetchTemplates}
template={template} template={template}
detailUrl={`${match.url}/${template.id}`} detailUrl={`${match.url}/${template.id}`}
isSelected={selected.some(row => row.id === template.id)} isSelected={selected.some(row => row.id === template.id)}

View File

@@ -14,9 +14,11 @@ import {
Tooltip, Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons';
import { timeOfDay } from '../../../util/dates';
import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api'; import { NotificationTemplatesAPI, NotificationsAPI } from '../../../api';
import DataListCell from '../../../components/DataListCell'; import DataListCell from '../../../components/DataListCell';
import StatusLabel from '../../../components/StatusLabel'; import StatusLabel from '../../../components/StatusLabel';
import CopyButton from '../../../components/CopyButton';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { NOTIFICATION_TYPES } from '../constants'; import { NOTIFICATION_TYPES } from '../constants';
@@ -24,7 +26,7 @@ const DataListAction = styled(_DataListAction)`
align-items: center; align-items: center;
display: grid; display: grid;
grid-gap: 16px; grid-gap: 16px;
grid-template-columns: 40px 40px; grid-template-columns: repeat(3, 40px);
`; `;
const NUM_RETRIES = 25; const NUM_RETRIES = 25;
@@ -33,6 +35,7 @@ const RETRY_TIMEOUT = 5000;
function NotificationTemplateListItem({ function NotificationTemplateListItem({
template, template,
detailUrl, detailUrl,
fetchTemplates,
isSelected, isSelected,
onSelect, onSelect,
i18n, i18n,
@@ -42,6 +45,22 @@ function NotificationTemplateListItem({
? recentNotifications[0]?.status ? recentNotifications[0]?.status
: null; : null;
const [status, setStatus] = useState(latestStatus); const [status, setStatus] = useState(latestStatus);
const [isCopyDisabled, setIsCopyDisabled] = useState(false);
const copyTemplate = useCallback(async () => {
await NotificationTemplatesAPI.copy(template.id, {
name: `${template.name} @ ${timeOfDay()}`,
});
await fetchTemplates();
}, [template.id, template.name, fetchTemplates]);
const handleCopyStart = useCallback(() => {
setIsCopyDisabled(true);
}, []);
const handleCopyFinish = useCallback(() => {
setIsCopyDisabled(false);
}, []);
useEffect(() => { useEffect(() => {
setStatus(latestStatus); setStatus(latestStatus);
@@ -114,7 +133,7 @@ function NotificationTemplateListItem({
aria-label={i18n._(t`Test Notification`)} aria-label={i18n._(t`Test Notification`)}
variant="plain" variant="plain"
onClick={sendTestNotification} onClick={sendTestNotification}
disabled={isLoading} isDisabled={isLoading || status === 'running'}
> >
<BellIcon /> <BellIcon />
</Button> </Button>
@@ -136,6 +155,18 @@ function NotificationTemplateListItem({
) : ( ) : (
<div /> <div />
)} )}
{template.summary_fields.user_capabilities.copy && (
<CopyButton
copyItem={copyTemplate}
isCopyDisabled={isCopyDisabled}
onCopyStart={handleCopyStart}
onCopyFinish={handleCopyFinish}
helperText={{
tooltip: i18n._(t`Copy Notification Template`),
errorMessage: i18n._(t`Failed to copy template.`),
}}
/>
)}
</DataListAction> </DataListAction>
</DataListItemRow> </DataListItemRow>
</DataListItem> </DataListItem>

View File

@@ -13,6 +13,7 @@ const template = {
summary_fields: { summary_fields: {
user_capabilities: { user_capabilities: {
edit: true, edit: true,
copy: true,
}, },
recent_notifications: [ recent_notifications: [
{ {
@@ -63,4 +64,56 @@ describe('<NotificationTemplateListItem />', () => {
.text() .text()
).toEqual('Running'); ).toEqual('Running');
}); });
test('should call api to copy inventory', async () => {
NotificationTemplatesAPI.copy.mockResolvedValue();
const wrapper = mountWithContexts(
<NotificationTemplateListItem
template={template}
detailUrl="/notification_templates/3/detail"
/>
);
await act(async () =>
wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
);
expect(NotificationTemplatesAPI.copy).toHaveBeenCalled();
jest.clearAllMocks();
});
test('should render proper alert modal on copy error', async () => {
NotificationTemplatesAPI.copy.mockRejectedValue(new Error());
const wrapper = mountWithContexts(
<NotificationTemplateListItem
template={template}
detailUrl="/notification_templates/3/detail"
/>
);
await act(async () =>
wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
);
wrapper.update();
expect(wrapper.find('Modal').prop('isOpen')).toBe(true);
jest.clearAllMocks();
});
test('should not render copy button', async () => {
const wrapper = mountWithContexts(
<NotificationTemplateListItem
template={{
...template,
summary_fields: {
user_capabilities: {
copy: false,
edit: false,
},
},
}}
detailUrl="/notification_templates/3/detail"
/>
);
expect(wrapper.find('CopyButton').length).toBe(0);
});
}); });