[MM-67361] Hide playbook (#9478)

* feat: hide Playbooks button when no running playbook runs in team

The PlaybooksButton on the home screen channel list now only appears
when there is at least one in-progress playbook run in the current team,
in addition to requiring the playbooks feature to be enabled.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* improve team query

* test: add unit tests for observeHasRunningPlaybookRunsInTeam query

Add tests to verify the new query correctly:
- Returns true when running playbook runs exist (end_at = 0)
- Returns false when all runs are finished (end_at > 0)
- Returns false when no runs exist
- Filters by team_id correctly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Guillermo Vayá 2026-01-29 11:58:58 +01:00 committed by GitHub
parent fbaf13c7aa
commit d014856a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 13 deletions

View file

@ -3,7 +3,7 @@
/* eslint-disable max-lines */
import {of as of$} from 'rxjs';
import {of as of$, firstValueFrom} from 'rxjs';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
@ -18,6 +18,7 @@ import {
getLastPlaybookRunsFetchAt,
queryParticipantsFromAPIRun,
observeParticipantsIdsFromPlaybookModel,
observeHasRunningPlaybookRunsInTeam,
} from './run';
import type ServerDataOperator from '@database/operator/server_data_operator';
@ -763,4 +764,111 @@ describe('Playbook Run Queries', () => {
expect(runIds).not.toContain(mockRuns[1].id);
});
});
describe('observeHasRunningPlaybookRunsInTeam', () => {
it('should return true when there are running playbook runs in the team', async () => {
const teamId = 'team-id-1';
const mockRuns = TestHelper.createPlaybookRuns(1, 0, 0).map((run) => ({
...run,
team_id: teamId,
end_at: 0,
}));
await operator.handlePlaybookRun({
runs: mockRuns,
prepareRecordsOnly: false,
removeAssociatedRecords: false,
});
const result = await firstValueFrom(observeHasRunningPlaybookRunsInTeam(operator.database, teamId));
expect(result).toBe(true);
});
it('should return false when all runs in the team are finished', async () => {
const teamId = 'team-id-1';
const mockRuns = TestHelper.createPlaybookRuns(2, 0, 0).map((run) => ({
...run,
team_id: teamId,
end_at: 1620000000000,
}));
await operator.handlePlaybookRun({
runs: mockRuns,
prepareRecordsOnly: false,
removeAssociatedRecords: false,
});
const result = await firstValueFrom(observeHasRunningPlaybookRunsInTeam(operator.database, teamId));
expect(result).toBe(false);
});
it('should return false when no runs exist in the team', async () => {
const teamId = 'team-id-1';
const result = await firstValueFrom(observeHasRunningPlaybookRunsInTeam(operator.database, teamId));
expect(result).toBe(false);
});
it('should only count runs from the specified team', async () => {
const teamId = 'team-id-1';
const otherTeamId = 'team-id-2';
const mockRuns = [
{
...TestHelper.createPlaybookRuns(1, 0, 0)[0],
id: 'run-1',
team_id: teamId,
end_at: 1620000000000, // Finished in target team
},
{
...TestHelper.createPlaybookRuns(1, 0, 0)[0],
id: 'run-2',
team_id: otherTeamId,
end_at: 0, // Running in other team
},
];
await operator.handlePlaybookRun({
runs: mockRuns,
prepareRecordsOnly: false,
removeAssociatedRecords: false,
});
const result = await firstValueFrom(observeHasRunningPlaybookRunsInTeam(operator.database, teamId));
expect(result).toBe(false);
});
it('should return true when at least one run is in progress among mixed runs', async () => {
const teamId = 'team-id-1';
const mockRuns = [
{
...TestHelper.createPlaybookRuns(1, 0, 0)[0],
id: 'run-1',
team_id: teamId,
end_at: 1620000000000, // Finished
},
{
...TestHelper.createPlaybookRuns(1, 0, 0)[0],
id: 'run-2',
team_id: teamId,
end_at: 0, // Running
},
];
await operator.handlePlaybookRun({
runs: mockRuns,
prepareRecordsOnly: false,
removeAssociatedRecords: false,
});
const result = await firstValueFrom(observeHasRunningPlaybookRunsInTeam(operator.database, teamId));
expect(result).toBe(true);
});
});
});

View file

@ -97,3 +97,14 @@ export const queryPlaybookRunsByParticipantAndTeam = (database: Database, partic
Q.sortBy('create_at', 'desc'),
);
};
export const observeHasRunningPlaybookRunsInTeam = (database: Database, teamId: string) => {
return database.get<PlaybookRunModel>(PLAYBOOK_RUN).query(
Q.and(
Q.where('team_id', teamId),
Q.where('end_at', Q.eq(0)),
),
).observeCount().pipe(
switchMap((count) => of$(count > 0)),
);
};

View file

@ -38,7 +38,7 @@ type ChannelListProps = {
scheduledPostHasError: boolean;
lastChannelId?: string;
scheduledPostsEnabled?: boolean;
playbooksEnabled?: boolean;
showPlaybooksButton?: boolean;
};
const getTabletWidth = (moreThanOneTeam: boolean) => {
@ -57,7 +57,7 @@ const CategoriesList = ({
scheduledPostHasError,
lastChannelId,
scheduledPostsEnabled,
playbooksEnabled,
showPlaybooksButton,
}: ChannelListProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
@ -128,14 +128,14 @@ const CategoriesList = ({
}, [activeScreen, draftsCount, isTablet, scheduledPostCount, scheduledPostHasError, scheduledPostsEnabled]);
const playbooksButtonComponent = useMemo(() => {
if (!playbooksEnabled) {
if (!showPlaybooksButton) {
return null;
}
return (
<PlaybooksButton/>
);
}, [playbooksEnabled]);
}, [showPlaybooksButton]);
const content = useMemo(() => {
if (!hasChannels) {

View file

@ -171,7 +171,7 @@ describe('components/categories_list', () => {
expect(wrapper.queryByText('Drafts')).not.toBeTruthy();
});
it('should not render channel list with Playbooks menu if playbooks feature is disabled', () => {
it('should not render Playbooks menu when showPlaybooksButton is false', () => {
const wrapper = renderWithEverything(
<CategoriesList
moreThanOneTeam={false}
@ -179,14 +179,14 @@ describe('components/categories_list', () => {
draftsCount={0}
scheduledPostCount={0}
scheduledPostHasError={false}
playbooksEnabled={false}
showPlaybooksButton={false}
/>,
{database},
);
expect(wrapper.queryByText('Playbook checklists')).not.toBeTruthy();
expect(wrapper.queryByText('Playbook checklists')).toBeNull();
});
it('should render channel list with Playbooks menu if playbooks feature is enabled', () => {
it('should render Playbooks menu when showPlaybooksButton is true', () => {
const wrapper = renderWithEverything(
<CategoriesList
moreThanOneTeam={false}
@ -194,7 +194,7 @@ describe('components/categories_list', () => {
draftsCount={0}
scheduledPostCount={0}
scheduledPostHasError={false}
playbooksEnabled={true}
showPlaybooksButton={true}
/>,
{database},
);

View file

@ -2,9 +2,10 @@
// See LICENSE.txt for license information.
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of} from 'rxjs';
import {combineLatest, of} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeHasRunningPlaybookRunsInTeam} from '@playbooks/database/queries/run';
import {observeIsPlaybooksEnabled} from '@playbooks/database/queries/version';
import {observeDraftCount} from '@queries/servers/drafts';
import {observeScheduledPostEnabled, observeScheduledPostsForTeam} from '@queries/servers/scheduled_post';
@ -28,7 +29,13 @@ const enchanced = withObservables([], ({database}: WithDatabaseArgs) => {
switchMap((scheduledPosts) => of(hasScheduledPostError(scheduledPosts))),
);
const scheduledPostsEnabled = observeScheduledPostEnabled(database);
const playbooksEnabled = observeIsPlaybooksEnabled(database);
const showPlaybooksButton = currentTeamId.pipe(
switchMap((teamId) => combineLatest([
observeIsPlaybooksEnabled(database),
observeHasRunningPlaybookRunsInTeam(database, teamId),
])),
switchMap(([enabled, hasRunningRuns]) => of(enabled && hasRunningRuns)),
);
return {
lastChannelId,
@ -36,7 +43,7 @@ const enchanced = withObservables([], ({database}: WithDatabaseArgs) => {
scheduledPostCount,
scheduledPostHasError,
scheduledPostsEnabled,
playbooksEnabled,
showPlaybooksButton,
};
});