* Fix order of finished runs and add a show more button
* Add missing text
* Add prevent double tap
* Fix show more button showing when it shouldn't
* fix tests
---------
(cherry picked from commit 80e3659c53)
Co-authored-by: Daniel Espino García <larkox@gmail.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
dbfbe6dd17
commit
7996ab4fd3
8 changed files with 321 additions and 42 deletions
|
|
@ -172,7 +172,8 @@ describe('fetchFinishedRunsForChannel', () => {
|
|||
per_page: PER_PAGE_DEFAULT,
|
||||
channel_id: channelId,
|
||||
statuses: ['Finished'],
|
||||
sort: 'end_at',
|
||||
sort: 'create_at',
|
||||
direction: 'desc',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -192,7 +193,8 @@ describe('fetchFinishedRunsForChannel', () => {
|
|||
per_page: PER_PAGE_DEFAULT,
|
||||
channel_id: channelId,
|
||||
statuses: ['Finished'],
|
||||
sort: 'end_at',
|
||||
sort: 'create_at',
|
||||
direction: 'desc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ export const fetchFinishedRunsForChannel = async (serverUrl: string, channelId:
|
|||
per_page: PER_PAGE_DEFAULT,
|
||||
channel_id: channelId,
|
||||
statuses: ['Finished'],
|
||||
sort: 'end_at',
|
||||
sort: 'create_at',
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
return {runs, has_more};
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const PlaybookHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supe
|
|||
|
||||
const clauses: Q.Clause[] = [
|
||||
Q.where('end_at', Q.notEq(0)),
|
||||
Q.sortBy('end_at', 'desc'),
|
||||
Q.sortBy('create_at', 'desc'),
|
||||
];
|
||||
|
||||
if (numNewFinished < 5) {
|
||||
|
|
|
|||
|
|
@ -85,4 +85,22 @@ describe('PlaybookRuns', () => {
|
|||
expect(queryByTestId('playbook-card')).toBeNull();
|
||||
expect(getByTestId('empty-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the show more button only on finished tabs', () => {
|
||||
const {getByText, queryByText} = renderWithIntl(
|
||||
<PlaybookRuns
|
||||
allRuns={[inProgressRun, finishedRun]}
|
||||
componentId={'PlaybookRuns'}
|
||||
channelId={'channel-id-1'}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('Show More')).toBeNull();
|
||||
|
||||
act(() => {
|
||||
fireEvent.press(getByText('Finished'));
|
||||
});
|
||||
|
||||
expect(getByText('Show More')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,23 +2,22 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {FlashList, type ListRenderItem} from '@shopify/flash-list';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {defineMessage} from 'react-intl';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useTabs, {type TabDefinition} from '@hooks/use_tabs';
|
||||
import Tabs from '@hooks/use_tabs/tabs';
|
||||
import {fetchFinishedRunsForChannel} from '@playbooks/actions/remote/runs';
|
||||
import {isRunFinished} from '@playbooks/utils/run';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import EmptyState from './empty_state';
|
||||
import PlaybookCard, {CARD_HEIGHT} from './playbook_card';
|
||||
import ShowMoreButton from './show_more_button';
|
||||
|
||||
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
|
@ -72,12 +71,15 @@ const PlaybookRuns = ({
|
|||
allRuns,
|
||||
componentId,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const [fetchedFinishedRuns, setFetchedFinishedRuns] = useState<PlaybookRun[]>([]);
|
||||
|
||||
const pushNewFinishedRuns = useCallback((runs: PlaybookRun[]) => {
|
||||
setFetchedFinishedRuns((prev) => [...prev, ...runs]);
|
||||
}, []);
|
||||
|
||||
const exit = useCallback(() => {
|
||||
popTopScreen(componentId);
|
||||
}, [componentId]);
|
||||
|
|
@ -98,9 +100,6 @@ const PlaybookRuns = ({
|
|||
|
||||
return [inProgress, finished] as const;
|
||||
}, [allRuns]);
|
||||
const hasMoreFinishedRuns = useRef(true);
|
||||
const finishedRunsPage = useRef(0);
|
||||
const fetching = useRef(false);
|
||||
|
||||
const initialTab: TabsNames = inProgressRuns.length ? 'in-progress' : 'finished';
|
||||
const [activeTab, tabsProps] = useTabs<TabsNames>(initialTab, tabs);
|
||||
|
|
@ -116,6 +115,14 @@ const PlaybookRuns = ({
|
|||
|
||||
const isEmpty = data.length === 0;
|
||||
|
||||
const footerComponent = useMemo(() => (
|
||||
<ShowMoreButton
|
||||
channelId={channelId}
|
||||
addMoreRuns={pushNewFinishedRuns}
|
||||
visible={activeTab === 'finished'}
|
||||
/>
|
||||
), [channelId, pushNewFinishedRuns, activeTab]);
|
||||
|
||||
const renderItem: ListRenderItem<PlaybookRunModel> = useCallback(({item}) => {
|
||||
return (
|
||||
<PlaybookCard
|
||||
|
|
@ -125,36 +132,6 @@ const PlaybookRuns = ({
|
|||
);
|
||||
}, []);
|
||||
|
||||
const fetchFinishedRuns = useCallback(async () => {
|
||||
if (fetching.current) {
|
||||
return;
|
||||
}
|
||||
fetching.current = true;
|
||||
const {runs, has_more, error} = await fetchFinishedRunsForChannel(serverUrl, channelId, finishedRunsPage.current);
|
||||
fetching.current = false;
|
||||
if (error) {
|
||||
hasMoreFinishedRuns.current = false;
|
||||
return;
|
||||
}
|
||||
hasMoreFinishedRuns.current = has_more ?? false;
|
||||
finishedRunsPage.current++;
|
||||
if (runs?.length) {
|
||||
setFetchedFinishedRuns(runs);
|
||||
}
|
||||
}, [channelId, serverUrl]);
|
||||
|
||||
const onFinishedRunsReachEnd = useCallback(() => {
|
||||
if (hasMoreFinishedRuns.current) {
|
||||
fetchFinishedRuns();
|
||||
}
|
||||
}, [fetchFinishedRuns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'finished' && hasMoreFinishedRuns.current && !finishedRuns.length) {
|
||||
fetchFinishedRuns();
|
||||
}
|
||||
}, [activeTab, fetchFinishedRuns, finishedRuns.length]);
|
||||
|
||||
let content = (<EmptyState tab={activeTab}/>);
|
||||
if (!isEmpty) {
|
||||
content = (
|
||||
|
|
@ -164,7 +141,7 @@ const PlaybookRuns = ({
|
|||
contentContainerStyle={styles.container}
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
estimatedItemSize={CARD_HEIGHT}
|
||||
onEndReached={activeTab === 'finished' ? onFinishedRunsReachEnd : undefined}
|
||||
ListFooterComponent={footerComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, fireEvent, waitFor} from '@testing-library/react-native';
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {fetchFinishedRunsForChannel} from '@playbooks/actions/remote/runs';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import ShowMoreButton from './show_more_button';
|
||||
|
||||
jest.mock('@playbooks/actions/remote/runs');
|
||||
jest.mock('@context/server', () => ({
|
||||
useServerUrl: jest.fn(() => 'https://test.server.com'),
|
||||
}));
|
||||
|
||||
describe('ShowMoreButton', () => {
|
||||
const mockFetchFinishedRunsForChannel = jest.mocked(fetchFinishedRunsForChannel);
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof ShowMoreButton> {
|
||||
return {
|
||||
channelId: 'test-channel-id',
|
||||
addMoreRuns: jest.fn(),
|
||||
visible: true,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly when hasMore is true', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
const button = getByText('Show More');
|
||||
expect(button).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does not render when hasMore is false', async () => {
|
||||
const props = getBaseProps();
|
||||
mockFetchFinishedRunsForChannel.mockResolvedValueOnce({
|
||||
runs: [],
|
||||
has_more: false,
|
||||
});
|
||||
|
||||
const {getByText, queryByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
// Initially renders the button
|
||||
expect(getByText('Show More')).toBeTruthy();
|
||||
|
||||
// Click the button to trigger the fetch
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
// Wait for the component to update and hide
|
||||
await waitFor(() => {
|
||||
expect(queryByText('Show More')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls fetchFinishedRunsForChannel with correct parameters when button is pressed', async () => {
|
||||
const props = getBaseProps();
|
||||
mockFetchFinishedRunsForChannel.mockResolvedValueOnce({
|
||||
runs: [],
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const {getByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
expect(mockFetchFinishedRunsForChannel).toHaveBeenCalledWith(
|
||||
'https://test.server.com',
|
||||
'test-channel-id',
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it('increments page number on subsequent calls', async () => {
|
||||
const props = getBaseProps();
|
||||
mockFetchFinishedRunsForChannel.
|
||||
mockResolvedValueOnce({
|
||||
runs: [],
|
||||
has_more: true,
|
||||
}).
|
||||
mockResolvedValueOnce({
|
||||
runs: [],
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const {getByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
// First click
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
expect(mockFetchFinishedRunsForChannel).toHaveBeenCalledWith(
|
||||
'https://test.server.com',
|
||||
'test-channel-id',
|
||||
0,
|
||||
);
|
||||
|
||||
// Wait for DELAY before second click to simulate the double tap prevention
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800)); // DELAY is 750ms
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
expect(mockFetchFinishedRunsForChannel).toHaveBeenCalledWith(
|
||||
'https://test.server.com',
|
||||
'test-channel-id',
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls addMoreRuns when runs are returned', async () => {
|
||||
const props = getBaseProps();
|
||||
const mockRuns = [
|
||||
TestHelper.fakePlaybookRun({id: 'run-1'}),
|
||||
TestHelper.fakePlaybookRun({id: 'run-2'}),
|
||||
];
|
||||
mockFetchFinishedRunsForChannel.mockResolvedValueOnce({
|
||||
runs: mockRuns,
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const {getByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(props.addMoreRuns).toHaveBeenCalledWith(mockRuns);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call addMoreRuns when no runs are returned', async () => {
|
||||
const props = getBaseProps();
|
||||
mockFetchFinishedRunsForChannel.mockResolvedValueOnce({
|
||||
runs: [],
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const {getByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(props.addMoreRuns).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets hasMore to false when API returns an error', async () => {
|
||||
const props = getBaseProps();
|
||||
mockFetchFinishedRunsForChannel.mockResolvedValueOnce({
|
||||
error: new Error('API Error'),
|
||||
});
|
||||
|
||||
const {getByText, queryByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(queryByText('Show More')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates runs and maintains hasMore when API returns runs with has_more true', async () => {
|
||||
const props = getBaseProps();
|
||||
const mockRuns = [TestHelper.fakePlaybookRun({id: 'run-1'})];
|
||||
mockFetchFinishedRunsForChannel.mockResolvedValueOnce({
|
||||
runs: mockRuns,
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const {getByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.press(getByText('Show More'));
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
expect(props.addMoreRuns).toHaveBeenCalledWith(mockRuns);
|
||||
expect(getByText('Show More')).toBeTruthy(); // Button should still be visible
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render when visible is false', () => {
|
||||
const props = getBaseProps();
|
||||
props.visible = false;
|
||||
|
||||
const {queryByText} = renderWithIntl(<ShowMoreButton {...props}/>);
|
||||
expect(queryByText('Show More')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import {fetchFinishedRunsForChannel} from '@playbooks/actions/remote/runs';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
addMoreRuns: (runs: PlaybookRun[]) => void;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingTop: 16,
|
||||
},
|
||||
});
|
||||
|
||||
const ShowMoreButton = ({
|
||||
channelId,
|
||||
addMoreRuns,
|
||||
visible,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const page = useRef(0);
|
||||
|
||||
const fetchFinishedRuns = usePreventDoubleTap(useCallback(async () => {
|
||||
if (fetching) {
|
||||
return;
|
||||
}
|
||||
setFetching(true);
|
||||
const {runs, has_more, error} = await fetchFinishedRunsForChannel(serverUrl, channelId, page.current);
|
||||
setFetching(false);
|
||||
if (error) {
|
||||
setHasMore(false);
|
||||
return;
|
||||
}
|
||||
setHasMore(has_more ?? false);
|
||||
page.current++;
|
||||
if (runs?.length) {
|
||||
addMoreRuns(runs);
|
||||
}
|
||||
}, [channelId, fetching, serverUrl, addMoreRuns]));
|
||||
|
||||
if (!hasMore || !visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button
|
||||
text={intl.formatMessage({id: 'playbooks.runs.show_more', defaultMessage: 'Show More'})}
|
||||
emphasis='tertiary'
|
||||
onPress={fetchFinishedRuns}
|
||||
theme={theme}
|
||||
showLoader={fetching}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowMoreButton;
|
||||
|
|
@ -1014,6 +1014,7 @@
|
|||
"playbooks.runs.finished.title": "No finished runs",
|
||||
"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",
|
||||
"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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue