Skip to content
Draft
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 @@ -211,4 +211,47 @@ describe('playbooks > edit status update', {testIsolation: true}, () => {
});
});
});

describe('status update reminder "Never"', () => {
it('can set the reminder to Never and reconfigure it back to a duration', () => {
// # Visit the selected playbook outline tab
cy.visit(`/playbooks/playbooks/${testPlaybook.id}/outline`);

// * Verify the default reminder is a duration
cy.findAllByTestId('status-update-section').within(() => {
cy.contains('A status update is expected every');
});

// # Open the reminder timer dropdown and select "Never"
cy.findByTestId('status-update-timer').click();
cy.get('.playbook-react-select__option').contains('Never').click();

cy.wait(TWO_SEC);

// # Refresh the page
cy.visit(`/playbooks/playbooks/${testPlaybook.id}/outline`);

// * Verify the text now reads "never expected" and the picker shows "Never"
cy.findAllByTestId('status-update-section').within(() => {
cy.contains('A status update is expected every').should('not.exist');
cy.contains('expected');
cy.findByTestId('status-update-timer').contains('Never');
});

// # Reconfigure back to a duration
cy.findByTestId('status-update-timer').click();
cy.get('.playbook-react-select__option').contains('7 days').click();

cy.wait(TWO_SEC);

// # Refresh the page
cy.visit(`/playbooks/playbooks/${testPlaybook.id}/outline`);

// * Verify the duration phrasing is restored
cy.findAllByTestId('status-update-section').within(() => {
cy.contains('A status update is expected every');
cy.contains('7 days');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ describe('runs > run details page > status update', {testIsolation: true}, () =>
cy.findByTestId('run-statusupdate-section').getStyledComponent('Kebab').click();

cy.findByTestId('dropdownmenu').within(($dropdown) => {
cy.wrap($dropdown).children().should('have.length', 2);
// View all updates, Request update..., and Remove reminder (run has a scheduled reminder)
cy.wrap($dropdown).children().should('have.length', 3);

// # Click on request update
cy.findByText('Request update...').click();
Expand All @@ -171,7 +172,8 @@ describe('runs > run details page > status update', {testIsolation: true}, () =>
// # Click on kebab menu
cy.findByTestId('run-statusupdate-section').getStyledComponent('Kebab').click();
cy.findByTestId('dropdownmenu').within(($dropdown) => {
cy.wrap($dropdown).children().should('have.length', 2);
// View all updates, Request update..., and Remove reminder (run has a scheduled reminder)
cy.wrap($dropdown).children().should('have.length', 3);

// # Click on request update
cy.findByText('Request update...').click();
Expand All @@ -187,6 +189,33 @@ describe('runs > run details page > status update', {testIsolation: true}, () =>
cy.getLastPost().should('not.contain', `${testUser.username} requested a status update for ${testRun.name}.`);
});
});

describe('remove reminder', () => {
it('clears a scheduled reminder from the kebab menu', () => {
// * A reminder is currently scheduled
cy.findByTestId('update-due-date-text').contains('Update due');

// # Open the kebab menu and click "Remove reminder"
cy.findByTestId('run-statusupdate-section').getStyledComponent('Kebab').click();
cy.findByTestId('dropdownmenu').within(() => {
cy.findByText('Remove reminder').click();
});

// # Confirm removal
cy.get('#confirmModalButton').click();

// * The widget no longer expects an update (reminder cleared, no error)
cy.findByTestId('update-due-date-text').contains('Last update');

// # Reopen the kebab menu
cy.findByTestId('run-statusupdate-section').getStyledComponent('Kebab').click();

// * "Remove reminder" is gone now that no reminder is scheduled
cy.findByTestId('dropdownmenu').within(() => {
cy.findByText('Remove reminder').should('not.exist');
});
});
});
});

describe('as viewer', () => {
Expand Down
8 changes: 4 additions & 4 deletions server/api/playbook_runs.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,8 +923,8 @@ func (h *PlaybookRunHandler) updateStatus(playbookRunID, userID string, options
return "message must not be empty", errors.New("message field empty")
}

if options.Reminder <= 0 && !options.FinishRun {
return "the reminder must be set and not 0", errors.New("reminder was 0")
if options.Reminder < 0 && !options.FinishRun {
return "the reminder must not be negative", errors.New("reminder was negative")
}
if options.Reminder < 0 || options.FinishRun {
options.Reminder = 0
Expand Down Expand Up @@ -1247,8 +1247,8 @@ func (h *PlaybookRunHandler) reminderReset(c *Context, w http.ResponseWriter, r
return
}

if payload.NewReminderSeconds <= 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "new_reminder_seconds must be > 0", errors.New("new_reminder_seconds was <= 0"))
if payload.NewReminderSeconds < 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "new_reminder_seconds must be >= 0", errors.New("new_reminder_seconds was negative"))
return
}

Expand Down
4 changes: 2 additions & 2 deletions server/api/playbooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ func (h *PlaybookHandler) createPlaybook(c *Context, w http.ResponseWriter, r *h

playbook.NextRunNumber = 0

if playbook.ReminderTimerDefaultSeconds <= 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook ReminderTimerDefaultSeconds must be > 0", nil)
if playbook.ReminderTimerDefaultSeconds < 0 {
h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook ReminderTimerDefaultSeconds must be >= 0", nil)
return
}

Expand Down
4 changes: 4 additions & 0 deletions server/app/playbook_run_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3922,6 +3922,10 @@ func (s *PlaybookRunServiceImpl) newUpdatePlaybookRunDialog(description, message
introductionText := T("app.user.run.update_status.num_channel", data)

reminderOptions := []*model.PostActionOptions{
{
Text: "Never",
Value: "0",
},
{
Text: "15min",
Value: "900",
Expand Down
7 changes: 7 additions & 0 deletions webapp/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
"GD+JsI": "{count} {count, plural, one {task} other {tasks}} selected",
"GDCpPr": "Recent status update",
"GHxJtB": "Select options...",
"GLVuAA": "Remove the scheduled status update reminder? You can set a new one when posting an update.",
"GPhFZn": "Only the run owner can restart this run",
"GR4us3": "Only admins can rename this playbook.",
"GVpA4Q": "Create New Playbook",
Expand All @@ -209,6 +210,7 @@
"GjCS6U": "Choose a template",
"Gwmqz5": "Request an update",
"GxJAK1": "The playbook you're requesting is private or does not exist.",
"GxYGs2": "Status update reminder removed.",
"GzjoWC": "Failed to update status updates setting",
"H+U7mq": "Join as a participant to restart",
"H7IzRB": "Disable status updates",
Expand Down Expand Up @@ -396,6 +398,7 @@
"WGSprq": "Remove condition",
"WUwxYi": "{name} cleared {property}",
"WqjrUR": "RUN ROLES",
"WrKOe+": "The reminder could not be removed.",
"X/koAN": "Invalid entry: the maximum number of webhooks allowed is 64",
"X2K92H": "Checklist name",
"X5Q310": "Hide details",
Expand Down Expand Up @@ -436,6 +439,7 @@
"a/4SZM": "Done editing",
"a0hBZ0": "Delete metric",
"a2r7Vb": "Private channel",
"a7+DzC": "A status update is <duration></duration> expected. New updates will be posted to <channels>{channelCount, plural, =0 {no channels} one {# channel} other {# channels}}</channels> and <webhooks>{webhookCount, plural, =0 {no outgoing webhooks} one {# outgoing webhook} other {# outgoing webhooks}}</webhooks>.",
"aACJNp": "Run started by {name}",
"aEhjYg": "Outline",
"aWpBzj": "Show more",
Expand Down Expand Up @@ -479,6 +483,7 @@
"dSL9sW": "Confirm enable retrospective",
"dZmYk6": "Successfully duplicated playbook",
"dn57lO": "Add custom attributes to capture additional information about your playbook runs.",
"du1laW": "Never",
"dvhvum": "(Optional) Describe how this playbook should be used",
"dx+O3r": "{name} updated {property} from {oldValue} to {newValue}",
"dxyZg3": "Let me explore for myself",
Expand Down Expand Up @@ -598,12 +603,14 @@
"m/Q4ye": "Rename checklist",
"m4vqJl": "Files",
"m8hzTK": "Last used {time}",
"mBJDw5": "Remove reminder",
"mCrdeS": "Total Playbook Runs",
"mILd++": "The run name should not exceed {maxLength} characters",
"mKWDap": "Insert variable",
"mLrh+0": "No due date",
"mNgqXf": "To unlock this feature:",
"mVpO8u": "Seen this before?",
"md4Qkb": "never",
"mkLeuq": "Broadcast update to selected channels",
"mm5vL8": "Only invited members",
"mw9jVA": "Add a title",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See LICENSE.txt for license information.

import React, {useMemo} from 'react';
import {useIntl} from 'react-intl';

import DateTimeSelector from 'src/components/datetime_selector';

Expand All @@ -20,16 +21,18 @@ interface Props {
}

const UpdateTimer = (props: Props) => {
const {formatMessage} = useIntl();
const makeOption = useMakeOption(Mode.DurationValue);

const defaults = useMemo(() => {
const neverOption: Option = {value: null, label: formatMessage({defaultMessage: 'Never'})};
const options = [
makeOption({hours: 1}),
makeOption({days: 1}),
makeOption({days: 7}),
];

let value: Option | undefined;
let value: Option | undefined = neverOption;
if (props.seconds) {
value = makeOption({seconds: props.seconds});

Expand All @@ -42,8 +45,12 @@ const UpdateTimer = (props: Props) => {
options.sort((a, b) => ms(a.value) - ms(b.value));
}

// "Never" leads the list as the "no reminder" choice; it is kept out of the
// duration sort above so a null value (ms 0) does not interleave with real durations.
options.unshift(neverOption);

return {options, value};
}, [props.seconds]);
}, [props.seconds, formatMessage]);

return (
<DateTimeSelector
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import renderer from 'react-test-renderer';
import {IntlProvider} from 'react-intl';

import StatusUpdates from './section_status_updates';

const renderWithIntl = (element: React.ReactElement) => renderer.create(
<IntlProvider locale='en'>{element}</IntlProvider>,
);

jest.mock('src/graphql/hooks', () => ({
useUpdatePlaybook: () => jest.fn(),
}));

jest.mock('src/components/markdown_edit', () => ({
__esModule: true,
default: () => <div data-testid='markdown-edit'/>,
}));

jest.mock('./inputs/update_timer_selector', () => ({
__esModule: true,
default: () => <div data-testid='update-timer-selector'/>,
}));

jest.mock('./inputs/broadcast_channels_selector', () => ({
__esModule: true,
default: ({children}: {children: React.ReactNode}) => <div data-testid='broadcast-channels'>{children}</div>,
}));

jest.mock('./inputs/webhooks_input', () => ({
__esModule: true,
default: ({children}: {children: React.ReactNode}) => <div data-testid='webhooks-input'>{children}</div>,
}));

const makePlaybook = (overrides: Record<string, unknown> = {}): any => ({
id: 'playbook-1',
status_update_enabled: true,
reminder_timer_default_seconds: 24 * 60 * 60,
reminder_message_template: '',
broadcast_enabled: false,
broadcast_channel_ids: [],
webhook_on_status_update_enabled: false,
webhook_on_status_update_urls: [],
delete_at: 0,
...overrides,
});

describe('StatusUpdates section', () => {
it('shows the not-expected placeholder when status updates are disabled', () => {
const treeStr = JSON.stringify(renderWithIntl(
<StatusUpdates playbook={makePlaybook({status_update_enabled: false})}/>,
).toJSON());

expect(treeStr).toContain('Status updates are not expected.');
expect(treeStr).not.toContain('update-timer-selector');
});

it('uses the "expected every" phrasing when a positive reminder is set', () => {
const treeStr = JSON.stringify(renderWithIntl(
<StatusUpdates playbook={makePlaybook({reminder_timer_default_seconds: 24 * 60 * 60})}/>,
).toJSON());

expect(treeStr).toContain('every');
expect(treeStr).toContain('update-timer-selector');
});

it('uses the "never expected" phrasing (no "every") when the reminder is 0', () => {
const treeStr = JSON.stringify(renderWithIntl(
<StatusUpdates playbook={makePlaybook({reminder_timer_default_seconds: 0})}/>,
).toJSON());

expect(treeStr).not.toContain('every');
expect(treeStr).toContain('update-timer-selector');
});

it('renders the literal "never" instead of the picker when read-only with no reminder', () => {
const treeStr = JSON.stringify(renderWithIntl(
<StatusUpdates
playbook={makePlaybook({reminder_timer_default_seconds: 0})}
canEdit={false}
/>,
).toJSON());

expect(treeStr).toContain('never');
expect(treeStr).not.toContain('every');
expect(treeStr).not.toContain('update-timer-selector');
});
});
Loading
Loading