Initial version of the new search interface (#27301)
* Initial version of the new search interface * Fixing CI * style tweaks * tweaks to date picker in search * change search input to button * Addressing a lot of small problems * Addressing current search visualization * Fixing problem on filter autocompletion * Adding some more fixes * Tiny style ajustment * Removing unnecessary slice call * Adding search extensibility * Improving the extensibility * Fixing and refactoring a bit * On clear click, clear the current search * splitting components for simplicity * tweaks to file ext search * Addressing PR review comments and adding color to the extensions search icons * Improving a bit the extension search * Fixing CI * Fixing some bugs * Keep the focus on search input whenever we change the search type * Adding SearchHint tests * Adding search_box_type_selector tests * Adding tests for the SearchBox component * Adding tests for search box suggestions * Adding SearchBoxInput tests * Splitting extension suggestion component and provider * ExtensionSuggestion tests * Removing unneeded property * Adding SearchBoxhints tests * Adding tests for NewSearch component * Adding tests for the extension suggestion provider * Fixing linter errors * Fixing tests * Fixing linter errors * Fixing linter errors * Adding license check for plugins * Addressing PR review comments * Addressing PR review comments * Addressing PR review comments * Fixing tests * Fixing some cypress tests * More work around cypress tests for search * Some accessibility changes * more work on cypress tests * fixing more cypress tests * Fixing linter errors * Fixing tests * Fixing ctrl+f6 switching * Fixing scrolling on small windows wheneve you are searching * Improve editing in the middle of the search text * Addressing some PR review comments * Addressing some PR review comments * Addressing some PR review comments * Addressing some PR review comments * Fixing tests and linter errors * Fixingls tests and linter errors * Fix type checks * Clearer logic to render plugin or builtin suggestion * Addressing PR review changes * Addressing PR review changes * Using selectors for plugin components * Changing uppercase to lowercase on lists of components * Updating tests * Updating snapshots * minor css tweaks * minor css tweak * Adding the search type and the clear button * Fixing problem with the filetype selector * fixing linter errors * Some fixes * More linter fixes * Reverting prettier auto-formating * Fixing CI * Fixing CI * Fixing chrome problem * Fixing CI * Addressing PR review comments --------- Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> Co-authored-by: Christopher Speller <crspeller@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b7ccd745c8
Коммит
5f8bdba459
@@ -52,7 +52,8 @@ describe('Verify Accessibility keyboard usability across different regions in th
|
||||
postMessages(testChannel, otherUser, count);
|
||||
|
||||
// # Search for a term
|
||||
cy.get('#searchBox').typeWithForce('hello').typeWithForce('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').typeWithForce('hello').typeWithForce('{enter}');
|
||||
|
||||
// # Change the focus to search results
|
||||
cy.get('#searchContainer').within(() => {
|
||||
|
||||
@@ -125,7 +125,8 @@ describe('Verify Quick Navigation support across different regions in the app',
|
||||
|
||||
it('MM-T1460_8 Verify Navigation Support in Search Results', () => {
|
||||
// # Search for some text
|
||||
cy.get('#searchBox').should('be.visible').type('hello {enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('be.visible').type('hello {enter}');
|
||||
|
||||
// # Change the focus to search results
|
||||
cy.get('#searchContainer').within(() => {
|
||||
|
||||
@@ -64,7 +64,8 @@ describe('Archived channels', () => {
|
||||
|
||||
// # Search for "Test archive reaction"
|
||||
cy.get('body').type('{esc}');
|
||||
cy.get('#searchBox').should('be.visible').type(messageText).type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('be.visible').type(messageText).type('{enter}');
|
||||
|
||||
// # Click on post dot menu so we can check for reaction icon
|
||||
cy.clickPostDotMenu(postId, 'SEARCH');
|
||||
|
||||
@@ -60,7 +60,8 @@ describe('archive tests while preventing viewing archived channels', () => {
|
||||
cy.postMessage(getRandomId());
|
||||
|
||||
// # Search for the post from step 1')
|
||||
cy.get('#searchBox').click().clear().type(`${messageText}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${messageText}{enter}`);
|
||||
|
||||
// * Post is returned by search, since it's not archived anymore
|
||||
cy.get('#searchContainer').should('be.visible');
|
||||
@@ -94,7 +95,8 @@ describe('archive tests while preventing viewing archived channels', () => {
|
||||
cy.uiArchiveChannel();
|
||||
|
||||
// # Archive dialogue message reads "This will archive the channel from the team and make its contents inaccessible for all users" (Mobile dialogue makes no mention of the data will be accessible)
|
||||
cy.get('#searchBox').click().clear().type(`${testArchivedMessage}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${testArchivedMessage}{enter}`);
|
||||
|
||||
// * Post is not returned by search
|
||||
cy.get('#searchContainer').should('be.visible');
|
||||
@@ -109,7 +111,8 @@ describe('archive tests while preventing viewing archived channels', () => {
|
||||
cy.postMessage(messageText);
|
||||
|
||||
// # Search for the string of text from step 1
|
||||
cy.get('#searchBox').click().clear().type(`${messageText}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${messageText}{enter}`);
|
||||
|
||||
// * Post is returned by search, since it's not archived anymore
|
||||
cy.get('#searchContainer').should('be.visible');
|
||||
@@ -127,7 +130,8 @@ describe('archive tests while preventing viewing archived channels', () => {
|
||||
it('MM-T1710 archived channels are not listed on the "in:" autocomplete', () => {
|
||||
// # Archive a channel and make a mental note of the channel name
|
||||
// # Type "in:" and note the list of channels that appear
|
||||
cy.get('#searchBox').click().clear().type(`in:${testChannel.name}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`in:${testChannel.name}`);
|
||||
cy.findByTestId(testChannel.name).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,10 +41,11 @@ describe('Leave an archived channel', () => {
|
||||
cy.get('#channelInfoModalLabel span.icon__archive').should('not.exist');
|
||||
|
||||
// # Search for a post in an archived channel
|
||||
cy.get('#searchBox').click().clear().type(`${testArchivedMessage}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${testArchivedMessage}{enter}`);
|
||||
|
||||
// # Open the archived channel by selecting Jump from search results and then selecting the link to move to the most recent posts in the channel
|
||||
cy.get('#searchContainer').should('be.visible');
|
||||
cy.uiGetSearchContainer().should('be.visible');
|
||||
|
||||
cy.get('a.search-item__jump').first().click();
|
||||
|
||||
@@ -111,10 +112,11 @@ describe('Leave an archived channel', () => {
|
||||
const messageList = Array.from({length: 40}, (_, i) => `${i}. any - ${getRandomId()}`);
|
||||
createArchivedChannel({prefix: 'archived-search-for'}, messageList).then(({name}) => {
|
||||
// # Locate the post in a search
|
||||
cy.get('#searchBox').click().clear().type(`${messageList[1]}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${messageList[1]}{enter}`);
|
||||
|
||||
// # Click jump to open an archive post in permalink view
|
||||
cy.get('#searchContainer').should('be.visible');
|
||||
cy.uiGetSearchContainer().should('be.visible');
|
||||
cy.get('a.search-item__jump').first().click();
|
||||
|
||||
// * Archived channel is opened in permalink view
|
||||
|
||||
@@ -96,7 +96,8 @@ describe('Leave an archived channel', () => {
|
||||
cy.visit(previousChannel);
|
||||
|
||||
// # Search for content from an archived channel
|
||||
cy.get('#searchBox').click().clear().type(`${messageD}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${messageD}{enter}`);
|
||||
|
||||
// # Open the channel from search results
|
||||
cy.get('#searchContainer').should('be.visible');
|
||||
@@ -107,7 +108,8 @@ describe('Leave an archived channel', () => {
|
||||
cy.url().should('contain', `${testTeam.name}/channels/${archivedChannelD.name}`);
|
||||
|
||||
// # Search for content from a different archived channel
|
||||
cy.get('#searchBox').click().clear().type(`${messageC}{enter}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${messageC}{enter}`);
|
||||
|
||||
// # Open the channel from search result by clicking Jump
|
||||
cy.get('#searchContainer').should('be.visible').findByText('Jump').click().wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
@@ -133,7 +133,8 @@ describe('Channel RHS', () => {
|
||||
cy.postMessage(MESSAGES.SMALL);
|
||||
|
||||
// # Enter the search terms and hit enter to start the search
|
||||
cy.get('#searchBox').clear().type(MESSAGES.TINY).type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(MESSAGES.TINY).type('{enter}');
|
||||
|
||||
// * Verify that the search results is opened in RHS
|
||||
verifyRHSisOpenAndHasTitle('Search Results');
|
||||
|
||||
@@ -78,15 +78,18 @@ describe('Verify Accessibility Support in different input fields', () => {
|
||||
});
|
||||
}
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// * Verify Accessibility support in search input
|
||||
cy.get('#searchBox').should('have.attr', 'aria-describedby', 'searchbar-help-popup').and('have.attr', 'aria-label', 'Search').focus();
|
||||
cy.get('#searchbar-help-popup').should('be.visible').and('have.attr', 'role', 'tooltip');
|
||||
cy.uiGetSearchBox().should('have.attr', 'aria-describedby', 'searchHints').and('have.attr', 'aria-label', 'Search');
|
||||
cy.uiGetSearchBox().find('input').focus();
|
||||
cy.get('#searchHints').should('be.visible');
|
||||
|
||||
// # Ensure User list is cached once in UI
|
||||
cy.get('#searchBox').type('from:').wait(TIMEOUTS.FIVE_SEC);
|
||||
cy.uiGetSearchBox().find('input').type('from:').wait(TIMEOUTS.FIVE_SEC);
|
||||
|
||||
// # Trigger the user autocomplete again
|
||||
cy.get('#searchBox').clear().type('from:').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
|
||||
cy.uiGetSearchBox().find('input').clear().type('from:').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
|
||||
|
||||
// * Verify Accessibility Support in search autocomplete
|
||||
verifySearchAutocomplete(2);
|
||||
@@ -100,10 +103,10 @@ describe('Verify Accessibility Support in different input fields', () => {
|
||||
verifySearchAutocomplete(3);
|
||||
|
||||
// # Type the in: filter and ensure channel list is cached once
|
||||
cy.get('#searchBox').clear().type('in:').wait(TIMEOUTS.FIVE_SEC);
|
||||
cy.uiGetSearchBox().find('input').clear().type('in:').wait(TIMEOUTS.FIVE_SEC);
|
||||
|
||||
// # Trigger the channel autocomplete again
|
||||
cy.get('#searchBox').clear().type('in:').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
|
||||
cy.uiGetSearchBox().find('input').clear().type('in:').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
|
||||
|
||||
// * Verify Accessibility Support in search autocomplete
|
||||
verifySearchAutocomplete(2, 'channel');
|
||||
@@ -287,7 +290,7 @@ function getUserMentionAriaLabel(displayName) {
|
||||
}
|
||||
|
||||
function verifySearchAutocomplete(index, type = 'user') {
|
||||
cy.get('#search-autocomplete__popover').find('.suggestion-list__item').eq(index).should('be.visible').and('have.class', 'suggestion--selected').within((el) => {
|
||||
cy.uiGetSearchBox().find('.suggestion-list__item').eq(index).should('be.visible').and('have.class', 'suggestion--selected').within((el) => {
|
||||
if (type === 'user') {
|
||||
cy.get('.suggestion-list__ellipsis').invoke('text').then((text) => {
|
||||
const usernameLength = 12;
|
||||
|
||||
@@ -78,10 +78,10 @@ describe('Guest Account - Guest User Experience', () => {
|
||||
cy.sendDirectMessageToUser(guestUser, 'hello');
|
||||
|
||||
// # Search for the Guest User
|
||||
cy.get('#searchBox').wait(TIMEOUTS.FIVE_SEC).type(`in:${guestUser.username}`);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').wait(TIMEOUTS.FIVE_SEC).type(`in:${guestUser.username}`);
|
||||
|
||||
// * Verify Guest Badge is not displayed at Search auto-complete
|
||||
cy.get('#search-autocomplete__popover').should('be.visible');
|
||||
cy.contains('.suggestion-list__item', guestUser.username).should('be.visible').within(($el) => {
|
||||
cy.wrap($el).find('.Tag').should('not.exist');
|
||||
});
|
||||
|
||||
@@ -212,10 +212,10 @@ describe('Verify Guest User Identification in different screens', () => {
|
||||
|
||||
it('Verify Guest Badge not displayed in Search Autocomplete', () => {
|
||||
// # Search for the Guest User
|
||||
cy.get('#searchBox').type('from:');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').type('from:');
|
||||
|
||||
// * Verify Guest Badge is not displayed at Search auto-complete
|
||||
cy.get('#search-autocomplete__popover').should('be.visible');
|
||||
cy.contains('.suggestion-list__item', guestUser.username).scrollIntoView().should('be.visible').within(($el) => {
|
||||
cy.wrap($el).find('.Tag').should('not.exist');
|
||||
});
|
||||
|
||||
@@ -67,7 +67,8 @@ describe('Incoming webhook', () => {
|
||||
|
||||
cy.postIncomingWebhook({url: incomingWebhook.url, data: payload});
|
||||
|
||||
cy.get('#searchBox').
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
typeWithForce('findme').
|
||||
typeWithForce('{enter}');
|
||||
|
||||
@@ -66,13 +66,13 @@ describe('Upload Files - Settings', () => {
|
||||
});
|
||||
|
||||
// # Click on the search input
|
||||
cy.uiGetSearchBox().click();
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// * Verify search hint does not have File button
|
||||
cy.get('#searchbar-help-popup').find('.search-hint__search-type-selector button > .icon-file-text-outline').should('not.exist');
|
||||
cy.get('#searchHints').find('.search-hint__search-type-selector button > .icon-file-text-outline').should('not.exist');
|
||||
|
||||
// # Search for posts
|
||||
cy.get('#searchBox').type('sample').type('{enter}');
|
||||
cy.uiGetSearchBox().find('input').type('sample').type('{enter}');
|
||||
|
||||
// * Verify search results do not have File button
|
||||
cy.get('.files-tab').should('not.exist');
|
||||
|
||||
@@ -42,7 +42,8 @@ describe('Keyboard Shortcuts', () => {
|
||||
|
||||
it('MM-T1250 CTRL/CMD+SHIFT+L - Set focus to center channel message box (with SEARCH RHS open)', () => {
|
||||
// # Search
|
||||
cy.get('#searchBox').click().type('test{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').type('test{enter}');
|
||||
|
||||
// * Wait for the RHS to open and the search results to appear
|
||||
cy.contains('.sidebar--right__header', 'Search Results').should('be.visible');
|
||||
|
||||
@@ -370,11 +370,12 @@ describe('Keyboard Shortcuts', () => {
|
||||
|
||||
it('MM-T1248 - CTRL/CMD+SHIFT+L - Set focus to center channel message box', () => {
|
||||
// # Open search box to change focus
|
||||
cy.get('#searchBox').click().should('be.focused').then(() => {
|
||||
// # Type CTRL/CMD+SHIFT+L
|
||||
cy.get('body').cmdOrCtrlShortcut('{shift}L');
|
||||
cy.uiGetPostTextBox().should('be.focused');
|
||||
});
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('be.focused');
|
||||
|
||||
// # Type CTRL/CMD+SHIFT+L
|
||||
cy.get('body').cmdOrCtrlShortcut('{shift}L');
|
||||
cy.uiGetPostTextBox().should('be.focused');
|
||||
|
||||
// # Post a message and open RHS
|
||||
const message = `hello${Date.now()}`;
|
||||
|
||||
@@ -718,7 +718,8 @@ describe('Messaging', () => {
|
||||
|
||||
cy.getLastPostId().then((postId) => {
|
||||
// # Search for the posted message
|
||||
cy.get('#searchBox').should('be.visible').type(messageX).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('be.visible').type(messageX).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Click on post dot menu so we can edit
|
||||
cy.clickPostDotMenu(postId, 'SEARCH');
|
||||
|
||||
@@ -54,7 +54,8 @@ describe('Message permalink', () => {
|
||||
}
|
||||
|
||||
// # Search for a message in the current channel
|
||||
cy.get('#searchBox').clear().type('in:town-square').type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type('in:town-square').wait(TIMEOUTS.HALF_SEC).type('{enter}{enter}');
|
||||
|
||||
// # Jump to first permalink view (most recent message)
|
||||
cy.get('.search-item__jump').first().click();
|
||||
@@ -71,7 +72,8 @@ describe('Message permalink', () => {
|
||||
});
|
||||
|
||||
// # Search for a message in the current channel
|
||||
cy.get('#searchBox').clear().type('in:town-square').type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').clear().type('in:town-square').wait(TIMEOUTS.HALF_SEC).type('{enter}{enter}');
|
||||
|
||||
// # Jump to first permalink view (most recent message)
|
||||
cy.get('.search-item__jump').first().click();
|
||||
|
||||
@@ -40,7 +40,8 @@ describe('Permalink message edit', () => {
|
||||
cy.postMessage(searchWord);
|
||||
|
||||
// # Search for searchWord
|
||||
cy.get('#searchBox').type(searchWord).type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').type(searchWord).type('{enter}');
|
||||
|
||||
// # Jump to permalink view
|
||||
cy.get('.search-item__jump').first().click();
|
||||
@@ -69,7 +70,8 @@ describe('Permalink message edit', () => {
|
||||
cy.postMessage('hello');
|
||||
|
||||
// # Find searchWord and verify edited post
|
||||
cy.get('#searchBox').should('be.visible').type(searchWord).type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('be.visible').type(searchWord).type('{enter}');
|
||||
cy.get('.search-item__jump').first().click();
|
||||
|
||||
// # Check if url include the permalink
|
||||
|
||||
@@ -186,7 +186,8 @@ describe('Post PreHeader', () => {
|
||||
});
|
||||
|
||||
// # Search for the channel.
|
||||
cy.get('#searchBox').type('test both pinned and saved {enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').type('test both pinned and saved {enter}');
|
||||
|
||||
// * Check that the post pre-header has both pinned and saved links in RHS search results
|
||||
cy.get('#searchContainer').should('be.visible').within(() => {
|
||||
|
||||
@@ -67,8 +67,10 @@ describe('Notifications', () => {
|
||||
});
|
||||
|
||||
// # Enter "in:town-square" in the search bar and hit ENTER
|
||||
cy.get('#searchBox').
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').
|
||||
typeWithForce('in:town-square').
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
typeWithForce('{enter}').
|
||||
typeWithForce('{enter}');
|
||||
|
||||
|
||||
@@ -25,11 +25,13 @@ describe('Search', () => {
|
||||
// * X should not be visible on empty input
|
||||
cy.uiGetSearchContainer().find('.input-clear-x').should('not.exist');
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Write something on the input
|
||||
cy.uiGetSearchBox().clear().type('abc');
|
||||
cy.uiGetSearchBox().find('input').clear().type('abc');
|
||||
|
||||
// * The input should contain what we wrote
|
||||
cy.uiGetSearchBox().should('have.value', 'abc');
|
||||
cy.uiGetSearchBox().find('input').should('have.value', 'abc');
|
||||
|
||||
// * The X should be visible
|
||||
// # Then click X to clear the input field
|
||||
@@ -50,7 +52,7 @@ describe('Search', () => {
|
||||
|
||||
// * Verify search input field exists and not search button, as inputs contains placeholder not buttons/icons
|
||||
// and then type in a search text
|
||||
cy.uiGetSearchBox().click().wait(TIMEOUTS.HALF_SEC).type(searchText);
|
||||
cy.uiGetSearchBox().click().wait(TIMEOUTS.HALF_SEC).type(searchText + '{enter}');
|
||||
|
||||
// # Click on the pinned post button from the header
|
||||
cy.uiGetChannelPinButton().click();
|
||||
|
||||
@@ -29,9 +29,11 @@ describe('Search', () => {
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().
|
||||
type(`${term}{enter}`).
|
||||
wait(TIMEOUTS.ONE_SEC).
|
||||
clear();
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
wait(TIMEOUTS.ONE_SEC);
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().clear();
|
||||
|
||||
cy.get('#searchHints').should('be.visible');
|
||||
cy.uiGetSearchContainer().type('{esc}');
|
||||
|
||||
// # Verify the Search side bar opens up
|
||||
@@ -43,13 +45,17 @@ describe('Search', () => {
|
||||
cy.uiGetRHS({visible: false});
|
||||
|
||||
// # Verify that the cleared search text does not appear on the search box
|
||||
cy.uiGetSearchBox().should('be.empty');
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').should('be.empty');
|
||||
cy.uiGetSearchContainer().type('{esc}');
|
||||
|
||||
// # Click the pin icon to open the pinned messages RHS
|
||||
cy.uiGetChannelPinButton().click();
|
||||
cy.uiGetRHS().should('contain', 'Pinned messages');
|
||||
|
||||
// # Verify that the Search term input box is still cleared and search term does not reappear when RHS opens
|
||||
cy.uiGetSearchBox().and('be.empty');
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').and('be.empty');
|
||||
cy.uiGetSearchContainer().type('{esc}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,8 +36,11 @@ describe('Search', () => {
|
||||
// # Post a message
|
||||
cy.postMessage(testSearch);
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Search the world 'hello' that is generated by /test command
|
||||
cy.get('#searchBox').click().type('hello{enter}').wait(TIMEOUTS.HALF_SEC).should('have.value', 'hello');
|
||||
cy.uiGetSearchBox().find('input').type('hello{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#search-items-container span').should('contain.text', 'hello');
|
||||
|
||||
// # RHS should be visible with search results
|
||||
cy.get('#search-items-container').should('be.visible');
|
||||
@@ -52,14 +55,10 @@ describe('Search', () => {
|
||||
// # Jump to conversation
|
||||
cy.wrap($result).get('a.search-item__jump').first().click();
|
||||
|
||||
// * Search query clear icon is still present
|
||||
cy.get('.input-clear.visible').should('be.visible');
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Hover search query clear icon
|
||||
cy.get('.input-clear-x').first().trigger('mouseover', {force: true}).then(($span) => {
|
||||
// * Assert that tooltip has shown
|
||||
cy.wrap($span).should('have.attr', 'aria-describedby', 'InputClearTooltip');
|
||||
|
||||
// # Click the clear query icon
|
||||
cy.wrap($span).click({force: true});
|
||||
|
||||
|
||||
@@ -37,23 +37,23 @@ describe('Search', () => {
|
||||
// # Post a message
|
||||
cy.postMessage(searchWord);
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// * Search word in searchBox and validate searchWord
|
||||
cy.get('#searchBox').click().type(searchWord + '{enter}').should('have.value', searchWord);
|
||||
cy.uiGetSearchBox().find('input').type(searchWord + '{enter}');
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('have.value', searchWord);
|
||||
|
||||
// # Click on "x" displayed on searchbox
|
||||
cy.get('#searchbarContainer').should('be.visible').within(() => {
|
||||
cy.get('#searchFormContainer').find('.input-clear-x').wait(TIMEOUTS.ONE_SEC).click({force: true});
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
cy.get('#searchBox').type('{esc}');
|
||||
});
|
||||
cy.uiGetSearchBox().find('.input-clear-x').wait(TIMEOUTS.ONE_SEC).click({force: true});
|
||||
cy.uiGetSearchBox().find('#searchHints').should('be.visible');
|
||||
cy.uiGetSearchBox().type('{esc}');
|
||||
|
||||
// # RHS should be visible with search results
|
||||
cy.get('#search-items-container').should('be.visible');
|
||||
|
||||
// # Click on searchbox
|
||||
cy.get('#searchbarContainer').should('be.visible').within(() => {
|
||||
cy.get('#searchBox').click();
|
||||
});
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
assertSearchHintFilesOrMessages();
|
||||
});
|
||||
@@ -74,29 +74,28 @@ describe('Search', () => {
|
||||
// # Visit town-square.
|
||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Search for posts from that user
|
||||
cy.get('#searchBox').click().type(testSearch, {force: true});
|
||||
cy.uiGetSearchBox().find('input').type(testSearch, {force: true}).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Select user from suggestion list
|
||||
cy.contains('.suggestion-list__item', `@${testUser.username}`).scrollIntoView().click({force: true});
|
||||
|
||||
// # Verify that search box has the updated query
|
||||
cy.get('#searchBox').should('have.value', `FROM:${testUser.username} `);
|
||||
cy.uiGetSearchBox().find('input').should('have.value', `FROM:${testUser.username} `);
|
||||
|
||||
// # Perform search
|
||||
cy.get('#searchBox').click().type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchBox().find('input').type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Assert that RHS should be visible with search results
|
||||
cy.get('#search-items-container').should('be.visible');
|
||||
|
||||
// * Search query clear icon is still present
|
||||
cy.get('.input-clear.visible').should('be.visible');
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Hover search query clear icon
|
||||
cy.get('.input-clear-x').first().trigger('mouseover', {force: true}).then(($span) => {
|
||||
// * Assert that tooltip has shown
|
||||
cy.wrap($span).should('have.attr', 'aria-describedby', 'InputClearTooltip');
|
||||
|
||||
// # Click the clear query icon
|
||||
cy.wrap($span).click({force: true});
|
||||
|
||||
@@ -111,15 +110,13 @@ describe('Search', () => {
|
||||
cy.postMessage('hello');
|
||||
|
||||
// # Click on searchbox
|
||||
cy.get('#searchbarContainer').should('be.visible').within(() => {
|
||||
cy.get('input#searchBox').should('be.visible').click();
|
||||
});
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
|
||||
// * Check the contents in search options
|
||||
assertSearchHintFilesOrMessages();
|
||||
|
||||
// # Search for search term in:
|
||||
cy.get('#searchBox').click().type('in:');
|
||||
cy.uiGetSearchBox().find('input').type('in:');
|
||||
|
||||
// # Select option from suggestion list
|
||||
cy.get('.suggestion-list__item').first().click({force: true});
|
||||
@@ -131,31 +128,31 @@ describe('Search', () => {
|
||||
cy.get('.input-clear-x').first().click({force: true}).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Search for search term in:town-square{space}
|
||||
cy.get('#searchBox').click().type('in:town-square ').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchBox().find('input').type('in:town-square ').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Check the hint contents are now visible
|
||||
assertSearchHint();
|
||||
|
||||
// # Clear search box
|
||||
cy.get('.input-clear-x').first().click({force: true}).wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchBox().get('.input-clear-x').first().click({force: true}).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Search for search term in:town-square{enter}
|
||||
cy.get('#searchBox').click().type('in:town-square').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchBox().find('input').type('in:town-square').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Assert that channel name displays appropriately
|
||||
cy.get('.suggestion-list__item').first().should('contain.text', 'Town Square~town-square');
|
||||
|
||||
// # Press enter to register search term
|
||||
cy.get('#searchBox').click().type('{enter}');
|
||||
cy.uiGetSearchBox().find('input').type('{enter}');
|
||||
|
||||
// * Check the hint contents are now visible
|
||||
assertSearchHint();
|
||||
|
||||
// * Assert that searchBox now includes a trailing space
|
||||
cy.get('#searchBox').should('have.value', 'in:town-square ');
|
||||
cy.uiGetSearchBox().find('input').should('have.value', 'in:town-square ');
|
||||
|
||||
// # Perform the search
|
||||
cy.get('#searchBox').click().type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchBox().find('input').type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Assert autocomplete list is gone
|
||||
cy.get('.suggestion-list__item').should('not.exist');
|
||||
@@ -167,8 +164,10 @@ describe('Search', () => {
|
||||
// # Post message
|
||||
cy.postMessage(testMessage);
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// # Search for `Hell*`
|
||||
cy.get('input#searchBox').click().type('Hell*{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
cy.uiGetSearchBox().find('input').type('Hell*{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # RHS should be visible with search results
|
||||
cy.get('#search-items-container').should('be.visible');
|
||||
@@ -180,22 +179,9 @@ describe('Search', () => {
|
||||
});
|
||||
|
||||
const assertSearchHintFilesOrMessages = () => {
|
||||
cy.get('#searchbar-help-popup').should('be.visible').within(() => {
|
||||
cy.get('div span').first().should('have.text', 'What are you searching for?');
|
||||
cy.get('div button:first-child span').first().should('have.text', 'Messages');
|
||||
cy.get('div button:last-child span').first().should('have.text', 'Files');
|
||||
});
|
||||
cy.get('#searchHints').should('be.visible');
|
||||
};
|
||||
|
||||
const assertSearchHint = () => {
|
||||
cy.get('#searchbar-help-popup').should('be.visible').within(() => {
|
||||
cy.get('div span').first().should('have.text', 'Search options');
|
||||
cy.get('div ul li').first().should('have.text', 'From:Messages from a user');
|
||||
cy.get('div ul li').eq(1).should('have.text', 'In:Messages in a channel');
|
||||
cy.get('div ul li').eq(2).should('have.text', 'On:Messages on a date');
|
||||
cy.get('div ul li').eq(3).should('have.text', 'Before:Messages before a date');
|
||||
cy.get('div ul li').eq(4).should('have.text', 'After:Messages after a date');
|
||||
cy.get('div ul li').eq(5).should('have.text', '—Exclude search terms');
|
||||
cy.get('div ul li').last().should('have.text', '""Messages with phrases');
|
||||
});
|
||||
cy.get('#searchHints').should('be.visible');
|
||||
};
|
||||
|
||||
@@ -36,7 +36,8 @@ describe('Search', () => {
|
||||
cy.postMessage(message);
|
||||
|
||||
// # Search for "apple"
|
||||
cy.get('#searchBox').should('be.visible').type(apple).type('{enter}');
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().find('input').should('be.visible').type(apple).type('{enter}');
|
||||
|
||||
// # Get last postId
|
||||
cy.getLastPostId().as('lastPostId');
|
||||
@@ -46,8 +47,10 @@ describe('Search', () => {
|
||||
verifySearchResult(1, postId, message, apple);
|
||||
});
|
||||
|
||||
cy.uiGetSearchContainer().click();
|
||||
|
||||
// * Type banana on search box but don't hit search
|
||||
cy.get('#searchBox').clear({force: true}).type(banana, {force: true});
|
||||
cy.uiGetSearchBox().find('input').clear({force: true}).type(banana, {force: true});
|
||||
|
||||
// * Search result should not change and remain as one result with highlight still on apple
|
||||
cy.get('@lastPostId').then((postId) => {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a page)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element ID when selecting an element. Create one if none.
|
||||
// ***************************************************************
|
||||
|
||||
// Stage: @prod
|
||||
// Group: @channels @search
|
||||
|
||||
describe('Search', () => {
|
||||
before(() => {
|
||||
// # Login as test user and visit off-topic
|
||||
cy.apiInitSetup({loginAfter: true}).then(({offTopicUrl}) => {
|
||||
cy.visit(offTopicUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T4945_1 - Search bar popup should be visible when focus changes to one of its buttons', () => {
|
||||
//# clicks on the search box
|
||||
cy.uiGetSearchBox().click();
|
||||
|
||||
//* search bar popup should be visibl
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
|
||||
//# moves the focus to the next button (in this case Messages)
|
||||
cy.focused().tab();
|
||||
|
||||
//* search bar popup should be visible
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T4945_2 - Search bar popup should be hidden when focus is out of search bar items', () => {
|
||||
//# clicks on the search box
|
||||
cy.uiGetSearchBox().click();
|
||||
|
||||
//* search bar popup should be visibl
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
|
||||
//# press tab three times to move focus out of the search bar componment
|
||||
cy.focused().tab();
|
||||
cy.focused().tab();
|
||||
cy.focused().tab();
|
||||
|
||||
//* now the popup should be closed
|
||||
cy.get('#searchbar-help-popup').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('MM-T4945_3 - Search bar popup should be open when focus back to search box', () => {
|
||||
//# clicks on the search box
|
||||
cy.uiGetSearchBox().click();
|
||||
|
||||
//* search bar popup should be visibl
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
|
||||
//# moves the focus to the next button (in this case Messages)
|
||||
cy.focused().tab();
|
||||
|
||||
//* search bar popup should be visible
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
|
||||
//# moves the focus back to the search box
|
||||
cy.focused().tab({shift: true});
|
||||
|
||||
//* search bar popup should still be visible
|
||||
cy.get('#searchbar-help-popup').should('be.visible');
|
||||
});
|
||||
});
|
||||
@@ -51,25 +51,24 @@ describe('Search', () => {
|
||||
// # Post a message
|
||||
cy.postMessage(message);
|
||||
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
|
||||
//# Type "in:" text in search input
|
||||
cy.get('#searchBox').type('in:');
|
||||
cy.uiGetSearchBox().find('input').type('in:');
|
||||
|
||||
const sortedUsernames = groupMembers.
|
||||
map((member) => member.username).
|
||||
sort((a, b) => a.localeCompare(b, 'en', {numeric: true}));
|
||||
|
||||
//# Search group members in the menu
|
||||
cy.get('#search-autocomplete__popover').should('be.visible').within(() => {
|
||||
cy.findAllByTestId('listItem').contains(sortedUsernames.join(',')).click();
|
||||
});
|
||||
cy.uiGetSearchBox().find('.suggestion-list__main').contains(sortedUsernames.join(',')).click();
|
||||
|
||||
//# Press enter to select
|
||||
cy.get('#searchBox').type('{enter}');
|
||||
cy.uiGetSearchBox().find('input').type('{enter}');
|
||||
|
||||
//# Search for the message
|
||||
cy.get('#searchbarContainer').should('be.visible').within(() => {
|
||||
cy.get('#searchBox').clear().type(`${message}{enter}`);
|
||||
});
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').clear().type(`${message}{enter}`);
|
||||
|
||||
// * Should return exactly one result from the group channel and matches the message
|
||||
cy.findAllByTestId('search-item-container').should('be.visible').and('have.length', 1).within(() => {
|
||||
@@ -89,25 +88,20 @@ describe('Search', () => {
|
||||
cy.get('.post-image__thumbnail').should('be.visible');
|
||||
cy.uiGetPostTextBox().clear().type('{enter}');
|
||||
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
|
||||
//# Type "in:" text in search input
|
||||
cy.get('#searchBox').type('in:');
|
||||
cy.uiGetSearchBox().find('input').type('in:');
|
||||
|
||||
const sortedUsernames = groupMembers.
|
||||
map((member) => member.username).
|
||||
sort((a, b) => a.localeCompare(b, 'en', {numeric: true}));
|
||||
|
||||
//# Search group members in the menu
|
||||
cy.get('#search-autocomplete__popover').should('be.visible').within(() => {
|
||||
cy.findAllByTestId('listItem').contains(sortedUsernames.join(',')).click();
|
||||
});
|
||||
|
||||
//# Press enter to select
|
||||
cy.get('#searchBox').type('{enter}');
|
||||
cy.uiGetSearchBox().find('.suggestion-list__main').contains(sortedUsernames.join(',')).click();
|
||||
|
||||
//# Search for the message
|
||||
cy.get('#searchbarContainer').should('be.visible').within(() => {
|
||||
cy.get('#searchBox').type(' word-file{enter}');
|
||||
});
|
||||
cy.uiGetSearchBox().find('input').type('word-file{enter}');
|
||||
|
||||
// # Click the files tab
|
||||
cy.get('.files-tab').should('be.visible').click();
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
// Group: @channels @search @smoke @filesearch
|
||||
|
||||
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
||||
|
||||
/**
|
||||
* create new DM channel
|
||||
* @param {String} text - DM channel name
|
||||
@@ -19,7 +21,7 @@ function createNewDMChannel(channelname) {
|
||||
cy.get('#selectItems input').typeWithForce(channelname);
|
||||
|
||||
cy.contains('.more-modal__description', channelname).click({force: true});
|
||||
cy.get('#saveItems').click();
|
||||
cy.get('#saveItems').click().wait(TIMEOUTS.ONE_SEC);
|
||||
}
|
||||
|
||||
describe('Search in DMs', () => {
|
||||
@@ -57,17 +59,19 @@ describe('Search in DMs', () => {
|
||||
cy.get('.post-image__thumbnail').should('be.visible');
|
||||
cy.uiGetPostTextBox().clear().type('{enter}');
|
||||
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
|
||||
// # Type `in:` in searchbox
|
||||
cy.get('#searchBox').type('in:');
|
||||
cy.uiGetSearchBox().find('input').type('in:');
|
||||
|
||||
// # Select user from suggestion list
|
||||
cy.contains('.suggestion-list__item', `@${otherUser.username}`).scrollIntoView().click();
|
||||
|
||||
// # Validate searchbox contains the username
|
||||
cy.get('#searchBox').should('have.value', 'in:@' + otherUser.username + ' ');
|
||||
cy.uiGetSearchBox().find('input').should('have.value', 'in:@' + otherUser.username + ' ');
|
||||
|
||||
// # Press Enter in searchbox
|
||||
cy.get('#searchBox').type('word-file.doc').type('{enter}');
|
||||
cy.uiGetSearchBox().find('input').type('word-file').type('{enter}');
|
||||
|
||||
// # Click the files tab
|
||||
cy.get('.files-tab').should('be.visible').click();
|
||||
|
||||
@@ -59,16 +59,17 @@ describe('Search in DMs', () => {
|
||||
cy.postMessage(message);
|
||||
|
||||
// # Type `in:` in searchbox
|
||||
cy.get('#searchBox').type('in:');
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').type('in:');
|
||||
|
||||
// # Select user from suggestion list
|
||||
cy.contains('.suggestion-list__item', `@${otherUser.username}`).scrollIntoView().click();
|
||||
|
||||
// # Validate searchbox contains the username
|
||||
cy.get('#searchBox').should('have.value', 'in:@' + otherUser.username + ' ');
|
||||
cy.uiGetSearchBox().find('input').should('have.value', 'in:@' + otherUser.username + ' ');
|
||||
|
||||
// # Press Enter in searchbox
|
||||
cy.get('#searchBox').type(message).type('{enter}');
|
||||
cy.uiGetSearchBox().find('input').type(message).type('{enter}');
|
||||
|
||||
// # Search message in each filtered result
|
||||
cy.get('#search-items-container').find('.search-highlight').each(($el) => {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a page)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element ID when selecting an element. Create one if none.
|
||||
// ***************************************************************
|
||||
|
||||
// Stage: @prod
|
||||
// Group: @channels @autocomplete @search
|
||||
|
||||
describe('Autocomplete in the search box - scrolling', () => {
|
||||
const usersCount = 15;
|
||||
const timestamp = Date.now();
|
||||
|
||||
before(() => {
|
||||
// # Create new team for tests
|
||||
cy.apiCreateTeam(`search-${timestamp}`, `search-${timestamp}`).then(({team}) => {
|
||||
// # Create pool of users for tests
|
||||
for (let i = 0; i < usersCount; i++) {
|
||||
cy.apiCreateUser().then(({user}) => {
|
||||
cy.apiAddUserToTeam(team.id, user.id);
|
||||
});
|
||||
}
|
||||
cy.visit(`/${team.name}/channels/off-topic`);
|
||||
|
||||
// # Post a new message to ensure page fully rendered before acting into the searchBox
|
||||
cy.postMessage('hello');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T4084 correctly scrolls when the user navigates through options with the keyboard', () => {
|
||||
// # Type into the searchBox to show list of users
|
||||
cy.get('#searchBox').type('from:');
|
||||
|
||||
cy.get('#search-autocomplete__popover .suggestion-list__item').first().as('firstItem');
|
||||
cy.get('#search-autocomplete__popover .suggestion-list__item').last().as('lastItem');
|
||||
|
||||
// * Check that list is scrolled to top
|
||||
cy.get('@firstItem').should('be.visible');
|
||||
cy.get('@lastItem').should('not.be.visible');
|
||||
|
||||
// # Move to bottom of the list using keyboard
|
||||
cy.get('body').type('{downarrow}'.repeat(usersCount));
|
||||
|
||||
// * Check that list is scrolled to bottom
|
||||
cy.get('@firstItem').should('not.be.visible');
|
||||
cy.get('@lastItem').should('be.visible');
|
||||
|
||||
// # Move to top of the list using keyboard
|
||||
cy.get('body').type('{uparrow}'.repeat(usersCount));
|
||||
|
||||
// * Check that list is scrolled to top
|
||||
cy.get('@firstItem').should('be.visible');
|
||||
cy.get('@lastItem').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
@@ -12,13 +12,13 @@ import {getRandomId} from '../../../utils';
|
||||
|
||||
export function searchAndValidate(query, expectedResults = []) {
|
||||
// # Enter in search query, and hit enter
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().
|
||||
clear().
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
type(query).
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
type('{enter}').
|
||||
should('be.empty');
|
||||
type('{enter}');
|
||||
|
||||
cy.get('#loadingSpinner').should('not.exist');
|
||||
|
||||
|
||||
@@ -53,49 +53,53 @@ describe('Search Date Filter', () => {
|
||||
const today = Cypress.dayjs().format('YYYY-MM-DD');
|
||||
|
||||
// # Type before: in search field
|
||||
cy.get('#searchBox').clear().type('before:');
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').clear().type('before:');
|
||||
|
||||
// * Day picker should be visible
|
||||
cy.get('.DayPicker').
|
||||
cy.get('.rdp').
|
||||
as('dayPicker').
|
||||
should('be.visible');
|
||||
|
||||
// # Select today's day
|
||||
cy.get('@dayPicker').
|
||||
find('.DayPicker-Day--today').click();
|
||||
find('.rdp-day_today').click();
|
||||
|
||||
cy.get('@dayPicker').should('not.exist');
|
||||
|
||||
// * Verify date picker output gets put into field as expected date
|
||||
cy.get('#searchBox').should('have.value', `before:${today} `);
|
||||
cy.uiGetSearchBox().find('input').should('have.value', `before:${today} `);
|
||||
|
||||
// # Click "x" to the right of the search term
|
||||
cy.get('#searchFormContainer').find('.input-clear-x').click({force: true});
|
||||
cy.uiGetSearchBox().find('.input-clear-x').click({force: true});
|
||||
|
||||
// * The "x" to clear the search query has disappeared
|
||||
cy.get('#searchBox').should('have.value', '');
|
||||
cy.uiGetSearchBox().find('input').should('have.value', '');
|
||||
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
});
|
||||
|
||||
it('MM-T3997 Backspace after last character of filter makes calendar reappear', () => {
|
||||
const today = Cypress.dayjs().format('YYYY-MM-DD');
|
||||
|
||||
// # Type before: in search field
|
||||
cy.get('#searchBox').clear().type('before:');
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').clear().type('before:');
|
||||
|
||||
// * Date picker should be visible
|
||||
cy.get('.DayPicker').
|
||||
cy.get('.rdp').
|
||||
as('dayPicker').
|
||||
should('be.visible');
|
||||
|
||||
// # Select today's day
|
||||
cy.get('@dayPicker').
|
||||
find('.DayPicker-Day--today').click();
|
||||
find('.rdp-day_today').click();
|
||||
|
||||
// * Date picker should disappear
|
||||
cy.get('@dayPicker').should('not.exist');
|
||||
|
||||
// # Hit backspace with focus right after the date
|
||||
cy.get('#searchBox').
|
||||
cy.uiGetSearchBox().find('input').
|
||||
should('have.value', `before:${today} `).
|
||||
focus().
|
||||
type('{backspace}');
|
||||
@@ -122,7 +126,8 @@ describe('Search Date Filter', () => {
|
||||
const queryString = `on:${Cypress.dayjs().format('YYYY-MM-DD')} ${commonText}`;
|
||||
|
||||
// * Filter can be removed with keyboard
|
||||
cy.get('#searchBox').
|
||||
cy.uiGetSearchContainer().should('be.visible').click();
|
||||
cy.uiGetSearchBox().find('input').
|
||||
clear().
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
type(queryString).
|
||||
@@ -130,10 +135,10 @@ describe('Search Date Filter', () => {
|
||||
should('have.value', '');
|
||||
|
||||
// # Enter query to search box and then click "x" to the right of the search term
|
||||
cy.get('#searchBox').clear().wait(TIMEOUTS.HALF_SEC).type(queryString);
|
||||
cy.get('#searchFormContainer').find('.input-clear-x').click({force: true});
|
||||
cy.uiGetSearchBox().find('input').clear().wait(TIMEOUTS.HALF_SEC).type(queryString);
|
||||
cy.uiGetSearchBox().find('.input-clear-x').click({force: true});
|
||||
|
||||
// * The "x" to clear the search query has disappeared
|
||||
cy.get('#searchBox').should('have.value', '');
|
||||
cy.uiGetSearchBox().find('input').should('have.value', '');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ import {ChainableT} from 'tests/types';
|
||||
|
||||
function uiSearchPosts(searchTerm: string) {
|
||||
// # Enter the search terms and hit enter to start the search
|
||||
cy.get('#searchBox').clear().type(searchTerm).type('{enter}');
|
||||
cy.get('#searchFormContainer').should('be.visible').click();
|
||||
cy.get('#searchBox').find('input').clear().type(searchTerm).type('{enter}');
|
||||
|
||||
// * Wait for the RHS to open and the search results to appear
|
||||
cy.contains('.sidebar--right__header', 'Search Results').should('be.visible');
|
||||
|
||||
@@ -129,6 +129,7 @@ const initialState = {
|
||||
rhs: {
|
||||
rhsState: null,
|
||||
searchTerms: '',
|
||||
searchType: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -150,6 +151,7 @@ describe('executeCommand', () => {
|
||||
{type: 'UPDATE_RHS_SEARCH_TERMS', terms: 'foo bar'},
|
||||
{type: 'UPDATE_RHS_STATE', state: 'search'},
|
||||
{type: 'UPDATE_RHS_SEARCH_RESULTS_TERMS', terms: ''},
|
||||
{type: 'UPDATE_RHS_SEARCH_RESULTS_TYPE', searchType: ''},
|
||||
{type: 'SEARCH_POSTS_REQUEST', isGettingMore: false},
|
||||
{type: 'SEARCH_FILES_REQUEST', isGettingMore: false},
|
||||
]);
|
||||
|
||||
@@ -192,6 +192,7 @@ describe('Actions.Posts', () => {
|
||||
},
|
||||
rhs: {
|
||||
searchTerms: '',
|
||||
searchType: '',
|
||||
filesSearchExtFilter: [],
|
||||
},
|
||||
},
|
||||
@@ -344,6 +345,7 @@ describe('Actions.Posts', () => {
|
||||
{terms: 'hello', type: 'UPDATE_RHS_SEARCH_TERMS'},
|
||||
{state: 'search', type: 'UPDATE_RHS_STATE'},
|
||||
{terms: '', type: 'UPDATE_RHS_SEARCH_RESULTS_TERMS'},
|
||||
{searchType: '', type: 'UPDATE_RHS_SEARCH_RESULTS_TYPE'},
|
||||
{isGettingMore: false, type: 'SEARCH_POSTS_REQUEST'},
|
||||
{isGettingMore: false, type: 'SEARCH_FILES_REQUEST'},
|
||||
]);
|
||||
|
||||
@@ -125,6 +125,7 @@ describe('rhs view actions', () => {
|
||||
rhs: {
|
||||
rhsState: null,
|
||||
filesSearchExtFilter: [] as string[],
|
||||
searchType: '',
|
||||
},
|
||||
posts: {
|
||||
editingPost: {
|
||||
@@ -219,7 +220,12 @@ describe('rhs view actions', () => {
|
||||
|
||||
describe('performSearch', () => {
|
||||
// timezone offset in seconds
|
||||
const timeZoneOffset = getBrowserUtcOffset() * 60;
|
||||
let timeZoneOffset = getBrowserUtcOffset() * 60;
|
||||
|
||||
// Avoid problems with negative cero
|
||||
if (timeZoneOffset === 0) {
|
||||
timeZoneOffset = 0;
|
||||
}
|
||||
|
||||
test('it dispatches searchPosts correctly', () => {
|
||||
const terms = '@here test search';
|
||||
@@ -276,6 +282,7 @@ describe('rhs view actions', () => {
|
||||
views: {
|
||||
rhs: {
|
||||
searchTerms: terms,
|
||||
searchType: 'messages',
|
||||
filesSearchExtFilter: [] as string[],
|
||||
},
|
||||
},
|
||||
@@ -292,6 +299,10 @@ describe('rhs view actions', () => {
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS,
|
||||
terms,
|
||||
});
|
||||
compareStore.dispatch({
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||
searchType: 'messages',
|
||||
});
|
||||
compareStore.dispatch(performSearch(terms));
|
||||
|
||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||
@@ -861,6 +872,7 @@ describe('rhs view actions', () => {
|
||||
views: {
|
||||
rhs: {
|
||||
searchTerms: terms,
|
||||
searchType: 'messages',
|
||||
filesSearchExtFilter: [] as string[],
|
||||
},
|
||||
},
|
||||
@@ -875,6 +887,10 @@ describe('rhs view actions', () => {
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS,
|
||||
terms,
|
||||
});
|
||||
compareStore.dispatch({
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||
searchType: 'messages',
|
||||
});
|
||||
compareStore.dispatch(performSearch(terms));
|
||||
|
||||
expect(store.getActions()).toEqual(compareStore.getActions());
|
||||
|
||||
@@ -26,7 +26,14 @@ import {getCurrentUser, getCurrentUserMentionKeys} from 'mattermost-redux/select
|
||||
import type {ActionFunc, ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
import {getSearchTerms, getRhsState, getPluggableId, getFilesSearchExtFilter, getPreviousRhsState} from 'selectors/rhs';
|
||||
import {
|
||||
getSearchType,
|
||||
getSearchTerms,
|
||||
getRhsState,
|
||||
getPluggableId,
|
||||
getFilesSearchExtFilter,
|
||||
getPreviousRhsState,
|
||||
} from 'selectors/rhs';
|
||||
|
||||
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||
|
||||
@@ -187,6 +194,13 @@ function updateSearchResultsTerms(terms: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function updateSearchResultsType(searchType: string) {
|
||||
return {
|
||||
type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE,
|
||||
searchType,
|
||||
};
|
||||
}
|
||||
|
||||
export function performSearch(terms: string, isMentionSearch?: boolean): ThunkActionFunc<unknown, GlobalState> {
|
||||
return (dispatch, getState) => {
|
||||
let searchTerms = terms;
|
||||
@@ -240,6 +254,7 @@ export function showSearchResults(isMentionSearch = false): ThunkActionFunc<unkn
|
||||
const state = getState();
|
||||
|
||||
const searchTerms = getSearchTerms(state);
|
||||
const searchType = getSearchType(state);
|
||||
|
||||
if (isMentionSearch) {
|
||||
dispatch(updateRhsState(RHSStates.MENTION));
|
||||
@@ -247,6 +262,7 @@ export function showSearchResults(isMentionSearch = false): ThunkActionFunc<unkn
|
||||
dispatch(updateRhsState(RHSStates.SEARCH));
|
||||
}
|
||||
dispatch(updateSearchResultsTerms(searchTerms));
|
||||
dispatch(updateSearchResultsType(searchType));
|
||||
|
||||
return dispatch(performSearch(searchTerms));
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import Flex from '@mattermost/compass-components/utilities/layout/Flex'; // esli
|
||||
import {closeRightHandSide, showMentions} from 'actions/views/rhs';
|
||||
import {getRhsState} from 'selectors/rhs';
|
||||
|
||||
import Search from 'components/search';
|
||||
import NewSearch from 'components/new_search/new_search';
|
||||
|
||||
import {
|
||||
Constants,
|
||||
@@ -50,9 +50,7 @@ const GlobalSearchNav = (): JSX.Element => {
|
||||
flex={1}
|
||||
alignment='center'
|
||||
>
|
||||
<Search
|
||||
enableFindShortcut={true}
|
||||
/>
|
||||
<NewSearch/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import ExtensionSuggestion from './extension_suggestions';
|
||||
|
||||
describe('components/new_search/ExtensionSuggestion', () => {
|
||||
const baseProps = {
|
||||
item: {type: 'test-type', label: 'test-label', value: 'test-value'},
|
||||
term: 'test',
|
||||
matchedPretext: 'test',
|
||||
isSelection: false,
|
||||
onClick: jest.fn(),
|
||||
onMouseMove: jest.fn(),
|
||||
};
|
||||
|
||||
test('should show the file-type as label and the value as extension whenever is not a know filetype', () => {
|
||||
renderWithContext(<ExtensionSuggestion {...baseProps}/>);
|
||||
expect(screen.getByText('test-type')).toBeInTheDocument();
|
||||
expect(screen.getByText('(.test-value)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the name of the type of file whenever it knows the filetype', () => {
|
||||
const props = {...baseProps, item: {...baseProps.item, type: 'pdf', value: 'pdf'}};
|
||||
renderWithContext(<ExtensionSuggestion {...props}/>);
|
||||
expect(screen.getByText('Acrobat')).toBeInTheDocument();
|
||||
expect(screen.getByText('(.pdf)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should pass the right data on click', () => {
|
||||
const props = {...baseProps};
|
||||
renderWithContext(<ExtensionSuggestion {...props}/>);
|
||||
screen.getByText('test-type').click();
|
||||
expect(props.onClick).toHaveBeenCalledWith('test-value', 'test');
|
||||
});
|
||||
|
||||
test('should have selected class whenever is selected', () => {
|
||||
const props = {...baseProps, isSelection: true};
|
||||
renderWithContext(<ExtensionSuggestion {...props}/>);
|
||||
expect(screen.getByText('test-type')).toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
131
webapp/channels/src/components/new_search/extension_suggestions.tsx
Обычный файл
131
webapp/channels/src/components/new_search/extension_suggestions.tsx
Обычный файл
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback} from 'react';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {useIntl, defineMessages} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {SuggestionProps} from 'components/suggestion/suggestion';
|
||||
|
||||
import {getCompassIconClassName} from 'utils/utils';
|
||||
|
||||
import type {ExtensionItem} from './extension_suggestions_provider';
|
||||
|
||||
const SearchFileExtensionSuggestionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 2.4rem;
|
||||
&.selected,
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
background-size: 16px 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.icon-file-excel-outline {
|
||||
color: #339970;
|
||||
}
|
||||
&.icon-file-powerpoint-outline {
|
||||
color: #e07315;
|
||||
}
|
||||
&.icon-file-pdf-outline {
|
||||
color: #c43133;
|
||||
}
|
||||
&.icon-file-image-outline,
|
||||
&.icon-file-audio-outline,
|
||||
&.icon-file-video-outline,
|
||||
&.icon-file-word-outline {
|
||||
color: #5d89ea;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ExtensionText = styled.span`
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
const messages: Record<string, MessageDescriptor> =
|
||||
defineMessages({
|
||||
pdf: {
|
||||
id: 'file_type.pdf',
|
||||
defaultMessage: 'Acrobat',
|
||||
},
|
||||
word: {
|
||||
id: 'file_type.word',
|
||||
defaultMessage: 'Word Document',
|
||||
},
|
||||
image: {
|
||||
id: 'file_type.image',
|
||||
defaultMessage: 'Image',
|
||||
},
|
||||
audio: {
|
||||
id: 'file_type.audio',
|
||||
defaultMessage: 'Audio',
|
||||
},
|
||||
video: {
|
||||
id: 'file_type.video',
|
||||
defaultMessage: 'Video',
|
||||
},
|
||||
presentation: {
|
||||
id: 'file_type.presentation',
|
||||
defaultMessage: 'Powerpoint Presentation',
|
||||
},
|
||||
spreadsheet: {
|
||||
id: 'file_type.spreadsheet',
|
||||
defaultMessage: 'Excel spreadsheet',
|
||||
},
|
||||
code: {
|
||||
id: 'file_type.code',
|
||||
defaultMessage: 'Code file',
|
||||
},
|
||||
patch: {
|
||||
id: 'file_type.patch',
|
||||
defaultMessage: 'Patch file',
|
||||
},
|
||||
svg: {
|
||||
id: 'file_type.svg',
|
||||
defaultMessage: 'Vector graphics',
|
||||
},
|
||||
text: {
|
||||
id: 'file_type.text',
|
||||
defaultMessage: 'Text file',
|
||||
},
|
||||
});
|
||||
|
||||
const SearchFileExtensionSuggestion = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
SuggestionProps<ExtensionItem>
|
||||
>(({item, onClick, matchedPretext, isSelection}, ref) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const optionClicked = useCallback(() => {
|
||||
onClick(item.value, matchedPretext);
|
||||
}, [onClick, item.value, matchedPretext]);
|
||||
|
||||
const labelName = messages[item.type] ?
|
||||
intl.formatMessage(messages[item.type]) :
|
||||
item.type;
|
||||
|
||||
return (
|
||||
<SearchFileExtensionSuggestionContainer
|
||||
ref={ref}
|
||||
className={classNames({selected: isSelection})}
|
||||
onClick={optionClicked}
|
||||
>
|
||||
<div className={classNames('file-icon', getCompassIconClassName(item.type))}/>
|
||||
{labelName}
|
||||
<ExtensionText>{`(.${item.value})`}</ExtensionText>
|
||||
</SearchFileExtensionSuggestionContainer>
|
||||
);
|
||||
});
|
||||
SearchFileExtensionSuggestion.displayName = 'SearchFileExtensionSuggestion';
|
||||
|
||||
export default SearchFileExtensionSuggestion;
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SearchFileExtensionProvider} from './extension_suggestions_provider';
|
||||
|
||||
describe('components/new_search/SearchFileExtensionProvider', () => {
|
||||
test('should not provide any file extensions whenever there is not pretext with ext: in it', () => {
|
||||
const provider = new SearchFileExtensionProvider();
|
||||
const callback = jest.fn();
|
||||
provider.handlePretextChanged('', callback);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should provide a predetermined file extensions whenever there is not pretext', () => {
|
||||
const provider = new SearchFileExtensionProvider();
|
||||
const callback = jest.fn();
|
||||
provider.handlePretextChanged('ext:', callback);
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
component: expect.any(Object),
|
||||
items: [
|
||||
{label: 'text', type: 'text', value: 'txt'},
|
||||
{label: 'word', type: 'word', value: 'docx'},
|
||||
{label: 'spreadsheet', type: 'spreadsheet', value: 'xlsx'},
|
||||
{label: 'presentation', type: 'presentation', value: 'pptx'},
|
||||
{label: 'pdf', type: 'pdf', value: 'pdf'},
|
||||
{label: 'image', type: 'image', value: 'png'},
|
||||
{label: 'image', type: 'image', value: 'jpg'},
|
||||
{label: 'audio', type: 'audio', value: 'mp3'},
|
||||
{label: 'video', type: 'video', value: 'mp4'},
|
||||
],
|
||||
matchedPretext: '',
|
||||
terms: ['txt', 'docx', 'xlsx', 'pptx', 'pdf', 'png', 'jpg', 'mp3', 'mp4'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should provide a filtered set of file extensions whenever there is pretext', () => {
|
||||
const provider = new SearchFileExtensionProvider();
|
||||
const callback = jest.fn();
|
||||
provider.handlePretextChanged('ext:t', callback);
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
component: expect.any(Object),
|
||||
items: [
|
||||
{label: 'tex', type: 'code', value: 'tex'},
|
||||
{label: 'thor', type: 'code', value: 'thor'},
|
||||
{label: 'tif', type: 'image', value: 'tif'},
|
||||
{label: 'tiff', type: 'image', value: 'tiff'},
|
||||
{label: 'txt', type: 'text', value: 'txt'},
|
||||
],
|
||||
matchedPretext: 't',
|
||||
terms: ['tex', 'thor', 'tif', 'tiff', 'txt'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ResultsCallback} from 'components/suggestion/provider';
|
||||
import Provider from 'components/suggestion/provider';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import SearchFileExtensionSuggestion from './extension_suggestions';
|
||||
|
||||
export type ExtensionItem = {
|
||||
label: string;
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const globalExtensions: ExtensionItem[] = [];
|
||||
Constants.TEXT_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'text', value: extension}));
|
||||
Constants.IMAGE_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'image', value: extension}));
|
||||
Constants.AUDIO_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'audio', value: extension}));
|
||||
Constants.VIDEO_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'video', value: extension}));
|
||||
Constants.PRESENTATION_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'presentation', value: extension}));
|
||||
Constants.SPREADSHEET_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'spreadsheet', value: extension}));
|
||||
Constants.WORD_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'word', value: extension}));
|
||||
Constants.CODE_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'code', value: extension}));
|
||||
Constants.PDF_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'pdf', value: extension}));
|
||||
Constants.PATCH_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'patch', value: extension}));
|
||||
Constants.SVG_TYPES.forEach((extension) => globalExtensions.push({label: extension, type: 'svg', value: extension}));
|
||||
globalExtensions.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
export class SearchFileExtensionProvider extends Provider {
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<ExtensionItem>) {
|
||||
const captured = (/\b(?:ext):\s*(\S*)$/i).exec(pretext.toLowerCase());
|
||||
if (captured) {
|
||||
const matchedPretext = captured[1];
|
||||
|
||||
let extensions: ExtensionItem[] = [...globalExtensions];
|
||||
if (matchedPretext.length > 0) {
|
||||
extensions = extensions.filter((extension) => extension.label.startsWith(matchedPretext.toLowerCase()));
|
||||
extensions = extensions.slice(0, 10);
|
||||
} else {
|
||||
extensions = [
|
||||
{label: 'text', type: 'text', value: 'txt'},
|
||||
{label: 'word', type: 'word', value: 'docx'},
|
||||
{label: 'spreadsheet', type: 'spreadsheet', value: 'xlsx'},
|
||||
{label: 'presentation', type: 'presentation', value: 'pptx'},
|
||||
{label: 'pdf', type: 'pdf', value: 'pdf'},
|
||||
{label: 'image', type: 'image', value: 'png'},
|
||||
{label: 'image', type: 'image', value: 'jpg'},
|
||||
{label: 'audio', type: 'audio', value: 'mp3'},
|
||||
{label: 'video', type: 'video', value: 'mp4'},
|
||||
];
|
||||
}
|
||||
|
||||
const terms = extensions.map((extension) => extension.value);
|
||||
|
||||
resultsCallback({
|
||||
matchedPretext,
|
||||
terms,
|
||||
items: extensions,
|
||||
component: SearchFileExtensionSuggestion,
|
||||
});
|
||||
}
|
||||
|
||||
return Boolean(captured);
|
||||
}
|
||||
|
||||
allowDividers() {
|
||||
return false;
|
||||
}
|
||||
|
||||
presentationType() {
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
89
webapp/channels/src/components/new_search/hooks.tsx
Обычный файл
89
webapp/channels/src/components/new_search/hooks.tsx
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useRef, useEffect, useMemo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
|
||||
import {autocompleteChannelsForSearch} from 'actions/channel_actions';
|
||||
import {autocompleteUsersInTeam} from 'actions/user_actions';
|
||||
|
||||
import type {ProviderResult} from 'components/suggestion/provider';
|
||||
import type Provider from 'components/suggestion/provider';
|
||||
import SearchChannelProvider from 'components/suggestion/search_channel_provider';
|
||||
import SearchDateProvider from 'components/suggestion/search_date_provider';
|
||||
import SearchUserProvider from 'components/suggestion/search_user_provider';
|
||||
|
||||
import {SearchFileExtensionProvider} from './extension_suggestions_provider';
|
||||
|
||||
const useSearchSuggestions = (searchType: string, searchTerms: string, caretPosition: number, getCaretPosition: () => number, setSelectedOption: (idx: number) => void): [ProviderResult<unknown>|null, React.ReactNode] => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [providerResults, setProviderResults] = useState<ProviderResult<unknown>|null>(null);
|
||||
const [suggestionsHeader, setSuggestionsHeader] = useState<React.ReactNode>(<span/>);
|
||||
|
||||
const suggestionProviders = useRef<Provider[]>([
|
||||
new SearchDateProvider(),
|
||||
new SearchChannelProvider((term: string, success?: (channels: Channel[]) => void, error?: (err: ServerError) => void) => dispatch(autocompleteChannelsForSearch(term, success, error))),
|
||||
new SearchUserProvider((username: string) => dispatch(autocompleteUsersInTeam(username))),
|
||||
new SearchFileExtensionProvider(),
|
||||
]);
|
||||
|
||||
const headers = useMemo<React.ReactNode[]>(() => [
|
||||
<span key={1}/>,
|
||||
<FormattedMessage
|
||||
id='search_bar.channels'
|
||||
defaultMessage='Channels'
|
||||
key={2}
|
||||
/>,
|
||||
<FormattedMessage
|
||||
id='search_bar.users'
|
||||
defaultMessage='Users'
|
||||
key={3}
|
||||
/>,
|
||||
<FormattedMessage
|
||||
id='search_bar.file_types'
|
||||
defaultMessage='File types'
|
||||
key={4}
|
||||
/>,
|
||||
], []);
|
||||
|
||||
useEffect(() => {
|
||||
setProviderResults(null);
|
||||
if (searchType !== '' && searchType !== 'messages' && searchType !== 'files') {
|
||||
return;
|
||||
}
|
||||
|
||||
const partialSearchTerms = searchTerms.slice(0, caretPosition);
|
||||
if (searchTerms.length > caretPosition && searchTerms[caretPosition] !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (caretPosition > 0 && searchTerms[caretPosition - 1] === ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
suggestionProviders.current.forEach((provider, idx) => {
|
||||
provider.handlePretextChanged(partialSearchTerms, (res: ProviderResult<unknown>) => {
|
||||
if (idx === 3 && searchType !== 'files') {
|
||||
return;
|
||||
}
|
||||
if (caretPosition !== getCaretPosition()) {
|
||||
return;
|
||||
}
|
||||
res.items = res.items.slice(0, 10);
|
||||
res.terms = res.terms.slice(0, 10);
|
||||
setProviderResults(res);
|
||||
setSelectedOption(0);
|
||||
setSuggestionsHeader(headers[idx]);
|
||||
});
|
||||
});
|
||||
}, [searchTerms, searchType, caretPosition]);
|
||||
|
||||
return [providerResults, suggestionsHeader];
|
||||
};
|
||||
|
||||
export default useSearchSuggestions;
|
||||
89
webapp/channels/src/components/new_search/new_search.test.tsx
Обычный файл
89
webapp/channels/src/components/new_search/new_search.test.tsx
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
fireEvent,
|
||||
act,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import NewSearch from './new_search';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux') as typeof import('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
describe('components/new_search/NewSearch', () => {
|
||||
test('should open the search box on click search', () => {
|
||||
renderWithContext(<NewSearch/>);
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
screen.getByText('Search').click();
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open the search box when pressing any key differnt than tab', () => {
|
||||
renderWithContext(<NewSearch/>);
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
fireEvent.keyDown(screen.getByText('Search'), {key: 'Tab', code: 'Tab', keyCode: 9, charCode: 9});
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
fireEvent.keyDown(screen.getByText('Search'), {key: 'a', code: 'KeyA', keyCode: 65, charCode: 65});
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should close the search box on click outside the searchbox', () => {
|
||||
renderWithContext(<div><NewSearch/>{'Outside'}</div>);
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
act(() => {
|
||||
screen.getByText('Search').click();
|
||||
});
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
act(() => {
|
||||
screen.getByText('Outside').click();
|
||||
});
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should close the search box on Esc key is pressed', () => {
|
||||
renderWithContext(<NewSearch/>);
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
act(() => {
|
||||
screen.getByText('Search').click();
|
||||
});
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
act(() => {
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search messages'), {key: 'Escape', code: 'Escape', keyCode: 27, charCode: 27});
|
||||
});
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should close on search after calling dispatch', () => {
|
||||
renderWithContext(<NewSearch/>);
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
act(() => {
|
||||
screen.getByText('Search').click();
|
||||
});
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
act(() => {
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search messages'), {key: 'Enter', code: 'Enter', keyCode: 13, charCode: 13});
|
||||
});
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
expect(mockDispatch).toHaveBeenCalledWith({searchType: 'messages', type: 'UPDATE_RHS_SEARCH_TYPE'});
|
||||
expect(mockDispatch).toHaveBeenCalledWith({terms: '', type: 'UPDATE_RHS_SEARCH_TERMS'});
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should open the search ctrl+shift+f is press on web app', () => {
|
||||
renderWithContext(<div><NewSearch/>{'Outside'}</div>);
|
||||
expect(screen.queryByText('Messages')).not.toBeInTheDocument();
|
||||
act(() => {
|
||||
fireEvent.keyDown(screen.getByText('Outside'), {key: 'f', code: 'KeyF', keyCode: 70, charCode: 70, ctrlKey: true, shiftKey: true});
|
||||
});
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
280
webapp/channels/src/components/new_search/new_search.tsx
Обычный файл
280
webapp/channels/src/components/new_search/new_search.tsx
Обычный файл
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState, useRef, useCallback} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {getCurrentChannelNameForSearchShortcut} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {updateSearchTerms, showSearchResults, updateSearchType} from 'actions/views/rhs';
|
||||
import {getSearchButtons} from 'selectors/plugins';
|
||||
import {getSearchTerms, getSearchType} from 'selectors/rhs';
|
||||
|
||||
import Popover from 'components/widgets/popover';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
||||
import {isDesktopApp, getDesktopVersion, isMacApp} from 'utils/user_agent';
|
||||
|
||||
import SearchBox from './search_box';
|
||||
|
||||
const PopoverStyled = styled(Popover)`
|
||||
min-width: 600px;
|
||||
left: -90px;
|
||||
top: -12px;
|
||||
border-radius: 12px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.popover-content {
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchTypeBadge = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 2px 2px 7px;
|
||||
border-radius: 4px;
|
||||
margin: 0 6px;
|
||||
background: rgba(var(--sidebar-text-rgb), 0.08);
|
||||
color: var(--sidebar-text);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
|
||||
.icon-close {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(v(button-bg-rgb), 0.16);
|
||||
}
|
||||
`;
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
display: flex;
|
||||
width: 2.4rem;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
visibility: visible;
|
||||
transition: opacity 0.12s easy-out;
|
||||
`;
|
||||
|
||||
const NewSearchContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
width: 100%;
|
||||
background-color: rgba(var(--sidebar-text-rgb), 0.08);
|
||||
color: rgba(var(--sidebar-text-rgb), 0.64);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-s);
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: rgba(var(--sidebar-text-rgb), 0.16);
|
||||
color: rgba(var(--sidebar-text-rgb), 0.88);
|
||||
}
|
||||
`;
|
||||
|
||||
const NewSearch = (): JSX.Element => {
|
||||
const currentChannelName = useSelector(getCurrentChannelNameForSearchShortcut);
|
||||
const searchTerms = useSelector(getSearchTerms) || '';
|
||||
const searchType = useSelector(getSearchType) || '';
|
||||
const pluginSearch = useSelector(getSearchButtons);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [focused, setFocused] = useState<boolean>(false);
|
||||
const [currentChannel, setCurrentChannel] = useState('');
|
||||
const searchBoxRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '4.7.0');
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (Keyboard.isKeyPressed(e, Constants.KeyCodes.ESCAPE)) {
|
||||
e.preventDefault();
|
||||
setCurrentChannel('');
|
||||
setFocused(false);
|
||||
}
|
||||
|
||||
if (Keyboard.cmdOrCtrlPressed(e) && Keyboard.isKeyPressed(e, Constants.KeyCodes.F6)) {
|
||||
e.preventDefault();
|
||||
setCurrentChannel('');
|
||||
setFocused(false);
|
||||
}
|
||||
|
||||
if (Keyboard.cmdOrCtrlPressed(e) && Keyboard.isKeyPressed(e, Constants.KeyCodes.F)) {
|
||||
if (!isDesktop && !e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case for Mac Desktop xApp where Ctrl+Cmd+F triggers full screen view
|
||||
if (isMacApp() && e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
setCurrentChannel(currentChannelName || '');
|
||||
setFocused(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [currentChannelName]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (searchBoxRef.current) {
|
||||
if (e.target !== searchBoxRef.current && !searchBoxRef.current.contains(e.target as Node)) {
|
||||
setFocused(false);
|
||||
setCurrentChannel('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick, {capture: true});
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const closeSearchBox = useCallback(() => {
|
||||
setFocused(false);
|
||||
setCurrentChannel('');
|
||||
}, []);
|
||||
|
||||
const openSearchBox = useCallback(() => {
|
||||
setFocused(true);
|
||||
}, []);
|
||||
|
||||
const openSearchBoxOnKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (Keyboard.isKeyPressed(e, Constants.KeyCodes.TAB)) {
|
||||
return;
|
||||
}
|
||||
if (Keyboard.cmdOrCtrlPressed(e) && Keyboard.isKeyPressed(e, Constants.KeyCodes.F6)) {
|
||||
setFocused(false);
|
||||
return;
|
||||
}
|
||||
openSearchBox();
|
||||
},
|
||||
[openSearchBox],
|
||||
);
|
||||
|
||||
const runSearch = useCallback(
|
||||
(searchType: string, searchTerms: string) => {
|
||||
dispatch(updateSearchType(searchType));
|
||||
dispatch(updateSearchTerms(searchTerms));
|
||||
|
||||
if (searchType === '' || searchType === 'messages' || searchType === 'files') {
|
||||
dispatch(showSearchResults(false));
|
||||
} else {
|
||||
pluginSearch.forEach((pluginData: any) => {
|
||||
if (pluginData.pluginId === searchType) {
|
||||
pluginData.action(searchTerms);
|
||||
}
|
||||
});
|
||||
}
|
||||
setFocused(false);
|
||||
setCurrentChannel('');
|
||||
},
|
||||
[pluginSearch],
|
||||
);
|
||||
|
||||
const onClose = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(updateSearchType(''));
|
||||
dispatch(updateSearchTerms(''));
|
||||
}, []);
|
||||
|
||||
const clearSearchType = useCallback(() => dispatch(updateSearchType('')), []);
|
||||
|
||||
return (
|
||||
<NewSearchContainer
|
||||
tabIndex={0}
|
||||
onKeyDown={openSearchBoxOnKeyPress}
|
||||
onClick={openSearchBox}
|
||||
id='searchFormContainer'
|
||||
role='search'
|
||||
className='a11y__region'
|
||||
>
|
||||
<i className='icon icon-magnify'/>
|
||||
{(searchType === 'messages' || searchType === 'files') && (
|
||||
<SearchTypeBadge>
|
||||
{searchType === 'messages' && (
|
||||
<FormattedMessage
|
||||
id='search_bar.search_types.messages'
|
||||
defaultMessage='MESSAGES'
|
||||
/>
|
||||
)}
|
||||
{searchType === 'files' && (
|
||||
<FormattedMessage
|
||||
id='search_bar.search_types.files'
|
||||
defaultMessage='FILES'
|
||||
/>
|
||||
)}
|
||||
<i
|
||||
className='icon icon-close icon-12'
|
||||
onClick={clearSearchType}
|
||||
/>
|
||||
</SearchTypeBadge>
|
||||
)}
|
||||
{searchTerms && <span tabIndex={0}>{searchTerms}</span>}
|
||||
{searchTerms && (
|
||||
<CloseIcon
|
||||
data-testid='input-clear'
|
||||
role='button'
|
||||
onClick={onClose}
|
||||
>
|
||||
<span
|
||||
className='input-clear-x'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<i className='icon icon-close-circle'/>
|
||||
</span>
|
||||
</CloseIcon>
|
||||
)}
|
||||
{!searchTerms && (
|
||||
<FormattedMessage
|
||||
id='search_bar.search'
|
||||
defaultMessage='Search'
|
||||
/>
|
||||
)}
|
||||
{focused && (
|
||||
<PopoverStyled
|
||||
id='searchPopover'
|
||||
placement='bottom'
|
||||
>
|
||||
<SearchBox
|
||||
ref={searchBoxRef}
|
||||
onClose={closeSearchBox}
|
||||
onSearch={runSearch}
|
||||
initialSearchTerms={currentChannel ? `in:${currentChannel} ` : searchTerms}
|
||||
initialSearchType={searchType}
|
||||
/>
|
||||
</PopoverStyled>
|
||||
)}
|
||||
</NewSearchContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewSearch;
|
||||
73
webapp/channels/src/components/new_search/search_box.test.tsx
Обычный файл
73
webapp/channels/src/components/new_search/search_box.test.tsx
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
fireEvent,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import SearchBox from './search_box';
|
||||
|
||||
describe('components/new_search/SearchBox', () => {
|
||||
const baseProps = {
|
||||
onClose: jest.fn(),
|
||||
onSearch: jest.fn(),
|
||||
initialSearchTerms: '',
|
||||
initialSearchType: 'messages',
|
||||
};
|
||||
|
||||
test('should have the focus on the input field', () => {
|
||||
renderWithContext(<SearchBox {...baseProps}/>);
|
||||
expect(screen.getByPlaceholderText('Search messages')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Search messages')).toHaveFocus();
|
||||
});
|
||||
|
||||
test('should have set the initial search terms', () => {
|
||||
const props = {...baseProps, initialSearchTerms: 'test'};
|
||||
renderWithContext(<SearchBox {...props}/>);
|
||||
expect(screen.getByPlaceholderText('Search messages')).toHaveValue('test');
|
||||
});
|
||||
|
||||
test('should have the focus on the input field after switching search type', () => {
|
||||
renderWithContext(<SearchBox {...baseProps}/>);
|
||||
screen.getByText('Files').click();
|
||||
expect(screen.getByPlaceholderText('Search files')).toHaveFocus();
|
||||
});
|
||||
|
||||
test('should see files hints when i click on files', () => {
|
||||
renderWithContext(<SearchBox {...baseProps}/>);
|
||||
expect(screen.getByText('From:')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Ext:')).not.toBeInTheDocument();
|
||||
screen.getByText('Files').click();
|
||||
expect(screen.getByText('Ext:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call close on esc keydown', () => {
|
||||
renderWithContext(<SearchBox {...baseProps}/>);
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search messages'), {key: 'Escape', code: 'Escape'});
|
||||
expect(baseProps.onClose).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call search on enter keydown', () => {
|
||||
renderWithContext(<SearchBox {...baseProps}/>);
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search messages'), {key: 'Enter', code: 'Enter'});
|
||||
expect(baseProps.onSearch).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should be able to select with the up and down arrows', () => {
|
||||
renderWithContext(<SearchBox {...baseProps}/>);
|
||||
screen.getByText('Files').click();
|
||||
fireEvent.change(screen.getByPlaceholderText('Search files'), {target: {value: 'ext:'}});
|
||||
expect(screen.getByText('Text file')).toHaveClass('selected');
|
||||
expect(screen.getByText('Word Document')).not.toHaveClass('selected');
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search files'), {key: 'ArrowDown', code: 'ArrowDown'});
|
||||
expect(screen.getByText('Text file')).not.toHaveClass('selected');
|
||||
expect(screen.getByText('Word Document')).toHaveClass('selected');
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search files'), {key: 'ArrowUp', code: 'ArrowUp'});
|
||||
expect(screen.getByText('Text file')).toHaveClass('selected');
|
||||
expect(screen.getByText('Word Document')).not.toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
268
webapp/channels/src/components/new_search/search_box.tsx
Обычный файл
268
webapp/channels/src/components/new_search/search_box.tsx
Обычный файл
@@ -0,0 +1,268 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useRef, forwardRef, useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {escapeRegex} from 'utils/text_formatting';
|
||||
|
||||
import useSearchSuggestions from './hooks';
|
||||
import SearchBoxHints from './search_box_hints';
|
||||
import SearchInput from './search_box_input';
|
||||
import SearchSuggestions from './search_box_suggestions';
|
||||
import SearchTypeSelector from './search_box_type_selector';
|
||||
|
||||
const {KeyCodes} = Constants;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSearch: (searchType: string, searchTerms: string) => void;
|
||||
initialSearchTerms: string;
|
||||
initialSearchType: string;
|
||||
};
|
||||
|
||||
const SearchBoxContainer = styled.div`
|
||||
padding: 0px;
|
||||
|
||||
.rdp {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
|
||||
.rdp-months {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rdp-month {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rdp-table {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rdp-tbody .rdp-button {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CloseIcon = styled.button`
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const SearchBox = forwardRef(
|
||||
(
|
||||
{onClose, onSearch, initialSearchTerms, initialSearchType}: Props,
|
||||
ref: React.Ref<HTMLDivElement>,
|
||||
): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [caretPosition, setCaretPosition] = useState<number>(0);
|
||||
const [searchTerms, setSearchTerms] = useState<string>(initialSearchTerms);
|
||||
const [searchType, setSearchType] = useState<string>(initialSearchType || 'messages');
|
||||
const [selectedOption, setSelectedOption] = useState<number>(-1);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const getCaretPosition = useCallback(() => {
|
||||
return inputRef.current?.selectionEnd || 0;
|
||||
}, []);
|
||||
|
||||
const addSearchHint = useCallback((newSearchTerms: string) => {
|
||||
setSearchTerms(newSearchTerms);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.setSelectionRange(newSearchTerms.length, newSearchTerms.length);
|
||||
setCaretPosition(newSearchTerms.length);
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function updateCaretPosition() {
|
||||
setCaretPosition(inputRef.current?.selectionEnd || 0);
|
||||
}
|
||||
|
||||
const input = inputRef.current;
|
||||
|
||||
if (input) {
|
||||
input.addEventListener('change', updateCaretPosition);
|
||||
input.addEventListener('keypress', updateCaretPosition);
|
||||
input.addEventListener('keyup', updateCaretPosition);
|
||||
input.addEventListener('mousedown', updateCaretPosition);
|
||||
input.addEventListener('touchstart', updateCaretPosition);
|
||||
input.addEventListener('input', updateCaretPosition);
|
||||
input.addEventListener('paste', updateCaretPosition);
|
||||
input.addEventListener('cut', updateCaretPosition);
|
||||
input.addEventListener('mousemove', updateCaretPosition);
|
||||
input.addEventListener('select', updateCaretPosition);
|
||||
input.addEventListener('selectstart', updateCaretPosition);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (input) {
|
||||
input.removeEventListener('change', updateCaretPosition);
|
||||
input.removeEventListener('keypress', updateCaretPosition);
|
||||
input.removeEventListener('mousedown', updateCaretPosition);
|
||||
input.removeEventListener('keyup', updateCaretPosition);
|
||||
input.removeEventListener('touchstart', updateCaretPosition);
|
||||
input.removeEventListener('input', updateCaretPosition);
|
||||
input.removeEventListener('paste', updateCaretPosition);
|
||||
input.removeEventListener('cut', updateCaretPosition);
|
||||
input.removeEventListener('mousemove', updateCaretPosition);
|
||||
input.removeEventListener('select', updateCaretPosition);
|
||||
input.removeEventListener('selectstart', updateCaretPosition);
|
||||
}
|
||||
};
|
||||
}, [inputRef.current]);
|
||||
|
||||
const [providerResults, suggestionsHeader] = useSearchSuggestions(
|
||||
searchType,
|
||||
searchTerms,
|
||||
caretPosition,
|
||||
getCaretPosition,
|
||||
setSelectedOption,
|
||||
);
|
||||
|
||||
const focus = useCallback((newposition: number) => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
setTimeout(() => {
|
||||
inputRef.current?.setSelectionRange(newposition, newposition);
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateSearchValue = useCallback(
|
||||
(value: string, matchedPretext: string) => {
|
||||
const escapedMatchedPretext = escapeRegex(matchedPretext);
|
||||
const caretPosition = getCaretPosition();
|
||||
const extraSpace = caretPosition === searchTerms.length ? ' ' : '';
|
||||
setSearchTerms(
|
||||
searchTerms.slice(0, caretPosition).replace(new RegExp(escapedMatchedPretext + '$'), '') +
|
||||
value +
|
||||
extraSpace +
|
||||
searchTerms.slice(caretPosition),
|
||||
);
|
||||
focus((caretPosition + value.length + 1) - matchedPretext.length);
|
||||
},
|
||||
[searchTerms, setSearchTerms, focus, getCaretPosition],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<Element>): void => {
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.ESCAPE)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!providerResults || providerResults?.items.length === 0 || selectedOption === -1) {
|
||||
onClose();
|
||||
} else {
|
||||
setSelectedOption(-1);
|
||||
}
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.DOWN)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const totalItems = providerResults?.items.length || 0;
|
||||
if (selectedOption + 1 < totalItems) {
|
||||
setSelectedOption(selectedOption + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.UP)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (selectedOption > 0) {
|
||||
setSelectedOption(selectedOption - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e as any, KeyCodes.ENTER)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!providerResults || providerResults?.items.length === 0 || selectedOption === -1) {
|
||||
onSearch(searchType, searchTerms);
|
||||
} else {
|
||||
const matchedPretext = providerResults?.matchedPretext;
|
||||
const value = providerResults?.terms[selectedOption];
|
||||
updateSearchValue(value, matchedPretext);
|
||||
setSelectedOption(-1);
|
||||
}
|
||||
}
|
||||
},
|
||||
[providerResults, onClose, selectedOption, onSearch, searchType, searchTerms, updateSearchValue],
|
||||
);
|
||||
|
||||
const closeHandler = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [searchType]);
|
||||
|
||||
return (
|
||||
<SearchBoxContainer
|
||||
ref={ref}
|
||||
id='searchBox'
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'search_bar.search',
|
||||
defaultMessage: 'Search',
|
||||
})}
|
||||
aria-describedby='searchHints'
|
||||
role='searchbox'
|
||||
>
|
||||
<CloseIcon
|
||||
className='btn btn-icon btn-m'
|
||||
onClick={closeHandler}
|
||||
>
|
||||
<i className='icon icon-close'/>
|
||||
</CloseIcon>
|
||||
<SearchTypeSelector
|
||||
searchType={searchType}
|
||||
setSearchType={setSearchType}
|
||||
/>
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
searchTerms={searchTerms}
|
||||
searchType={searchType}
|
||||
setSearchTerms={setSearchTerms}
|
||||
onKeyDown={handleKeyDown}
|
||||
focus={focus}
|
||||
/>
|
||||
<SearchSuggestions
|
||||
searchType={searchType}
|
||||
searchTerms={searchTerms}
|
||||
suggestionsHeader={suggestionsHeader}
|
||||
providerResults={providerResults}
|
||||
selectedOption={selectedOption}
|
||||
setSelectedOption={setSelectedOption}
|
||||
onSearch={onSearch}
|
||||
onSuggestionSelected={updateSearchValue}
|
||||
/>
|
||||
<SearchBoxHints
|
||||
searchTerms={searchTerms}
|
||||
setSearchTerms={addSearchHint}
|
||||
searchType={searchType}
|
||||
providerResults={providerResults}
|
||||
selectedOption={selectedOption}
|
||||
focus={focus}
|
||||
/>
|
||||
</SearchBoxContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default SearchBox;
|
||||
110
webapp/channels/src/components/new_search/search_box_hints.test.tsx
Обычный файл
110
webapp/channels/src/components/new_search/search_box_hints.test.tsx
Обычный файл
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
fireEvent,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import SearchBoxHints from './search_box_hints';
|
||||
|
||||
const TestProviderResultComponent = ({item, term, matchedPretext, isSelection, onClick, onMouseMove}: any) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onClick(item.username, matchedPretext)}
|
||||
onMouseMove={() => onMouseMove()}
|
||||
className={isSelection ? 'selected' : ''}
|
||||
>
|
||||
<span>{item.username}</span>
|
||||
<span>{term}</span>
|
||||
<span>{matchedPretext}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TestPluginProviderComponent = ({searchTerms, onChangeSearch}: any) => {
|
||||
return (
|
||||
<div>
|
||||
<span>{'Plugin suggestion'}</span>
|
||||
<span>{searchTerms}</span>
|
||||
<span onClick={() => onChangeSearch('test', 't')}>{'onChangeSearch'}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe('components/new_search/SearchBoxHints', () => {
|
||||
const baseProps = {
|
||||
searchType: 'messages',
|
||||
searchTerms: '',
|
||||
setSearchTerms: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
selectedOption: -1,
|
||||
providerResults: {
|
||||
matchedPretext: '',
|
||||
terms: ['user1', 'user2'],
|
||||
items: [{username: 'test-username1'}, {username: 'test-username2'}],
|
||||
component: TestProviderResultComponent,
|
||||
},
|
||||
};
|
||||
|
||||
test('should show the hints for messages', () => {
|
||||
renderWithContext(<SearchBoxHints {...baseProps}/>);
|
||||
expect(screen.getByText('From:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should change the search term and focus on click', () => {
|
||||
renderWithContext(<SearchBoxHints {...baseProps}/>);
|
||||
fireEvent.click(screen.getByText('From:'));
|
||||
expect(baseProps.setSearchTerms).toHaveBeenCalledWith('From:');
|
||||
expect(baseProps.focus).toHaveBeenCalledWith(5);
|
||||
});
|
||||
|
||||
test('should set the selected option if it is passed from the parent', () => {
|
||||
const props = {...baseProps, selectedOption: 1};
|
||||
renderWithContext(<SearchBoxHints {...props}/>);
|
||||
expect(screen.getByText('Press Enter to select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not show the plugin suggestions without license', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'test-search-terms'};
|
||||
renderWithContext(
|
||||
<SearchBoxHints {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchHints: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'false'}}},
|
||||
},
|
||||
);
|
||||
expect(screen.queryByText('Plugin suggestion')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('test-search-terms')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the plugin suggestions', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'test-search-terms'};
|
||||
renderWithContext(
|
||||
<SearchBoxHints {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchHints: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
expect(screen.getByText('Plugin suggestion')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-search-terms')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should on search change change the search term and focus', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'something from:t'};
|
||||
renderWithContext(
|
||||
<SearchBoxHints {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchHints: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
screen.getByText('onChangeSearch').click();
|
||||
expect(baseProps.setSearchTerms).toHaveBeenCalledWith('something from:test ');
|
||||
expect(baseProps.focus).toHaveBeenCalledWith(20);
|
||||
});
|
||||
});
|
||||
80
webapp/channels/src/components/new_search/search_box_hints.tsx
Обычный файл
80
webapp/channels/src/components/new_search/search_box_hints.tsx
Обычный файл
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getSearchBoxHints} from 'selectors/plugins';
|
||||
|
||||
import type {ProviderResult} from 'components/suggestion/provider';
|
||||
import SearchDateSuggestion from 'components/suggestion/search_date_suggestion';
|
||||
|
||||
import ErrorBoundary from 'plugins/pluggable/error_boundary';
|
||||
|
||||
import SearchHints from './search_hint';
|
||||
|
||||
type Props = {
|
||||
searchTerms: string;
|
||||
setSearchTerms: (searchTerms: string) => void;
|
||||
searchType: string;
|
||||
selectedOption: number;
|
||||
providerResults: ProviderResult<unknown>|null;
|
||||
focus: (pos: number) => void;
|
||||
}
|
||||
|
||||
const SearchBoxHints = ({searchTerms, setSearchTerms, searchType, providerResults, selectedOption, focus}: Props) => {
|
||||
const filterSelectedCallback = useCallback((filter: string) => {
|
||||
if (searchTerms.endsWith(' ') || searchTerms.length === 0) {
|
||||
setSearchTerms(searchTerms + filter);
|
||||
focus(searchTerms.length + filter.length);
|
||||
} else {
|
||||
setSearchTerms(searchTerms + ' ' + filter);
|
||||
focus(searchTerms.length + filter.length + 1);
|
||||
}
|
||||
}, [searchTerms, setSearchTerms, focus]);
|
||||
|
||||
const searchChangeCallback = useCallback((value: string, matchedPretext: string) => {
|
||||
const changedValue = value.replace(matchedPretext, '');
|
||||
setSearchTerms(searchTerms + changedValue + ' ');
|
||||
focus(searchTerms.length + changedValue.length + 1);
|
||||
}, [searchTerms, setSearchTerms, focus]);
|
||||
|
||||
const searchPluginHints = useSelector(getSearchBoxHints);
|
||||
|
||||
if (searchType === '' || searchType === 'messages' || searchType === 'files') {
|
||||
return (
|
||||
<SearchHints
|
||||
onSelectFilter={filterSelectedCallback}
|
||||
searchType={searchType}
|
||||
searchTerms={searchTerms}
|
||||
hasSelectedOption={Boolean(providerResults && providerResults.items.length > 0 && selectedOption !== -1)}
|
||||
isDate={providerResults?.component === SearchDateSuggestion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const pluginComponentInfo = searchPluginHints.find(({pluginId}: any) => {
|
||||
if (searchType === pluginId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!pluginComponentInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component: any = pluginComponentInfo.component;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Component
|
||||
key={pluginComponentInfo.pluginId}
|
||||
onChangeSearch={searchChangeCallback}
|
||||
searchTerms={searchTerms}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBoxHints;
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
fireEvent,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import SearchBoxInput from './search_box_input';
|
||||
|
||||
describe('components/new_search/SearchBoxInput', () => {
|
||||
const baseProps = {
|
||||
searchType: 'messages',
|
||||
searchTerms: '',
|
||||
setSearchTerms: jest.fn(),
|
||||
onKeyDown: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
|
||||
test('should show the right placeholder for messages', () => {
|
||||
renderWithContext(<SearchBoxInput {...baseProps}/>);
|
||||
expect(screen.getByPlaceholderText('Search messages')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the right placeholder for files', () => {
|
||||
const props = {...baseProps, searchType: 'files'};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
expect(screen.getByPlaceholderText('Search files')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the right placeholder for plugins', () => {
|
||||
const props = {...baseProps, searchType: 'plugin-id'};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the right value when the searchTerms are set', () => {
|
||||
const props = {...baseProps, searchTerms: 'test-value'};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
expect(screen.getByPlaceholderText('Search messages')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Search messages')).toHaveValue('test-value');
|
||||
});
|
||||
|
||||
test('should call on key down when there is a key down event on the input field', () => {
|
||||
const props = {...baseProps};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search messages'), {key: 'Enter'});
|
||||
expect(props.onKeyDown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call on key down when there is a key down event on the input field', () => {
|
||||
const props = {...baseProps};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
fireEvent.keyDown(screen.getByPlaceholderText('Search messages'), {key: 'Enter'});
|
||||
expect(props.onKeyDown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should update the search term on change', () => {
|
||||
const props = {...baseProps};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
fireEvent.change(screen.getByPlaceholderText('Search messages'), {target: {value: 'new-value'}});
|
||||
expect(props.setSearchTerms).toHaveBeenCalledWith('new-value');
|
||||
});
|
||||
|
||||
test('should clear the terms and focus in the input whenever click the clear button', () => {
|
||||
const props = {...baseProps, searchTerms: 'test-value'};
|
||||
renderWithContext(<SearchBoxInput {...props}/>);
|
||||
fireEvent.click(screen.getByText('Clear'));
|
||||
expect(props.setSearchTerms).toHaveBeenCalledWith('');
|
||||
expect(props.focus).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
136
webapp/channels/src/components/new_search/search_box_input.tsx
Обычный файл
136
webapp/channels/src/components/new_search/search_box_input.tsx
Обычный файл
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
updateSearchTerms,
|
||||
updateSearchType,
|
||||
} from 'actions/views/rhs';
|
||||
|
||||
import QuickInput from 'components/quick_input';
|
||||
|
||||
const SearchInputContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
i {
|
||||
color: var(--center-channel-color-56);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.icon-close {
|
||||
postion: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
&.icon-magnify {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 21px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.input-wrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
&& input {
|
||||
padding: 20px 100px 20px 58px;
|
||||
height: auto;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: var(--border-default);
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-family: Metropolis, sans-serif;
|
||||
:focus {
|
||||
border: none;
|
||||
border-bottom: var(--border-default);
|
||||
box-shadow: none;
|
||||
}
|
||||
::placeholder {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ClearButton = styled.button`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
background: none;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
&:hover{
|
||||
color: rgba(var(--center-channel-color-rgb), 0.88);
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
searchTerms: string;
|
||||
searchType: string;
|
||||
setSearchTerms: (searchTerms: string) => void;
|
||||
onKeyDown: (e: React.KeyboardEvent<Element>) => void;
|
||||
focus: (newPosition: number) => void;
|
||||
}
|
||||
|
||||
const SearchInput = ({searchTerms, searchType, setSearchTerms, onKeyDown, focus}: Props, inputRef: React.Ref<HTMLInputElement>) => {
|
||||
const intl = useIntl();
|
||||
let searchPlaceholder = intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'});
|
||||
|
||||
if (searchType === 'messages' || searchType === '') {
|
||||
searchPlaceholder = intl.formatMessage({id: 'search_bar.search_messages', defaultMessage: 'Search messages'});
|
||||
}
|
||||
if (searchType === 'files') {
|
||||
searchPlaceholder = intl.formatMessage({id: 'search_bar.search_files', defaultMessage: 'Search files'});
|
||||
}
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const inputChangeCallback = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerms(e.target.value);
|
||||
}, [setSearchTerms]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setSearchTerms('');
|
||||
dispatch(updateSearchTerms(''));
|
||||
dispatch(updateSearchType('messages'));
|
||||
focus(0);
|
||||
}, [focus, setSearchTerms]);
|
||||
|
||||
return (
|
||||
<SearchInputContainer>
|
||||
<i className='icon icon-magnify'/>
|
||||
<QuickInput
|
||||
ref={inputRef}
|
||||
className={'search-bar form-control'}
|
||||
aria-describedby={'searchbar-help-popup'}
|
||||
aria-label={searchPlaceholder}
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchTerms}
|
||||
onChange={inputChangeCallback}
|
||||
type='search'
|
||||
delayInputUpdate={true}
|
||||
clearable={true}
|
||||
autoFocus={true}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={0}
|
||||
/>
|
||||
{searchTerms.length > 0 && (
|
||||
<ClearButton
|
||||
className='btn btn-sm input-clear-x'
|
||||
onClick={clearSearch}
|
||||
>
|
||||
<i className='icon icon-close-circle'/>
|
||||
<FormattedMessage
|
||||
id='search_bar.clear'
|
||||
defaultMessage='Clear'
|
||||
/>
|
||||
</ClearButton>
|
||||
)}
|
||||
</SearchInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef<HTMLInputElement, Props>(SearchInput);
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
fireEvent,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import SearchBoxSuggestions from './search_box_suggestions';
|
||||
|
||||
const TestProviderResultComponent = ({item, term, matchedPretext, isSelection, onClick, onMouseMove}: any) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onClick(item.username, matchedPretext)}
|
||||
onMouseMove={() => onMouseMove()}
|
||||
className={isSelection ? 'selected' : ''}
|
||||
>
|
||||
<span>{item.username}</span>
|
||||
<span>{term}</span>
|
||||
<span>{matchedPretext}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TestPluginProviderComponent = ({searchTerms, onChangeSearch, onRunSearch}: any) => {
|
||||
return (
|
||||
<div>
|
||||
<span>{'Plugin suggestion'}</span>
|
||||
<span>{searchTerms}</span>
|
||||
<span onClick={() => onChangeSearch('test', 't')}>{'onChangeSearch'}</span>
|
||||
<span onClick={() => onRunSearch(searchTerms)}>{'onRunSearch'}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe('components/new_search/SearchBoxSuggestions', () => {
|
||||
const baseProps = {
|
||||
searchType: 'messages',
|
||||
searchTerms: '',
|
||||
selectedOption: -1,
|
||||
setSelectedOption: jest.fn(),
|
||||
suggestionsHeader: <p>{'Test Header'}</p>,
|
||||
providerResults: {
|
||||
matchedPretext: '',
|
||||
terms: ['user1', 'user2'],
|
||||
items: [{username: 'test-username1'}, {username: 'test-username2'}],
|
||||
component: TestProviderResultComponent,
|
||||
},
|
||||
onSearch: jest.fn(),
|
||||
onSuggestionSelected: jest.fn(),
|
||||
};
|
||||
|
||||
test('should show the suggestions and the suggestion header on messages', () => {
|
||||
renderWithContext(<SearchBoxSuggestions {...baseProps}/>);
|
||||
expect(screen.getByText('Test Header')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-username1')).toBeInTheDocument();
|
||||
expect(screen.getByText('user1')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-username2')).toBeInTheDocument();
|
||||
expect(screen.getByText('user2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call the onSuggestionSelected on click', () => {
|
||||
renderWithContext(<SearchBoxSuggestions {...baseProps}/>);
|
||||
fireEvent.click(screen.getByText('test-username1'));
|
||||
expect(baseProps.onSuggestionSelected).toHaveBeenCalledWith('test-username1', '');
|
||||
});
|
||||
|
||||
test('should call the onSuggestionSelected on click with matchedPretext and previous text', () => {
|
||||
const props = {...baseProps, searchTerms: 'something from:test-user', providerResults: {...baseProps.providerResults, matchedPretext: 'test-user'}};
|
||||
renderWithContext(<SearchBoxSuggestions {...props}/>);
|
||||
fireEvent.click(screen.getByText('test-username1'));
|
||||
expect(baseProps.onSuggestionSelected).toHaveBeenCalledWith('test-username1', 'test-user');
|
||||
});
|
||||
|
||||
test('should change the selected option on mousemove', () => {
|
||||
const props = {...baseProps};
|
||||
renderWithContext(<SearchBoxSuggestions {...props}/>);
|
||||
fireEvent.mouseMove(screen.getByText('test-username2'));
|
||||
expect(baseProps.setSelectedOption).toHaveBeenCalledWith(1);
|
||||
fireEvent.mouseMove(screen.getByText('test-username1'));
|
||||
expect(baseProps.setSelectedOption).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('should not show the plugin suggestions without license', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'test-search-terms'};
|
||||
renderWithContext(
|
||||
<SearchBoxSuggestions {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchSuggestions: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'false'}}},
|
||||
},
|
||||
);
|
||||
expect(screen.queryByText('Plugin suggestion')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('test-search-terms')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the plugin suggestions', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'test-search-terms'};
|
||||
renderWithContext(
|
||||
<SearchBoxSuggestions {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchSuggestions: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
expect(screen.getByText('Plugin suggestion')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-search-terms')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call the onSuggestionSelected on plugin search change', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'something from:t'};
|
||||
renderWithContext(
|
||||
<SearchBoxSuggestions {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchSuggestions: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
screen.getByText('onChangeSearch').click();
|
||||
expect(baseProps.onSuggestionSelected).toHaveBeenCalledWith('test', 't');
|
||||
});
|
||||
|
||||
test('should run search whenver onRunSearch is executed', () => {
|
||||
const props = {...baseProps, searchType: 'test-id', searchTerms: 'something from:t'};
|
||||
renderWithContext(
|
||||
<SearchBoxSuggestions {...props}/>,
|
||||
{
|
||||
plugins: {components: {SearchSuggestions: [{component: TestPluginProviderComponent as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
screen.getByText('onRunSearch').click();
|
||||
expect(baseProps.onSearch).toHaveBeenCalledWith('test-id', 'something from:t');
|
||||
});
|
||||
});
|
||||
142
webapp/channels/src/components/new_search/search_box_suggestions.tsx
Обычный файл
142
webapp/channels/src/components/new_search/search_box_suggestions.tsx
Обычный файл
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {getSearchPluginSuggestions} from 'selectors/plugins';
|
||||
|
||||
import type {ProviderResult} from 'components/suggestion/provider';
|
||||
import type {SuggestionProps} from 'components/suggestion/suggestion';
|
||||
|
||||
import ErrorBoundary from 'plugins/pluggable/error_boundary';
|
||||
|
||||
const SuggestionsHeader = styled.div`
|
||||
margin-top: 16px;
|
||||
padding: 8px 24px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const SuggestionsBody = styled.div`
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
padding-bottom: 16px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
searchType: string;
|
||||
searchTerms: string;
|
||||
selectedOption: number;
|
||||
setSelectedOption: (idx: number) => void;
|
||||
suggestionsHeader: React.ReactNode;
|
||||
providerResults: ProviderResult<unknown> | null;
|
||||
onSearch: (searchType: string, searchTerms: string) => void;
|
||||
onSuggestionSelected: (value: string, matchedPretext: string) => void;
|
||||
}
|
||||
|
||||
const SearchSuggestions = ({searchType, searchTerms, suggestionsHeader, providerResults, selectedOption, setSelectedOption, onSearch, onSuggestionSelected}: Props) => {
|
||||
const runSearch = useCallback((searchTerms: string) => {
|
||||
onSearch(searchType, searchTerms);
|
||||
}, [onSearch, searchType]);
|
||||
|
||||
const searchPluginSuggestions = useSelector(getSearchPluginSuggestions);
|
||||
|
||||
const generateLabel = (item: any) => {
|
||||
let label = '';
|
||||
if (item.username) {
|
||||
label = item.username;
|
||||
if ((item.first_name || item.last_name) && item.nickname) {
|
||||
label += ` ${item.first_name} ${item.last_name} ${item.nickname}`;
|
||||
} else if (item.nickname) {
|
||||
label += ` ${item.nickname}`;
|
||||
} else if (item.first_name || item.last_name) {
|
||||
label += ` ${item.first_name} ${item.last_name}`;
|
||||
}
|
||||
} else if (item.type === 'D' || item.type === 'G') {
|
||||
label = item.display_name;
|
||||
} else if (item.type === 'P' || item.type === 'O') {
|
||||
label = item.name;
|
||||
} else if (item.emoji) {
|
||||
label = item.name;
|
||||
}
|
||||
|
||||
if (label) {
|
||||
label = label.toLowerCase();
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
if (searchType === '' || searchType === 'messages' || searchType === 'files') {
|
||||
if (!providerResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SuggestionsBody>
|
||||
<SuggestionsHeader>{suggestionsHeader}</SuggestionsHeader>
|
||||
{providerResults.component && providerResults.items[selectedOption] && (
|
||||
<div
|
||||
aria-live='polite'
|
||||
role='alert'
|
||||
className='sr-only'
|
||||
key={providerResults.terms[selectedOption]}
|
||||
>
|
||||
{generateLabel(providerResults.items[selectedOption])}
|
||||
</div>
|
||||
)}
|
||||
{providerResults.items.map((item, idx) => {
|
||||
if (!providerResults.component) {
|
||||
return null;
|
||||
}
|
||||
const Component = providerResults.component as React.ComponentType<SuggestionProps<any>>;
|
||||
return (
|
||||
<Component
|
||||
key={providerResults.terms[idx]}
|
||||
item={item as UserProfile}
|
||||
term={providerResults.terms[idx]}
|
||||
matchedPretext={providerResults.matchedPretext}
|
||||
isSelection={idx === selectedOption}
|
||||
onClick={onSuggestionSelected}
|
||||
onMouseMove={() => {
|
||||
setSelectedOption(idx);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SuggestionsBody>
|
||||
);
|
||||
}
|
||||
|
||||
const pluginComponentInfo = searchPluginSuggestions.find(({pluginId}: any) => {
|
||||
if (searchType === pluginId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!pluginComponentInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component: any = pluginComponentInfo.component;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Component
|
||||
key={pluginComponentInfo.pluginId}
|
||||
searchTerms={searchTerms}
|
||||
onChangeSearch={onSuggestionSelected}
|
||||
onRunSearch={runSearch}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchSuggestions;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import SearchBoxTypeSelector from './search_box_type_selector';
|
||||
|
||||
describe('components/new_search/SearchBoxTypeSelector', () => {
|
||||
const baseProps = {
|
||||
setSearchType: jest.fn(),
|
||||
searchType: 'messages',
|
||||
};
|
||||
|
||||
test('should have the built-in type options', () => {
|
||||
renderWithContext(<SearchBoxTypeSelector {...baseProps}/>);
|
||||
expect(screen.getByText('Files')).toBeInTheDocument();
|
||||
expect(screen.getByText('Messages')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('on option clicked should call the setSearchType', () => {
|
||||
renderWithContext(<SearchBoxTypeSelector {...baseProps}/>);
|
||||
screen.getByText('Messages').click();
|
||||
expect(baseProps.setSearchType).toHaveBeenCalledWith('messages');
|
||||
});
|
||||
|
||||
test('should not have the plugin options without license', () => {
|
||||
renderWithContext(
|
||||
<SearchBoxTypeSelector {...baseProps}/>,
|
||||
{
|
||||
plugins: {components: {SearchButtons: [{component: (() => <pre>{'test'}</pre>) as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'false'}}},
|
||||
},
|
||||
);
|
||||
expect(screen.queryByText('test')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should have the plugin options', () => {
|
||||
renderWithContext(
|
||||
<SearchBoxTypeSelector {...baseProps}/>,
|
||||
{
|
||||
plugins: {components: {SearchButtons: [{component: (() => <pre>{'test'}</pre>) as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
expect(screen.getByText('test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('on plugin option clicked should call the setSearchType', () => {
|
||||
renderWithContext(
|
||||
<SearchBoxTypeSelector {...baseProps}/>,
|
||||
{
|
||||
plugins: {components: {SearchButtons: [{component: (() => <pre>{'test'}</pre>) as React.ComponentType, pluginId: 'test-id'}]}},
|
||||
entities: {general: {license: {IsLicensed: 'true'}}},
|
||||
},
|
||||
);
|
||||
screen.getByText('test').click();
|
||||
expect(baseProps.setSearchType).toHaveBeenCalledWith('test-id');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {getSearchButtons} from 'selectors/plugins';
|
||||
|
||||
import ErrorBoundary from 'plugins/pluggable/error_boundary';
|
||||
|
||||
const SearchTypeSelectorContainer = styled.div`
|
||||
margin: 20px 20px 0px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3px;
|
||||
background-color: var(--center-channel-bg);
|
||||
border-radius: var(--radius-m);
|
||||
border: var(--border-default);
|
||||
width: fit-content;
|
||||
gap: 3px;
|
||||
`;
|
||||
|
||||
type SearchTypeItemProps = {
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
const SearchTypeItem = styled.button<SearchTypeItemProps>`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
background-color: ${(props) => (props.selected ? 'rgba(var(--button-bg-rgb), 0.08)' : 'transparent')};
|
||||
color: ${(props) => (props.selected ? 'var(--button-bg)' : 'rgba(var(--center-channel-color-rgb), 0.75)')};
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
&:hover {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.88);
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
searchType: string;
|
||||
setSearchType: (searchType: string) => void;
|
||||
}
|
||||
|
||||
const SearchTypeSelector = ({searchType, setSearchType}: Props) => {
|
||||
const setMessagesSearchType = useCallback(() => setSearchType('messages'), [setSearchType]);
|
||||
const setFilesSearchType = useCallback(() => setSearchType('files'), [setSearchType]);
|
||||
|
||||
const searchPluginButtons = useSelector(getSearchButtons);
|
||||
|
||||
return (
|
||||
<SearchTypeSelectorContainer>
|
||||
<SearchTypeItem
|
||||
selected={searchType === 'messages'}
|
||||
onClick={setMessagesSearchType}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='search_bar.usage.search_type_messages'
|
||||
defaultMessage='Messages'
|
||||
/>
|
||||
</SearchTypeItem>
|
||||
<SearchTypeItem
|
||||
selected={searchType === 'files'}
|
||||
onClick={setFilesSearchType}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='search_bar.usage.search_type_files'
|
||||
defaultMessage='Files'
|
||||
/>
|
||||
</SearchTypeItem>
|
||||
{searchPluginButtons.map(({component, pluginId}: any) => {
|
||||
const Component = component as React.ComponentType;
|
||||
return (
|
||||
<SearchTypeItem
|
||||
key={pluginId}
|
||||
selected={searchType === pluginId}
|
||||
onClick={() => setSearchType(pluginId)}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Component/>
|
||||
</ErrorBoundary>
|
||||
</SearchTypeItem>
|
||||
);
|
||||
})}
|
||||
</SearchTypeSelectorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchTypeSelector;
|
||||
78
webapp/channels/src/components/new_search/search_hint.test.tsx
Обычный файл
78
webapp/channels/src/components/new_search/search_hint.test.tsx
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
renderWithContext,
|
||||
screen,
|
||||
} from 'tests/react_testing_utils';
|
||||
|
||||
import SearchHint from './search_hint';
|
||||
|
||||
describe('components/new_search/SearchHint', () => {
|
||||
const baseProps = {
|
||||
onSelectFilter: jest.fn(),
|
||||
searchType: 'messages',
|
||||
searchTerms: '',
|
||||
hasSelectedOption: false,
|
||||
isDate: false,
|
||||
};
|
||||
|
||||
test('should have the right hint options on search messages empty string', () => {
|
||||
renderWithContext(<SearchHint {...baseProps}/>);
|
||||
expect(screen.getByText('From:')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Ext:')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should suggest to hit enter to search on search messages with not empty string not ended with space', () => {
|
||||
const props = {...baseProps, searchTerms: 'test'};
|
||||
renderWithContext(<SearchHint {...props}/>);
|
||||
expect(screen.getByText('Press Enter to search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should have the right hint options on search messages with not empty string ended with space', () => {
|
||||
const props = {...baseProps, searchTerms: 'test '};
|
||||
renderWithContext(<SearchHint {...props}/>);
|
||||
expect(screen.getByText('From:')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Ext:')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should have the right hint options on search files empty string', () => {
|
||||
const props = {...baseProps, searchType: 'files'};
|
||||
renderWithContext(<SearchHint {...props}/>);
|
||||
expect(screen.getByText('From:')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Ext:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should suggest to hit enter to search on search files with not empty string not ended with space', () => {
|
||||
const props = {...baseProps, searchType: 'files', searchTerms: 'test'};
|
||||
renderWithContext(<SearchHint {...props}/>);
|
||||
expect(screen.getByText('Press Enter to search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should have the right hint options on search files with not empty string ended with space', () => {
|
||||
const props = {...baseProps, searchType: 'files', searchTerms: 'test '};
|
||||
renderWithContext(<SearchHint {...props}/>);
|
||||
expect(screen.getByText('From:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ext:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should be empty on search if is date', () => {
|
||||
const props = {...baseProps, isDate: true};
|
||||
const {asFragment} = renderWithContext(<SearchHint {...props}/>);
|
||||
expect(asFragment()).toMatchInlineSnapshot('<DocumentFragment />');
|
||||
});
|
||||
|
||||
test('should suggest to hit enter to select on search if has selected option', () => {
|
||||
const props = {...baseProps, hasSelectedOption: true};
|
||||
renderWithContext(<SearchHint {...props}/>);
|
||||
expect(screen.getByText('Press Enter to select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('on filter clicked should call the onSelectFilter', () => {
|
||||
renderWithContext(<SearchHint {...baseProps}/>);
|
||||
screen.getByText('From:').click();
|
||||
expect(baseProps.onSelectFilter).toHaveBeenCalledWith('From:');
|
||||
});
|
||||
});
|
||||
101
webapp/channels/src/components/new_search/search_hint.tsx
Обычный файл
101
webapp/channels/src/components/new_search/search_hint.tsx
Обычный файл
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {searchHintOptions, searchFilesHintOptions} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
onSelectFilter: (filter: string) => void;
|
||||
searchType: string;
|
||||
searchTerms: string;
|
||||
hasSelectedOption: boolean;
|
||||
isDate: boolean;
|
||||
}
|
||||
|
||||
const SearchHintsContainer = styled.div`
|
||||
display: flex;
|
||||
padding: 20px 24px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
i {
|
||||
margin-right: 8px;
|
||||
color: var(--center-channel-color-56);
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchFilter = styled.button`
|
||||
display: flex;
|
||||
padding: 4px 10px;
|
||||
color: var(--center-channel-color);
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
border-radius: var(--radius-l);
|
||||
border: none;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
margin-left: 8px;
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchHints = ({onSelectFilter, searchType, searchTerms, hasSelectedOption, isDate}: Props): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
let filters = searchHintOptions.filter((filter) => filter.searchTerm !== '-' && filter.searchTerm !== '""');
|
||||
if (searchType === 'files') {
|
||||
filters = searchFilesHintOptions.filter((filter) => filter.searchTerm !== '-' && filter.searchTerm !== '""');
|
||||
}
|
||||
|
||||
if (isDate) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (hasSelectedOption) {
|
||||
return (
|
||||
<SearchHintsContainer id='searchHints'>
|
||||
<i className='icon icon-keyboard-return'/>
|
||||
<FormattedMessage
|
||||
id='search_hint.enter_to_select'
|
||||
defaultMessage='Press Enter to select'
|
||||
/>
|
||||
</SearchHintsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (searchTerms.length > 0 && searchTerms[searchTerms.length - 1] !== ' ') {
|
||||
return (
|
||||
<SearchHintsContainer id='searchHints'>
|
||||
<i className='icon icon-keyboard-return'/>
|
||||
<FormattedMessage
|
||||
id='search_hint.enter_to_search'
|
||||
defaultMessage='Press Enter to search'
|
||||
/>
|
||||
</SearchHintsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchHintsContainer id='searchHints'>
|
||||
<i className='icon icon-lightbulb-outline'/>
|
||||
<FormattedMessage
|
||||
id='search_hint.filter'
|
||||
defaultMessage='Filter your search with:'
|
||||
/>
|
||||
{filters.map((filter) => (
|
||||
<SearchFilter
|
||||
key={filter.searchTerm}
|
||||
onClick={() => onSelectFilter(filter.searchTerm)}
|
||||
>
|
||||
<span title={intl.formatMessage(filter.message)}>
|
||||
{filter.searchTerm}
|
||||
</span>
|
||||
</SearchFilter>
|
||||
))}
|
||||
</SearchHintsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchHints;
|
||||
|
||||
@@ -65,6 +65,11 @@ export type Props = {
|
||||
*/
|
||||
onKeyUp?: (event: React.KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback to handle the key down of the input
|
||||
*/
|
||||
onKeyDown?: (event: React.KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* When true, and an onClear callback is defined, show an X on the input field even if
|
||||
* the input is empty.
|
||||
@@ -80,6 +85,7 @@ export type Props = {
|
||||
type?: string;
|
||||
id?: string;
|
||||
onInput?: (e?: React.FormEvent<HTMLInputElement>) => void;
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
// A component that can be used to make controlled inputs that function properly in certain
|
||||
|
||||
@@ -15,6 +15,7 @@ import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
getSearchResultsTerms,
|
||||
getSearchResultsType,
|
||||
getIsSearchingTerm,
|
||||
getIsSearchingFlaggedPost,
|
||||
getIsSearchingPinnedPost,
|
||||
@@ -76,7 +77,7 @@ function makeMapStateToProps() {
|
||||
|
||||
// this is basically a hack to make ts compiler happy
|
||||
// add correct type when it is known what exactly is returned from the function
|
||||
const currentSearch = getCurrentSearchForCurrentTeam(state) as unknown as Record<string, any> || {};
|
||||
const currentSearch = (getCurrentSearchForCurrentTeam(state) as unknown as Record<string, any>) || {};
|
||||
const currentTeamName = getCurrentTeam(state)?.name ?? '';
|
||||
|
||||
return {
|
||||
@@ -84,6 +85,7 @@ function makeMapStateToProps() {
|
||||
fileResults: files,
|
||||
matches: getSearchMatches(state),
|
||||
searchTerms: getSearchResultsTerms(state),
|
||||
searchSelectedType: getSearchResultsType(state),
|
||||
isSearchingTerm: getIsSearchingTerm(state),
|
||||
isSearchingFlaggedPost: getIsSearchingFlaggedPost(state),
|
||||
isSearchingPinnedPost: getIsSearchingPinnedPost(state),
|
||||
|
||||
@@ -92,6 +92,10 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
scrollbars.current?.scrollToTop();
|
||||
}, [props.searchTerms]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchType(props.searchSelectedType);
|
||||
}, [props.searchSelectedType]);
|
||||
|
||||
useEffect(() => {
|
||||
// reset search type when switching views
|
||||
setSearchType(props.searchType);
|
||||
|
||||
@@ -29,24 +29,24 @@ export type OwnProps = {
|
||||
setSearchType: (searchType: SearchType) => void;
|
||||
searchFilterType: SearchFilterType;
|
||||
setSearchFilterType: (filterType: SearchFilterType) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
results: Post[];
|
||||
fileResults: FileInfo[];
|
||||
matches: Record<string, string[]>;
|
||||
searchTerms: string;
|
||||
searchSelectedType: string;
|
||||
isSearchingTerm: boolean;
|
||||
isSearchingFlaggedPost: boolean;
|
||||
isSearchingPinnedPost: boolean;
|
||||
isSearchGettingMore: boolean;
|
||||
isSearchAtEnd: boolean;
|
||||
isSearchFilesAtEnd: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type IntlProps = {
|
||||
intl: IntlShape;
|
||||
}
|
||||
};
|
||||
|
||||
export type Props = OwnProps & StateProps & IntlProps;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer} from './suggestion';
|
||||
import type {SuggestionProps} from './suggestion';
|
||||
|
||||
const SearchUserSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<UserProfile>>((props, ref) => {
|
||||
export const SearchUserSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<UserProfile>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
const username = item.username;
|
||||
|
||||
@@ -674,6 +674,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
||||
day="numeric"
|
||||
dispatch={[Function]}
|
||||
hourCycle="h12"
|
||||
timeZone="UTC"
|
||||
units={
|
||||
Array [
|
||||
"now",
|
||||
@@ -736,6 +737,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
|
||||
relNearest={1}
|
||||
second="numeric"
|
||||
style="long"
|
||||
timeZone="UTC"
|
||||
timeZoneName="short"
|
||||
units={
|
||||
Array [
|
||||
@@ -1520,6 +1522,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
||||
day="numeric"
|
||||
dispatch={[Function]}
|
||||
hourCycle="h12"
|
||||
timeZone="UTC"
|
||||
units={
|
||||
Array [
|
||||
"now",
|
||||
@@ -1582,6 +1585,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
|
||||
relNearest={1}
|
||||
second="numeric"
|
||||
style="long"
|
||||
timeZone="UTC"
|
||||
timeZoneName="short"
|
||||
units={
|
||||
Array [
|
||||
|
||||
@@ -3742,6 +3742,17 @@
|
||||
"file_search_result_item.download": "Download",
|
||||
"file_search_result_item.more_actions": "More Actions",
|
||||
"file_search_result_item.open_in_channel": "Open in channel",
|
||||
"file_type.audio": "Audio",
|
||||
"file_type.code": "Code file",
|
||||
"file_type.image": "Image",
|
||||
"file_type.patch": "Patch file",
|
||||
"file_type.pdf": "Acrobat",
|
||||
"file_type.presentation": "Powerpoint Presentation",
|
||||
"file_type.spreadsheet": "Excel spreadsheet",
|
||||
"file_type.svg": "Vector graphics",
|
||||
"file_type.text": "Text file",
|
||||
"file_type.video": "Video",
|
||||
"file_type.word": "Word Document",
|
||||
"file_upload.disabled": "File attachments are disabled.",
|
||||
"file_upload.drag_folder": "This attachment cannot be uploaded.",
|
||||
"file_upload.fileAbove": "File above {max}MB could not be uploaded: {filename}",
|
||||
@@ -4810,9 +4821,14 @@
|
||||
"saveChangesPanel.save": "Save",
|
||||
"saveChangesPanel.saved": "Settings saved",
|
||||
"saveChangesPanel.tryAgain": "Try again",
|
||||
"search_bar.channels": "Channels",
|
||||
"search_bar.clear": "Clear",
|
||||
"search_bar.file_types": "File types",
|
||||
"search_bar.files_tab": "Files",
|
||||
"search_bar.messages_tab": "Messages",
|
||||
"search_bar.search": "Search",
|
||||
"search_bar.search_files": "Search files",
|
||||
"search_bar.search_messages": "Search messages",
|
||||
"search_bar.search_types.files": "FILES",
|
||||
"search_bar.search_types.messages": "MESSAGES",
|
||||
"search_bar.searchGroupMembers": "Search group members",
|
||||
@@ -4823,6 +4839,7 @@
|
||||
"search_bar.usage.title": "Search options",
|
||||
"search_bar.usage.title_files": "File search options",
|
||||
"search_bar.usage.title_messages": "Message search options",
|
||||
"search_bar.users": "Users",
|
||||
"search_files_list_option.after": "Files after a date",
|
||||
"search_files_list_option.before": "Files before a date",
|
||||
"search_files_list_option.exclude": "Exclude search terms",
|
||||
@@ -4840,6 +4857,9 @@
|
||||
"search_header.title2": "Recent Mentions",
|
||||
"search_header.title3": "Saved messages",
|
||||
"search_header.title5": "Extra Information",
|
||||
"search_hint.enter_to_search": "Press Enter to search",
|
||||
"search_hint.enter_to_select": "Press Enter to select",
|
||||
"search_hint.filter": "Filter your search with:",
|
||||
"search_item.channelArchived": "Archived",
|
||||
"search_item.direct": "Direct Message (with {username})",
|
||||
"search_item.file_tag.direct_message": "Direct Message",
|
||||
|
||||
@@ -21,9 +21,9 @@ export function getUserCurrentTimezone(userTimezone?: UserTimezone): string {
|
||||
}
|
||||
|
||||
if (useAutomatic) {
|
||||
return automaticTimezone;
|
||||
return automaticTimezone || 'UTC';
|
||||
}
|
||||
return manualTimezone;
|
||||
return manualTimezone || 'UTC';
|
||||
}
|
||||
|
||||
export function getTimezoneRegion(timezone: string): string {
|
||||
|
||||
@@ -126,6 +126,25 @@ export default class PluginRegistry {
|
||||
return dispatchPluginComponentAction('PostMessageAttachment', this.id, component);
|
||||
});
|
||||
|
||||
// Register components for search.
|
||||
// Accepts React components. Returns a unique identifier.
|
||||
registerSearchComponents = ({buttonComponent, suggestionsComponent, hintsComponent, action}: any) => {
|
||||
const id = generateId();
|
||||
store.dispatch({
|
||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
||||
name: 'SearchButtons',
|
||||
data: {
|
||||
id,
|
||||
pluginId: this.id,
|
||||
component: buttonComponent,
|
||||
action,
|
||||
},
|
||||
});
|
||||
dispatchPluginComponentAction('SearchSuggestions', this.id, suggestionsComponent, id);
|
||||
dispatchPluginComponentAction('SearchHints', this.id, hintsComponent, id);
|
||||
return id;
|
||||
};
|
||||
|
||||
// Register a component to show as a tooltip when a user hovers on a link in a post.
|
||||
// Accepts a React component. Returns a unique identifier.
|
||||
// The component will be passed the following props:
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('Reducers.RHS', () => {
|
||||
searchTerms: '',
|
||||
searchType: '',
|
||||
searchResultsTerms: '',
|
||||
searchResultsType: '',
|
||||
size: 'medium',
|
||||
pluggableId: '',
|
||||
isSearchingFlaggedPost: false,
|
||||
@@ -617,6 +618,7 @@ describe('Reducers.RHS', () => {
|
||||
searchTerms: 'user_id',
|
||||
searchType: '',
|
||||
searchResultsTerms: 'user id',
|
||||
searchResultsType: '',
|
||||
size: 'medium',
|
||||
pluggableId: 'pluggable_id',
|
||||
isSearchingFlaggedPost: true,
|
||||
|
||||
@@ -255,6 +255,18 @@ function searchResultsTerms(state = '', action: AnyAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function searchResultsType(state = '', action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TYPE:
|
||||
return action.searchType;
|
||||
|
||||
case UserTypes.LOGOUT_SUCCESS:
|
||||
return '';
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function isSearchingFlaggedPost(state = false, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case SearchTypes.SEARCH_FLAGGED_POSTS_REQUEST:
|
||||
@@ -407,6 +419,7 @@ export default combineReducers({
|
||||
searchTerms,
|
||||
searchType,
|
||||
searchResultsTerms,
|
||||
searchResultsType,
|
||||
size,
|
||||
pluggableId,
|
||||
isSearchingFlaggedPost,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {AppBinding} from '@mattermost/types/apps';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {appBarEnabled, getAppBarAppBindings} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {createShallowSelector} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
@@ -108,3 +109,39 @@ export function showNewChannelWithBoardPulsatingDot(state: GlobalState): boolean
|
||||
const showPulsatingDot = pulsatingDotState !== '' && JSON.parse(pulsatingDotState)[Preferences.NEW_CHANNEL_WITH_BOARD_TOUR_SHOWED] === false;
|
||||
return showPulsatingDot;
|
||||
}
|
||||
|
||||
export const getSearchPluginSuggestions = createSelector(
|
||||
'getSearchPluginSuggestions',
|
||||
getLicense,
|
||||
(state: GlobalState) => state.plugins.components.SearchSuggestions,
|
||||
(license, components = []) => {
|
||||
if (license.IsLicensed !== 'true') {
|
||||
return [];
|
||||
}
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const getSearchBoxHints = createSelector(
|
||||
'getSearchBoxHints',
|
||||
getLicense,
|
||||
(state: GlobalState) => state.plugins.components.SearchHints,
|
||||
(license, components = []) => {
|
||||
if (license.IsLicensed !== 'true') {
|
||||
return [];
|
||||
}
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const getSearchButtons = createSelector(
|
||||
'getSearchButtons',
|
||||
getLicense,
|
||||
(state: GlobalState) => state.plugins.components.SearchButtons,
|
||||
(license, components = []) => {
|
||||
if (license.IsLicensed !== 'true') {
|
||||
return [];
|
||||
}
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -81,7 +81,12 @@ export const getSelectedPost = createSelector(
|
||||
getRealSelectedPost,
|
||||
getSelectedChannelId,
|
||||
getCurrentUserId,
|
||||
(selectedPostId: Post['id'], selectedPost: Post, selectedPostChannelId: Channel['id'], currentUserId): Post|FakePost => {
|
||||
(
|
||||
selectedPostId: Post['id'],
|
||||
selectedPost: Post,
|
||||
selectedPostChannelId: Channel['id'],
|
||||
currentUserId,
|
||||
): Post | FakePost => {
|
||||
if (selectedPost) {
|
||||
return selectedPost;
|
||||
}
|
||||
@@ -91,7 +96,11 @@ export const getSelectedPost = createSelector(
|
||||
id: selectedPostId,
|
||||
exists: false,
|
||||
type: PostTypes.FAKE_PARENT_DELETED as PostType,
|
||||
message: localizeMessage({id: 'rhs_thread.rootPostDeletedMessage.body', defaultMessage: 'Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.'}),
|
||||
message: localizeMessage({
|
||||
id: 'rhs_thread.rootPostDeletedMessage.body',
|
||||
defaultMessage:
|
||||
'Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.',
|
||||
}),
|
||||
channel_id: selectedPostChannelId,
|
||||
user_id: currentUserId,
|
||||
reply_count: 0,
|
||||
@@ -122,6 +131,10 @@ export function getSearchResultsTerms(state: GlobalState): string {
|
||||
return state.views.rhs.searchResultsTerms;
|
||||
}
|
||||
|
||||
export function getSearchResultsType(state: GlobalState): string {
|
||||
return state.views.rhs.searchResultsType;
|
||||
}
|
||||
|
||||
export function getIsSearchingTerm(state: GlobalState): boolean {
|
||||
return state.entities.search.isSearchingTerm;
|
||||
}
|
||||
@@ -139,10 +152,26 @@ export function getIsSearchGettingMore(state: GlobalState): boolean {
|
||||
}
|
||||
|
||||
export function makeGetDraft() {
|
||||
let defaultDraft = {message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''};
|
||||
let defaultDraft = {
|
||||
message: '',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
channelId: '',
|
||||
rootId: '',
|
||||
};
|
||||
return (state: GlobalState, channelId: string, rootId = ''): PostDraft => {
|
||||
if (defaultDraft.channelId !== channelId || defaultDraft.rootId !== rootId) {
|
||||
defaultDraft = {message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId, rootId};
|
||||
defaultDraft = {
|
||||
message: '',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
channelId,
|
||||
rootId,
|
||||
};
|
||||
}
|
||||
const prefix = rootId ? StoragePrefixes.COMMENT_DRAFT : StoragePrefixes.DRAFT;
|
||||
const suffix = rootId || channelId;
|
||||
@@ -170,7 +199,15 @@ export function makeGetDraft() {
|
||||
}
|
||||
|
||||
export function makeGetChannelDraft() {
|
||||
const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''});
|
||||
const defaultDraft = Object.freeze({
|
||||
message: '',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
channelId: '',
|
||||
rootId: '',
|
||||
});
|
||||
const getDraft = makeGetGlobalItemWithDefault(defaultDraft);
|
||||
|
||||
return (state: GlobalState, channelId?: string): PostDraft => {
|
||||
@@ -192,7 +229,15 @@ export function makeGetChannelDraft() {
|
||||
}
|
||||
|
||||
export function getPostDraft(state: GlobalState, prefixId: string, suffixId: string): PostDraft {
|
||||
const defaultDraft = {message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''};
|
||||
const defaultDraft = {
|
||||
message: '',
|
||||
fileInfos: [],
|
||||
uploadsInProgress: [],
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
channelId: '',
|
||||
rootId: '',
|
||||
};
|
||||
|
||||
if (prefixId === StoragePrefixes.COMMENT_DRAFT) {
|
||||
defaultDraft.rootId = suffixId;
|
||||
|
||||
@@ -34,6 +34,7 @@ export type RhsViewState = {
|
||||
searchType: SearchType;
|
||||
pluggableId: string;
|
||||
searchResultsTerms: string;
|
||||
searchResultsType: string;
|
||||
isSearchingFlaggedPost: boolean;
|
||||
isSearchingPinnedPost: boolean;
|
||||
isSidebarOpen: boolean;
|
||||
|
||||
@@ -197,6 +197,7 @@ export const ActionTypes = keyMirror({
|
||||
UPDATE_RHS_SEARCH_TERMS: null,
|
||||
UPDATE_RHS_SEARCH_TYPE: null,
|
||||
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
||||
UPDATE_RHS_SEARCH_RESULTS_TYPE: null,
|
||||
|
||||
SET_RHS_SIZE: null,
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user