diff --git a/app/actions/remote/command.test.ts b/app/actions/remote/command.test.ts
index 3e565ea5c..858ef7990 100644
--- a/app/actions/remote/command.test.ts
+++ b/app/actions/remote/command.test.ts
@@ -382,7 +382,7 @@ describe('handleGotoLocation', () => {
expect(getConfig).toHaveBeenCalledWith(mockOperator.database);
expect(matchDeepLink).toHaveBeenCalledWith(location, serverUrl, serverUrl);
- expect(handleDeepLink).toHaveBeenCalledWith(location, intl, location);
+ expect(handleDeepLink).toHaveBeenCalledWith({url: location}, intl, location);
expect(result).toEqual({data: true});
});
diff --git a/app/actions/remote/command.ts b/app/actions/remote/command.ts
index 68721c5a0..f907955ba 100644
--- a/app/actions/remote/command.ts
+++ b/app/actions/remote/command.ts
@@ -143,7 +143,7 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
if (match) {
- handleDeepLink(match.url, intl, location);
+ handleDeepLink(match, intl, location);
} else {
const {formatMessage} = intl;
const onError = () => Alert.alert(
diff --git a/app/components/post_list/post/body/index.tsx b/app/components/post_list/post/body/index.tsx
index df41b6e7d..d17436390 100644
--- a/app/components/post_list/post/body/index.tsx
+++ b/app/components/post_list/post/body/index.tsx
@@ -9,6 +9,8 @@ import FormattedText from '@components/formatted_text';
import JumboEmoji from '@components/jumbo_emoji';
import {Screens} from '@constants';
import {THREAD} from '@constants/screens';
+import StatusUpdatePost from '@playbooks/components/status_update_post';
+import {PLAYBOOKS_UPDATE_STATUS_POST_TYPE} from '@playbooks/constants/plugin';
import {isEdited as postEdited, isPostFailed} from '@utils/post';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -139,6 +141,14 @@ const Body = ({
defaultMessage='(message deleted)'
/>
);
+ } else if (post.type === PLAYBOOKS_UPDATE_STATUS_POST_TYPE && post.props != null) {
+ message = (
+
+ );
} else if (isPostAddChannelMember) {
message = (
=> {
+ try {
+ const client = NetworkManager.getClient(serverUrl);
+ const run = await client.fetchPlaybookRun(runId);
+
+ if (!fetchOnly) {
+ const result = await handlePlaybookRuns(serverUrl, [run], false, true);
+ if (result.error) {
+ logDebug('error on handlePlaybookRuns', getFullErrorMessage(result.error));
+ return {error: result.error};
+ }
+ }
+
+ return {runs: [run]};
+ } catch (error) {
+ logDebug('error on fetchPlaybookRun', getFullErrorMessage(error));
+ forceLogoutIfNecessary(serverUrl, error);
+ return {error};
+ }
+};
diff --git a/app/products/playbooks/client/rest.ts b/app/products/playbooks/client/rest.ts
index 6765fed3e..c3f7f657a 100644
--- a/app/products/playbooks/client/rest.ts
+++ b/app/products/playbooks/client/rest.ts
@@ -10,7 +10,7 @@ export interface ClientPlaybooksMix {
// Playbook Runs
fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise;
- // fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise;
+ fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise;
// Run Management
// finishRun: (playbookRunId: string) => Promise;
@@ -54,12 +54,12 @@ const ClientPlaybooks = >(superclass: TBas
}
};
- // fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
- // return this.doFetch(
- // `${this.getPlaybookRunRoute(id)}`,
- // {method: 'get', groupLabel},
- // );
- // };
+ fetchPlaybookRun = async (id: string, groupLabel?: RequestGroupLabel) => {
+ return this.doFetch(
+ `${this.getPlaybookRunRoute(id)}`,
+ {method: 'get', groupLabel},
+ );
+ };
// Run Management
// finishRun = async (playbookRunId: string) => {
diff --git a/app/products/playbooks/components/status_update_post/index.tsx b/app/products/playbooks/components/status_update_post/index.tsx
new file mode 100644
index 000000000..ceacc8f93
--- /dev/null
+++ b/app/products/playbooks/components/status_update_post/index.tsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useEffect} from 'react';
+import {useIntl} from 'react-intl';
+import {Text, View} from 'react-native';
+
+import {fetchUsersByIds} from '@actions/remote/user';
+import CompassIcon from '@components/compass_icon';
+import Markdown from '@components/markdown';
+import {useServerUrl} from '@context/server';
+import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+import Participants from './participants';
+
+import type PostModel from '@typings/database/models/servers/post';
+import type {AvailableScreens} from '@typings/screens/navigation';
+
+type Props = {
+ location: AvailableScreens;
+ post: PostModel;
+ theme: Theme;
+};
+
+type StatusUpdatePostProps = {
+ authorUsername: string;
+ numTasks: number;
+ numTasksChecked: number;
+ participantIds: string[];
+ playbookRunId: string;
+ runName: string;
+};
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
+ return {
+ messageContainer: {
+ width: '100%',
+ },
+ icon: {
+ marginRight: 5,
+ },
+ reply: {
+ paddingRight: 10,
+ },
+ message: {
+ color: theme.centerChannelColor,
+ ...typography('Body', 200),
+ lineHeight: undefined, // remove line height, not needed and causes problems with md images
+ },
+ pendingPost: {
+ opacity: 0.5,
+ },
+ updateContainer: {
+ marginTop: 5,
+ borderWidth: 1,
+ borderColor: changeOpacity(theme.centerChannelColor, 0.16),
+ padding: 12,
+ borderRadius: 4,
+ },
+ updateSeparator: {
+ width: '100%',
+ height: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.16),
+ marginVertical: 10,
+ },
+ detailsContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ },
+ row: {
+ flexDirection: 'row',
+ },
+ detailsText: {
+ color: changeOpacity(theme.centerChannelColor, 0.64),
+ ...typography('Body', 75),
+ },
+ detailsSeparator: {
+ color: changeOpacity(theme.centerChannelColor, 0.24),
+ ...typography('Body', 75),
+ },
+ };
+});
+
+const StatusUpdatePost = ({location, post, theme}: Props) => {
+ const {authorUsername, numTasks, numTasksChecked, participantIds, playbookRunId, runName} = post.props as StatusUpdatePostProps;
+ const serverUrl = useServerUrl();
+ const style = getStyleSheet(theme);
+ const blockStyles = getMarkdownBlockStyles(theme);
+ const textStyles = getMarkdownTextStyles(theme);
+ const intl = useIntl();
+
+ useEffect(() => {
+ fetchUsersByIds(serverUrl, participantIds);
+
+ // Only do this on mount
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const updatePosted = intl.formatMessage({
+ id: 'playbooks.status_update_post.update',
+ defaultMessage: '@{authorUsername} posted an update for [{runName}]({link})',
+ }, {authorUsername, runName, link: `/playbooks/runs/${playbookRunId}`});
+ const tasks = intl.formatMessage({
+ id: 'playbooks.status_update_post.num_tasks',
+ defaultMessage: '**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked',
+ }, {numTasksChecked, numTasks});
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {'•'}
+
+
+
+
+
+
+ );
+};
+
+export default StatusUpdatePost;
diff --git a/app/products/playbooks/components/status_update_post/participants/index.ts b/app/products/playbooks/components/status_update_post/participants/index.ts
new file mode 100644
index 000000000..97845b297
--- /dev/null
+++ b/app/products/playbooks/components/status_update_post/participants/index.ts
@@ -0,0 +1,22 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
+
+import {queryUsersById} from '@queries/servers/user';
+
+import Participants from './participants';
+
+import type {WithDatabaseArgs} from '@typings/database/database';
+
+type Props = WithDatabaseArgs & {
+ participantIds: string[];
+}
+
+const enhanced = withObservables(['participantIds'], ({database, participantIds}: Props) => {
+ return {
+ users: queryUsersById(database, participantIds).observe(),
+ };
+});
+
+export default withDatabase(enhanced(Participants));
diff --git a/app/products/playbooks/components/status_update_post/participants/participants.tsx b/app/products/playbooks/components/status_update_post/participants/participants.tsx
new file mode 100644
index 000000000..ab58e196f
--- /dev/null
+++ b/app/products/playbooks/components/status_update_post/participants/participants.tsx
@@ -0,0 +1,128 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback} from 'react';
+import {useIntl} from 'react-intl';
+import {Platform, Text, TouchableOpacity, View, type StyleProp, type TextStyle} from 'react-native';
+
+import CompassIcon from '@components/compass_icon';
+import Markdown from '@components/markdown';
+import UsersList from '@components/user_avatars_stack/users_list';
+import {useTheme} from '@context/theme';
+import {useIsTablet} from '@hooks/device';
+import {usePreventDoubleTap} from '@hooks/utils';
+import {BOTTOM_SHEET_ANDROID_OFFSET} from '@screens/bottom_sheet';
+import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
+import {bottomSheet} from '@screens/navigation';
+import {bottomSheetSnapPoint} from '@utils/helpers';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+import type {UserModel} from '@database/models/server';
+import type {MarkdownTextStyles} from '@typings/global/markdown';
+import type {AvailableScreens} from '@typings/screens/navigation';
+
+type Props = {
+ participantIds: string[];
+ users: UserModel[];
+ location: AvailableScreens;
+ baseTextStyle: StyleProp;
+ textStyles: MarkdownTextStyles;
+}
+
+const USER_ROW_HEIGHT = 40;
+const MAX_USERS_DISPLAYED = 5;
+const BOTTOM_SHEET_MAX_HEIGHT = '80%';
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ participantsContainer: {
+ flexDirection: 'row',
+ },
+ icon: {
+ marginRight: 5,
+ },
+ listHeader: {
+ marginBottom: 12,
+ },
+ listHeaderText: {
+ color: theme.centerChannelColor,
+ ...typography('Heading', 600, 'SemiBold'),
+ },
+}));
+
+const Participants = ({baseTextStyle, participantIds, users, location, textStyles}: Props) => {
+ const intl = useIntl();
+ const theme = useTheme();
+ const isTablet = useIsTablet();
+ const style = getStyleSheet(theme);
+
+ const participants = intl.formatMessage({
+ id: 'playbooks.status_update_post.participants',
+ defaultMessage: '{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}',
+ }, {numParticipants: participantIds.length});
+
+ const showParticipantsList = usePreventDoubleTap(useCallback(() => {
+ const bottomSheetTitle = intl.formatMessage({
+ id: 'playbooks.playbook_run.participants_title',
+ defaultMessage: 'Run Participants',
+ });
+
+ const renderContent = () => (
+ <>
+ {!isTablet && (
+
+
+ {bottomSheetTitle}
+
+
+ )}
+
+ >
+ );
+
+ let height = bottomSheetSnapPoint(Math.min(users.length, MAX_USERS_DISPLAYED), USER_ROW_HEIGHT) + TITLE_HEIGHT;
+ if (Platform.OS === 'android') {
+ height += BOTTOM_SHEET_ANDROID_OFFSET;
+ }
+
+ const snapPoints: Array = [1, height];
+ if (users.length > MAX_USERS_DISPLAYED) {
+ snapPoints.push(BOTTOM_SHEET_MAX_HEIGHT);
+ }
+
+ bottomSheet({
+ closeButtonId: 'close-set-user-status',
+ renderContent,
+ initialSnapIndex: 1,
+ snapPoints,
+ title: bottomSheetTitle,
+ theme,
+ });
+ }, [users, intl, theme, isTablet, style.listHeader, style.listHeaderText, location]));
+
+ return (
+
+
+
+
+ );
+};
+
+export default Participants;
diff --git a/app/products/playbooks/constants/plugin.ts b/app/products/playbooks/constants/plugin.ts
index 33f1f38b9..0a5ee32eb 100644
--- a/app/products/playbooks/constants/plugin.ts
+++ b/app/products/playbooks/constants/plugin.ts
@@ -2,3 +2,4 @@
// See LICENSE.txt for license information.
export const PLAYBOOKS_PLUGIN_ID = 'playbooks';
+export const PLAYBOOKS_UPDATE_STATUS_POST_TYPE = 'custom_run_update';
diff --git a/app/screens/channel_info/channel_info.tsx b/app/screens/channel_info/channel_info.tsx
index 5358f8374..da0ef2475 100644
--- a/app/screens/channel_info/channel_info.tsx
+++ b/app/screens/channel_info/channel_info.tsx
@@ -35,6 +35,7 @@ type Props = {
componentId: AvailableScreens;
isBookmarksEnabled: boolean;
isCallsEnabledInChannel: boolean;
+ isPlaybooksEnabled: boolean;
groupCallsAllowed: boolean;
canManageMembers: boolean;
isConvertGMFeatureAvailable: boolean;
@@ -70,6 +71,7 @@ const ChannelInfo = ({
componentId,
isBookmarksEnabled,
isCallsEnabledInChannel,
+ isPlaybooksEnabled,
groupCallsAllowed,
isConvertGMFeatureAvailable,
isCRTEnabled,
@@ -139,6 +141,7 @@ const ChannelInfo = ({
canManageMembers={canManageMembers}
isCRTEnabled={isCRTEnabled}
canManageSettings={canManageSettings}
+ isPlaybooksEnabled={isPlaybooksEnabled}
/>
{convertGMOptionAvailable &&
diff --git a/app/screens/channel_info/index.ts b/app/screens/channel_info/index.ts
index 20c964ea1..b3a7aa8ec 100644
--- a/app/screens/channel_info/index.ts
+++ b/app/screens/channel_info/index.ts
@@ -8,6 +8,7 @@ import {distinctUntilChanged, switchMap, combineLatestWith} from 'rxjs/operators
import {observeIsCallsEnabledInChannel} from '@calls/observers';
import {observeCallsConfig} from '@calls/state';
import {withServerUrl} from '@context/server';
+import {observeIsPlaybooksEnabled} from '@playbooks/database/queries/version';
import {observeCurrentChannel} from '@queries/servers/channel';
import {observeCanAddBookmarks} from '@queries/servers/channel_bookmark';
import {observeCanManageChannelMembers, observeCanManageChannelSettings} from '@queries/servers/role';
@@ -146,6 +147,8 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
}),
);
+ const isPlaybooksEnabled = observeIsPlaybooksEnabled(database);
+
return {
type,
canEnableDisableCalls,
@@ -158,6 +161,7 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
isCRTEnabled: observeIsCRTEnabled(database),
isGuestUser,
isConvertGMFeatureAvailable,
+ isPlaybooksEnabled,
};
});
diff --git a/app/screens/channel_info/options/index.test.tsx b/app/screens/channel_info/options/index.test.tsx
index 3a679566c..b4b11ec36 100644
--- a/app/screens/channel_info/options/index.test.tsx
+++ b/app/screens/channel_info/options/index.test.tsx
@@ -28,6 +28,7 @@ describe('ChannelInfoOptions', () => {
canManageMembers: false,
isCRTEnabled: false,
canManageSettings: false,
+ isPlaybooksEnabled: true,
};
}
beforeEach(async () => {
diff --git a/app/screens/channel_info/options/index.tsx b/app/screens/channel_info/options/index.tsx
index 68f424bc3..40eba982d 100644
--- a/app/screens/channel_info/options/index.tsx
+++ b/app/screens/channel_info/options/index.tsx
@@ -23,6 +23,7 @@ type Props = {
callsEnabled: boolean;
canManageMembers: boolean;
isCRTEnabled: boolean;
+ isPlaybooksEnabled: boolean;
canManageSettings: boolean;
}
@@ -32,6 +33,7 @@ const Options = ({
callsEnabled,
canManageMembers,
isCRTEnabled,
+ isPlaybooksEnabled,
canManageSettings,
}: Props) => {
const isDMorGM = isTypeDMorGM(type);
@@ -49,10 +51,12 @@ const Options = ({
+ {isPlaybooksEnabled &&
+ }
{type !== General.DM_CHANNEL &&
}
diff --git a/app/screens/home/index.tsx b/app/screens/home/index.tsx
index 9aa6e6678..7620f7e89 100644
--- a/app/screens/home/index.tsx
+++ b/app/screens/home/index.tsx
@@ -19,7 +19,7 @@ import SecurityManager from '@managers/security_manager';
import {getAllServers} from '@queries/app/servers';
import {findChannels, popToRoot} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
-import {alertInvalidDeepLink, handleDeepLink} from '@utils/deep_link';
+import {alertInvalidDeepLink, parseAndHandleDeepLink} from '@utils/deep_link';
import {logError} from '@utils/log';
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
import {notificationError} from '@utils/notification';
@@ -136,7 +136,7 @@ export function HomeScreen(props: HomeProps) {
const deepLink = props.extra as DeepLinkWithData;
if (deepLink?.url) {
- handleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
+ parseAndHandleDeepLink(deepLink.url, intl, props.componentId, true).then((result) => {
if (result.error) {
alertInvalidDeepLink(intl);
}
diff --git a/app/utils/deep_link/index.test.ts b/app/utils/deep_link/index.test.ts
index 55a18d832..4185c7fb1 100644
--- a/app/utils/deep_link/index.test.ts
+++ b/app/utils/deep_link/index.test.ts
@@ -20,7 +20,7 @@ import {addNewServer} from '@utils/server';
import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '../draft';
-import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, handleDeepLink} from '.';
+import {alertInvalidDeepLink, extractServerUrl, getLaunchPropsFromDeepLink, parseAndHandleDeepLink} from '.';
jest.mock('@actions/remote/user', () => ({
fetchUsersByUsernames: jest.fn(),
@@ -94,7 +94,7 @@ describe('extractServerUrl', () => {
});
});
-describe('handleDeepLink', () => {
+describe('parseAndHandleDeepLink', () => {
const intl = createIntl({locale: 'en', messages: {}});
beforeEach(() => {
@@ -102,14 +102,14 @@ describe('handleDeepLink', () => {
});
it('should return error for invalid deep link', async () => {
- const result = await handleDeepLink('invalid-url');
+ const result = await parseAndHandleDeepLink('invalid-url');
expect(result).toEqual({error: true});
});
it('should add new server if not existing', async () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('');
- const result = await handleDeepLink('https://newserver.com/team/channels/town-square');
+ const result = await parseAndHandleDeepLink('https://newserver.com/team/channels/town-square');
expect(addNewServer).toHaveBeenCalledWith(Preferences.THEMES.denim, 'newserver.com', undefined, {type: DeepLink.Channel,
data: {
serverUrl: 'newserver.com',
@@ -124,7 +124,7 @@ describe('handleDeepLink', () => {
it('should handle existing server and switch to home screen', async () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://currentserver.com');
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
- const result = await handleDeepLink('https://existingserver.com/team/channels/town-square');
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/channels/town-square');
expect(dismissAllModalsAndPopToRoot).toHaveBeenCalled();
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'DeepLink');
@@ -136,7 +136,8 @@ describe('handleDeepLink', () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce(null);
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.SERVER);
- const result = await handleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
+ const result = await parseAndHandleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
+ console.log('result', JSON.stringify(result, null, 2));
const spyOnUpdateProps = jest.spyOn(Navigation, 'updateProps');
expect(spyOnUpdateProps).toHaveBeenCalledWith(Screens.SERVER, {serverUrl: 'currentserver.com'});
expect(result).toEqual({error: false});
@@ -148,7 +149,7 @@ describe('handleDeepLink', () => {
(NavigationStore.getVisibleScreen as jest.Mock).mockReturnValueOnce(Screens.LOGIN);
(NavigationStore.getScreensInStack as jest.Mock).mockReturnValueOnce([Screens.SERVER, Screens.LOGIN]);
- const result = await handleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
+ const result = await parseAndHandleDeepLink('https://currentserver.com/team/channels/town-square', undefined, undefined, true);
expect(addNewServer).not.toHaveBeenCalled();
expect(result).toEqual({error: false});
});
@@ -156,7 +157,7 @@ describe('handleDeepLink', () => {
it('should switch to channel by name for Channel deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
- const result = await handleDeepLink('https://existingserver.com/team/channels/town-square', intl);
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/channels/town-square', intl);
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', 'town-square', 'team', errorBadChannel, intl);
expect(result).toEqual({error: false});
});
@@ -165,7 +166,7 @@ describe('handleDeepLink', () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => ['user-id'])});
- const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
expect(result).toEqual({error: false});
});
@@ -175,7 +176,7 @@ describe('handleDeepLink', () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: [{id: 'user-id'}]});
- const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
expect(makeDirectChannel).toHaveBeenCalledWith('https://existingserver.com', 'user-id', '', true);
expect(result).toEqual({error: false});
});
@@ -185,7 +186,7 @@ describe('handleDeepLink', () => {
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
(queryUsersByUsername as jest.Mock).mockReturnValueOnce({fetchIds: jest.fn(() => [])});
(fetchUsersByUsernames as jest.Mock).mockResolvedValueOnce({users: []});
- const result = await handleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/@user-id', intl);
expect(errorUnkownUser).toHaveBeenCalledWith(intl);
expect(result).toEqual({error: false});
});
@@ -193,7 +194,7 @@ describe('handleDeepLink', () => {
it('should switch to group message channel for GroupMessage deep link', async () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
- const result = await handleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f', intl);
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f', intl);
expect(switchToChannelByName).toHaveBeenCalledWith('https://existingserver.com', '7b35c77a645e1906e03a2c330f89203385db102f', 'team', errorBadChannel, intl);
expect(result).toEqual({error: false});
});
@@ -202,7 +203,7 @@ describe('handleDeepLink', () => {
(DatabaseManager.searchUrl as jest.Mock).mockReturnValueOnce('https://existingserver.com');
(getActiveServerUrl as jest.Mock).mockResolvedValueOnce('https://existingserver.com');
const postid = '7b35c77a645e1906e03a2c330f';
- const result = await handleDeepLink(`https://existingserver.com/team/pl/${postid}`, intl);
+ const result = await parseAndHandleDeepLink(`https://existingserver.com/team/pl/${postid}`, intl);
expect(showPermalink).toHaveBeenCalledWith('https://existingserver.com', 'team', postid);
expect(result).toEqual({error: false});
});
@@ -211,7 +212,7 @@ describe('handleDeepLink', () => {
(getActiveServerUrl as jest.Mock).mockImplementationOnce(() => {
throw new Error('DB does not exist error');
});
- const result = await handleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f');
+ const result = await parseAndHandleDeepLink('https://existingserver.com/team/messages/7b35c77a645e1906e03a2c330f89203385db102f');
expect(logError).toHaveBeenCalledWith('Failed to open channel from deeplink', expect.any(Error), undefined);
expect(result).toEqual({error: true});
});
diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts
index ab37a1aef..24342eea5 100644
--- a/app/utils/deep_link/index.ts
+++ b/app/utils/deep_link/index.ts
@@ -3,6 +3,7 @@
import {match} from 'path-to-regexp';
import {type IntlShape} from 'react-intl';
+import {Alert} from 'react-native';
import {Navigation} from 'react-native-navigation';
import urlParse from 'url-parse';
@@ -15,6 +16,10 @@ import {getDefaultThemeByAppearance} from '@context/theme';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, t} from '@i18n';
import WebsocketManager from '@managers/websocket_manager';
+import {fetchPlaybookRun} from '@playbooks/actions/remote/runs';
+import {getPlaybookRunById} from '@playbooks/database/queries/run';
+import {fetchIsPlaybooksEnabled} from '@playbooks/database/queries/version';
+import {goToPlaybookRun} from '@playbooks/screens/navigation';
import {getActiveServerUrl} from '@queries/app/servers';
import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
@@ -32,28 +37,27 @@ import {
ID_PATH_PATTERN,
} from '@utils/url/path';
-import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch';
+import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkPlaybookRuns, DeepLinkWithData, LaunchProps} from '@typings/launch';
import type {AvailableScreens} from '@typings/screens/navigation';
const deepLinkScreens: AvailableScreens[] = [Screens.HOME, Screens.CHANNEL, Screens.GLOBAL_THREADS, Screens.THREAD];
-export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string, asServer = false) {
+export async function handleDeepLink(deepLink: DeepLinkWithData, intlShape?: IntlShape, location?: string) {
try {
- const parsed = parseDeepLink(deepLinkUrl, asServer);
- if (parsed.type === DeepLink.Invalid || !parsed.data || !parsed.data.serverUrl) {
+ if (deepLink.type === DeepLink.Invalid || !deepLink.data || !deepLink.data.serverUrl) {
return {error: true};
}
const currentServerUrl = await getActiveServerUrl();
- const existingServerUrl = DatabaseManager.searchUrl(parsed.data.serverUrl);
+ const existingServerUrl = DatabaseManager.searchUrl(deepLink.data.serverUrl);
// After checking the server for http & https then we add it
if (!existingServerUrl) {
const theme = EphemeralStore.theme || getDefaultThemeByAppearance();
if (NavigationStore.getVisibleScreen() === Screens.SERVER) {
- Navigation.updateProps(Screens.SERVER, {serverUrl: parsed.data.serverUrl});
+ Navigation.updateProps(Screens.SERVER, {serverUrl: deepLink.data.serverUrl});
} else if (!NavigationStore.getScreensInStack().includes(Screens.SERVER)) {
- addNewServer(theme, parsed.data.serverUrl, undefined, parsed);
+ addNewServer(theme, deepLink.data.serverUrl, undefined, deepLink);
}
return {error: false};
}
@@ -70,14 +74,14 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
const locale = currentUser?.locale || DEFAULT_LOCALE;
const intl = intlShape || getIntlShape(locale);
- switch (parsed.type) {
+ switch (deepLink.type) {
case DeepLink.Channel: {
- const deepLinkData = parsed.data as DeepLinkChannel;
+ const deepLinkData = deepLink.data as DeepLinkChannel;
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
break;
}
case DeepLink.DirectMessage: {
- const deepLinkData = parsed.data as DeepLinkDM;
+ const deepLinkData = deepLink.data as DeepLinkDM;
const userIds = await queryUsersByUsername(database, [deepLinkData.userName]).fetchIds();
let userId = userIds.length ? userIds[0] : undefined;
if (!userId) {
@@ -95,12 +99,12 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
break;
}
case DeepLink.GroupMessage: {
- const deepLinkData = parsed.data as DeepLinkGM;
+ const deepLinkData = deepLink.data as DeepLinkGM;
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
break;
}
case DeepLink.Permalink: {
- const deepLinkData = parsed.data as DeepLinkPermalink;
+ const deepLinkData = deepLink.data as DeepLinkPermalink;
if (
NavigationStore.hasModalsOpened() ||
!deepLinkScreens.includes(NavigationStore.getVisibleScreen())
@@ -110,6 +114,49 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId);
break;
}
+ case DeepLink.Playbooks: {
+ // Alert that playbooks should be access from the webapp or desktop app
+ Alert.alert(
+ intl.formatMessage({id: 'playbooks.only_runs_available.title', defaultMessage: 'Playbooks not available'}),
+ intl.formatMessage({id: 'playbooks.only_runs_available.description', defaultMessage: 'Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.'}),
+ [{
+ text: intl.formatMessage({id: 'playbooks.only_runs_available.ok', defaultMessage: 'OK'}),
+ }],
+ );
+ break;
+ }
+ case DeepLink.PlaybookRuns: {
+ const deepLinkData = deepLink.data as DeepLinkPlaybookRuns;
+ const playbookEnabled = await fetchIsPlaybooksEnabled(database);
+ if (playbookEnabled) {
+ // Go to playbook Run
+ const playbook = await getPlaybookRunById(database, deepLinkData.playbookRunId);
+ if (!playbook) {
+ const {error} = await fetchPlaybookRun(existingServerUrl, deepLinkData.playbookRunId);
+ if (error) {
+ Alert.alert(
+ intl.formatMessage({id: 'playbooks.fetch_error.title', defaultMessage: 'Unable to open Run'}),
+ intl.formatMessage({id: 'playbooks.fetch_error.description', defaultMessage: "You don't have permission to view this run, or it may no longer exist."}),
+ [{
+ text: intl.formatMessage({id: 'playbooks.fetch_error.OK', defaultMessage: 'Okay'}),
+ }],
+ );
+ break;
+ }
+ }
+ goToPlaybookRun(intl, deepLinkData.playbookRunId);
+ } else {
+ // Alert playbooks not enabled or version not supported
+ Alert.alert(
+ intl.formatMessage({id: 'playbooks.not_enabled_or_unsupported.title', defaultMessage: 'Playbooks not available'}),
+ intl.formatMessage({id: 'playbooks.not_enabled_or_unsupported.description', defaultMessage: 'Playbooks are either not enabled on this server or the Playbooks version is not supported. Please contact your system administrator.'}),
+ [{
+ text: intl.formatMessage({id: 'playbooks.not_enabled_or_unsupported.OK', defaultMessage: 'OK'}),
+ }],
+ );
+ }
+ break;
+ }
}
return {error: false};
} catch (error) {
@@ -118,6 +165,11 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
}
}
+export async function parseAndHandleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string, asServer = false) {
+ const parsed = parseDeepLink(deepLinkUrl, asServer);
+ return handleDeepLink(parsed, intlShape, location);
+}
+
type ChannelPathParams = {
hostname: string;
serverUrl: string[];
@@ -129,6 +181,22 @@ type ChannelPathParams = {
const CHANNEL_PATH = '*serverUrl/:teamName/:path/:identifier';
export const matchChannelDeeplink = match(CHANNEL_PATH);
+type PlaybooksPathParams = {
+ serverUrl: string[];
+ playbookId: string;
+};
+
+const PLAYBOOKS_PATH = '*serverUrl/playbooks/playbooks/:playbookId';
+export const matchPlaybooksDeeplink = match(PLAYBOOKS_PATH);
+
+type PlaybookRunsPathParams = {
+ serverUrl: string[];
+ playbookRunId: string;
+};
+
+const PLAYBOOK_RUNS_PATH = '*serverUrl/playbooks/runs/:playbookRunId';
+export const matchPlaybookRunsDeeplink = match(PLAYBOOK_RUNS_PATH);
+
type PermalinkPathParams = {
serverUrl: string[];
teamName: string;
@@ -204,14 +272,16 @@ function isValidIdentifierPathPattern(id: string): boolean {
return regex.test(id);
}
-function isValidPostId(id: string): boolean {
+function isValidId(id: string): boolean {
const regex = new RegExp(`^${ID_PATH_PATTERN}$`);
return regex.test(id);
}
export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWithData {
try {
- const url = removeProtocol(deepLinkUrl);
+ const parsedUrl = urlParse(deepLinkUrl);
+ const urlWithoutQuery = stripTrailingSlashes(parsedUrl.protocol + '//' + parsedUrl.host + parsedUrl.pathname);
+ const url = removeProtocol(urlWithoutQuery);
const channelMatch = matchChannelDeeplink(url);
if (channelMatch && isValidTeamName(channelMatch.params.teamName) && isValidIdentifierPathPattern(channelMatch.params.identifier)) {
@@ -231,11 +301,23 @@ export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWi
}
const permalinkMatch = matchPermalinkDeeplink(url);
- if (permalinkMatch && isValidTeamName(permalinkMatch.params.teamName) && isValidPostId(permalinkMatch.params.postId)) {
+ if (permalinkMatch && isValidTeamName(permalinkMatch.params.teamName) && isValidId(permalinkMatch.params.postId)) {
const {params: {serverUrl, teamName, postId}} = permalinkMatch;
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, postId}};
}
+ const playbooksMatch = matchPlaybooksDeeplink(url);
+ if (playbooksMatch && isValidId(playbooksMatch.params.playbookId)) {
+ const {params: {serverUrl, playbookId}} = playbooksMatch;
+ return {type: DeepLink.Playbooks, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), playbookId}};
+ }
+
+ const playbooksRunsMatch = matchPlaybookRunsDeeplink(url);
+ if (playbooksRunsMatch && isValidId(playbooksRunsMatch.params.playbookRunId)) {
+ const {params: {serverUrl, playbookRunId}} = playbooksRunsMatch;
+ return {type: DeepLink.PlaybookRuns, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), playbookRunId}};
+ }
+
if (asServer) {
const serverMatch = extractServerUrl(url);
if (serverMatch) {
diff --git a/app/utils/url/links.test.ts b/app/utils/url/links.test.ts
index 631fe491a..a314f3a42 100644
--- a/app/utils/url/links.test.ts
+++ b/app/utils/url/links.test.ts
@@ -4,7 +4,7 @@
import {createIntl} from 'react-intl';
import {Alert} from 'react-native';
-import {onOpenLinkError, openLink} from './links';
+import * as Links from './links';
describe('onOpenLinkError', () => {
const intl = createIntl({
@@ -26,7 +26,7 @@ describe('onOpenLinkError', () => {
mockReturnValueOnce('Error').
mockReturnValueOnce('Unable to open the link.'); // Mock message
- onOpenLinkError(intl);
+ Links.onOpenLinkError(intl);
expect(intl.formatMessage).toHaveBeenCalledTimes(2);
expect(intl.formatMessage).toHaveBeenCalledWith({
@@ -55,7 +55,7 @@ describe('openLink', () => {
it('should call normalizeProtocol and return early if the URL is invalid', async () => {
const normalizeProtocolMock = jest.spyOn(require('.'), 'normalizeProtocol').mockReturnValue(null);
- await openLink('', 'https://server-url.com', 'https://site-url.com', intl);
+ await Links.openLink('', 'https://server-url.com', 'https://site-url.com', intl);
expect(normalizeProtocolMock).toHaveBeenCalledWith('');
normalizeProtocolMock.mockRestore();
@@ -66,11 +66,11 @@ describe('openLink', () => {
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue({url: 'https://example.com'});
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: null});
- await openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
+ await Links.openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
expect(normalizeProtocolMock).toHaveBeenCalledWith('https://example.com');
expect(matchDeepLinkMock).toHaveBeenCalledWith('https://example.com', 'https://server-url.com', 'https://site-url.com');
- expect(handleDeepLinkMock).toHaveBeenCalledWith('https://example.com', intl);
+ expect(handleDeepLinkMock).toHaveBeenCalledWith({url: 'https://example.com'}, intl);
normalizeProtocolMock.mockRestore();
matchDeepLinkMock.mockRestore();
@@ -83,9 +83,9 @@ describe('openLink', () => {
const handleDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'handleDeepLink').mockResolvedValue({error: true});
const tryOpenURLMock = jest.spyOn(require('.'), 'tryOpenURL').mockImplementation(() => {});
- await openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
+ await Links.openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
- expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', onOpenLinkError);
+ expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', expect.any(Function));
normalizeProtocolMock.mockRestore();
matchDeepLinkMock.mockRestore();
@@ -98,9 +98,9 @@ describe('openLink', () => {
const matchDeepLinkMock = jest.spyOn(require('@utils/deep_link'), 'matchDeepLink').mockReturnValue(null);
const tryOpenURLMock = jest.spyOn(require('.'), 'tryOpenURL').mockImplementation(() => {});
- await openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
+ await Links.openLink('https://example.com', 'https://server-url.com', 'https://site-url.com', intl);
- expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', onOpenLinkError);
+ expect(tryOpenURLMock).toHaveBeenCalledWith('https://example.com', expect.any(Function));
normalizeProtocolMock.mockRestore();
matchDeepLinkMock.mockRestore();
diff --git a/app/utils/url/links.ts b/app/utils/url/links.ts
index e165ef5c8..b0c257b97 100644
--- a/app/utils/url/links.ts
+++ b/app/utils/url/links.ts
@@ -31,11 +31,15 @@ export const openLink = async (link: string, serverUrl: string, siteURL: string,
const match = matchDeepLink(url, serverUrl, siteURL);
if (match) {
- const {error} = await handleDeepLink(match.url, intl);
+ const {error} = await handleDeepLink(match, intl);
if (error) {
- tryOpenURL(match.url, onOpenLinkError);
+ tryOpenURL(match.url, () => {
+ onOpenLinkError(intl);
+ });
}
} else {
- tryOpenURL(url, onOpenLinkError);
+ tryOpenURL(url, () => {
+ onOpenLinkError(intl);
+ });
}
};
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 2bd7f3ba5..97ebf0a51 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -996,6 +996,15 @@
"playbook.participants": "Run Participants",
"playbook.runs.finished": "Finished",
"playbook.runs.in-progress": "In Progress",
+ "playbooks.fetch_error.description": "You don't have permission to view this run, or it may no longer exist.",
+ "playbooks.fetch_error.OK": "Okay",
+ "playbooks.fetch_error.title": "Unable to open Run",
+ "playbooks.not_enabled_or_unsupported.description": "Playbooks are either not enabled on this server or the Playbooks version is not supported. Please contact your system administrator.",
+ "playbooks.not_enabled_or_unsupported.OK": "OK",
+ "playbooks.not_enabled_or_unsupported.title": "Playbooks not available",
+ "playbooks.only_runs_available.description": "Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.",
+ "playbooks.only_runs_available.ok": "OK",
+ "playbooks.only_runs_available.title": "Playbooks not available",
"playbooks.playbook_run.error.description": "Please check your network connection or try again later.",
"playbooks.playbook_run.error.title": "Unable to fetch run details",
"playbooks.playbook_run.finished": "Finished",
@@ -1015,6 +1024,9 @@
"playbooks.runs.in_progress.description": "When a run starts in this channel, you’ll see it here.",
"playbooks.runs.in_progress.title": "No in progress runs",
"playbooks.runs.show_more": "Show More",
+ "playbooks.status_update_post.num_tasks": "**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked",
+ "playbooks.status_update_post.participants": "{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}",
+ "playbooks.status_update_post.update": "@{authorUsername} posted an update for [{runName}]({link})",
"plus_menu.browse_channels.title": "Browse Channels",
"plus_menu.create_new_channel.title": "Create New Channel",
"plus_menu.invite_people_to_team.title": "Invite people to the team",
diff --git a/fastlane/.env.ios.beta b/fastlane/.env.ios.beta
index a534a7472..d867b472e 100644
--- a/fastlane/.env.ios.beta
+++ b/fastlane/.env.ios.beta
@@ -8,6 +8,8 @@ BETA_BUILD=true
BUILD_FOR_RELEASE=true
COLLECT_NETWORK_METRICS=true
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
+FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
+FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6
IOS_APP_GROUP=group.com.mattermost.rnbeta
IOS_BUILD_EXPORT_METHOD=app-store
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
diff --git a/fastlane/.env.ios.pr b/fastlane/.env.ios.pr
index 29d79ac8a..29c813ed7 100644
--- a/fastlane/.env.ios.pr
+++ b/fastlane/.env.ios.pr
@@ -9,6 +9,8 @@ BUILD_FOR_RELEASE=true
BUILD_PR=true
COLLECT_NETWORK_METRICS=true
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
+FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
+FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6
IOS_APP_GROUP=group.com.mattermost.rnbeta
IOS_BUILD_EXPORT_METHOD=ad-hoc
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
diff --git a/fastlane/.env.ios.simulator b/fastlane/.env.ios.simulator
index d39f555f6..4a439312a 100644
--- a/fastlane/.env.ios.simulator
+++ b/fastlane/.env.ios.simulator
@@ -2,3 +2,5 @@
AWS_BUCKET_NAME=releases.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile-beta
AWS_REGION=us-east-1
+FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
+FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6
diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts
index bedbbb090..0b8699dd3 100644
--- a/types/api/posts.d.ts
+++ b/types/api/posts.d.ts
@@ -27,7 +27,8 @@ type PostType =
| 'add_bot_teams_channels'
| 'system_auto_responder'
| 'custom_calls'
- | 'custom_calls_recording';
+ | 'custom_calls_recording'
+ | 'custom_run_update';
type PostEmbedType = 'image' | 'message_attachment' | 'opengraph';
diff --git a/types/launch/index.ts b/types/launch/index.ts
index 30fa52292..4a179208a 100644
--- a/types/launch/index.ts
+++ b/types/launch/index.ts
@@ -33,12 +33,20 @@ export interface DeepLinkPlugin extends DeepLink {
route?: string;
}
+export interface DeepLinkPlaybooks extends DeepLink {
+ playbookId: string;
+}
+
+export interface DeepLinkPlaybookRuns extends DeepLink {
+ playbookRunId: string;
+}
+
export type DeepLinkType = typeof DeepLinkConstant[keyof typeof DeepLinkConstant];
export interface DeepLinkWithData {
type: DeepLinkType;
url: string;
- data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin | DeepLinkServer;
+ data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin | DeepLinkServer | DeepLinkPlaybooks | DeepLinkPlaybookRuns;
}
export type LaunchType = typeof Launch[keyof typeof Launch];