From 8c7c6212059ec92313b9abe58b39a20580a6c353 Mon Sep 17 00:00:00 2001 From: Bree Hall <40739624+breehall@users.noreply.github.com> Date: Tue, 1 Aug 2023 04:07:25 -0400 Subject: [PATCH] [Cases] Replace `EditableTitle` component with `EuiInlineEdit` component (#162095) Included in https://github.com/elastic/eui/issues/6778 Hi team! EUI recently released the EuiInlineEdit component and the Cases page title was identified as a good candidate for the new component. This PR is replaces the inner workings of the EditableTitle component and replaces it with the new EuiInlineEdit component. ## Summary Replace inner component within `EditableTitle` to use to the new `EuiInlineEdit` component. **Read Mode** image --- **Edit Mode** image --- **Insufficient Permissions** image --- **Error States** image image --- **Release Phases** image image --- ### Checklist Delete any items that are not applicable to this PR. - [ ] ~Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)~ - [ ] ~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials~ - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] ~If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~ - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../header_page/editable_title.test.tsx | 83 +++++---- .../components/header_page/editable_title.tsx | 168 +++++++----------- .../public/components/header_page/index.tsx | 2 +- .../public/components/header_page/title.tsx | 2 + .../cypress/screens/case_details.ts | 2 +- .../services/cases/single_case_view.ts | 4 +- .../apps/cases/group1/create_case_form.ts | 2 +- .../apps/cases/group1/view_case.ts | 6 +- .../apps/cases/group2/attachment_framework.ts | 4 +- .../apps/cases/group2/upgrade.ts | 2 +- 10 files changed, 120 insertions(+), 155 deletions(-) diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx index b8486e67dff759..f7e4df4d6a2e21 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx @@ -35,14 +35,16 @@ describe('EditableTitle', () => { expect(renderResult.getByText('Test title')).toBeInTheDocument(); }); - it('does not show the edit icon when the user does not have edit permissions', () => { + it('inline edit defaults to readOnly when the user does not have the edit permissions', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').exists()).toBeFalsy(); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').prop('disabled') + ).toBe(true); }); it('shows the edit title input field', () => { @@ -52,7 +54,7 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); expect(wrapper.find('[data-test-subj="editable-title-input-field"]').first().exists()).toBe( @@ -67,12 +69,12 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); - expect(wrapper.find('[data-test-subj="editable-title-submit-btn"]').first().exists()).toBe( - true - ); + expect( + wrapper.find('button[data-test-subj="editable-title-submit-btn"]').first().exists() + ).toBe(true); }); it('shows the cancel button', () => { @@ -82,27 +84,12 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); - wrapper.update(); - - expect(wrapper.find('[data-test-subj="editable-title-cancel-btn"]').first().exists()).toBe( - true - ); - }); - - it('DOES NOT shows the edit icon when in edit mode', () => { - const wrapper = mount( - - - - ); - - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); - expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe( - false - ); + expect( + wrapper.find('button[data-test-subj="editable-title-cancel-btn"]').first().exists() + ).toBe(true); }); it('switch to non edit mode when canceled', () => { @@ -112,11 +99,13 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); wrapper.find('button[data-test-subj="editable-title-cancel-btn"]').simulate('click'); - expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(true); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists() + ).toBe(true); }); it('should change the title', () => { @@ -128,7 +117,7 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); wrapper @@ -152,18 +141,21 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); wrapper .find('input[data-test-subj="editable-title-input-field"]') + .simulate('change', { target: { value: newTitle } }); wrapper.update(); wrapper.find('button[data-test-subj="editable-title-cancel-btn"]').simulate('click'); wrapper.update(); - expect(wrapper.find('h1[data-test-subj="header-page-title"]').text()).toEqual(title); + expect(wrapper.find('button[data-test-subj="editable-title-header-value"]').text()).toEqual( + title + ); }); it('submits the title', () => { @@ -175,11 +167,12 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); wrapper .find('input[data-test-subj="editable-title-input-field"]') + .last() .simulate('change', { target: { value: newTitle } }); wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click'); @@ -187,7 +180,9 @@ describe('EditableTitle', () => { expect(submitTitle).toHaveBeenCalled(); expect(submitTitle.mock.calls[0][0]).toEqual(newTitle); - expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe(true); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists() + ).toBe(true); }); it('does not submit the title when the length is longer than 160 characters', () => { @@ -199,7 +194,7 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); wrapper @@ -213,9 +208,9 @@ describe('EditableTitle', () => { ); expect(submitTitle).not.toHaveBeenCalled(); - expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe( - false - ); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists() + ).toBe(false); }); it('does not submit the title is empty', () => { @@ -225,11 +220,12 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); wrapper .find('input[data-test-subj="editable-title-input-field"]') + .simulate('change', { target: { value: '' } }); wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click'); @@ -237,9 +233,9 @@ describe('EditableTitle', () => { expect(wrapper.find('.euiFormErrorText').text()).toBe('A name is required.'); expect(submitTitle).not.toHaveBeenCalled(); - expect(wrapper.find('[data-test-subj="editable-title-edit-icon"]').first().exists()).toBe( - false - ); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists() + ).toBe(false); }); it('does not show an error after a previous edit error was displayed', () => { @@ -252,7 +248,7 @@ describe('EditableTitle', () => { ); - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); // simualte a long title @@ -269,6 +265,7 @@ describe('EditableTitle', () => { // write a shorter one wrapper .find('input[data-test-subj="editable-title-input-field"]') + .simulate('change', { target: { value: shortTitle } }); wrapper.update(); @@ -277,7 +274,7 @@ describe('EditableTitle', () => { wrapper.update(); // edit again - wrapper.find('button[data-test-subj="editable-title-edit-icon"]').simulate('click'); + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); wrapper.update(); // no error should appear diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx index fdf5b09ca3c64d..9ddb6978c35cd9 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx @@ -5,38 +5,15 @@ * 2.0. */ -import type { ChangeEvent } from 'react'; import React, { useState, useCallback } from 'react'; -import styled, { css } from 'styled-components'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFieldText, - EuiButtonIcon, - EuiLoadingSpinner, - EuiFormRow, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiInlineEditTitle } from '@elastic/eui'; import { MAX_TITLE_LENGTH } from '../../../common/constants'; import * as i18n from './translations'; -import { Title } from './title'; +import { TitleExperimentalBadge, TitleBetaBadge } from './title'; import { useCasesContext } from '../cases_context/use_cases_context'; -const MyEuiButtonIcon = styled(EuiButtonIcon)` - ${({ theme }) => css` - margin-left: ${theme.eui.euiSize}; - `} -`; - -const MySpinner = styled(EuiLoadingSpinner)` - ${({ theme }) => css` - margin-left: ${theme.eui.euiSize}; - `} -`; - export interface EditableTitleProps { isLoading: boolean; title: string; @@ -47,92 +24,81 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoad const { releasePhase, permissions } = useCasesContext(); const [editMode, setEditMode] = useState(false); const [errors, setErrors] = useState([]); - const [newTitle, setNewTitle] = useState(title); - const onCancel = useCallback(() => { - setEditMode(false); - setErrors([]); - setNewTitle(title); - }, [title]); + const onClickSubmit = useCallback( + (newTitleValue: string): boolean => { + if (!newTitleValue.trim().length) { + setErrors([i18n.TITLE_REQUIRED]); + return false; + } - const onClickEditIcon = useCallback(() => setEditMode(true), []); - const onClickSubmit = useCallback((): void => { - if (!newTitle.trim().length) { - setErrors([i18n.TITLE_REQUIRED]); - return; - } + if (newTitleValue.trim().length > MAX_TITLE_LENGTH) { + setErrors([i18n.MAX_LENGTH_ERROR('title', MAX_TITLE_LENGTH)]); + return false; + } - if (newTitle.trim().length > MAX_TITLE_LENGTH) { - setErrors([i18n.MAX_LENGTH_ERROR('title', MAX_TITLE_LENGTH)]); - return; - } + if (newTitleValue !== title) { + onSubmit(newTitleValue.trim()); + } + setEditMode(false); + setErrors([]); - if (newTitle !== title) { - onSubmit(newTitle); - } - setEditMode(false); - setErrors([]); - }, [newTitle, onSubmit, title]); + return true; + }, + [onSubmit, title] + ); - const handleOnChange = useCallback((e: ChangeEvent) => { - setNewTitle(e.target.value); + const onCancel = () => { setErrors([]); - }, []); + setEditMode(false); + }; const hasErrors = errors.length > 0; - return editMode ? ( - - - - - - - - {i18n.SAVE} - - - - - {i18n.CANCEL} - - - - - ) : ( - - {isLoading && <MySpinner data-test-subj="editable-title-loading" />} - {!isLoading && permissions.update && ( - <MyEuiButtonIcon - aria-label={i18n.EDIT_TITLE_ARIA(title as string)} - iconType="pencil" - onClick={onClickEditIcon} - data-test-subj="editable-title-edit-icon" + return ( + <EuiFlexGroup> + <EuiFlexItem grow={true} css={releasePhase && { overflow: 'hidden' }}> + <EuiInlineEditTitle + defaultValue={title} + readModeProps={{ + onClick: () => setEditMode(true), + 'data-test-subj': 'editable-title-header-value', + }} + editModeProps={{ + formRowProps: { error: errors }, + inputProps: { + 'data-test-subj': 'editable-title-input-field', + onChange: () => { + setErrors([]); + }, + }, + saveButtonProps: { + 'data-test-subj': 'editable-title-submit-btn', + isDisabled: hasErrors, + }, + cancelButtonProps: { + onClick: () => onCancel(), + 'data-test-subj': 'editable-title-cancel-btn', + }, + }} + inputAriaLabel="Editable title input field" + heading="h1" + size="s" + isInvalid={hasErrors} + isLoading={isLoading} + isReadOnly={!permissions.update} + onSave={(value) => { + return onClickSubmit(value); + }} + startWithEditOpen={editMode} + data-test-subj="header-page-title" /> - )} - + + + {releasePhase === 'experimental' && } + {releasePhase === 'beta' && } + + ); }; EditableTitleComponent.displayName = 'EditableTitle'; diff --git a/x-pack/plugins/cases/public/components/header_page/index.tsx b/x-pack/plugins/cases/public/components/header_page/index.tsx index 9d267d30b0dfa7..c820de4b25fc1e 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.tsx @@ -87,7 +87,7 @@ const HeaderPageComponent: React.FC = ({ return (
- + {showBackButton && ( ( ); ExperimentalBadge.displayName = 'ExperimentalBadge'; +export const TitleExperimentalBadge = React.memo(ExperimentalBadge); const BetaBadge: React.FC = () => ( ); BetaBadge.displayName = 'BetaBadge'; +export const TitleBetaBadge = React.memo(BetaBadge); const TitleComponent: React.FC = ({ title, releasePhase, children }) => ( diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 271ef54922d5d5..1b4a6cab6524af 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -14,7 +14,7 @@ export const CASE_DELETE = '[data-test-subj="property-actions-trash"]'; export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="description"] [data-test-subj="scrollable-markdown"]'; -export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]'; +export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="editable-title-header-value"]'; export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status-dropdown"]'; diff --git a/x-pack/test/functional/services/cases/single_case_view.ts b/x-pack/test/functional/services/cases/single_case_view.ts index cf27d4c2498d58..008f89c5b3ee88 100644 --- a/x-pack/test/functional/services/cases/single_case_view.ts +++ b/x-pack/test/functional/services/cases/single_case_view.ts @@ -110,7 +110,7 @@ export function CasesSingleViewServiceProvider({ getService, getPageObject }: Ft }, async assertCaseTitle(expectedTitle: string) { - const actionTitle = await testSubjects.getVisibleText('header-page-title'); + const actionTitle = await testSubjects.getVisibleText('editable-title-header-value'); expect(actionTitle).to.eql( expectedTitle, `Expected case title to be '${expectedTitle}' (got '${actionTitle}')` @@ -138,7 +138,7 @@ export function CasesSingleViewServiceProvider({ getService, getPageObject }: Ft async closeAssigneesPopover() { await retry.try(async () => { // Click somewhere outside the popover - await testSubjects.click('header-page-title'); + await testSubjects.click('editable-title-header-value'); await header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('euiSelectableList'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts index 51b6ecbe73d8bd..7eec8e96ef3809 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts @@ -48,7 +48,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { }); // validate title - const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).equal(caseTitle); // validate description diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts index 0790b9d66ddf2d..ab4ed593368268 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts @@ -58,13 +58,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('edits a case title from the case view page', async () => { const newTitle = `test-${uuidv4()}`; - await testSubjects.click('editable-title-edit-icon'); + await testSubjects.click('editable-title-header-value'); await testSubjects.setValue('editable-title-input-field', newTitle); await testSubjects.click('editable-title-submit-btn'); // wait for backend response await retry.tryForTime(5000, async () => { - const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).equal(newTitle); }); @@ -75,7 +75,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('shows error message when title is more than 160 characters', async () => { const longTitle = Array(161).fill('x').toString(); - await testSubjects.click('editable-title-edit-icon'); + await testSubjects.click('editable-title-header-value'); await testSubjects.setValue('editable-title-input-field', longTitle); await testSubjects.click('editable-title-submit-btn'); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index 648362070907a2..d3ce3d1a85d7ff 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -385,7 +385,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.common.expectToasterToContain(`${caseTitle} has been updated`); await testSubjects.click('toaster-content-case-view-link'); - const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(caseTitle); await testSubjects.existOrFail('comment-persistableState-.lens'); @@ -412,7 +412,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`); await testSubjects.click('toaster-content-case-view-link'); - const title = await find.byCssSelector('[data-test-subj="header-page-title"]'); + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); expect(await title.getVisibleText()).toEqual(theCaseTitle); await testSubjects.existOrFail('comment-persistableState-.lens'); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/upgrade.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/upgrade.ts index 93d12b5d908daf..4076ddf286d8fd 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/upgrade.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/upgrade.ts @@ -80,7 +80,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('shows the title correctly', async () => { - const title = await testSubjects.find('header-page-title'); + const title = await testSubjects.find('editable-title-header-value'); expect(await title.getVisibleText()).equal('Upgrade test in Kibana'); });