Merge pull request #7803 from mabashian/6425-approval-notif

Adds support for toggling approval notifications on orgs and wfjts

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-08-20 20:23:50 +00:00
committed by GitHub
10 changed files with 210 additions and 30 deletions

View File

@@ -87,6 +87,13 @@ const NotificationsMixin = parent =>
notificationId, notificationId,
notificationType notificationType
) { ) {
if (notificationType === 'approvals') {
return this.associateNotificationTemplatesApprovals(
resourceId,
notificationId
);
}
if (notificationType === 'started') { if (notificationType === 'started') {
return this.associateNotificationTemplatesStarted( return this.associateNotificationTemplatesStarted(
resourceId, resourceId,
@@ -126,6 +133,13 @@ const NotificationsMixin = parent =>
notificationId, notificationId,
notificationType notificationType
) { ) {
if (notificationType === 'approvals') {
return this.disassociateNotificationTemplatesApprovals(
resourceId,
notificationId
);
}
if (notificationType === 'started') { if (notificationType === 'started') {
return this.disassociateNotificationTemplatesStarted( return this.disassociateNotificationTemplatesStarted(
resourceId, resourceId,

View File

@@ -27,6 +27,27 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
createUser(id, data) { createUser(id, data) {
return this.http.post(`${this.baseUrl}${id}/users/`, data); return this.http.post(`${this.baseUrl}${id}/users/`, data);
} }
readNotificationTemplatesApprovals(id, params) {
return this.http.get(
`${this.baseUrl}${id}/notification_templates_approvals/`,
{ params }
);
}
associateNotificationTemplatesApprovals(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
{ id: notificationId }
);
}
disassociateNotificationTemplatesApprovals(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
{ id: notificationId, disassociate: true }
);
}
} }
export default Organizations; export default Organizations;

View File

@@ -69,6 +69,27 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) {
destroySurvey(id) { destroySurvey(id) {
return this.http.delete(`${this.baseUrl}${id}/survey_spec/`); return this.http.delete(`${this.baseUrl}${id}/survey_spec/`);
} }
readNotificationTemplatesApprovals(id, params) {
return this.http.get(
`${this.baseUrl}${id}/notification_templates_approvals/`,
{ params }
);
}
associateNotificationTemplatesApprovals(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
{ id: notificationId }
);
}
disassociateNotificationTemplatesApprovals(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_approvals/`,
{ id: notificationId, disassociate: true }
);
}
} }
export default WorkflowJobTemplates; export default WorkflowJobTemplates;

View File

@@ -17,9 +17,15 @@ const QS_CONFIG = getQSConfig('notification', {
order_by: 'name', order_by: 'name',
}); });
function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { function NotificationList({
apiModel,
canToggleNotifications,
id,
i18n,
showApprovalsToggle,
}) {
const location = useLocation(); const location = useLocation();
const [isToggleLoading, setIsToggleLoading] = useState(false); const [loadingToggleIds, setLoadingToggleIds] = useState([]);
const [toggleError, setToggleError] = useState(null); const [toggleError, setToggleError] = useState(null);
const { const {
@@ -27,6 +33,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
result: { result: {
notifications, notifications,
itemCount, itemCount,
approvalsTemplateIds,
startedTemplateIds, startedTemplateIds,
successTemplateIds, successTemplateIds,
errorTemplateIds, errorTemplateIds,
@@ -71,7 +78,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
apiModel.readNotificationTemplatesError(id, idMatchParams), apiModel.readNotificationTemplatesError(id, idMatchParams),
]); ]);
return { const rtnObj = {
notifications: notificationsResults, notifications: notificationsResults,
itemCount: notificationsCount, itemCount: notificationsCount,
startedTemplateIds: startedTemplates.results.map(st => st.id), startedTemplateIds: startedTemplates.results.map(st => st.id),
@@ -85,10 +92,27 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
actionsResponse.data.actions?.GET || {} actionsResponse.data.actions?.GET || {}
).filter(key => actionsResponse.data.actions?.GET[key].filterable), ).filter(key => actionsResponse.data.actions?.GET[key].filterable),
}; };
}, [apiModel, id, location]),
if (showApprovalsToggle) {
const {
data: approvalsTemplates,
} = await apiModel.readNotificationTemplatesApprovals(
id,
idMatchParams
);
rtnObj.approvalsTemplateIds = approvalsTemplates.results.map(
st => st.id
);
} else {
rtnObj.approvalsTemplateIds = [];
}
return rtnObj;
}, [apiModel, id, location, showApprovalsToggle]),
{ {
notifications: [], notifications: [],
itemCount: 0, itemCount: 0,
approvalsTemplateIds: [],
startedTemplateIds: [], startedTemplateIds: [],
successTemplateIds: [], successTemplateIds: [],
errorTemplateIds: [], errorTemplateIds: [],
@@ -107,7 +131,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
isCurrentlyOn, isCurrentlyOn,
status status
) => { ) => {
setIsToggleLoading(true); setLoadingToggleIds(loadingToggleIds.concat([notificationId]));
try { try {
if (isCurrentlyOn) { if (isCurrentlyOn) {
await apiModel.disassociateNotificationTemplate( await apiModel.disassociateNotificationTemplate(
@@ -137,7 +161,9 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
} catch (err) { } catch (err) {
setToggleError(err); setToggleError(err);
} finally { } finally {
setIsToggleLoading(false); setLoadingToggleIds(
loadingToggleIds.filter(item => item !== notificationId)
);
} }
}; };
@@ -194,12 +220,17 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
key={notification.id} key={notification.id}
notification={notification} notification={notification}
detailUrl={`/notifications/${notification.id}`} detailUrl={`/notifications/${notification.id}`}
canToggleNotifications={canToggleNotifications && !isToggleLoading} canToggleNotifications={
canToggleNotifications &&
!loadingToggleIds.includes(notification.id)
}
toggleNotification={handleNotificationToggle} toggleNotification={handleNotificationToggle}
approvalsTurnedOn={approvalsTemplateIds.includes(notification.id)}
errorTurnedOn={errorTemplateIds.includes(notification.id)} errorTurnedOn={errorTemplateIds.includes(notification.id)}
startedTurnedOn={startedTemplateIds.includes(notification.id)} startedTurnedOn={startedTemplateIds.includes(notification.id)}
successTurnedOn={successTemplateIds.includes(notification.id)} successTurnedOn={successTemplateIds.includes(notification.id)}
typeLabels={typeLabels} typeLabels={typeLabels}
showApprovalsToggle={showApprovalsToggle}
/> />
)} )}
/> />
@@ -207,7 +238,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
<AlertModal <AlertModal
variant="error" variant="error"
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
isOpen={!isToggleLoading} isOpen={loadingToggleIds.length === 0}
onClose={() => setToggleError(null)} onClose={() => setToggleError(null)}
> >
{i18n._(t`Failed to toggle notification.`)} {i18n._(t`Failed to toggle notification.`)}
@@ -222,6 +253,11 @@ NotificationList.propTypes = {
apiModel: shape({}).isRequired, apiModel: shape({}).isRequired,
id: number.isRequired, id: number.isRequired,
canToggleNotifications: bool.isRequired, canToggleNotifications: bool.isRequired,
showApprovalsToggle: bool,
};
NotificationList.defaultProps = {
showApprovalsToggle: false,
}; };
export default withI18n()(NotificationList); export default withI18n()(NotificationList);

View File

@@ -17,25 +17,25 @@ const DataListAction = styled(_DataListAction)`
align-items: center; align-items: center;
display: grid; display: grid;
grid-gap: 16px; grid-gap: 16px;
grid-template-columns: repeat(3, max-content); grid-template-columns: ${props => `repeat(${props.columns}, max-content)`};
`; `;
const Label = styled.b` const Label = styled.b`
margin-right: 20px; margin-right: 20px;
`; `;
function NotificationListItem(props) { function NotificationListItem({
const { canToggleNotifications,
canToggleNotifications, notification,
notification, detailUrl,
detailUrl, approvalsTurnedOn,
startedTurnedOn, startedTurnedOn,
successTurnedOn, successTurnedOn,
errorTurnedOn, errorTurnedOn,
toggleNotification, toggleNotification,
i18n, i18n,
typeLabels, typeLabels,
} = props; showApprovalsToggle,
}) {
return ( return (
<DataListItem <DataListItem
aria-labelledby={`items-list-item-${notification.id}`} aria-labelledby={`items-list-item-${notification.id}`}
@@ -66,7 +66,25 @@ function NotificationListItem(props) {
aria-label="actions" aria-label="actions"
aria-labelledby={`items-list-item-${notification.id}`} aria-labelledby={`items-list-item-${notification.id}`}
id={`items-list-item-${notification.id}`} id={`items-list-item-${notification.id}`}
columns={showApprovalsToggle ? 4 : 3}
> >
{showApprovalsToggle && (
<Switch
id={`notification-${notification.id}-approvals-toggle`}
label={i18n._(t`Approval`)}
labelOff={i18n._(t`Approval`)}
isChecked={approvalsTurnedOn}
isDisabled={!canToggleNotifications}
onChange={() =>
toggleNotification(
notification.id,
approvalsTurnedOn,
'approvals'
)
}
aria-label={i18n._(t`Toggle notification approvals`)}
/>
)}
<Switch <Switch
id={`notification-${notification.id}-started-toggle`} id={`notification-${notification.id}-started-toggle`}
label={i18n._(t`Start`)} label={i18n._(t`Start`)}
@@ -114,17 +132,21 @@ NotificationListItem.propTypes = {
}).isRequired, }).isRequired,
canToggleNotifications: bool.isRequired, canToggleNotifications: bool.isRequired,
detailUrl: string.isRequired, detailUrl: string.isRequired,
approvalsTurnedOn: bool,
errorTurnedOn: bool, errorTurnedOn: bool,
startedTurnedOn: bool, startedTurnedOn: bool,
successTurnedOn: bool, successTurnedOn: bool,
toggleNotification: func.isRequired, toggleNotification: func.isRequired,
typeLabels: shape().isRequired, typeLabels: shape().isRequired,
showApprovalsToggle: bool,
}; };
NotificationListItem.defaultProps = { NotificationListItem.defaultProps = {
approvalsTurnedOn: false,
errorTurnedOn: false, errorTurnedOn: false,
startedTurnedOn: false, startedTurnedOn: false,
successTurnedOn: false, successTurnedOn: false,
showApprovalsToggle: false,
}; };
export default withI18n()(NotificationListItem); export default withI18n()(NotificationListItem);

View File

@@ -39,6 +39,21 @@ describe('<NotificationListItem canToggleNotifications />', () => {
/> />
); );
expect(wrapper.find('NotificationListItem')).toMatchSnapshot(); expect(wrapper.find('NotificationListItem')).toMatchSnapshot();
expect(wrapper.find('Switch').length).toBe(3);
});
test('shows approvals toggle when configured', () => {
wrapper = mountWithContexts(
<NotificationListItem
notification={mockNotif}
toggleNotification={toggleNotification}
detailUrl="/foo"
canToggleNotifications
typeLabels={typeLabels}
showApprovalsToggle
/>
);
expect(wrapper.find('Switch').length).toBe(4);
}); });
test('displays correct label in correct column', () => { test('displays correct label in correct column', () => {
@@ -58,7 +73,46 @@ describe('<NotificationListItem canToggleNotifications />', () => {
expect(typeCell.text()).toContain('Slack'); expect(typeCell.text()).toContain('Slack');
}); });
test('handles start click when toggle is on', () => { test('handles approvals click when toggle is on', () => {
wrapper = mountWithContexts(
<NotificationListItem
notification={mockNotif}
approvalsTurnedOn
toggleNotification={toggleNotification}
detailUrl="/foo"
canToggleNotifications
typeLabels={typeLabels}
showApprovalsToggle
/>
);
wrapper
.find('Switch[aria-label="Toggle notification approvals"]')
.first()
.find('input')
.simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'approvals');
});
test('handles approvals click when toggle is off', () => {
wrapper = mountWithContexts(
<NotificationListItem
notification={mockNotif}
approvalsTurnedOn={false}
toggleNotification={toggleNotification}
detailUrl="/foo"
canToggleNotifications
typeLabels={typeLabels}
showApprovalsToggle
/>
);
wrapper
.find('Switch[aria-label="Toggle notification approvals"]')
.find('input')
.simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'approvals');
});
test('handles started click when toggle is on', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<NotificationListItem <NotificationListItem
notification={mockNotif} notification={mockNotif}
@@ -70,14 +124,13 @@ describe('<NotificationListItem canToggleNotifications />', () => {
/> />
); );
wrapper wrapper
.find('Switch') .find('Switch[aria-label="Toggle notification start"]')
.first()
.find('input') .find('input')
.simulate('change'); .simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'started'); expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'started');
}); });
test('handles start click when toggle is off', () => { test('handles started click when toggle is off', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<NotificationListItem <NotificationListItem
notification={mockNotif} notification={mockNotif}
@@ -95,7 +148,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'started'); expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'started');
}); });
test('handles error click when toggle is on', () => { test('handles success click when toggle is on', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<NotificationListItem <NotificationListItem
notification={mockNotif} notification={mockNotif}
@@ -113,7 +166,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success'); expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
}); });
test('handles error click when toggle is off', () => { test('handles success click when toggle is off', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<NotificationListItem <NotificationListItem
notification={mockNotif} notification={mockNotif}

View File

@@ -2,6 +2,7 @@
exports[`<NotificationListItem canToggleNotifications /> initially renders succesfully and displays correct label 1`] = ` exports[`<NotificationListItem canToggleNotifications /> initially renders succesfully and displays correct label 1`] = `
<NotificationListItem <NotificationListItem
approvalsTurnedOn={false}
canToggleNotifications={true} canToggleNotifications={true}
detailUrl="/foo" detailUrl="/foo"
errorTurnedOn={false} errorTurnedOn={false}
@@ -13,6 +14,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
"notification_type": "slack", "notification_type": "slack",
} }
} }
showApprovalsToggle={false}
startedTurnedOn={false} startedTurnedOn={false}
successTurnedOn={false} successTurnedOn={false}
toggleNotification={[MockFunction]} toggleNotification={[MockFunction]}
@@ -215,6 +217,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
<Styled(DataListAction) <Styled(DataListAction)
aria-label="actions" aria-label="actions"
aria-labelledby="items-list-item-9000" aria-labelledby="items-list-item-9000"
columns={3}
id="items-list-item-9000" id="items-list-item-9000"
key=".1" key=".1"
rowid="items-list-item-9000" rowid="items-list-item-9000"
@@ -222,6 +225,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
<StyledComponent <StyledComponent
aria-label="actions" aria-label="actions"
aria-labelledby="items-list-item-9000" aria-labelledby="items-list-item-9000"
columns={3}
forwardedComponent={ forwardedComponent={
Object { Object {
"$$typeof": Symbol(react.forward_ref), "$$typeof": Symbol(react.forward_ref),
@@ -235,7 +239,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
align-items: center; align-items: center;
display: grid; display: grid;
grid-gap: 16px; grid-gap: 16px;
grid-template-columns: repeat(3, max-content); grid-template-columns: ",
[Function],
";
", ",
], ],
}, },
@@ -257,11 +263,13 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
aria-label="actions" aria-label="actions"
aria-labelledby="items-list-item-9000" aria-labelledby="items-list-item-9000"
className="sc-bwzfXH llKtln" className="sc-bwzfXH llKtln"
columns={3}
id="items-list-item-9000" id="items-list-item-9000"
rowid="items-list-item-9000" rowid="items-list-item-9000"
> >
<div <div
className="pf-c-data-list__item-action sc-bwzfXH llKtln" className="pf-c-data-list__item-action sc-bwzfXH llKtln"
columns={3}
rowid="items-list-item-9000" rowid="items-list-item-9000"
> >
<Switch <Switch

