MM-61382 Date/Time Picker Input Fix and Refactor (#31330)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
6f5c92fc79
Коммит
77e50cf110
@@ -118,17 +118,19 @@ describe('MM-T4065 Setting manual status clear time less than 7 days away', () =
|
||||
|
||||
it('MM-T4065_7 should show selected time in the time input field', () => {
|
||||
// * Check that the timepicker menu is not present and click to open it
|
||||
cy.get('#custom_status_modal .dateTime__time-menu #expiryTimeMenu').should('not.exist');
|
||||
cy.get('#custom_status_modal .dateTime__time-menu').click();
|
||||
cy.get('#expiryTimeMenu').should('not.exist');
|
||||
|
||||
// # Click the time button
|
||||
cy.get('#custom_status_modal #time_button').click();
|
||||
|
||||
// * Check that the time picker menu is present
|
||||
cy.get('#custom_status_modal .dateTime__time-menu #expiryTimeMenu').should('exist');
|
||||
cy.get('#expiryTimeMenu').should('exist');
|
||||
|
||||
// # Choose the last item in the time picker menu
|
||||
cy.get('#custom_status_modal .dateTime__time-menu #expiryTimeMenu li').last().click();
|
||||
cy.get('#expiryTimeMenu li').last().click();
|
||||
|
||||
// * Check that the time input contains the correct time
|
||||
cy.get('.dateTime__time-menu .dateTime__input time').should('have.text', '11:30 PM');
|
||||
cy.get('#custom_status_modal #time_button time').should('have.text', '11:30 PM');
|
||||
});
|
||||
|
||||
it('MM-T4065_8 should set custom status when click on Set Status', () => {
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('MM-T4066 Setting manual status clear time more than 7 days away', () =
|
||||
cy.get('.date-picker__popper').find(`.rdp-month button[aria-label="${dateToBeSelected.format('Do MMMM (dddd)')}"]`).click();
|
||||
|
||||
// * Check that the date input should have the correct value
|
||||
cy.get('input#customStatus__calendar-input').should('have.value', dateToBeSelected.format('YYYY-MM-DD'));
|
||||
cy.get('#custom_status_modal [role="button"][aria-label*="Date"]').should('contain.text', dateToBeSelected.format('MMM DD'));
|
||||
});
|
||||
|
||||
it('MM-T4066_7 should set custom status when click on Set Status', () => {
|
||||
|
||||
@@ -20,8 +20,6 @@ describe('DND Status - Setting Your Own DND Status', () => {
|
||||
];
|
||||
|
||||
before(() => {
|
||||
cy.shouldHaveFeatureFlag('TimedDND', true);
|
||||
|
||||
// # Login as test user and visit channel
|
||||
cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
|
||||
cy.visit(`/${team.name}/channels/${channel.name}`);
|
||||
@@ -77,13 +75,13 @@ describe('DND Status - Setting Your Own DND Status', () => {
|
||||
cy.get(`.SubMenuItemContainer li#${dndTimes[4]}`).click();
|
||||
|
||||
// # Click on DayPicker input field
|
||||
cy.get('.DayPickerInput input').click();
|
||||
cy.get('.dateTime__date .date-time-input').click();
|
||||
|
||||
// * Verify that DayPicker overlay is visible
|
||||
cy.get('.DayPickerInput-Overlay').should('be.visible');
|
||||
cy.get('.date-picker__popper').should('be.visible');
|
||||
|
||||
// # Click on tomorrow's day
|
||||
cy.get('.DayPickerInput-Overlay').find('.DayPicker-Day--today').next('.DayPicker-Day').click();
|
||||
cy.get('.date-picker__popper').find('.rdp-day_today').next('.rdp-day').click();
|
||||
|
||||
// # Click 'Disable Notification' button
|
||||
cy.get('.DndModal__footer span').should('have.text', 'Disable Notifications').click();
|
||||
@@ -105,13 +103,13 @@ describe('DND Status - Setting Your Own DND Status', () => {
|
||||
cy.get(`.SubMenuItemContainer li#${dndTimes[4]}`).click();
|
||||
|
||||
// # Click on time picker input field
|
||||
cy.get('.MenuWrapper .DndModal__input').click();
|
||||
cy.get('.dateTime__time .date-time-input').click();
|
||||
|
||||
// * Verify that time picker menu is visible
|
||||
cy.get('ul.Menu__content.dropdown-menu').should('be.visible');
|
||||
cy.get('#expiryTimeMenu').should('be.visible');
|
||||
|
||||
// # Click on last time available in list
|
||||
cy.get('ul.Menu__content.dropdown-menu').last('.MenuItem').click();
|
||||
cy.get('#expiryTimeMenu li').last().click();
|
||||
|
||||
// # Click 'Disable Notification' button
|
||||
cy.get('.DndModal__footer span').should('have.text', 'Disable Notifications').click();
|
||||
@@ -133,22 +131,22 @@ describe('DND Status - Setting Your Own DND Status', () => {
|
||||
cy.get(`.SubMenuItemContainer li#${dndTimes[4]}`).click();
|
||||
|
||||
// # Click on DayPicker input field
|
||||
cy.get('.DayPickerInput input').click();
|
||||
cy.get('.dateTime__date .date-time-input').click();
|
||||
|
||||
// * Verify that DayPicker overlay is visible
|
||||
cy.get('.DayPickerInput-Overlay').should('be.visible');
|
||||
cy.get('.date-picker__popper').should('be.visible');
|
||||
|
||||
// # Click on tomorrow's day
|
||||
cy.get('.DayPickerInput-Overlay').find('.DayPicker-Day--today').next('.DayPicker-Day').click();
|
||||
cy.get('.date-picker__popper').find('.rdp-day_today').next('.rdp-day').click();
|
||||
|
||||
// # Click on time picker input field
|
||||
cy.get('.MenuWrapper .DndModal__input').click();
|
||||
cy.get('.dateTime__time .date-time-input').click();
|
||||
|
||||
// * Verify that time picker menu is visible
|
||||
cy.get('ul.Menu__content.dropdown-menu').should('be.visible');
|
||||
cy.get('#expiryTimeMenu').should('be.visible');
|
||||
|
||||
// # Click on last time available in list
|
||||
cy.get('ul.Menu__content.dropdown-menu').last('.MenuItem').click();
|
||||
cy.get('#expiryTimeMenu li').last().click();
|
||||
|
||||
// # Click 'Disable Notification' button
|
||||
cy.get('.DndModal__footer span').should('have.text', 'Disable Notifications').click();
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class ScheduleMessageModal {
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.dateButton = container.locator('#customStatus__calendar-input');
|
||||
this.dateButton = container.getByRole('button', {name: /Date/});
|
||||
this.timeButton = container.getByTestId('time_button');
|
||||
this.timeOptionDropdown = container.getByLabel('Choose a time');
|
||||
this.closeButton = container.getByRole('button', {name: 'Close'});
|
||||
@@ -80,7 +80,7 @@ export default class ScheduleMessageModal {
|
||||
|
||||
async selectTime(optionIndex: number = 0) {
|
||||
await this.timeButton.click();
|
||||
const timeButton = this.timeOptionDropdown.getByTestId(`time_option_${optionIndex}-button`);
|
||||
const timeButton = this.container.page().getByTestId(`time_option_${optionIndex}`);
|
||||
await expect(timeButton).toBeVisible();
|
||||
await timeButton.click();
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class ScheduleMessageModal {
|
||||
await this.toBeVisible();
|
||||
|
||||
const selectedDate = await this.selectDate(dayFromToday);
|
||||
const fromDateButton = await this.dateButton.inputValue();
|
||||
const fromDateButton = await this.dateButton.textContent();
|
||||
|
||||
const selectedTime = await this.selectTime(timeOptionIndex);
|
||||
await this.scheduleButton.click();
|
||||
|
||||
@@ -36,8 +36,7 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 0, 1);
|
||||
|
||||
// * Verify scheduled post indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
||||
|
||||
// * Verify scheduled post badge in left sidebar shows count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
@@ -46,8 +45,7 @@ test(
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send ${selectedDate} at ${selectedTime}`;
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, selectedDate, selectedTime, badgeCountOnTab: 1});
|
||||
|
||||
// # Return to the channels page
|
||||
await page.goBack();
|
||||
@@ -98,17 +96,16 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessageFromThread(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled post indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, indicatorMessage);
|
||||
await verifyScheduledPostIndicator(sidebarRight.scheduledPostIndicator, selectedDate, selectedTime);
|
||||
|
||||
// # Navigate to scheduled posts page using indicator link
|
||||
await sidebarRight.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -153,8 +150,7 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
||||
|
||||
// * Verify scheduled post badge appears with count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
@@ -163,10 +159,10 @@ test(
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -178,8 +174,11 @@ test(
|
||||
await channelsPage.goto();
|
||||
|
||||
// * Verify indicator shows the updated scheduled time
|
||||
const newIndicatorMessage = `Message scheduled for ${newSelectedDate} at ${newSelectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, newIndicatorMessage);
|
||||
await verifyScheduledPostIndicator(
|
||||
channelsPage.centerView.scheduledPostIndicator,
|
||||
newSelectedDate,
|
||||
newSelectedTime,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -205,17 +204,16 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
||||
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -256,17 +254,16 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
||||
|
||||
// # Navigate to scheduled posts page via indicator link
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -314,14 +311,20 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 1);
|
||||
|
||||
// * Verify appropriate scheduled message indicator appears
|
||||
let indicatorMessage;
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
indicatorMessage = 'You have one scheduled message.';
|
||||
} else {
|
||||
indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
}
|
||||
await channelsPage.centerView.scheduledPostIndicator.toBeVisible();
|
||||
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(indicatorMessage);
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
// Special case for timezone - expect generic message
|
||||
await expect(channelsPage.centerView.scheduledPostIndicator.messageText).toContainText(
|
||||
'You have one scheduled message.',
|
||||
);
|
||||
} else {
|
||||
// Normal case - verify the scheduled indicator
|
||||
await verifyScheduledPostIndicator(
|
||||
channelsPage.centerView.scheduledPostIndicator,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
);
|
||||
}
|
||||
|
||||
// # Navigate to scheduled posts page using appropriate link
|
||||
if (pw.isOutsideRemoteUserHour(otherUser.timezone)) {
|
||||
@@ -331,10 +334,10 @@ test(
|
||||
}
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -399,8 +402,7 @@ test(
|
||||
await scheduledPostsPage.goto(team.name);
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, sendOnMessage, badgeCountOnTab: 1});
|
||||
await verifyScheduledPost(scheduledPostsPage, {draftMessage, selectedDate, selectedTime, badgeCountOnTab: 1});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -426,8 +428,7 @@ test(
|
||||
const {selectedDate, selectedTime} = await channelsPage.scheduleMessage(draftMessage, 2);
|
||||
|
||||
// * Verify scheduled message indicator shows correct date and time
|
||||
const indicatorMessage = `Message scheduled for ${selectedDate} at ${selectedTime}.`;
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, indicatorMessage);
|
||||
await verifyScheduledPostIndicator(channelsPage.centerView.scheduledPostIndicator, selectedDate, selectedTime);
|
||||
|
||||
// * Verify scheduled post badge shows count of 1
|
||||
await verifyScheduledPostBadgeOnLeftSidebar(channelsPage, 1);
|
||||
@@ -436,10 +437,10 @@ test(
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -454,7 +455,7 @@ test(
|
||||
await expect(scheduledPost.panelBody).toContainText(updatedText);
|
||||
|
||||
// * Verify scheduled date/time remains unchanged
|
||||
await expect(scheduledPost.panelHeader).toContainText(`Send on ${selectedDate} at ${selectedTime}`);
|
||||
await expect(scheduledPost.panelHeader).toContainText(selectedTime);
|
||||
|
||||
// # Send the edited message immediately
|
||||
await scheduledPost.hover();
|
||||
@@ -506,10 +507,10 @@ test(
|
||||
await channelsPage.centerView.scheduledPostIndicator.seeAllLink.click();
|
||||
|
||||
// * Verify scheduled post appears with correct information
|
||||
const sendOnMessage = `Send on ${selectedDate} at ${selectedTime}`;
|
||||
const scheduledPost = await verifyScheduledPost(scheduledPostsPage, {
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab: 1,
|
||||
});
|
||||
|
||||
@@ -532,15 +533,36 @@ test(
|
||||
);
|
||||
|
||||
/**
|
||||
* Verifies that the scheduled post indicator is visible and displays the correct date and time.
|
||||
*
|
||||
* @param scheduledPostIndicator - The ScheduledPostIndicator instance
|
||||
* @param messageText - A post message
|
||||
* Verifies that the scheduled post indicator shows the correct date and time.
|
||||
*/
|
||||
async function verifyScheduledPostIndicator(scheduledPostIndicator: ScheduledPostIndicator, messageText: string) {
|
||||
async function verifyScheduledPostIndicator(
|
||||
scheduledPostIndicator: ScheduledPostIndicator,
|
||||
selectedDate: string,
|
||||
selectedTime: string | null,
|
||||
) {
|
||||
await scheduledPostIndicator.toBeVisible();
|
||||
await expect(scheduledPostIndicator.icon).toBeVisible();
|
||||
await expect(scheduledPostIndicator.messageText).toContainText(messageText);
|
||||
|
||||
if (!selectedTime) {
|
||||
throw new Error('selectedTime is required');
|
||||
}
|
||||
|
||||
// Verify the indicator contains both the time and a valid date
|
||||
const messageText = await scheduledPostIndicator.messageText.textContent();
|
||||
await expect(scheduledPostIndicator.messageText).toContainText(selectedTime);
|
||||
const datePatterns = [
|
||||
selectedDate, // Original date
|
||||
'Today',
|
||||
'Tomorrow',
|
||||
];
|
||||
|
||||
const hasValidDate = datePatterns.some((pattern) => messageText?.toLowerCase().includes(pattern.toLowerCase()));
|
||||
|
||||
if (!hasValidDate) {
|
||||
throw new Error(
|
||||
`Indicator text "${messageText}" does not contain any expected date pattern: ${datePatterns.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyScheduledPostBadgeOnLeftSidebar(channelsPage: ChannelsPage, count: number) {
|
||||
@@ -552,9 +574,10 @@ async function verifyScheduledPost(
|
||||
scheduledPostsPage: ScheduledPostsPage,
|
||||
{
|
||||
draftMessage,
|
||||
sendOnMessage,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
badgeCountOnTab,
|
||||
}: {draftMessage: string; sendOnMessage: string; badgeCountOnTab: number},
|
||||
}: {draftMessage: string; selectedDate: string; selectedTime: string | null; badgeCountOnTab: number},
|
||||
) {
|
||||
// * Verify scheduled posts page is visible
|
||||
await scheduledPostsPage.toBeVisible();
|
||||
@@ -566,7 +589,26 @@ async function verifyScheduledPost(
|
||||
const scheduledPost = await scheduledPostsPage.getLastPost();
|
||||
await expect(scheduledPost.panelBody).toContainText(draftMessage);
|
||||
|
||||
await expect(scheduledPost.panelHeader).toContainText(sendOnMessage);
|
||||
if (!selectedTime) {
|
||||
throw new Error('selectedTime is required');
|
||||
}
|
||||
|
||||
// Verify the header contains both the time and a valid date
|
||||
const headerText = await scheduledPost.panelHeader.textContent();
|
||||
await expect(scheduledPost.panelHeader).toContainText(selectedTime);
|
||||
const datePatterns = [
|
||||
selectedDate, // Original date
|
||||
'Today',
|
||||
'Tomorrow',
|
||||
];
|
||||
|
||||
const hasValidDate = datePatterns.some((pattern) => headerText?.toLowerCase().includes(pattern.toLowerCase()));
|
||||
|
||||
if (!hasValidDate) {
|
||||
throw new Error(
|
||||
`Header "${headerText}" does not contain any expected date pattern: ${datePatterns.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return scheduledPost;
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/custom_status/date_time_input should match snapshot 1`] = `
|
||||
<DateTimeInputContainer
|
||||
handleChange={[MockFunction]}
|
||||
time={"2021-05-03T14:53:39.127Z"}
|
||||
timezone="Australia/Sydney"
|
||||
/>
|
||||
`;
|
||||
@@ -33,46 +33,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dateTime__calendar-input {
|
||||
border: 2px solid var(--button-bg);
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
border: 2px solid var(--button-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
button.dateTime__calendar-icon {
|
||||
margin: 0 8px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
|
||||
input#customStatus__calendar-input {
|
||||
&.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-weight: 400;
|
||||
|
||||
&[readonly] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.GenericModal {
|
||||
@@ -283,7 +243,8 @@ span.emoticon[style]:hover {
|
||||
|
||||
.statusExpiry {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
margin: 12px 0 24px 0;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
&__menu {
|
||||
@@ -295,71 +256,3 @@ span.emoticon[style]:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dateTime {
|
||||
display: flex;
|
||||
margin: 24px 0 8px;
|
||||
gap: 16px;
|
||||
|
||||
& &__date {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
top: 11.3px;
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
& &__time {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.style--none {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#expiryTimeMenu .dropdown-menu {
|
||||
overflow: auto;
|
||||
min-width: 266px;
|
||||
max-height: 268px;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
top: 9.5px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.dateTime__input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
& &__input {
|
||||
width: 266px;
|
||||
height: 40px;
|
||||
padding: 10px 40px;
|
||||
border: 2px solid var(--button-bg);
|
||||
border-radius: 4px;
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-color);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
|
||||
&-title {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
left: 12px;
|
||||
padding: 0 4px;
|
||||
background: var(--center-channel-bg);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import {closeModal} from 'actions/views/modals';
|
||||
import {makeGetCustomStatus, getRecentCustomStatuses, showStatusDropdownPulsatingDot, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
|
||||
import CustomStatusSuggestion from 'components/custom_status/custom_status_suggestion';
|
||||
import DateTimeInput, {getRoundedTime} from 'components/custom_status/date_time_input';
|
||||
import ExpiryMenu from 'components/custom_status/expiry_menu';
|
||||
import DateTimeInput, {getRoundedTime} from 'components/datetime_input/datetime_input';
|
||||
import RenderEmoji from 'components/emoji/render_emoji';
|
||||
import useEmojiPicker from 'components/emoji_picker/use_emoji_picker';
|
||||
import QuickInput, {MaxLengthInput} from 'components/quick_input';
|
||||
@@ -416,6 +416,7 @@ const CustomStatusModal: React.FC<Props> = (props: Props) => {
|
||||
handleChange={setCustomExpiryTime}
|
||||
timezone={timezone}
|
||||
setIsInteracting={setIsInteracting}
|
||||
relativeDate={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import moment from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import * as i18Selectors from 'selectors/i18n';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import DateTimeInput, {getTimeInIntervals} from './date_time_input';
|
||||
|
||||
jest.mock('selectors/i18n');
|
||||
|
||||
describe('components/custom_status/date_time_input', () => {
|
||||
const store = mockStore({});
|
||||
|
||||
(i18Selectors.getCurrentLocale as jest.Mock).mockReturnValue(General.DEFAULT_LOCALE);
|
||||
const baseProps = {
|
||||
time: moment('2021-05-03T14:53:39.127Z'),
|
||||
handleChange: jest.fn(),
|
||||
timezone: 'Australia/Sydney',
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<DateTimeInput {...baseProps}/>
|
||||
</Provider>,
|
||||
);
|
||||
expect(wrapper.dive()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['2024-03-02T02:00:00+0100', 48],
|
||||
['2024-03-31T02:00:00+0100', 46],
|
||||
['2024-10-07T02:00:00+0100', 48],
|
||||
['2024-10-27T02:00:00+0100', 48],
|
||||
['2025-01-01T03:00:00+0200', 48],
|
||||
])('should not infinitely loop on DST', (time, expected) => {
|
||||
const timezone = 'Europe/Paris';
|
||||
|
||||
const intervals = getTimeInIntervals(moment.tz(time, timezone).startOf('day'));
|
||||
expect(intervals).toHaveLength(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,23 @@
|
||||
.date-picker__popper {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
background: var(--center-channel-bg);
|
||||
box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.12);
|
||||
.date-picker {
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__popper {
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
background: var(--center-channel-bg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
// React Day Picker styles
|
||||
.rdp {
|
||||
--rdp-accent-color: var(--button-bg);
|
||||
--rdp-background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
--rdp-cell-size: 36px;
|
||||
margin: 0;
|
||||
|
||||
.rdp-caption {
|
||||
padding: 4px;
|
||||
|
||||
@@ -27,9 +27,12 @@ type Props = {
|
||||
isPopperOpen: boolean;
|
||||
locale: string;
|
||||
handlePopperOpenState: (isOpen: boolean) => void;
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const DatePicker = ({children, datePickerProps, isPopperOpen, handlePopperOpenState, locale}: Props) => {
|
||||
const DatePicker = ({children, datePickerProps, isPopperOpen, handlePopperOpenState, locale, label, icon, value}: Props) => {
|
||||
const [loadedLocales, setLoadedLocales] = useState<Record<string, Locale>>({});
|
||||
const {x, y, strategy, context, refs: {setReference, setFloating}} = useFloating({
|
||||
open: isPopperOpen,
|
||||
@@ -66,13 +69,34 @@ const DatePicker = ({children, datePickerProps, isPopperOpen, handlePopperOpenSt
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleWrapperClick = useCallback((e: React.MouseEvent) => {
|
||||
// Prevent click from bubbling up to parent elements
|
||||
e.stopPropagation();
|
||||
|
||||
// Open the popper when clicking anywhere in the wrapper
|
||||
handlePopperOpenState(true);
|
||||
}, [handlePopperOpenState]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='date-picker__wrapper'>
|
||||
<div
|
||||
ref={setReference}
|
||||
{...getReferenceProps()}
|
||||
onClick={handleWrapperClick}
|
||||
className={isPopperOpen ? 'date-time-input date-time-input--open' : 'date-time-input'}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handlePopperOpenState(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{label && <span className='date-time-input__label'>{label}</span>}
|
||||
{icon && <span className='date-time-input__icon'>{icon}</span>}
|
||||
{value && <span className='date-time-input__value'>{value}</span>}
|
||||
{!value && children}
|
||||
</div>
|
||||
{isPopperOpen && (
|
||||
<FloatingFocusManager
|
||||
@@ -101,7 +125,6 @@ const DatePicker = ({children, datePickerProps, isPopperOpen, handlePopperOpenSt
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</FloatingFocusManager>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {GenericModal} from '@mattermost/components';
|
||||
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import DateTimeInput, {getRoundedTime} from 'components/custom_status/date_time_input';
|
||||
import DateTimeInput, {getRoundedTime} from 'components/datetime_input/datetime_input';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
@@ -107,7 +107,8 @@ export default function DateTimePickerModal({
|
||||
handleEnterKeyPress={handleEnterKeyPress}
|
||||
className={classnames('date-time-picker-modal', className)}
|
||||
compassDesign={true}
|
||||
keyboardEscape={false}
|
||||
keyboardEscape={true}
|
||||
enforceFocus={false}
|
||||
cancelButtonText={cancelButtonText}
|
||||
autoCloseOnConfirmButton={false}
|
||||
errorText={errorText}
|
||||
|
||||
@@ -2,78 +2,4 @@
|
||||
.modal-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dateTime__calendar-input {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
button.dateTime__calendar-icon {
|
||||
margin: 0 8px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
|
||||
input#customStatus__calendar-input {
|
||||
&.form-control {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-weight: 400;
|
||||
|
||||
&[readonly] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dateTime__time-menu .dateTime__input, .dateTime__calendar-input {
|
||||
height: 40px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
|
||||
&:hover, &.isOpen {
|
||||
border-color: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
box-shadow: 0 0 0 2px var(--button-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
button:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dateTime__time-menu {
|
||||
&.MenuWrapper--open {
|
||||
.dateTime__input {
|
||||
box-shadow: 0 0 0 2px var(--button-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.dateTime__time-icon {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dateTime {
|
||||
width: 100%;
|
||||
margin: unset;
|
||||
|
||||
.dateTime__input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/datetime_input/DateTimeInput should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="dateTime"
|
||||
>
|
||||
<div
|
||||
class="dateTime__date"
|
||||
>
|
||||
<div
|
||||
class="date-picker__wrapper"
|
||||
>
|
||||
<div
|
||||
class="date-time-input"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="date-time-input__label"
|
||||
>
|
||||
Date
|
||||
</span>
|
||||
<span
|
||||
class="date-time-input__icon"
|
||||
>
|
||||
<i
|
||||
class="icon-calendar-outline"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="date-time-input__value"
|
||||
>
|
||||
6/8/2025
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dateTime__time"
|
||||
>
|
||||
<button
|
||||
aria-controls="expiryTimeMenu"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Time"
|
||||
class="date-time-input"
|
||||
data-testid="time_button"
|
||||
id="time_button"
|
||||
>
|
||||
<span
|
||||
class="date-time-input__label"
|
||||
>
|
||||
Time
|
||||
</span>
|
||||
<span
|
||||
class="date-time-input__icon"
|
||||
>
|
||||
<i
|
||||
class="icon-clock-outline"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="date-time-input__value"
|
||||
>
|
||||
<time
|
||||
datetime="2025-06-08T12:09:00.000"
|
||||
>
|
||||
12:09 PM
|
||||
</time>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, act} from '@testing-library/react';
|
||||
import moment from 'moment-timezone';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
||||
import * as timezoneUtils from 'utils/timezone';
|
||||
|
||||
import DateTimeInput, {getTimeInIntervals} from './datetime_input';
|
||||
|
||||
// Mock timezone utilities
|
||||
jest.mock('utils/timezone', () => ({
|
||||
getCurrentMomentForTimezone: jest.fn(),
|
||||
isBeforeTime: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetCurrentMomentForTimezone = timezoneUtils.getCurrentMomentForTimezone as jest.MockedFunction<typeof timezoneUtils.getCurrentMomentForTimezone>;
|
||||
const mockIsBeforeTime = timezoneUtils.isBeforeTime as jest.MockedFunction<typeof timezoneUtils.isBeforeTime>;
|
||||
|
||||
describe('components/datetime_input/DateTimeInput', () => {
|
||||
const baseProps = {
|
||||
time: moment('2025-06-08T12:09:00.000Z'),
|
||||
handleChange: jest.fn(),
|
||||
timezone: 'UTC',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetCurrentMomentForTimezone.mockReturnValue(moment('2025-06-08T10:00:00.000Z'));
|
||||
mockIsBeforeTime.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const {container} = renderWithContext(
|
||||
<DateTimeInput {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render date and time selectors', () => {
|
||||
renderWithContext(
|
||||
<DateTimeInput {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Date')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Time')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not infinitely loop on DST', () => {
|
||||
const timezone = 'Europe/Paris';
|
||||
const time = '2024-03-31T02:00:00+0100';
|
||||
|
||||
const intervals = getTimeInIntervals(moment.tz(time, timezone).startOf('day'));
|
||||
expect(intervals.length).toBeGreaterThan(0);
|
||||
expect(intervals.length).toBeLessThan(100); // Reasonable upper bound
|
||||
});
|
||||
|
||||
describe('user interactions', () => {
|
||||
test('should call setIsInteracting when date picker opens', async () => {
|
||||
const mockSetIsInteracting = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
setIsInteracting: mockSetIsInteracting,
|
||||
};
|
||||
|
||||
renderWithContext(<DateTimeInput {...props}/>);
|
||||
|
||||
const dateButton = screen.getByText('Date').closest('.date-time-input');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(dateButton!);
|
||||
});
|
||||
|
||||
expect(mockSetIsInteracting).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should call setIsInteracting when time menu opens', async () => {
|
||||
const mockSetIsInteracting = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
setIsInteracting: mockSetIsInteracting,
|
||||
};
|
||||
|
||||
renderWithContext(<DateTimeInput {...props}/>);
|
||||
|
||||
const timeButton = screen.getByLabelText('Time');
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(timeButton);
|
||||
});
|
||||
|
||||
expect(mockSetIsInteracting).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should close date picker on escape key', async () => {
|
||||
const mockSetIsInteracting = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
setIsInteracting: mockSetIsInteracting,
|
||||
};
|
||||
|
||||
renderWithContext(<DateTimeInput {...props}/>);
|
||||
|
||||
// Open date picker first
|
||||
const dateButton = screen.getByText('Date').closest('.date-time-input');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(dateButton!);
|
||||
});
|
||||
|
||||
// Press escape key
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(document, {key: 'Escape', code: 'Escape'});
|
||||
});
|
||||
|
||||
expect(mockSetIsInteracting).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('date selection', () => {
|
||||
test('should handle day selection for today with time adjustment', async () => {
|
||||
mockGetCurrentMomentForTimezone.mockReturnValue(moment('2025-06-08T08:00:00.000Z'));
|
||||
mockIsBeforeTime.mockReturnValue(true);
|
||||
|
||||
renderWithContext(<DateTimeInput {...baseProps}/>);
|
||||
|
||||
const dateButton = screen.getByText('Date').closest('.date-time-input');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(dateButton!);
|
||||
});
|
||||
|
||||
// Simulate clicking on today's date
|
||||
const todayButton = screen.getByText('8'); // June 8th
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(todayButton);
|
||||
});
|
||||
|
||||
expect(baseProps.handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle day selection for future date', async () => {
|
||||
mockGetCurrentMomentForTimezone.mockReturnValue(moment('2025-06-08T08:00:00.000Z'));
|
||||
|
||||
renderWithContext(<DateTimeInput {...baseProps}/>);
|
||||
|
||||
const dateButton = screen.getByText('Date').closest('.date-time-input');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(dateButton!);
|
||||
});
|
||||
|
||||
// Simulate clicking on a future date
|
||||
const futureButton = screen.getByText('15'); // June 15th
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(futureButton);
|
||||
});
|
||||
|
||||
expect(baseProps.handleChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('timezone handling', () => {
|
||||
test('should handle timezone prop', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
timezone: 'America/New_York',
|
||||
};
|
||||
|
||||
renderWithContext(<DateTimeInput {...props}/>);
|
||||
|
||||
expect(mockGetCurrentMomentForTimezone).toHaveBeenCalledWith('America/New_York');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom configuration', () => {
|
||||
test('should accept custom time picker interval', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
timePickerInterval: 15,
|
||||
};
|
||||
|
||||
renderWithContext(<DateTimeInput {...props}/>);
|
||||
|
||||
// Component should render without errors with custom interval
|
||||
expect(screen.getByLabelText('Time')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle relative date formatting', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
relativeDate: true,
|
||||
};
|
||||
|
||||
renderWithContext(<DateTimeInput {...props}/>);
|
||||
|
||||
// Component should render without errors with relative formatting
|
||||
expect(screen.getByText('Date')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {DateTime} from 'luxon';
|
||||
import type {Moment} from 'moment-timezone';
|
||||
import moment from 'moment-timezone';
|
||||
@@ -10,21 +9,16 @@ import type {DayModifiers, DayPickerProps} from 'react-day-picker';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import IconButton from '@mattermost/compass-components/components/icon-button'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
|
||||
import CompassThemeProvider from 'components/compass_theme_provider/compass_theme_provider';
|
||||
import DatePicker from 'components/date_picker';
|
||||
import * as Menu from 'components/menu';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
||||
import Constants from 'utils/constants';
|
||||
import {relativeFormatDate} from 'utils/datetime';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {getCurrentMomentForTimezone, isBeforeTime} from 'utils/timezone';
|
||||
@@ -80,20 +74,43 @@ const DateTimeInputContainer: React.FC<Props> = ({
|
||||
const locale = useSelector(getCurrentLocale);
|
||||
const [timeOptions, setTimeOptions] = useState<Date[]>([]);
|
||||
const [isPopperOpen, setIsPopperOpen] = useState(false);
|
||||
const [isTimeMenuOpen, setIsTimeMenuOpen] = useState(false);
|
||||
const [menuWidth, setMenuWidth] = useState<string>('200px');
|
||||
const {formatMessage} = useIntl();
|
||||
const timeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const theme = useSelector(getTheme);
|
||||
const timeContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handlePopperOpenState = useCallback((isOpen: boolean) => {
|
||||
setIsPopperOpen(isOpen);
|
||||
setIsInteracting?.(isOpen);
|
||||
}, [setIsInteracting]);
|
||||
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE) && isPopperOpen) {
|
||||
handlePopperOpenState(false);
|
||||
const handleTimeMenuToggle = useCallback((isOpen: boolean) => {
|
||||
setIsTimeMenuOpen(isOpen);
|
||||
setIsInteracting?.(isOpen);
|
||||
|
||||
// Measure and set menu width when opening
|
||||
if (isOpen && timeContainerRef.current) {
|
||||
const button = timeContainerRef.current.querySelector('button');
|
||||
if (button) {
|
||||
const buttonWidth = button.getBoundingClientRect().width;
|
||||
setMenuWidth(`${Math.max(buttonWidth, 200)}px`); // Ensure minimum width of 200px
|
||||
}
|
||||
}
|
||||
}, [isPopperOpen, handlePopperOpenState]);
|
||||
}, [setIsInteracting]);
|
||||
|
||||
const handleTimeChange = useCallback((time: Date) => {
|
||||
handleChange(timezone ? moment.tz(time, timezone) : moment(time));
|
||||
}, [handleChange, timezone]);
|
||||
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
// Handle escape key for date picker when time menu is not open
|
||||
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE)) {
|
||||
if (isPopperOpen && !isTimeMenuOpen) {
|
||||
handlePopperOpenState(false);
|
||||
}
|
||||
}
|
||||
}, [isPopperOpen, isTimeMenuOpen, handlePopperOpenState]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
@@ -131,37 +148,18 @@ const DateTimeInputContainer: React.FC<Props> = ({
|
||||
handlePopperOpenState(false);
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback((time: Date, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
handleChange(timezone ? moment.tz(time, timezone) : moment(time));
|
||||
focusTimeButton();
|
||||
}, [handleChange]);
|
||||
|
||||
const currentTime = getCurrentMomentForTimezone(timezone).toDate();
|
||||
|
||||
const focusTimeButton = useCallback(() => {
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: timeButtonRef.current,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
}, []);
|
||||
|
||||
const formatDate = (date: Moment): string => {
|
||||
return relativeDate ? relativeFormatDate(date, formatMessage) : DateTime.fromJSDate(date.toDate()).toLocaleString();
|
||||
};
|
||||
|
||||
const inputIcon = (
|
||||
<IconButton
|
||||
onClick={() => handlePopperOpenState(true)}
|
||||
icon={'calendar-outline'}
|
||||
className='dateTime__calendar-icon'
|
||||
size={'sm'}
|
||||
aria-haspopup='grid'
|
||||
/>
|
||||
const calendarIcon = (
|
||||
<i className='icon-calendar-outline'/>
|
||||
);
|
||||
|
||||
const clockIcon = (
|
||||
<i className='icon-clock-outline'/>
|
||||
);
|
||||
|
||||
const datePickerProps: DayPickerProps = {
|
||||
@@ -185,65 +183,72 @@ const DateTimeInputContainer: React.FC<Props> = ({
|
||||
handlePopperOpenState={handlePopperOpenState}
|
||||
locale={locale}
|
||||
datePickerProps={datePickerProps}
|
||||
label={formatMessage({
|
||||
id: 'datetime.date',
|
||||
defaultMessage: 'Date',
|
||||
})}
|
||||
icon={calendarIcon}
|
||||
value={formatDate(time)}
|
||||
>
|
||||
<Input
|
||||
value={formatDate(time)}
|
||||
id='customStatus__calendar-input'
|
||||
readOnly={true}
|
||||
className={classNames('dateTime__calendar-input', {isOpen: isPopperOpen})}
|
||||
label={formatMessage({id: 'dnd_custom_time_picker_modal.date', defaultMessage: 'Date'})}
|
||||
onClick={() => handlePopperOpenState(true)}
|
||||
tabIndex={-1}
|
||||
inputPrefix={inputIcon}
|
||||
/>
|
||||
<></>
|
||||
</DatePicker>
|
||||
</div>
|
||||
<div className='dateTime__time'>
|
||||
<MenuWrapper
|
||||
className='dateTime__time-menu'
|
||||
onToggle={setIsInteracting}
|
||||
<div
|
||||
className='dateTime__time'
|
||||
ref={timeContainerRef}
|
||||
>
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: 'time_button',
|
||||
dataTestId: 'time_button',
|
||||
'aria-label': formatMessage({
|
||||
id: 'datetime.time',
|
||||
defaultMessage: 'Time',
|
||||
}),
|
||||
class: isTimeMenuOpen ? 'date-time-input date-time-input--open' : 'date-time-input',
|
||||
children: (
|
||||
<>
|
||||
<span className='date-time-input__label'>{formatMessage({
|
||||
id: 'datetime.time',
|
||||
defaultMessage: 'Time',
|
||||
})}</span>
|
||||
<span className='date-time-input__icon'>{clockIcon}</span>
|
||||
<span className='date-time-input__value'>
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
value={time.toString()}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
menu={{
|
||||
id: 'expiryTimeMenu',
|
||||
'aria-label': formatMessage({id: 'time_dropdown.choose_time', defaultMessage: 'Choose a time'}),
|
||||
onToggle: handleTimeMenuToggle,
|
||||
width: menuWidth,
|
||||
className: 'time-menu-scrollable',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
data-testid='time_button'
|
||||
className='style--none'
|
||||
ref={timeButtonRef}
|
||||
>
|
||||
<span className='dateTime__input-title'>{formatMessage({id: 'custom_status.expiry.time_picker.title', defaultMessage: 'Time'})}</span>
|
||||
<span className='dateTime__time-icon'>
|
||||
<i className='icon-clock-outline'/>
|
||||
</span>
|
||||
<div
|
||||
className='dateTime__input'
|
||||
>
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
value={time.toString()}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel={formatMessage({id: 'time_dropdown.choose_time', defaultMessage: 'Choose a time'})}
|
||||
id='expiryTimeMenu'
|
||||
>
|
||||
<Menu.Group>
|
||||
{Array.isArray(timeOptions) && timeOptions.map((option, index) => (
|
||||
<Menu.ItemAction
|
||||
id={`time_option_${index}`}
|
||||
onClick={handleTimeChange.bind(this, option)}
|
||||
key={index}
|
||||
text={
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
value={option}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
{timeOptions.map((option, index) => (
|
||||
<Menu.Item
|
||||
key={index}
|
||||
id={`time_option_${index}`}
|
||||
data-testid={`time_option_${index}`}
|
||||
labels={
|
||||
<span>
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
value={option}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
onClick={() => handleTimeChange(option)}
|
||||
/>
|
||||
))}
|
||||
</Menu.Container>
|
||||
</div>
|
||||
</div>
|
||||
</CompassThemeProvider>
|
||||
@@ -44,104 +44,3 @@
|
||||
max-height: 268px;
|
||||
}
|
||||
}
|
||||
|
||||
.DndModal__input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--center-channel-bg-rgb), 1);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
.MenuWrapper--open & {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding: 0 0 2px 40px;
|
||||
border: none;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.DndModal__input--no-border {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.DndModal__input__label {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -11px;
|
||||
left: 0;
|
||||
padding: 0 4px;
|
||||
margin: 0 14px;
|
||||
background: rgba(var(--center-channel-bg-rgb), 1);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.DndModal__calendar-input {
|
||||
height: 40px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.popper-open {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||
color: var(--button-bg);
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
padding-left: 8px;
|
||||
|
||||
button.dateTime__calendar-icon {
|
||||
margin: 0 8px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
|
||||
&:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input#DndModal__calendar-input {
|
||||
height: auto;
|
||||
border: 0;
|
||||
|
||||
&[readonly] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {DateTime} from 'luxon';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import type {DayPickerProps} from 'react-day-picker';
|
||||
import {defineMessage, FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, injectIntl, type WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import IconButton from '@mattermost/compass-components/components/icon-button'; // eslint-disable-line no-restricted-imports
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {UserStatus} from '@mattermost/types/users';
|
||||
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import CompassThemeProvider from 'components/compass_theme_provider/compass_theme_provider';
|
||||
import DatePicker from 'components/date_picker';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import DateTimeInput from 'components/datetime_input/datetime_input';
|
||||
|
||||
import Constants, {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import {toUTCUnixInSeconds} from 'utils/datetime';
|
||||
import Constants, {UserStatuses} from 'utils/constants';
|
||||
import {toUTCUnixInSeconds, relativeFormatDate} from 'utils/datetime';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
@@ -37,36 +30,26 @@ type Props = {
|
||||
actions: {
|
||||
setStatus: (status: UserStatus) => void;
|
||||
};
|
||||
};
|
||||
} & WrappedComponentProps;
|
||||
|
||||
type State = {
|
||||
selectedDate: Date;
|
||||
selectedTime: string;
|
||||
timeMenuList: string[];
|
||||
dayPickerStartDate: Date;
|
||||
isPopperOpen: boolean;
|
||||
popperElement: HTMLDivElement | null;
|
||||
selectedDateTime: moment.Moment;
|
||||
}
|
||||
|
||||
export default class DndCustomTimePicker extends React.PureComponent<Props, State> {
|
||||
private buttonRef = React.createRef<HTMLButtonElement>();
|
||||
export default injectIntl(class DndCustomTimePicker extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const {currentDate} = this.props;
|
||||
const selectedDate: Date = new Date(currentDate);
|
||||
let selectedDateTime = moment(currentDate);
|
||||
|
||||
// if current time is > 23:20 then we will set date to tomorrow and show all times
|
||||
if (currentDate.getHours() === 23 && currentDate.getMinutes() > 20) {
|
||||
selectedDate.setDate(currentDate.getDate() + 1);
|
||||
selectedDateTime = selectedDateTime.add(1, 'day').startOf('day').add(9, 'hours');
|
||||
}
|
||||
|
||||
this.state = {
|
||||
selectedDate,
|
||||
dayPickerStartDate: selectedDate,
|
||||
...this.makeTimeMenuList(selectedDate),
|
||||
isPopperOpen: false,
|
||||
popperElement: null,
|
||||
selectedDateTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,16 +63,12 @@ export default class DndCustomTimePicker extends React.PureComponent<Props, Stat
|
||||
|
||||
handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE)) {
|
||||
if (this.state.isPopperOpen) {
|
||||
this.handlePopperOpenState(false);
|
||||
} else {
|
||||
this.props.onExited();
|
||||
}
|
||||
this.props.onExited();
|
||||
}
|
||||
};
|
||||
|
||||
formatDate = (date: Date): string => {
|
||||
return DateTime.fromJSDate(date).toFormat('yyyy-MM-dd');
|
||||
formatDate = (date: moment.Moment): string => {
|
||||
return relativeFormatDate(date, this.props.intl.formatMessage);
|
||||
};
|
||||
|
||||
getText = () => {
|
||||
@@ -113,13 +92,7 @@ export default class DndCustomTimePicker extends React.PureComponent<Props, Stat
|
||||
};
|
||||
|
||||
handleConfirm = async () => {
|
||||
if (this.state.isPopperOpen) {
|
||||
return;
|
||||
}
|
||||
const hours = parseInt(this.state.selectedTime.split(':')[0], 10);
|
||||
const minutes = parseInt(this.state.selectedTime.split(':')[1], 10);
|
||||
const endTime = new Date(this.state.selectedDate);
|
||||
endTime.setHours(hours, minutes);
|
||||
const endTime = this.state.selectedDateTime.toDate();
|
||||
if (endTime < new Date()) {
|
||||
return;
|
||||
}
|
||||
@@ -133,49 +106,9 @@ export default class DndCustomTimePicker extends React.PureComponent<Props, Stat
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
handleDaySelection = (day: Date) => {
|
||||
handleDateTimeChange = (newDateTime: moment.Moment) => {
|
||||
this.setState({
|
||||
isPopperOpen: false,
|
||||
selectedDate: day,
|
||||
...this.makeTimeMenuList(day),
|
||||
});
|
||||
};
|
||||
|
||||
makeTimeMenuList = (date: Date): {timeMenuList: string[]; selectedTime: string} => {
|
||||
const timeMenuItems = [];
|
||||
let h = 0;
|
||||
let m = 0;
|
||||
const curr = this.props.currentDate;
|
||||
|
||||
if (this.formatDate(curr) === this.formatDate(date)) {
|
||||
h = curr.getHours();
|
||||
m = curr.getMinutes();
|
||||
if (m > 20) {
|
||||
h++;
|
||||
m = 0;
|
||||
} else {
|
||||
m = 30;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = h; i < 24; i++) {
|
||||
for (let j = m / 30; j < 2; j++) {
|
||||
const t = i.toString().padStart(2, '0') + ':' + (j * 30).toString().padStart(2, '0');
|
||||
timeMenuItems.push(
|
||||
t,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timeMenuList: timeMenuItems,
|
||||
selectedTime: timeMenuItems[0],
|
||||
};
|
||||
};
|
||||
|
||||
handlePopperOpenState = (isOpen: boolean) => {
|
||||
this.setState({
|
||||
isPopperOpen: isOpen,
|
||||
selectedDateTime: newDateTime,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -185,53 +118,7 @@ export default class DndCustomTimePicker extends React.PureComponent<Props, Stat
|
||||
confirmButtonText,
|
||||
} = this.getText();
|
||||
|
||||
const {timeMenuList, selectedTime, selectedDate, dayPickerStartDate, isPopperOpen} = this.state;
|
||||
const timeMenuItems = timeMenuList.map((time) => {
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={`dndTime_dropdown_${time}`}
|
||||
key={time}
|
||||
text={time}
|
||||
ariaLabel={`${time} hours`}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
selectedTime: time,
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||
A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: this.buttonRef.current,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
},
|
||||
));
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Menu.ItemAction>
|
||||
);
|
||||
});
|
||||
|
||||
const inputIcon = (
|
||||
<IconButton
|
||||
icon={'calendar-outline'}
|
||||
onClick={() => this.handlePopperOpenState(true)}
|
||||
className='dateTime__calendar-icon'
|
||||
size={'sm'}
|
||||
aria-haspopup='grid'
|
||||
/>
|
||||
);
|
||||
|
||||
const dayPickerProps: DayPickerProps = {
|
||||
initialFocus: isPopperOpen,
|
||||
mode: 'single',
|
||||
selected: selectedDate,
|
||||
onDayClick: this.handleDaySelection,
|
||||
disabled: [{
|
||||
before: dayPickerStartDate,
|
||||
}],
|
||||
showOutsideDays: true,
|
||||
};
|
||||
const {selectedDateTime} = this.state;
|
||||
|
||||
return (
|
||||
<CompassThemeProvider theme={this.props.theme}>
|
||||
@@ -246,54 +133,19 @@ export default class DndCustomTimePicker extends React.PureComponent<Props, Stat
|
||||
id='dndCustomTimePickerModal'
|
||||
className={'DndModal modal-overflow'}
|
||||
tabIndex={-1}
|
||||
keyboardEscape={false}
|
||||
keyboardEscape={true}
|
||||
enforceFocus={false}
|
||||
>
|
||||
<div className='DndModal__content'>
|
||||
<DatePicker
|
||||
isPopperOpen={isPopperOpen}
|
||||
handlePopperOpenState={this.handlePopperOpenState}
|
||||
locale={this.props.locale}
|
||||
datePickerProps={dayPickerProps}
|
||||
>
|
||||
<Input
|
||||
value={this.formatDate(selectedDate)}
|
||||
readOnly={true}
|
||||
id='DndModal__calendar-input'
|
||||
className={classNames('DndModal__calendar-input', {'popper-open': isPopperOpen})}
|
||||
label={defineMessage({id: 'dnd_custom_time_picker_modal.date', defaultMessage: 'Date'})}
|
||||
onClick={() => this.handlePopperOpenState(true)}
|
||||
tabIndex={-1}
|
||||
inputPrefix={inputIcon}
|
||||
/>
|
||||
</DatePicker>
|
||||
<MenuWrapper
|
||||
id='dropdown-no-caret'
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<button
|
||||
className='DndModal__input'
|
||||
type='button'
|
||||
ref={this.buttonRef}
|
||||
>
|
||||
<div className='DndModal__input__label'>
|
||||
<FormattedMessage
|
||||
id='dnd_custom_time_picker_modal.time'
|
||||
defaultMessage='Time'
|
||||
/>
|
||||
</div>
|
||||
<i className='icon icon--no-spacing icon-clock-outline icon--xs icon-14'/>
|
||||
<span>{selectedTime}</span>
|
||||
</button>
|
||||
<Menu
|
||||
openLeft={false}
|
||||
ariaLabel={'Clear custom status after'}
|
||||
>
|
||||
{timeMenuItems}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<DateTimeInput
|
||||
time={selectedDateTime}
|
||||
handleChange={this.handleDateTimeChange}
|
||||
timezone={this.props.locale}
|
||||
relativeDate={true}
|
||||
/>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</CompassThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import type {Moment} from 'moment-timezone';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {getRoundedTime} from 'components/custom_status/date_time_input';
|
||||
import DateTimePickerModal from 'components/date_time_picker_modal/date_time_picker_modal';
|
||||
import {getRoundedTime} from 'components/datetime_input/datetime_input';
|
||||
|
||||
import {toUTCUnixInSeconds} from 'utils/datetime';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
@@ -43,6 +43,7 @@ function PostReminderCustomTimePicker({userId, timezone, onExited, postId, actio
|
||||
initialTime={initialReminderTime}
|
||||
onConfirm={handleConfirm}
|
||||
confirmButtonText={confirmButtonText}
|
||||
relativeDate={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3829,7 +3829,6 @@
|
||||
"custom_status.expiry_dropdown.thirty_minutes": "30 minutes",
|
||||
"custom_status.expiry_dropdown.this_week": "This week",
|
||||
"custom_status.expiry_dropdown.today": "Today",
|
||||
"custom_status.expiry.time_picker.title": "Time",
|
||||
"custom_status.expiry.until": "Until {time}",
|
||||
"custom_status.modal_cancel": "Clear Status",
|
||||
"custom_status.modal_confirm": "Set Status",
|
||||
@@ -3845,6 +3844,8 @@
|
||||
"date_separator.today": "Today",
|
||||
"date_separator.tomorrow": "Tomorrow",
|
||||
"date_separator.yesterday": "Yesterday",
|
||||
"datetime.date": "Date",
|
||||
"datetime.time": "Time",
|
||||
"datetime.today": "today",
|
||||
"datetime.yesterday": "yesterday",
|
||||
"deactivate_member_modal.deactivate": "Deactivate",
|
||||
@@ -3892,10 +3893,8 @@
|
||||
"discard_changes_modal.leave": "Yes, Discard",
|
||||
"discard_changes_modal.message": "You have unsaved changes, are you sure you want to discard them?",
|
||||
"discard_changes_modal.title": "Discard Changes?",
|
||||
"dnd_custom_time_picker_modal.date": "Date",
|
||||
"dnd_custom_time_picker_modal.defaultMsg": "Disable notifications until",
|
||||
"dnd_custom_time_picker_modal.submitButton": "Disable Notifications",
|
||||
"dnd_custom_time_picker_modal.time": "Time",
|
||||
"drafts.actions.delete": "Delete draft",
|
||||
"drafts.actions.edit": "Edit draft",
|
||||
"drafts.actions.scheduled": "Schedule draft",
|
||||
|
||||
@@ -126,3 +126,100 @@ input::-webkit-file-upload-button {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-time-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--center-channel-color-24);
|
||||
border-radius: 4px;
|
||||
background-color: var(--center-channel-bg);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--center-channel-color-40);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&--open {
|
||||
border-color: var(--button-bg);
|
||||
box-shadow: 0 0 0 1px var(--button-bg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__label {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 12px;
|
||||
padding: 0 4px;
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-color-75);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
color: var(--center-channel-color-64);
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
overflow: hidden;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Date Time layout
|
||||
.dateTime {
|
||||
display: flex;
|
||||
margin: 0 0 8px 0;
|
||||
gap: 24px;
|
||||
|
||||
&__date {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__time {
|
||||
flex: 1;
|
||||
|
||||
// Ensure Menu.Container and its button take full width
|
||||
.date-time-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal-specific dateTime layout
|
||||
.modal {
|
||||
.dateTime {
|
||||
width: 100%;
|
||||
|
||||
&__date,
|
||||
&__time {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Ensure all nested elements take full width
|
||||
.date-time-input,
|
||||
.date-picker__wrapper,
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable time menu
|
||||
.time-menu-scrollable {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user