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**
---
**Edit Mode**
---
**Insufficient Permissions**
---
**Error States**
---
**Release Phases**
---
### 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 && }
- {!isLoading && permissions.update && (
-
+
+ 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');
});