View File

@@ -268,7 +268,10 @@ describe('<CredentialEdit />', () => {
test('handleCancel returns the user to credential detail', async () => { test('handleCancel returns the user to credential detail', async () => {
await waitForElement(wrapper, 'isLoading', el => el.length === 0); await waitForElement(wrapper, 'isLoading', el => el.length === 0);
wrapper.find('Button[aria-label="Cancel"]').simulate('click'); await act(async () => {
wrapper.find('Button[aria-label="Cancel"]').simulate('click');
});
wrapper.update();
expect(history.location.pathname).toEqual('/credentials/3/details'); expect(history.location.pathname).toEqual('/credentials/3/details');
}); });

View File

@@ -200,6 +200,7 @@ class Organization extends Component {
id={Number(match.params.id)} id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications} canToggleNotifications={canToggleNotifications}
apiModel={OrganizationsAPI} apiModel={OrganizationsAPI}
showApprovalsToggle
/> />
</Route> </Route>
)} )}

View File

@@ -233,6 +233,7 @@ class WorkflowJobTemplate extends Component {
id={Number(match.params.id)} id={Number(match.params.id)}
canToggleNotifications={canToggleNotifications} canToggleNotifications={canToggleNotifications}
apiModel={WorkflowJobTemplatesAPI} apiModel={WorkflowJobTemplatesAPI}
showApprovalsToggle
/> />
</Route> </Route>
)} )}