diff --git a/i18n/en-US.properties b/i18n/en-US.properties index 2289c55bb6..5ec5d363d6 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -646,6 +646,8 @@ be.rootBreadcrumb = All Files be.save = Save # Shown as the title in the sub header while searching. be.searchBreadcrumb = Search Results +# Aria label for the clear button in the search box. +be.searchClear = Clear search # Shown as a placeholder in the search box. be.searchPlaceholder = Search files and folders # Default label for selected items list in the footer. diff --git a/src/elements/common/__mocks__/mockRecentItems.ts b/src/elements/common/__mocks__/mockRecentItems.ts new file mode 100644 index 0000000000..8b4c146fc8 --- /dev/null +++ b/src/elements/common/__mocks__/mockRecentItems.ts @@ -0,0 +1,238 @@ +const mockRecentItems = { + next_marker: '', + limit: 100, + order: { + by: 'interacted_at', + direction: 'DESC', + }, + entries: [ + { + type: 'recent_item', + interaction_type: 'item_preview', + interacted_at: '2025-01-31T13:40:24-08:00', + item: { + type: 'file', + id: '1222291629977', + etag: '1', + name: 'Resume (4).pdf', + size: 69266, + parent: { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + extension: 'pdf', + permissions: { + can_download: true, + can_preview: true, + can_upload: true, + can_comment: true, + can_rename: true, + can_delete: true, + can_share: true, + can_set_share_access: true, + can_invite_collaborator: true, + can_annotate: true, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: true, + can_view_annotations: true, + }, + path_collection: { + total_count: 1, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + ], + }, + modified_at: '2023-05-24T11:43:41-07:00', + created_at: '2023-05-24T11:43:41-07:00', + modified_by: { + type: 'user', + id: '24991742828', + name: 'GW', + login: 'gw@boxdemo.com', + }, + has_collaborations: false, + is_externally_owned: false, + authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/1222291629977/content', + is_download_available: true, + representations: { + entries: [ + { + representation: 'jpg', + properties: { + dimensions: '1024x1024', + paged: 'false', + thumb: 'false', + }, + info: { + url: 'https://api.box.com/2.0/internal_files/1222291629977/versions/1334003366777/representations/jpg_1024x1024', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/1222291629977/versions/1334003366777/representations/jpg_1024x1024/content/{+asset_path}', + }, + }, + ], + }, + file_version: { + type: 'file_version', + id: '1334003366777', + sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59', + }, + sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59', + shared_link: { + url: 'https://app.box.com/s/6wvvmdmgmo9ky62p054zbtgn1ph78els', + download_url: 'https://app.box.com/shared/static/6wvvmdmgmo9ky62p054zbtgn1ph78els.pdf', + vanity_url: null, + vanity_name: null, + effective_access: 'open', + effective_permission: 'can_download', + is_password_enabled: false, + unshared_at: null, + download_count: 0, + preview_count: 0, + access: 'open', + permissions: { + can_preview: true, + can_download: true, + can_edit: false, + }, + }, + watermark_info: { + is_watermarked: false, + }, + }, + interaction_shared_link: null, + }, + { + type: 'recent_item', + interaction_type: 'item_preview', + interacted_at: '2025-01-28T11:12:18-08:00', + item: { + type: 'file', + id: '1318276254035', + etag: '1', + name: 'Resume (2) (1).pdf', + size: 69266, + parent: { + type: 'folder', + id: '218662304788', + sequence_id: '0', + etag: '0', + name: 'metadataquery', + }, + extension: 'pdf', + permissions: { + can_download: true, + can_preview: true, + can_upload: true, + can_comment: true, + can_rename: true, + can_delete: true, + can_share: true, + can_set_share_access: true, + can_invite_collaborator: true, + can_annotate: true, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: true, + can_view_annotations: true, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '218662304788', + sequence_id: '0', + etag: '0', + name: 'metadataquery', + }, + ], + }, + modified_at: '2023-09-26T14:04:52-07:00', + created_at: '2023-09-26T14:04:52-07:00', + modified_by: { + type: 'user', + id: '24991742828', + name: 'GW', + login: 'gw@boxdemo.com', + }, + has_collaborations: false, + is_externally_owned: false, + authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/1318276254035/content', + is_download_available: true, + representations: { + entries: [ + { + representation: 'jpg', + properties: { + dimensions: '1024x1024', + paged: 'false', + thumb: 'false', + }, + info: { + url: 'https://api.box.com/2.0/internal_files/1318276254035/versions/1442246185235/representations/jpg_1024x1024', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/1318276254035/versions/1442246185235/representations/jpg_1024x1024/content/{+asset_path}', + }, + }, + ], + }, + file_version: { + type: 'file_version', + id: '1442246185235', + sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59', + }, + sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59', + shared_link: { + url: 'https://app.box.com/s/4isrn71f96h1q76dluj1a71h6o8d6z17', + download_url: 'https://app.box.com/shared/static/4isrn71f96h1q76dluj1a71h6o8d6z17.pdf', + vanity_url: null, + vanity_name: null, + effective_access: 'open', + effective_permission: 'can_download', + is_password_enabled: false, + unshared_at: null, + download_count: 0, + preview_count: 0, + access: 'open', + permissions: { + can_preview: true, + can_download: true, + can_edit: false, + }, + }, + watermark_info: { + is_watermarked: false, + }, + }, + interaction_shared_link: null, + }, + ], +}; +export default mockRecentItems; diff --git a/src/elements/common/header/Header.js b/src/elements/common/header/Header.js deleted file mode 100644 index 412c043674..0000000000 --- a/src/elements/common/header/Header.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @flow - * @file Header bar - * @author Box - */ - -import * as React from 'react'; -import { injectIntl } from 'react-intl'; -import Logo from './Logo'; -import messages from '../messages'; -import { VIEW_FOLDER, VIEW_SEARCH } from '../../../constants'; -import type { View } from '../../../common/types/core'; - -import './Header.scss'; - -type Props = { - intl: any, - isHeaderLogoVisible?: boolean, - isSmall: boolean, - logoUrl?: string, - onSearch: Function, - searchQuery: string, - view: View, -}; - -// eslint-disable-next-line react/prop-types -const Header = ({ isHeaderLogoVisible = true, view, isSmall, searchQuery, onSearch, logoUrl, intl }: Props) => { - const { formatMessage } = intl; - const search = ({ currentTarget }: { currentTarget: HTMLInputElement }) => onSearch(currentTarget.value); - const searchMessage = formatMessage(messages.searchPlaceholder); - const isFolder = view === VIEW_FOLDER; - const isSearch = view === VIEW_SEARCH; - - return ( -
- {isHeaderLogoVisible && } -
- -
-
- ); -}; - -export { Header as HeaderBase }; -export default injectIntl(Header); diff --git a/src/elements/common/header/Header.js.flow b/src/elements/common/header/Header.js.flow new file mode 100644 index 0000000000..232dc0daf2 --- /dev/null +++ b/src/elements/common/header/Header.js.flow @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { SearchInput } from '@box/blueprint-web'; +import Logo from './Logo'; +import messages from '../messages'; +import { VIEW_FOLDER, VIEW_SEARCH } from '../../../constants'; +import type { View } from '../../../common/types/core'; + +import './Header.scss'; + +type HeaderProps = { + isHeaderLogoVisible?: boolean, + logoUrl?: string, + onSearch: any, + view: View +}; + +const Header = ({ + isHeaderLogoVisible = true, + view, + onSearch, + logoUrl, +}: HeaderProps) => { + const { formatMessage } = useIntl(); + const searchMessage = formatMessage(messages.searchPlaceholder); + const isFolder = view === VIEW_FOLDER; + const isSearch = view === VIEW_SEARCH; + + return ( +
+ {isHeaderLogoVisible && } +
+ +
+
+ ); +}; + +export default Header; diff --git a/src/elements/common/header/Header.scss b/src/elements/common/header/Header.scss index 174ca91ac6..e41e428d19 100644 --- a/src/elements/common/header/Header.scss +++ b/src/elements/common/header/Header.scss @@ -5,8 +5,7 @@ flex: 0 0 70px; align-items: center; padding: 0 25px 0 0; - background: $almost-white; - border-bottom: 1px solid $bdl-gray-10; + border-bottom: 1px solid var(--border-divider-border); .be-is-small & { padding-right: 20px; @@ -14,6 +13,7 @@ .be-search { flex: 1; + max-width: 520px; padding-left: 20px; } diff --git a/src/elements/common/header/Header.tsx b/src/elements/common/header/Header.tsx new file mode 100644 index 0000000000..5f0451aeb9 --- /dev/null +++ b/src/elements/common/header/Header.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { SearchInput } from '@box/blueprint-web'; +import Logo from './Logo'; +import messages from '../messages'; +import { VIEW_FOLDER, VIEW_SEARCH } from '../../../constants'; +import type { View } from '../../../common/types/core'; + +import './Header.scss'; + +export interface HeaderProps { + isHeaderLogoVisible?: boolean; + logoUrl?: string; + onSearch: (value: string) => void; + view: View; +} + +const Header = ({ isHeaderLogoVisible = true, logoUrl, onSearch, view }: HeaderProps) => { + const { formatMessage } = useIntl(); + const searchMessage = formatMessage(messages.searchPlaceholder); + const isFolder = view === VIEW_FOLDER; + const isSearch = view === VIEW_SEARCH; + + return ( +
+ {isHeaderLogoVisible && } +
+ +
+
+ ); +}; + +export default Header; diff --git a/src/elements/common/header/Logo.js b/src/elements/common/header/Logo.js.flow similarity index 85% rename from src/elements/common/header/Logo.js rename to src/elements/common/header/Logo.js.flow index cf073f539e..edd09a2fd2 100644 --- a/src/elements/common/header/Logo.js +++ b/src/elements/common/header/Logo.js.flow @@ -1,9 +1,3 @@ -/** - * @flow - * @file Logo for the header - * @author Box - */ - import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import IconLogo from '../../../icons/general/IconLogo'; @@ -11,7 +5,7 @@ import messages from '../messages'; import './Logo.scss'; type Props = { - url?: string, + url?: string }; function getLogo(url?: string) { @@ -29,7 +23,9 @@ function getLogo(url?: string) { ); } -const Logo = ({ url }: Props) => ( +const Logo = ({ + url, +}: Props) => (
{getLogo(url)}
diff --git a/src/elements/common/header/Logo.scss b/src/elements/common/header/Logo.scss index 2d16fa03e8..21adf093e2 100644 --- a/src/elements/common/header/Logo.scss +++ b/src/elements/common/header/Logo.scss @@ -4,8 +4,8 @@ padding-left: 20px; .be-logo-custom { - max-width: 80px; - max-height: 32px; + max-width: 240px; + max-height: 40px; .be-is-small & { max-width: 75px; @@ -16,9 +16,9 @@ display: flex; align-items: center; justify-content: center; - width: 75px; - height: 32px; - background-color: $bdl-gray-10; + width: 208px; + height: 48px; + background-color: var(--gray-10); border: 1px dashed; .be-is-small & { diff --git a/src/elements/common/header/Logo.tsx b/src/elements/common/header/Logo.tsx new file mode 100644 index 0000000000..257c299ddf --- /dev/null +++ b/src/elements/common/header/Logo.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { BoxLogo } from '@box/blueprint-web-assets/icons/Logo'; +import messages from '../messages'; +import './Logo.scss'; + +export interface LogoProps { + url?: string; +} + +function getLogo(url?: string) { + if (url === 'box') { + return ; + } + + if (typeof url === 'string') { + return ; + } + + return ( +
+ +
+ ); +} + +const Logo = ({ url }: LogoProps) => ( +
+ {getLogo(url)} +
+); + +export default Logo; diff --git a/src/elements/common/header/__tests__/Header.test.js b/src/elements/common/header/__tests__/Header.test.js deleted file mode 100644 index 814b01508a..0000000000 --- a/src/elements/common/header/__tests__/Header.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { render, screen } from '@testing-library/react'; -import { HeaderBase as Header } from '../Header'; - -describe('elements/common/header/Header', () => { - const intl = { - formatMessage: jest.fn().mockImplementation(message => message.defaultMessage), - }; - - const renderComponent = props => render(
); - - test('renders Logo component when isHeaderLogoVisible is `true`', () => { - renderComponent({ isHeaderLogoVisible: true }); - expect(screen.getByTestId('be-Logo')).toBeInTheDocument(); - }); - - test('does not render Logo component when isHeaderLogoVisible is `false`', () => { - renderComponent({ isHeaderLogoVisible: false }); - expect(screen.queryByTestId('be-Logo')).not.toBeInTheDocument(); - }); - - test('renders matching values for aria-label and placeholder attributes', () => { - renderComponent(); - const searchInput = screen.getByTestId('be-Header-searchInput'); - const searchMessage = 'Search files and folders'; - - expect(searchInput.getAttribute('aria-label')).toBe(searchMessage); - expect(searchInput.getAttribute('placeholder')).toBe(searchMessage); - }); - - test('disables search input when view is not `folder` and not `search`', () => { - renderComponent({ view: 'recents' }); - expect(screen.getByTestId('be-Header-searchInput')).toBeDisabled(); - }); - - test.each(['folder', 'search'])('does not disable search input when view is %s', view => { - renderComponent({ view }); - expect(screen.getByTestId('be-Header-searchInput')).not.toBeDisabled(); - }); -}); diff --git a/src/elements/common/header/__tests__/Header.test.tsx b/src/elements/common/header/__tests__/Header.test.tsx new file mode 100644 index 0000000000..3a14166ece --- /dev/null +++ b/src/elements/common/header/__tests__/Header.test.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { userEvent } from '@testing-library/user-event'; +import { render, screen } from '../../../../test-utils/testing-library'; +import Header, { HeaderProps } from '../Header'; + +jest.mock('@box/blueprint-web-assets/icons/Logo', () => { + return { + BoxLogo: () =>
BoxLogo
, + }; +}); + +describe('elements/common/header/Header', () => { + const renderComponent = (props?: Partial) => + render(
); + + test('disables search input when view is not `folder` and not `search`', () => { + renderComponent({ view: 'recents' }); + expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument(); + expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeDisabled(); + }); + + test.each(['folder', 'search'])('does not disable search input when view is %s', view => { + renderComponent({ view }); + + expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument(); + expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).not.toBeDisabled(); + }); + + test('onSearch is called when search input changes', async () => { + const onSearch = jest.fn(); + renderComponent({ onSearch }); + const searchInput = screen.getByRole('searchbox', { name: 'Search files and folders' }); + + expect(onSearch).not.toHaveBeenCalled(); + await userEvent.type(searchInput, 'test'); + expect(onSearch).toHaveBeenCalled(); + }); + + describe('Logo', () => { + test('renders Logo component when isHeaderLogoVisible is `true`', () => { + renderComponent({ isHeaderLogoVisible: true }); + expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument(); + + expect(screen.getByText('Logo')).toBeInTheDocument(); + }); + + test('does not render Logo component when isHeaderLogoVisible is `false`', () => { + renderComponent({ isHeaderLogoVisible: false }); + expect(screen.queryByText('Logo')).not.toBeInTheDocument(); + expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument(); + }); + + test('renders BoxLogo component when logoUrl is `box`', () => { + renderComponent({ logoUrl: 'box' }); + expect(screen.getByText('BoxLogo')).toBeInTheDocument(); + }); + + test('renders custom logo component when logoUrl is a string', () => { + renderComponent({ logoUrl: 'https://example.com/logo.png' }); + expect(screen.getByRole('presentation')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/elements/common/header/index.js b/src/elements/common/header/index.js.flow similarity index 80% rename from src/elements/common/header/index.js rename to src/elements/common/header/index.js.flow index 31c1b1dcee..579f1ac23f 100644 --- a/src/elements/common/header/index.js +++ b/src/elements/common/header/index.js.flow @@ -1,2 +1 @@ -// @flow export { default } from './Header'; diff --git a/src/elements/common/header/index.ts b/src/elements/common/header/index.ts new file mode 100644 index 0000000000..579f1ac23f --- /dev/null +++ b/src/elements/common/header/index.ts @@ -0,0 +1 @@ +export { default } from './Header'; diff --git a/src/elements/common/messages.js b/src/elements/common/messages.js index 62ec32afd7..140acf9ee2 100644 --- a/src/elements/common/messages.js +++ b/src/elements/common/messages.js @@ -377,6 +377,11 @@ const messages = defineMessages({ description: 'Dropdown select option to remove access.', defaultMessage: 'Remove shared link', }, + searchClear: { + id: 'be.searchClear', + description: 'Aria label for the clear button in the search box.', + defaultMessage: 'Clear search', + }, searchPlaceholder: { id: 'be.searchPlaceholder', description: 'Shown as a placeholder in the search box.', diff --git a/src/elements/common/sub-header/SubHeader.scss b/src/elements/common/sub-header/SubHeader.scss index 3f249854ac..d8a3bc8bc0 100644 --- a/src/elements/common/sub-header/SubHeader.scss +++ b/src/elements/common/sub-header/SubHeader.scss @@ -6,7 +6,7 @@ align-items: center; justify-content: space-between; padding: 0 20px 0 25px; - border-bottom: 1px solid $bdl-gray-10; + border-bottom: 1px solid var(--border-divider-border); box-shadow: 0 4px 6px -2px $transparent-black; .be-is-small & { diff --git a/src/elements/content-explorer/ContentExplorer.js b/src/elements/content-explorer/ContentExplorer.js index 41c4848983..79259cc4da 100644 --- a/src/elements/content-explorer/ContentExplorer.js +++ b/src/elements/content-explorer/ContentExplorer.js @@ -1643,7 +1643,6 @@ class ContentExplorer extends Component { isUploadModalOpen, markers, rootName, - searchQuery, selected, view, }: State = this.state; @@ -1672,13 +1671,7 @@ class ContentExplorer extends Component {
{!isDefaultViewMetadata && ( <> -
+
{ return new HttpResponse('Internal Server Error', { status: 500 }); }), + http.get(`${DEFAULT_HOSTNAME_API}/2.0/recent_items`, () => { + return HttpResponse.json(mockRecentItems); + }), ], }, }, diff --git a/src/elements/content-picker/ContentPicker.js b/src/elements/content-picker/ContentPicker.js index f7966c892e..892b1c043b 100644 --- a/src/elements/content-picker/ContentPicker.js +++ b/src/elements/content-picker/ContentPicker.js @@ -1244,7 +1244,6 @@ class ContentPicker extends Component {