[MM-66995] Add editing summary for a checklist (#9414)
* Add editing summary for a checklist * fix spacing * fix duplication issue due to rerender off-screen * remove worktree from cursor as I'm not using that * add tests * address PR review feedback - add double-tap prevention to save button - rename goToRenamePlaybookRun to goToEditPlaybookRun for consistency - add debug log when local DB update fails after successful API call Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update playbook run rename spacing * [MM-66995] Edit summary gatekeep (#9490) * gatekeeping summary edit * Fix playbook run summary edit gating * more tests * Use options object for playbook run edit --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a9258543bb
commit
882c2ec57b
16 changed files with 618 additions and 137 deletions
|
|
@ -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, renamePlaybookRun} from './run';
|
||||
import {handlePlaybookRuns, setOwner, updatePlaybookRun} from './run';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type PlaybookRunModel from '@playbooks/types/database/models/playbook_run';
|
||||
|
|
@ -154,23 +154,113 @@ describe('setOwner', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('renamePlaybookRun', () => {
|
||||
describe('updatePlaybookRun', () => {
|
||||
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 update both name and summary 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 newSummary = 'Updated run summary';
|
||||
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, newName, newSummary);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(newName);
|
||||
expect(updatedRun.summary).toBe(newSummary);
|
||||
});
|
||||
|
||||
it('should trim both name and summary', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const nameWithSpaces = ' Updated Run Name ';
|
||||
const summaryWithSpaces = ' Updated summary ';
|
||||
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, nameWithSpaces, summaryWithSpaces);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe('Updated Run Name');
|
||||
expect(updatedRun.summary).toBe('Updated summary');
|
||||
});
|
||||
|
||||
it('should allow empty summary', 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 emptySummary = '';
|
||||
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, newName, emptySummary);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(newName);
|
||||
expect(updatedRun.summary).toBe('');
|
||||
});
|
||||
|
||||
it('should reject empty name', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const originalName = runs[0].name;
|
||||
const originalSummary = runs[0].summary;
|
||||
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, '', 'New summary');
|
||||
|
||||
expect(error).toBe('Name cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(originalName);
|
||||
expect(updatedRun.summary).toBe(originalSummary);
|
||||
});
|
||||
|
||||
it('should reject whitespace-only name', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const originalName = runs[0].name;
|
||||
const originalSummary = runs[0].summary;
|
||||
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, ' ', 'New summary');
|
||||
|
||||
expect(error).toBe('Name cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(originalName);
|
||||
expect(updatedRun.summary).toBe(originalSummary);
|
||||
});
|
||||
|
||||
it('should handle playbook run not found', async () => {
|
||||
const {error} = await renamePlaybookRun(serverUrl, 'nonexistent', 'New Name');
|
||||
const {error} = await updatePlaybookRun(serverUrl, 'nonexistent', 'New Name', 'New summary');
|
||||
expect(error).toBe('Playbook run not found: nonexistent');
|
||||
});
|
||||
|
||||
it('should handle not found database', async () => {
|
||||
const {error} = await updatePlaybookRun('foo', 'runid', 'New Name', 'New summary');
|
||||
expect(error).toBeTruthy();
|
||||
expect((error as Error).message).toContain('foo database not found');
|
||||
});
|
||||
|
||||
it('should handle database write errors', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
|
@ -180,93 +270,46 @@ describe('renamePlaybookRun', () => {
|
|||
const originalWrite = database.write;
|
||||
database.write = jest.fn().mockRejectedValue(new Error('Database write failed'));
|
||||
|
||||
const {error} = await renamePlaybookRun(serverUrl, playbookRunId, 'New Name');
|
||||
const {error} = await updatePlaybookRun(serverUrl, playbookRunId, 'New Name', 'New summary');
|
||||
expect(error).toBeTruthy();
|
||||
expect((error as Error).message).toBe('Database write failed');
|
||||
|
||||
database.write = originalWrite;
|
||||
});
|
||||
|
||||
it('should rename playbook run successfully', async () => {
|
||||
it('should update only name when summary is unchanged', 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 originalSummary = runs[0].summary;
|
||||
const newName = 'New Name Only';
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, newName);
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, newName, originalSummary);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(newName);
|
||||
expect(updatedRun.summary).toBe(originalSummary);
|
||||
});
|
||||
|
||||
it('should reject empty name string', async () => {
|
||||
it('should handle very long name and summary values', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const originalName = runs[0].name;
|
||||
const longName = 'A'.repeat(300);
|
||||
const longSummary = 'B'.repeat(1000);
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, '');
|
||||
|
||||
expect(error).toBe('Name cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Verify the name was not changed
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(originalName);
|
||||
});
|
||||
|
||||
it('should reject whitespace-only name', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const originalName = runs[0].name;
|
||||
const whitespaceName = ' ';
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, whitespaceName);
|
||||
|
||||
expect(error).toBe('Name cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Verify the name was not changed
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(originalName);
|
||||
});
|
||||
|
||||
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);
|
||||
const {data, error} = await updatePlaybookRun(serverUrl, playbookRunId, longName, longSummary);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(longName);
|
||||
});
|
||||
|
||||
it('should trim leading and trailing whitespace from name', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const nameWithSpaces = ' Updated Run Name ';
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, nameWithSpaces);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe('Updated Run Name');
|
||||
expect(updatedRun.summary).toBe(longSummary);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export async function setOwner(serverUrl: string, playbookRunId: string, ownerId
|
|||
}
|
||||
}
|
||||
|
||||
export async function renamePlaybookRun(serverUrl: string, playbookRunId: string, name: string) {
|
||||
export async function updatePlaybookRun(serverUrl: string, playbookRunId: string, name: string, summary?: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const run = await getPlaybookRunById(database, playbookRunId);
|
||||
|
|
@ -57,12 +57,15 @@ export async function renamePlaybookRun(serverUrl: string, playbookRunId: string
|
|||
await database.write(async () => {
|
||||
run.update((r) => {
|
||||
r.name = name.trim();
|
||||
if (summary !== undefined) {
|
||||
r.summary = summary.trim();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to rename playbook run', error);
|
||||
logError('[updatePlaybookRun]', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ 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, renamePlaybookRun as localRenamePlaybookRun} from '@playbooks/actions/local/run';
|
||||
import {handlePlaybookRuns, setOwner as localSetOwner, updatePlaybookRun as localUpdatePlaybookRun} 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 {fetchPlaybookRunPropertyFields} from './property_fields';
|
||||
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, fetchPlaybookRunsPageForParticipant, setOwner, finishRun, renamePlaybookRun, createPlaybookRun, fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from './runs';
|
||||
import {fetchPlaybookRunsForChannel, fetchFinishedRunsForChannel, fetchPlaybookRunsPageForParticipant, setOwner, finishRun, updatePlaybookRun, createPlaybookRun, fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from './runs';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
const channelId = 'channel-id-1';
|
||||
|
|
@ -827,60 +827,72 @@ describe('postStatusUpdate', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('renamePlaybookRun', () => {
|
||||
describe('updatePlaybookRun', () => {
|
||||
const playbookRunId = 'playbook-run-id-1';
|
||||
const newName = 'New Run Name';
|
||||
const newSummary = 'New run summary';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(localRenamePlaybookRun).mockResolvedValue({data: true});
|
||||
jest.mocked(localUpdatePlaybookRun).mockResolvedValue({data: true});
|
||||
});
|
||||
|
||||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await renamePlaybookRun(serverUrl, playbookRunId, newName);
|
||||
const result = await updatePlaybookRun(serverUrl, playbookRunId, newName, newSummary, true);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(localRenamePlaybookRun).not.toHaveBeenCalled();
|
||||
expect(localUpdatePlaybookRun).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);
|
||||
const result = await updatePlaybookRun(serverUrl, playbookRunId, newName, newSummary, true);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName});
|
||||
expect(localRenamePlaybookRun).not.toHaveBeenCalled();
|
||||
expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName, summary: newSummary});
|
||||
expect(localUpdatePlaybookRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle local DB update failure', async () => {
|
||||
mockClient.patchPlaybookRun.mockResolvedValueOnce(undefined);
|
||||
jest.mocked(localRenamePlaybookRun).mockResolvedValueOnce({error: 'DB error'});
|
||||
jest.mocked(localUpdatePlaybookRun).mockResolvedValueOnce({error: 'DB error'});
|
||||
|
||||
const result = await renamePlaybookRun(serverUrl, playbookRunId, newName);
|
||||
const result = await updatePlaybookRun(serverUrl, playbookRunId, newName, newSummary, true);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName});
|
||||
expect(localRenamePlaybookRun).toHaveBeenCalledWith(serverUrl, playbookRunId, newName);
|
||||
expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName, summary: newSummary});
|
||||
expect(localUpdatePlaybookRun).toHaveBeenCalledWith(serverUrl, playbookRunId, newName, newSummary);
|
||||
});
|
||||
|
||||
it('should rename playbook run successfully', async () => {
|
||||
it('should update playbook run successfully', async () => {
|
||||
mockClient.patchPlaybookRun.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await renamePlaybookRun(serverUrl, playbookRunId, newName);
|
||||
const result = await updatePlaybookRun(serverUrl, playbookRunId, newName, newSummary, true);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.patchPlaybookRun).toHaveBeenCalledWith(playbookRunId, {name: newName, summary: newSummary});
|
||||
expect(localUpdatePlaybookRun).toHaveBeenCalledWith(serverUrl, playbookRunId, newName, newSummary);
|
||||
});
|
||||
|
||||
it('should not include summary in API call when canEditSummary is false', async () => {
|
||||
mockClient.patchPlaybookRun.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await updatePlaybookRun(serverUrl, playbookRunId, newName, newSummary, false);
|
||||
|
||||
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);
|
||||
|
||||
expect(localUpdatePlaybookRun).toHaveBeenCalledWith(serverUrl, playbookRunId, newName, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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, renamePlaybookRun as localRenamePlaybookRun} from '@playbooks/actions/local/run';
|
||||
import {handlePlaybookRuns, setOwner as localSetOwner, updatePlaybookRun as localUpdatePlaybookRun} from '@playbooks/actions/local/run';
|
||||
import {getLastPlaybookRunsFetchAt} from '@playbooks/database/queries/run';
|
||||
import {getMaxRunUpdateAt} from '@playbooks/utils/run';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
|
@ -146,16 +146,24 @@ export const setOwner = async (serverUrl: string, playbookRunId: string, ownerId
|
|||
}
|
||||
};
|
||||
|
||||
export const renamePlaybookRun = async (serverUrl: string, playbookRunId: string, newName: string) => {
|
||||
export const updatePlaybookRun = async (serverUrl: string, playbookRunId: string, name: string, summary: string, canEditSummary: boolean) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.patchPlaybookRun(playbookRunId, {name: newName});
|
||||
const updates: Partial<PlaybookRun> = {name};
|
||||
if (canEditSummary) {
|
||||
updates.summary = summary;
|
||||
}
|
||||
await client.patchPlaybookRun(playbookRunId, updates);
|
||||
|
||||
// Update local database
|
||||
const result = await localRenamePlaybookRun(serverUrl, playbookRunId, newName);
|
||||
return result.error ? result : {data: true};
|
||||
const result = await localUpdatePlaybookRun(serverUrl, playbookRunId, name, canEditSummary ? summary : undefined);
|
||||
if (result.error) {
|
||||
logDebug('[updatePlaybookRun] local update failed after successful API call', getFullErrorMessage(result.error));
|
||||
return result;
|
||||
}
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on renamePlaybookRun', getFullErrorMessage(error));
|
||||
logDebug('[updatePlaybookRun]', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,8 @@
|
|||
export const MINIMUM_MAJOR_VERSION = 2;
|
||||
export const MINIMUM_MINOR_VERSION = 3;
|
||||
export const MINIMUM_PATCH_VERSION = 0;
|
||||
|
||||
// Minimum version required for summary editing feature
|
||||
export const SUMMARY_EDIT_MINIMUM_MAJOR_VERSION = 2;
|
||||
export const SUMMARY_EDIT_MINIMUM_MINOR_VERSION = 7;
|
||||
export const SUMMARY_EDIT_MINIMUM_PATCH_VERSION = 0;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,26 @@
|
|||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {MINIMUM_MAJOR_VERSION, MINIMUM_MINOR_VERSION, MINIMUM_PATCH_VERSION} from '@playbooks/constants/version';
|
||||
import {
|
||||
MINIMUM_MAJOR_VERSION,
|
||||
MINIMUM_MINOR_VERSION,
|
||||
MINIMUM_PATCH_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_MAJOR_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_MINOR_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_PATCH_VERSION,
|
||||
} from '@playbooks/constants/version';
|
||||
|
||||
import {fetchIsPlaybooksEnabled, observeIsPlaybooksEnabled} from './version';
|
||||
import {
|
||||
fetchIsPlaybooksEnabled,
|
||||
fetchIsSummaryEditEnabled,
|
||||
observeIsPlaybooksEnabled,
|
||||
observeIsSummaryEditEnabled,
|
||||
} from './version';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
const MINIMUM_VERSION = `${MINIMUM_MAJOR_VERSION}.${MINIMUM_MINOR_VERSION}.${MINIMUM_PATCH_VERSION}`;
|
||||
const MINIMUM_SUMMARY_VERSION = `${SUMMARY_EDIT_MINIMUM_MAJOR_VERSION}.${SUMMARY_EDIT_MINIMUM_MINOR_VERSION}.${SUMMARY_EDIT_MINIMUM_PATCH_VERSION}`;
|
||||
|
||||
describe('Playbook Version Queries', () => {
|
||||
let operator: ServerDataOperator;
|
||||
|
|
@ -222,4 +235,106 @@ describe('Playbook Version Queries', () => {
|
|||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeIsSummaryEditEnabled', () => {
|
||||
it('should return false when no playbooks version is set', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const result = observeIsSummaryEditEnabled(operator.database);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it(`should return true when playbooks version meets minimum requirements (${MINIMUM_SUMMARY_VERSION})`, async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const result = observeIsSummaryEditEnabled(operator.database);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(false);
|
||||
subscriptionNext.mockClear();
|
||||
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: MINIMUM_SUMMARY_VERSION}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should react to version changes', async () => {
|
||||
const subscriptionNext = jest.fn();
|
||||
const result = observeIsSummaryEditEnabled(operator.database);
|
||||
result.subscribe({next: subscriptionNext});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(false);
|
||||
subscriptionNext.mockClear();
|
||||
|
||||
const aboveVersion = `${SUMMARY_EDIT_MINIMUM_MAJOR_VERSION + 1}.${SUMMARY_EDIT_MINIMUM_MINOR_VERSION + 1}.${SUMMARY_EDIT_MINIMUM_PATCH_VERSION + 1}`;
|
||||
const belowVersion = `${SUMMARY_EDIT_MINIMUM_MAJOR_VERSION - 1}.${SUMMARY_EDIT_MINIMUM_MINOR_VERSION}.${SUMMARY_EDIT_MINIMUM_PATCH_VERSION}`;
|
||||
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: aboveVersion}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(true);
|
||||
subscriptionNext.mockClear();
|
||||
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: belowVersion}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(subscriptionNext).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchIsSummaryEditEnabled', () => {
|
||||
it('should return false when no playbooks version is set', async () => {
|
||||
const result = await fetchIsSummaryEditEnabled(operator.database);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it(`should return true when playbooks version meets minimum requirements (${MINIMUM_SUMMARY_VERSION})`, async () => {
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: MINIMUM_SUMMARY_VERSION}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = await fetchIsSummaryEditEnabled(operator.database);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when playbooks version has higher major version', async () => {
|
||||
const higherVersion = `${SUMMARY_EDIT_MINIMUM_MAJOR_VERSION + 1}.0.0`;
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: higherVersion}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = await fetchIsSummaryEditEnabled(operator.database);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty version string', async () => {
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: ''}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = await fetchIsSummaryEditEnabled(operator.database);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when playbooks version is below minimum', async () => {
|
||||
const belowVersion = `${SUMMARY_EDIT_MINIMUM_MAJOR_VERSION - 1}.${SUMMARY_EDIT_MINIMUM_MINOR_VERSION}.${SUMMARY_EDIT_MINIMUM_PATCH_VERSION}`;
|
||||
await operator.handleSystem({
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION, value: belowVersion}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const result = await fetchIsSummaryEditEnabled(operator.database);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {SYSTEM_IDENTIFIERS, MM_TABLES} from '@constants/database';
|
||||
import {MINIMUM_MAJOR_VERSION, MINIMUM_MINOR_VERSION, MINIMUM_PATCH_VERSION} from '@playbooks/constants/version';
|
||||
import {
|
||||
MINIMUM_MAJOR_VERSION,
|
||||
MINIMUM_MINOR_VERSION,
|
||||
MINIMUM_PATCH_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_MAJOR_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_MINOR_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_PATCH_VERSION,
|
||||
} from '@playbooks/constants/version';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
|
@ -35,8 +42,35 @@ export function observeIsPlaybooksEnabled(database: Database) {
|
|||
return database.get<SystemModel>(MM_TABLES.SERVER.SYSTEM).query(
|
||||
Q.where('id', SYSTEM_IDENTIFIERS.PLAYBOOKS_VERSION),
|
||||
).observeWithColumns(['value']).pipe(
|
||||
switchMap((systems) => {
|
||||
switchMap((systems: SystemModel[]) => {
|
||||
return of$(isPlaybooksEnabledFromSystemModel(systems));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function isSummaryEditEnabledFromSystemModel(systems: SystemModel[]) {
|
||||
const version = systems[0]?.value;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isMinimumServerVersion(
|
||||
version,
|
||||
SUMMARY_EDIT_MINIMUM_MAJOR_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_MINOR_VERSION,
|
||||
SUMMARY_EDIT_MINIMUM_PATCH_VERSION,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchIsSummaryEditEnabled(database: Database) {
|
||||
const systems = await queryPlaybooksVersion(database).fetch();
|
||||
return isSummaryEditEnabledFromSystemModel(systems);
|
||||
}
|
||||
|
||||
export function observeIsSummaryEditEnabled(database: Database) {
|
||||
return queryPlaybooksVersion(database).observeWithColumns(['value']).pipe(
|
||||
switchMap((systems: SystemModel[]) => {
|
||||
return of$(isSummaryEditEnabledFromSystemModel(systems));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {goToScreen} from '@screens/navigation';
|
|||
import TestHelper from '@test/test_helper';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToParticipantPlaybooks, goToPlaybookRunWithChannelSwitch, goToEditCommand, goToSelectUser, goToSelectDate, goToPostUpdate, goToSelectPlaybook, goToStartARun, goToRenameChecklist, goToAddChecklistItem, goToEditChecklistItem, goToRenamePlaybookRun, goToCreateQuickChecklist} from './navigation';
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToParticipantPlaybooks, goToPlaybookRunWithChannelSwitch, goToEditCommand, goToSelectUser, goToSelectDate, goToPostUpdate, goToSelectPlaybook, goToStartARun, goToRenameChecklist, goToAddChecklistItem, goToEditChecklistItem, goToEditPlaybookRun, goToCreateQuickChecklist} from './navigation';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
goToScreen: jest.fn(),
|
||||
|
|
@ -603,23 +603,27 @@ describe('Playbooks Navigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('goToRenamePlaybookRun', () => {
|
||||
it('should navigate to rename playbook run screen with correct parameters', async () => {
|
||||
describe('goToEditPlaybookRun', () => {
|
||||
it('should navigate to edit playbook run screen with correct parameters', async () => {
|
||||
const currentTitle = 'Playbook Run Title';
|
||||
const currentSummary = 'Playbook run summary';
|
||||
const playbookRunId = 'run-id-123';
|
||||
const canEditSummary = true;
|
||||
|
||||
await goToRenamePlaybookRun(mockIntl, Preferences.THEMES.denim, currentTitle, playbookRunId);
|
||||
await goToEditPlaybookRun(mockIntl, Preferences.THEMES.denim, currentTitle, currentSummary, playbookRunId, {canEditSummary});
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.playbook_run.rename.title',
|
||||
defaultMessage: 'Rename playbook run',
|
||||
id: 'playbooks.playbook_run.edit.title',
|
||||
defaultMessage: 'Edit playbook run',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_RENAME_RUN,
|
||||
'Rename playbook run',
|
||||
'Edit playbook run',
|
||||
{
|
||||
currentTitle,
|
||||
currentSummary,
|
||||
playbookRunId,
|
||||
canEditSummary,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -129,16 +129,25 @@ export async function goToEditChecklistItem(
|
|||
}, options);
|
||||
}
|
||||
|
||||
export async function goToRenamePlaybookRun(
|
||||
export type GoToEditPlaybookRunOptions = {
|
||||
canEditSummary?: boolean;
|
||||
};
|
||||
|
||||
export async function goToEditPlaybookRun(
|
||||
intl: IntlShape,
|
||||
theme: Theme,
|
||||
currentTitle: string,
|
||||
currentSummary: string,
|
||||
playbookRunId: string,
|
||||
options?: GoToEditPlaybookRunOptions,
|
||||
) {
|
||||
const title = intl.formatMessage({id: 'playbooks.playbook_run.rename.title', defaultMessage: 'Rename playbook run'});
|
||||
const {canEditSummary = true} = options ?? {};
|
||||
const title = intl.formatMessage({id: 'playbooks.playbook_run.edit.title', defaultMessage: 'Edit playbook run'});
|
||||
goToScreen(Screens.PLAYBOOK_RENAME_RUN, title, {
|
||||
currentTitle,
|
||||
currentSummary,
|
||||
playbookRunId,
|
||||
canEditSummary,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ const Checklist = ({
|
|||
>
|
||||
{items.map((item, index) => (
|
||||
<ChecklistItem
|
||||
key={item.id}
|
||||
key={`calc-${item.id}`}
|
||||
item={item}
|
||||
channelId={channelId}
|
||||
checklistNumber={checklistNumber}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {distinctUntilChanged, map, switchMap} from 'rxjs/operators';
|
|||
import {queryPlaybookChecklistByRun} from '@playbooks/database/queries/checklist';
|
||||
import {queryPlaybookChecklistItemsByChecklists} from '@playbooks/database/queries/item';
|
||||
import {observeParticipantsIdsFromPlaybookModel, observePlaybookRunById, queryParticipantsFromAPIRun} from '@playbooks/database/queries/run';
|
||||
import {observeIsSummaryEditEnabled} from '@playbooks/database/queries/version';
|
||||
import {areItemsOrdersEqual} from '@playbooks/utils/items_order';
|
||||
import {isOverdue, isPending} from '@playbooks/utils/run';
|
||||
import {observeCurrentUserId} from '@queries/servers/system';
|
||||
|
|
@ -58,6 +59,7 @@ const enhanced = withObservables(['playbookRunId', 'playbookRun'], ({playbookRun
|
|||
pendingCount: of$(pendingCount),
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||
canEditSummary: observeIsSummaryEditEnabled(database),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +106,7 @@ const enhanced = withObservables(['playbookRunId', 'playbookRun'], ({playbookRun
|
|||
pendingCount: countObserver.pipe(map((c) => c.pendingCount)),
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
teammateNameDisplay: observeTeammateNameDisplay(database),
|
||||
canEditSummary: observeIsSummaryEditEnabled(database),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {fireEvent, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
|||
import TestHelper from '@test/test_helper';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import {goToRenamePlaybookRun, goToSelectUser} from '../navigation';
|
||||
import {goToEditPlaybookRun, goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import ErrorState from './error_state';
|
||||
|
|
@ -75,13 +75,12 @@ jest.mocked(StatusUpdateIndicator).mockImplementation(
|
|||
);
|
||||
|
||||
jest.mock('../navigation', () => ({
|
||||
goToRenamePlaybookRun: jest.fn(),
|
||||
goToEditPlaybookRun: jest.fn(),
|
||||
goToSelectUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@playbooks/actions/remote/runs', () => ({
|
||||
finishRun: jest.fn(),
|
||||
renamePlaybookRun: jest.fn(),
|
||||
setOwner: jest.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -141,6 +140,7 @@ describe('PlaybookRun', () => {
|
|||
pendingCount: 3,
|
||||
currentUserId: 'current-user',
|
||||
teammateNameDisplay: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
||||
canEditSummary: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -650,11 +650,13 @@ describe('PlaybookRun', () => {
|
|||
act(() => {
|
||||
fireEvent.press(editIcon);
|
||||
});
|
||||
expect(goToRenamePlaybookRun).toHaveBeenCalledWith(
|
||||
expect(goToEditPlaybookRun).toHaveBeenCalledWith(
|
||||
expect.anything(), // intl
|
||||
expect.anything(), // theme
|
||||
'Test Playbook Run',
|
||||
'Test summary',
|
||||
props.playbookRun!.id,
|
||||
{canEditSummary: true},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
|||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import {goToRenamePlaybookRun, goToSelectUser} from '../navigation';
|
||||
import {goToEditPlaybookRun, goToSelectUser} from '../navigation';
|
||||
|
||||
import ChecklistList from './checklist_list';
|
||||
import {PropertyFieldsList} from './components';
|
||||
|
|
@ -173,6 +173,7 @@ type Props = {
|
|||
pendingCount: number;
|
||||
currentUserId: string;
|
||||
teammateNameDisplay: string;
|
||||
canEditSummary: boolean;
|
||||
}
|
||||
|
||||
export default function PlaybookRun({
|
||||
|
|
@ -185,6 +186,7 @@ export default function PlaybookRun({
|
|||
componentId,
|
||||
currentUserId,
|
||||
teammateNameDisplay,
|
||||
canEditSummary,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -265,8 +267,8 @@ export default function PlaybookRun({
|
|||
return;
|
||||
}
|
||||
|
||||
goToRenamePlaybookRun(intl, theme, playbookRun.name, playbookRun.id);
|
||||
}, [intl, theme, playbookRun]);
|
||||
goToEditPlaybookRun(intl, theme, playbookRun.name, playbookRun.summary, playbookRun.id, {canEditSummary});
|
||||
}, [intl, theme, playbookRun, canEditSummary]);
|
||||
|
||||
const handleFinishRun = useCallback(() => {
|
||||
if (!playbookRun) {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import {Keyboard} from 'react-native';
|
|||
import {Preferences} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {renamePlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {updatePlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import RenamePlaybookRunBottomSheet from './rename_playbook_run_bottom_sheet';
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ jest.mock('@managers/security_manager', () => ({
|
|||
getShieldScreenId: jest.fn((id) => `shield-${id}`),
|
||||
}));
|
||||
jest.mock('@playbooks/actions/remote/runs', () => ({
|
||||
renamePlaybookRun: jest.fn(),
|
||||
updatePlaybookRun: jest.fn(),
|
||||
}));
|
||||
jest.mock('@utils/snack_bar', () => ({
|
||||
showPlaybookErrorSnackbar: jest.fn(),
|
||||
|
|
@ -46,6 +47,7 @@ jest.mock('@context/server', () => ({
|
|||
describe('RenamePlaybookRunBottomSheet', () => {
|
||||
const componentId = 'test-component-id' as any;
|
||||
const currentTitle = 'Original Playbook Run';
|
||||
const currentSummary = 'Original summary';
|
||||
const playbookRunId = 'run-id-123';
|
||||
|
||||
const mockRightButton = {
|
||||
|
|
@ -57,7 +59,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(buildNavigationButton).mockReturnValue(mockRightButton as any);
|
||||
jest.mocked(renamePlaybookRun).mockResolvedValue({data: true});
|
||||
jest.mocked(updatePlaybookRun).mockResolvedValue({data: true});
|
||||
jest.mocked(useNavButtonPressed).mockImplementation((buttonId, compId, callback) => {
|
||||
// Simulate button press when buttonId matches
|
||||
if (buttonId === 'save-playbook-run-name' && compId === componentId) {
|
||||
|
|
@ -71,26 +73,35 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
});
|
||||
});
|
||||
|
||||
function getBaseProps() {
|
||||
function getBaseProps(canEditSummary = true) {
|
||||
return {
|
||||
componentId,
|
||||
currentTitle,
|
||||
currentSummary,
|
||||
playbookRunId,
|
||||
canEditSummary,
|
||||
};
|
||||
}
|
||||
|
||||
it('should render correctly with currentTitle', () => {
|
||||
it('should render correctly with currentTitle and currentSummary', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.props.value).toBe(currentTitle);
|
||||
expect(input.props.autoFocus).toBe(true);
|
||||
const titleInput = getByTestId('playbooks.playbook_run.rename.input');
|
||||
expect(titleInput).toBeTruthy();
|
||||
expect(titleInput.props.value).toBe(currentTitle);
|
||||
expect(titleInput.props.autoFocus).toBe(true);
|
||||
|
||||
// Check label is rendered
|
||||
const label = getByText('Checklist name');
|
||||
expect(label).toBeTruthy();
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
expect(summaryInput).toBeTruthy();
|
||||
expect(summaryInput.props.value).toBe(currentSummary);
|
||||
expect(summaryInput.props.multiline).toBe(true);
|
||||
|
||||
// Check labels are rendered
|
||||
const nameLabel = getByText('Checklist name');
|
||||
expect(nameLabel).toBeTruthy();
|
||||
const summaryLabel = getByText('Summary');
|
||||
expect(summaryLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set up navigation buttons on mount', () => {
|
||||
|
|
@ -259,7 +270,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should call renamePlaybookRun and close when save button is pressed with valid title', async () => {
|
||||
it('should call updatePlaybookRun and close when save button is pressed with valid title', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
|
|
@ -277,7 +288,7 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(renamePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, newTitle);
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, newTitle, currentSummary, true);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
|
@ -300,8 +311,8 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(renamePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, 'New Playbook Run Name');
|
||||
expect(renamePlaybookRun).not.toHaveBeenCalledWith('some.server.url', playbookRunId, titleWithSpaces);
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, 'New Playbook Run Name', currentSummary, true);
|
||||
expect(updatePlaybookRun).not.toHaveBeenCalledWith('some.server.url', playbookRunId, titleWithSpaces, currentSummary, true);
|
||||
});
|
||||
|
||||
it('should close when Android back button is pressed', () => {
|
||||
|
|
@ -316,7 +327,31 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
expect(renamePlaybookRun).not.toHaveBeenCalled();
|
||||
expect(updatePlaybookRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should enable save button when only summary changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
|
||||
// Initially disabled (same as original)
|
||||
expect(mockRightButton.enabled).toBe(false);
|
||||
|
||||
// Update with different summary
|
||||
act(() => {
|
||||
fireEvent.changeText(summaryInput, 'New summary');
|
||||
});
|
||||
|
||||
// Button should be enabled now
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update navigation button when canSave changes', () => {
|
||||
|
|
@ -340,5 +375,186 @@ describe('RenamePlaybookRunBottomSheet', () => {
|
|||
rightButtons: [{...mockRightButton, enabled: true}],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updatePlaybookRun with updated summary', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
const newSummary = 'Updated summary text';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(summaryInput, newSummary);
|
||||
});
|
||||
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, currentTitle, newSummary, true);
|
||||
});
|
||||
|
||||
it('should trim summary when saving', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
const summaryWithSpaces = ' New summary with spaces ';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(summaryInput, summaryWithSpaces);
|
||||
});
|
||||
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, currentTitle, 'New summary with spaces', true);
|
||||
});
|
||||
|
||||
it('should show error snackbar when save fails', async () => {
|
||||
jest.mocked(updatePlaybookRun).mockResolvedValueOnce({error: 'Some error'});
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'New Title');
|
||||
});
|
||||
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
expect(popTopScreen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow clearing an existing summary', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(summaryInput, '');
|
||||
});
|
||||
|
||||
// Save button should be enabled since summary changed (from non-empty to empty)
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, currentTitle, '', true);
|
||||
});
|
||||
|
||||
it('should disable save button when summary is reverted to original', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
|
||||
// Change summary to something different
|
||||
act(() => {
|
||||
fireEvent.changeText(summaryInput, 'Different Summary');
|
||||
});
|
||||
|
||||
// Button should be enabled
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: true}],
|
||||
});
|
||||
|
||||
// Change back to original summary
|
||||
act(() => {
|
||||
fireEvent.changeText(summaryInput, currentSummary);
|
||||
});
|
||||
|
||||
// Button should be disabled since nothing changed
|
||||
expect(setButtons).toHaveBeenLastCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: false}],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update both name and summary when both are changed', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const titleInput = getByTestId('playbooks.playbook_run.rename.input');
|
||||
const summaryInput = getByTestId('playbooks.playbook_run.edit.summary_input');
|
||||
|
||||
const newTitle = 'Brand New Title';
|
||||
const newSummary = 'Brand New Summary';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(titleInput, newTitle);
|
||||
fireEvent.changeText(summaryInput, newSummary);
|
||||
});
|
||||
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, newTitle, newSummary, true);
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should hide summary input when summary editing is disabled', () => {
|
||||
const props = getBaseProps(false);
|
||||
const {queryByTestId, queryByText} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
expect(queryByTestId('playbooks.playbook_run.edit.summary_input')).toBeNull();
|
||||
expect(queryByText('Summary')).toBeNull();
|
||||
});
|
||||
|
||||
it('should enable save button when name changes and summary editing is disabled', () => {
|
||||
const props = getBaseProps(false);
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'New Playbook Run Title');
|
||||
});
|
||||
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call updatePlaybookRun when summary editing is disabled', async () => {
|
||||
const props = getBaseProps(false);
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
const newTitle = 'New Playbook Run Title';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
await act(async () => {
|
||||
await saveCallback();
|
||||
});
|
||||
|
||||
expect(updatePlaybookRun).toHaveBeenCalledWith('some.server.url', playbookRunId, newTitle, currentSummary, false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {renamePlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {updatePlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
|
|
@ -20,7 +21,9 @@ import type {AvailableScreens} from '@typings/screens/navigation';
|
|||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
currentTitle: string;
|
||||
currentSummary: string;
|
||||
playbookRunId: string;
|
||||
canEditSummary: boolean;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ID = 'save-playbook-run-name';
|
||||
|
|
@ -33,6 +36,7 @@ const close = (componentId: AvailableScreens): void => {
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
gap: 16,
|
||||
paddingVertical: 32,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
|
|
@ -41,7 +45,9 @@ const styles = StyleSheet.create({
|
|||
const RenamePlaybookRunBottomSheet = ({
|
||||
componentId,
|
||||
currentTitle,
|
||||
currentSummary,
|
||||
playbookRunId,
|
||||
canEditSummary,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
|
|
@ -49,10 +55,14 @@ const RenamePlaybookRunBottomSheet = ({
|
|||
const serverUrl = useServerUrl();
|
||||
|
||||
const [title, setTitle] = useState<string>(currentTitle);
|
||||
const [summary, setSummary] = useState<string>(currentSummary);
|
||||
|
||||
const canSave = useMemo(() => {
|
||||
return title.trim().length > 0 && title !== currentTitle;
|
||||
}, [title, currentTitle]);
|
||||
const nameValid = title.trim().length > 0;
|
||||
const nameChanged = title !== currentTitle;
|
||||
const summaryChanged = canEditSummary && summary !== currentSummary;
|
||||
return nameValid && (nameChanged || summaryChanged);
|
||||
}, [title, currentTitle, summary, currentSummary, canEditSummary]);
|
||||
|
||||
const rightButton = React.useMemo(() => {
|
||||
const base = buildNavigationButton(
|
||||
|
|
@ -78,19 +88,22 @@ const RenamePlaybookRunBottomSheet = ({
|
|||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (canSave) {
|
||||
const res = await renamePlaybookRun(serverUrl, playbookRunId, title.trim());
|
||||
const res = await updatePlaybookRun(serverUrl, playbookRunId, title.trim(), summary.trim(), canEditSummary);
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
} else {
|
||||
close(componentId);
|
||||
}
|
||||
}
|
||||
}, [canSave, title, componentId, serverUrl, playbookRunId]);
|
||||
}, [canSave, title, summary, componentId, serverUrl, playbookRunId, canEditSummary]);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]);
|
||||
const onSave = usePreventDoubleTap(handleSave);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, onSave, [onSave]);
|
||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
||||
const label = formatMessage({id: 'playbooks.playbook_run.rename.label', defaultMessage: 'Checklist name'});
|
||||
const nameLabel = formatMessage({id: 'playbooks.playbook_run.rename.label', defaultMessage: 'Checklist name'});
|
||||
const summaryLabel = formatMessage({id: 'playbooks.playbook_run.edit.summary_label', defaultMessage: 'Summary'});
|
||||
|
||||
return (
|
||||
<View
|
||||
|
|
@ -98,13 +111,24 @@ const RenamePlaybookRunBottomSheet = ({
|
|||
style={styles.container}
|
||||
>
|
||||
<FloatingTextInput
|
||||
label={label}
|
||||
label={nameLabel}
|
||||
onChangeText={setTitle}
|
||||
testID='playbooks.playbook_run.rename.input'
|
||||
value={title}
|
||||
theme={theme}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{canEditSummary && (
|
||||
<FloatingTextInput
|
||||
label={summaryLabel}
|
||||
onChangeText={setSummary}
|
||||
testID='playbooks.playbook_run.edit.summary_input'
|
||||
value={summary}
|
||||
theme={theme}
|
||||
multiline={true}
|
||||
multilineInputHeight={100}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1087,6 +1087,8 @@
|
|||
"playbooks.only_runs_available.ok": "OK",
|
||||
"playbooks.only_runs_available.title": "Playbooks not available",
|
||||
"playbooks.participant_playbooks.title": "Playbook checklists",
|
||||
"playbooks.playbook_run.edit.summary_label": "Summary",
|
||||
"playbooks.playbook_run.edit.title": "Edit playbook run",
|
||||
"playbooks.playbook_run.error.description": "Please check your network connection or try again later.",
|
||||
"playbooks.playbook_run.error.title": "Unable to fetch details",
|
||||
"playbooks.playbook_run.finish_run_button": "Finish",
|
||||
|
|
@ -1103,7 +1105,6 @@
|
|||
"playbooks.playbook_run.property_fields": "Property Fields",
|
||||
"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}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue