Add set due date functionality to playbooks (#9091)
* Add set due date functionality to playbooks * Fix i18n * Fix test * Add tests * Address feedback * Remove unneeded line break * Fix test
This commit is contained in:
parent
4e95364344
commit
0df334dd2f
27 changed files with 1108 additions and 51 deletions
|
|
@ -10,21 +10,21 @@ import {of as of$} from 'rxjs';
|
|||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryDisplayNamePreferences} from '@queries/servers/preference';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime, getUtcOffsetForTimeZone} from '@utils/helpers';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = {
|
||||
timezone: string | null;
|
||||
timezone: string;
|
||||
isMilitaryTime: boolean;
|
||||
theme: Theme;
|
||||
handleChange: (currentDate: Moment) => void;
|
||||
showInitially?: AndroidMode;
|
||||
initialDate?: Moment;
|
||||
minuteInterval?: 5 | 30;
|
||||
}
|
||||
|
||||
type AndroidMode = 'date' | 'time';
|
||||
|
|
@ -44,11 +44,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const DateTimeSelector = ({timezone, handleChange, isMilitaryTime, theme, showInitially, initialDate}: Props) => {
|
||||
const DateTimeSelector = ({
|
||||
timezone,
|
||||
handleChange,
|
||||
isMilitaryTime,
|
||||
theme,
|
||||
showInitially,
|
||||
initialDate,
|
||||
minuteInterval = 30,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||
const timezoneOffSetInMinutes = timezone ? getUtcOffsetForTimeZone(timezone) : undefined;
|
||||
const minimumDate = getRoundedTime(currentTime);
|
||||
const minimumDate = getRoundedTime(currentTime, minuteInterval);
|
||||
|
||||
const defaultDate = initialDate && initialDate.isAfter(minimumDate) ? initialDate : minimumDate;
|
||||
const [date, setDate] = useState<Moment>(defaultDate);
|
||||
|
|
@ -108,8 +115,8 @@ const DateTimeSelector = ({timezone, handleChange, isMilitaryTime, theme, showIn
|
|||
onChange={onChange}
|
||||
textColor={theme.centerChannelColor}
|
||||
minimumDate={minimumDate.toDate()}
|
||||
minuteInterval={CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES}
|
||||
timeZoneOffsetInMinutes={timezoneOffSetInMinutes}
|
||||
minuteInterval={minuteInterval}
|
||||
timeZoneName={timezone}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {updateChecklistItem, setChecklistItemCommand, setAssignee} from './checklist';
|
||||
import {updateChecklistItem, setChecklistItemCommand, setAssignee, setDueDate} from './checklist';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
|
|
@ -180,3 +180,87 @@ describe('setAssignee', () => {
|
|||
expect(updated!.assigneeId).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDueDate', () => {
|
||||
it('should handle not found database', async () => {
|
||||
const {error} = await setDueDate('foo', 'itemid', 1234567890);
|
||||
expect(error).toBeTruthy();
|
||||
expect((error as Error).message).toContain('foo database not found');
|
||||
});
|
||||
|
||||
it('should handle item not found', async () => {
|
||||
const {error} = await setDueDate(serverUrl, 'nonexistent', 1234567890);
|
||||
expect(error).toBe('Item not found: nonexistent');
|
||||
});
|
||||
|
||||
it('should handle database write errors', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
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 setDueDate(serverUrl, item.id, 1234567890);
|
||||
expect(error).toBeTruthy();
|
||||
|
||||
operator.database.write = originalWrite;
|
||||
});
|
||||
|
||||
it('should set valid due date successfully', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const testDate = 1640995200000; // January 1, 2022 00:00:00 UTC
|
||||
const {data, error} = await setDueDate(serverUrl, item.id, testDate);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.dueDate).toBe(testDate);
|
||||
});
|
||||
|
||||
it('should set due date to 0 when undefined is passed', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const {data, error} = await setDueDate(serverUrl, item.id, undefined);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.dueDate).toBe(0);
|
||||
});
|
||||
|
||||
it('should set due date to 0 when no date parameter is passed', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const {data, error} = await setDueDate(serverUrl, item.id);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.dueDate).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle setting due date to 0 explicitly', async () => {
|
||||
const checklistId = 'checklistid';
|
||||
const item = TestHelper.createPlaybookItem(checklistId, 0);
|
||||
await operator.handlePlaybookChecklistItem({items: [{...item, checklist_id: checklistId}], prepareRecordsOnly: false});
|
||||
|
||||
const {data, error} = await setDueDate(serverUrl, item.id, 0);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
|
||||
const updated = await getPlaybookChecklistItemById(operator.database, item.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.dueDate).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,3 +67,24 @@ export async function setAssignee(serverUrl: string, itemId: string, assigneeId:
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function setDueDate(serverUrl: string, itemId: string, date?: number) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const item = await getPlaybookChecklistItemById(database, itemId);
|
||||
if (!item) {
|
||||
return {error: `Item not found: ${itemId}`};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
item.update((i) => {
|
||||
i.dueDate = date ?? 0;
|
||||
});
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logError('failed to set due date', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
setChecklistItemCommand as localSetChecklistItemCommand,
|
||||
updateChecklistItem as localUpdateChecklistItem,
|
||||
setAssignee as localSetAssignee,
|
||||
setDueDate as localSetDueDate,
|
||||
} from '@playbooks/actions/local/checklist';
|
||||
|
||||
import {
|
||||
|
|
@ -17,6 +18,7 @@ import {
|
|||
restoreChecklistItem,
|
||||
setChecklistItemCommand,
|
||||
setAssignee,
|
||||
setDueDate,
|
||||
} from './checklist';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
|
@ -34,6 +36,7 @@ const mockClient = {
|
|||
restoreChecklistItem: jest.fn(),
|
||||
setAssignee: jest.fn(),
|
||||
setChecklistItemCommand: jest.fn(),
|
||||
setDueDate: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@playbooks/actions/local/checklist');
|
||||
|
|
@ -265,4 +268,58 @@ describe('checklist', () => {
|
|||
expect(localSetChecklistItemCommand).toHaveBeenCalledWith(serverUrl, itemId, command);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDueDate', () => {
|
||||
it('should handle client error', async () => {
|
||||
jest.spyOn(NetworkManager, 'getClient').mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setDueDate(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 1234567890);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(localSetDueDate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle API exception', async () => {
|
||||
mockClient.setDueDate.mockImplementationOnce(throwFunc);
|
||||
|
||||
const result = await setDueDate(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 1234567890);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(mockClient.setDueDate).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, 1234567890);
|
||||
expect(localSetDueDate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set due date successfully with valid date', async () => {
|
||||
mockClient.setDueDate.mockResolvedValueOnce({});
|
||||
|
||||
const result = await setDueDate(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 1234567890);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setDueDate).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, 1234567890);
|
||||
expect(localSetDueDate).toHaveBeenCalledWith(serverUrl, itemId, 1234567890);
|
||||
});
|
||||
|
||||
it('should set due date successfully with undefined date', async () => {
|
||||
mockClient.setDueDate.mockResolvedValueOnce({});
|
||||
|
||||
const result = await setDueDate(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setDueDate).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, undefined);
|
||||
expect(localSetDueDate).toHaveBeenCalledWith(serverUrl, itemId, undefined);
|
||||
});
|
||||
|
||||
it('should set due date successfully with zero date', async () => {
|
||||
mockClient.setDueDate.mockResolvedValueOnce({});
|
||||
|
||||
const result = await setDueDate(serverUrl, playbookRunId, itemId, checklistNumber, itemNumber, 0);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setDueDate).toHaveBeenCalledWith(playbookRunId, checklistNumber, itemNumber, 0);
|
||||
expect(localSetDueDate).toHaveBeenCalledWith(serverUrl, itemId, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
setChecklistItemCommand as localSetChecklistItemCommand,
|
||||
updateChecklistItem as localUpdateChecklistItem,
|
||||
setAssignee as localSetAssignee,
|
||||
setDueDate as localSetDueDate,
|
||||
} from '@playbooks/actions/local/checklist';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
|
@ -134,3 +135,24 @@ export const setAssignee = async (
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const setDueDate = async (
|
||||
serverUrl: string,
|
||||
playbookRunId: string,
|
||||
itemId: string,
|
||||
checklistNumber: number,
|
||||
itemNumber: number,
|
||||
date?: number,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.setDueDate(playbookRunId, checklistNumber, itemNumber, date);
|
||||
|
||||
await localSetDueDate(serverUrl, itemId, date);
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
logDebug('error on setDueDate', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -279,6 +279,33 @@ describe('setChecklistItemCommand', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setDueDate', () => {
|
||||
test('should set due date successfully with valid date', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const date = 1640995200000; // January 1, 2022 00:00:00 UTC
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/duedate`;
|
||||
const expectedOptions = {method: 'put', body: {due_date: date}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.setDueDate(playbookRunId, checklistNum, itemNum, date);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
test('should handle error when setting due date', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const itemNum = 2;
|
||||
const date = 1640995200000;
|
||||
|
||||
jest.mocked(client.doFetch).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(client.setDueDate(playbookRunId, checklistNum, itemNum, date)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOwner', () => {
|
||||
test('should set owner successfully', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export interface ClientPlaybooksMix {
|
|||
skipChecklistItem: (playbookRunID: string, checklistNum: number, itemNum: number) => Promise<void>;
|
||||
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>;
|
||||
|
||||
// skipChecklist: (playbookRunID: string, checklistNum: number) => Promise<void>;
|
||||
|
||||
|
|
@ -120,6 +121,13 @@ const ClientPlaybooks = <TBase extends Constructor<ClientBase>>(superclass: TBas
|
|||
);
|
||||
};
|
||||
|
||||
setDueDate = async (playbookRunId: string, checklistNum: number, itemNum: number, date?: number) => {
|
||||
await this.doFetch(
|
||||
`${this.getPlaybookRunRoute(playbookRunId)}/checklists/${checklistNum}/item/${itemNum}/duedate`,
|
||||
{method: 'put', body: {due_date: date}},
|
||||
);
|
||||
};
|
||||
|
||||
// Slash Commands
|
||||
runChecklistItemSlashCommand = async (playbookRunId: string, checklistNumber: number, itemNumber: number) => {
|
||||
const data = await this.doFetch(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ export const PLAYBOOKS_RUNS = 'PlaybookRuns';
|
|||
export const PLAYBOOK_RUN = 'PlaybookRun';
|
||||
export const PLAYBOOK_EDIT_COMMAND = 'PlaybookEditCommand';
|
||||
export const PLAYBOOK_SELECT_USER = 'PlaybookSelectUser';
|
||||
export const PLAYBOOKS_SELECT_DATE = 'PlaybooksSelectDate';
|
||||
|
||||
export default {
|
||||
PLAYBOOKS_RUNS,
|
||||
PLAYBOOK_RUN,
|
||||
PLAYBOOK_EDIT_COMMAND,
|
||||
PLAYBOOK_SELECT_USER,
|
||||
PLAYBOOKS_SELECT_DATE,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {render} from '@test/intl-test-helper';
|
|||
import EditCommand from './edit_command';
|
||||
import PlaybookRun from './playbook_run';
|
||||
import PlaybookRuns from './playbooks_runs';
|
||||
import SelectDate from './select_date';
|
||||
import SelectUser from './select_user';
|
||||
|
||||
import {loadPlaybooksScreen} from '.';
|
||||
|
|
@ -45,6 +46,12 @@ jest.mock('@playbooks/screens/select_user', () => ({
|
|||
}));
|
||||
jest.mocked(SelectUser).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOK_SELECT_USER}</Text>);
|
||||
|
||||
jest.mock('@playbooks/screens/select_date', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SelectDate).mockImplementation((props) => <Text {...props}>{Screens.PLAYBOOKS_SELECT_DATE}</Text>);
|
||||
|
||||
describe('Screen Registration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export function loadPlaybooksScreen(screenName: string | number) {
|
|||
return withServerDatabase(require('@playbooks/screens/edit_command').default);
|
||||
case Screens.PLAYBOOK_SELECT_USER:
|
||||
return withServerDatabase(require('@playbooks/screens/select_user').default);
|
||||
case Screens.PLAYBOOKS_SELECT_DATE:
|
||||
return withServerDatabase(require('@playbooks/screens/select_date').default);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {goToScreen} from '@screens/navigation';
|
|||
import TestHelper from '@test/test_helper';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToSelectUser} from './navigation';
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToSelectUser, goToSelectDate} from './navigation';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
goToScreen: jest.fn(),
|
||||
|
|
@ -88,4 +88,47 @@ describe('Playbooks Navigation', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToSelectDate', () => {
|
||||
it('should navigate to select date screen with selected date', async () => {
|
||||
const onSave = jest.fn();
|
||||
const selectedDate = 1640995200000; // January 1, 2022
|
||||
|
||||
await goToSelectDate(mockIntl, onSave, selectedDate);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.select_date.title',
|
||||
defaultMessage: 'Due date',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOKS_SELECT_DATE,
|
||||
'Due date',
|
||||
{
|
||||
onSave,
|
||||
selectedDate,
|
||||
},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('should navigate to select date screen without selected date', async () => {
|
||||
const onSave = jest.fn();
|
||||
|
||||
await goToSelectDate(mockIntl, onSave, undefined);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.select_date.title',
|
||||
defaultMessage: 'Due date',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOKS_SELECT_DATE,
|
||||
'Due date',
|
||||
{
|
||||
onSave,
|
||||
selectedDate: undefined,
|
||||
},
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,3 +39,15 @@ export async function goToSelectUser(
|
|||
handleRemove,
|
||||
}, {});
|
||||
}
|
||||
|
||||
export async function goToSelectDate(
|
||||
intl: IntlShape,
|
||||
onSave: (date: number | undefined) => void,
|
||||
selectedDate: number | undefined,
|
||||
) {
|
||||
const title = intl.formatMessage({id: 'playbooks.select_date.title', defaultMessage: 'Due date'});
|
||||
goToScreen(Screens.PLAYBOOKS_SELECT_DATE, title, {
|
||||
onSave,
|
||||
selectedDate,
|
||||
}, {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import {ScrollView} from 'react-native';
|
|||
import OptionBox from '@components/option_box';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {setAssignee, setChecklistItemCommand} from '@playbooks/actions/remote/checklist';
|
||||
import {goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {setAssignee, setChecklistItemCommand, setDueDate} from '@playbooks/actions/remote/checklist';
|
||||
import {goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
|
@ -42,6 +42,7 @@ jest.mock('@context/server', () => ({
|
|||
useServerUrl: jest.fn().mockReturnValue('server-url'),
|
||||
}));
|
||||
|
||||
jest.mock('@playbooks/screens/navigation');
|
||||
jest.mock('@utils/snack_bar');
|
||||
|
||||
describe('ChecklistItemBottomSheet', () => {
|
||||
|
|
@ -90,6 +91,7 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
onRunCommand: mockOnRunCommand,
|
||||
teammateNameDisplay: mockTeammateNameDisplay,
|
||||
isDisabled: false,
|
||||
currentUserTimezone: {useAutomaticTimezone: false, automaticTimezone: '', manualTimezone: 'America/New_York'},
|
||||
participantIds: ['user-1', 'user-2'],
|
||||
};
|
||||
}
|
||||
|
|
@ -289,7 +291,7 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const dueDateItem = getByTestId('checklist_item.due_date');
|
||||
expect(dueDateItem.props.info).toBe('Saturday, January 1 at 12:00 AM');
|
||||
expect(dueDateItem.props.info).toBe('Saturday, January 1 at 07:00 PM');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
|
@ -327,6 +329,21 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
expect(commandItem).toHaveProp('action', expect.any(Function));
|
||||
});
|
||||
|
||||
it('set date is disabled when isDisabled is true', () => {
|
||||
const props = getBaseProps();
|
||||
props.isDisabled = true;
|
||||
const {getByTestId, rerender} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
let dueDateItem = getByTestId('checklist_item.due_date');
|
||||
expect(dueDateItem).toHaveProp('type', 'none');
|
||||
expect(dueDateItem).toHaveProp('action', undefined);
|
||||
|
||||
props.isDisabled = false;
|
||||
rerender(<ChecklistItemBottomSheet {...props}/>);
|
||||
dueDateItem = getByTestId('checklist_item.due_date');
|
||||
expect(dueDateItem).toHaveProp('type', 'arrow');
|
||||
expect(dueDateItem).toHaveProp('action', expect.any(Function));
|
||||
});
|
||||
|
||||
it('displays correct command information', () => {
|
||||
const props = getBaseProps();
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
|
|
@ -408,6 +425,47 @@ describe('ChecklistItemBottomSheet', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('opens the set date modal when the due date is clicked', async () => {
|
||||
const props = getBaseProps();
|
||||
props.checklistNumber = 2;
|
||||
props.itemNumber = 4;
|
||||
props.item = TestHelper.fakePlaybookChecklistItemModel({
|
||||
...props.item,
|
||||
dueDate: 1640995200000, // 2022-01-01
|
||||
});
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
const dueDateItem = getByTestId('checklist_item.due_date');
|
||||
act(() => {
|
||||
dueDateItem.props.action();
|
||||
});
|
||||
expect(goToSelectDate).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.any(Function),
|
||||
1640995200000,
|
||||
);
|
||||
|
||||
const setDate = jest.mocked(goToSelectDate).mock.calls[0][1];
|
||||
await act(async () => {
|
||||
setDate(1672531200000);
|
||||
});
|
||||
expect(setDueDate).toHaveBeenCalledWith(
|
||||
'server-url',
|
||||
'run-1',
|
||||
'item-1',
|
||||
2,
|
||||
4,
|
||||
1672531200000,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not open the set date modal when the due date is clicked and isDisabled is true', async () => {
|
||||
const props = getBaseProps();
|
||||
props.isDisabled = true;
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
const dueDateItem = getByTestId('checklist_item.due_date');
|
||||
expect(dueDateItem.props.action).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles user profile modal opening correctly', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntl(<ChecklistItemBottomSheet {...props}/>);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, type ComponentProps} from 'react';
|
||||
import {defineMessages, useIntl, type IntlShape} from 'react-intl';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {View, Text} from 'react-native';
|
||||
|
||||
import MenuDivider from '@components/menu_divider';
|
||||
|
|
@ -10,13 +10,14 @@ 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} from '@playbooks/actions/remote/checklist';
|
||||
import {goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {setAssignee, setChecklistItemCommand, setDueDate} from '@playbooks/actions/remote/checklist';
|
||||
import {goToSelectDate, goToSelectUser} from '@playbooks/screens/navigation';
|
||||
import {getDueDateString} from '@playbooks/utils/time';
|
||||
import {dismissBottomSheet, goToScreen, openUserProfileModal} from '@screens/navigation';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import Checkbox from '../checkbox';
|
||||
|
||||
|
|
@ -64,10 +65,6 @@ const messages = defineMessages({
|
|||
id: 'playbooks.checklist_item.none',
|
||||
defaultMessage: 'None',
|
||||
},
|
||||
dateAtTime: {
|
||||
id: 'playbooks.checklist_item.date_at_time',
|
||||
defaultMessage: '{date} at {time}',
|
||||
},
|
||||
});
|
||||
|
||||
const ACTION_BUTTON_HEIGHT = 62;
|
||||
|
|
@ -127,22 +124,10 @@ type Props = {
|
|||
onRunCommand: () => void;
|
||||
teammateNameDisplay: string;
|
||||
isDisabled: boolean;
|
||||
currentUserTimezone: UserTimezone | null | undefined;
|
||||
participantIds: string[];
|
||||
};
|
||||
|
||||
function getDueDateInfo(intl: IntlShape, dueDate: number | undefined) {
|
||||
if (!dueDate) {
|
||||
return intl.formatMessage(messages.none);
|
||||
}
|
||||
const dateObject = new Date(dueDate);
|
||||
const dateString = dateObject.toLocaleDateString(intl.locale, {month: 'long', day: 'numeric', weekday: 'long'});
|
||||
if (Math.abs(dueDate - Date.now()) < toMilliseconds({days: 1})) {
|
||||
const timeString = dateObject.toLocaleTimeString(intl.locale, {hour: '2-digit', minute: '2-digit'});
|
||||
return intl.formatMessage(messages.dateAtTime, {date: dateString, time: timeString});
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const ChecklistItemBottomSheet = ({
|
||||
runId,
|
||||
checklistNumber,
|
||||
|
|
@ -155,6 +140,7 @@ const ChecklistItemBottomSheet = ({
|
|||
onRunCommand,
|
||||
teammateNameDisplay,
|
||||
isDisabled,
|
||||
currentUserTimezone,
|
||||
participantIds,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
|
@ -162,6 +148,8 @@ const ChecklistItemBottomSheet = ({
|
|||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const timezone = getTimezone(currentUserTimezone);
|
||||
|
||||
const dueDate = 'dueDate' in item ? item.dueDate : item.due_date;
|
||||
const isChecked = item.state === 'closed';
|
||||
const isSkipped = item.state === 'skipped';
|
||||
|
|
@ -248,6 +236,12 @@ const ChecklistItemBottomSheet = ({
|
|||
};
|
||||
}, [assignee, intl, onUserChipPress, teammateNameDisplay]);
|
||||
|
||||
const openEditDateModal = useCallback(async () => {
|
||||
goToSelectDate(intl, (date) => {
|
||||
setDueDate(serverUrl, runId, item.id, checklistNumber, itemNumber, date);
|
||||
}, dueDate);
|
||||
}, [intl, dueDate, serverUrl, runId, item.id, checklistNumber, itemNumber]);
|
||||
|
||||
const handleSelect = useCallback(async (selected: UserProfile) => {
|
||||
const res = await setAssignee(serverUrl, runId, item.id, checklistNumber, itemNumber, selected.id);
|
||||
if (res.error) {
|
||||
|
|
@ -283,11 +277,12 @@ const ChecklistItemBottomSheet = ({
|
|||
action={isDisabled ? undefined : openUserSelector}
|
||||
/>
|
||||
<OptionItem
|
||||
type='none'
|
||||
type={isDisabled ? 'none' : 'arrow'}
|
||||
icon='calendar-outline'
|
||||
label={intl.formatMessage(messages.dueDate)}
|
||||
info={getDueDateInfo(intl, dueDate)}
|
||||
info={getDueDateString(intl, dueDate, timezone)}
|
||||
testID='checklist_item.due_date'
|
||||
action={isDisabled ? undefined : openEditDateModal}
|
||||
/>
|
||||
<OptionItem
|
||||
type={isDisabled ? 'none' : 'arrow'}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPlaybookChecklistItemById} from '@playbooks/database/queries/item';
|
||||
import {act, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
|
|
@ -71,6 +72,8 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
|||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'currentUser'}], prepareRecordsOnly: false});
|
||||
await addUserToDatabase(TestHelper.fakeUser({id: 'currentUser', timezone: {useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}}));
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
@ -98,6 +101,7 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
|||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', item);
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
expect(bottomSheet).toHaveProp('currentUserTimezone', expect.objectContaining({useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}));
|
||||
expect(bottomSheet).toHaveProp('participantIds', ['user-1', 'user-2']);
|
||||
});
|
||||
|
||||
|
|
@ -187,6 +191,7 @@ describe('ChecklistItemBottomSheet Enhanced Component', () => {
|
|||
expect(bottomSheet).toBeTruthy();
|
||||
expect(bottomSheet).toHaveProp('item', apiItem);
|
||||
expect(bottomSheet).toHaveProp('assignee', expect.objectContaining({id: 'user-1'}));
|
||||
expect(bottomSheet).toHaveProp('currentUserTimezone', expect.objectContaining({useAutomaticTimezone: false, manualTimezone: 'America/New_York', automaticTimezone: 'America/New_York'}));
|
||||
expect(bottomSheet).toHaveProp('participantIds', []);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
|||
import {of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {observeParticipantsIdsFromPlaybookModel, observePlaybookRunById} from '@playbooks/database/queries/run';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {observeCurrentUser, observeUser} from '@queries/servers/user';
|
||||
|
||||
import ChecklistItemBottomSheet, {BOTTOM_SHEET_HEIGHT} from './checklist_item_bottom_sheet';
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ type OwnProps = {
|
|||
} & WithDatabaseArgs;
|
||||
|
||||
const enhanced = withObservables(['item', 'runId'], ({item, runId, database}: OwnProps) => {
|
||||
const currentUserTimezone = observeCurrentUser(database).pipe(switchMap((u) => of$(u?.timezone)));
|
||||
if ('observe' in item) {
|
||||
const observedItem = item.observe();
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ const enhanced = withObservables(['item', 'runId'], ({item, runId, database}: Ow
|
|||
return {
|
||||
item: observedItem,
|
||||
assignee,
|
||||
currentUserTimezone,
|
||||
participantIds: observePlaybookRunById(database, runId).pipe(
|
||||
switchMap((run) => observeParticipantsIdsFromPlaybookModel(run, true)),
|
||||
),
|
||||
|
|
@ -46,6 +48,7 @@ const enhanced = withObservables(['item', 'runId'], ({item, runId, database}: Ow
|
|||
return {
|
||||
item: of$(item),
|
||||
assignee,
|
||||
currentUserTimezone,
|
||||
participantIds: of$([]),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
157
app/products/playbooks/screens/select_date/index.test.tsx
Normal file
157
app/products/playbooks/screens/select_date/index.test.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {act, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SelectDate from './select_date';
|
||||
|
||||
import SelectDateEnhanced from './index';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('./select_date', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mocked(SelectDate).mockImplementation(
|
||||
(props) => React.createElement('SelectDate', {testID: 'select_date', ...props}),
|
||||
);
|
||||
|
||||
describe('SelectDate Enhanced Component', () => {
|
||||
const serverUrl = 'server-url';
|
||||
|
||||
let database: Database;
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof SelectDateEnhanced> {
|
||||
return {
|
||||
componentId: 'SelectDate' as AvailableScreens,
|
||||
selectedDate: undefined,
|
||||
onSave: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
async function addUserToDatabase(user: UserProfile) {
|
||||
await operator.handleUsers({
|
||||
prepareRecordsOnly: false,
|
||||
users: [user],
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = serverDatabaseAndOperator.database;
|
||||
operator = serverDatabaseAndOperator.operator;
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: 'currentUser'}], prepareRecordsOnly: false});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
describe('with database integration', () => {
|
||||
it('should render enhanced component with currentUserTimezone from database', async () => {
|
||||
const user = TestHelper.fakeUser({
|
||||
id: 'currentUser',
|
||||
timezone: {
|
||||
useAutomaticTimezone: false,
|
||||
manualTimezone: 'America/New_York',
|
||||
automaticTimezone: 'America/New_York',
|
||||
},
|
||||
});
|
||||
await addUserToDatabase(user);
|
||||
|
||||
const props = getBaseProps();
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectDateEnhanced {...props}/>, {database});
|
||||
|
||||
const selectDate = getByTestId('select_date');
|
||||
expect(selectDate).toBeTruthy();
|
||||
expect(selectDate).toHaveProp('currentUserTimezone', expect.objectContaining({
|
||||
useAutomaticTimezone: false,
|
||||
manualTimezone: 'America/New_York',
|
||||
automaticTimezone: 'America/New_York',
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle timezone changes through observable', async () => {
|
||||
const user = TestHelper.fakeUser({
|
||||
id: 'currentUser',
|
||||
timezone: {
|
||||
useAutomaticTimezone: false,
|
||||
manualTimezone: 'America/New_York',
|
||||
automaticTimezone: 'America/New_York',
|
||||
},
|
||||
});
|
||||
await addUserToDatabase(user);
|
||||
|
||||
const props = getBaseProps();
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectDateEnhanced {...props}/>, {database});
|
||||
|
||||
const selectDate = getByTestId('select_date');
|
||||
expect(selectDate).toHaveProp('currentUserTimezone', expect.objectContaining({
|
||||
manualTimezone: 'America/New_York',
|
||||
}));
|
||||
|
||||
// Update user timezone
|
||||
await act(async () => {
|
||||
database.write(async () => {
|
||||
const userRecord = await database.get('User').find('currentUser');
|
||||
userRecord.update((u) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(u as any).timezone = {
|
||||
useAutomaticTimezone: true,
|
||||
manualTimezone: '',
|
||||
automaticTimezone: 'Europe/London',
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(selectDate).toHaveProp('currentUserTimezone', expect.objectContaining({
|
||||
useAutomaticTimezone: true,
|
||||
manualTimezone: '',
|
||||
automaticTimezone: 'Europe/London',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing current user gracefully', async () => {
|
||||
const props = getBaseProps();
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectDateEnhanced {...props}/>, {database});
|
||||
|
||||
const selectDate = getByTestId('select_date');
|
||||
expect(selectDate).toBeTruthy();
|
||||
expect(selectDate.props.currentUserTimezone).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle user without timezone gracefully', async () => {
|
||||
const user = TestHelper.fakeUser({
|
||||
id: 'currentUser',
|
||||
timezone: undefined,
|
||||
});
|
||||
await addUserToDatabase(user);
|
||||
|
||||
const props = getBaseProps();
|
||||
|
||||
const {getByTestId} = renderWithEverything(<SelectDateEnhanced {...props}/>, {database});
|
||||
|
||||
const selectDate = getByTestId('select_date');
|
||||
expect(selectDate).toBeTruthy();
|
||||
expect(selectDate.props.currentUserTimezone).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
21
app/products/playbooks/screens/select_date/index.ts
Normal file
21
app/products/playbooks/screens/select_date/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import SelectDate from './select_date';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const currentUserTimezone = observeCurrentUser(database).pipe(switchMap((u) => of$(u?.timezone)));
|
||||
return {
|
||||
currentUserTimezone,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(SelectDate));
|
||||
254
app/products/playbooks/screens/select_date/select_date.test.tsx
Normal file
254
app/products/playbooks/screens/select_date/select_date.test.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment';
|
||||
import React, {type ComponentProps} from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import DateTimeSelector from '@components/data_time_selector';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {act, fireEvent, renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
|
||||
import SelectDate from './select_date';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@components/data_time_selector');
|
||||
jest.mocked(DateTimeSelector).mockImplementation(
|
||||
(props) => React.createElement('DateTimeSelector', {testID: 'date-time-selector', ...props}),
|
||||
);
|
||||
|
||||
jest.mock('@hooks/navigation_button_pressed');
|
||||
jest.mock('@hooks/android_back_handler');
|
||||
|
||||
describe('SelectDate', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function getBaseProps(): ComponentProps<typeof SelectDate> {
|
||||
const mockOnSave = jest.fn();
|
||||
|
||||
const mockComponentId: AvailableScreens = 'SelectDate';
|
||||
const mockCurrentUserTimezone: UserTimezone = {
|
||||
automaticTimezone: 'UTC',
|
||||
manualTimezone: '',
|
||||
useAutomaticTimezone: true,
|
||||
};
|
||||
return {
|
||||
componentId: mockComponentId,
|
||||
selectedDate: undefined,
|
||||
currentUserTimezone: mockCurrentUserTimezone,
|
||||
onSave: mockOnSave,
|
||||
};
|
||||
}
|
||||
|
||||
it('renders correctly with no selected date', () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2025-09-11T17:40:18.086Z'));
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
expect(dateTimeSelector).toBeTruthy();
|
||||
expect(moment('2025-09-11T17:40:18.086Z').isSame(dateTimeSelector.props.initialDate)).toBeTruthy();
|
||||
expect(dateTimeSelector).toHaveProp('timezone', 'UTC');
|
||||
expect(dateTimeSelector).toHaveProp('minuteInterval', 5);
|
||||
expect(dateTimeSelector).toHaveProp('handleChange', expect.any(Function));
|
||||
expect(getByText('Clear')).toBeTruthy();
|
||||
expect(getByText('None')).toBeTruthy();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders correctly with selected date', () => {
|
||||
const selectedDate = 1699705458086;
|
||||
const props = {
|
||||
...getBaseProps(),
|
||||
selectedDate,
|
||||
};
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
expect(dateTimeSelector).toBeTruthy();
|
||||
expect(moment(selectedDate).isSame(dateTimeSelector.props.initialDate)).toBeTruthy();
|
||||
expect(dateTimeSelector).toHaveProp('timezone', 'UTC');
|
||||
expect(dateTimeSelector).toHaveProp('minuteInterval', 5);
|
||||
expect(dateTimeSelector).toHaveProp('handleChange', expect.any(Function));
|
||||
expect(getByText('Clear')).toBeTruthy();
|
||||
expect(getByText('Saturday, November 11 at 12:24 PM')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sets up navigation buttons correctly', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
id: 'save-due-date',
|
||||
text: 'Save',
|
||||
enabled: false,
|
||||
showAsAction: 'always',
|
||||
color: '#ffffff',
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('sets up navigation button pressed handler', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Change the date to enable the save button
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
|
||||
await act(async () => {
|
||||
dateTimeSelector.props.handleChange(moment(1234567890));
|
||||
});
|
||||
|
||||
expect(useNavButtonPressed).toHaveBeenCalledWith(
|
||||
'save-due-date',
|
||||
props.componentId,
|
||||
expect.any(Function),
|
||||
[expect.any(Function)],
|
||||
);
|
||||
|
||||
const saveHandler = jest.mocked(useNavButtonPressed).mock.calls[0][2];
|
||||
saveHandler();
|
||||
|
||||
expect(props.onSave).toHaveBeenCalledWith(expect.any(Number));
|
||||
expect(popTopScreen).toHaveBeenCalledWith(props.componentId);
|
||||
});
|
||||
|
||||
it('sets up Android back handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
expect(useAndroidHardwareBackHandler).toHaveBeenCalledWith(
|
||||
props.componentId,
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
const closeHandler = jest.mocked(useAndroidHardwareBackHandler).mock.calls[0][1];
|
||||
closeHandler();
|
||||
|
||||
expect(popTopScreen).toHaveBeenCalledWith(props.componentId);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('canSave logic', () => {
|
||||
it('returns false when no date and no selectedDate', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
// Simulate no date selected
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
await act(async () => {
|
||||
dateTimeSelector.props.handleChange(undefined);
|
||||
});
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
enabled: false,
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true when date changes from undefined to defined', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
// Simulate date selection
|
||||
const newDate = moment();
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
await act(async () => {
|
||||
dateTimeSelector.props.handleChange(newDate);
|
||||
});
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
enabled: true,
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true when date changes from defined to undefined', async () => {
|
||||
const selectedDate = Date.now();
|
||||
const props = getBaseProps();
|
||||
props.selectedDate = selectedDate;
|
||||
const {getByTestId} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
// Simulate clearing date
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
await act(async () => {
|
||||
dateTimeSelector.props.handleChange(undefined);
|
||||
});
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
enabled: true,
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true when date is different from selectedDate', async () => {
|
||||
const selectedDate = Date.now();
|
||||
const props = getBaseProps();
|
||||
props.selectedDate = selectedDate;
|
||||
const {getByTestId} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
// Simulate different date selection
|
||||
const newDate = moment(selectedDate).add(1, 'day');
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
await act(async () => {
|
||||
dateTimeSelector.props.handleChange(newDate);
|
||||
});
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
enabled: true,
|
||||
})],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false when date is same as selectedDate', async () => {
|
||||
const selectedDate = Date.now();
|
||||
const props = getBaseProps();
|
||||
props.selectedDate = selectedDate;
|
||||
const {getByTestId} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
// Simulate same date selection
|
||||
const sameDate = moment(selectedDate);
|
||||
const dateTimeSelector = getByTestId('date-time-selector');
|
||||
await act(async () => {
|
||||
dateTimeSelector.props.handleChange(sameDate);
|
||||
});
|
||||
|
||||
expect(setButtons).toHaveBeenCalledWith(props.componentId, {
|
||||
rightButtons: [expect.objectContaining({
|
||||
enabled: false,
|
||||
})],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleClear', () => {
|
||||
it('clears the selected date when Clear button is pressed', () => {
|
||||
const selectedDate = Date.now();
|
||||
const props = getBaseProps();
|
||||
props.selectedDate = selectedDate;
|
||||
const {getByText} = renderWithIntlAndTheme(<SelectDate {...props}/>);
|
||||
|
||||
const clearButton = getByText('Clear');
|
||||
act(() => {
|
||||
fireEvent.press(clearButton);
|
||||
});
|
||||
|
||||
expect(getByText('None')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
137
app/products/playbooks/screens/select_date/select_date.tsx
Normal file
137
app/products/playbooks/screens/select_date/select_date.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {type Moment} from 'moment';
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, Text, View} from 'react-native';
|
||||
|
||||
import Button from '@components/button';
|
||||
import DateTimeSelector from '@components/data_time_selector';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {getDueDateString} from '@playbooks/utils/time';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
selectedDate?: number;
|
||||
currentUserTimezone: UserTimezone | null | undefined;
|
||||
onSave: (date: number | undefined) => void;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ID = 'save-due-date';
|
||||
|
||||
function close (componentId: AvailableScreens) {
|
||||
Keyboard.dismiss();
|
||||
popTopScreen(componentId);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
gap: 8,
|
||||
},
|
||||
dateText: {
|
||||
...typography('Body', 300, 'Regular'),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SelectDate({
|
||||
componentId,
|
||||
selectedDate,
|
||||
currentUserTimezone,
|
||||
onSave,
|
||||
}: Props) {
|
||||
const [date, setDate] = useState<Moment | undefined>(() => (selectedDate ? moment(selectedDate) : undefined));
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const initialDate = useMemo(() => moment(selectedDate), [selectedDate]);
|
||||
const canSave = useMemo(() => {
|
||||
if (!date && !selectedDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((date && !selectedDate) || (!date && selectedDate)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !date.isSame(selectedDate);
|
||||
}, [date, selectedDate]);
|
||||
|
||||
const rightButton = useMemo(() => {
|
||||
const base = buildNavigationButton(
|
||||
SAVE_BUTTON_ID,
|
||||
'playbooks.edit_due_date.save.button',
|
||||
undefined,
|
||||
intl.formatMessage({id: 'playbooks.edit_due_date.save.button', defaultMessage: 'Save'}),
|
||||
);
|
||||
base.enabled = canSave;
|
||||
base.showAsAction = 'always';
|
||||
base.color = theme.sidebarHeaderTextColor;
|
||||
return base;
|
||||
}, [intl, canSave, theme.sidebarHeaderTextColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [rightButton],
|
||||
});
|
||||
}, [componentId, rightButton]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (!canSave) {
|
||||
return;
|
||||
}
|
||||
onSave(date?.valueOf());
|
||||
close(componentId);
|
||||
}, [canSave, date, onSave, componentId]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setDate(undefined);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
useNavButtonPressed(SAVE_BUTTON_ID, componentId, handleSave, [handleSave]);
|
||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
||||
const timezone = getTimezone(currentUserTimezone);
|
||||
|
||||
const dateText = getDueDateString(intl, date?.valueOf(), timezone, true);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.dateText}>{dateText}</Text>
|
||||
<Button
|
||||
text={intl.formatMessage({id: 'playbooks.edit_due_date.clear.button', defaultMessage: 'Clear'})}
|
||||
onPress={handleClear}
|
||||
emphasis='link'
|
||||
theme={theme}
|
||||
size='lg'
|
||||
/>
|
||||
<DateTimeSelector
|
||||
handleChange={setDate}
|
||||
theme={theme}
|
||||
initialDate={initialDate}
|
||||
timezone={timezone}
|
||||
minuteInterval={5}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
98
app/products/playbooks/utils/time.test.ts
Normal file
98
app/products/playbooks/utils/time.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createIntl} from 'react-intl';
|
||||
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
|
||||
import {getDueDateString} from './time';
|
||||
|
||||
describe('getDueDateString', () => {
|
||||
const mockIntl = createIntl({
|
||||
locale: 'en-US',
|
||||
messages: {
|
||||
'playbooks.due_date.none': 'None',
|
||||
'playbooks.due_date.date_at_time': '{date} at {time}',
|
||||
},
|
||||
});
|
||||
|
||||
describe('when dueDate is undefined', () => {
|
||||
it('should return "None" message', () => {
|
||||
const result = getDueDateString(mockIntl, undefined, 'America/New_York');
|
||||
expect(result).toBe('None');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dueDate is 0', () => {
|
||||
it('should return "None" message', () => {
|
||||
const result = getDueDateString(mockIntl, 0, 'America/New_York');
|
||||
expect(result).toBe('None');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dueDate is valid and more than 1 day away', () => {
|
||||
it('should return date only without time', () => {
|
||||
const futureDate = Date.now() + toMilliseconds({days: 2});
|
||||
const result = getDueDateString(mockIntl, futureDate, 'America/New_York');
|
||||
|
||||
// Should not contain "at" since time is not shown
|
||||
expect(result).not.toContain(' at ');
|
||||
expect(result).toMatch(/^\w+, \w+ \d+$/); // Format: "Monday, January 15"
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dueDate is within 1 day', () => {
|
||||
it('should return date with time', () => {
|
||||
const nearDate = Date.now() + toMilliseconds({hours: 12});
|
||||
const result = getDueDateString(mockIntl, nearDate, 'America/New_York');
|
||||
|
||||
expect(result).toContain(' at ');
|
||||
expect(result).toMatch(/^\w+, \w+ \d+ at \d{2}:\d{2} [AP]M$/); // Format: "Monday, January 15 at 14:30"
|
||||
});
|
||||
|
||||
it('should use the provided timezone for time formatting', () => {
|
||||
const nearDate = Date.now() + toMilliseconds({hours: 12});
|
||||
const resultNY = getDueDateString(mockIntl, nearDate, 'America/New_York');
|
||||
const resultLA = getDueDateString(mockIntl, nearDate, 'America/Los_Angeles');
|
||||
|
||||
// Times should be different due to different timezones
|
||||
expect(resultNY).not.toBe(resultLA);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showAlwaysTime is true', () => {
|
||||
it('should always show time even for dates more than 1 day away', () => {
|
||||
const futureDate = Date.now() + toMilliseconds({days: 2});
|
||||
const result = getDueDateString(mockIntl, futureDate, 'America/New_York', true);
|
||||
|
||||
expect(result).toContain(' at ');
|
||||
expect(result).toMatch(/^\w+, \w+ \d+ at \d{2}:\d{2} [AP]M$/);
|
||||
});
|
||||
|
||||
it('should show time for dates within 1 day when showAlwaysTime is true', () => {
|
||||
const nearDate = Date.now() + toMilliseconds({hours: 12});
|
||||
const result = getDueDateString(mockIntl, nearDate, 'America/New_York', true);
|
||||
|
||||
expect(result).toContain(' at ');
|
||||
expect(result).toMatch(/^\w+, \w+ \d+ at \d{2}:\d{2} [AP]M$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle dates in the past', () => {
|
||||
const pastDate = Date.now() - toMilliseconds({hours: 12});
|
||||
const result = getDueDateString(mockIntl, pastDate, 'America/New_York');
|
||||
|
||||
// Should show time since it's within 1 day (in the past)
|
||||
expect(result).toContain(' at ');
|
||||
});
|
||||
|
||||
it('should handle different timezone formats', () => {
|
||||
const nearDate = Date.now() + toMilliseconds({hours: 12});
|
||||
const resultUTC = getDueDateString(mockIntl, nearDate, 'UTC');
|
||||
const resultEST = getDueDateString(mockIntl, nearDate, 'America/New_York');
|
||||
|
||||
expect(resultUTC).not.toBe(resultEST);
|
||||
});
|
||||
});
|
||||
});
|
||||
30
app/products/playbooks/utils/time.ts
Normal file
30
app/products/playbooks/utils/time.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {defineMessages, type IntlShape} from 'react-intl';
|
||||
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
|
||||
const messages = defineMessages({
|
||||
none: {
|
||||
id: 'playbooks.due_date.none',
|
||||
defaultMessage: 'None',
|
||||
},
|
||||
dateAtTime: {
|
||||
id: 'playbooks.due_date.date_at_time',
|
||||
defaultMessage: '{date} at {time}',
|
||||
},
|
||||
});
|
||||
|
||||
export function getDueDateString(intl: IntlShape, dueDate: number | undefined, timezone: string, showAlwaysTime: boolean = false) {
|
||||
if (!dueDate) {
|
||||
return intl.formatMessage(messages.none);
|
||||
}
|
||||
const dateObject = new Date(dueDate);
|
||||
const dateString = dateObject.toLocaleDateString(intl.locale, {month: 'long', day: 'numeric', weekday: 'long'});
|
||||
if (showAlwaysTime || Math.abs(dueDate - Date.now()) < toMilliseconds({days: 1})) {
|
||||
const timeString = dateObject.toLocaleTimeString(intl.locale, {timeZone: timezone, hour: '2-digit', minute: '2-digit'});
|
||||
return intl.formatMessage(messages.dateAtTime, {date: dateString, time: timeString});
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import {removeRecentCustomStatus, updateCustomStatus, unsetCustomStatus} from '@
|
|||
import CompassIcon from '@components/compass_icon';
|
||||
import TabletTitle from '@components/tablet_title';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {CustomStatusDurationEnum, SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES, CustomStatusDurationEnum, SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
|
|
@ -179,7 +179,7 @@ const CustomStatus = ({
|
|||
return getUserCustomStatus(currentUser);
|
||||
}, [currentUser]);
|
||||
|
||||
const initialStatus = useMemo(() => {
|
||||
const [initialStatus] = useState(() => {
|
||||
const userTimezone = getTimezone(currentUser?.timezone);
|
||||
|
||||
// May be a ref
|
||||
|
|
@ -187,7 +187,7 @@ const CustomStatus = ({
|
|||
|
||||
const currentTime = getCurrentMomentForTimezone(userTimezone ?? '');
|
||||
|
||||
let initialCustomExpiryTime = getRoundedTime(currentTime);
|
||||
let initialCustomExpiryTime = getRoundedTime(currentTime, CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES);
|
||||
const isCurrentCustomStatusSet = !isCustomStatusExpired && (storedStatus?.text || storedStatus?.emoji);
|
||||
if (isCurrentCustomStatusSet && storedStatus?.duration === 'date_and_time' && storedStatus?.expires_at) {
|
||||
initialCustomExpiryTime = moment(storedStatus?.expires_at);
|
||||
|
|
@ -199,7 +199,7 @@ const CustomStatus = ({
|
|||
expiresAt: initialCustomExpiryTime,
|
||||
text: isCurrentCustomStatusSet ? storedStatus?.text : '',
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
const [newStatus, dispatchStatus] = useReducer(reducer, initialStatus);
|
||||
|
||||
|
|
@ -353,7 +353,7 @@ const CustomStatus = ({
|
|||
}],
|
||||
},
|
||||
});
|
||||
}, [isBtnEnabled]);
|
||||
}, [componentId, intl, isBtnEnabled, theme.sidebarHeaderTextColor]);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import CompassIcon from '@components/compass_icon';
|
|||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import DateTimePicker from '@components/data_time_selector';
|
||||
import {CST, CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {CST, CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES, CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -136,6 +136,7 @@ const ClearAfterMenuItem = ({currentUser, duration, expiryTime = '', handleItemC
|
|||
handleChange={handleCustomExpiresAtChange}
|
||||
theme={theme}
|
||||
timezone={getTimezone(currentUser?.timezone)}
|
||||
minuteInterval={CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -207,12 +207,16 @@ describe('Helpers', () => {
|
|||
describe('getRoundedTime', () => {
|
||||
test('should round time to nearest interval', () => {
|
||||
const time = moment('2024-06-01T12:34:00Z');
|
||||
const result = getRoundedTime(time);
|
||||
expect(result.minute() % 30).toBe(0); // Assuming CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES is 15
|
||||
const result = getRoundedTime(time, 3);
|
||||
expect(result.minute() % 3).toBe(0);
|
||||
|
||||
const time2 = moment('2024-06-01T12:00:00Z');
|
||||
const result2 = getRoundedTime(time2);
|
||||
expect(result2.minute() % 30).toBe(0);
|
||||
const result2 = getRoundedTime(time2, 7);
|
||||
expect(result2.minute() % 7).toBe(0);
|
||||
|
||||
const time3 = moment('2024-06-01T12:34:00Z');
|
||||
const result3 = getRoundedTime(time3, 11);
|
||||
expect(result3.minute() % 11).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import RNUtils, {type SplitViewResult} from '@mattermost/rnutils';
|
|||
import moment, {type Moment} from 'moment-timezone';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
import {STATUS_BAR_HEIGHT} from '@constants/view';
|
||||
|
||||
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
||||
|
|
@ -143,8 +142,7 @@ export function toTitleCase(str: string) {
|
|||
return str.replace(/\w\S*/g, doTitleCase);
|
||||
}
|
||||
|
||||
export function getRoundedTime(value: Moment) {
|
||||
const roundedTo = CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES;
|
||||
export function getRoundedTime(value: Moment, roundedTo: number) {
|
||||
const start = moment(value);
|
||||
const diff = start.minute() % roundedTo;
|
||||
if (diff === 0) {
|
||||
|
|
|
|||
|
|
@ -993,17 +993,20 @@
|
|||
"playbooks.checklist_item.check": "Check",
|
||||
"playbooks.checklist_item.checked": "Checked",
|
||||
"playbooks.checklist_item.command": "Command",
|
||||
"playbooks.checklist_item.date_at_time": "{date} at {time}",
|
||||
"playbooks.checklist_item.due_date": "Due date",
|
||||
"playbooks.checklist_item.none": "None",
|
||||
"playbooks.checklist_item.rerun_command": "Rerun command",
|
||||
"playbooks.checklist_item.run_command": "Run command",
|
||||
"playbooks.checklist_item.skip": "Skip",
|
||||
"playbooks.checklist_item.skipped": "Skipped",
|
||||
"playbooks.due_date.date_at_time": "{date} at {time}",
|
||||
"playbooks.due_date.none": "None",
|
||||
"playbooks.edit_command.label": "Command",
|
||||
"playbooks.edit_command.placeholder": "Type a command here",
|
||||
"playbooks.edit_command.save.button": "Save",
|
||||
"playbooks.edit_command.title": "Slash command",
|
||||
"playbooks.edit_due_date.clear.button": "Clear",
|
||||
"playbooks.edit_due_date.save.button": "Save",
|
||||
"playbooks.fetch_error.description": "You don't have permission to view this run, or it may no longer exist.",
|
||||
"playbooks.fetch_error.OK": "Okay",
|
||||
"playbooks.fetch_error.title": "Unable to open Run",
|
||||
|
|
@ -1035,6 +1038,7 @@
|
|||
"playbooks.runs.in_progress.description": "When a run starts in this channel, you’ll see it here.",
|
||||
"playbooks.runs.in_progress.title": "No in progress runs",
|
||||
"playbooks.runs.show_more": "Show More",
|
||||
"playbooks.select_date.title": "Due date",
|
||||
"playbooks.select_user.no_assignee": "No Assignee",
|
||||
"playbooks.select_user.not_participants": "NOT PARTICIPATING",
|
||||
"playbooks.select_user.participants": "RUN PARTICIPANTS",
|
||||
|
|
|
|||
Loading…
Reference in a new issue