Add playbooks edit command functionality (#9084)
* Add playbooks edit command functionality * Fix tests * Add tests * Remove option to change command when the run is disabled (not participant or finished) * Fix tests * Address feedback * Fix test * Remove unneeded variables
This commit is contained in:
parent
881342cb5f
commit
b227837fa5
28 changed files with 1539 additions and 62 deletions
123
app/components/autocomplete/autocomplete.test.tsx
Normal file
123
app/components/autocomplete/autocomplete.test.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {render} from '@test/intl-test-helper';
|
||||
|
||||
import AtMention from './at_mention/';
|
||||
import Autocomplete from './autocomplete';
|
||||
import ChannelMention from './channel_mention/';
|
||||
import EmojiSuggestion from './emoji_suggestion/';
|
||||
import SlashSuggestion from './slash_suggestion/';
|
||||
import AppSlashSuggestion from './slash_suggestion/app_slash_suggestion/';
|
||||
|
||||
import type {SharedValue} from 'react-native-reanimated';
|
||||
|
||||
jest.mock('./at_mention/');
|
||||
jest.mocked(AtMention).mockImplementation((props) => React.createElement('AtMention', {...props, testID: 'at-mention-mock'}));
|
||||
|
||||
jest.mock('./channel_mention/');
|
||||
jest.mocked(ChannelMention).mockImplementation((props) => React.createElement('ChannelMention', {...props, testID: 'channel-mention-mock'}));
|
||||
|
||||
jest.mock('./emoji_suggestion/');
|
||||
jest.mocked(EmojiSuggestion).mockImplementation((props) => React.createElement('EmojiSuggestion', {...props, testID: 'emoji-suggestion-mock'}));
|
||||
|
||||
jest.mock('./slash_suggestion/');
|
||||
jest.mocked(SlashSuggestion).mockImplementation((props) => React.createElement('SlashSuggestion', {...props, testID: 'slash-suggestion-mock'}));
|
||||
|
||||
jest.mock('./slash_suggestion/app_slash_suggestion/');
|
||||
jest.mocked(AppSlashSuggestion).mockImplementation((props) => React.createElement('AppSlashSuggestion', {...props, testID: 'app-slash-suggestion-mock'}));
|
||||
|
||||
describe('Autocomplete', () => {
|
||||
function getBaseProps(): ComponentProps<typeof Autocomplete> {
|
||||
return {
|
||||
availableSpace: {value: 0} as SharedValue<number>,
|
||||
cursorPosition: 0,
|
||||
isAppsEnabled: true,
|
||||
position: {value: 0} as SharedValue<number>,
|
||||
updateValue: jest.fn(),
|
||||
value: '',
|
||||
autocompleteProviders: {
|
||||
channel: true,
|
||||
emoji: true,
|
||||
slash: true,
|
||||
user: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
it('should only render the selected autocomplete providers', () => {
|
||||
const props = getBaseProps();
|
||||
props.autocompleteProviders = {
|
||||
user: true,
|
||||
channel: true,
|
||||
emoji: true,
|
||||
slash: true,
|
||||
};
|
||||
props.channelId = 'channel-id'; // required for slash suggestions
|
||||
|
||||
const {getByTestId, queryByTestId, rerender} = render(<Autocomplete {...props}/>);
|
||||
|
||||
expect(getByTestId('at-mention-mock')).toBeTruthy();
|
||||
expect(getByTestId('channel-mention-mock')).toBeTruthy();
|
||||
expect(getByTestId('emoji-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('slash-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('app-slash-suggestion-mock')).toBeTruthy();
|
||||
|
||||
props.autocompleteProviders = {
|
||||
user: true,
|
||||
channel: true,
|
||||
emoji: true,
|
||||
slash: false,
|
||||
};
|
||||
rerender(<Autocomplete {...props}/>);
|
||||
|
||||
expect(getByTestId('at-mention-mock')).toBeTruthy();
|
||||
expect(getByTestId('channel-mention-mock')).toBeTruthy();
|
||||
expect(getByTestId('emoji-suggestion-mock')).toBeTruthy();
|
||||
expect(queryByTestId('slash-suggestion-mock')).toBeNull();
|
||||
expect(queryByTestId('app-slash-suggestion-mock')).toBeNull();
|
||||
|
||||
props.autocompleteProviders = {
|
||||
user: false,
|
||||
channel: true,
|
||||
emoji: true,
|
||||
slash: true,
|
||||
};
|
||||
rerender(<Autocomplete {...props}/>);
|
||||
|
||||
expect(queryByTestId('at-mention-mock')).toBeNull();
|
||||
expect(getByTestId('channel-mention-mock')).toBeTruthy();
|
||||
expect(getByTestId('emoji-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('slash-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('app-slash-suggestion-mock')).toBeTruthy();
|
||||
|
||||
props.autocompleteProviders = {
|
||||
user: true,
|
||||
channel: false,
|
||||
emoji: true,
|
||||
slash: true,
|
||||
};
|
||||
rerender(<Autocomplete {...props}/>);
|
||||
|
||||
expect(getByTestId('at-mention-mock')).toBeTruthy();
|
||||
expect(queryByTestId('channel-mention-mock')).toBeNull();
|
||||
expect(getByTestId('emoji-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('slash-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('app-slash-suggestion-mock')).toBeTruthy();
|
||||
|
||||
props.autocompleteProviders = {
|
||||
user: true,
|
||||
channel: true,
|
||||
emoji: false,
|
||||
slash: true,
|
||||
};
|
||||
rerender(<Autocomplete {...props}/>);
|
||||
|
||||
expect(getByTestId('at-mention-mock')).toBeTruthy();
|
||||
expect(getByTestId('channel-mention-mock')).toBeTruthy();
|
||||
expect(queryByTestId('emoji-suggestion-mock')).toBeNull();
|
||||
expect(getByTestId('slash-suggestion-mock')).toBeTruthy();
|
||||
expect(getByTestId('app-slash-suggestion-mock')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -64,8 +64,23 @@ type Props = {
|
|||
growDown?: boolean;
|
||||
teamId?: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
autocompleteProviders?: AutocompleteProviders;
|
||||
}
|
||||
|
||||
type AutocompleteProviders = {
|
||||
user: boolean;
|
||||
channel: boolean;
|
||||
emoji: boolean;
|
||||
slash: boolean;
|
||||
}
|
||||
|
||||
const defaultAutocompleteProviders: AutocompleteProviders = {
|
||||
user: true,
|
||||
channel: true,
|
||||
emoji: true,
|
||||
slash: true,
|
||||
};
|
||||
|
||||
const Autocomplete = ({
|
||||
cursorPosition,
|
||||
position,
|
||||
|
|
@ -83,6 +98,7 @@ const Autocomplete = ({
|
|||
growDown = false,
|
||||
containerStyle,
|
||||
teamId,
|
||||
autocompleteProviders = defaultAutocompleteProviders,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -133,7 +149,7 @@ const Autocomplete = ({
|
|||
testID='autocomplete'
|
||||
style={containerStyles}
|
||||
>
|
||||
{isAppsEnabled && channelId && (
|
||||
{isAppsEnabled && channelId && autocompleteProviders.slash && (
|
||||
<AppSlashSuggestion
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
|
|
@ -145,29 +161,33 @@ const Autocomplete = ({
|
|||
/>
|
||||
)}
|
||||
{(!appsTakeOver || !isAppsEnabled) && (<>
|
||||
<AtMention
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingAtMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<ChannelMention
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingChannelMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
{!isSearch &&
|
||||
{autocompleteProviders.user && (
|
||||
<AtMention
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingAtMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
)}
|
||||
{autocompleteProviders.channel && (
|
||||
<ChannelMention
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
onShowingChange={setShowingChannelMention}
|
||||
value={value || ''}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
isSearch={isSearch}
|
||||
channelId={channelId}
|
||||
teamId={teamId}
|
||||
/>
|
||||
)}
|
||||
{!isSearch && autocompleteProviders.emoji && (
|
||||
<EmojiSuggestion
|
||||
cursorPosition={cursorPosition}
|
||||
listStyle={style.listStyle}
|
||||
|
|
@ -178,8 +198,8 @@ const Autocomplete = ({
|
|||
rootId={rootId}
|
||||
shouldDirectlyReact={shouldDirectlyReact}
|
||||
/>
|
||||
}
|
||||
{showCommands && channelId &&
|
||||
)}
|
||||
{showCommands && channelId && autocompleteProviders.slash && (
|
||||
<SlashSuggestion
|
||||
listStyle={style.listStyle}
|
||||
updateValue={updateValue}
|
||||
|
|
@ -190,7 +210,7 @@ const Autocomplete = ({
|
|||
rootId={rootId}
|
||||
isAppsEnabled={isAppsEnabled}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
{/* {(isSearch && enableDateSuggestion) &&
|
||||
<DateSuggestion
|
||||
cursorPosition={cursorPosition}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import PLAYBOOKS_SCREENS from '@playbooks/constants/screens';
|
||||
|
||||
export const ABOUT = 'About';
|
||||
export const ACCOUNT = 'Account';
|
||||
export const APPS_FORM = 'AppForm';
|
||||
|
|
@ -52,8 +54,6 @@ export const ONBOARDING = 'Onboarding';
|
|||
export const PDF_VIEWER = 'PdfViewer';
|
||||
export const PERMALINK = 'Permalink';
|
||||
export const PINNED_MESSAGES = 'PinnedMessages';
|
||||
export const PLAYBOOKS_RUNS = 'PlaybookRuns';
|
||||
export const PLAYBOOK_RUN = 'PlaybookRun';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
export const POST_PRIORITY_PICKER = 'PostPriorityPicker';
|
||||
export const REACTIONS = 'Reactions';
|
||||
|
|
@ -141,8 +141,6 @@ export default {
|
|||
PDF_VIEWER,
|
||||
PERMALINK,
|
||||
PINNED_MESSAGES,
|
||||
PLAYBOOKS_RUNS,
|
||||
PLAYBOOK_RUN,
|
||||
POST_OPTIONS,
|
||||
POST_PRIORITY_PICKER,
|
||||
REACTIONS,
|
||||
|
|
@ -178,6 +176,7 @@ export default {
|
|||
THREAD_FOLLOW_BUTTON,
|
||||
THREAD_OPTIONS,
|
||||
USER_PROFILE,
|
||||
...PLAYBOOKS_SCREENS,
|
||||
} as const;
|
||||
|
||||
export const MODAL_SCREENS_WITHOUT_BACK = new Set<string>([
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {updateChecklistItem} from './checklist';
|
||||
import {updateChecklistItem, setChecklistItemCommand} from './checklist';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
|
|
@ -68,3 +68,59 @@ describe('updateChecklistItem', () => {
|
|||
await Promise.all(testPromises);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setChecklistItemCommand', () => {
|
||||
it('should handle not found database', async () => {
|
||||
const {error} = await setChecklistItemCommand('foo', 'itemid', '/test command');
|
||||
expect(error).toBeTruthy();
|
||||
expect((error as Error).message).toContain('foo database not found');
|
||||
});
|
||||
|
||||
it('should handle item not found', async () => {
|
||||
const {error} = await setChecklistItemCommand(serverUrl, 'nonexistent', '/test command');
|
||||
expect(error).toBe('Item not found: nonexistent');
|
||||
});
|
||||
|
||||
it('should handle database write errors', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const originalWrite = operator.database.write;
|
||||
operator.database.write = jest.fn().mockRejectedValue(new Error('Database write failed'));
|
||||
|
||||
const {error} = await setChecklistItemCommand(serverUrl, item.id, '/test command');
|
||||
expect(error).toBeTruthy();
|
||||
|
||||
operator.database.write = originalWrite;
|
||||
});
|
||||
|
||||
it('should set command successfully', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const testCommand = '/test command with parameters';
|
||||
const {data, error} = await setChecklistItemCommand(serverUrl, item.id, testCommand);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.command).toBe(testCommand);
|
||||
});
|
||||
|
||||
it('should handle empty command string', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const {data, error} = await setChecklistItemCommand(serverUrl, item.id, '');
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.command).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,3 +25,24 @@ export async function updateChecklistItem(serverUrl: string, itemId: string, sta
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function setChecklistItemCommand(serverUrl: string, itemId: string, command: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const item = await getPlaybookChecklistItemById(database, itemId);
|
||||
if (!item) {
|
||||
return {error: `Item not found: ${itemId}`};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
item.update((i) => {
|
||||
i.command = command;
|
||||
});
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to set checklist item command', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
|
||||
import {setChecklistItemCommand as localSetChecklistItemCommand, updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
|
||||
|
||||
import {updateChecklistItem, runChecklistItem, skipChecklistItem, restoreChecklistItem} from './checklist';
|
||||
import {updateChecklistItem, runChecklistItem, skipChecklistItem, restoreChecklistItem, setChecklistItemCommand} from './checklist';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
||||
|
|
@ -13,12 +14,14 @@ const playbookRunId = 'playbook-run-id-1';
|
|||
const itemId = 'checklist-item-id-1';
|
||||
const checklistNumber = 0;
|
||||
const itemNumber = 1;
|
||||
const command = '/test-command';
|
||||
|
||||
const mockClient = {
|
||||
setChecklistItemState: jest.fn(),
|
||||
runChecklistItemSlashCommand: jest.fn(),
|
||||
skipChecklistItem: jest.fn(),
|
||||
restoreChecklistItem: jest.fn(),
|
||||
setChecklistItemCommand: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@playbooks/actions/local/checklist');
|
||||
|
|
@ -89,6 +92,32 @@ describe('checklist', () => {
|
|||
expect(result.data).toBe(true);
|
||||
expect(mockClient.runChecklistItemSlashCommand).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber);
|
||||
});
|
||||
|
||||
it('should set trigger id if it is returned', async () => {
|
||||
mockClient.runChecklistItemSlashCommand.mockResolvedValueOnce({trigger_id: 'trigger_id'});
|
||||
const setTriggerId = jest.fn();
|
||||
jest.spyOn(IntegrationsManager, 'getManager').mockReturnValue({
|
||||
setTriggerId,
|
||||
} as any);
|
||||
|
||||
const result = await runChecklistItem(serverUrl, playbookRunId, checklistNumber, itemNumber);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(setTriggerId).toHaveBeenCalledWith('trigger_id');
|
||||
});
|
||||
|
||||
it('should not set trigger id if it is not returned', async () => {
|
||||
mockClient.runChecklistItemSlashCommand.mockResolvedValueOnce({});
|
||||
const setTriggerId = jest.fn();
|
||||
jest.spyOn(IntegrationsManager, 'getManager').mockReturnValue({
|
||||
setTriggerId,
|
||||
} as any);
|
||||
|
||||
const result = await runChecklistItem(serverUrl, playbookRunId, checklistNumber, itemNumber);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(setTriggerId).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('skipChecklistItem', () => {
|
||||
|
|
@ -152,4 +181,36 @@ describe('checklist', () => {
|
|||
expect(localUpdateChecklistItem).toHaveBeenCalledWith(serverUrl, itemId, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setChecklistItemCommand', () => {
|
||||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setChecklistItemCommand(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, command);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(localSetChecklistItemCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle API exception', async () => {
|
||||
mockClient.setChecklistItemCommand.mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setChecklistItemCommand(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, command);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.setChecklistItemCommand).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, command);
|
||||
expect(localSetChecklistItemCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set checklist item command successfully', async () => {
|
||||
mockClient.setChecklistItemCommand.mockResolvedValueOnce({});
|
||||
|
||||
const result = await setChecklistItemCommand(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, command);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setChecklistItemCommand).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, command);
|
||||
expect(localSetChecklistItemCommand).toHaveBeenCalledWith(serverUrl, itemId, command);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
|
||||
import {setChecklistItemCommand as localSetChecklistItemCommand, updateChecklistItem as localUpdateChecklistItem} from '@playbooks/actions/local/checklist';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
|
|
@ -36,7 +37,10 @@ export const runChecklistItem = async (
|
|||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber);
|
||||
const {trigger_id} = await client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber);
|
||||
if (trigger_id) {
|
||||
IntegrationsManager.getManager(serverUrl)?.setTriggerId(trigger_id);
|
||||
}
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on runChecklistItem', getFullErrorMessage(error));
|
||||
|
|
@ -84,3 +88,24 @@ export const restoreChecklistItem = async (
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const setChecklistItemCommand = async (
|
||||
serverUrl: string,
|
||||
playbookRunId: string,
|
||||
itemId: string,
|
||||
checklistNumber: number,
|
||||
itemNumber: number,
|
||||
command: string,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.setChecklistItemCommand(playbookRunId, checklistNumber, itemNumber, command);
|
||||
|
||||
await localSetChecklistItemCommand(serverUrl, itemId, command);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on setChecklistItemCommand', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -141,9 +141,10 @@ describe('runChecklistItemSlashCommand', () => {
|
|||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNumber}/item/${itemNumber}/run`;
|
||||
const expectedOptions = {method: 'post'};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
jest.mocked(client.doFetch).mockResolvedValue({trigger_id: 'trigger123'});
|
||||
|
||||
await client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber);
|
||||
const result = await client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber);
|
||||
expect(result).toEqual({trigger_id: 'trigger123'});
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
|
@ -155,9 +156,10 @@ describe('runChecklistItemSlashCommand', () => {
|
|||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNumber}/item/${itemNumber}/run`;
|
||||
const expectedOptions = {method: 'post'};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
jest.mocked(client.doFetch).mockResolvedValue({trigger_id: ''});
|
||||
|
||||
await client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber);
|
||||
const result = await client.runChecklistItemSlashCommand(playbookRunId, checklistNumber, itemNumber);
|
||||
expect(result).toEqual({trigger_id: ''});
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
|
@ -253,3 +255,33 @@ describe('restoreChecklistItem', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setChecklistItemCommand', () => {
|
||||
test('should set checklist item command successfully', async () => {
|
||||
const playbookRunID = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const command = '/test command';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/command`;
|
||||
const expectedOptions = {method: 'put', body: {command}};
|
||||
const mockResponse = {success: true};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await client.setChecklistItemCommand(playbookRunID, checklistNum, itemNum, command);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should handle error when setting checklist item command', async () => {
|
||||
const playbookRunID = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const command = '/test command';
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.setChecklistItemCommand(playbookRunID, checklistNum, itemNum, command)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ export interface ClientPlaybooksMix {
|
|||
// skipChecklist: (playbookRunID: string, checklistNum: number) => Promise<void>;
|
||||
|
||||
// Slash Commands
|
||||
runChecklistItemSlashCommand: (playbookRunId: string, checklistNumber: number, itemNumber: number) => Promise<void>;
|
||||
runChecklistItemSlashCommand: (playbookRunId: string, checklistNumber: number, itemNumber: number) => Promise<{trigger_id: string}>;
|
||||
setChecklistItemCommand: (playbookRunID: string, checklistNum: number, itemNum: number, command: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -108,10 +109,21 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
|
||||
// Slash Commands
|
||||
runChecklistItemSlashCommand = async (playbookRunId: string, checklistNumber: number, itemNumber: number) => {
|
||||
await this.doFetch(
|
||||
const data = await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNumber}/item/${itemNumber}/run`,
|
||||
{method: 'post'},
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
setChecklistItemCommand = async (playbookRunID: string, checklistNum: number, itemNum: number, command: string) => {
|
||||
const data = await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunID)}/checklists/${checklistNum}/item/${itemNum}/command`,
|
||||
{method: 'put', body: {command}},
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
12
app/products/playbooks/constants/screens.ts
Normal file
12
app/products/playbooks/constants/screens.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const PLAYBOOKS_RUNS = 'PlaybookRuns';
|
||||
export const PLAYBOOK_RUN = 'PlaybookRun';
|
||||
export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
|
||||
|
||||
export default {
|
||||
PLAYBOOKS_RUNS,
|
||||
PLAYBOOK_RUN,
|
||||
PLAYBOOK_EDIT_COMMAND,
|
||||
};
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {act, renderWithEverything} from '@test/intl-test-helper';
|
||||
|
||||
import EditCommand from './edit_command';
|
||||
import EditCommandForm from './edit_command_form';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
// Mock the EditCommandForm component
|
||||
jest.mock('./edit_command_form');
|
||||
jest.mocked(EditCommandForm).mockImplementation(
|
||||
(props) => React.createElement('EditCommandForm', {testID: 'edit-command-form', ...props}),
|
||||
);
|
||||
|
||||
jest.mock('@hooks/navigation_button_pressed');
|
||||
jest.mock('@hooks/android_back_handler');
|
||||
|
||||
const serverUrl = 'some.server.url';
|
||||
|
||||
describe('EditCommand', () => {
|
||||
let database: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof EditCommand> {
|
||||
return {
|
||||
componentId: 'EditCommand' as const,
|
||||
savedCommand: '/test command',
|
||||
updateCommand: jest.fn(),
|
||||
channelId: 'channel-123',
|
||||
};
|
||||
}
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
const editCommandForm = getByTestId('edit-command-form');
|
||||
expect(editCommandForm).toBeTruthy();
|
||||
expect(editCommandForm.props.command).toBe(props.savedCommand);
|
||||
expect(editCommandForm.props.onCommandChange).toBeDefined();
|
||||
expect(editCommandForm.props.channelId).toBe(props.channelId);
|
||||
});
|
||||
|
||||
it('renders correctly without saved command', () => {
|
||||
const props = getBaseProps();
|
||||
props.savedCommand = undefined;
|
||||
const {getByTestId} = renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
const editCommandForm = getByTestId('edit-command-form');
|
||||
expect(editCommandForm.props.command).toBe('');
|
||||
});
|
||||
|
||||
it('sets up navigation buttons correctly', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
id: 'save-command',
|
||||
enabled: false,
|
||||
showAsAction: 'always',
|
||||
text: 'Save',
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('enables save button when command changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
const editCommandForm = getByTestId('edit-command-form');
|
||||
|
||||
// Initially, save button should be disabled
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({enabled: false})],
|
||||
});
|
||||
|
||||
jest.mocked(setButtons).mockClear();
|
||||
|
||||
act(() => {
|
||||
// Simulate command change
|
||||
editCommandForm.props.onCommandChange('/new command');
|
||||
});
|
||||
|
||||
// Save button should now be enabled
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({enabled: true})],
|
||||
});
|
||||
|
||||
jest.mocked(setButtons).mockClear();
|
||||
|
||||
act(() => {
|
||||
// Change back to original
|
||||
editCommandForm.props.onCommandChange(props.savedCommand);
|
||||
});
|
||||
|
||||
// Save button should now be enabled
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({enabled: false})],
|
||||
});
|
||||
});
|
||||
|
||||
it('calls updateCommand and closes when save button is pressed', () => {
|
||||
const props = getBaseProps();
|
||||
const updateCommandMock = jest.fn();
|
||||
props.updateCommand = updateCommandMock;
|
||||
|
||||
renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
// Get the onEditCommand function that was passed to useNavButtonPressed
|
||||
const onEditCommand = jest.mocked(useNavButtonPressed).mock.calls[0][2];
|
||||
|
||||
// Simulate save button press
|
||||
onEditCommand();
|
||||
|
||||
expect(updateCommandMock).toHaveBeenCalledWith(props.savedCommand);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(props.componentId);
|
||||
});
|
||||
|
||||
it('closes without updating when close is called', () => {
|
||||
const props = getBaseProps();
|
||||
const updateCommandMock = jest.fn();
|
||||
props.updateCommand = updateCommandMock;
|
||||
|
||||
renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
// Get the handleClose function that was passed to useAndroidHardwareBackHandler
|
||||
const handleClose = jest.mocked(useAndroidHardwareBackHandler).mock.calls[0][1];
|
||||
|
||||
// Simulate close
|
||||
handleClose();
|
||||
|
||||
expect(updateCommandMock).not.toHaveBeenCalled();
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(props.componentId);
|
||||
});
|
||||
|
||||
it('updates command state when onCommandChange is called', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<EditCommand {...props}/>, {database});
|
||||
|
||||
const editCommandForm = getByTestId('edit-command-form');
|
||||
|
||||
// Change command
|
||||
const newCommand = '/new command';
|
||||
act(() => {
|
||||
editCommandForm.props.onCommandChange(newCommand);
|
||||
});
|
||||
|
||||
const updatedForm = getByTestId('edit-command-form');
|
||||
expect(updatedForm.props.command).toBe(newCommand);
|
||||
});
|
||||
});
|
||||
99
app/products/playbooks/screens/edit_command/edit_command.tsx
Normal file
99
app/products/playbooks/screens/edit_command/edit_command.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, StyleSheet, View} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
|
||||
import EditCommandForm from './edit_command_form';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
savedCommand?: string;
|
||||
updateCommand: (command: string) => void;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ID = 'save-command';
|
||||
|
||||
const close = (componentId: AvailableScreens): void => {
|
||||
Keyboard.dismiss();
|
||||
popTopScreen(componentId);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const CreateOrEditChannel = ({
|
||||
componentId,
|
||||
savedCommand,
|
||||
updateCommand,
|
||||
channelId,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const theme = useTheme();
|
||||
|
||||
const [canSave, setCanSave] = useState(false);
|
||||
const [command, setCommand] = useState<string>(savedCommand || '');
|
||||
|
||||
const rightButton = useMemo(() => {
|
||||
const base = buildNavigationButton(
|
||||
SAVE_BUTTON_ID,
|
||||
'playbooks.edit_command.save.button',
|
||||
undefined,
|
||||
formatMessage({id: 'playbooks.edit_command.save.button', defaultMessage: 'Save'}),
|
||||
);
|
||||
base.enabled = canSave;
|
||||
base.color = theme.sidebarHeaderTextColor;
|
||||
return base;
|
||||
}, [formatMessage, canSave, theme.sidebarHeaderTextColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [rightButton],
|
||||
});
|
||||
}, [rightButton, componentId]);
|
||||
|
||||
useEffect(() => {
|
||||
setCanSave(command !== savedCommand);
|
||||
}, [command, savedCommand]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
const onEditCommand = useCallback(() => {
|
||||
close(componentId);
|
||||
updateCommand(command);
|
||||
}, [command, componentId, updateCommand]);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, onEditCommand, [onEditCommand]);
|
||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
||||
return (
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.container}
|
||||
>
|
||||
<EditCommandForm
|
||||
command={command}
|
||||
onCommandChange={setCommand}
|
||||
channelId={channelId}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrEditChannel;
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import FloatingTextInput from '@components/floating_text_input_label';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
|
||||
import EditCommandForm from './edit_command_form';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
jest.mock('@components/floating_text_input_label', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(FloatingTextInput).mockImplementation((props) => React.createElement('FloatingTextInput', {...props}));
|
||||
|
||||
jest.mock('@components/autocomplete', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(Autocomplete).mockImplementation((props) => React.createElement('Autocomplete', {testID: 'autocomplete', ...props}));
|
||||
|
||||
const serverUrl = 'some.server.url';
|
||||
|
||||
describe('EditCommandForm', () => {
|
||||
let database: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database;
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof EditCommandForm> {
|
||||
return {
|
||||
command: '/test command',
|
||||
onCommandChange: jest.fn(),
|
||||
channelId: 'channel-123',
|
||||
};
|
||||
}
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithEverything(<EditCommandForm {...props}/>, {database});
|
||||
|
||||
const floatingTextInput = getByTestId('playbooks.edit_command.input');
|
||||
expect(floatingTextInput).toBeTruthy();
|
||||
expect(floatingTextInput).toHaveProp('value', props.command);
|
||||
expect(floatingTextInput).toHaveProp('onChangeText', props.onCommandChange);
|
||||
expect(floatingTextInput).toHaveProp('autoCorrect', false);
|
||||
expect(floatingTextInput).toHaveProp('autoCapitalize', 'none');
|
||||
expect(floatingTextInput).toHaveProp('disableFullscreenUI', true);
|
||||
expect(floatingTextInput).toHaveProp('showErrorIcon', false);
|
||||
expect(floatingTextInput).toHaveProp('spellCheck', false);
|
||||
expect(floatingTextInput).toHaveProp('disableFullscreenUI', true);
|
||||
|
||||
const autocomplete = getByTestId('autocomplete');
|
||||
expect(autocomplete).toBeTruthy();
|
||||
expect(autocomplete).toHaveProp('value', props.command);
|
||||
expect(autocomplete).toHaveProp('updateValue', props.onCommandChange);
|
||||
expect(autocomplete).toHaveProp('cursorPosition', props.command.length);
|
||||
expect(autocomplete).toHaveProp('nestedScrollEnabled', true);
|
||||
expect(autocomplete).toHaveProp('shouldDirectlyReact', false);
|
||||
expect(autocomplete).toHaveProp('channelId', props.channelId);
|
||||
expect(autocomplete).toHaveProp('autocompleteProviders', {
|
||||
user: false,
|
||||
channel: false,
|
||||
emoji: false,
|
||||
slash: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onCommandChange when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const onCommandChangeMock = jest.fn();
|
||||
props.onCommandChange = onCommandChangeMock;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<EditCommandForm {...props}/>, {database});
|
||||
|
||||
const floatingTextInput = getByTestId('playbooks.edit_command.input');
|
||||
const newCommand = '/new command';
|
||||
floatingTextInput.props.onChangeText(newCommand);
|
||||
|
||||
expect(onCommandChangeMock).toHaveBeenCalledWith(newCommand);
|
||||
});
|
||||
|
||||
it('calls onCommandChange when autocomplete updates value', () => {
|
||||
const props = getBaseProps();
|
||||
const onCommandChangeMock = jest.fn();
|
||||
props.onCommandChange = onCommandChangeMock;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<EditCommandForm {...props}/>, {database});
|
||||
|
||||
const autocomplete = getByTestId('autocomplete');
|
||||
const newCommand = '/autocomplete command';
|
||||
autocomplete.props.updateValue(newCommand);
|
||||
|
||||
expect(onCommandChangeMock).toHaveBeenCalledWith(newCommand);
|
||||
});
|
||||
|
||||
it('updates cursor position when command changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, rerender} = renderWithEverything(<EditCommandForm {...props}/>, {database});
|
||||
|
||||
const autocomplete = getByTestId('autocomplete');
|
||||
expect(autocomplete.props.cursorPosition).toBe(props.command.length);
|
||||
|
||||
// Update command
|
||||
const newProps = {...props, command: '/updated command'};
|
||||
rerender(<EditCommandForm {...newProps}/>);
|
||||
|
||||
const updatedAutocomplete = getByTestId('autocomplete');
|
||||
expect(updatedAutocomplete.props.cursorPosition).toBe(newProps.command.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
type LayoutChangeEvent,
|
||||
View,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import {SafeAreaView, type Edges} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import FloatingTextInput from '@components/floating_text_input_label';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
|
||||
import {
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from '@utils/theme';
|
||||
|
||||
const BOTTOM_AUTOCOMPLETE_SEPARATION = Platform.select({ios: 10, default: 10});
|
||||
const LIST_PADDING = 32;
|
||||
const AUTOCOMPLETE_ADJUST = 5;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
mainView: {
|
||||
paddingVertical: LIST_PADDING,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
command: string;
|
||||
onCommandChange: (text: string) => void;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const edges: Edges = ['bottom', 'left', 'right'];
|
||||
const autocompleteProviders = {
|
||||
user: false,
|
||||
channel: false,
|
||||
emoji: false,
|
||||
slash: true,
|
||||
};
|
||||
|
||||
export default function EditCommandForm({
|
||||
command,
|
||||
onCommandChange,
|
||||
channelId,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const theme = useTheme();
|
||||
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
|
||||
const [commandFieldHeight, setCommandFieldHeight] = useState(0);
|
||||
|
||||
const labelCommand = formatMessage({id: 'playbooks.edit_command.label', defaultMessage: 'Command'});
|
||||
const placeholderCommand = formatMessage({id: 'playbooks.edit_command.placeholder', defaultMessage: 'Type a command here'});
|
||||
|
||||
const onLayoutCommand = useCallback((e: LayoutChangeEvent) => {
|
||||
setCommandFieldHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
const onLayoutWrapper = useCallback((e: LayoutChangeEvent) => {
|
||||
setWrapperHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const spaceOnTop = LIST_PADDING - AUTOCOMPLETE_ADJUST;
|
||||
const spaceOnBottom = (wrapperHeight) - (LIST_PADDING + commandFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION);
|
||||
|
||||
const bottomPosition = (LIST_PADDING + commandFieldHeight);
|
||||
const topPosition = (wrapperHeight + AUTOCOMPLETE_ADJUST) - LIST_PADDING;
|
||||
const autocompletePosition = spaceOnBottom > spaceOnTop ? bottomPosition : topPosition;
|
||||
const autocompleteAvailableSpace = spaceOnBottom > spaceOnTop ? spaceOnBottom : spaceOnTop;
|
||||
const growDown = spaceOnBottom > spaceOnTop;
|
||||
|
||||
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.container}
|
||||
testID='playbooks.edit_command.form'
|
||||
onLayout={onLayoutWrapper}
|
||||
>
|
||||
<View style={styles.mainView}>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
disableFullscreenUI={true}
|
||||
label={labelCommand}
|
||||
placeholder={placeholderCommand}
|
||||
onChangeText={onCommandChange}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
showErrorIcon={false}
|
||||
spellCheck={false}
|
||||
testID='playbooks.edit_command.input'
|
||||
value={command}
|
||||
theme={theme}
|
||||
onLayout={onLayoutCommand}
|
||||
/>
|
||||
</View>
|
||||
<Autocomplete
|
||||
position={animatedAutocompletePosition}
|
||||
updateValue={onCommandChange}
|
||||
cursorPosition={command.length}
|
||||
value={command}
|
||||
nestedScrollEnabled={true}
|
||||
availableSpace={animatedAutocompleteAvailableSpace}
|
||||
shouldDirectlyReact={false}
|
||||
growDown={growDown}
|
||||
channelId={channelId}
|
||||
autocompleteProviders={autocompleteProviders}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
4
app/products/playbooks/screens/edit_command/index.tsx
Normal file
4
app/products/playbooks/screens/edit_command/index.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './edit_command';
|
||||
66
app/products/playbooks/screens/index.test.tsx
Normal file
66
app/products/playbooks/screens/index.test.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {withServerDatabase} from '@database/components';
|
||||
import Screens from '@playbooks/constants/screens';
|
||||
import {render} from '@test/intl-test-helper';
|
||||
|
||||
import EditCommand from './edit_command';
|
||||
import PlaybookRun from './playbook_run';
|
||||
import PlaybookRuns from './playbooks_runs';
|
||||
|
||||
import {loadPlaybooksScreen} from '.';
|
||||
|
||||
jest.mock('@database/components', () => ({
|
||||
withServerDatabase: jest.fn(),
|
||||
}));
|
||||
jest.mocked(withServerDatabase).mockImplementation((Component) => function MockedWithServerDatabase(props: any) {
|
||||
return <View testID='withDatabase'><Component {...props}/></View>;
|
||||
});
|
||||
|
||||
jest.mock('@playbooks/screens/playbooks_runs', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(PlaybookRuns).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_RUNS}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/playbook_run', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(PlaybookRun).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_RUN}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/edit_command', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(EditCommand).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_EDIT_COMMAND}</Text>);
|
||||
|
||||
describe('Screen Registration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each(Object.values(Screens))('register screen %s when requested', (screenName) => {
|
||||
const Screen = loadPlaybooksScreen(screenName);
|
||||
expect(Screen).toBeDefined();
|
||||
|
||||
// the previous expect will fail if the screen is not defined.
|
||||
// This is a workaround to keep the types correct.
|
||||
if (!Screen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {getByTestId, getByText} = render(<Screen/>);
|
||||
|
||||
expect(getByTestId('withDatabase')).toBeDefined();
|
||||
expect(getByText(screenName)).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns undefined for unknown screen names', () => {
|
||||
const Screen = loadPlaybooksScreen('unknown');
|
||||
expect(Screen).toBeUndefined();
|
||||
});
|
||||
});
|
||||
18
app/products/playbooks/screens/index.tsx
Normal file
18
app/products/playbooks/screens/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withServerDatabase} from '@database/components';
|
||||
import Screens from '@playbooks/constants/screens';
|
||||
|
||||
export function loadPlaybooksScreen(screenName: string | number) {
|
||||
switch (screenName) {
|
||||
case Screens.PLAYBOOKS_RUNS:
|
||||
return withServerDatabase(require('@playbooks/screens/playbooks_runs').default);
|
||||
case Screens.PLAYBOOK_RUN:
|
||||
return withServerDatabase(require('@playbooks/screens/playbook_run').default);
|
||||
case Screens.PLAYBOOK_EDIT_COMMAND:
|
||||
return withServerDatabase(require('@playbooks/screens/edit_command').default);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,10 @@
|
|||
import {act, fireEvent, waitFor} from '@testing-library/react-native';
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {handleCallsSlashCommand} from '@calls/actions';
|
||||
import BaseChip from '@components/chips/base_chip';
|
||||
import UserChip from '@components/chips/user_chip';
|
||||
import {Preferences} from '@constants';
|
||||
import {General, Preferences} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {runChecklistItem, skipChecklistItem, updateChecklistItem} from '@playbooks/actions/remote/checklist';
|
||||
import {bottomSheet, openUserProfileModal, popTo} from '@screens/navigation';
|
||||
|
|
@ -34,6 +35,7 @@ jest.mocked(BaseChip).mockImplementation((props) => React.createElement('BaseChi
|
|||
jest.mock('./checklist_item_bottom_sheet');
|
||||
jest.mocked(ChecklistItemBottomSheet).mockImplementation((props) => React.createElement('ChecklistItemBottomSheet', {...props, testID: 'checklist-item-bottom-sheet-component'}));
|
||||
|
||||
jest.mock('@calls/actions');
|
||||
jest.mock('@playbooks/actions/remote/checklist');
|
||||
jest.mock('@utils/snack_bar');
|
||||
|
||||
|
|
@ -60,6 +62,8 @@ describe('ChecklistItem', () => {
|
|||
itemNumber: 0,
|
||||
playbookRunId: 'run-id-1',
|
||||
isDisabled: false,
|
||||
currentUserId: 'user-id-1',
|
||||
channelType: General.OPEN_CHANNEL,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +307,7 @@ describe('ChecklistItem', () => {
|
|||
expect(chip.props.prefix.props.style).toBeDefined();
|
||||
|
||||
let resolve: (value: {data: boolean}) => void;
|
||||
jest.mocked(runChecklistItem).mockImplementation(() => new Promise((r) => {
|
||||
jest.mocked(runChecklistItem).mockImplementationOnce(() => new Promise((r) => {
|
||||
resolve = r;
|
||||
}));
|
||||
|
||||
|
|
@ -325,6 +329,29 @@ describe('ChecklistItem', () => {
|
|||
await waitFor(() => {
|
||||
expect(getByTestId('base-chip-component')).toBeVisible();
|
||||
expect(popTo).toHaveBeenCalledWith('Channel');
|
||||
expect(handleCallsSlashCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call handleCallsSlashCommand when the command is a call command', async () => {
|
||||
const props = getBaseProps();
|
||||
const item = TestHelper.fakePlaybookChecklistItemModel({});
|
||||
item.command = '/call start';
|
||||
props.item = item;
|
||||
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItem {...props}/>);
|
||||
|
||||
const chip = getByTestId('base-chip-component');
|
||||
jest.mocked(runChecklistItem).mockResolvedValueOnce({data: true});
|
||||
jest.mocked(handleCallsSlashCommand).mockResolvedValueOnce({handled: true});
|
||||
|
||||
act(() => {
|
||||
chip.props.onPress();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(runChecklistItem).toHaveBeenCalled();
|
||||
expect(handleCallsSlashCommand).toHaveBeenCalledWith(item.command, serverUrl, props.channelId, props.channelType, '', props.currentUserId, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -352,6 +379,8 @@ describe('ChecklistItem', () => {
|
|||
|
||||
it('opens the bottom sheet when the item is pressed', async () => {
|
||||
const props = getBaseProps();
|
||||
props.itemNumber = 2;
|
||||
props.checklistNumber = 4;
|
||||
|
||||
const {getByText} = renderWithIntl(<ChecklistItem {...props}/>);
|
||||
|
||||
|
|
@ -371,8 +400,11 @@ describe('ChecklistItem', () => {
|
|||
const {getByTestId} = renderWithIntl(<BottomSheetComponent/>);
|
||||
const bottomSheetRenderedComponent = getByTestId('checklist-item-bottom-sheet-component');
|
||||
expect(bottomSheetRenderedComponent.props.item).toBe(props.item);
|
||||
expect(bottomSheetRenderedComponent.props.assignee).toBe(props.assignee);
|
||||
expect(bottomSheetRenderedComponent.props.teammateNameDisplay).toBe(props.teammateNameDisplay);
|
||||
expect(bottomSheetRenderedComponent.props.runId).toBe(props.playbookRunId);
|
||||
expect(bottomSheetRenderedComponent.props.checklistNumber).toBe(props.checklistNumber);
|
||||
expect(bottomSheetRenderedComponent.props.itemNumber).toBe(props.itemNumber);
|
||||
expect(bottomSheetRenderedComponent.props.channelId).toBe(props.channelId);
|
||||
|
||||
bottomSheetRenderedComponent.props.onCheck();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import React, {useCallback, useMemo, useState} from 'react';
|
|||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, ActivityIndicator} from 'react-native';
|
||||
|
||||
import {handleCallsSlashCommand} from '@calls/actions';
|
||||
import BaseChip from '@components/chips/base_chip';
|
||||
import UserChip from '@components/chips/user_chip';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
|
@ -78,6 +79,8 @@ type Props = {
|
|||
itemNumber: number;
|
||||
playbookRunId: string;
|
||||
isDisabled: boolean;
|
||||
currentUserId: string;
|
||||
channelType: ChannelType;
|
||||
}
|
||||
|
||||
const ChecklistItem = ({
|
||||
|
|
@ -89,6 +92,8 @@ const ChecklistItem = ({
|
|||
itemNumber,
|
||||
playbookRunId,
|
||||
isDisabled,
|
||||
currentUserId,
|
||||
channelType,
|
||||
}: Props) => {
|
||||
const dueDate = 'dueDate' in item ? item.dueDate : item.due_date;
|
||||
const theme = useTheme();
|
||||
|
|
@ -120,11 +125,15 @@ const ChecklistItem = ({
|
|||
const res = await runChecklistItem(serverUrl, playbookRunId, checklistNumber, itemNumber);
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
} else {
|
||||
popTo('Channel');
|
||||
setIsExecuting(false);
|
||||
return;
|
||||
}
|
||||
setIsExecuting(false);
|
||||
}, [checklistNumber, isExecuting, itemNumber, playbookRunId, serverUrl]);
|
||||
|
||||
popTo('Channel');
|
||||
if (item.command?.startsWith('/call')) {
|
||||
await handleCallsSlashCommand(item.command, serverUrl, channelId, channelType, '', currentUserId, intl);
|
||||
}
|
||||
}, [channelId, channelType, checklistNumber, currentUserId, intl, isExecuting, item.command, itemNumber, playbookRunId, serverUrl]);
|
||||
|
||||
const toggleChecked = useCallback(async () => {
|
||||
if (isChecking) {
|
||||
|
|
@ -193,15 +202,29 @@ const ChecklistItem = ({
|
|||
|
||||
const renderBottomSheet = useCallback(() => (
|
||||
<ChecklistItemBottomSheet
|
||||
runId={playbookRunId}
|
||||
checklistNumber={checklistNumber}
|
||||
itemNumber={itemNumber}
|
||||
channelId={channelId}
|
||||
item={item}
|
||||
assignee={assignee}
|
||||
onCheck={toggleChecked}
|
||||
onSkip={toggleSkipped}
|
||||
onRunCommand={executeCommand}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
), [assignee, executeCommand, item, teammateNameDisplay, toggleChecked, toggleSkipped, isDisabled]);
|
||||
), [
|
||||
playbookRunId,
|
||||
checklistNumber,
|
||||
itemNumber,
|
||||
channelId,
|
||||
item,
|
||||
toggleChecked,
|
||||
toggleSkipped,
|
||||
executeCommand,
|
||||
teammateNameDisplay,
|
||||
isDisabled,
|
||||
]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
const initialHeight = BOTTOM_SHEET_HEIGHT.base + (isDisabled ? 0 : BOTTOM_SHEET_HEIGHT.actionButtons);
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@ import {ScrollView} from 'react-native';
|
|||
import OptionBox from '@components/option_box';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
|
||||
import {setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import ChecklistItemBottomSheet from './checklist_item_bottom_sheet';
|
||||
|
||||
import type EditCommand from '@playbooks/screens/edit_command';
|
||||
|
||||
jest.mock('@hooks/device', () => ({
|
||||
useIsTablet: jest.fn(),
|
||||
}));
|
||||
|
|
@ -30,6 +33,12 @@ jest.mocked(OptionBox).mockImplementation((props) => React.createElement('Option
|
|||
jest.mock('@components/option_item');
|
||||
jest.mocked(OptionItem).mockImplementation((props) => React.createElement('OptionItem', props));
|
||||
|
||||
jest.mock('@playbooks/actions/remote/checklist');
|
||||
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn().mockReturnValue('server-url'),
|
||||
}));
|
||||
|
||||
describe('ChecklistItemBottomSheet', () => {
|
||||
const mockOnCheck = jest.fn();
|
||||
const mockOnSkip = jest.fn();
|
||||
|
|
@ -65,6 +74,10 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
|
||||
function getBaseProps(): ComponentProps<typeof ChecklistItemBottomSheet> {
|
||||
return {
|
||||
runId: 'run-1',
|
||||
checklistNumber: 1,
|
||||
itemNumber: 1,
|
||||
channelId: 'channel-1',
|
||||
item: mockItem,
|
||||
assignee: mockAssignee,
|
||||
onCheck: mockOnCheck,
|
||||
|
|
@ -287,6 +300,27 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
expect(dueDateItem.props.info).toBe('None');
|
||||
});
|
||||
|
||||
it('command is disabled when isDisabled is true', () => {
|
||||
const props = getBaseProps();
|
||||
props.isDisabled = true;
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
...props.item,
|
||||
command: 'test command',
|
||||
});
|
||||
const {getByTestId, rerender} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
let commandItem = getByTestId('checklist_item.command');
|
||||
expect(commandItem).toHaveProp('type', 'none');
|
||||
expect(commandItem).toHaveProp('action', undefined);
|
||||
|
||||
props.isDisabled = false;
|
||||
rerender(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
commandItem = getByTestId('checklist_item.command');
|
||||
expect(commandItem).toHaveProp('type', 'arrow');
|
||||
expect(commandItem).toHaveProp('action', expect.any(Function));
|
||||
});
|
||||
|
||||
it('displays correct command information', () => {
|
||||
const props = getBaseProps();
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
|
|
@ -311,6 +345,42 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
expect(commandItem.props.info).toBe('None');
|
||||
});
|
||||
|
||||
it('opens the command modal when the command is clicked', async () => {
|
||||
const props = getBaseProps();
|
||||
props.checklistNumber = 2;
|
||||
props.itemNumber = 4;
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
const commandItem = getByTestId('checklist_item.command');
|
||||
|
||||
act(() => {
|
||||
commandItem.props.action();
|
||||
});
|
||||
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
'PlaybookEditCommand',
|
||||
'Slash command',
|
||||
{
|
||||
savedCommand: 'test command',
|
||||
updateCommand: expect.any(Function),
|
||||
channelId: 'channel-1',
|
||||
},
|
||||
);
|
||||
|
||||
const updateCommand = (jest.mocked(goToScreen).mock.calls[0][2] as ComponentProps<typeof EditCommand>).updateCommand;
|
||||
await act(async () => {
|
||||
updateCommand('new command');
|
||||
});
|
||||
|
||||
expect(setChecklistItemCommand).toHaveBeenCalledWith(
|
||||
'server-url',
|
||||
'run-1',
|
||||
'item-1',
|
||||
2,
|
||||
4,
|
||||
'new command',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles user profile modal opening correctly', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
|
@ -8,13 +8,15 @@ import {View, Text} from 'react-native';
|
|||
import MenuDivider from '@components/menu_divider';
|
||||
import OptionBox from '@components/option_box';
|
||||
import OptionItem, {ITEM_HEIGHT} from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
|
||||
import {setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import Checkbox from './checkbox';
|
||||
import Checkbox from '../checkbox';
|
||||
|
||||
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
|
@ -112,6 +114,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
|||
}));
|
||||
|
||||
type Props = {
|
||||
runId: string;
|
||||
checklistNumber: number;
|
||||
itemNumber: number;
|
||||
channelId: string;
|
||||
item: PlaybookChecklistItemModel | PlaybookChecklistItem;
|
||||
assignee?: UserModel;
|
||||
onCheck: () => void;
|
||||
|
|
@ -135,6 +141,10 @@ function getDueDateInfo(intl: IntlShape, dueDate: number | undefined) {
|
|||
}
|
||||
|
||||
const ChecklistItemBottomSheet = ({
|
||||
runId,
|
||||
checklistNumber,
|
||||
itemNumber,
|
||||
channelId,
|
||||
item,
|
||||
assignee,
|
||||
onCheck,
|
||||
|
|
@ -146,6 +156,7 @@ const ChecklistItemBottomSheet = ({
|
|||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const dueDate = 'dueDate' in item ? item.dueDate : item.due_date;
|
||||
const isChecked = item.state === 'closed';
|
||||
|
|
@ -205,6 +216,22 @@ const ChecklistItemBottomSheet = ({
|
|||
});
|
||||
}, [intl, theme]);
|
||||
|
||||
const updateCommand = useCallback(async (command: string) => {
|
||||
await setChecklistItemCommand(serverUrl, runId, item.id, checklistNumber, itemNumber, command);
|
||||
}, [checklistNumber, item.id, itemNumber, runId, serverUrl]);
|
||||
|
||||
const openEditCommandModal = useCallback(() => {
|
||||
goToScreen(
|
||||
'PlaybookEditCommand',
|
||||
intl.formatMessage({id: 'playbooks.edit_command.title', defaultMessage: 'Slash command'}),
|
||||
{
|
||||
savedCommand: item.command,
|
||||
updateCommand,
|
||||
channelId,
|
||||
},
|
||||
);
|
||||
}, [intl, item.command, updateCommand, channelId]);
|
||||
|
||||
const assigneeInfo: ComponentProps<typeof OptionItem>['info'] = useMemo(() => {
|
||||
if (!assignee) {
|
||||
return intl.formatMessage(messages.none);
|
||||
|
|
@ -234,12 +261,13 @@ const ChecklistItemBottomSheet = ({
|
|||
testID='checklist_item.due_date'
|
||||
/>
|
||||
<OptionItem
|
||||
type='none'
|
||||
type={isDisabled ? 'none' : 'arrow'}
|
||||
icon='slash-forward'
|
||||
label={intl.formatMessage(messages.command)}
|
||||
info={item.command || intl.formatMessage(messages.none)}
|
||||
testID='checklist_item.command'
|
||||
longInfo={true}
|
||||
action={isDisabled ? undefined : openEditCommandModal}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item';
|
||||
import {act, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import ChecklistItemBottomSheetComponent from './checklist_item_bottom_sheet';
|
||||
|
||||
import ChecklistItemBottomSheet from './index';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
jest.mock('./checklist_item_bottom_sheet', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(ChecklistItemBottomSheetComponent).mockImplementation(
|
||||
(props) => React.createElement('ChecklistItemBottomSheet', {testID: 'checklist_item_bottom_sheet', ...props}),
|
||||
);
|
||||
|
||||
describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
||||
const serverUrl = 'server-url';
|
||||
|
||||
let database: Database;
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof ChecklistItemBottomSheet> {
|
||||
return {
|
||||
item: TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1'}),
|
||||
runId: 'run-1',
|
||||
checklistNumber: 1,
|
||||
itemNumber: 1,
|
||||
channelId: 'channel-1',
|
||||
onCheck: jest.fn(),
|
||||
onSkip: jest.fn(),
|
||||
onRunCommand: jest.fn(),
|
||||
teammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
||||
isDisabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function addItemToDatabase(item: PartialChecklistItem) {
|
||||
await operator.handlePlaybookChecklistItem({
|
||||
prepareRecordsOnly: false,
|
||||
items: [item],
|
||||
});
|
||||
}
|
||||
|
||||
async function addUserToDatabase(user: UserProfile) {
|
||||
await operator.handleUsers({
|
||||
prepareRecordsOnly: false,
|
||||
users: [user],
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
describe('with database model item', () => {
|
||||
it('should render enhanced component with database model item', async () => {
|
||||
const rawItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1', assignee_id: 'user-1'});
|
||||
await addItemToDatabase(rawItem);
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-1'}));
|
||||
|
||||
const item = await getPlaybookChecklistItemById(database, rawItem.id);
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = item!;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', item);
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
});
|
||||
|
||||
it('should handle assigneeId changes through observable', async () => {
|
||||
const rawItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1', assignee_id: 'user-1'});
|
||||
await addItemToDatabase(rawItem);
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-1'}));
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-2'}));
|
||||
|
||||
const item = await getPlaybookChecklistItemById(database, rawItem.id);
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = item!;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
await act(async () => {
|
||||
database.write(async () => {
|
||||
item?.update((i) => {
|
||||
i.assigneeId = 'user-2';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-2'}));
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle no assignee correctly', async () => {
|
||||
const rawItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1', assignee_id: ''});
|
||||
await addItemToDatabase(rawItem);
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = rawItem;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet.props.assignee).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing assignee correctly', async () => {
|
||||
const rawItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-1', assignee_id: 'missing-user-id'});
|
||||
await addItemToDatabase(rawItem);
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = rawItem;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet.props.assignee).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with API item', () => {
|
||||
it('should render enhanced component with API item', async () => {
|
||||
const apiItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-2', assignee_id: 'user-1'});
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-1'}));
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = apiItem;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', apiItem);
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
});
|
||||
|
||||
it('should handle assigneeId changes through observable', async () => {
|
||||
const apiItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-2', assignee_id: 'user-1'});
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-1'}));
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'user-2'}));
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = apiItem!;
|
||||
|
||||
const {getByTestId, rerender} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
|
||||
// Clone the objet to pass a new reference to the render
|
||||
props.item = {...props.item};
|
||||
props.item.assignee_id = 'user-2';
|
||||
rerender(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-2'}));
|
||||
});
|
||||
|
||||
it('should handle no assignee correctly', () => {
|
||||
const apiItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-2', assignee_id: ''});
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = apiItem;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', apiItem);
|
||||
expect(bottomSheet.props.assignee).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing assignee correctly', () => {
|
||||
const apiItem = TestHelper.fakePlaybookChecklistItem('checklist-id', {id: 'item-2', assignee_id: 'missing-user-id'});
|
||||
|
||||
const props = getBaseProps();
|
||||
props.item = apiItem;
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItemBottomSheet {...props}/>, {database});
|
||||
|
||||
const bottomSheet = getByTestId('checklist_item_bottom_sheet');
|
||||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', apiItem);
|
||||
expect(bottomSheet.props.assignee).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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$, switchMap} from 'rxjs';
|
||||
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
|
||||
import ChecklistItemBottomSheet, {BOTTOM_SHEET_HEIGHT} from './checklist_item_bottom_sheet';
|
||||
|
||||
import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type OwnProps = {
|
||||
item: PlaybookChecklistItemModel | PlaybookChecklistItem;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
||||
if ('observe' in item) {
|
||||
const observedItem = item.observe();
|
||||
|
||||
// We don't use assignee query from the model because if it cannot find the user
|
||||
// it will throw an error.
|
||||
const assignee = observedItem.pipe(
|
||||
switchMap((i) => {
|
||||
if (i.assigneeId) {
|
||||
return observeUser(database, i.assigneeId);
|
||||
}
|
||||
|
||||
return of$(undefined);
|
||||
}),
|
||||
);
|
||||
return {
|
||||
item: observedItem,
|
||||
assignee,
|
||||
};
|
||||
}
|
||||
|
||||
const assignee = observeUser(database, item.assignee_id);
|
||||
|
||||
return {
|
||||
item: of$(item),
|
||||
assignee,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChecklistItemBottomSheet));
|
||||
export {BOTTOM_SHEET_HEIGHT};
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import React, {act, type ComponentProps} from 'react';
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
|
@ -73,6 +74,63 @@ describe('ChecklistItem', () => {
|
|||
expect(checklistItem.props.teammateNameDisplay).toBe(General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the correct channel type', async () => {
|
||||
const props = {
|
||||
item: TestHelper.fakePlaybookChecklistItem(checklistId, {id: itemId}),
|
||||
channelId: 'channel-id',
|
||||
checklistNumber: 0,
|
||||
itemNumber: 0,
|
||||
playbookRunId: 'run-id',
|
||||
isDisabled: false,
|
||||
};
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItem {...props}/>, {database});
|
||||
|
||||
// Default value is open channel
|
||||
const checklistItem = getByTestId('checklist-item');
|
||||
expect(checklistItem.props.channelType).toBe(General.OPEN_CHANNEL);
|
||||
|
||||
await act((async () => {
|
||||
await operator.handleChannel({
|
||||
prepareRecordsOnly: false,
|
||||
channels: [TestHelper.fakeChannel({id: 'channel-id', type: General.PRIVATE_CHANNEL})],
|
||||
});
|
||||
}));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checklistItem.props.channelType).toBe(General.PRIVATE_CHANNEL);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the correct current user id', async () => {
|
||||
const props = {
|
||||
item: TestHelper.fakePlaybookChecklistItem(checklistId, {id: itemId}),
|
||||
channelId: 'channel-id',
|
||||
checklistNumber: 0,
|
||||
itemNumber: 0,
|
||||
playbookRunId: 'run-id',
|
||||
isDisabled: false,
|
||||
};
|
||||
|
||||
const {getByTestId} = renderWithEverything(<ChecklistItem {...props}/>, {database});
|
||||
|
||||
const checklistItem = getByTestId('checklist-item');
|
||||
|
||||
// Default value is empty string
|
||||
expect(checklistItem.props.currentUserId).toBe('');
|
||||
|
||||
await act((async () => {
|
||||
await operator.handleSystem({
|
||||
prepareRecordsOnly: false,
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'user-id'}],
|
||||
});
|
||||
}));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checklistItem.props.currentUserId).toBe('user-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('api run', () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeTeammateNameDisplay, observeUser} from '@queries/servers/user';
|
||||
|
||||
import ChecklistItem from './checklist_item';
|
||||
|
|
@ -13,10 +16,13 @@ import type {WithDatabaseArgs} from '@typings/database/database';
|
|||
|
||||
type OwnProps = {
|
||||
item: PlaybookChecklistItemModel | PlaybookChecklistItem;
|
||||
channelId: string;
|
||||
} & WithDatabaseArgs;
|
||||
|
||||
const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
||||
const enhanced = withObservables(['item', 'channelId'], ({item, database, channelId}: OwnProps) => {
|
||||
const teammateNameDisplay = observeTeammateNameDisplay(database);
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const channelType = observeChannel(database, channelId).pipe(switchMap((c) => of$(c?.type || General.OPEN_CHANNEL)));
|
||||
|
||||
if ('observe' in item) {
|
||||
const observedItem = item.observe();
|
||||
|
|
@ -36,6 +42,8 @@ const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
|||
item: observedItem,
|
||||
assignee,
|
||||
teammateNameDisplay,
|
||||
currentUserId,
|
||||
channelType,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +53,8 @@ const enhanced = withObservables(['item'], ({item, database}: OwnProps) => {
|
|||
item: of$(item),
|
||||
assignee,
|
||||
teammateNameDisplay,
|
||||
currentUserId,
|
||||
channelType,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import {SafeAreaProvider} from 'react-native-safe-area-context';
|
|||
|
||||
import {Screens} from '@constants';
|
||||
import {withServerDatabase} from '@database/components';
|
||||
import EditCommand from '@playbooks/screens/edit_command';
|
||||
import PlaybookRun from '@playbooks/screens/playbook_run';
|
||||
import PlaybooksRuns from '@playbooks/screens/playbooks_runs';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import EditServer from './edit_server';
|
||||
import InAppNotification from './in_app_notification';
|
||||
|
|
@ -106,6 +108,12 @@ jest.mock('@playbooks/screens/playbook_run', () => ({
|
|||
}));
|
||||
jest.mocked(PlaybookRun).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_RUN}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/edit_command', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(EditCommand).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_EDIT_COMMAND}</Text>);
|
||||
|
||||
describe('Screen Registration', () => {
|
||||
let registrator: (screenName: string) => void;
|
||||
|
||||
|
|
@ -192,6 +200,17 @@ describe('Screen Registration', () => {
|
|||
platform: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
Screens.PLAYBOOK_EDIT_COMMAND,
|
||||
{
|
||||
withServerDatabase: false,
|
||||
withGestures: true,
|
||||
withSafeAreaInsets: true,
|
||||
withManagedConfig: true,
|
||||
withIntl: true,
|
||||
platform: undefined,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
it.each(ttcc)('register screen %s when requested', (screenName, testCase) => {
|
||||
|
|
@ -228,5 +247,6 @@ describe('Screen Registration', () => {
|
|||
it('handles unknown screen names gracefully', () => {
|
||||
registrator('UNKNOWN_SCREEN');
|
||||
expect(Navigation.registerComponent).not.toHaveBeenCalled();
|
||||
expect(logDebug).toHaveBeenCalledWith('Screen not found: UNKNOWN_SCREEN');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {SafeAreaProvider} from 'react-native-safe-area-context';
|
|||
import {Screens} from '@constants';
|
||||
import {withServerDatabase} from '@database/components';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
import {loadPlaybooksScreen} from '@playbooks/screens';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
const withGestures = (Screen: React.ComponentType) => {
|
||||
return function gestureHoc(props: any) {
|
||||
|
|
@ -189,12 +191,6 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.PINNED_MESSAGES:
|
||||
screen = withServerDatabase(require('@screens/pinned_messages').default);
|
||||
break;
|
||||
case Screens.PLAYBOOKS_RUNS:
|
||||
screen = withServerDatabase(require('@playbooks/screens/playbooks_runs').default);
|
||||
break;
|
||||
case Screens.PLAYBOOK_RUN:
|
||||
screen = withServerDatabase(require('@playbooks/screens/playbook_run').default);
|
||||
break;
|
||||
case Screens.POST_OPTIONS:
|
||||
screen = withServerDatabase(require('@screens/post_options').default);
|
||||
break;
|
||||
|
|
@ -308,6 +304,14 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
break;
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
screen = loadPlaybooksScreen(screenName);
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
logDebug(`Screen not found: ${screenName}`);
|
||||
}
|
||||
|
||||
if (screen) {
|
||||
Navigation.registerComponent(screenName, () => withGestures(withSafeAreaInsets(withManagedConfig(screen))));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -998,6 +998,10 @@
|
|||
"playbooks.checklist_item.run_command": "Run command",
|
||||
"playbooks.checklist_item.skip": "Skip",
|
||||
"playbooks.checklist_item.skipped": "Skipped",
|
||||
"playbooks.edit_command.label": "Command",
|
||||
"playbooks.edit_command.placeholder": "Type a command here",
|
||||
"playbooks.edit_command.save.button": "Save",
|
||||
"playbooks.edit_command.title": "Slash command",
|
||||
"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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue