[MM-67007] [MM-67008] Allow Item renaming and add-edit description (#9378)
* allow renaming and add-edit description * address commit reviews * fix tests * missing tests, fix a problem with rebase/conflict * use Pressable * address review comments
This commit is contained in:
parent
a3248d2a41
commit
90135d8580
21 changed files with 1110 additions and 51 deletions
|
|
@ -6,7 +6,7 @@ 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, renameChecklist, deleteChecklistItem} from './checklist';
|
||||
import {updateChecklistItem, setChecklistItemCommand, setAssignee, setDueDate, renameChecklist, deleteChecklistItem, updateChecklistItemTitleAndDescription} from './checklist';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
|
|
@ -460,3 +460,79 @@ describe('deleteChecklistItem', () => {
|
|||
await Promise.all(testPromises);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateChecklistItemTitleAndDescription', () => {
|
||||
it('should handle not found database', async () => {
|
||||
const {error} = await updateChecklistItemTitleAndDescription('foo', 'itemid', 'New Title', 'New Description');
|
||||
expect(error).toBeTruthy();
|
||||
expect((error as Error).message).toContain('foo database not found');
|
||||
});
|
||||
|
||||
it('should handle item not found', async () => {
|
||||
const {error} = await updateChecklistItemTitleAndDescription(serverUrl, 'nonexistent', 'New Title', 'New Description');
|
||||
expect(error).toBe('Item not found: nonexistent');
|
||||
});
|
||||
|
||||
it('should handle database write errors', async () => {
|
||||
const checklistId = 'updatetitledesc-checklist-write-error';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const originalWrite = operator.database.write;
|
||||
operator.database.write = jest.fn().mockRejectedValue(new Error('Database write failed'));
|
||||
|
||||
const {error} = await updateChecklistItemTitleAndDescription(serverUrl, item.id, 'New Title', 'New Description');
|
||||
expect(error).toBeTruthy();
|
||||
|
||||
operator.database.write = originalWrite;
|
||||
});
|
||||
|
||||
it('should update title and description successfully', async () => {
|
||||
const checklistId = 'updatetitledesc-checklist-success';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const newTitle = 'Updated Item Title';
|
||||
const newDescription = 'Updated Item Description';
|
||||
const {data, error} = await updateChecklistItemTitleAndDescription(serverUrl, item.id, newTitle, newDescription);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe(newTitle);
|
||||
expect(updated!.description).toBe(newDescription);
|
||||
});
|
||||
|
||||
it('should update with empty description', async () => {
|
||||
const checklistId = 'updatetitledesc-checklist-empty-desc';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const newTitle = 'Updated Item Title';
|
||||
const {data, error} = await updateChecklistItemTitleAndDescription(serverUrl, item.id, newTitle, '');
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe(newTitle);
|
||||
expect(updated!.description).toBe('');
|
||||
});
|
||||
|
||||
it('should update with undefined description', async () => {
|
||||
const checklistId = 'updatetitledesc-checklist-undef-desc';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const newTitle = 'Updated Item Title';
|
||||
const {data, error} = await updateChecklistItemTitleAndDescription(serverUrl, item.id, newTitle, undefined);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe(newTitle);
|
||||
expect(updated!.description).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -134,3 +134,30 @@ export async function deleteChecklistItem(serverUrl: string, itemId: string) {
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateChecklistItemTitleAndDescription(serverUrl: string, itemId: string, title: string, description?: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const item = await getPlaybookChecklistItemById(database, itemId);
|
||||
if (!item) {
|
||||
return {error: `Item not found: ${itemId}`};
|
||||
}
|
||||
|
||||
// Validate title is not empty or whitespace-only
|
||||
if (!title || !title.trim()) {
|
||||
return {error: 'Title cannot be empty or whitespace-only'};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
item.update((i) => {
|
||||
i.title = title.trim();
|
||||
i.description = description?.trim() ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to update checklist item title and description', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
setDueDate as localSetDueDate,
|
||||
renameChecklist as localRenameChecklist,
|
||||
deleteChecklistItem as localDeleteChecklistItem,
|
||||
updateChecklistItemTitleAndDescription as localUpdateChecklistItemTitleAndDescription,
|
||||
} from '@playbooks/actions/local/checklist';
|
||||
import {handlePlaybookRuns} from '@playbooks/actions/local/run';
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ import {
|
|||
renameChecklist,
|
||||
addChecklistItem,
|
||||
deleteChecklistItem,
|
||||
updateChecklistItemTitleAndDescription,
|
||||
} from './checklist';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
|
@ -47,6 +49,7 @@ const mockClient = {
|
|||
addChecklistItem: jest.fn(),
|
||||
deleteChecklistItem: jest.fn(),
|
||||
fetchPlaybookRun: jest.fn(),
|
||||
updateChecklistItem: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@playbooks/actions/local/checklist');
|
||||
|
|
@ -400,7 +403,7 @@ describe('checklist', () => {
|
|||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title);
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, {title});
|
||||
expect(result).toBeDefined();
|
||||
expect('error' in result && result.error).toBeDefined();
|
||||
});
|
||||
|
|
@ -408,10 +411,10 @@ describe('checklist', () => {
|
|||
it('should handle API exception', async () => {
|
||||
mockClient.addChecklistItem.mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title);
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, {title});
|
||||
expect(result).toBeDefined();
|
||||
expect('error' in result && result.error).toBeDefined();
|
||||
expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, title);
|
||||
expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, {title});
|
||||
});
|
||||
|
||||
it('should add checklist item successfully', async () => {
|
||||
|
|
@ -420,11 +423,11 @@ describe('checklist', () => {
|
|||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
(handlePlaybookRuns as jest.Mock).mockResolvedValueOnce({data: true});
|
||||
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title);
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, {title});
|
||||
expect(result).toBeDefined();
|
||||
expect('error' in result ? result.error : undefined).toBeUndefined();
|
||||
expect('data' in result && result.data).toBe(true);
|
||||
expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, title);
|
||||
expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, {title});
|
||||
expect(mockClient.fetchPlaybookRun).toHaveBeenCalledWith(playbookRunId);
|
||||
expect(handlePlaybookRuns).toHaveBeenCalledWith(serverUrl, [mockRun], false, true);
|
||||
});
|
||||
|
|
@ -436,11 +439,11 @@ describe('checklist', () => {
|
|||
mockClient.fetchPlaybookRun.mockResolvedValueOnce(mockRun);
|
||||
(handlePlaybookRuns as jest.Mock).mockResolvedValueOnce({data: true});
|
||||
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, emptyTitle);
|
||||
const result = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, {title: emptyTitle});
|
||||
expect(result).toBeDefined();
|
||||
expect('error' in result ? result.error : undefined).toBeUndefined();
|
||||
expect('data' in result && result.data).toBe(true);
|
||||
expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, emptyTitle);
|
||||
expect(mockClient.addChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, {title: emptyTitle});
|
||||
expect(mockClient.fetchPlaybookRun).toHaveBeenCalledWith(playbookRunId);
|
||||
expect(handlePlaybookRuns).toHaveBeenCalledWith(serverUrl, [mockRun], false, true);
|
||||
});
|
||||
|
|
@ -486,4 +489,49 @@ describe('checklist', () => {
|
|||
expect(localDeleteChecklistItem).toHaveBeenCalledWith(serverUrl, itemId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateChecklistItemTitleAndDescription', () => {
|
||||
const newTitle = 'New Item Title';
|
||||
const newDescription = 'New Item Description';
|
||||
const item: ChecklistItemInput = {title: newTitle, description: newDescription};
|
||||
|
||||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await updateChecklistItemTitleAndDescription(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, item);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.updateChecklistItem).not.toHaveBeenCalled();
|
||||
expect(localUpdateChecklistItemTitleAndDescription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle API exception', async () => {
|
||||
mockClient.updateChecklistItem.mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await updateChecklistItemTitleAndDescription(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, item);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.updateChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, item);
|
||||
expect(localUpdateChecklistItemTitleAndDescription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update checklist item title and description successfully', async () => {
|
||||
mockClient.updateChecklistItem.mockResolvedValueOnce({});
|
||||
|
||||
const result = await updateChecklistItemTitleAndDescription(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, item);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.updateChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, item);
|
||||
expect(localUpdateChecklistItemTitleAndDescription).toHaveBeenCalledWith(serverUrl, itemId, newTitle, newDescription);
|
||||
});
|
||||
|
||||
it('should update with empty description', async () => {
|
||||
mockClient.updateChecklistItem.mockResolvedValueOnce({});
|
||||
const itemWithEmptyDesc: ChecklistItemInput = {title: newTitle, description: ''};
|
||||
|
||||
const result = await updateChecklistItemTitleAndDescription(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, itemWithEmptyDesc);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.updateChecklistItem).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, itemWithEmptyDesc);
|
||||
expect(localUpdateChecklistItemTitleAndDescription).toHaveBeenCalledWith(serverUrl, itemId, newTitle, '');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
setDueDate as localSetDueDate,
|
||||
renameChecklist as localRenameChecklist,
|
||||
deleteChecklistItem as localDeleteChecklistItem,
|
||||
updateChecklistItemTitleAndDescription as localUpdateChecklistItemTitleAndDescription,
|
||||
} from '@playbooks/actions/local/checklist';
|
||||
import {handlePlaybookRuns} from '@playbooks/actions/local/run';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
|
|
@ -187,11 +188,11 @@ export const addChecklistItem = async (
|
|||
serverUrl: string,
|
||||
playbookRunId: string,
|
||||
checklistNumber: number,
|
||||
title: string,
|
||||
item: ChecklistItemInput,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.addChecklistItem(playbookRunId, checklistNumber, title);
|
||||
await client.addChecklistItem(playbookRunId, checklistNumber, item);
|
||||
|
||||
// Fetch and sync the entire run to get the created item with server-generated ID
|
||||
const run = await client.fetchPlaybookRun(playbookRunId);
|
||||
|
|
@ -229,3 +230,24 @@ export const deleteChecklistItem = async (
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const updateChecklistItemTitleAndDescription = async (
|
||||
serverUrl: string,
|
||||
playbookRunId: string,
|
||||
itemId: string,
|
||||
checklistNumber: number,
|
||||
itemNumber: number,
|
||||
item: ChecklistItemInput,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.updateChecklistItem(playbookRunId, checklistNumber, itemNumber, item);
|
||||
|
||||
await localUpdateChecklistItemTitleAndDescription(serverUrl, itemId, item.title, item.description);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on updateChecklistItemTitleAndDescription', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1040,16 +1040,30 @@ describe('renameChecklist', () => {
|
|||
});
|
||||
|
||||
describe('addChecklistItem', () => {
|
||||
test('should add item successfully', async () => {
|
||||
test('should add item with title only', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const title = 'New Item';
|
||||
const item: ChecklistItemInput = {title: 'New Item'};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`;
|
||||
const expectedOptions = {method: 'post', body: {title}};
|
||||
const expectedOptions = {method: 'post', body: item};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.addChecklistItem(playbookRunId, checklistNum, title);
|
||||
await client.addChecklistItem(playbookRunId, checklistNum, item);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should add item with title and description', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const item: ChecklistItemInput = {title: 'New Item', description: 'Item description'};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`;
|
||||
const expectedOptions = {method: 'post', body: item};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.addChecklistItem(playbookRunId, checklistNum, item);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
|
@ -1057,28 +1071,86 @@ describe('addChecklistItem', () => {
|
|||
test('should handle network errors', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const title = 'New Item';
|
||||
const item: ChecklistItemInput = {title: 'New Item'};
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.addChecklistItem(playbookRunId, checklistNum, title)).rejects.toThrow('Network error');
|
||||
await expect(client.addChecklistItem(playbookRunId, checklistNum, item)).rejects.toThrow('Network error');
|
||||
});
|
||||
|
||||
test('should handle invalid checklist number', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = -1;
|
||||
const title = 'New Item';
|
||||
const item: ChecklistItemInput = {title: 'New Item'};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`;
|
||||
const expectedOptions = {method: 'post', body: {title}};
|
||||
const expectedOptions = {method: 'post', body: item};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.addChecklistItem(playbookRunId, checklistNum, title);
|
||||
await client.addChecklistItem(playbookRunId, checklistNum, item);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateChecklistItem', () => {
|
||||
test('should update item with title only', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const item: ChecklistItemInput = {title: 'Updated Title'};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}`;
|
||||
const expectedOptions = {method: 'put', body: item};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.updateChecklistItem(playbookRunId, checklistNum, itemNum, item);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should update item with title and description', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const item: ChecklistItemInput = {title: 'Updated Title', description: 'Updated description'};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}`;
|
||||
const expectedOptions = {method: 'put', body: item};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.updateChecklistItem(playbookRunId, checklistNum, itemNum, item);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should handle zero indices', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 0;
|
||||
const itemNum = 0;
|
||||
const item: ChecklistItemInput = {title: 'Updated Title'};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}`;
|
||||
const expectedOptions = {method: 'put', body: item};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.updateChecklistItem(playbookRunId, checklistNum, itemNum, item);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should handle network errors', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const item: ChecklistItemInput = {title: 'Updated Title'};
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.updateChecklistItem(playbookRunId, checklistNum, itemNum, item)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRunPropertyFields', () => {
|
||||
test('should fetch property fields without updatedSince parameter', async () => {
|
||||
const runId = 'run123';
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ export interface ClientPlaybooksMix {
|
|||
restoreChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise<void>;
|
||||
setAssignee: (playbookRunId: string, checklistNum: number, itemNum: number, assigneeId?: string) => Promise<void>;
|
||||
setDueDate: (playbookRunId: string, checklistNum: number, itemNum: number, date?: number) => Promise<void>;
|
||||
addChecklistItem: (playbookRunId: string, checklistNum: number, title: string) => Promise<void>;
|
||||
addChecklistItem: (playbookRunId: string, checklistNum: number, item: ChecklistItemInput) => Promise<void>;
|
||||
deleteChecklistItem: (playbookRunId: string, checklistNum: number, itemNum: number) => Promise<void>;
|
||||
updateChecklistItem: (playbookRunId: string, checklistNum: number, itemNum: number, item: ChecklistItemInput) => Promise<void>;
|
||||
|
||||
renameChecklist: (playbookRunId: string, checklistNumber: number, newName: string) => Promise<void>;
|
||||
|
||||
|
|
@ -210,10 +211,17 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
);
|
||||
};
|
||||
|
||||
addChecklistItem = async (playbookRunId: string, checklistNum: number, title: string) => {
|
||||
addChecklistItem = async (playbookRunId: string, checklistNum: number, item: ChecklistItemInput) => {
|
||||
await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNum}/add`,
|
||||
{method: 'post', body: {title}},
|
||||
{method: 'post', body: item},
|
||||
);
|
||||
};
|
||||
|
||||
updateChecklistItem = async (playbookRunId: string, checklistNum: number, itemNum: number, item: ChecklistItemInput) => {
|
||||
await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNum}/item/${itemNum}`,
|
||||
{method: 'put', body: item},
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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_EDIT_CHECKLIST_ITEM = 'PlaybookEditChecklistItem';
|
||||
export const PLAYBOOK_RENAME_RUN = 'PlaybookRenameRun';
|
||||
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
|
||||
export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate';
|
||||
|
|
@ -23,6 +24,7 @@ export default {
|
|||
PLAYBOOK_POST_UPDATE,
|
||||
PLAYBOOK_RENAME_CHECKLIST,
|
||||
PLAYBOOK_ADD_CHECKLIST_ITEM,
|
||||
PLAYBOOK_EDIT_CHECKLIST_ITEM,
|
||||
PLAYBOOK_RENAME_RUN,
|
||||
PLAYBOOK_SELECT_USER,
|
||||
PLAYBOOKS_SELECT_DATE,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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 EditChecklistItemBottomSheet from './playbook_run/checklist/edit_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';
|
||||
|
|
@ -101,6 +102,12 @@ jest.mock('@playbooks/screens/playbook_run/checklist/add_checklist_item_bottom_s
|
|||
}));
|
||||
jest.mocked(AddChecklistItemBottomSheet).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_ADD_CHECKLIST_ITEM}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/playbook_run/checklist/edit_checklist_item_bottom_sheet', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(EditChecklistItemBottomSheet).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_EDIT_CHECKLIST_ITEM}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/playbook_run/rename_playbook_run_bottom_sheet', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export function loadPlaybooksScreen(screenName: string | number) {
|
|||
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_EDIT_CHECKLIST_ITEM:
|
||||
return withServerDatabase(require('@playbooks/screens/playbook_run/checklist/edit_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:
|
||||
|
|
|
|||
|
|
@ -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, goToRenamePlaybookRun, goToCreateQuickChecklist} from './navigation';
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToParticipantPlaybooks, goToPlaybookRunWithChannelSwitch, goToEditCommand, goToSelectUser, goToSelectDate, goToPostUpdate, goToSelectPlaybook, goToStartARun, goToRenameChecklist, goToAddChecklistItem, goToEditChecklistItem, goToRenamePlaybookRun, goToCreateQuickChecklist} from './navigation';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
goToScreen: jest.fn(),
|
||||
|
|
@ -542,6 +542,67 @@ describe('Playbooks Navigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('goToEditChecklistItem', () => {
|
||||
it('should navigate to edit checklist item screen with correct parameters', async () => {
|
||||
const currentTitle = 'Test Task';
|
||||
const currentDescription = 'Test Description';
|
||||
const onSave = jest.fn();
|
||||
|
||||
await goToEditChecklistItem(mockIntl, Preferences.THEMES.denim, 'Run 1', currentTitle, currentDescription, onSave);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.checklist_item.edit.title',
|
||||
defaultMessage: 'Edit Task',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_EDIT_CHECKLIST_ITEM,
|
||||
'Edit Task',
|
||||
{
|
||||
currentTitle,
|
||||
currentDescription,
|
||||
onSave,
|
||||
},
|
||||
{
|
||||
topBar: {
|
||||
subtitle: {
|
||||
text: 'Run 1',
|
||||
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should navigate to edit checklist item screen with undefined description', async () => {
|
||||
const currentTitle = 'Test Task';
|
||||
const onSave = jest.fn();
|
||||
|
||||
await goToEditChecklistItem(mockIntl, Preferences.THEMES.denim, 'Run 1', currentTitle, undefined, onSave);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.checklist_item.edit.title',
|
||||
defaultMessage: 'Edit Task',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_EDIT_CHECKLIST_ITEM,
|
||||
'Edit Task',
|
||||
{
|
||||
currentTitle,
|
||||
currentDescription: undefined,
|
||||
onSave,
|
||||
},
|
||||
{
|
||||
topBar: {
|
||||
subtitle: {
|
||||
text: 'Run 1',
|
||||
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToRenamePlaybookRun', () => {
|
||||
it('should navigate to rename playbook run screen with correct parameters', async () => {
|
||||
const currentTitle = 'Playbook Run Title';
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export async function goToAddChecklistItem(
|
|||
intl: IntlShape,
|
||||
theme: Theme,
|
||||
runName: string,
|
||||
onSave: (title: string) => void,
|
||||
onSave: (item: ChecklistItemInput) => void,
|
||||
) {
|
||||
const title = intl.formatMessage({id: 'playbooks.checklist_item.add.title', defaultMessage: 'New Task'});
|
||||
const options = getSubtitleOptions(theme, runName);
|
||||
|
|
@ -112,6 +112,23 @@ export async function goToAddChecklistItem(
|
|||
}, options);
|
||||
}
|
||||
|
||||
export async function goToEditChecklistItem(
|
||||
intl: IntlShape,
|
||||
theme: Theme,
|
||||
runName: string,
|
||||
currentTitle: string,
|
||||
currentDescription: string | undefined,
|
||||
onSave: (item: ChecklistItemInput) => void,
|
||||
) {
|
||||
const title = intl.formatMessage({id: 'playbooks.checklist_item.edit.title', defaultMessage: 'Edit Task'});
|
||||
const options = getSubtitleOptions(theme, runName);
|
||||
goToScreen(Screens.PLAYBOOK_EDIT_CHECKLIST_ITEM, title, {
|
||||
currentTitle,
|
||||
currentDescription,
|
||||
onSave,
|
||||
}, options);
|
||||
}
|
||||
|
||||
export async function goToRenamePlaybookRun(
|
||||
intl: IntlShape,
|
||||
theme: Theme,
|
||||
|
|
|
|||
|
|
@ -66,9 +66,11 @@ describe('AddChecklistItemBottomSheet', () => {
|
|||
expect(input.props.value).toBe('');
|
||||
expect(input.props.autoFocus).toBe(true);
|
||||
|
||||
// Check label is rendered
|
||||
const label = getByText('Task name');
|
||||
expect(label).toBeTruthy();
|
||||
// Check labels are rendered
|
||||
const titleLabel = getByText('Task name');
|
||||
expect(titleLabel).toBeTruthy();
|
||||
const descriptionLabel = getByText('Description');
|
||||
expect(descriptionLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set up navigation buttons on mount', () => {
|
||||
|
|
@ -122,6 +124,45 @@ describe('AddChecklistItemBottomSheet', () => {
|
|||
expect(input.props.value).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should update description when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.add.description_input');
|
||||
const newDescription = 'Task description';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(descriptionInput, newDescription);
|
||||
});
|
||||
|
||||
expect(descriptionInput.props.value).toBe(newDescription);
|
||||
});
|
||||
|
||||
it('should call onSave with description when provided', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const titleInput = getByTestId('playbooks.checklist_item.add.input');
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.add.description_input');
|
||||
const taskTitle = 'New Task';
|
||||
const taskDescription = 'Task description';
|
||||
|
||||
// Set title and description
|
||||
act(() => {
|
||||
fireEvent.changeText(titleInput, taskTitle);
|
||||
fireEvent.changeText(descriptionInput, taskDescription);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'add-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith({title: taskTitle, description: taskDescription});
|
||||
});
|
||||
|
||||
it('should enable save button when title has content', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
|
@ -221,7 +262,7 @@ describe('AddChecklistItemBottomSheet', () => {
|
|||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith(taskTitle);
|
||||
expect(mockOnSave).toHaveBeenCalledWith({title: taskTitle});
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
|
@ -245,7 +286,7 @@ describe('AddChecklistItemBottomSheet', () => {
|
|||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('Task with spaces');
|
||||
expect(mockOnSave).toHaveBeenCalledWith({title: 'Task with spaces'});
|
||||
expect(mockOnSave).not.toHaveBeenCalledWith(taskTitle);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type {AvailableScreens} from '@typings/screens/navigation';
|
|||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
onSave: (title: string) => void;
|
||||
onSave: (item: ChecklistItemInput) => void;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ID = 'add-checklist-item';
|
||||
|
|
@ -31,6 +31,7 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
paddingVertical: 32,
|
||||
paddingHorizontal: 20,
|
||||
gap: 16,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ const AddChecklistItemBottomSheet = ({
|
|||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
|
||||
const canSave = useMemo(() => {
|
||||
return Boolean(title.trim().length);
|
||||
|
|
@ -72,15 +74,19 @@ const AddChecklistItemBottomSheet = ({
|
|||
|
||||
const handleSave = useCallback(() => {
|
||||
if (canSave) {
|
||||
onSave(title.trim());
|
||||
onSave({
|
||||
title: title.trim(),
|
||||
...(description.trim() && {description: description.trim()}),
|
||||
});
|
||||
close(componentId);
|
||||
}
|
||||
}, [canSave, title, componentId, onSave]);
|
||||
}, [canSave, title, description, componentId, onSave]);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]);
|
||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
||||
const label = formatMessage({id: 'playbooks.checklist_item.add.label', defaultMessage: 'Task name'});
|
||||
const titleLabel = formatMessage({id: 'playbooks.checklist_item.add.label', defaultMessage: 'Task name'});
|
||||
const descriptionLabel = formatMessage({id: 'playbooks.checklist_item.add.description_label', defaultMessage: 'Description'});
|
||||
|
||||
return (
|
||||
<View
|
||||
|
|
@ -88,13 +94,22 @@ const AddChecklistItemBottomSheet = ({
|
|||
style={styles.container}
|
||||
>
|
||||
<FloatingTextInput
|
||||
label={label}
|
||||
label={titleLabel}
|
||||
onChangeText={setTitle}
|
||||
testID='playbooks.checklist_item.add.input'
|
||||
value={title}
|
||||
theme={theme}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<FloatingTextInput
|
||||
label={descriptionLabel}
|
||||
onChangeText={setDescription}
|
||||
testID='playbooks.checklist_item.add.description_input'
|
||||
value={description}
|
||||
theme={theme}
|
||||
multiline={true}
|
||||
multilineInputHeight={100}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ describe('Checklist', () => {
|
|||
|
||||
const onAdd = jest.mocked(goToAddChecklistItem).mock.calls[0][3];
|
||||
await act(async () => {
|
||||
await onAdd('New Item');
|
||||
await onAdd({title: 'New Item'});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -394,7 +394,7 @@ describe('Checklist', () => {
|
|||
serverUrl,
|
||||
props.playbookRunId,
|
||||
props.checklistNumber,
|
||||
'New Item',
|
||||
{title: 'New Item'},
|
||||
);
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
expect(logError).toHaveBeenCalledWith('error on addChecklistItem', expect.any(String));
|
||||
|
|
@ -404,7 +404,7 @@ describe('Checklist', () => {
|
|||
it('handles successful add item without errors', async () => {
|
||||
jest.mocked(addChecklistItem).mockResolvedValueOnce({data: true} as {data: boolean});
|
||||
jest.mocked(goToAddChecklistItem).mockImplementation(async (intl, theme, playbookRunName, onAdd) => {
|
||||
onAdd('New Item');
|
||||
onAdd({title: 'New Item'});
|
||||
});
|
||||
|
||||
const props = getBaseProps();
|
||||
|
|
@ -423,7 +423,7 @@ describe('Checklist', () => {
|
|||
serverUrl,
|
||||
props.playbookRunId,
|
||||
props.checklistNumber,
|
||||
'New Item',
|
||||
{title: 'New Item'},
|
||||
);
|
||||
expect(showPlaybookErrorSnackbar).not.toHaveBeenCalled();
|
||||
expect(logError).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -157,8 +157,8 @@ const Checklist = ({
|
|||
goToRenameChecklist(intl, theme, playbookRunName, checklist.title, handleRename);
|
||||
}, [intl, theme, playbookRunName, checklist.title, handleRename]);
|
||||
|
||||
const handleAddItem = useCallback(async (title: string) => {
|
||||
const res = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, title);
|
||||
const handleAddItem = useCallback(async (item: ChecklistItemInput) => {
|
||||
const res = await addChecklistItem(serverUrl, playbookRunId, checklistNumber, item);
|
||||
if ('error' in res && res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
logError('error on addChecklistItem', getFullErrorMessage(res.error));
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import OptionBox from '@components/option_box';
|
|||
import OptionItem from '@components/option_item';
|
||||
import {Preferences} from '@constants';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {setAssignee, setChecklistItemCommand, setDueDate, deleteChecklistItem} from '@playbooks/actions/remote/checklist';
|
||||
import {goToEditCommand, goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {setAssignee, setChecklistItemCommand, setDueDate, deleteChecklistItem, updateChecklistItemTitleAndDescription} from '@playbooks/actions/remote/checklist';
|
||||
import {goToEditChecklistItem, goToEditCommand, goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
|
@ -837,4 +837,110 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit button', () => {
|
||||
it('should render edit button when isDisabled is false', () => {
|
||||
const props = getBaseProps();
|
||||
props.isDisabled = false;
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(getByTestId('checklist_item.edit_button')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not render edit button when isDisabled is true', () => {
|
||||
const props = getBaseProps();
|
||||
props.isDisabled = true;
|
||||
const {queryByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(queryByTestId('checklist_item.edit_button')).toBeNull();
|
||||
});
|
||||
|
||||
it('should open edit modal when edit button is pressed', () => {
|
||||
const props = getBaseProps();
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
id: 'item-1',
|
||||
title: 'Test Checklist Item',
|
||||
description: 'This is a test description',
|
||||
state: '',
|
||||
command: 'test command',
|
||||
});
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const editButton = getByTestId('checklist_item.edit_button');
|
||||
fireEvent.press(editButton);
|
||||
|
||||
expect(goToEditChecklistItem).toHaveBeenCalledWith(
|
||||
expect.anything(), // intl
|
||||
expect.anything(), // theme
|
||||
'Run 1',
|
||||
'Test Checklist Item',
|
||||
'This is a test description',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should open edit modal with undefined description when item has no description', () => {
|
||||
const props = getBaseProps();
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
...mockItem,
|
||||
description: '',
|
||||
});
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const editButton = getByTestId('checklist_item.edit_button');
|
||||
fireEvent.press(editButton);
|
||||
|
||||
expect(goToEditChecklistItem).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'Run 1',
|
||||
'Test Checklist Item',
|
||||
undefined,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call updateChecklistItemTitleAndDescription when edit is saved', async () => {
|
||||
jest.mocked(updateChecklistItemTitleAndDescription).mockResolvedValue({data: true});
|
||||
const props = getBaseProps();
|
||||
props.checklistNumber = 2;
|
||||
props.itemNumber = 3;
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const editButton = getByTestId('checklist_item.edit_button');
|
||||
fireEvent.press(editButton);
|
||||
|
||||
const handleEditItem = jest.mocked(goToEditChecklistItem).mock.calls[0][5];
|
||||
await act(async () => {
|
||||
handleEditItem({title: 'New Title', description: 'New Description'});
|
||||
});
|
||||
|
||||
expect(updateChecklistItemTitleAndDescription).toHaveBeenCalledWith(
|
||||
'server-url',
|
||||
'run-1',
|
||||
'item-1',
|
||||
2,
|
||||
3,
|
||||
{title: 'New Title', description: 'New Description'},
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error snackbar when updateChecklistItemTitleAndDescription fails', async () => {
|
||||
jest.mocked(updateChecklistItemTitleAndDescription).mockResolvedValue({error: 'Update failed'});
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const editButton = getByTestId('checklist_item.edit_button');
|
||||
fireEvent.press(editButton);
|
||||
|
||||
const handleEditItem = jest.mocked(goToEditChecklistItem).mock.calls[0][5];
|
||||
await act(async () => {
|
||||
handleEditItem({title: 'New Title', description: 'New Description'});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import OptionBox from '@components/option_box';
|
|||
import OptionItem, {ITEM_HEIGHT} from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {setAssignee, setChecklistItemCommand, setDueDate, deleteChecklistItem} from '@playbooks/actions/remote/checklist';
|
||||
import {goToEditCommand, goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {setAssignee, setChecklistItemCommand, setDueDate, deleteChecklistItem, updateChecklistItemTitleAndDescription} from '@playbooks/actions/remote/checklist';
|
||||
import {goToEditChecklistItem, goToEditCommand, goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {getDueDateString} from '@playbooks/utils/time';
|
||||
import {dismissBottomSheet, openUserProfileModal} from '@screens/navigation';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
|
@ -121,6 +121,20 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
|||
alignItems: 'flex-start',
|
||||
gap: 12,
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
},
|
||||
editIcon: {
|
||||
fontSize: 18,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
editIconPressed: {
|
||||
opacity: 0.2,
|
||||
},
|
||||
taskTitle: {
|
||||
...typography('Body', 300, 'Regular'),
|
||||
color: theme.centerChannelColor,
|
||||
|
|
@ -333,6 +347,18 @@ const ChecklistItemBottomSheet = ({
|
|||
);
|
||||
}, [assignee?.id, handleRemove, handleSelect, intl, participantIds, runName, theme]);
|
||||
|
||||
const handleEditItem = useCallback(async (itemInput: ChecklistItemInput) => {
|
||||
const res = await updateChecklistItemTitleAndDescription(serverUrl, runId, item.id, checklistNumber, itemNumber, itemInput);
|
||||
if (res.error) {
|
||||
showPlaybookErrorSnackbar();
|
||||
}
|
||||
}, [checklistNumber, item.id, itemNumber, runId, serverUrl]);
|
||||
|
||||
const openEditItemModal = useCallback(() => {
|
||||
const itemDescription = ('description' in item && item.description) || undefined;
|
||||
goToEditChecklistItem(intl, theme, runName, item.title, itemDescription, handleEditItem);
|
||||
}, [intl, theme, runName, item, handleEditItem]);
|
||||
|
||||
const renderTaskDetails = () => (
|
||||
<View style={styles.taskDetailsContainer}>
|
||||
<OptionItem
|
||||
|
|
@ -431,14 +457,28 @@ const ChecklistItemBottomSheet = ({
|
|||
checked={isChecked}
|
||||
onPress={handleCheck}
|
||||
/>
|
||||
<View style={styles.flex}>
|
||||
<Text style={styles.taskTitle}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{Boolean(item.description) && (
|
||||
<Text style={styles.taskDescription}>
|
||||
{item.description}
|
||||
<View style={styles.titleContainer}>
|
||||
<View style={styles.flex}>
|
||||
<Text style={styles.taskTitle}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{(item.description) && (
|
||||
<Text style={styles.taskDescription}>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{!isDisabled && (
|
||||
<Pressable
|
||||
onPress={openEditItemModal}
|
||||
style={({pressed}) => pressed && styles.editIconPressed}
|
||||
testID='checklist_item.edit_button'
|
||||
>
|
||||
<CompassIcon
|
||||
name='pencil-outline'
|
||||
style={styles.editIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,379 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, fireEvent} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {Preferences, Screens} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
import {getLastCallForButton} from '@test/mock_helpers';
|
||||
|
||||
import EditChecklistItemBottomSheet from './edit_checklist_item_bottom_sheet';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
buildNavigationButton: jest.fn(),
|
||||
popTopScreen: jest.fn(),
|
||||
setButtons: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@hooks/navigation_button_pressed', () => jest.fn());
|
||||
jest.mock('@hooks/android_back_handler', () => jest.fn());
|
||||
jest.mock('@managers/security_manager', () => ({
|
||||
getShieldScreenId: jest.fn((id) => `shield-${id}`),
|
||||
}));
|
||||
|
||||
describe('EditChecklistItemBottomSheet', () => {
|
||||
const componentId = Screens.PLAYBOOK_EDIT_CHECKLIST_ITEM;
|
||||
const mockOnSave = jest.fn();
|
||||
|
||||
const mockRightButton = {
|
||||
id: 'save-checklist-item',
|
||||
enabled: false,
|
||||
color: Preferences.THEMES.denim.sidebarHeaderTextColor,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(buildNavigationButton).mockReturnValue(mockRightButton as any);
|
||||
});
|
||||
|
||||
function getBaseProps() {
|
||||
return {
|
||||
componentId,
|
||||
currentTitle: 'Current Task',
|
||||
currentDescription: 'Current description',
|
||||
onSave: mockOnSave,
|
||||
};
|
||||
}
|
||||
|
||||
it('should render correctly with default props', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const titleInput = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
expect(titleInput).toBeTruthy();
|
||||
expect(titleInput.props.value).toBe('Current Task');
|
||||
expect(titleInput.props.autoFocus).toBe(true);
|
||||
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
expect(descriptionInput).toBeTruthy();
|
||||
expect(descriptionInput.props.value).toBe('Current description');
|
||||
|
||||
// Check labels are rendered
|
||||
const titleLabel = getByText('Task name');
|
||||
expect(titleLabel).toBeTruthy();
|
||||
const descriptionLabel = getByText('Description');
|
||||
expect(descriptionLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render correctly without description', () => {
|
||||
const props = {
|
||||
...getBaseProps(),
|
||||
currentDescription: undefined,
|
||||
};
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
expect(descriptionInput.props.value).toBe('');
|
||||
});
|
||||
|
||||
it('should set up navigation buttons on mount', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(buildNavigationButton).toHaveBeenCalledWith(
|
||||
'save-checklist-item',
|
||||
'playbooks.checklist_item.edit.button',
|
||||
undefined,
|
||||
'Save',
|
||||
);
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [mockRightButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should register navigation button press handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(useNavButtonPressed).toHaveBeenCalledWith(
|
||||
'save-checklist-item',
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
[expect.any(Function)],
|
||||
);
|
||||
});
|
||||
|
||||
it('should register Android back handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(useAndroidHardwareBackHandler).toHaveBeenCalledWith(
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update title when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
const newTitle = 'New Task Title';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
expect(input.props.value).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should update description when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
const newDescription = 'New description';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(descriptionInput, newDescription);
|
||||
});
|
||||
|
||||
expect(descriptionInput.props.value).toBe(newDescription);
|
||||
});
|
||||
|
||||
it('should enable save button when title is changed', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
|
||||
// Initially disabled (no changes)
|
||||
expect(mockRightButton.enabled).toBe(false);
|
||||
|
||||
// Update with different title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Button should be enabled now
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable save button when description is changed', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
|
||||
// Initially disabled (no changes)
|
||||
expect(mockRightButton.enabled).toBe(false);
|
||||
|
||||
// Update with different description
|
||||
act(() => {
|
||||
fireEvent.changeText(descriptionInput, 'Different description');
|
||||
});
|
||||
|
||||
// Button should be enabled now
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is empty', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
|
||||
// Clear title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, '');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is only whitespace', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
|
||||
// Set title to whitespace only
|
||||
act(() => {
|
||||
fireEvent.changeText(input, ' ');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onSave and close when save button is pressed with valid changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
const newTitle = 'Updated Task';
|
||||
|
||||
// Set new title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'save-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith({title: newTitle, description: 'Current description'});
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should call onSave with updated description', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const titleInput = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
const newTitle = 'Updated Task';
|
||||
const newDescription = 'Updated description';
|
||||
|
||||
// Set new title and description
|
||||
act(() => {
|
||||
fireEvent.changeText(titleInput, newTitle);
|
||||
fireEvent.changeText(descriptionInput, newDescription);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'save-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith({title: newTitle, description: newDescription});
|
||||
});
|
||||
|
||||
it('should trim title and description when saving', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const titleInput = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
const taskTitle = ' Task with spaces ';
|
||||
const taskDescription = ' Description with spaces ';
|
||||
|
||||
// Set title and description with spaces
|
||||
act(() => {
|
||||
fireEvent.changeText(titleInput, taskTitle);
|
||||
fireEvent.changeText(descriptionInput, taskDescription);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'save-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith({title: 'Task with spaces', description: 'Description with spaces'});
|
||||
expect(mockOnSave).not.toHaveBeenCalledWith(taskTitle, taskDescription);
|
||||
});
|
||||
|
||||
it('should not call onSave when save button is pressed with empty title', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
|
||||
// Clear title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, '');
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'save-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call onSave when save button is pressed with whitespace-only title', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
|
||||
// Set title to whitespace only
|
||||
act(() => {
|
||||
fireEvent.changeText(input, ' ');
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'save-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not enable save button when values are unchanged', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<EditChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const titleInput = getByTestId('playbooks.checklist_item.edit.title_input');
|
||||
const descriptionInput = getByTestId('playbooks.checklist_item.edit.description_input');
|
||||
|
||||
// Set values back to original
|
||||
act(() => {
|
||||
fireEvent.changeText(titleInput, 'Current Task');
|
||||
fireEvent.changeText(descriptionInput, 'Current description');
|
||||
});
|
||||
|
||||
// Button should remain disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
// 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;
|
||||
currentDescription?: string;
|
||||
onSave: (item: ChecklistItemInput) => void;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ID = 'save-checklist-item';
|
||||
|
||||
const close = (componentId: AvailableScreens): void => {
|
||||
Keyboard.dismiss();
|
||||
popTopScreen(componentId);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingVertical: 32,
|
||||
paddingHorizontal: 20,
|
||||
gap: 16,
|
||||
},
|
||||
});
|
||||
|
||||
const EditChecklistItemBottomSheet = ({
|
||||
componentId,
|
||||
currentTitle,
|
||||
currentDescription = '',
|
||||
onSave,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState<string>(currentTitle);
|
||||
const [description, setDescription] = useState<string>(currentDescription);
|
||||
const [canSave, setCanSave] = useState(false);
|
||||
|
||||
const rightButton = React.useMemo(() => {
|
||||
const base = buildNavigationButton(
|
||||
SAVE_BUTTON_ID,
|
||||
'playbooks.checklist_item.edit.button',
|
||||
undefined,
|
||||
formatMessage({id: 'playbooks.checklist_item.edit.button', defaultMessage: 'Save'}),
|
||||
);
|
||||
base.enabled = canSave;
|
||||
base.color = theme.sidebarHeaderTextColor;
|
||||
return base;
|
||||
}, [formatMessage, canSave, theme.sidebarHeaderTextColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [rightButton],
|
||||
});
|
||||
}, [rightButton, componentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const titleChanged = title.trim() !== currentTitle.trim();
|
||||
const descriptionChanged = description.trim() !== currentDescription.trim();
|
||||
setCanSave(title.trim().length > 0 && (titleChanged || descriptionChanged));
|
||||
}, [title, description, currentTitle, currentDescription]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const saveTitle = title.trim();
|
||||
if (saveTitle.length > 0) {
|
||||
onSave({
|
||||
title: saveTitle,
|
||||
description: description.trim(),
|
||||
});
|
||||
close(componentId);
|
||||
}
|
||||
}, [title, description, componentId, onSave]);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]);
|
||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
||||
const titleLabel = formatMessage({id: 'playbooks.checklist_item.edit.title_label', defaultMessage: 'Task name'});
|
||||
const descriptionLabel = formatMessage({id: 'playbooks.checklist_item.edit.description_label', defaultMessage: 'Description'});
|
||||
|
||||
return (
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.container}
|
||||
>
|
||||
<FloatingTextInput
|
||||
label={titleLabel}
|
||||
onChangeText={setTitle}
|
||||
testID='playbooks.checklist_item.edit.title_input'
|
||||
value={title}
|
||||
theme={theme}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<FloatingTextInput
|
||||
label={descriptionLabel}
|
||||
onChangeText={setDescription}
|
||||
testID='playbooks.checklist_item.edit.description_input'
|
||||
value={description}
|
||||
theme={theme}
|
||||
multiline={true}
|
||||
multilineInputHeight={100}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditChecklistItemBottomSheet;
|
||||
|
||||
5
app/products/playbooks/types/api.d.ts
vendored
5
app/products/playbooks/types/api.d.ts
vendored
|
|
@ -38,6 +38,11 @@ type PlaybookChecklistItem = {
|
|||
update_at: number;
|
||||
}
|
||||
|
||||
type ChecklistItemInput = {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type PlaybookChecklist = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -1037,6 +1037,7 @@
|
|||
"playbook.last_updated": "Last update {date}",
|
||||
"playbook.participants": "Participants",
|
||||
"playbooks.checklist_item.add.button": "New",
|
||||
"playbooks.checklist_item.add.description_label": "Description",
|
||||
"playbooks.checklist_item.add.label": "Task name",
|
||||
"playbooks.checklist_item.add.save": "Add",
|
||||
"playbooks.checklist_item.add.title": "New Task",
|
||||
|
|
@ -1050,6 +1051,10 @@
|
|||
"playbooks.checklist_item.delete_task_confirmation": "Are you sure you want to delete this task? This action cannot be undone.",
|
||||
"playbooks.checklist_item.delete_task_title": "Delete task",
|
||||
"playbooks.checklist_item.due_date": "Due date",
|
||||
"playbooks.checklist_item.edit.button": "Save",
|
||||
"playbooks.checklist_item.edit.description_label": "Description",
|
||||
"playbooks.checklist_item.edit.title": "Edit Task",
|
||||
"playbooks.checklist_item.edit.title_label": "Task name",
|
||||
"playbooks.checklist_item.none": "None",
|
||||
"playbooks.checklist_item.rerun_command": "Rerun command",
|
||||
"playbooks.checklist_item.run_command": "Run command",
|
||||
|
|
|
|||
Loading…
Reference in a new issue