diff --git a/app/components/navigation_header/header.tsx b/app/components/navigation_header/header.tsx index 60274935b..14acfe5c8 100644 --- a/app/components/navigation_header/header.tsx +++ b/app/components/navigation_header/header.tsx @@ -17,7 +17,7 @@ export type HeaderRightButton = { buttonType?: 'native' | 'opacity' | 'highlight'; color?: string; iconName: string; - count?: number; + count?: number | string; onPress: () => void; rippleRadius?: number; testID?: string; diff --git a/app/database/migration/server/index.ts b/app/database/migration/server/index.ts index 6b8870da2..42ed18da3 100644 --- a/app/database/migration/server/index.ts +++ b/app/database/migration/server/index.ts @@ -25,6 +25,17 @@ const { const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_ATTRIBUTE, PLAYBOOK_RUN_ATTRIBUTE_VALUE} = PLAYBOOK_TABLES; export default schemaMigrations({migrations: [ + { + toVersion: 15, + steps: [ + addColumns({ + table: PLAYBOOK_RUN, + columns: [ + {name: 'type', type: 'string'}, + ], + }), + ], + }, { toVersion: 14, steps: [ diff --git a/app/database/schema/server/index.ts b/app/database/schema/server/index.ts index 1c22cd5e6..48f1ac486 100644 --- a/app/database/schema/server/index.ts +++ b/app/database/schema/server/index.ts @@ -45,7 +45,7 @@ import { } from './table_schemas'; export const serverSchema: AppSchema = appSchema({ - version: 14, + version: 15, tables: [ CategorySchema, CategoryChannelSchema, diff --git a/app/database/schema/server/test.ts b/app/database/schema/server/test.ts index d71a0b6ce..33f0052d8 100644 --- a/app/database/schema/server/test.ts +++ b/app/database/schema/server/test.ts @@ -52,7 +52,7 @@ const {PLAYBOOK_RUN, PLAYBOOK_CHECKLIST, PLAYBOOK_CHECKLIST_ITEM, PLAYBOOK_RUN_A describe('*** Test schema for SERVER database ***', () => { it('=> The SERVER SCHEMA should strictly match', () => { expect(serverSchema).toEqual({ - version: 14, + version: 15, unsafeSql: undefined, tables: { [CATEGORY]: { @@ -463,6 +463,7 @@ describe('*** Test schema for SERVER database ***', () => { items_order: {name: 'items_order', type: 'string'}, previous_reminder: {name: 'previous_reminder', type: 'number', isOptional: true}, update_at: {name: 'update_at', type: 'number'}, + type: {name: 'type', type: 'string'}, }, columnArray: [ {name: 'playbook_id', type: 'string'}, @@ -489,6 +490,7 @@ describe('*** Test schema for SERVER database ***', () => { {name: 'previous_reminder', type: 'number', isOptional: true}, {name: 'items_order', type: 'string'}, {name: 'update_at', type: 'number'}, + {name: 'type', type: 'string'}, ], }, [PLAYBOOK_CHECKLIST]: { diff --git a/app/products/playbooks/actions/local/checklist.test.ts b/app/products/playbooks/actions/local/checklist.test.ts index 8d7317fb1..bfed0634f 100644 --- a/app/products/playbooks/actions/local/checklist.test.ts +++ b/app/products/playbooks/actions/local/checklist.test.ts @@ -2,10 +2,11 @@ // See LICENSE.txt for license information. import DatabaseManager from '@database/manager'; +import {getPlaybookChecklistById} from '@playbooks/database/queries/checklist'; import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item'; import TestHelper from '@test/test_helper'; -import {updateChecklistItem, setChecklistItemCommand, setAssignee, setDueDate} from './checklist'; +import {updateChecklistItem, setChecklistItemCommand, setAssignee, setDueDate, renameChecklist} from './checklist'; import type ServerDataOperator from '@database/operator/server_data_operator'; @@ -264,3 +265,109 @@ describe('setDueDate', () => { expect(updated!.dueDate).toBe(0); }); }); + +describe('renameChecklist', () => { + it('should handle not found database', async () => { + const {error} = await renameChecklist('foo', 'checklistid', 'New Title'); + expect(error).toBeTruthy(); + expect((error as Error).message).toContain('foo database not found'); + }); + + it('should handle checklist not found', async () => { + const {error} = await renameChecklist(serverUrl, 'nonexistent', 'New Title'); + expect(error).toBe('Checklist not found: nonexistent'); + }); + + it('should handle database write errors', async () => { + const runId = 'runid'; + const checklist = { + ...TestHelper.createPlaybookChecklist(runId, 0, 0), + run_id: runId, + order: 0, + }; + await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false}); + + const originalWrite = operator.database.write; + operator.database.write = jest.fn().mockRejectedValue(new Error('Database write failed')); + + const {error} = await renameChecklist(serverUrl, checklist.id, 'New Title'); + expect(error).toBeTruthy(); + + operator.database.write = originalWrite; + }); + + it('should rename checklist successfully', async () => { + const runId = 'runid'; + const checklist = { + ...TestHelper.createPlaybookChecklist(runId, 0, 0), + run_id: runId, + order: 0, + }; + await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false}); + + const newTitle = 'Updated Checklist Title'; + const {data, error} = await renameChecklist(serverUrl, checklist.id, newTitle); + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updated = await getPlaybookChecklistById(operator.database, checklist.id); + expect(updated).toBeDefined(); + expect(updated!.title).toBe(newTitle); + }); + + it('should handle empty title string', async () => { + const runId = 'runid'; + const checklist = { + ...TestHelper.createPlaybookChecklist(runId, 0, 0), + run_id: runId, + order: 0, + }; + await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false}); + + const {data, error} = await renameChecklist(serverUrl, checklist.id, ''); + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updated = await getPlaybookChecklistById(operator.database, checklist.id); + expect(updated).toBeDefined(); + expect(updated!.title).toBe(''); + }); + + it('should handle whitespace-only title', async () => { + const runId = 'runid'; + const checklist = { + ...TestHelper.createPlaybookChecklist(runId, 0, 0), + run_id: runId, + order: 0, + }; + await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false}); + + const whitespaceTitle = ' '; + const {data, error} = await renameChecklist(serverUrl, checklist.id, whitespaceTitle); + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updated = await getPlaybookChecklistById(operator.database, checklist.id); + expect(updated).toBeDefined(); + expect(updated!.title).toBe(whitespaceTitle); + }); + + it('should handle very long titles', async () => { + const runId = 'runid'; + const checklist = { + ...TestHelper.createPlaybookChecklist(runId, 0, 0), + run_id: runId, + order: 0, + }; + await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false}); + + const longTitle = 'A'.repeat(300); // 300 characters + const {data, error} = await renameChecklist(serverUrl, checklist.id, longTitle); + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updated = await getPlaybookChecklistById(operator.database, checklist.id); + expect(updated).toBeDefined(); + expect(updated!.title).toBe(longTitle); + }); +}); diff --git a/app/products/playbooks/actions/local/checklist.ts b/app/products/playbooks/actions/local/checklist.ts index 185df8640..052ab1455 100644 --- a/app/products/playbooks/actions/local/checklist.ts +++ b/app/products/playbooks/actions/local/checklist.ts @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import DatabaseManager from '@database/manager'; +import {getPlaybookChecklistById} from '@playbooks/database/queries/checklist'; import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item'; import {logError} from '@utils/log'; @@ -88,3 +89,24 @@ export async function setDueDate(serverUrl: string, itemId: string, date?: numbe return {error}; } } + +export async function renameChecklist(serverUrl: string, checklistId: string, title: string) { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const checklist = await getPlaybookChecklistById(database, checklistId); + if (!checklist) { + return {error: `Checklist not found: ${checklistId}`}; + } + + await database.write(async () => { + checklist.update((c) => { + c.title = title; + }); + }); + + return {data: true}; + } catch (error) { + logError('failed to rename checklist', error); + return {error}; + } +} diff --git a/app/products/playbooks/actions/local/run.test.ts b/app/products/playbooks/actions/local/run.test.ts index 5647c2737..cf44289d5 100644 --- a/app/products/playbooks/actions/local/run.test.ts +++ b/app/products/playbooks/actions/local/run.test.ts @@ -5,7 +5,7 @@ import DatabaseManager from '@database/manager'; import {PLAYBOOK_TABLES} from '@playbooks/constants/database'; import TestHelper from '@test/test_helper'; -import {handlePlaybookRuns, setOwner} from './run'; +import {handlePlaybookRuns, setOwner, renamePlaybookRun} from './run'; import type {Database} from '@nozbe/watermelondb'; import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run'; @@ -80,7 +80,7 @@ describe('setOwner', () => { const {data, error} = await setOwner(serverUrl, nonExistentRunId, newOwnerId); - expect(error).toBe('Run not found'); + expect(error).toBe('Checklist not found'); expect(data).toBeUndefined(); }); @@ -153,3 +153,100 @@ describe('setOwner', () => { expect(updatedRun.ownerUserId).toBe(emptyOwnerId); }); }); + +describe('renamePlaybookRun', () => { + let database: Database; + beforeEach(() => { + database = DatabaseManager.getServerDatabaseAndOperator(serverUrl).database; + }); + + it('should handle not found database', async () => { + const {error} = await renamePlaybookRun('foo', 'runid', 'New Name'); + expect(error).toBeTruthy(); + expect((error as Error).message).toContain('foo database not found'); + }); + + it('should handle playbook run not found', async () => { + const {error} = await renamePlaybookRun(serverUrl, 'nonexistent', 'New Name'); + expect(error).toBe('Playbook run not found: nonexistent'); + }); + + it('should handle database write errors', async () => { + const runs = TestHelper.createPlaybookRuns(1, 0, 0); + await handlePlaybookRuns(serverUrl, runs, false, false); + + const playbookRunId = runs[0].id; + + const originalWrite = database.write; + database.write = jest.fn().mockRejectedValue(new Error('Database write failed')); + + const {error} = await renamePlaybookRun(serverUrl, playbookRunId, 'New Name'); + expect(error).toBeTruthy(); + expect((error as Error).message).toBe('Database write failed'); + + database.write = originalWrite; + }); + + it('should rename playbook run successfully', async () => { + const runs = TestHelper.createPlaybookRuns(1, 0, 0); + await handlePlaybookRuns(serverUrl, runs, false, false); + + const playbookRunId = runs[0].id; + const newName = 'Updated Run Name'; + + const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, newName); + + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updatedRun = await database.get(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId); + expect(updatedRun.name).toBe(newName); + }); + + it('should handle empty name string', async () => { + const runs = TestHelper.createPlaybookRuns(1, 0, 0); + await handlePlaybookRuns(serverUrl, runs, false, false); + + const playbookRunId = runs[0].id; + + const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, ''); + + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updatedRun = await database.get(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId); + expect(updatedRun.name).toBe(''); + }); + + it('should handle whitespace-only name', async () => { + const runs = TestHelper.createPlaybookRuns(1, 0, 0); + await handlePlaybookRuns(serverUrl, runs, false, false); + + const playbookRunId = runs[0].id; + const whitespaceName = ' '; + + const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, whitespaceName); + + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updatedRun = await database.get(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId); + expect(updatedRun.name).toBe(whitespaceName); + }); + + it('should handle very long names', async () => { + const runs = TestHelper.createPlaybookRuns(1, 0, 0); + await handlePlaybookRuns(serverUrl, runs, false, false); + + const playbookRunId = runs[0].id; + const longName = 'A'.repeat(300); // 300 characters + + const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, longName); + + expect(error).toBeUndefined(); + expect(data).toBe(true); + + const updatedRun = await database.get(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId); + expect(updatedRun.name).toBe(longName); + }); +}); diff --git a/app/products/playbooks/actions/local/run.ts b/app/products/playbooks/actions/local/run.ts index 2b4b172df..c0ca803cd 100644 --- a/app/products/playbooks/actions/local/run.ts +++ b/app/products/playbooks/actions/local/run.ts @@ -15,7 +15,7 @@ export async function handlePlaybookRuns(serverUrl: string, runs: PlaybookRun[], }); return {data}; } catch (error) { - logError('failed to handle playbook runs', error); + logError('failed to handle playbook checklist', error); return {error}; } } @@ -25,7 +25,7 @@ export async function setOwner(serverUrl: string, playbookRunId: string, ownerId const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const run = await getPlaybookRunById(database, playbookRunId); if (!run) { - return {error: 'Run not found'}; + return {error: 'Checklist not found'}; } await database.write(async () => { @@ -40,3 +40,24 @@ export async function setOwner(serverUrl: string, playbookRunId: string, ownerId return {error}; } } + +export async function renamePlaybookRun(serverUrl: string, playbookRunId: string, name: string) { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const run = await getPlaybookRunById(database, playbookRunId); + if (!run) { + return {error: `Playbook run not found: ${playbookRunId}`}; + } + + await database.write(async () => { + run.update((r) => { + r.name = name; + }); + }); + + return {data: true}; + } catch (error) { + logError('failed to rename playbook run', error); + return {error}; + } +} diff --git a/app/products/playbooks/actions/remote/checklist.test.ts b/app/products/playbooks/actions/remote/checklist.test.ts index 2c0fad3b8..df83305ef 100644 --- a/app/products/playbooks/actions/remote/checklist.test.ts +++ b/app/products/playbooks/actions/remote/checklist.test.ts @@ -9,6 +9,7 @@ import { updateChecklistItem as localUpdateChecklistItem, setAssignee as localSetAssignee, setDueDate as localSetDueDate, + renameChecklist as localRenameChecklist, } from '@playbooks/actions/local/checklist'; import { @@ -19,6 +20,8 @@ import { setChecklistItemCommand, setAssignee, setDueDate, + renameChecklist, + addChecklistItem, } from './checklist'; const serverUrl = 'baseHandler.test.com'; @@ -37,6 +40,8 @@ const mockClient = { setAssignee: jest.fn(), setChecklistItemCommand: jest.fn(), setDueDate: jest.fn(), + renameChecklist: jest.fn(), + addChecklistItem: jest.fn(), }; jest.mock('@playbooks/actions/local/checklist'); @@ -322,4 +327,104 @@ describe('checklist', () => { expect(localSetDueDate).toHaveBeenCalledWith(serverUrl, itemId, 0); }); }); + + describe('renameChecklist', () => { + const checklistId = 'checklist-id-1'; + const newTitle = 'New Checklist Title'; + + it('should handle client error', async () => { + jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc); + + const result = await renameChecklist(serverUrl, playbookRunId, checklistNumber, checklistId, newTitle); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(localRenameChecklist).not.toHaveBeenCalled(); + }); + + it('should handle API exception', async () => { + mockClient.renameChecklist.mockImplementationOnce(throwFunc); + + const result = await renameChecklist(serverUrl, playbookRunId, checklistNumber, checklistId, newTitle); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(mockClient.renameChecklist).toHaveBeenCalledWith(playbookRunId, checklistNumber, newTitle); + expect(localRenameChecklist).not.toHaveBeenCalled(); + }); + + it('should handle local DB update failure', async () => { + mockClient.renameChecklist.mockResolvedValueOnce({}); + (localRenameChecklist as jest.Mock).mockRejectedValueOnce(new Error('DB error')); + + const result = await renameChecklist(serverUrl, playbookRunId, checklistNumber, checklistId, newTitle); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(mockClient.renameChecklist).toHaveBeenCalledWith(playbookRunId, checklistNumber, newTitle); + expect(localRenameChecklist).toHaveBeenCalledWith(serverUrl, checklistId, newTitle); + }); + + it('should rename checklist successfully', async () => { + mockClient.renameChecklist.mockResolvedValueOnce({}); + + const result = await renameChecklist(serverUrl, playbookRunId, checklistNumber, checklistId, newTitle); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.data).toBe(true); + expect(mockClient.renameChecklist).toHaveBeenCalledWith(playbookRunId, checklistNumber, newTitle); + expect(localRenameChecklist).toHaveBeenCalledWith(serverUrl, checklistId, newTitle); + }); + + it('should rename checklist with empty title', async () => { + mockClient.renameChecklist.mockResolvedValueOnce({}); + const emptyTitle = ''; + + const result = await renameChecklist(serverUrl, playbookRunId, checklistNumber, checklistId, emptyTitle); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.data).toBe(true); + expect(mockClient.renameChecklist).toHaveBeenCalledWith(playbookRunId, checklistNumber, emptyTitle); + expect(localRenameChecklist).toHaveBeenCalledWith(serverUrl, checklistId, emptyTitle); + }); + }); + + describe('addChecklistItem', () => { + const title = 'New Item Title'; + + it('should handle client error', async () => { + jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc); + + const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + }); + + it('should handle API exception', async () => { + mockClient.addChecklistItem.mockImplementationOnce(throwFunc); + + const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, title); + }); + + it('should add checklist item successfully', async () => { + mockClient.addChecklistItem.mockResolvedValueOnce({}); + + const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.data).toBe(true); + expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, title); + }); + + it('should add checklist item with empty title', async () => { + mockClient.addChecklistItem.mockResolvedValueOnce({}); + const emptyTitle = ''; + + const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, emptyTitle); + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.data).toBe(true); + expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, emptyTitle); + }); + }); }); diff --git a/app/products/playbooks/actions/remote/checklist.ts b/app/products/playbooks/actions/remote/checklist.ts index 8cce9664f..272ea4a23 100644 --- a/app/products/playbooks/actions/remote/checklist.ts +++ b/app/products/playbooks/actions/remote/checklist.ts @@ -9,6 +9,7 @@ import { updateChecklistItem as localUpdateChecklistItem, setAssignee as localSetAssignee, setDueDate as localSetDueDate, + renameChecklist as localRenameChecklist, } from '@playbooks/actions/local/checklist'; import {getFullErrorMessage} from '@utils/errors'; import {logDebug} from '@utils/log'; @@ -156,3 +157,43 @@ export const setDueDate = async ( return {error}; } }; + +export const renameChecklist = async ( + serverUrl: string, + playbookRunId: string, + checklistNumber: number, + checklistId: string, + newTitle: string, +) => { + try { + const client = NetworkManager.getClient(serverUrl); + + // Patch the playbook run with updated checklists + await client.renameChecklist(playbookRunId, checklistNumber, newTitle); + + // Update local database + await localRenameChecklist(serverUrl, checklistId, newTitle); + return {data: true}; + } catch (error) { + logDebug('error on renameChecklist', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; + +export const addChecklistItem = async ( + serverUrl: string, + playbookRunId: string, + checklistNumber: number, + title: string, +) => { + try { + const client = NetworkManager.getClient(serverUrl); + await client.addChecklistItem(playbookRunId, checklistNumber, title); + return {data: true}; + } catch (error) { + logDebug('error on addChecklistItem', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; diff --git a/app/products/playbooks/actions/remote/runs.test.ts b/app/products/playbooks/actions/remote/runs.test.ts index bb0d7446a..44b18cad7 100644 --- a/app/products/playbooks/actions/remote/runs.test.ts +++ b/app/products/playbooks/actions/remote/runs.test.ts @@ -7,12 +7,12 @@ import {PER_PAGE_DEFAULT} from '@client/rest/constants'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {updateLastPlaybookRunsFetchAt} from '@playbooks/actions/local/channel'; -import {handlePlaybookRuns, setOwner as localSetOwner} from '@playbooks/actions/local/run'; +import {handlePlaybookRuns, setOwner as localSetOwner, renamePlaybookRun as localRenamePlaybookRun} from '@playbooks/actions/local/run'; import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run'; import EphemeralStore from '@store/ephemeral_store'; import TestHelper from '@test/test_helper'; -import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, fetchPlaybookRunsPageForParticipant, setOwner, finishRun, createPlaybookRun, fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from './runs'; +import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, fetchPlaybookRunsPageForParticipant, setOwner, finishRun, renamePlaybookRun, createPlaybookRun, fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from './runs'; const serverUrl = 'baseHandler.test.com'; const channelId = 'channel-id-1'; @@ -28,6 +28,7 @@ const mockClient = { finishRun: jest.fn(), createPlaybookRun: jest.fn(), postStatusUpdate: jest.fn(), + patchPlaybookRun: jest.fn(), }; jest.mock('@playbooks/database/queries/run'); @@ -530,7 +531,6 @@ describe('createPlaybookRun', () => { jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc); const result = await createPlaybookRun(serverUrl, playbookId, ownerUserId, teamId, name, description); - expect(result).toBeDefined(); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); @@ -717,5 +717,53 @@ describe('postStatusUpdate', () => { const result = await postStatusUpdate(serverUrl, playbookRunID, payload, ids); expect(result.error).toBeDefined(); + }); }); + +describe('renamePlaybookRun', () => { + const playbookRunId = 'playbook-run-id-1'; + const newName = 'New Run Name'; + + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(localRenamePlaybookRun).mockResolvedValue({data: true}); + }); + + it('should handle client error', async () => { + jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc); + + const result = await renamePlaybookRun(serverUrl, playbookRunId, newName); + + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(localRenamePlaybookRun).not.toHaveBeenCalled(); + }); + + it('should handle API exception', async () => { + const clientError = new Error('Client error'); + mockClient.patchPlaybookRun.mockRejectedValueOnce(clientError); + + const result = await renamePlaybookRun(serverUrl, playbookRunId, newName); + expect(result).toBeDefined(); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName}); + expect(localRenamePlaybookRun).not.toHaveBeenCalled(); + }); + + it('should rename playbook run successfully', async () => { + mockClient.patchPlaybookRun.mockResolvedValueOnce(undefined); + + const result = await renamePlaybookRun(serverUrl, playbookRunId, newName); + + expect(result).toBeDefined(); + expect(result.error).toBeUndefined(); + expect(result.data).toBe(true); + expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName}); + expect(localRenamePlaybookRun).toHaveBeenCalledWith(serverUrl, playbookRunId, newName); + + }); +}); + diff --git a/app/products/playbooks/actions/remote/runs.ts b/app/products/playbooks/actions/remote/runs.ts index 891c65cd6..c7634adae 100644 --- a/app/products/playbooks/actions/remote/runs.ts +++ b/app/products/playbooks/actions/remote/runs.ts @@ -6,7 +6,7 @@ import {PER_PAGE_DEFAULT} from '@client/rest/constants'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {updateLastPlaybookRunsFetchAt} from '@playbooks/actions/local/channel'; -import {handlePlaybookRuns, setOwner as localSetOwner} from '@playbooks/actions/local/run'; +import {handlePlaybookRuns, setOwner as localSetOwner, renamePlaybookRun as localRenamePlaybookRun} from '@playbooks/actions/local/run'; import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run'; import {getMaxRunUpdateAt} from '@playbooks/utils/run'; import EphemeralStore from '@store/ephemeral_store'; @@ -136,6 +136,20 @@ export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId } }; +export const renamePlaybookRun = async (serverUrl: string, playbookRunId: string, newName: string) => { + try { + const client = NetworkManager.getClient(serverUrl); + await client.patchPlaybookRun(playbookRunId, {name: newName}); + + await localRenamePlaybookRun(serverUrl, playbookRunId, newName); + return {data: true}; + } catch (error) { + logDebug('error on renamePlaybookRun', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +}; + export const createPlaybookRun = async ( serverUrl: string, playbook_id: string, diff --git a/app/products/playbooks/client/rest.test.ts b/app/products/playbooks/client/rest.test.ts index 9e9f33d99..6c0a87a02 100644 --- a/app/products/playbooks/client/rest.test.ts +++ b/app/products/playbooks/client/rest.test.ts @@ -547,7 +547,6 @@ describe('postStatusUpdate', () => { jest.mocked(client.doFetch).mockResolvedValue(undefined); await client.postStatusUpdate(playbookRunID, payload, ids); - expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); }); @@ -965,3 +964,163 @@ describe('createPlaybookRun', () => { ).rejects.toThrow('Network error'); }); }); +describe('patchPlaybookRun', () => { + test('should patch with multiple field updates', async () => { + const playbookRunId = 'run123'; + const updates = { + name: 'Updated Name', + owner_user_id: 'user456', + description: 'Updated description', + }; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}`; + const expectedOptions = {method: 'patch', body: updates}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.patchPlaybookRun(playbookRunId, updates); + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + test('should patch with single field update', async () => { + const playbookRunId = 'run123'; + const updates = {name: 'New Name'}; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}`; + const expectedOptions = {method: 'patch', body: updates}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.patchPlaybookRun(playbookRunId, updates); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + test('should handle network errors', async () => { + const playbookRunId = 'run123'; + const updates = {name: 'New Name'}; + + jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error')); + + await expect(client.patchPlaybookRun(playbookRunId, updates)).rejects.toThrow('Network error'); + }); + + test('should handle empty updates object', async () => { + const playbookRunId = 'run123'; + const updates = {}; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}`; + const expectedOptions = {method: 'patch', body: updates}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.patchPlaybookRun(playbookRunId, updates); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); +}); + +describe('renameChecklist', () => { + test('should rename successfully', async () => { + const playbookRunId = 'run123'; + const checklistNumber = 1; + const newName = 'Updated Checklist Name'; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNumber}/rename`; + const expectedOptions = {method: 'put', body: {title: newName}}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.renameChecklist(playbookRunId, checklistNumber, newName); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + test('should rename with zero index', async () => { + const playbookRunId = 'run123'; + const checklistNumber = 0; + const newName = 'First Checklist'; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNumber}/rename`; + const expectedOptions = {method: 'put', body: {title: newName}}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.renameChecklist(playbookRunId, checklistNumber, newName); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + test('should handle network errors', async () => { + const playbookRunId = 'run123'; + const checklistNumber = 1; + const newName = 'Updated Name'; + + jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error')); + + await expect(client.renameChecklist(playbookRunId, checklistNumber, newName)).rejects.toThrow('Network error'); + }); + + test('should handle empty name', async () => { + const playbookRunId = 'run123'; + const checklistNumber = 1; + const newName = ''; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNumber}/rename`; + const expectedOptions = {method: 'put', body: {title: newName}}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.renameChecklist(playbookRunId, checklistNumber, newName); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); +}); + +describe('addChecklistItem', () => { + test('should add item successfully', async () => { + const playbookRunId = 'run123'; + const checklistNum = 1; + const title = 'New Item'; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`; + const expectedOptions = {method: 'post', body: {title}}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.addChecklistItem(playbookRunId, checklistNum, title); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + test('should add item with empty title', async () => { + const playbookRunId = 'run123'; + const checklistNum = 1; + const title = ''; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`; + const expectedOptions = {method: 'post', body: {title}}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.addChecklistItem(playbookRunId, checklistNum, title); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); + + test('should handle network errors', async () => { + const playbookRunId = 'run123'; + const checklistNum = 1; + const title = 'New Item'; + + jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error')); + + await expect(client.addChecklistItem(playbookRunId, checklistNum, title)).rejects.toThrow('Network error'); + }); + + test('should handle invalid checklist number', async () => { + const playbookRunId = 'run123'; + const checklistNum = -1; + const title = 'New Item'; + const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`; + const expectedOptions = {method: 'post', body: {title}}; + + jest.mocked(client.doFetch).mockResolvedValue(undefined); + + await client.addChecklistItem(playbookRunId, checklistNum, title); + + expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions); + }); +}); diff --git a/app/products/playbooks/client/rest.ts b/app/products/playbooks/client/rest.ts index 384b89f09..37dba785c 100644 --- a/app/products/playbooks/client/rest.ts +++ b/app/products/playbooks/client/rest.ts @@ -14,6 +14,7 @@ export interface ClientPlaybooksMix { fetchPlaybookRuns: (params: FetchPlaybookRunsParams, groupLabel?: RequestGroupLabel) => Promise; fetchPlaybookRun: (id: string, groupLabel?: RequestGroupLabel) => Promise; fetchPlaybookRunMetadata: (id: string) => Promise; + patchPlaybookRun: (playbookRunId: string, updates: Partial) => Promise; setOwner: (playbookRunId: string, ownerId: string) => Promise; // Run Management @@ -27,8 +28,9 @@ export interface ClientPlaybooksMix { restoreChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise; setAssignee: (playbookRunId: string, checklistNum: number, itemNum: number, assigneeId?: string) => Promise; setDueDate: (playbookRunId: string, checklistNum: number, itemNum: number, date?: number) => Promise; + addChecklistItem: (playbookRunId: string, checklistNum: number, title: string) => Promise; - // skipChecklist: (playbookRunID: string, checklistNum: number) => Promise; + renameChecklist: (playbookRunId: string, checklistNumber: number, newName: string) => Promise; // Slash Commands runChecklistItemSlashCommand: (playbookRunId: string, checklistNumber: number, itemNumber: number) => Promise<{trigger_id: string}>; @@ -85,6 +87,20 @@ const ClientPlaybooks = >(superclass: TBas ); }; + patchPlaybookRun = async (playbookRunId: string, updates: Partial) => { + await this.doFetch( + `${this.getPlaybookRunRoute(playbookRunId)}`, + {method: 'patch', body: updates}, + ); + }; + + renameChecklist = async (playbookRunId: string, checklistNumber: number, newName: string) => { + await this.doFetch( + `${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNumber}/rename`, + {method: 'put', body: {title: newName}}, + ); + }; + setOwner = async (playbookRunId: string, ownerId: string) => { const data = await this.doFetch( `${this.getPlaybookRunRoute(playbookRunId)}/owner`, @@ -188,6 +204,13 @@ const ClientPlaybooks = >(superclass: TBas ); }; + addChecklistItem = async (playbookRunId: string, checklistNum: number, title: string) => { + await this.doFetch( + `${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNum}/add`, + {method: 'post', body: {title}}, + ); + }; + // Slash Commands runChecklistItemSlashCommand = async (playbookRunId: string, checklistNumber: number, itemNumber: number) => { const data = await this.doFetch( diff --git a/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.test.tsx b/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.test.tsx index 13b154334..3d083d673 100644 --- a/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.test.tsx +++ b/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.test.tsx @@ -36,7 +36,7 @@ describe('PlaybookRunsOption', () => { const optionItem = getByTestId('option-item'); expect(optionItem).toBeTruthy(); - expect(optionItem.props.label).toBe('Playbook runs'); + expect(optionItem.props.label).toBe('Playbook checklists'); expect(optionItem.props.info).toBe('3'); expect(optionItem.props.icon).toBe('product-playbooks'); expect(optionItem.props.type).toBe('arrow'); @@ -49,7 +49,7 @@ describe('PlaybookRunsOption', () => { const slideUpPanelItem = getByTestId('slide-up-panel-item'); expect(slideUpPanelItem).toBeTruthy(); - expect(slideUpPanelItem.props.text).toBe('Playbook runs'); + expect(slideUpPanelItem.props.text).toBe('Playbook checklists'); expect(slideUpPanelItem.props.leftIcon).toBe('product-playbooks'); expect(slideUpPanelItem.props.rightIcon).toBe('chevron-right'); expect(slideUpPanelItem.props.onPress).toBeDefined(); diff --git a/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.tsx b/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.tsx index 8cad2f922..5d8d15b3b 100644 --- a/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.tsx +++ b/app/products/playbooks/components/channel_actions/playbook_runs_option/playbook_runs_option.tsx @@ -20,7 +20,7 @@ type Props = { const messages = defineMessages({ playbookRuns: { id: 'playbooks.playbooks_runs.title', - defaultMessage: 'Playbook runs', + defaultMessage: 'Playbook checklists', }, }); diff --git a/app/products/playbooks/components/playbooks_button/playbooks_button.test.tsx b/app/products/playbooks/components/playbooks_button/playbooks_button.test.tsx index a90155a43..1370b99af 100644 --- a/app/products/playbooks/components/playbooks_button/playbooks_button.test.tsx +++ b/app/products/playbooks/components/playbooks_button/playbooks_button.test.tsx @@ -25,7 +25,7 @@ describe('PlaybooksButton', () => { const icon = getByTestId('channel_list.playbooks.button-icon'); expect(icon).toHaveProp('name', 'product-playbooks'); - expect(getByText('Playbook runs')).toBeTruthy(); + expect(getByText('Playbook checklists')).toBeTruthy(); }); it('calls goToParticipantPlaybooks when pressed', () => { diff --git a/app/products/playbooks/components/playbooks_button/playbooks_button.tsx b/app/products/playbooks/components/playbooks_button/playbooks_button.tsx index 50647828d..d0617432c 100644 --- a/app/products/playbooks/components/playbooks_button/playbooks_button.tsx +++ b/app/products/playbooks/components/playbooks_button/playbooks_button.tsx @@ -75,7 +75,7 @@ const PlaybooksButton = () => { /> diff --git a/app/products/playbooks/components/run_list/empty_state.test.tsx b/app/products/playbooks/components/run_list/empty_state.test.tsx index c709165ac..89c8c0652 100644 --- a/app/products/playbooks/components/run_list/empty_state.test.tsx +++ b/app/products/playbooks/components/run_list/empty_state.test.tsx @@ -12,8 +12,8 @@ describe('EmptyState', () => { ); // Verify correct title and description are shown - expect(getByText('No in progress runs')).toBeTruthy(); - expect(getByText('When a run starts in this channel, you’ll see it here.')).toBeTruthy(); + expect(getByText('Nothing in progress')).toBeTruthy(); + expect(getByText('When a checklist starts in this channel, you’ll see it here.')).toBeTruthy(); }); it('renders finished empty state correctly', () => { @@ -22,7 +22,7 @@ describe('EmptyState', () => { ); // Verify correct title and description are shown - expect(getByText('No finished runs')).toBeTruthy(); - expect(getByText('When a run in this channel finishes, you’ll see it here.')).toBeTruthy(); + expect(getByText('Nothing finished')).toBeTruthy(); + expect(getByText('When a checklist in this channel finishes, you’ll see it here.')).toBeTruthy(); }); }); diff --git a/app/products/playbooks/components/run_list/empty_state.tsx b/app/products/playbooks/components/run_list/empty_state.tsx index ed29f082e..c9d7e5f22 100644 --- a/app/products/playbooks/components/run_list/empty_state.tsx +++ b/app/products/playbooks/components/run_list/empty_state.tsx @@ -40,19 +40,19 @@ const getStylesFromTheme = makeStyleSheetFromTheme((theme: Theme) => { const messages = defineMessages({ inProgressTitle: { id: 'playbooks.runs.in_progress.title', - defaultMessage: 'No in progress runs', + defaultMessage: 'Nothing in progress', }, finishedTitle: { id: 'playbooks.runs.finished.title', - defaultMessage: 'No finished runs', + defaultMessage: 'Nothing finished', }, inProgressDescription: { id: 'playbooks.runs.in_progress.description', - defaultMessage: 'When a run starts in this channel, you’ll see it here.', + defaultMessage: 'When a checklist starts in this channel, you’ll see it here.', }, finishedDescription: { id: 'playbooks.runs.finished.description', - defaultMessage: 'When a run in this channel finishes, you’ll see it here.', + defaultMessage: 'When a checklist in this channel finishes, you’ll see it here.', }, }); diff --git a/app/products/playbooks/components/run_list/playbook_card/playbook_card.test.tsx b/app/products/playbooks/components/run_list/playbook_card/playbook_card.test.tsx index 63bf05dbe..ee8a054f8 100644 --- a/app/products/playbooks/components/run_list/playbook_card/playbook_card.test.tsx +++ b/app/products/playbooks/components/run_list/playbook_card/playbook_card.test.tsx @@ -67,7 +67,7 @@ describe('PlaybookCard', () => { expect(userAvatarsStack.props.users).toEqual(props.participants); expect(userAvatarsStack.props.channelId).toBe((props.run as PlaybookRunModel).channelId); expect(userAvatarsStack.props.location).toBe(props.location); - expect(userAvatarsStack.props.bottomSheetTitle.defaultMessage).toBe('Run Participants'); + expect(userAvatarsStack.props.bottomSheetTitle.defaultMessage).toBe('Participants'); const progressBar = getByTestId('progress-bar'); expect(progressBar.props.progress).toBe(50); @@ -93,7 +93,7 @@ describe('PlaybookCard', () => { expect(userAvatarsStack.props.users).toEqual(props.participants); expect(userAvatarsStack.props.channelId).toBe(props.run.channel_id); expect(userAvatarsStack.props.location).toBe(props.location); - expect(userAvatarsStack.props.bottomSheetTitle.defaultMessage).toBe('Run Participants'); + expect(userAvatarsStack.props.bottomSheetTitle.defaultMessage).toBe('Participants'); const progressBar = getByTestId('progress-bar'); expect(progressBar.props.progress).toBe(50); diff --git a/app/products/playbooks/components/run_list/playbook_card/playbook_card.tsx b/app/products/playbooks/components/run_list/playbook_card/playbook_card.tsx index 13ed96020..c998b0844 100644 --- a/app/products/playbooks/components/run_list/playbook_card/playbook_card.tsx +++ b/app/products/playbooks/components/run_list/playbook_card/playbook_card.tsx @@ -78,7 +78,7 @@ type Props = { owner?: UserModel; }; -const bottomSheetTitleMessage = defineMessage({id: 'playbook.participants', defaultMessage: 'Run Participants'}); +const bottomSheetTitleMessage = defineMessage({id: 'playbook.participants', defaultMessage: 'Participants'}); const PlaybookCard = ({ run, diff --git a/app/products/playbooks/components/run_list/run_list.test.tsx b/app/products/playbooks/components/run_list/run_list.test.tsx index 4db5c5403..3e59c5c87 100644 --- a/app/products/playbooks/components/run_list/run_list.test.tsx +++ b/app/products/playbooks/components/run_list/run_list.test.tsx @@ -226,7 +226,7 @@ describe('RunList', () => { props.inProgressRuns = [inProgressRun]; const {getByText} = renderWithIntlAndTheme(); - expect(getByText('Start a new run')).toBeTruthy(); + expect(getByText('New')).toBeTruthy(); }); it('calls goToSelectPlaybook when Start a new run button is pressed', () => { @@ -235,7 +235,7 @@ describe('RunList', () => { const {getByText} = renderWithIntlAndTheme(); act(() => { - fireEvent.press(getByText('Start a new run')); + fireEvent.press(getByText('New')); }); expect(goToSelectPlaybook).toHaveBeenCalledTimes(1); @@ -348,7 +348,7 @@ describe('RunList', () => { const {getByText} = renderWithIntlAndTheme(); act(() => { - fireEvent.press(getByText('Start a new run')); + fireEvent.press(getByText('New')); }); expect(goToSelectPlaybook).toHaveBeenCalledTimes(1); diff --git a/app/products/playbooks/components/status_update_post/participants/participants.test.tsx b/app/products/playbooks/components/status_update_post/participants/participants.test.tsx index 3d7a519d6..6331f955c 100644 --- a/app/products/playbooks/components/status_update_post/participants/participants.test.tsx +++ b/app/products/playbooks/components/status_update_post/participants/participants.test.tsx @@ -108,7 +108,7 @@ describe('Participants', () => { renderContent: expect.any(Function), initialSnapIndex: 1, snapPoints: expect.any(Array), - title: 'Run Participants', + title: 'Participants', theme: expect.any(Object), })); }); @@ -146,7 +146,7 @@ describe('Participants', () => { const args = jest.mocked(bottomSheet).mock.calls[0][0]; const Content = args.renderContent; const {getByText} = renderWithEverything(, {database, serverUrl}); - expect(getByText('Run Participants')).toBeTruthy(); + expect(getByText('Participants')).toBeTruthy(); }); }); @@ -163,7 +163,7 @@ describe('Participants', () => { const args = jest.mocked(bottomSheet).mock.calls[0][0]; const Content = args.renderContent; const {queryByText} = renderWithEverything(, {database, serverUrl}); - expect(queryByText('Run Participants')).toBeNull(); + expect(queryByText('Participants')).toBeNull(); }); }); diff --git a/app/products/playbooks/constants/playbook_run.ts b/app/products/playbooks/constants/playbook_run.ts new file mode 100644 index 000000000..285c4544a --- /dev/null +++ b/app/products/playbooks/constants/playbook_run.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export const PLAYBOOK_RUN_TYPES = { + PlaybookType: 'playbook' as const, + ChannelChecklistType: 'channelChecklist' as const, +} as const; + +export type PlaybookRunType = typeof PLAYBOOK_RUN_TYPES[keyof typeof PLAYBOOK_RUN_TYPES]; diff --git a/app/products/playbooks/constants/screens.ts b/app/products/playbooks/constants/screens.ts index 43365b4d9..7e69705cf 100644 --- a/app/products/playbooks/constants/screens.ts +++ b/app/products/playbooks/constants/screens.ts @@ -6,6 +6,9 @@ export const PARTICIPANT_PLAYBOOKS = 'ParticipantPlaybooks'; export const PLAYBOOK_RUN = 'PlaybookRun'; export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand'; export const PLAYBOOK_POST_UPDATE = 'PlaybookPostUpdate'; +export const PLAYBOOK_RENAME_CHECKLIST = 'PlaybookRenameChecklist'; +export const PLAYBOOK_ADD_CHECKLIST_ITEM = 'PlaybookAddChecklistItem'; +export const PLAYBOOK_RENAME_RUN = 'PlaybookRenameRun'; export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser'; export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate'; export const PLAYBOOKS_SELECT_PLAYBOOK = 'PlaybooksSelectPlaybook'; @@ -17,6 +20,9 @@ export default { PLAYBOOK_RUN, PLAYBOOK_EDIT_COMMAND, PLAYBOOK_POST_UPDATE, + PLAYBOOK_RENAME_CHECKLIST, + PLAYBOOK_ADD_CHECKLIST_ITEM, + PLAYBOOK_RENAME_RUN, PLAYBOOK_SELECT_USER, PLAYBOOKS_SELECT_DATE, PLAYBOOKS_SELECT_PLAYBOOK, diff --git a/app/products/playbooks/database/models/playbook_run.ts b/app/products/playbooks/database/models/playbook_run.ts index f71d3e7a2..4c85855d9 100644 --- a/app/products/playbooks/database/models/playbook_run.ts +++ b/app/products/playbooks/database/models/playbook_run.ts @@ -10,6 +10,7 @@ import {safeParseJSONStringArray} from '@utils/helpers'; import type {Query, Relation} from '@nozbe/watermelondb'; import type PlaybookRunModelInterface from '@playbooks//types/database/models/playbook_run'; +import type {PlaybookRunType} from '@playbooks/constants/playbook_run'; import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist'; import type {SyncStatus} from '@typings/database/database'; import type ChannelModel from '@typings/database/models/servers/channel'; @@ -51,9 +52,12 @@ export default class PlaybookRunModel extends Model implements PlaybookRunModelI [PLAYBOOK_CHECKLIST]: {type: 'has_many', foreignKey: 'run_id'}, }; - /** playbook_id : The id of the playbook this run belongs to */ + /** playbook_id : The id of the playbook this run belongs to (empty string if it is a channel checklist) */ @field('playbook_id') playbookId!: string; + /** type : The type of run ('playbook' or 'channelChecklist') */ + @field('type') type!: PlaybookRunType; + /** name : Name of the playbook run */ @field('name') name!: string; diff --git a/app/products/playbooks/database/models/playbook_run_attribute_value.test.ts b/app/products/playbooks/database/models/playbook_run_attribute_value.test.ts index a2912da7f..0009fa79e 100644 --- a/app/products/playbooks/database/models/playbook_run_attribute_value.test.ts +++ b/app/products/playbooks/database/models/playbook_run_attribute_value.test.ts @@ -17,7 +17,8 @@ const SERVER_URL = `playbookRunAttributeValueModel.test.${Date.now()}.com`; const applyRunData = (run: PlaybookRunModel, mockData: PlaybookRun) => { run._raw.id = mockData.id; - run.playbookId = mockData.playbook_id; + run.playbookId = mockData.playbook_id ?? ''; + run.type = mockData.type ?? 'playbook'; run.postId = mockData.post_id ?? null; run.ownerUserId = mockData.owner_user_id; run.teamId = mockData.team_id; diff --git a/app/products/playbooks/database/operators/transformers/index.ts b/app/products/playbooks/database/operators/transformers/index.ts index 81f3a043c..f96bec618 100644 --- a/app/products/playbooks/database/operators/transformers/index.ts +++ b/app/products/playbooks/database/operators/transformers/index.ts @@ -4,6 +4,7 @@ import {OperationType} from '@constants/database'; import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index'; import {PLAYBOOK_TABLES} from '@playbooks/constants/database'; +import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run'; import type PlaybookChecklistModel from '@playbooks/types/database/models/playbook_checklist'; import type PlaybookChecklistItemModel from '@playbooks/types/database/models/playbook_checklist_item'; @@ -33,6 +34,7 @@ export const transformPlaybookRunRecord = ({action, database, value}: Transforme const fieldsMapper = (run: PlaybookRunModel) => { run._raw.id = isCreateAction ? (raw?.id ?? run.id) : run.id; run.playbookId = raw.playbook_id ?? record?.playbookId ?? ''; + run.type = raw.type ?? record?.type ?? (run.playbookId ? PLAYBOOK_RUN_TYPES.PlaybookType : PLAYBOOK_RUN_TYPES.ChannelChecklistType); run.postId = raw.post_id ?? record?.postId ?? null; run.ownerUserId = raw.owner_user_id ?? record?.ownerUserId ?? ''; run.teamId = raw.team_id ?? record?.teamId ?? ''; diff --git a/app/products/playbooks/database/schema/playbook_run.ts b/app/products/playbooks/database/schema/playbook_run.ts index 76e68a1ac..c3ad62437 100644 --- a/app/products/playbooks/database/schema/playbook_run.ts +++ b/app/products/playbooks/database/schema/playbook_run.ts @@ -34,5 +34,6 @@ export default tableSchema({ {name: 'previous_reminder', type: 'number', isOptional: true}, {name: 'items_order', type: 'string'}, // JSON string {name: 'update_at', type: 'number'}, + {name: 'type', type: 'string'}, ], }); diff --git a/app/products/playbooks/screens/index.test.tsx b/app/products/playbooks/screens/index.test.tsx index ca432f498..28345c703 100644 --- a/app/products/playbooks/screens/index.test.tsx +++ b/app/products/playbooks/screens/index.test.tsx @@ -10,6 +10,9 @@ import {render} from '@test/intl-test-helper'; import EditCommand from './edit_command'; import ParticipantPlaybooks from './participant_playbooks'; import PlaybookRun from './playbook_run'; +import AddChecklistItemBottomSheet from './playbook_run/checklist/add_checklist_item_bottom_sheet'; +import RenameChecklistBottomSheet from './playbook_run/checklist/rename_checklist_bottom_sheet'; +import RenamePlaybookRunBottomSheet from './playbook_run/rename_playbook_run_bottom_sheet'; import PlaybookRuns from './playbooks_runs'; import PostUpdate from './post_update'; import SelectDate from './select_date'; @@ -78,6 +81,25 @@ jest.mock('@playbooks/screens/post_update', () => ({ __esModule: true, default: jest.fn(), })); + +jest.mock('@playbooks/screens/playbook_run/checklist/rename_checklist_bottom_sheet', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(RenameChecklistBottomSheet).mockImplementation((props) => {Screens.PLAYBOOK_RENAME_CHECKLIST}); + +jest.mock('@playbooks/screens/playbook_run/checklist/add_checklist_item_bottom_sheet', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(AddChecklistItemBottomSheet).mockImplementation((props) => {Screens.PLAYBOOK_ADD_CHECKLIST_ITEM}); + +jest.mock('@playbooks/screens/playbook_run/rename_playbook_run_bottom_sheet', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(RenamePlaybookRunBottomSheet).mockImplementation((props) => {Screens.PLAYBOOK_RENAME_RUN}); + jest.mocked(PostUpdate).mockImplementation((props) => {Screens.PLAYBOOK_POST_UPDATE}); describe('Screen Registration', () => { diff --git a/app/products/playbooks/screens/index.tsx b/app/products/playbooks/screens/index.tsx index 48eee52e4..21975c146 100644 --- a/app/products/playbooks/screens/index.tsx +++ b/app/products/playbooks/screens/index.tsx @@ -16,6 +16,12 @@ export function loadPlaybooksScreen(screenName: string | number) { return withServerDatabase(require('@playbooks/screens/edit_command').default); case Screens.PLAYBOOK_POST_UPDATE: return withServerDatabase(require('@playbooks/screens/post_update').default); + case Screens.PLAYBOOK_RENAME_CHECKLIST: + return withServerDatabase(require('@playbooks/screens/playbook_run/checklist/rename_checklist_bottom_sheet').default); + case Screens.PLAYBOOK_ADD_CHECKLIST_ITEM: + return withServerDatabase(require('@playbooks/screens/playbook_run/checklist/add_checklist_item_bottom_sheet').default); + case Screens.PLAYBOOK_RENAME_RUN: + return withServerDatabase(require('@playbooks/screens/playbook_run/rename_playbook_run_bottom_sheet').default); case Screens.PLAYBOOK_SELECT_USER: return withServerDatabase(require('@playbooks/screens/select_user').default); case Screens.PLAYBOOKS_SELECT_DATE: diff --git a/app/products/playbooks/screens/navigation.test.ts b/app/products/playbooks/screens/navigation.test.ts index 4b2900316..9719fdf4f 100644 --- a/app/products/playbooks/screens/navigation.test.ts +++ b/app/products/playbooks/screens/navigation.test.ts @@ -33,11 +33,11 @@ describe('Playbooks Navigation', () => { expect(mockIntl.formatMessage).toHaveBeenCalledWith({ id: 'playbooks.playbooks_runs.title', - defaultMessage: 'Playbook runs', + defaultMessage: 'Playbook checklists', }); expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOKS_RUNS, - 'Playbook runs', + 'Playbook checklists', {channelId}, expect.objectContaining({ topBar: expect.objectContaining({ @@ -59,11 +59,11 @@ describe('Playbooks Navigation', () => { expect(mockIntl.formatMessage).toHaveBeenCalledWith({ id: 'playbooks.playbook_run.title', - defaultMessage: 'Playbook run', + defaultMessage: 'Playbook checklist', }); expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOK_RUN, - 'Playbook run', + 'Playbook checklist', {playbookRunId}, {}, ); @@ -79,11 +79,11 @@ describe('Playbooks Navigation', () => { expect(mockIntl.formatMessage).toHaveBeenCalledWith({ id: 'playbooks.playbook_run.title', - defaultMessage: 'Playbook run', + defaultMessage: 'Playbook checklist', }); expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOK_RUN, - 'Playbook run', + 'Playbook checklist', {playbookRunId, playbookRun}, {}, ); @@ -273,11 +273,11 @@ describe('Playbooks Navigation', () => { expect(mockIntl.formatMessage).toHaveBeenCalledWith({ id: 'playbooks.participant_playbooks.title', - defaultMessage: 'Playbook runs', + defaultMessage: 'Playbook checklists', }); expect(goToScreen).toHaveBeenCalledWith( Screens.PARTICIPANT_PLAYBOOKS, - 'Playbook runs', + 'Playbook checklists', {}, {}, ); @@ -307,11 +307,11 @@ describe('Playbooks Navigation', () => { expect(mockIntl.formatMessage).toHaveBeenCalledWith({ id: 'playbooks.playbook_run.title', - defaultMessage: 'Playbook run', + defaultMessage: 'Playbook checklist', }); expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOK_RUN, - 'Playbook run', + 'Playbook checklist', {playbookRunId: mockPlaybookRun.id}, {}, ); @@ -339,11 +339,11 @@ describe('Playbooks Navigation', () => { expect(mockIntl.formatMessage).toHaveBeenCalledWith({ id: 'playbooks.playbook_run.title', - defaultMessage: 'Playbook run', + defaultMessage: 'Playbook checklist', }); expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOK_RUN, - 'Playbook run', + 'Playbook checklist', {playbookRunId: mockPlaybookRunModel.id}, {}, ); @@ -401,7 +401,7 @@ describe('Playbooks Navigation', () => { expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOKS_SELECT_PLAYBOOK, - 'Start a run', + 'New', {channelId}, { topBar: { @@ -419,7 +419,7 @@ describe('Playbooks Navigation', () => { expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOKS_SELECT_PLAYBOOK, - 'Start a run', + 'New', {channelId: undefined}, { topBar: { @@ -446,7 +446,7 @@ describe('Playbooks Navigation', () => { expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOKS_START_A_RUN, - 'Start a run', + 'New', {playbook, onRunCreated, channelId}, { topBar: { @@ -470,7 +470,7 @@ describe('Playbooks Navigation', () => { expect(goToScreen).toHaveBeenCalledWith( Screens.PLAYBOOKS_START_A_RUN, - 'Start a run', + 'New', {playbook, onRunCreated, channelId: undefined}, { topBar: { diff --git a/app/products/playbooks/screens/navigation.ts b/app/products/playbooks/screens/navigation.ts index 0c9dc06af..d6e3c2c45 100644 --- a/app/products/playbooks/screens/navigation.ts +++ b/app/products/playbooks/screens/navigation.ts @@ -14,7 +14,7 @@ import type {Options as RNNOptions} from 'react-native-navigation'; export function goToPlaybookRuns(intl: IntlShape, channelId: string, channelName: string) { const theme = getThemeFromState(); - const title = intl.formatMessage({id: 'playbooks.playbooks_runs.title', defaultMessage: 'Playbook runs'}); + const title = intl.formatMessage({id: 'playbooks.playbooks_runs.title', defaultMessage: 'Playbook checklists'}); goToScreen(Screens.PLAYBOOKS_RUNS, title, {channelId}, { topBar: { subtitle: { @@ -26,12 +26,12 @@ export function goToPlaybookRuns(intl: IntlShape, channelId: string, channelName } export function goToParticipantPlaybooks(intl: IntlShape) { - const title = intl.formatMessage({id: 'playbooks.participant_playbooks.title', defaultMessage: 'Playbook runs'}); + const title = intl.formatMessage({id: 'playbooks.participant_playbooks.title', defaultMessage: 'Playbook checklists'}); goToScreen(Screens.PARTICIPANT_PLAYBOOKS, title, {}, {}); } export async function goToPlaybookRun(intl: IntlShape, playbookRunId: string, playbookRun?: PlaybookRun) { - const title = intl.formatMessage({id: 'playbooks.playbook_run.title', defaultMessage: 'Playbook run'}); + const title = intl.formatMessage({id: 'playbooks.playbook_run.title', defaultMessage: 'Playbook checklist'}); goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId, playbookRun}, {}); } @@ -47,7 +47,7 @@ export async function goToPlaybookRunWithChannelSwitch(intl: IntlShape, serverUr } // Then navigate to the playbook run - const title = intl.formatMessage({id: 'playbooks.playbook_run.title', defaultMessage: 'Playbook run'}); + const title = intl.formatMessage({id: 'playbooks.playbook_run.title', defaultMessage: 'Playbook checklist'}); goToScreen(Screens.PLAYBOOK_RUN, title, {playbookRunId: playbookRun.id}, {}); } @@ -84,6 +84,47 @@ export async function goToEditCommand( }, options); } +export async function goToRenameChecklist( + intl: IntlShape, + theme: Theme, + runName: string, + currentTitle: string, + onSave: (newTitle: string) => void, +) { + const title = intl.formatMessage({id: 'playbooks.checklist.rename.title', defaultMessage: 'Rename checklist'}); + const options = getSubtitleOptions(theme, runName); + goToScreen(Screens.PLAYBOOK_RENAME_CHECKLIST, title, { + currentTitle, + onSave, + }, options); +} + +export async function goToAddChecklistItem( + intl: IntlShape, + theme: Theme, + runName: string, + onSave: (title: string) => void, +) { + const title = intl.formatMessage({id: 'playbooks.checklist_item.add.title', defaultMessage: 'New Task'}); + const options = getSubtitleOptions(theme, runName); + goToScreen(Screens.PLAYBOOK_ADD_CHECKLIST_ITEM, title, { + onSave, + }, options); +} + +export async function goToRenamePlaybookRun( + intl: IntlShape, + theme: Theme, + currentTitle: string, + onSave: (newTitle: string) => void, +) { + const title = intl.formatMessage({id: 'playbooks.playbook_run.rename.title', defaultMessage: 'Rename playbook run'}); + goToScreen(Screens.PLAYBOOK_RENAME_RUN, title, { + currentTitle, + onSave, + }); +} + export async function goToSelectUser( theme: Theme, runName: string, @@ -122,7 +163,8 @@ export async function goToSelectPlaybook( theme: Theme, channelId?: string, ) { - const title = intl.formatMessage({id: 'playbooks.select_playbook.title', defaultMessage: 'Start a run'}); + + const title = intl.formatMessage({id: 'playbooks.select_playbook.title', defaultMessage: 'New'}); goToScreen(Screens.PLAYBOOKS_SELECT_PLAYBOOK, title, {channelId}, { topBar: { subtitle: { @@ -134,7 +176,7 @@ export async function goToSelectPlaybook( } export async function goToStartARun(intl: IntlShape, theme: Theme, playbook: Playbook, onRunCreated: (run: PlaybookRun) => void, channelId?: string) { - const title = intl.formatMessage({id: 'playbooks.start_a_run.title', defaultMessage: 'Start a run'}); + const title = intl.formatMessage({id: 'playbooks.start_a_run.title', defaultMessage: 'New'}); const subtitle = playbook.title; goToScreen(Screens.PLAYBOOKS_START_A_RUN, title, {playbook, onRunCreated, channelId}, { topBar: { diff --git a/app/products/playbooks/screens/playbook_run/checklist/add_checklist_item_bottom_sheet.tsx b/app/products/playbooks/screens/playbook_run/checklist/add_checklist_item_bottom_sheet.tsx new file mode 100644 index 000000000..8a8c4bae2 --- /dev/null +++ b/app/products/playbooks/screens/playbook_run/checklist/add_checklist_item_bottom_sheet.tsx @@ -0,0 +1,103 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Keyboard, StyleSheet, View} from 'react-native'; + +import FloatingTextInput from '@components/floating_input/floating_text_input_label'; +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 type {AvailableScreens} from '@typings/screens/navigation'; + +type Props = { + componentId: AvailableScreens; + onSave: (title: string) => void; +} + +const SAVE_BUTTON_ID = 'add-checklist-item'; + +const close = (componentId: AvailableScreens): void => { + Keyboard.dismiss(); + popTopScreen(componentId); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingVertical: 32, + paddingHorizontal: 20, + }, +}); + +const AddChecklistItemBottomSheet = ({ + componentId, + onSave, +}: Props) => { + const intl = useIntl(); + const {formatMessage} = intl; + const theme = useTheme(); + + const [title, setTitle] = useState(''); + const [canSave, setCanSave] = useState(false); + + const rightButton = React.useMemo(() => { + const base = buildNavigationButton( + SAVE_BUTTON_ID, + 'playbooks.checklist_item.add.save', + undefined, + formatMessage({id: 'playbooks.checklist_item.add.save', defaultMessage: 'Add'}), + ); + base.enabled = canSave; + base.color = theme.sidebarHeaderTextColor; + return base; + }, [formatMessage, canSave, theme.sidebarHeaderTextColor]); + + useEffect(() => { + setButtons(componentId, { + rightButtons: [rightButton], + }); + }, [rightButton, componentId]); + + useEffect(() => { + setCanSave(title.trim().length > 0); + }, [title]); + + const handleClose = useCallback(() => { + close(componentId); + }, [componentId]); + + const handleSave = useCallback(() => { + if (title.trim().length > 0) { + onSave(title.trim()); + close(componentId); + } + }, [title, componentId, onSave]); + + useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]); + useAndroidHardwareBackHandler(componentId, handleClose); + + const label = formatMessage({id: 'playbooks.checklist_item.add.label', defaultMessage: 'Task name'}); + + return ( + + + + ); +}; + +export default AddChecklistItemBottomSheet; diff --git a/app/products/playbooks/screens/playbook_run/checklist/checklist.test.tsx b/app/products/playbooks/screens/playbook_run/checklist/checklist.test.tsx index ffbff4eca..13147020f 100644 --- a/app/products/playbooks/screens/playbook_run/checklist/checklist.test.tsx +++ b/app/products/playbooks/screens/playbook_run/checklist/checklist.test.tsx @@ -47,6 +47,7 @@ describe('Checklist', () => { items: mockItems, channelId: 'channel-id-1', playbookRunId: 'run-id-1', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, checklistProgress: { diff --git a/app/products/playbooks/screens/playbook_run/checklist/checklist.tsx b/app/products/playbooks/screens/playbook_run/checklist/checklist.tsx index f8754daef..19d26a32f 100644 --- a/app/products/playbooks/screens/playbook_run/checklist/checklist.tsx +++ b/app/products/playbooks/screens/playbook_run/checklist/checklist.tsx @@ -2,12 +2,16 @@ // See LICENSE.txt for license information. import React, {useCallback, useMemo, useState} from 'react'; +import {useIntl} from 'react-intl'; import {View, Text, TouchableOpacity, type LayoutChangeEvent, useWindowDimensions, StyleSheet} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import CompassIcon from '@components/compass_icon'; +import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; +import {renameChecklist, addChecklistItem} from '@playbooks/actions/remote/checklist'; import ProgressBar from '@playbooks/components/progress_bar'; +import {goToRenameChecklist, goToAddChecklistItem} from '@playbooks/screens/navigation'; import {getChecklistProgress} from '@playbooks/utils/progress'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -37,6 +41,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ fontSize: 18, color: changeOpacity(theme.centerChannelColor, 0.56), }, + editIconContainer: { + alignItems: 'flex-end', + }, + editIcon: { + fontSize: 18, + color: changeOpacity(theme.centerChannelColor, 0.56), + paddingHorizontal: 4, + }, progressText: { ...typography('Body', 100, 'Regular'), color: changeOpacity(theme.centerChannelColor, 0.48), @@ -62,7 +74,29 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ textDecorationLine: 'line-through', }, titleContainer: { - flexShrink: 1, + flex: 1, + }, + progressAndEditContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginLeft: 'auto', + }, + addButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 8, + paddingHorizontal: 12, + gap: 6, + }, + addButtonText: { + ...typography('Body', 100, 'SemiBold'), + color: changeOpacity(theme.centerChannelColor, 0.64), + }, + addButtonIcon: { + fontSize: 18, + color: changeOpacity(theme.centerChannelColor, 0.64), }, })); @@ -72,6 +106,7 @@ type Props = { items: Array; channelId: string; playbookRunId: string; + playbookRunName: string; isFinished: boolean; isParticipant: boolean; checklistProgress: ReturnType; @@ -83,6 +118,7 @@ const Checklist = ({ items, channelId, playbookRunId, + playbookRunName, isFinished, isParticipant, checklistProgress: { @@ -93,6 +129,8 @@ const Checklist = ({ }, }: Props) => { const [expanded, setExpanded] = useState(true); + const intl = useIntl(); + const serverUrl = useServerUrl(); const theme = useTheme(); const styles = getStyleSheet(theme); const height = useSharedValue(0); @@ -102,6 +140,23 @@ const Checklist = ({ setExpanded((prev) => !prev); }, []); + const handleRename = useCallback((newTitle: string) => { + renameChecklist(serverUrl, playbookRunId, checklistNumber, checklist.id, newTitle); + }, [serverUrl, playbookRunId, checklist.id, checklistNumber]); + + const handleEditPress = useCallback((e: any) => { + e.stopPropagation(); + goToRenameChecklist(intl, theme, playbookRunName, checklist.title, handleRename); + }, [intl, theme, playbookRunName, checklist.title, handleRename]); + + const handleAddItem = useCallback((title: string) => { + addChecklistItem(serverUrl, playbookRunId, checklistNumber, title); + }, [serverUrl, playbookRunId, checklistNumber]); + + const handleAddPress = useCallback(() => { + goToAddChecklistItem(intl, theme, playbookRunName, handleAddItem); + }, [intl, theme, playbookRunName, handleAddItem]); + const animatedStyle = useAnimatedStyle(() => { return { height: withTiming(expanded ? height.value : 0, {duration: 300}), @@ -145,7 +200,15 @@ const Checklist = ({ {checklist.title} - {`${completed} / ${totalNumber} done`} + + {`${completed} / ${totalNumber} done`} + + + + ))} + {!isFinished && isParticipant && ( + + + + {intl.formatMessage({id: 'playbooks.checklist_item.add.button', defaultMessage: 'New'})} + + + )} {/* This is a hack to get the height of the checklist items */} ))} + {!isFinished && isParticipant && ( + + + + {intl.formatMessage({id: 'playbooks.checklist_item.add.button', defaultMessage: 'New'})} + + + )} ); diff --git a/app/products/playbooks/screens/playbook_run/checklist/index.test.tsx b/app/products/playbooks/screens/playbook_run/checklist/index.test.tsx index 0a97e999e..62782b3ba 100644 --- a/app/products/playbooks/screens/playbook_run/checklist/index.test.tsx +++ b/app/products/playbooks/screens/playbook_run/checklist/index.test.tsx @@ -63,6 +63,7 @@ describe('Checklist', () => { checklistNumber: 0, channelId: 'channel-id', playbookRunId: 'run-id', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, }; @@ -181,6 +182,7 @@ describe('Checklist', () => { checklistNumber: 0, channelId: 'channel-id', playbookRunId: 'run-id', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, }; @@ -240,6 +242,7 @@ describe('Checklist', () => { checklistNumber: 0, channelId: 'channel-id', playbookRunId: 'run-id', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, }; @@ -271,6 +274,7 @@ describe('Checklist', () => { checklistNumber: 0, channelId: 'channel-id', playbookRunId: 'run-id', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, }; @@ -323,6 +327,7 @@ describe('Checklist', () => { checklistNumber: 0, channelId: 'channel-id', playbookRunId: 'run-id', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, }; diff --git a/app/products/playbooks/screens/playbook_run/checklist/rename_checklist_bottom_sheet.tsx b/app/products/playbooks/screens/playbook_run/checklist/rename_checklist_bottom_sheet.tsx new file mode 100644 index 000000000..1286d8e3c --- /dev/null +++ b/app/products/playbooks/screens/playbook_run/checklist/rename_checklist_bottom_sheet.tsx @@ -0,0 +1,105 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Keyboard, StyleSheet, View} from 'react-native'; + +import FloatingTextInput from '@components/floating_input/floating_text_input_label'; +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 type {AvailableScreens} from '@typings/screens/navigation'; + +type Props = { + componentId: AvailableScreens; + currentTitle: string; + onSave: (newTitle: string) => void; +} + +const SAVE_BUTTON_ID = 'save-checklist-name'; + +const close = (componentId: AvailableScreens): void => { + Keyboard.dismiss(); + popTopScreen(componentId); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingVertical: 32, + paddingHorizontal: 20, + }, +}); + +const RenameChecklistBottomSheet = ({ + componentId, + currentTitle, + onSave, +}: Props) => { + const intl = useIntl(); + const {formatMessage} = intl; + const theme = useTheme(); + + const [title, setTitle] = useState(currentTitle); + const [canSave, setCanSave] = useState(false); + + const rightButton = React.useMemo(() => { + const base = buildNavigationButton( + SAVE_BUTTON_ID, + 'playbooks.checklist.rename.button', + undefined, + formatMessage({id: 'playbooks.checklist.rename.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(title.trim().length > 0 && title !== currentTitle); + }, [title, currentTitle]); + + const handleClose = useCallback(() => { + close(componentId); + }, [componentId]); + + const handleSave = useCallback(() => { + if (title.trim().length > 0) { + onSave(title.trim()); + close(componentId); + } + }, [title, componentId, onSave]); + + useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]); + useAndroidHardwareBackHandler(componentId, handleClose); + + const label = formatMessage({id: 'playbooks.checklist.rename.label', defaultMessage: 'Section name'}); + + return ( + + + + ); +}; + +export default RenameChecklistBottomSheet; diff --git a/app/products/playbooks/screens/playbook_run/checklist_list.test.tsx b/app/products/playbooks/screens/playbook_run/checklist_list.test.tsx index 2ecf54be7..93e963423 100644 --- a/app/products/playbooks/screens/playbook_run/checklist_list.test.tsx +++ b/app/products/playbooks/screens/playbook_run/checklist_list.test.tsx @@ -31,6 +31,7 @@ describe('ChecklistList', () => { checklists: mockChecklists, channelId: 'channel-id-1', playbookRunId: 'run-id-1', + playbookRunName: 'Test Run', isFinished: false, isParticipant: true, }; diff --git a/app/products/playbooks/screens/playbook_run/checklist_list.tsx b/app/products/playbooks/screens/playbook_run/checklist_list.tsx index 281bb4ab4..ef9f0aa68 100644 --- a/app/products/playbooks/screens/playbook_run/checklist_list.tsx +++ b/app/products/playbooks/screens/playbook_run/checklist_list.tsx @@ -12,6 +12,7 @@ type Props = { checklists: Array; channelId: string; playbookRunId: string; + playbookRunName: string; isFinished: boolean; isParticipant: boolean; } @@ -26,6 +27,7 @@ const ChecklistList = ({ checklists, channelId, playbookRunId, + playbookRunName, isFinished, isParticipant, }: Props) => { @@ -37,6 +39,7 @@ const ChecklistList = ({ checklist={checklist} channelId={channelId} playbookRunId={playbookRunId} + playbookRunName={playbookRunName} checklistNumber={index} isFinished={isFinished} isParticipant={isParticipant} diff --git a/app/products/playbooks/screens/playbook_run/error_state.test.tsx b/app/products/playbooks/screens/playbook_run/error_state.test.tsx index 6f4c1e010..6f3821d5e 100644 --- a/app/products/playbooks/screens/playbook_run/error_state.test.tsx +++ b/app/products/playbooks/screens/playbook_run/error_state.test.tsx @@ -17,7 +17,7 @@ describe('ErrorState', () => { it('renders error state correctly', () => { const {getByText} = renderWithIntl(); - expect(getByText('Unable to fetch run details')).toBeTruthy(); + expect(getByText('Unable to fetch details')).toBeTruthy(); expect(getByText('Please check your network connection or try again later.')).toBeTruthy(); }); diff --git a/app/products/playbooks/screens/playbook_run/error_state.tsx b/app/products/playbooks/screens/playbook_run/error_state.tsx index 32a93f13d..93f7f9112 100644 --- a/app/products/playbooks/screens/playbook_run/error_state.tsx +++ b/app/products/playbooks/screens/playbook_run/error_state.tsx @@ -38,7 +38,7 @@ const getStylesFromTheme = makeStyleSheetFromTheme((theme: Theme) => { const messages = defineMessages({ title: { id: 'playbooks.playbook_run.error.title', - defaultMessage: 'Unable to fetch run details', + defaultMessage: 'Unable to fetch details', }, description: { id: 'playbooks.playbook_run.error.description', diff --git a/app/products/playbooks/screens/playbook_run/playbook_run.test.tsx b/app/products/playbooks/screens/playbook_run/playbook_run.test.tsx index 56b60a717..c46908d20 100644 --- a/app/products/playbooks/screens/playbook_run/playbook_run.test.tsx +++ b/app/products/playbooks/screens/playbook_run/playbook_run.test.tsx @@ -193,7 +193,7 @@ describe('PlaybookRun', () => { const userAvatarsStack = getByTestId('user-avatars-stack'); expect(userAvatarsStack.props.users).toBe(props.participants); expect(userAvatarsStack.props.location).toBe('PlaybookRun'); - expect(userAvatarsStack.props.bottomSheetTitle.defaultMessage).toBe('Run Participants'); + expect(userAvatarsStack.props.bottomSheetTitle.defaultMessage).toBe('Participants'); props.owner = undefined; rerender(); @@ -330,7 +330,7 @@ describe('PlaybookRun', () => { const {getByText} = renderWithEverything(, {database}); - expect(getByText('Finish Run')).toBeTruthy(); + expect(getByText('Finish')).toBeTruthy(); }); it('handles finish run button press', () => { @@ -338,12 +338,12 @@ describe('PlaybookRun', () => { props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId})); const {getByText} = renderWithEverything(, {database}); - const finishRunButton = getByText('Finish Run'); + const finishRunButton = getByText('Finish'); fireEvent.press(finishRunButton); expect(Alert.alert).toHaveBeenCalledWith( - 'Finish Run', - 'There are 3 tasks pending.\n\nAre you sure you want to finish the run for all participants?', + 'Finish', + 'There are 3 tasks pending.\n\nAre you sure you want to finish the checklist for all participants?', [ {text: 'Cancel', style: 'cancel'}, {text: 'Finish', style: 'destructive', onPress: expect.any(Function)}, @@ -358,7 +358,7 @@ describe('PlaybookRun', () => { const props = getBaseProps(); const {queryByText} = renderWithEverything(, {database}); - expect(queryByText('Finish Run')).toBeNull(); + expect(queryByText('Finish')).toBeNull(); }); it('shows the error snackbar when finishing run fails', async () => { @@ -367,7 +367,7 @@ describe('PlaybookRun', () => { jest.mocked(finishRun).mockResolvedValue({error: 'error'}); const {getByText} = renderWithEverything(, {database}); - const finishRunButton = getByText('Finish Run'); + const finishRunButton = getByText('Finish'); fireEvent.press(finishRunButton); const finishAction = jest.mocked(Alert.alert).mock.calls[0][2]![1]; diff --git a/app/products/playbooks/screens/playbook_run/playbook_run.tsx b/app/products/playbooks/screens/playbook_run/playbook_run.tsx index 7a6d41c27..16ae9e763 100644 --- a/app/products/playbooks/screens/playbook_run/playbook_run.tsx +++ b/app/products/playbooks/screens/playbook_run/playbook_run.tsx @@ -15,6 +15,7 @@ import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {finishRun, setOwner} from '@playbooks/actions/remote/runs'; +import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run'; import {getRunScheduledTimestamp, isRunFinished} from '@playbooks/utils/run'; import {openUserProfileModal, popTopScreen} from '@screens/navigation'; import {showPlaybookErrorSnackbar} from '@utils/snack_bar'; @@ -52,11 +53,11 @@ const messages = defineMessages({ }, participantsTitle: { id: 'playbooks.playbook_run.participants_title', - defaultMessage: 'Run Participants', + defaultMessage: 'Participants', }, runDetails: { id: 'playbooks.playbook_run.run_details', - defaultMessage: 'Run details', + defaultMessage: 'Checklist details', }, overdue: { id: 'playbooks.playbook_run.overdue', @@ -68,11 +69,11 @@ const messages = defineMessages({ }, finishRunDialogTitle: { id: 'playbooks.playbook_run.finish_run_dialog_title', - defaultMessage: 'Finish Run', + defaultMessage: 'Finish', }, finishRunDialogDescription: { id: 'playbooks.playbook_run.finish_run_dialog_description', - defaultMessage: 'There are {pendingCount} {pendingCount, plural, =1 {task} other {tasks}} pending.\n\nAre you sure you want to finish the run for all participants?', + defaultMessage: 'There are {pendingCount} {pendingCount, plural, =1 {task} other {tasks}} pending.\n\nAre you sure you want to finish the checklist for all participants?', }, finishRunDialogCancel: { id: 'playbooks.playbook_run.finish_run_dialog_cancel', @@ -84,7 +85,7 @@ const messages = defineMessages({ }, finishRunButton: { id: 'playbooks.playbook_run.finish_run_button', - defaultMessage: 'Finish Run', + defaultMessage: 'Finish', }, }); @@ -100,9 +101,20 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ gap: 10, alignItems: 'flex-start', }, + titleRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, title: { ...typography('Heading', 400, 'SemiBold'), color: theme.centerChannelColor, + flex: 1, + }, + editIcon: { + fontSize: 18, + color: changeOpacity(theme.centerChannelColor, 0.56), + paddingHorizontal: 4, }, infoText: { ...typography('Body', 100, 'Regular'), @@ -186,6 +198,8 @@ export default function PlaybookRun({ const isFinished = isRunFinished(playbookRun); const readOnly = isFinished || !isParticipant; + const playbookRunType = useMemo(() => playbookRun?.type || 'playbook', [playbookRun]); + const containerStyle = useMemo(() => { return [ styles.container, @@ -231,6 +245,26 @@ export default function PlaybookRun({ ); }, [handleSelectOwner, intl, owner, participants, playbookRun?.name, theme]); + // this will be back once there is a rename function on the server side + // const handleRename = useCallback(async (newName: string) => { + // if (!playbookRun) { + // return; + // } + + // const res = await renamePlaybookRun(serverUrl, playbookRun.id, newName); + // if (res.error) { + // showPlaybookErrorSnackbar(); + // } + // }, [playbookRun, serverUrl]); + + // const handleEditPress = useCallback(() => { + // if (!playbookRun) { + // return; + // } + + // goToRenamePlaybookRun(intl, theme, playbookRun.name, handleRename); + // }, [intl, theme, playbookRun, handleRename]); + const handleFinishRun = useCallback(() => { if (!playbookRun) { return; @@ -279,7 +313,16 @@ export default function PlaybookRun({ - {playbookRun.name} + + {playbookRun.name} + {/* This will be back once there is a rename function on the server side + + + */} + {isFinished && ( )} - + {playbookRunType !== PLAYBOOK_RUN_TYPES.ChannelChecklistType && ( + + )} @@ -353,6 +398,7 @@ export default function PlaybookRun({ checklists={checklists} channelId={channelId} playbookRunId={playbookRun.id} + playbookRunName={playbookRun.name} isFinished={isRunFinished(playbookRun)} isParticipant={isParticipant} /> diff --git a/app/products/playbooks/screens/playbook_run/rename_playbook_run_bottom_sheet.tsx b/app/products/playbooks/screens/playbook_run/rename_playbook_run_bottom_sheet.tsx new file mode 100644 index 000000000..84e742130 --- /dev/null +++ b/app/products/playbooks/screens/playbook_run/rename_playbook_run_bottom_sheet.tsx @@ -0,0 +1,105 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Keyboard, StyleSheet, View} from 'react-native'; + +import FloatingTextInput from '@components/floating_input/floating_text_input_label'; +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 type {AvailableScreens} from '@typings/screens/navigation'; + +type Props = { + componentId: AvailableScreens; + currentTitle: string; + onSave: (newTitle: string) => void; +} + +const SAVE_BUTTON_ID = 'save-playbook-run-name'; + +const close = (componentId: AvailableScreens): void => { + Keyboard.dismiss(); + popTopScreen(componentId); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingVertical: 32, + paddingHorizontal: 20, + }, +}); + +const RenamePlaybookRunBottomSheet = ({ + componentId, + currentTitle, + onSave, +}: Props) => { + const intl = useIntl(); + const {formatMessage} = intl; + const theme = useTheme(); + + const [title, setTitle] = useState(currentTitle); + const [canSave, setCanSave] = useState(false); + + const rightButton = React.useMemo(() => { + const base = buildNavigationButton( + SAVE_BUTTON_ID, + 'playbooks.playbook_run.rename.button', + undefined, + formatMessage({id: 'playbooks.playbook_run.rename.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(title.trim().length > 0 && title !== currentTitle); + }, [title, currentTitle]); + + const handleClose = useCallback(() => { + close(componentId); + }, [componentId]); + + const handleSave = useCallback(() => { + if (title.trim().length > 0) { + onSave(title.trim()); + close(componentId); + } + }, [title, componentId, onSave]); + + useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]); + useAndroidHardwareBackHandler(componentId, handleClose); + + const label = formatMessage({id: 'playbooks.playbook_run.rename.label', defaultMessage: 'Checklist name'}); + + return ( + + + + ); +}; + +export default RenamePlaybookRunBottomSheet; diff --git a/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx b/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx index 9f73b2a7d..f083588f3 100644 --- a/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx +++ b/app/products/playbooks/screens/playbook_run/status_update_indicator.test.tsx @@ -61,7 +61,7 @@ describe('StatusUpdateIndicator', () => { />, ); - const text = getByText(/Update overdue/); + const text = getByText(/pdate overdue/); expect(text).toHaveStyle({color: Preferences.THEMES.denim.dndIndicator}); const icon = getByTestId('compass-icon'); @@ -79,7 +79,7 @@ describe('StatusUpdateIndicator', () => { />, ); - const text = getByText(/Run finished/); + const text = getByText(/Finished/); expect(text).toHaveStyle({color: changeOpacity(Preferences.THEMES.denim.centerChannelColor, 0.72)}); const icon = getByTestId('compass-icon'); diff --git a/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx b/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx index b9bd27b75..0952732b2 100644 --- a/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx +++ b/app/products/playbooks/screens/playbook_run/status_update_indicator.tsx @@ -71,7 +71,7 @@ const messages = defineMessages({ }, finished: { id: 'playbooks.playbook_run.status_update_finished', - defaultMessage: 'Run finished\n{time}', + defaultMessage: 'Finished at\n{time}', }, update: { id: 'playbooks.playbook_run.status_update', diff --git a/app/products/playbooks/screens/post_update/post_update.test.tsx b/app/products/playbooks/screens/post_update/post_update.test.tsx index e8f868e6a..9c81bb19e 100644 --- a/app/products/playbooks/screens/post_update/post_update.test.tsx +++ b/app/products/playbooks/screens/post_update/post_update.test.tsx @@ -140,7 +140,7 @@ describe('PostUpdate', () => { const {getByText, getByTestId} = renderWithIntlAndTheme(); await waitFor(() => { - expect(getByText(/This update for the run/)).toBeTruthy(); + expect(getByText(/This update for the checklist/)).toBeTruthy(); const input = getByTestId('FloatingTextInput'); expect(input).toHaveProp('value', 'Default message template'); expect(input).toHaveProp('label', 'Update message'); @@ -161,7 +161,7 @@ describe('PostUpdate', () => { expect(selector.props.options).toHaveLength(6); const toggle = getByTestId('playbooks.post_update.selector.also_mark_run_as_finished'); - expect(toggle).toHaveProp('label', 'Also mark the run as finished'); + expect(toggle).toHaveProp('label', 'Also mark the checklist as finished'); expect(toggle).toHaveProp('selected', false); expect(toggle).toHaveProp('type', 'toggle'); }); @@ -221,7 +221,7 @@ describe('PostUpdate', () => { const {getByText} = renderWithIntlAndTheme(); await waitFor(() => { - expect(getByText('This update for the run Test Run will be broadcasted to 2 channels and 2 direct messages.')).toBeTruthy(); + expect(getByText('This update for the checklist Test Run will be broadcasted to 2 channels and 2 direct messages.')).toBeTruthy(); }); }); @@ -430,15 +430,15 @@ describe('PostUpdate', () => { await waitFor(() => { expect(Alert.alert).toHaveBeenCalledWith( - 'Confirm finish run', - 'Are you sure you want to finish the run Test Run for all participants?', + 'Confirm finish checklist', + 'Are you sure you want to finish the checklist Test Run for all participants?', expect.arrayContaining([ expect.objectContaining({ text: 'Cancel', style: 'cancel', }), expect.objectContaining({ - text: 'Finish run', + text: 'Finish', }), ]), ); @@ -470,15 +470,15 @@ describe('PostUpdate', () => { await waitFor(() => { expect(Alert.alert).toHaveBeenCalledWith( - 'Confirm finish run', - 'There are 3 outstanding tasks. Are you sure you want to finish the run Test Run for all participants?', + 'Confirm finish checklist', + 'There are 3 outstanding tasks. Are you sure you want to finish the checklist Test Run for all participants?', expect.arrayContaining([ expect.objectContaining({ text: 'Cancel', style: 'cancel', }), expect.objectContaining({ - text: 'Finish run', + text: 'Finish', }), ]), ); @@ -507,7 +507,7 @@ describe('PostUpdate', () => { // Simulate alert confirm button press const alertCall = jest.mocked(Alert.alert).mock.calls[0]; - const confirmButton = alertCall[2]?.find((button) => button.text === 'Finish run'); + const confirmButton = alertCall[2]?.find((button) => button.text === 'Finish'); await act(async () => { confirmButton?.onPress?.(); diff --git a/app/products/playbooks/screens/post_update/post_update.tsx b/app/products/playbooks/screens/post_update/post_update.tsx index c0ff377ab..9c8432cef 100644 --- a/app/products/playbooks/screens/post_update/post_update.tsx +++ b/app/products/playbooks/screens/post_update/post_update.tsx @@ -226,12 +226,12 @@ const PostUpdate = ({ const onPostUpdate = useCallback(() => { if (alsoMarkRunAsFinished) { - let message = intl.formatMessage({id: 'playbooks.post_update.confirm.message', defaultMessage: 'Are you sure you want to finish the run {runName} for all participants?'}, {runName}); + let message = intl.formatMessage({id: 'playbooks.post_update.confirm.message', defaultMessage: 'Are you sure you want to finish the checklist {runName} for all participants?'}, {runName}); if (outstanding > 0) { - message = intl.formatMessage({id: 'playbooks.post_update.confirm.message.with_tasks', defaultMessage: 'There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run {runName} for all participants?'}, {runName, outstanding}); + message = intl.formatMessage({id: 'playbooks.post_update.confirm.message.with_tasks', defaultMessage: 'There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the checklist {runName} for all participants?'}, {runName, outstanding}); } Alert.alert( - intl.formatMessage({id: 'playbooks.post_update.confirm.title', defaultMessage: 'Confirm finish run'}), + intl.formatMessage({id: 'playbooks.post_update.confirm.title', defaultMessage: 'Confirm finish checklist'}), message, [ { @@ -239,7 +239,7 @@ const PostUpdate = ({ style: 'cancel', }, { - text: intl.formatMessage({id: 'playbooks.post_update.confirm.confirm', defaultMessage: 'Finish run'}), + text: intl.formatMessage({id: 'playbooks.post_update.confirm.confirm', defaultMessage: 'Finish'}), onPress: onConfirm, }, ], @@ -279,7 +279,7 @@ const PostUpdate = ({ introMessage = ( ); @@ -310,7 +310,7 @@ const PostUpdate = ({ label={intl.formatMessage({id: 'playbooks.post_update.label.next_update', defaultMessage: 'Timer for next update'})} /> { , ); - expect(getByText(/No runs in progress/)).toBeTruthy(); + expect(getByText(/No checklists in progress/)).toBeTruthy(); }); it('displays single run in progress correctly', () => { @@ -107,7 +107,7 @@ describe('PlaybookRow', () => { , ); - expect(getByText(/1 run in progress/)).toBeTruthy(); + expect(getByText(/1 checklist in progress/)).toBeTruthy(); }); it('displays multiple runs in progress correctly', () => { @@ -118,7 +118,7 @@ describe('PlaybookRow', () => { , ); - expect(getByText(/5 runs in progress/)).toBeTruthy(); + expect(getByText(/5 checklists in progress/)).toBeTruthy(); }); it('calls onPress when pressed', async () => { diff --git a/app/products/playbooks/screens/select_playbook/playbook_row.tsx b/app/products/playbooks/screens/select_playbook/playbook_row.tsx index 756b7c9f4..871015819 100644 --- a/app/products/playbooks/screens/select_playbook/playbook_row.tsx +++ b/app/products/playbooks/screens/select_playbook/playbook_row.tsx @@ -72,12 +72,12 @@ const PlaybookRow = ({ if (activeRuns === 0) { return intl.formatMessage({ id: 'playbooks.row.no_runs', - defaultMessage: 'No runs in progress', + defaultMessage: 'No checklists in progress', }); } return intl.formatMessage({ id: 'playbooks.row.runs', - defaultMessage: '{count} {count, plural, one {run} other {runs}} in progress', + defaultMessage: '{count} {count, plural, one {checklist} other {checklists}} in progress', }, {count: activeRuns}); }; diff --git a/app/products/playbooks/screens/select_user/select_user.test.tsx b/app/products/playbooks/screens/select_user/select_user.test.tsx index 6abb6d392..13e41806f 100644 --- a/app/products/playbooks/screens/select_user/select_user.test.tsx +++ b/app/products/playbooks/screens/select_user/select_user.test.tsx @@ -288,7 +288,7 @@ describe('SelectUser', () => { const sections = customSection(profiles); expect(sections).toHaveLength(2); - expect(sections[0].id).toBe('RUN PARTICIPANTS'); + expect(sections[0].id).toBe('PARTICIPANTS'); expect(sections[0].data).toContain(participantUser); expect(sections[1].id).toBe('NOT PARTICIPATING'); expect(sections[1].data).toContain(nonParticipantUser); diff --git a/app/products/playbooks/screens/select_user/select_user.tsx b/app/products/playbooks/screens/select_user/select_user.tsx index 24a072852..a7cc92767 100644 --- a/app/products/playbooks/screens/select_user/select_user.tsx +++ b/app/products/playbooks/screens/select_user/select_user.tsx @@ -40,7 +40,7 @@ export type Props = { const messages = defineMessages({ participants: { id: 'playbooks.select_user.participants', - defaultMessage: 'RUN PARTICIPANTS', + defaultMessage: 'PARTICIPANTS', }, notParticipants: { id: 'playbooks.select_user.not_participants', diff --git a/app/products/playbooks/screens/start_a_run/start_a_run.tsx b/app/products/playbooks/screens/start_a_run/start_a_run.tsx index 1a9c6d7c4..422ef5fa0 100644 --- a/app/products/playbooks/screens/start_a_run/start_a_run.tsx +++ b/app/products/playbooks/screens/start_a_run/start_a_run.tsx @@ -184,11 +184,11 @@ function StartARun({ { return { channelId: 'channel-id', channelType: 'O', + currentUserId: 'current-user-id', displayName: 'Test Channel', teamId: 'team-id', hasPlaybookRuns: false, @@ -60,17 +61,20 @@ describe('ChannelHeader', () => { jest.clearAllMocks(); }); - it('does not show playbook button when there are no active runs', () => { + it('shows playbook button with "+" when there are no active runs', () => { const props = getBaseProps(); props.hasPlaybookRuns = false; props.playbooksActiveRuns = 0; - renderWithIntl(); + props.isPlaybooksEnabled = true; - const navHeader = jest.mocked(NavigationHeader).mock.calls[0][0]; - expect(navHeader.rightButtons).toEqual( + const {getByTestId} = renderWithIntl(); + + const navHeader = getByTestId('navigation-header'); + expect(navHeader.props.rightButtons).toEqual( expect.arrayContaining([ - expect.not.objectContaining({ + expect.objectContaining({ iconName: 'product-playbooks', + count: '+', }), ]), ); @@ -168,6 +172,44 @@ describe('ChannelHeader', () => { expect(goToPlaybookRun).not.toHaveBeenCalled(); }); + it('creates a new playbook run when clicking + button with no active runs', async () => { + const mockCreatedRun = { + id: 'new-run-id', + channel_id: 'channel-id', + name: 'Test Channel Checklist', + } as any; + jest.mocked(createPlaybookRun).mockResolvedValue({data: mockCreatedRun}); + jest.mocked(fetchPlaybookRunsForChannel).mockResolvedValue({runs: [mockCreatedRun]}); + + const props = getBaseProps(); + props.playbooksActiveRuns = 0; + props.hasPlaybookRuns = false; + props.displayName = 'Test Channel'; + + const {getByTestId} = renderWithIntl(); + + const navHeader = getByTestId('navigation-header'); + const playbookButton = (navHeader.props as ComponentProps).rightButtons?.find((button) => button.iconName === 'product-playbooks'); + expect(playbookButton).toBeTruthy(); + expect(playbookButton?.count).toBe('+'); + + playbookButton?.onPress(); + + await waitFor(() => { + expect(createPlaybookRun).toHaveBeenCalledWith( + serverUrl, + '', // empty playbook_id + 'current-user-id', + 'team-id', + 'Test Channel Checklist', + '', // empty description + 'channel-id', + ); + expect(fetchPlaybookRunsForChannel).toHaveBeenCalledWith(serverUrl, 'channel-id'); + expect(goToPlaybookRun).toHaveBeenCalledWith(expect.anything(), 'new-run-id'); + }); + }); + it('should not fetch runs when playbooks are disabled', async () => { const ephemeralGetSpy = jest.spyOn(EphemeralStore, 'getChannelPlaybooksSynced'); diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx index ebe9ad982..c2de03f48 100644 --- a/app/screens/channel/header/header.tsx +++ b/app/screens/channel/header/header.tsx @@ -19,14 +19,17 @@ import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {useDefaultHeaderHeight} from '@hooks/header'; import {usePreventDoubleTap} from '@hooks/utils'; -import {fetchPlaybookRunsForChannel} from '@playbooks/actions/remote/runs'; +import {createPlaybookRun, fetchPlaybookRunsForChannel} from '@playbooks/actions/remote/runs'; import {goToPlaybookRun, goToPlaybookRuns} from '@playbooks/screens/navigation'; import {BOTTOM_SHEET_ANDROID_OFFSET} from '@screens/bottom_sheet'; import ChannelBanner from '@screens/channel/header/channel_banner'; import {bottomSheet, popTopScreen, showModal} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import {isTypeDMorGM} from '@utils/channel'; +import {getFullErrorMessage} from '@utils/errors'; import {bottomSheetSnapPoint} from '@utils/helpers'; +import {logDebug} from '@utils/log'; +import {showPlaybookErrorSnackbar} from '@utils/snack_bar'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -40,6 +43,7 @@ type ChannelProps = { canAddBookmarks: boolean; channelId: string; channelType: ChannelType; + currentUserId: string; customStatus?: UserCustomStatus; isBookmarksEnabled: boolean; isCustomStatusEnabled: boolean; @@ -93,6 +97,7 @@ const ChannelHeader = ({ channelId, channelType, componentId, + currentUserId, customStatus, displayName, hasBookmarks, @@ -214,22 +219,51 @@ const ChannelHeader = ({ }); }, [isTablet, callsAvailable, isDMorGM, hasPlaybookRuns, theme, onTitlePress, channelId]); + const handleCreateQuickRun = useCallback(async () => { + const runName = `${displayName} Checklist`; + const res = await createPlaybookRun( + serverUrl, + '', // empty playbook_id + currentUserId, + teamId, + runName, + '', // empty description + channelId, + ); + + if (res.error || !res.data) { + logDebug('error on createPlaybookRun', getFullErrorMessage(res.error)); + showPlaybookErrorSnackbar(); + return; + } + + // Fetch updated runs and navigate to the new run + await fetchPlaybookRunsForChannel(serverUrl, channelId); + await goToPlaybookRun(intl, res.data.id); + }, [serverUrl, currentUserId, teamId, displayName, channelId, intl]); + const openPlaybooksRuns = useCallback(() => { + // If no active runs, create a new one instead + if (playbooksActiveRuns === 0) { + handleCreateQuickRun(); + return; + } + if (activeRunId) { goToPlaybookRun(intl, activeRunId); return; } goToPlaybookRuns(intl, channelId, displayName); - }, [activeRunId, channelId, displayName, intl]); + }, [playbooksActiveRuns, handleCreateQuickRun, activeRunId, channelId, displayName, intl]); const rightButtons = useMemo(() => { const buttons: HeaderRightButton[] = []; - if (playbooksActiveRuns && !isDMorGM) { + if (isPlaybooksEnabled && !isDMorGM) { buttons.push({ iconName: 'product-playbooks', onPress: openPlaybooksRuns, buttonType: 'opacity', - count: playbooksActiveRuns, + count: playbooksActiveRuns || '+', }); } @@ -250,7 +284,7 @@ const ChannelHeader = ({ }); return buttons; - }, [playbooksActiveRuns, isDMorGM, onChannelQuickAction, openPlaybooksRuns]); + }, [isPlaybooksEnabled, playbooksActiveRuns, isDMorGM, onChannelQuickAction, openPlaybooksRuns]); let title = displayName; if (isOwnDirectMessage) { diff --git a/app/screens/channel/header/index.ts b/app/screens/channel/header/index.ts index 787db7ae6..dbec5df95 100644 --- a/app/screens/channel/header/index.ts +++ b/app/screens/channel/header/index.ts @@ -111,6 +111,7 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps return { canAddBookmarks, channelType, + currentUserId, customStatus, displayName, hasBookmarks, diff --git a/app/screens/home/channel_list/categories_list/index.test.tsx b/app/screens/home/channel_list/categories_list/index.test.tsx index f7007fd09..9ae0cc385 100644 --- a/app/screens/home/channel_list/categories_list/index.test.tsx +++ b/app/screens/home/channel_list/categories_list/index.test.tsx @@ -183,7 +183,7 @@ describe('components/categories_list', () => { />, {database}, ); - expect(wrapper.queryByText('Playbook runs')).not.toBeTruthy(); + expect(wrapper.queryByText('Playbook checklists')).not.toBeTruthy(); }); it('should render channel list with Playbooks menu if playbooks feature is enabled', () => { @@ -198,6 +198,6 @@ describe('components/categories_list', () => { />, {database}, ); - expect(wrapper.getByText('Playbook runs')).toBeTruthy(); + expect(wrapper.getByText('Playbook checklists')).toBeTruthy(); }); }); diff --git a/app/utils/deep_link/index.test.ts b/app/utils/deep_link/index.test.ts index b686b76a9..32c7244da 100644 --- a/app/utils/deep_link/index.test.ts +++ b/app/utils/deep_link/index.test.ts @@ -233,7 +233,7 @@ describe('parseAndHandleDeepLink', () => { await parseAndHandleDeepLink('https://existingserver.com/playbooks/playbooks/7b35c77a645e1906e03a2c330f', intl); expect(alertSpy).toHaveBeenCalledWith( intl.formatMessage({id: 'playbooks.only_runs_available.title', defaultMessage: 'Playbooks not available'}), - intl.formatMessage({id: 'playbooks.only_runs_available.description', defaultMessage: 'Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.'}), + intl.formatMessage({id: 'playbooks.only_runs_available.description', defaultMessage: 'Only Playbook Checklists are available on mobile. To access the Playbook, please use the desktop or web app.'}), [{text: intl.formatMessage({id: 'playbooks.only_runs_available.ok', defaultMessage: 'OK'})}], ); }); @@ -244,8 +244,8 @@ describe('parseAndHandleDeepLink', () => { jest.mocked(getActiveServerUrl).mockResolvedValueOnce('https://existingserver.com'); await parseAndHandleDeepLink('https://existingserver.com/playbooks/runs/7b35c77a645e1906e03a2c330f/retrospective', intl); expect(alertSpy).toHaveBeenCalledWith( - intl.formatMessage({id: 'playbooks.retrospective_not_available.title', defaultMessage: 'Playbooks Run Retrospective not available'}), - intl.formatMessage({id: 'playbooks.retrospective_not_available.description', defaultMessage: 'Only Playbook Runs are available on mobile. To fill the Run Retrospective, please use the desktop or web app.'}), + intl.formatMessage({id: 'playbooks.retrospective_not_available.title', defaultMessage: 'Playbooks Retrospective not available'}), + intl.formatMessage({id: 'playbooks.retrospective_not_available.description', defaultMessage: 'Only Playbook Checklists are available on mobile. To fill the Run Retrospective, please use the desktop or web app.'}), [{text: intl.formatMessage({id: 'playbooks.retrospective_not_available.ok', defaultMessage: 'OK'})}], ); }); @@ -273,8 +273,8 @@ describe('parseAndHandleDeepLink', () => { // Re-import to apply mocks await parseAndHandleDeepLink('https://existingserver.com/playbooks/runs/7b35c77a645e1906e03a2c330f', intl); expect(alertSpy).toHaveBeenCalledWith( - intl.formatMessage({id: 'playbooks.fetch_error.title', defaultMessage: 'Unable to open Run'}), - intl.formatMessage({id: 'playbooks.fetch_error.description', defaultMessage: "You don't have permission to view this run, or it may no longer exist."}), + intl.formatMessage({id: 'playbooks.fetch_error.title', defaultMessage: 'Unable to open Checklist'}), + intl.formatMessage({id: 'playbooks.fetch_error.description', defaultMessage: "You don't have permission to view this, or it may no longer exist."}), [{text: intl.formatMessage({id: 'playbooks.fetch_error.OK', defaultMessage: 'Okay'})}], ); }); diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index 70b19ca21..80ec2eaf6 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -118,7 +118,7 @@ export async function handleDeepLink(deepLink: DeepLinkWithData, intlShape?: Int // Alert that playbooks should be access from the webapp or desktop app Alert.alert( intl.formatMessage({id: 'playbooks.only_runs_available.title', defaultMessage: 'Playbooks not available'}), - intl.formatMessage({id: 'playbooks.only_runs_available.description', defaultMessage: 'Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.'}), + intl.formatMessage({id: 'playbooks.only_runs_available.description', defaultMessage: 'Only Playbook Checklists are available on mobile. To access the Playbook, please use the desktop or web app.'}), [{ text: intl.formatMessage({id: 'playbooks.only_runs_available.ok', defaultMessage: 'OK'}), }], @@ -127,8 +127,8 @@ export async function handleDeepLink(deepLink: DeepLinkWithData, intlShape?: Int } case DeepLink.PlaybookRunsRetrospective: { Alert.alert( - intl.formatMessage({id: 'playbooks.retrospective_not_available.title', defaultMessage: 'Playbooks Run Retrospective not available'}), - intl.formatMessage({id: 'playbooks.retrospective_not_available.description', defaultMessage: 'Only Playbook Runs are available on mobile. To fill the Run Retrospective, please use the desktop or web app.'}), + intl.formatMessage({id: 'playbooks.retrospective_not_available.title', defaultMessage: 'Playbooks Retrospective not available'}), + intl.formatMessage({id: 'playbooks.retrospective_not_available.description', defaultMessage: 'Only Playbook Checklists are available on mobile. To fill the Run Retrospective, please use the desktop or web app.'}), [{ text: intl.formatMessage({id: 'playbooks.retrospective_not_available.ok', defaultMessage: 'OK'}), }], @@ -145,8 +145,8 @@ export async function handleDeepLink(deepLink: DeepLinkWithData, intlShape?: Int const {error} = await fetchPlaybookRun(existingServerUrl, deepLinkData.playbookRunId); if (error) { Alert.alert( - intl.formatMessage({id: 'playbooks.fetch_error.title', defaultMessage: 'Unable to open Run'}), - intl.formatMessage({id: 'playbooks.fetch_error.description', defaultMessage: "You don't have permission to view this run, or it may no longer exist."}), + intl.formatMessage({id: 'playbooks.fetch_error.title', defaultMessage: 'Unable to open Checklist'}), + intl.formatMessage({id: 'playbooks.fetch_error.description', defaultMessage: "You don't have permission to view this, or it may no longer exist."}), [{ text: intl.formatMessage({id: 'playbooks.fetch_error.OK', defaultMessage: 'Okay'}), }], diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index bdb9e1f66..527a87c7b 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -988,7 +988,11 @@ "playbook_run.checklist.taskDetails": "Task Details", "playbook_run.out_of_date_header.message": "Unable to connect to server. Content may be out of date. Last updated {lastUpdated}.", "playbook.last_updated": "Last update {date}", - "playbook.participants": "Run Participants", + "playbook.participants": "Participants", + "playbooks.checklist_item.add.button": "New", + "playbooks.checklist_item.add.label": "Task name", + "playbooks.checklist_item.add.save": "Add", + "playbooks.checklist_item.add.title": "New Task", "playbooks.checklist_item.assignee": "Assignee", "playbooks.checklist_item.check": "Check", "playbooks.checklist_item.checked": "Checked", @@ -1001,6 +1005,9 @@ "playbooks.checklist_item.skipped": "Skipped", "playbooks.checklist_item.task_rendered_conditionally": "Task rendered conditionally", "playbooks.checklist_item.task_rendered_conditionally_explanation": "This task was rendered conditionally based on", + "playbooks.checklist.rename.button": "Save", + "playbooks.checklist.rename.label": "Checklist name", + "playbooks.checklist.rename.title": "Rename checklist", "playbooks.create_run.select_playbook.no_results": "No Results", "playbooks.due_date.date_at_time": "{date} at {time}", "playbooks.due_date.none": "None", @@ -1010,46 +1017,49 @@ "playbooks.edit_command.title": "Slash command", "playbooks.edit_due_date.clear.button": "Clear", "playbooks.edit_due_date.save.button": "Save", - "playbooks.fetch_error.description": "You don't have permission to view this run, or it may no longer exist.", + "playbooks.fetch_error.description": "You don't have permission to view this, or it may no longer exist.", "playbooks.fetch_error.OK": "Okay", - "playbooks.fetch_error.title": "Unable to open Run", - "playbooks.home_button.title": "Playbook runs", + "playbooks.fetch_error.title": "Unable to open Checklist", + "playbooks.home_button.title": "Playbook checklists", "playbooks.not_enabled_or_unsupported.description": "Playbooks are either not enabled on this server or the Playbooks version is not supported. Please contact your system administrator.", "playbooks.not_enabled_or_unsupported.OK": "OK", "playbooks.not_enabled_or_unsupported.title": "Playbooks not available", - "playbooks.only_runs_available.description": "Only Playbook Runs are available on mobile. To access the Playbook, please use the desktop or web app.", + "playbooks.only_runs_available.description": "Only Playbook Checklists are available on mobile. To access the Playbook, please use the desktop or web app.", "playbooks.only_runs_available.ok": "OK", "playbooks.only_runs_available.title": "Playbooks not available", - "playbooks.participant_playbooks.title": "Playbook runs", + "playbooks.participant_playbooks.title": "Playbook checklists", "playbooks.playbook_run.error.description": "Please check your network connection or try again later.", - "playbooks.playbook_run.error.title": "Unable to fetch run details", - "playbooks.playbook_run.finish_run_button": "Finish Run", + "playbooks.playbook_run.error.title": "Unable to fetch details", + "playbooks.playbook_run.finish_run_button": "Finish", "playbooks.playbook_run.finish_run_dialog_cancel": "Cancel", - "playbooks.playbook_run.finish_run_dialog_description": "There are {pendingCount} {pendingCount, plural, =1 {task} other {tasks}} pending.\n\nAre you sure you want to finish the run for all participants?", + "playbooks.playbook_run.finish_run_dialog_description": "There are {pendingCount} {pendingCount, plural, =1 {task} other {tasks}} pending.\n\nAre you sure you want to finish the checklist for all participants?", "playbooks.playbook_run.finish_run_dialog_finish": "Finish", - "playbooks.playbook_run.finish_run_dialog_title": "Finish Run", + "playbooks.playbook_run.finish_run_dialog_title": "Finish", "playbooks.playbook_run.finished": "Finished", "playbooks.playbook_run.overdue": "{num} {num, plural, =1 {task} other {tasks}} overdue", "playbooks.playbook_run.owner": "Owner", "playbooks.playbook_run.participants": "Participants", - "playbooks.playbook_run.participants_title": "Run Participants", - "playbooks.playbook_run.run_details": "Run details", + "playbooks.playbook_run.participants_title": "Participants", + "playbooks.playbook_run.rename.button": "Save", + "playbooks.playbook_run.rename.label": "Checklist name", + "playbooks.playbook_run.rename.title": "Rename playbook run", + "playbooks.playbook_run.run_details": "Checklist details", "playbooks.playbook_run.status_update": "Post update", "playbooks.playbook_run.status_update_due": "Update due\n{time}", - "playbooks.playbook_run.status_update_finished": "Run finished\n{time}", + "playbooks.playbook_run.status_update_finished": "Finished at\n{time}", "playbooks.playbook_run.status_update_overdue": "Update overdue\n{time}", "playbooks.playbook_run.tasks": "Tasks", - "playbooks.playbook_run.title": "Playbook run", - "playbooks.playbooks_runs.title": "Playbook runs", + "playbooks.playbook_run.title": "Playbook checklist", + "playbooks.playbooks_runs.title": "Playbook checklists", "playbooks.post_update.confirm.cancel": "Cancel", - "playbooks.post_update.confirm.confirm": "Finish run", - "playbooks.post_update.confirm.message": "Are you sure you want to finish the run {runName} for all participants?", - "playbooks.post_update.confirm.message.with_tasks": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run {runName} for all participants?", - "playbooks.post_update.confirm.title": "Confirm finish run", - "playbooks.post_update.intro": "This update for the run {runName} will be broadcasted to {hasChannels, select, true {{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}} other {}}.", + "playbooks.post_update.confirm.confirm": "Finish", + "playbooks.post_update.confirm.message": "Are you sure you want to finish the checklist {runName} for all participants?", + "playbooks.post_update.confirm.message.with_tasks": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the checklist {runName} for all participants?", + "playbooks.post_update.confirm.title": "Confirm finish checklist", + "playbooks.post_update.intro": "This update for the checklist {runName} will be broadcasted to {hasChannels, select, true {{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}} other {}}.", "playbooks.post_update.intro.no_followers_or_broadcast_channels": "This update will be saved to the overview page.", "playbooks.post_update.label": "Update message", - "playbooks.post_update.label.also_mark_run_as_finished": "Also mark the run as finished", + "playbooks.post_update.label.also_mark_run_as_finished": "Also mark the checklist as finished", "playbooks.post_update.label.next_update": "Timer for next update", "playbooks.post_update.option.1_day": "1 day", "playbooks.post_update.option.1_hour": "1 hour", @@ -1060,42 +1070,42 @@ "playbooks.post_update.placeholder": "Enter your update message", "playbooks.post_update.post.button": "Post", "playbooks.post_update.title": "Post update", - "playbooks.retrospective_not_available.description": "Only Playbook Runs are available on mobile. To fill the Run Retrospective, please use the desktop or web app.", + "playbooks.retrospective_not_available.description": "Only Playbook Checklists are available on mobile. To fill the Run Retrospective, please use the desktop or web app.", "playbooks.retrospective_not_available.ok": "OK", - "playbooks.retrospective_not_available.title": "Playbooks Run Retrospective not available", + "playbooks.retrospective_not_available.title": "Playbooks Retrospective not available", "playbooks.row.last_used": "Last used {time}", "playbooks.row.never_used": "Never used", - "playbooks.row.no_runs": "No runs in progress", - "playbooks.row.runs": "{count} {count, plural, one {run} other {runs}} in progress", - "playbooks.run_list.cached_warning_message": "Showing cached data only. Some playbook runs or updates may be missing from this list.", + "playbooks.row.no_runs": "No checklists in progress", + "playbooks.row.runs": "{count} {count, plural, one {checklist} other {checklists}} in progress", + "playbooks.run_list.cached_warning_message": "Showing cached data only. Some updates may be missing from this list.", "playbooks.run_list.cached_warning_title": "Cannot reach the server", "playbooks.run_list.tab_finished": "Finished", "playbooks.run_list.tab_in_progress": "In Progress", - "playbooks.runs.finished.description": "When a run in this channel finishes, you’ll see it here.", - "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.finished.description": "When a checklist in this channel finishes, you’ll see it here.", + "playbooks.runs.finished.title": "Nothing finished", + "playbooks.runs.in_progress.description": "When a checklist starts in this channel, you’ll see it here.", + "playbooks.runs.in_progress.title": "Nothing in progress", "playbooks.runs.show_more": "Show More", - "playbooks.runs.start_a_new_run": "Start a new run", + "playbooks.runs.start_a_new_run": "New", "playbooks.select_date.title": "Due date", "playbooks.select_playbook.in_this_channel": "In This Channel", "playbooks.select_playbook.other_playbooks": "Other Playbooks", "playbooks.select_playbook.subtitle": "Select a playbook", - "playbooks.select_playbook.title": "Start a run", + "playbooks.select_playbook.title": "New", "playbooks.select_playbook.your_playbooks": "Your Playbooks", "playbooks.select_user.no_assignee": "No Assignee", "playbooks.select_user.not_participants": "NOT PARTICIPATING", - "playbooks.select_user.participants": "RUN PARTICIPANTS", - "playbooks.start_a_run.title": "Start a run", + "playbooks.select_user.participants": "PARTICIPANTS", + "playbooks.start_a_run.title": "New", "playbooks.start_run.channel_label": "Channel", "playbooks.start_run.create_new_channel": "Create a new channel", "playbooks.start_run.create_new_channel.private_channel": "Private channel", "playbooks.start_run.create_new_channel.public_channel": "Public channel", "playbooks.start_run.link_existing_channel": "Link to an existing channel", - "playbooks.start_run.run_description_label": "Run description", - "playbooks.start_run.run_name_error": "Please add a name for this run", - "playbooks.start_run.run_name_label": "Run name", - "playbooks.start_run.run_name_placeholder": "Add a name for your run", + "playbooks.start_run.run_description_label": "Description", + "playbooks.start_run.run_name_error": "Please add a name", + "playbooks.start_run.run_name_label": "Name", + "playbooks.start_run.run_name_placeholder": "Add a name", "playbooks.status_update_post.invalid_status_update_props": "Playbooks status update post with invalid properties", "playbooks.status_update_post.num_tasks": "**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked", "playbooks.status_update_post.participants": "{numParticipants, number} {numParticipants, plural, =1 {participant} other {participants}}", diff --git a/test/test_helper.ts b/test/test_helper.ts index 2d5879de6..c63d8e972 100644 --- a/test/test_helper.ts +++ b/test/test_helper.ts @@ -16,6 +16,7 @@ import {Ringtone} from '@constants/calls'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import DatabaseManager from '@database/manager'; +import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run'; import {prepareCommonSystemValues} from '@queries/servers/system'; import type {APIClientInterface} from '@mattermost/react-native-network-client'; @@ -1175,6 +1176,7 @@ class TestHelperSingleton { update_at: Date.now() + i, items_order: checklists.map((checklist) => checklist.id), status_update_broadcast_channels_enabled: false, + type: PLAYBOOK_RUN_TYPES.PlaybookType, }); } return playbookRuns; @@ -1250,6 +1252,7 @@ class TestHelperSingleton { return { ...this.fakeModel(), playbookId: this.generateId(), + type: 'playbook', postId: null, ownerUserId: this.basicUser?.id || '', teamId: this.basicTeam?.id || '',