Add post update to playbooks (#9120)
* Add Floating Label Autocomplete Selector * Add post update to playbooks * Add intro message and template default * Fix type issue * Add missing texts * Fix test * Minor fix and refactor * Address feedback
This commit is contained in:
parent
b44aace169
commit
6d136a1343
20 changed files with 558 additions and 36 deletions
|
|
@ -6,7 +6,6 @@ import {type IntlShape, useIntl} from 'react-intl';
|
||||||
import {Text, View, type StyleProp, type TextStyle, type ViewStyle} from 'react-native';
|
import {Text, View, type StyleProp, type TextStyle, type ViewStyle} from 'react-native';
|
||||||
|
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
|
||||||
import {Screens, View as ViewConstants} from '@constants';
|
import {Screens, View as ViewConstants} from '@constants';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
|
@ -48,6 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
dropdownPlaceholder: {
|
dropdownPlaceholder: {
|
||||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||||
|
|
@ -174,14 +174,14 @@ function AutoCompleteSelector({
|
||||||
});
|
});
|
||||||
}, [dataSource, teammateNameDisplay, intl, options, selected, serverUrl]);
|
}, [dataSource, teammateNameDisplay, intl, options, selected, serverUrl]);
|
||||||
|
|
||||||
const touchableStyle = useMemo(() => {
|
const inputStyle = useMemo(() => {
|
||||||
const res: StyleProp<ViewStyle> = [{flex: 1}];
|
const res: StyleProp<ViewStyle> = [style.input];
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
res.push(style.disabled);
|
res.push(style.disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}, [disabled, style.disabled]);
|
}, [disabled, style.disabled, style.input]);
|
||||||
|
|
||||||
const dropdownTextStyle = useMemo(() => {
|
const dropdownTextStyle = useMemo(() => {
|
||||||
const res: StyleProp<TextStyle> = [style.dropdownText];
|
const res: StyleProp<TextStyle> = [style.dropdownText];
|
||||||
|
|
@ -200,30 +200,24 @@ function AutoCompleteSelector({
|
||||||
error={errorText}
|
error={errorText}
|
||||||
hideErrorIcon={true}
|
hideErrorIcon={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
focus={goToSelectorScreen}
|
||||||
focused={false}
|
focused={false}
|
||||||
focusedLabel={focusedLabel}
|
focusedLabel={focusedLabel}
|
||||||
editable={!disabled}
|
editable={!disabled}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
>
|
>
|
||||||
<TouchableWithFeedback
|
<View style={inputStyle}>
|
||||||
disabled={disabled}
|
<Text
|
||||||
onPress={goToSelectorScreen}
|
numberOfLines={1}
|
||||||
style={touchableStyle}
|
style={dropdownTextStyle}
|
||||||
type='opacity'
|
>
|
||||||
>
|
{itemText || placeholder}
|
||||||
<View style={style.input}>
|
</Text>
|
||||||
<Text
|
<CompassIcon
|
||||||
numberOfLines={1}
|
name='chevron-down'
|
||||||
style={dropdownTextStyle}
|
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||||
>
|
/>
|
||||||
{itemText || placeholder}
|
</View>
|
||||||
</Text>
|
|
||||||
<CompassIcon
|
|
||||||
name='chevron-down'
|
|
||||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableWithFeedback>
|
|
||||||
</FloatingInputContainer>
|
</FloatingInputContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ const FloatingInputContainer = ({
|
||||||
paddingHorizontal: focusedLabel || inputText ? 4 : 0,
|
paddingHorizontal: focusedLabel || inputText ? 4 : 0,
|
||||||
color,
|
color,
|
||||||
};
|
};
|
||||||
});
|
}, [styles, theme, focusedLabel, hasValue, shouldShowError, positions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ export const fetchFinishedRunsForChannel = async (serverUrl: string, channelId:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOnly = false): Promise<PlaybookRunsRequest> => {
|
export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOnly = false) => {
|
||||||
try {
|
try {
|
||||||
const client = NetworkManager.getClient(serverUrl);
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
const run = await client.fetchPlaybookRun(runId);
|
const run = await client.fetchPlaybookRun(runId);
|
||||||
|
|
@ -102,7 +102,7 @@ export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {runs: [run]};
|
return {run};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logDebug('error on fetchPlaybookRun', getFullErrorMessage(error));
|
logDebug('error on fetchPlaybookRun', getFullErrorMessage(error));
|
||||||
forceLogoutIfNecessary(serverUrl, error);
|
forceLogoutIfNecessary(serverUrl, error);
|
||||||
|
|
@ -110,6 +110,18 @@ export const fetchPlaybookRun = async (serverUrl: string, runId: string, fetchOn
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchPlaybookRunMetadata = async (serverUrl: string, runId: string) => {
|
||||||
|
try {
|
||||||
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
const metadata = await client.fetchPlaybookRunMetadata(runId);
|
||||||
|
return {metadata};
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('error on fetchPlaybookRunMetadata', getFullErrorMessage(error));
|
||||||
|
forceLogoutIfNecessary(serverUrl, error);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId: string) => {
|
export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId: string) => {
|
||||||
try {
|
try {
|
||||||
const client = NetworkManager.getClient(serverUrl);
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
|
@ -124,6 +136,16 @@ export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const postStatusUpdate = async (serverUrl: string, playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => {
|
||||||
|
try {
|
||||||
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
await client.postStatusUpdate(playbookRunID, payload, ids);
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('error on postStatusUpdate', getFullErrorMessage(error));
|
||||||
|
forceLogoutIfNecessary(serverUrl, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const finishRun = async (serverUrl: string, playbookRunId: string) => {
|
export const finishRun = async (serverUrl: string, playbookRunId: string) => {
|
||||||
try {
|
try {
|
||||||
const client = NetworkManager.getClient(serverUrl);
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@ export interface ClientPlaybooksMix {
|
||||||
// Playbook Runs
|
// Playbook Runs
|
||||||
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise<FetchPlaybookRunsReturn>;
|
||||||
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise<PlaybookRun>;
|
||||||
|
fetchPlaybookRunMetadata: (id: string) => Promise<PlaybookRunMetadata>;
|
||||||
setOwner: (playbookRunId: string, ownerId: string) => Promise<void>;
|
setOwner: (playbookRunId: string, ownerId: string) => Promise<void>;
|
||||||
|
|
||||||
// Run Management
|
// Run Management
|
||||||
|
// finishRun: (playbookRunId: string) => Promise<any>;
|
||||||
finishRun: (playbookRunId: string) => Promise<void>;
|
finishRun: (playbookRunId: string) => Promise<void>;
|
||||||
|
postStatusUpdate: (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => Promise<void>;
|
||||||
|
|
||||||
// Checklist Management
|
// Checklist Management
|
||||||
setChecklistItemState: (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => Promise<void>;
|
setChecklistItemState: (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => Promise<void>;
|
||||||
|
|
@ -65,6 +68,13 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchPlaybookRunMetadata = async (id: string) => {
|
||||||
|
return this.doFetch(
|
||||||
|
`${this.getPlaybookRunRoute(id)}/metadata`,
|
||||||
|
{method: 'get'},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
setOwner = async (playbookRunId: string, ownerId: string) => {
|
setOwner = async (playbookRunId: string, ownerId: string) => {
|
||||||
const data = await this.doFetch(
|
const data = await this.doFetch(
|
||||||
`${this.getPlaybookRunRoute(playbookRunId)}/owner`,
|
`${this.getPlaybookRunRoute(playbookRunId)}/owner`,
|
||||||
|
|
@ -81,6 +91,26 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
postStatusUpdate = async (playbookRunID: string, payload: PostStatusUpdatePayload, ids: PostStatusUpdateIds) => {
|
||||||
|
const body = {
|
||||||
|
type: 'dialog_submission',
|
||||||
|
callback_id: '',
|
||||||
|
state: '',
|
||||||
|
cancelled: false,
|
||||||
|
...ids,
|
||||||
|
submission: {
|
||||||
|
message: payload.message,
|
||||||
|
reminder: payload.reminder?.toFixed() ?? '',
|
||||||
|
finish_run: payload.finishRun,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.doFetch(
|
||||||
|
`${this.getPlaybookRunRoute(playbookRunID)}/update-status-dialog`,
|
||||||
|
{method: 'post', body},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Checklist Management
|
// Checklist Management
|
||||||
setChecklistItemState = async (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => {
|
setChecklistItemState = async (playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) => {
|
||||||
await this.doFetch(
|
await this.doFetch(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
export const PLAYBOOKS_RUNS = 'PlaybookRuns';
|
export const PLAYBOOKS_RUNS = 'PlaybookRuns';
|
||||||
export const PLAYBOOK_RUN = 'PlaybookRun';
|
export const PLAYBOOK_RUN = 'PlaybookRun';
|
||||||
export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
|
export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
|
||||||
|
export const PLAYBOOK_POST_UPDATE = 'PlaybookPostUpdate';
|
||||||
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
|
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
|
||||||
export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate';
|
export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate';
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ export default {
|
||||||
PLAYBOOKS_RUNS,
|
PLAYBOOKS_RUNS,
|
||||||
PLAYBOOK_RUN,
|
PLAYBOOK_RUN,
|
||||||
PLAYBOOK_EDIT_COMMAND,
|
PLAYBOOK_EDIT_COMMAND,
|
||||||
|
PLAYBOOK_POST_UPDATE,
|
||||||
PLAYBOOK_SELECT_USER,
|
PLAYBOOK_SELECT_USER,
|
||||||
PLAYBOOKS_SELECT_DATE,
|
PLAYBOOKS_SELECT_DATE,
|
||||||
};
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {render} from '@test/intl-test-helper';
|
||||||
import EditCommand from './edit_command';
|
import EditCommand from './edit_command';
|
||||||
import PlaybookRun from './playbook_run';
|
import PlaybookRun from './playbook_run';
|
||||||
import PlaybookRuns from './playbooks_runs';
|
import PlaybookRuns from './playbooks_runs';
|
||||||
|
import PostUpdate from './post_update';
|
||||||
import SelectDate from './select_date';
|
import SelectDate from './select_date';
|
||||||
import SelectUser from './select_user';
|
import SelectUser from './select_user';
|
||||||
|
|
||||||
|
|
@ -52,6 +53,12 @@ jest.mock('@playbooks/screens/select_date', () => ({
|
||||||
}));
|
}));
|
||||||
jest.mocked(SelectDate).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_DATE}</Text>);
|
jest.mocked(SelectDate).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_DATE}</Text>);
|
||||||
|
|
||||||
|
jest.mock('@playbooks/screens/post_update', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mocked(PostUpdate).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_POST_UPDATE}</Text>);
|
||||||
|
|
||||||
describe('Screen Registration', () => {
|
describe('Screen Registration', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export function loadPlaybooksScreen(screenName: string | number) {
|
||||||
return withServerDatabase(require('@playbooks/screens/playbook_run').default);
|
return withServerDatabase(require('@playbooks/screens/playbook_run').default);
|
||||||
case Screens.PLAYBOOK_EDIT_COMMAND:
|
case Screens.PLAYBOOK_EDIT_COMMAND:
|
||||||
return withServerDatabase(require('@playbooks/screens/edit_command').default);
|
return withServerDatabase(require('@playbooks/screens/edit_command').default);
|
||||||
|
case Screens.PLAYBOOK_POST_UPDATE:
|
||||||
|
return withServerDatabase(require('@playbooks/screens/post_update').default);
|
||||||
case Screens.PLAYBOOK_SELECT_USER:
|
case Screens.PLAYBOOK_SELECT_USER:
|
||||||
return withServerDatabase(require('@playbooks/screens/select_user').default);
|
return withServerDatabase(require('@playbooks/screens/select_user').default);
|
||||||
case Screens.PLAYBOOKS_SELECT_DATE:
|
case Screens.PLAYBOOKS_SELECT_DATE:
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ export async function goToPlaybookRun(intl: IntlShape, playbookRunId: string, pl
|
||||||
goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId, playbookRun}, {});
|
goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId, playbookRun}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function goToPostUpdate(intl: IntlShape, playbookRunId: string) {
|
||||||
|
const title = intl.formatMessage({id: 'playbooks.post_update.title', defaultMessage: 'Post update'});
|
||||||
|
goToScreen(Screens.PLAYBOOK_POST_UPDATE, title, {playbookRunId}, {});
|
||||||
|
}
|
||||||
|
|
||||||
function getSubtitleOptions(theme: Theme, runName: string): RNNOptions {
|
function getSubtitleOptions(theme: Theme, runName: string): RNNOptions {
|
||||||
return {
|
return {
|
||||||
topBar: {
|
topBar: {
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,8 @@ export default function PlaybookRun({
|
||||||
<StatusUpdateIndicator
|
<StatusUpdateIndicator
|
||||||
isFinished={isFinished}
|
isFinished={isFinished}
|
||||||
timestamp={getRunScheduledTimestamp(playbookRun)}
|
timestamp={getRunScheduledTimestamp(playbookRun)}
|
||||||
|
isParticipant={isParticipant}
|
||||||
|
playbookRunId={playbookRun.id}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.tasksContainer}>
|
<View style={styles.tasksContainer}>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ describe('StatusUpdateIndicator', () => {
|
||||||
<StatusUpdateIndicator
|
<StatusUpdateIndicator
|
||||||
isFinished={false}
|
isFinished={false}
|
||||||
timestamp={futureTimestamp}
|
timestamp={futureTimestamp}
|
||||||
|
isParticipant={true}
|
||||||
|
playbookRunId='run-id'
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -48,6 +50,8 @@ describe('StatusUpdateIndicator', () => {
|
||||||
<StatusUpdateIndicator
|
<StatusUpdateIndicator
|
||||||
isFinished={false}
|
isFinished={false}
|
||||||
timestamp={pastTimestamp}
|
timestamp={pastTimestamp}
|
||||||
|
isParticipant={true}
|
||||||
|
playbookRunId='run-id'
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -64,6 +68,8 @@ describe('StatusUpdateIndicator', () => {
|
||||||
<StatusUpdateIndicator
|
<StatusUpdateIndicator
|
||||||
isFinished={true}
|
isFinished={true}
|
||||||
timestamp={pastTimestamp}
|
timestamp={pastTimestamp}
|
||||||
|
isParticipant={true}
|
||||||
|
playbookRunId='run-id'
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useMemo} from 'react';
|
import React, {useCallback, useMemo} from 'react';
|
||||||
import {defineMessages, useIntl} from 'react-intl';
|
import {defineMessages, useIntl} from 'react-intl';
|
||||||
import {View, Text} from 'react-native';
|
import {View, Text} from 'react-native';
|
||||||
|
|
||||||
|
import Button from '@components/button';
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
import FriendlyDate from '@components/friendly_date';
|
import FriendlyDate from '@components/friendly_date';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import {goToPostUpdate} from '../navigation';
|
||||||
|
|
||||||
type StatusUpdateIndicatorProps = {
|
type StatusUpdateIndicatorProps = {
|
||||||
isFinished: boolean;
|
isFinished: boolean;
|
||||||
|
isParticipant: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
playbookRunId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||||
|
|
@ -27,6 +32,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
|
due: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
dueTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
icon: {
|
icon: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
|
@ -56,17 +70,24 @@ const messages = defineMessages({
|
||||||
id: 'playbooks.playbook_run.status_update_finished',
|
id: 'playbooks.playbook_run.status_update_finished',
|
||||||
defaultMessage: 'Run finished {time}',
|
defaultMessage: 'Run finished {time}',
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
id: 'playbooks.playbook_run.status_update',
|
||||||
|
defaultMessage: 'Post update',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const StatusUpdateIndicator = ({
|
const StatusUpdateIndicator = ({
|
||||||
isFinished,
|
isFinished,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
isParticipant,
|
||||||
|
playbookRunId,
|
||||||
}: StatusUpdateIndicatorProps) => {
|
}: StatusUpdateIndicatorProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyleSheet(theme);
|
const styles = getStyleSheet(theme);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const values = {time: <FriendlyDate value={timestamp}/>};
|
const values = {time: <FriendlyDate value={timestamp}/>};
|
||||||
|
const readOnly = !isParticipant || isFinished;
|
||||||
|
|
||||||
let message = messages.updateDue;
|
let message = messages.updateDue;
|
||||||
if (isFinished) {
|
if (isFinished) {
|
||||||
|
|
@ -88,16 +109,35 @@ const StatusUpdateIndicator = ({
|
||||||
];
|
];
|
||||||
}, [styles.icon, styles.overdueText, isFinished, timestamp]);
|
}, [styles.icon, styles.overdueText, isFinished, timestamp]);
|
||||||
|
|
||||||
|
const onUpdatePress = useCallback(async () => {
|
||||||
|
if (readOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await goToPostUpdate(intl, playbookRunId);
|
||||||
|
}, [intl, playbookRunId, readOnly]);
|
||||||
|
|
||||||
const icon = isFinished ? 'flag-checkered' : 'clock-outline';
|
const icon = isFinished ? 'flag-checkered' : 'clock-outline';
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<CompassIcon
|
<View style={styles.due}>
|
||||||
name={icon}
|
<CompassIcon
|
||||||
style={iconStyle}
|
name={icon}
|
||||||
|
style={iconStyle}
|
||||||
|
/>
|
||||||
|
<View style={styles.dueTextContainer}>
|
||||||
|
<Text style={textStyle}>
|
||||||
|
{intl.formatMessage(message, values)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(messages.update)}
|
||||||
|
onPress={onUpdatePress}
|
||||||
|
theme={theme}
|
||||||
|
size='lg'
|
||||||
|
disabled={readOnly}
|
||||||
/>
|
/>
|
||||||
<Text style={textStyle}>
|
|
||||||
{intl.formatMessage(message, values)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
56
app/products/playbooks/screens/post_update/index.ts
Normal file
56
app/products/playbooks/screens/post_update/index.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
import {switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {queryPlaybookChecklistByRun} from '@playbooks/database/queries/checklist';
|
||||||
|
import {queryPlaybookChecklistItemsByChecklists} from '@playbooks/database/queries/item';
|
||||||
|
import {observePlaybookRunById} from '@playbooks/database/queries/run';
|
||||||
|
import {isOutstanding} from '@playbooks/utils/run';
|
||||||
|
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||||
|
|
||||||
|
import PostUpdate from './post_update';
|
||||||
|
|
||||||
|
import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist';
|
||||||
|
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
playbookRunId: string;
|
||||||
|
playbookRun?: PlaybookRun;
|
||||||
|
} & WithDatabaseArgs;
|
||||||
|
|
||||||
|
const getIds = (checklists: PlaybookChecklistModel[]) => {
|
||||||
|
return checklists.map((c) => c.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanced = withObservables(['playbookRunId'], ({playbookRunId, database}: OwnProps) => {
|
||||||
|
const playbookRun = observePlaybookRunById(database, playbookRunId);
|
||||||
|
|
||||||
|
const checklists = queryPlaybookChecklistByRun(database, playbookRunId).observe();
|
||||||
|
const outstandingCount = checklists.pipe(
|
||||||
|
switchMap((cs) => {
|
||||||
|
const ids = getIds(cs);
|
||||||
|
return queryPlaybookChecklistItemsByChecklists(database, ids).observeWithColumns(['state']);
|
||||||
|
}),
|
||||||
|
switchMap((items) => {
|
||||||
|
const overdue = items.filter(isOutstanding).length;
|
||||||
|
return of$(overdue);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
runName: playbookRun.pipe(
|
||||||
|
switchMap((r) => of$(r?.name ?? '')),
|
||||||
|
),
|
||||||
|
userId: observeCurrentUserId(database),
|
||||||
|
channelId: playbookRun.pipe(
|
||||||
|
switchMap((r) => (r ? of$(r.channelId) : of$(undefined))),
|
||||||
|
),
|
||||||
|
teamId: observeCurrentTeamId(database),
|
||||||
|
outstanding: outstandingCount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withDatabase(enhanced(PostUpdate));
|
||||||
303
app/products/playbooks/screens/post_update/post_update.tsx
Normal file
303
app/products/playbooks/screens/post_update/post_update.tsx
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {type ComponentProps, useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
import {Alert, Keyboard, Text, View} from 'react-native';
|
||||||
|
|
||||||
|
import {getPosts} from '@actions/local/post';
|
||||||
|
import FloatingAutocompleteSelector from '@components/floating_input/floating_autocomplete_selector';
|
||||||
|
import FloatingTextInput from '@components/floating_input/floating_text_input_label';
|
||||||
|
import Loading from '@components/loading';
|
||||||
|
import OptionItem from '@components/option_item';
|
||||||
|
import {useServerUrl} from '@context/server';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import {fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from '@playbooks/actions/remote/runs';
|
||||||
|
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||||
|
import {toSeconds} from '@utils/datetime';
|
||||||
|
import {logDebug} from '@utils/log';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
componentId: AvailableScreens;
|
||||||
|
playbookRunId: string;
|
||||||
|
runName: string;
|
||||||
|
userId: string;
|
||||||
|
channelId?: string;
|
||||||
|
teamId: string;
|
||||||
|
outstanding: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
introMessage: {
|
||||||
|
...typography('Body', 200),
|
||||||
|
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||||
|
},
|
||||||
|
introMessageBold: {
|
||||||
|
...typography('Body', 200, 'SemiBold'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SAVE_BUTTON_ID = 'save-post-update';
|
||||||
|
|
||||||
|
const close = (componentId: AvailableScreens): void => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
popTopScreen(componentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const valueToTimeMap = {
|
||||||
|
'15_minutes': toSeconds({minutes: 15}),
|
||||||
|
'30_minutes': toSeconds({minutes: 30}),
|
||||||
|
'1_hour': toSeconds({hours: 1}),
|
||||||
|
'4_hours': toSeconds({hours: 4}),
|
||||||
|
'1_day': toSeconds({days: 1}),
|
||||||
|
'7_days': toSeconds({days: 7}),
|
||||||
|
};
|
||||||
|
|
||||||
|
type NextUpdateValues = keyof typeof valueToTimeMap;
|
||||||
|
|
||||||
|
const PostUpdate = ({
|
||||||
|
componentId,
|
||||||
|
playbookRunId,
|
||||||
|
runName,
|
||||||
|
userId,
|
||||||
|
channelId,
|
||||||
|
teamId,
|
||||||
|
outstanding,
|
||||||
|
}: Props) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const intl = useIntl();
|
||||||
|
const serverUrl = useServerUrl();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
const [updateMessage, setUpdateMessage] = useState('');
|
||||||
|
const [nextUpdate, setNextUpdate] = useState<NextUpdateValues>('15_minutes');
|
||||||
|
const [alsoMarkRunAsFinished, setAlsoMarkRunAsFinished] = useState(false);
|
||||||
|
const [canSave, setCanSave] = useState(false);
|
||||||
|
|
||||||
|
const [followersCount, setFollowersCount] = useState<number>(0);
|
||||||
|
const [broadcastChannelCount, setBroadcastChannelCount] = useState<number>(0);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const onChangeText = useCallback((text: string) => {
|
||||||
|
setUpdateMessage(text);
|
||||||
|
setCanSave(text.trim().length > 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const rightButton = useMemo(() => {
|
||||||
|
const base = buildNavigationButton(
|
||||||
|
SAVE_BUTTON_ID,
|
||||||
|
'playbooks.edit_command.save.button',
|
||||||
|
undefined,
|
||||||
|
intl.formatMessage({id: 'playbooks.post_update.post.button', defaultMessage: 'Post'}),
|
||||||
|
);
|
||||||
|
base.enabled = canSave;
|
||||||
|
base.color = theme.sidebarHeaderTextColor;
|
||||||
|
return base;
|
||||||
|
}, [intl, canSave, theme.sidebarHeaderTextColor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setButtons(componentId, {
|
||||||
|
rightButtons: [rightButton],
|
||||||
|
});
|
||||||
|
}, [rightButton, componentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function initialLoad() {
|
||||||
|
let calculatedFollowersCount = 0;
|
||||||
|
let calculatedBroadcastChannelCount = 0;
|
||||||
|
let calculatedDefaultMessage = '';
|
||||||
|
|
||||||
|
const metadataRes = await fetchPlaybookRunMetadata(serverUrl, playbookRunId);
|
||||||
|
if (metadataRes.error) {
|
||||||
|
logDebug('error on fetchPlaybookRunMetadata', metadataRes.error);
|
||||||
|
} else {
|
||||||
|
calculatedFollowersCount = metadataRes.metadata?.followers?.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runRes = await fetchPlaybookRun(serverUrl, playbookRunId, true);
|
||||||
|
if (runRes.error) {
|
||||||
|
logDebug('error on fetchPlaybookRun', runRes.error);
|
||||||
|
} else {
|
||||||
|
if (runRes.run?.status_update_broadcast_channels_enabled) {
|
||||||
|
calculatedBroadcastChannelCount = runRes.run?.broadcast_channel_ids?.length ?? 0;
|
||||||
|
}
|
||||||
|
calculatedDefaultMessage = runRes.run?.reminder_message_template ?? '';
|
||||||
|
const lastStatusPostMetadata = runRes.run?.status_posts?.slice().reverse().find((post) => !post.delete_at);
|
||||||
|
if (lastStatusPostMetadata?.id) {
|
||||||
|
const lastStatusPost = (await getPosts(serverUrl, [lastStatusPostMetadata?.id ?? '']))[0];
|
||||||
|
if (lastStatusPost) {
|
||||||
|
calculatedDefaultMessage = lastStatusPost.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFollowersCount(calculatedFollowersCount);
|
||||||
|
setBroadcastChannelCount(calculatedBroadcastChannelCount);
|
||||||
|
setUpdateMessage(calculatedDefaultMessage);
|
||||||
|
setCanSave(calculatedDefaultMessage.trim().length > 0);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialLoad();
|
||||||
|
|
||||||
|
// This is the initial load, so we don't need to re-run it on every change
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onNextUpdateSelected = useCallback((value: SelectedDialogOption) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// Multiselect, which should never happen
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNextUpdate(value.value as NextUpdateValues);
|
||||||
|
}, [setNextUpdate]);
|
||||||
|
|
||||||
|
const dialogOptions = useMemo<DialogOption[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.option.15_minutes', defaultMessage: '15 minutes'}),
|
||||||
|
value: '15_minutes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.option.30_minutes', defaultMessage: '30 minutes'}),
|
||||||
|
value: '30_minutes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.option.1_hour', defaultMessage: '1 hour'}),
|
||||||
|
value: '1_hour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.option.4_hours', defaultMessage: '4 hours'}),
|
||||||
|
value: '4_hours',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.option.1_day', defaultMessage: '1 day'}),
|
||||||
|
value: '1_day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.option.7_days', defaultMessage: '7 days'}),
|
||||||
|
value: '7_days',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [intl]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
close(componentId);
|
||||||
|
}, [componentId]);
|
||||||
|
|
||||||
|
const onConfirm = useCallback(() => {
|
||||||
|
close(componentId);
|
||||||
|
if (!channelId) {
|
||||||
|
// This should never happen, but this keeps typescript happy
|
||||||
|
logDebug('cannot post status update without a channel id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postStatusUpdate(serverUrl, playbookRunId, {message: updateMessage, reminder: valueToTimeMap[nextUpdate], finishRun: alsoMarkRunAsFinished}, {user_id: userId, channel_id: channelId, team_id: teamId});
|
||||||
|
}, [alsoMarkRunAsFinished, channelId, componentId, nextUpdate, playbookRunId, serverUrl, teamId, updateMessage, userId]);
|
||||||
|
|
||||||
|
const onPostUpdate = useCallback(() => {
|
||||||
|
if (alsoMarkRunAsFinished) {
|
||||||
|
let message = intl.formatMessage({id: 'playbooks.post_update.confirm.message', defaultMessage: 'Are you sure you want to finish the run {runName} for all participants?'}, {runName});
|
||||||
|
if (outstanding > 0) {
|
||||||
|
message = intl.formatMessage({id: 'playbooks.post_update.confirm.message.with_tasks', defaultMessage: 'There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run {runName} for all participants?'}, {runName, outstanding});
|
||||||
|
}
|
||||||
|
Alert.alert(
|
||||||
|
intl.formatMessage({id: 'playbooks.post_update.confirm.title', defaultMessage: 'Confirm finish run'}),
|
||||||
|
message,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.confirm.cancel', defaultMessage: 'Cancel'}),
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({id: 'playbooks.post_update.confirm.confirm', defaultMessage: 'Finish run'}),
|
||||||
|
onPress: onConfirm,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
}, [alsoMarkRunAsFinished, intl, onConfirm, outstanding, runName]);
|
||||||
|
|
||||||
|
useNavButtonPressed(SAVE_BUTTON_ID, componentId, onPostUpdate, [onPostUpdate]);
|
||||||
|
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||||
|
|
||||||
|
const introMessageValues = useMemo<ComponentProps<typeof FormattedMessage>['values']>(() => {
|
||||||
|
return {
|
||||||
|
runName,
|
||||||
|
hasFollowersAndChannels: Boolean(followersCount && broadcastChannelCount).toString(),
|
||||||
|
hasChannels: Boolean(broadcastChannelCount).toString(),
|
||||||
|
hasFollowers: Boolean(followersCount).toString(),
|
||||||
|
broadcastChannelCount,
|
||||||
|
followersChannelCount: followersCount,
|
||||||
|
Bold: (...chunks) => <Text style={styles.introMessageBold}>{chunks}</Text>,
|
||||||
|
};
|
||||||
|
}, [runName, followersCount, broadcastChannelCount, styles.introMessageBold]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading/>;
|
||||||
|
}
|
||||||
|
let introMessage;
|
||||||
|
if (broadcastChannelCount + followersCount === 0) {
|
||||||
|
introMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='playbooks.post_update.intro.no_followers_or_broadcast_channels'
|
||||||
|
defaultMessage='This update will be saved to the overview page.'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
introMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='playbooks.post_update.intro'
|
||||||
|
defaultMessage='This update for the run <Bold>{runName}</Bold> will be broadcasted to {hasChannels, select, true {<Bold>{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}</Bold>} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {<Bold>{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}</Bold>} other {}}.'
|
||||||
|
values={introMessageValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.introMessage}>{introMessage}</Text>
|
||||||
|
<FloatingTextInput
|
||||||
|
label={intl.formatMessage({id: 'playbooks.post_update.label', defaultMessage: 'Update message'})}
|
||||||
|
placeholder={intl.formatMessage({id: 'playbooks.post_update.placeholder', defaultMessage: 'Enter your update message'})}
|
||||||
|
value={updateMessage}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
theme={theme}
|
||||||
|
multiline={true}
|
||||||
|
/>
|
||||||
|
<FloatingAutocompleteSelector
|
||||||
|
options={dialogOptions}
|
||||||
|
testID='playbooks.post_update.selector'
|
||||||
|
selected={nextUpdate}
|
||||||
|
onSelected={onNextUpdateSelected}
|
||||||
|
label={intl.formatMessage({id: 'playbooks.post_update.label.next_update', defaultMessage: 'Timer for next update'})}
|
||||||
|
/>
|
||||||
|
<OptionItem
|
||||||
|
label={intl.formatMessage({id: 'playbooks.post_update.label.also_mark_run_as_finished', defaultMessage: 'Also mark the run as finished'})}
|
||||||
|
action={setAlsoMarkRunAsFinished}
|
||||||
|
testID='playbooks.post_update.selector.also_mark_run_as_finished'
|
||||||
|
selected={alsoMarkRunAsFinished}
|
||||||
|
type='toggle'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostUpdate;
|
||||||
|
|
@ -32,7 +32,7 @@ describe('SelectDate', () => {
|
||||||
function getBaseProps(): ComponentProps<typeof SelectDate> {
|
function getBaseProps(): ComponentProps<typeof SelectDate> {
|
||||||
const mockOnSave = jest.fn();
|
const mockOnSave = jest.fn();
|
||||||
|
|
||||||
const mockComponentId: AvailableScreens = 'SelectDate';
|
const mockComponentId: AvailableScreens = 'PlaybooksSelectDate';
|
||||||
const mockCurrentUserTimezone: UserTimezone = {
|
const mockCurrentUserTimezone: UserTimezone = {
|
||||||
automaticTimezone: 'UTC',
|
automaticTimezone: 'UTC',
|
||||||
manualTimezone: '',
|
manualTimezone: '',
|
||||||
|
|
|
||||||
11
app/products/playbooks/types/api.d.ts
vendored
11
app/products/playbooks/types/api.d.ts
vendored
|
|
@ -54,6 +54,7 @@ type RunMetricData = {
|
||||||
type StatusPost = {
|
type StatusPost = {
|
||||||
id: string;
|
id: string;
|
||||||
create_at: number;
|
create_at: number;
|
||||||
|
delete_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimelineEvent = {
|
type TimelineEvent = {
|
||||||
|
|
@ -135,4 +136,14 @@ type PlaybookRun = {
|
||||||
metrics_data: RunMetricData[];
|
metrics_data: RunMetricData[];
|
||||||
update_at: number;
|
update_at: number;
|
||||||
items_order: string[];
|
items_order: string[];
|
||||||
|
status_update_broadcast_channels_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaybookRunMetadata = {
|
||||||
|
channel_name: string;
|
||||||
|
channel_display_name: string;
|
||||||
|
team_name: string;
|
||||||
|
num_participants: number;
|
||||||
|
total_posts: number;
|
||||||
|
followers: string[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
app/products/playbooks/types/client.d.ts
vendored
12
app/products/playbooks/types/client.d.ts
vendored
|
|
@ -35,3 +35,15 @@ type FetchPlaybookRunsParams = {
|
||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
since?: number;
|
since?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostStatusUpdatePayload = {
|
||||||
|
message: string;
|
||||||
|
reminder?: number;
|
||||||
|
finishRun: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PostStatusUpdateIds = {
|
||||||
|
user_id: string;
|
||||||
|
channel_id: string;
|
||||||
|
team_id: string;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -70,3 +70,7 @@ export function isDueSoon(item: PlaybookChecklistItemModel | PlaybookChecklistIt
|
||||||
|
|
||||||
return dueDate < Date.now() + toMilliseconds({hours: 12});
|
return dueDate < Date.now() + toMilliseconds({hours: 12});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOutstanding(item: PlaybookChecklistItemModel | PlaybookChecklistItem): boolean {
|
||||||
|
return item.state === '' || item.state === 'in_progress';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,15 @@ export function isYesterday(date: Date): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toMilliseconds({days, hours, minutes, seconds}: {days?: number; hours?: number; minutes?: number; seconds?: number}) {
|
export function toMilliseconds({days, hours, minutes, seconds}: {days?: number; hours?: number; minutes?: number; seconds?: number}) {
|
||||||
|
const totalSeconds = toSeconds({days, hours, minutes, seconds});
|
||||||
|
return totalSeconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSeconds({days, hours, minutes, seconds}: {days?: number; hours?: number; minutes?: number; seconds?: number}) {
|
||||||
const totalHours = ((days || 0) * 24) + (hours || 0);
|
const totalHours = ((days || 0) * 24) + (hours || 0);
|
||||||
const totalMinutes = (totalHours * 60) + (minutes || 0);
|
const totalMinutes = (totalHours * 60) + (minutes || 0);
|
||||||
const totalSeconds = (totalMinutes * 60) + (seconds || 0);
|
const totalSeconds = (totalMinutes * 60) + (seconds || 0);
|
||||||
return totalSeconds * 1000;
|
return totalSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReadableTimestamp(timestamp: number, timeZone: string, isMilitaryTime: boolean, currentUserLocale: string): string {
|
export function getReadableTimestamp(timestamp: number, timeZone: string, isMilitaryTime: boolean, currentUserLocale: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1034,12 +1034,32 @@
|
||||||
"playbooks.playbook_run.participants": "Participants",
|
"playbooks.playbook_run.participants": "Participants",
|
||||||
"playbooks.playbook_run.participants_title": "Run Participants",
|
"playbooks.playbook_run.participants_title": "Run Participants",
|
||||||
"playbooks.playbook_run.run_details": "Run details",
|
"playbooks.playbook_run.run_details": "Run details",
|
||||||
|
"playbooks.playbook_run.status_update": "Post update",
|
||||||
"playbooks.playbook_run.status_update_due": "Status update due {time}",
|
"playbooks.playbook_run.status_update_due": "Status update due {time}",
|
||||||
"playbooks.playbook_run.status_update_finished": "Run finished {time}",
|
"playbooks.playbook_run.status_update_finished": "Run finished {time}",
|
||||||
"playbooks.playbook_run.status_update_overdue": "Status update overdue {time}",
|
"playbooks.playbook_run.status_update_overdue": "Status update overdue {time}",
|
||||||
"playbooks.playbook_run.tasks": "Tasks",
|
"playbooks.playbook_run.tasks": "Tasks",
|
||||||
"playbooks.playbook_run.title": "Playbook run",
|
"playbooks.playbook_run.title": "Playbook run",
|
||||||
"playbooks.playbooks_runs.title": "Playbook runs",
|
"playbooks.playbooks_runs.title": "Playbook runs",
|
||||||
|
"playbooks.post_update.confirm.cancel": "Cancel",
|
||||||
|
"playbooks.post_update.confirm.confirm": "Finish run",
|
||||||
|
"playbooks.post_update.confirm.message": "Are you sure you want to finish the run {runName} for all participants?",
|
||||||
|
"playbooks.post_update.confirm.message.with_tasks": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run {runName} for all participants?",
|
||||||
|
"playbooks.post_update.confirm.title": "Confirm finish run",
|
||||||
|
"playbooks.post_update.intro": "This update for the run <Bold>{runName}</Bold> will be broadcasted to {hasChannels, select, true {<Bold>{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}</Bold>} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {<Bold>{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}</Bold>} other {}}.",
|
||||||
|
"playbooks.post_update.intro.no_followers_or_broadcast_channels": "This update will be saved to the overview page.",
|
||||||
|
"playbooks.post_update.label": "Update message",
|
||||||
|
"playbooks.post_update.label.also_mark_run_as_finished": "Also mark the run as finished",
|
||||||
|
"playbooks.post_update.label.next_update": "Timer for next update",
|
||||||
|
"playbooks.post_update.option.1_day": "1 day",
|
||||||
|
"playbooks.post_update.option.1_hour": "1 hour",
|
||||||
|
"playbooks.post_update.option.15_minutes": "15 minutes",
|
||||||
|
"playbooks.post_update.option.30_minutes": "30 minutes",
|
||||||
|
"playbooks.post_update.option.4_hours": "4 hours",
|
||||||
|
"playbooks.post_update.option.7_days": "7 days",
|
||||||
|
"playbooks.post_update.placeholder": "Enter your update message",
|
||||||
|
"playbooks.post_update.post.button": "Post",
|
||||||
|
"playbooks.post_update.title": "Post update",
|
||||||
"playbooks.retrospective_not_available.description": "Only Playbook Runs are available on mobile. To fill the Run Retrospective, please use the desktop or web app.",
|
"playbooks.retrospective_not_available.description": "Only Playbook Runs are available on mobile. To fill the Run Retrospective, please use the desktop or web app.",
|
||||||
"playbooks.retrospective_not_available.ok": "OK",
|
"playbooks.retrospective_not_available.ok": "OK",
|
||||||
"playbooks.retrospective_not_available.title": "Playbooks Run Retrospective not available",
|
"playbooks.retrospective_not_available.title": "Playbooks Run Retrospective not available",
|
||||||
|
|
|
||||||
|
|
@ -1174,6 +1174,7 @@ class TestHelperSingleton {
|
||||||
checklists,
|
checklists,
|
||||||
update_at: Date.now() + i,
|
update_at: Date.now() + i,
|
||||||
items_order: checklists.map((checklist) => checklist.id),
|
items_order: checklists.map((checklist) => checklist.id),
|
||||||
|
status_update_broadcast_channels_enabled: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return playbookRuns;
|
return playbookRuns;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue