From d121368e6f8c252a4e0e35f729b9265d05fa5047 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Mon, 21 Nov 2022 15:12:54 +0100 Subject: [PATCH 01/32] feat: store draft description and comment --- .../components/markdown_editor/eui_form.tsx | 27 +++++++++++++++++-- .../user_actions/comment/comment.tsx | 1 + .../components/user_actions/comment/user.tsx | 3 +++ .../components/user_actions/description.tsx | 1 + .../components/user_actions/markdown_form.tsx | 4 ++- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index ff073f7eef08a..b7b39ec277ce5 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { forwardRef, useMemo } from 'react'; +import React, { forwardRef, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -14,6 +14,7 @@ import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static import type { MarkdownEditorRef } from './editor'; import { MarkdownEditor } from './editor'; import { CommentEditorContext } from './context'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -24,6 +25,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { bottomRightContent?: React.ReactNode; caseTitle?: string; caseTags?: string[]; + caseId: string; disabledUiPlugins?: string[]; }; @@ -44,11 +46,14 @@ export const MarkdownEditorForm = React.memo( bottomRightContent, caseTitle, caseTags, + caseId, disabledUiPlugins, }, ref ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [contentForLocalStorage, setContentForLocalStorage] = useLocalStorage(`${caseId}.${id}.caseView.markdownEditor`, field.value); + const commentEditorContextValue = useMemo( () => ({ @@ -60,6 +65,24 @@ export const MarkdownEditorForm = React.memo( [id, field.value, caseTitle, caseTags] ); + useEffect(() => { + console.log('useeffect', contentForLocalStorage, field); + if (contentForLocalStorage && field.value !== contentForLocalStorage) { + field.setValue(contentForLocalStorage) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // console.log('eui_form caseId', caseId, 'field value', field.value, 'field id', id ); + + + const onChange = (value: string) => { + console.log('onchange', value) + field.setValue(value); + setContentForLocalStorage(value); + } + + return ( & { comment: SnakeToCamelCase; + caseId: string; outlined: boolean; isEdit: boolean; isLoading: boolean; @@ -40,6 +41,7 @@ export const createUserAttachmentUserActionBuilder = ({ isEdit, isLoading, commentRefs, + caseId, handleManageMarkdownEditId, handleSaveComment, handleManageQuote, @@ -65,6 +67,7 @@ export const createUserAttachmentUserActionBuilder = ({ id={comment.id} content={comment.comment} isEditable={isEdit} + caseId={caseId} onChangeEditable={handleManageMarkdownEditId} onSaveContent={handleSaveComment.bind(null, { id: comment.id, diff --git a/x-pack/plugins/cases/public/components/user_actions/description.tsx b/x-pack/plugins/cases/public/components/user_actions/description.tsx index 19874dc05f2d0..754ce7bfd1f34 100644 --- a/x-pack/plugins/cases/public/components/user_actions/description.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/description.tsx @@ -54,6 +54,7 @@ export const getDescriptionUserAction = ({ (commentRefs.current[DESCRIPTION_ID] = element)} + caseId={caseData.id} id={DESCRIPTION_ID} content={caseData.description} isEditable={isEditable} diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index b2b7443e001e8..f10becefd3728 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -22,6 +22,7 @@ export const ContentWrapper = styled.div` interface UserActionMarkdownProps { content: string; id: string; + caseId: string; isEditable: boolean; onChangeEditable: (id: string) => void; onSaveContent: (content: string) => void; @@ -34,7 +35,7 @@ export interface UserActionMarkdownRefObject { const UserActionMarkdownComponent = forwardRef< UserActionMarkdownRefObject, UserActionMarkdownProps ->(({ id, content, isEditable, onChangeEditable, onSaveContent }, ref) => { +>(({ id, content, caseId, isEditable, onChangeEditable, onSaveContent }, ref) => { const editorRef = useRef(); const initialState = { content }; const { form } = useForm({ @@ -111,6 +112,7 @@ const UserActionMarkdownComponent = forwardRef< 'aria-label': 'Cases markdown editor', value: content, id, + caseId, bottomRightContent: EditorButtons, }} /> From 58925e1a065bcbe37912d530563de982005646fd Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 23 Nov 2022 17:29:57 +0100 Subject: [PATCH 02/32] feat: save draft comments to session storage using storage kibana utils --- .../public/components/add_comment/index.tsx | 6 ++++ .../components/markdown_editor/eui_form.tsx | 33 +++++++++---------- .../components/user_actions/markdown_form.tsx | 9 +++-- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index dc5c1c48cfd9b..af64e196827ea 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -12,6 +12,7 @@ import React, { useImperativeHandle, useEffect, useState, + useMemo, } from 'react'; import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; @@ -28,6 +29,7 @@ import { useCreateAttachments } from '../../containers/use_create_attachments'; import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; @@ -66,10 +68,12 @@ export const AddComment = React.memo( { id, caseId, onCommentPosted, onCommentSaving, showLoading = true, statusActionButton }, ref ) => { + const storage = useMemo(() => new Storage(window.sessionStorage), []); const editorRef = useRef(null); const [focusOnContext, setFocusOnContext] = useState(false); const { permissions, owner } = useCasesContext(); const { isLoading, createAttachments } = useCreateAttachments(); + const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor` const { form } = useForm({ defaultValue: initialCommentValue, @@ -116,6 +120,7 @@ export const AddComment = React.memo( data: [{ ...data, type: CommentType.user }], updateCase: onCommentPosted, }); + storage.remove(draftCommentStorageKey); reset(); } }, [submit, onCommentSaving, createAttachments, caseId, owner, onCommentPosted, reset]); @@ -163,6 +168,7 @@ export const AddComment = React.memo( componentProps={{ ref: editorRef, id, + draftCommentStorageKey, idAria: 'caseComment', isDisabled: isLoading, dataTestSubj: 'add-comment', diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index b7b39ec277ce5..6885f2289e79e 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { forwardRef, useEffect, useMemo } from 'react'; +import React, { forwardRef, useEffect, useMemo, useCallback, useState } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -14,7 +14,7 @@ import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static import type { MarkdownEditorRef } from './editor'; import { MarkdownEditor } from './editor'; import { CommentEditorContext } from './context'; -import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -25,7 +25,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { bottomRightContent?: React.ReactNode; caseTitle?: string; caseTags?: string[]; - caseId: string; + draftCommentStorageKey?: string; disabledUiPlugins?: string[]; }; @@ -46,14 +46,12 @@ export const MarkdownEditorForm = React.memo( bottomRightContent, caseTitle, caseTags, - caseId, + draftCommentStorageKey, disabledUiPlugins, }, ref ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [contentForLocalStorage, setContentForLocalStorage] = useLocalStorage(`${caseId}.${id}.caseView.markdownEditor`, field.value); - const commentEditorContextValue = useMemo( () => ({ @@ -65,22 +63,21 @@ export const MarkdownEditorForm = React.memo( [id, field.value, caseTitle, caseTags] ); + const storage = useMemo(() => new Storage(window.sessionStorage), []); + useEffect(() => { - console.log('useeffect', contentForLocalStorage, field); - if (contentForLocalStorage && field.value !== contentForLocalStorage) { - field.setValue(contentForLocalStorage) + const storageDraftComment = draftCommentStorageKey && storage.get(draftCommentStorageKey); + if(storageDraftComment && storageDraftComment!=='') { + field.setValue(storageDraftComment); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - - // console.log('eui_form caseId', caseId, 'field value', field.value, 'field id', id ); - - const onChange = (value: string) => { - console.log('onchange', value) - field.setValue(value); - setContentForLocalStorage(value); - } + const onChange = useCallback((value: string) => { + field.setValue(value); + if (draftCommentStorageKey) { + storage.set(draftCommentStorageKey, value); + } + },[field, storage]); return ( diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index f10becefd3728..47793a1055bd8 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; @@ -14,6 +14,7 @@ import * as i18n from '../case_view/translations'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; export const ContentWrapper = styled.div` padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; @@ -38,6 +39,7 @@ const UserActionMarkdownComponent = forwardRef< >(({ id, content, caseId, isEditable, onChangeEditable, onSaveContent }, ref) => { const editorRef = useRef(); const initialState = { content }; + const storage = useMemo(() => new Storage(window.sessionStorage), []); const { form } = useForm({ defaultValue: initialState, options: { stripEmptyFields: false }, @@ -45,9 +47,11 @@ const UserActionMarkdownComponent = forwardRef< }); const fieldName = 'content'; + const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor` const { setFieldValue, submit } = form; const handleCancelAction = useCallback(() => { + storage.remove(draftCommentStorageKey); onChangeEditable(id); }, [id, onChangeEditable]); @@ -57,6 +61,7 @@ const UserActionMarkdownComponent = forwardRef< if (isValid && data.content !== content) { onSaveContent(data.content); } + storage.remove(draftCommentStorageKey); onChangeEditable(id); }, [content, id, onChangeEditable, onSaveContent, submit]); @@ -112,7 +117,7 @@ const UserActionMarkdownComponent = forwardRef< 'aria-label': 'Cases markdown editor', value: content, id, - caseId, + draftCommentStorageKey, bottomRightContent: EditorButtons, }} /> From cffe533a72c833f6e2093fb533ebb42151fa9a02 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Thu, 24 Nov 2022 18:21:37 +0100 Subject: [PATCH 03/32] feat: added unit test and e2e UI tests --- .../components/markdown_editor/eui_form.tsx | 20 +-- .../components/user_actions/index.test.tsx | 121 ++++++++++++++++++ .../apps/cases/view_case.ts | 87 +++++++++++++ 3 files changed, 218 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 6885f2289e79e..3eb085cf41a76 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import React, { forwardRef, useEffect, useMemo, useCallback, useState } from 'react'; +import React, { forwardRef, useEffect, useMemo, useCallback } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { MarkdownEditorRef } from './editor'; import { MarkdownEditor } from './editor'; import { CommentEditorContext } from './context'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -67,18 +67,18 @@ export const MarkdownEditorForm = React.memo( useEffect(() => { const storageDraftComment = draftCommentStorageKey && storage.get(draftCommentStorageKey); - if(storageDraftComment && storageDraftComment!=='') { + if( storageDraftComment && storageDraftComment!=='' ) { field.setValue(storageDraftComment); } }, []); - const onChange = useCallback((value: string) => { - field.setValue(value); - if (draftCommentStorageKey) { - storage.set(draftCommentStorageKey, value); - } + const handleOnChange = useCallback((value: string) => { + field.setValue(value); + if (draftCommentStorageKey) { + storage.set(draftCommentStorageKey, value); + } },[field, storage]); - + return ( @@ -96,7 +96,7 @@ export const MarkdownEditorForm = React.memo( ref={ref} ariaLabel={idAria} editorId={id} - onChange={onChange} + onChange={handleOnChange} value={field.value} disabledUiPlugins={disabledUiPlugins} data-test-subj={`${dataTestSubj}-markdown-editor`} diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index 631772ef768d0..79e5e47ad6f1c 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -386,6 +386,127 @@ describe(`UserActions`, () => { }); }); + it('it should persist the draft comment while old comment is updated', async () => { + const editedComment = 'it is an edited comment'; + const newComment = 'another cool comment'; + const ourActions = [getUserAction('comment', Actions.create)]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + + + + ); + + // type new comment in text area + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: newComment } }); + + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-ellipses"]` + ) + .first() + .simulate('click'); + + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-pencil"]` + ) + .first() + .simulate('click'); + + wrapper + .find(`.euiMarkdownEditorTextArea`) + .first() + .simulate('change', { + target: { value: editedComment }, + }); + + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] button[data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: editedComment, + caseId: 'case-id', + commentId: props.data.comments[0].id, + version: props.data.comments[0].version, + }); + }); + + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); + }); + + it('it should persist the draft comment while description is updated', async () => { + const newComment = 'another cool comment'; + const wrapper = mount( + + + + ); + + // type new comment in text area + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: newComment } }); + + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + + wrapper + .find(`.euiMarkdownEditorTextArea`) + .first() + .simulate('change', { + target: { value: sampleData.content }, + }); + + wrapper + .find( + `[data-test-subj="description-action"] button[data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="description-action"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); + }); + + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); + }); + describe('Host isolation action', () => { it('renders in the cases details view', async () => { const isolateAction = [getHostIsolationUserAction()]; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index 5a3862d5ac781..8d724ac76d09a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -181,6 +181,93 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { ); }); }); + + describe('draft comments', () => { + it('persists new comment when status is updated in dropdown', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); + + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="case-status-badge-in-progress"]' + ); + // validates dropdown tag + await testSubjects.existOrFail( + 'case-view-status-dropdown > case-status-badge-popover-button-in-progress' + ); + + await testSubjects.click('submit-comment'); + + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); + + it('persists new comment when case is closed the close case button', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); + + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); + await header.waitUntilLoadingHasFinished(); + await testSubjects.click('case-view-status-action-button'); + await header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail( + 'header-page-supplements > case-status-badge-popover-button-closed', + { + timeout: 5000, + } + ); + + // validate user action + await find.byCssSelector( + '[data-test-subj*="status-update-action"] [data-test-subj="case-status-badge-closed"]' + ); + // validates dropdown tag + await testSubjects.existOrFail( + 'case-view-status-dropdown >case-status-badge-popover-button-closed' + ); + + await testSubjects.click('submit-comment'); + + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); + + it('persists new comment to the case when user goes to case list table and comes back to the case', async () => { + const commentArea = await find.byCssSelector( + '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' + ); + await commentArea.focus(); + await commentArea.type('Test comment from automation'); + + await testSubjects.click('backToCases'); + + const caseLink = await find.byCssSelector('[data-test-subj="case-details-link"'); + + caseLink.click(); + + await testSubjects.click('submit-comment'); + + // validate user action + const newComment = await find.byCssSelector( + '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' + ); + expect(await newComment.getVisibleText()).equal('Test comment from automation'); + }); + }); }); describe('actions', () => { From 21f7e8b4d31c3eb02f82f8ead4109d0c2daa179b Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 24 Nov 2022 18:02:10 +0000 Subject: [PATCH 04/32] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../public/components/add_comment/index.tsx | 4 +- .../components/markdown_editor/eui_form.tsx | 22 +++++----- .../components/user_actions/index.test.tsx | 40 +++++++++---------- .../components/user_actions/markdown_form.tsx | 6 +-- .../apps/cases/view_case.ts | 10 ++--- 5 files changed, 42 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index af64e196827ea..8ab15b4474f66 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -24,12 +24,12 @@ import { UseField, useFormData, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { CommentType } from '../../../common/api'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; @@ -73,7 +73,7 @@ export const AddComment = React.memo( const [focusOnContext, setFocusOnContext] = useState(false); const { permissions, owner } = useCasesContext(); const { isLoading, createAttachments } = useCreateAttachments(); - const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor` + const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor`; const { form } = useForm({ defaultValue: initialCommentValue, diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 3eb085cf41a76..b70aba7fc7c01 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -67,19 +67,21 @@ export const MarkdownEditorForm = React.memo( useEffect(() => { const storageDraftComment = draftCommentStorageKey && storage.get(draftCommentStorageKey); - if( storageDraftComment && storageDraftComment!=='' ) { + if (storageDraftComment && storageDraftComment !== '') { field.setValue(storageDraftComment); } }, []); - - const handleOnChange = useCallback((value: string) => { - field.setValue(value); - if (draftCommentStorageKey) { - storage.set(draftCommentStorageKey, value); - } - },[field, storage]); - - + + const handleOnChange = useCallback( + (value: string) => { + field.setValue(value); + if (draftCommentStorageKey) { + storage.set(draftCommentStorageKey, value); + } + }, + [field, storage] + ); + return ( { .first() .simulate('click'); - await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find( - `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(patchComment).toBeCalledWith({ - commentUpdate: editedComment, - caseId: 'case-id', - commentId: props.data.comments[0].id, - version: props.data.comments[0].version, - }); + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: editedComment, + caseId: 'case-id', + commentId: props.data.comments[0].id, + version: props.data.comments[0].version, }); + }); - expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); }); it('it should persist the draft comment while description is updated', async () => { @@ -464,9 +464,9 @@ describe(`UserActions`, () => { // type new comment in text area wrapper - .find(`[data-test-subj="add-comment"] textarea`) - .first() - .simulate('change', { target: { value: newComment } }); + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: newComment } }); wrapper .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) @@ -504,7 +504,7 @@ describe(`UserActions`, () => { expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); }); - expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); }); describe('Host isolation action', () => { diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 47793a1055bd8..637c6bd2edd08 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -6,15 +6,15 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import * as i18n from '../case_view/translations'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; export const ContentWrapper = styled.div` padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; @@ -47,7 +47,7 @@ const UserActionMarkdownComponent = forwardRef< }); const fieldName = 'content'; - const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor` + const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor`; const { setFieldValue, submit } = form; const handleCancelAction = useCallback(() => { diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index 8d724ac76d09a..12f2d76eaf9be 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -189,7 +189,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { ); await commentArea.focus(); await commentArea.type('Test comment from automation'); - + await cases.common.changeCaseStatusViaDropdownAndVerify(CaseStatuses['in-progress']); // validate user action await find.byCssSelector( @@ -202,7 +202,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('submit-comment'); - // validate user action + // validate user action const newComment = await find.byCssSelector( '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' ); @@ -239,13 +239,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('submit-comment'); - // validate user action + // validate user action const newComment = await find.byCssSelector( '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' ); expect(await newComment.getVisibleText()).equal('Test comment from automation'); }); - + it('persists new comment to the case when user goes to case list table and comes back to the case', async () => { const commentArea = await find.byCssSelector( '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' @@ -261,7 +261,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('submit-comment'); - // validate user action + // validate user action const newComment = await find.byCssSelector( '[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]' ); From 9c8880347e526551a84f90966e4d4641a6a8f47a Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Mon, 28 Nov 2022 17:40:25 +0100 Subject: [PATCH 05/32] feat: added debounce, unit tests for draft comments --- .../components/add_comment/index.test.tsx | 33 ++++++++++++++ .../public/components/add_comment/index.tsx | 16 +++++-- .../components/markdown_editor/eui_form.tsx | 29 ++++++------ .../components/user_actions/index.test.tsx | 40 ++++++++--------- .../user_actions/markdown_form.test.tsx | 45 +++++++++++++++++++ .../components/user_actions/markdown_form.tsx | 12 ++--- 6 files changed, 132 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index f0f3c88f1a7e0..2892e7d5ca1be 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -52,6 +52,10 @@ describe('AddComment ', () => { beforeEach(() => { jest.clearAllMocks(); useCreateAttachmentsMock.mockImplementation(() => defaultResponse); + Object.defineProperty(window, 'sessionStorage', { + value: { clear: jest.fn(), removeItem: jest.fn(), getItem: jest.fn(), setItem: jest.fn() }, + writable: true, + }); }); it('should post comment on submit click', async () => { @@ -208,4 +212,33 @@ describe('AddComment ', () => { expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)'); }); }); + + it('should clear session storage on submit', async () => { + const wrapper = mount( + + + + ); + + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.comment } }); + + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); + + wrapper.find(`button[data-test-subj="submit-comment"]`).first().simulate('click'); + await waitFor(() => { + expect(onCommentSaving).toBeCalled(); + expect(createAttachments).toBeCalledWith({ + caseId: addCommentProps.caseId, + caseOwner: SECURITY_SOLUTION_OWNER, + data: [sampleData], + updateCase: onCommentPosted, + }); + expect(window.sessionStorage.removeItem).toHaveBeenCalled(); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(''); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index af64e196827ea..26ad096f999e4 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -24,12 +24,12 @@ import { UseField, useFormData, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { CommentType } from '../../../common/api'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; @@ -73,7 +73,7 @@ export const AddComment = React.memo( const [focusOnContext, setFocusOnContext] = useState(false); const { permissions, owner } = useCasesContext(); const { isLoading, createAttachments } = useCreateAttachments(); - const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor` + const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor`; const { form } = useForm({ defaultValue: initialCommentValue, @@ -123,7 +123,17 @@ export const AddComment = React.memo( storage.remove(draftCommentStorageKey); reset(); } - }, [submit, onCommentSaving, createAttachments, caseId, owner, onCommentPosted, reset]); + }, [ + submit, + onCommentSaving, + createAttachments, + caseId, + owner, + onCommentPosted, + reset, + storage, + draftCommentStorageKey, + ]); /** * Focus on the text area when a quote has been added. diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 3eb085cf41a76..96696435b50bc 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { forwardRef, useEffect, useMemo, useCallback } from 'react'; +import React, { forwardRef, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Storage } from '@kbn/kibana-utils-plugin/public'; @@ -52,6 +53,7 @@ export const MarkdownEditorForm = React.memo( ref ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const storage = useMemo(() => new Storage(window.sessionStorage), []); const commentEditorContextValue = useMemo( () => ({ @@ -63,23 +65,22 @@ export const MarkdownEditorForm = React.memo( [id, field.value, caseTitle, caseTags] ); - const storage = useMemo(() => new Storage(window.sessionStorage), []); - useEffect(() => { const storageDraftComment = draftCommentStorageKey && storage.get(draftCommentStorageKey); - if( storageDraftComment && storageDraftComment!=='' ) { + if (storageDraftComment && storageDraftComment !== '') { field.setValue(storageDraftComment); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - - const handleOnChange = useCallback((value: string) => { - field.setValue(value); - if (draftCommentStorageKey) { - storage.set(draftCommentStorageKey, value); - } - },[field, storage]); - - + + useDebounce( + () => { + if (draftCommentStorageKey) storage.set(draftCommentStorageKey, field.value); + }, + 500, + [field.value] + ); + return ( { .first() .simulate('click'); - await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find( - `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(patchComment).toBeCalledWith({ - commentUpdate: editedComment, - caseId: 'case-id', - commentId: props.data.comments[0].id, - version: props.data.comments[0].version, - }); + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: editedComment, + caseId: 'case-id', + commentId: props.data.comments[0].id, + version: props.data.comments[0].version, }); + }); - expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); }); it('it should persist the draft comment while description is updated', async () => { @@ -464,9 +464,9 @@ describe(`UserActions`, () => { // type new comment in text area wrapper - .find(`[data-test-subj="add-comment"] textarea`) - .first() - .simulate('change', { target: { value: newComment } }); + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: newComment } }); wrapper .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) @@ -504,7 +504,7 @@ describe(`UserActions`, () => { expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); }); - expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); }); describe('Host isolation action', () => { diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index f9fb8594ea51e..b1c6044f4ea7d 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -17,10 +17,13 @@ const onSaveContent = jest.fn(); const newValue = 'Hello from Tehas'; const hyperlink = `[hyperlink](http://elastic.co)`; +const draftCommentStorageKey = `xpack.cases.caseView.caseId.markdown-id.markdownEditor`; const defaultProps = { content: `A link to a timeline ${hyperlink}`, id: 'markdown-id', + caseId: 'caseId', isEditable: true, + draftCommentStorageKey, onChangeEditable, onSaveContent, }; @@ -197,4 +200,46 @@ describe('UserActionMarkdown ', () => { expect(result.container.querySelector('textarea')!.value).not.toEqual(oldContent); }); }); + describe('draft comment ', () => { + beforeEach(() => { + Object.defineProperty(window, 'sessionStorage', { + value: { clear: jest.fn(), removeItem: jest.fn(), getItem: jest.fn(), setItem: jest.fn() }, + writable: true, + }); + }); + + it('Save button click clears session storage', async () => { + const wrapper = mount( + + + + ); + + wrapper + .find(`.euiMarkdownEditorTextArea`) + .first() + .simulate('change', { + target: { value: newValue }, + }); + + wrapper.find(`button[data-test-subj="user-action-save-markdown"]`).first().simulate('click'); + + await waitFor(() => { + expect(onSaveContent).toHaveBeenCalledWith(newValue); + expect(window.sessionStorage.removeItem).toHaveBeenCalled(); + expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); + }); + }); + + it('Cancel button click calls storage remove', async () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="user-action-cancel-markdown"]`).first().simulate('click'); + + expect(window.sessionStorage.removeItem).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 47793a1055bd8..4d5b7e0186efd 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -6,15 +6,15 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import * as i18n from '../case_view/translations'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; export const ContentWrapper = styled.div` padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; @@ -47,23 +47,23 @@ const UserActionMarkdownComponent = forwardRef< }); const fieldName = 'content'; - const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor` + const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor`; const { setFieldValue, submit } = form; const handleCancelAction = useCallback(() => { storage.remove(draftCommentStorageKey); onChangeEditable(id); - }, [id, onChangeEditable]); + }, [id, onChangeEditable, storage, draftCommentStorageKey]); const handleSaveAction = useCallback(async () => { const { isValid, data } = await submit(); if (isValid && data.content !== content) { onSaveContent(data.content); + storage.remove(draftCommentStorageKey); } - storage.remove(draftCommentStorageKey); onChangeEditable(id); - }, [content, id, onChangeEditable, onSaveContent, submit]); + }, [content, id, onChangeEditable, onSaveContent, submit, storage, draftCommentStorageKey]); const setComment = useCallback( (newComment) => { From 06c2da98853af26b820a2237471436021be4078f Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Mon, 28 Nov 2022 17:45:00 +0100 Subject: [PATCH 06/32] fixed conflict issue --- .../plugins/cases/public/components/markdown_editor/eui_form.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index c52fe335feb43..96696435b50bc 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -67,7 +67,6 @@ export const MarkdownEditorForm = React.memo( useEffect(() => { const storageDraftComment = draftCommentStorageKey && storage.get(draftCommentStorageKey); - if (storageDraftComment && storageDraftComment !== '') { if (storageDraftComment && storageDraftComment !== '') { field.setValue(storageDraftComment); } From ffbab09f9e4b8cd7a648ffdcd3976ce832d7f31e Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:37:25 +0100 Subject: [PATCH 07/32] fix: show version conflict error when user is editing a comment which was updated recently --- x-pack/plugins/cases/public/common/translations.ts | 8 ++++++++ .../public/components/markdown_editor/eui_form.tsx | 10 ++++++++++ .../public/components/user_actions/markdown_form.tsx | 1 + 3 files changed, 19 insertions(+) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 08f7c0aa4e8d0..d39cf379ca7ae 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -308,6 +308,14 @@ export const ADD_TAG_CUSTOM_OPTION_LABEL = (searchValue: string) => values: { searchValue }, }); +export const COMMENT_VERSION_CONFLICT_ERROR = i18n.translate( + 'xpack.cases.configure.commentVersionConflictError', + { + defaultMessage: + 'You do not have permission to view connectors. If you would like to view connectors, contact your Kibana administrator.', + } +); + /** * EUI checkbox replace {searchValue} with the current * search value. We need to put the template variable diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 96696435b50bc..a74e899d0621f 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -13,6 +13,7 @@ import useDebounce from 'react-use/lib/useDebounce'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Storage } from '@kbn/kibana-utils-plugin/public'; +import * as i18n from '../../common/translations'; import type { MarkdownEditorRef } from './editor'; import { MarkdownEditor } from './editor'; import { CommentEditorContext } from './context'; @@ -28,6 +29,7 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { caseTags?: string[]; draftCommentStorageKey?: string; disabledUiPlugins?: string[]; + initialValue?: string; }; const BottomContentWrapper = styled(EuiFlexGroup)` @@ -49,6 +51,7 @@ export const MarkdownEditorForm = React.memo( caseTags, draftCommentStorageKey, disabledUiPlugins, + initialValue, }, ref ) => { @@ -73,6 +76,13 @@ export const MarkdownEditorForm = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (initialValue && initialValue !== field.value) { + field.setErrors([{ message: i18n.COMMENT_VERSION_CONFLICT_ERROR }]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialValue]); + useDebounce( () => { if (draftCommentStorageKey) storage.set(draftCommentStorageKey, field.value); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 4d5b7e0186efd..937cac7de7a80 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -119,6 +119,7 @@ const UserActionMarkdownComponent = forwardRef< id, draftCommentStorageKey, bottomRightContent: EditorButtons, + initialValue: content, }} /> From 5374a933bf3e4e5c5a507140c99f32157b4a4a1f Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:56:50 +0100 Subject: [PATCH 08/32] fix: update error message --- x-pack/plugins/cases/public/common/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index d39cf379ca7ae..48f2128f2ea5a 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -312,7 +312,7 @@ export const COMMENT_VERSION_CONFLICT_ERROR = i18n.translate( 'xpack.cases.configure.commentVersionConflictError', { defaultMessage: - 'You do not have permission to view connectors. If you would like to view connectors, contact your Kibana administrator.', + 'This case has been updated. Please refresh before saving additional updates.', } ); From 66e383718722fd85a5863f306af4c94ff31fe01c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:04:23 +0000 Subject: [PATCH 09/32] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- x-pack/plugins/cases/public/common/translations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 48f2128f2ea5a..98378624d0047 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -311,8 +311,7 @@ export const ADD_TAG_CUSTOM_OPTION_LABEL = (searchValue: string) => export const COMMENT_VERSION_CONFLICT_ERROR = i18n.translate( 'xpack.cases.configure.commentVersionConflictError', { - defaultMessage: - 'This case has been updated. Please refresh before saving additional updates.', + defaultMessage: 'This case has been updated. Please refresh before saving additional updates.', } ); From 875959537d1a8e01fca9083d5bbbec472411f8c4 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 30 Nov 2022 16:32:55 +0100 Subject: [PATCH 10/32] test description updated --- .../cases/public/components/user_actions/index.test.tsx | 4 ++-- .../public/components/user_actions/markdown_form.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx index f9393039ebd2b..3ff81914fecc1 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx @@ -386,7 +386,7 @@ describe(`UserActions`, () => { }); }); - it('it should persist the draft comment while old comment is updated', async () => { + it('it should persist the draft of new comment while existing old comment is updated', async () => { const editedComment = 'it is an edited comment'; const newComment = 'another cool comment'; const ourActions = [getUserAction('comment', Actions.create)]; @@ -454,7 +454,7 @@ describe(`UserActions`, () => { expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(newComment); }); - it('it should persist the draft comment while description is updated', async () => { + it('it should persist the draft of new comment while description is updated', async () => { const newComment = 'another cool comment'; const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index b1c6044f4ea7d..0c88c15011e96 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -231,7 +231,7 @@ describe('UserActionMarkdown ', () => { }); }); - it('Cancel button click calls storage remove', async () => { + it('Cancel button click clears session storage', async () => { const wrapper = mount( From 9e7d3e64f03af990055cd6f4d7cafb247bf5dcd3 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 30 Nov 2022 16:35:08 +0100 Subject: [PATCH 11/32] add constant for debounce time --- .../cases/public/components/markdown_editor/eui_form.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index a74e899d0621f..13a9a559c00df 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -32,6 +32,8 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { initialValue?: string; }; +const STORAGE_DEBOUNCE_TIME = 500; + const BottomContentWrapper = styled(EuiFlexGroup)` ${({ theme }) => ` padding: ${theme.eui.euiSizeM} 0; @@ -87,7 +89,7 @@ export const MarkdownEditorForm = React.memo( () => { if (draftCommentStorageKey) storage.set(draftCommentStorageKey, field.value); }, - 500, + STORAGE_DEBOUNCE_TIME, [field.value] ); From d20b100f2ad2f189eff50e7c79933743fa402022 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:50:34 +0100 Subject: [PATCH 12/32] fix: remove empty keys from session storage, prevent empty key on first render --- .../components/markdown_editor/eui_form.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 13a9a559c00df..b581e1f35d60e 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { forwardRef, useEffect, useMemo } from 'react'; +import React, { forwardRef, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -59,6 +59,7 @@ export const MarkdownEditorForm = React.memo( ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const storage = useMemo(() => new Storage(window.sessionStorage), []); + const isFirstRender = useRef(true); const commentEditorContextValue = useMemo( () => ({ @@ -87,7 +88,17 @@ export const MarkdownEditorForm = React.memo( useDebounce( () => { - if (draftCommentStorageKey) storage.set(draftCommentStorageKey, field.value); + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + if (draftCommentStorageKey) { + if (field.value !== '') { + storage.set(draftCommentStorageKey, field.value); + } else { + storage.remove(draftCommentStorageKey); + } + } }, STORAGE_DEBOUNCE_TIME, [field.value] From 04942ae682bac3eebfbcf58878c8ef4245ff5503 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:32:17 +0100 Subject: [PATCH 13/32] fix: show version conflict warning instead of error --- x-pack/plugins/cases/public/common/translations.ts | 4 ++-- .../cases/public/components/markdown_editor/eui_form.tsx | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 98378624d0047..a0410fd6fef1f 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -308,8 +308,8 @@ export const ADD_TAG_CUSTOM_OPTION_LABEL = (searchValue: string) => values: { searchValue }, }); -export const COMMENT_VERSION_CONFLICT_ERROR = i18n.translate( - 'xpack.cases.configure.commentVersionConflictError', +export const COMMENT_VERSION_CONFLICT_WARNING = i18n.translate( + 'xpack.cases.configure.commentVersionConflictWarning', { defaultMessage: 'This case has been updated. Please refresh before saving additional updates.', } diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index b581e1f35d60e..59dd96e515da0 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { forwardRef, useEffect, useMemo, useRef } from 'react'; +import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -58,6 +58,7 @@ export const MarkdownEditorForm = React.memo( ref ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [showVersionConflictWarning, setShowVersionConflictWarning] = useState(false); const storage = useMemo(() => new Storage(window.sessionStorage), []); const isFirstRender = useRef(true); @@ -81,7 +82,7 @@ export const MarkdownEditorForm = React.memo( useEffect(() => { if (initialValue && initialValue !== field.value) { - field.setErrors([{ message: i18n.COMMENT_VERSION_CONFLICT_ERROR }]); + setShowVersionConflictWarning(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialValue]); @@ -111,7 +112,9 @@ export const MarkdownEditorForm = React.memo( describedByIds={idAria ? [idAria] : undefined} fullWidth error={errorMessage} - helpText={field.helpText} + helpText={ + showVersionConflictWarning ? i18n.COMMENT_VERSION_CONFLICT_WARNING : field.helpText + } isInvalid={isInvalid} label={field.label} labelAppend={field.labelAppend} From 9c68478195c6ccdbef7157151826df7bf24ddae4 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 7 Dec 2022 10:25:30 +0100 Subject: [PATCH 14/32] fix: create a hook to add draft comments to session storage, save description on create case, unit tests updated --- .../cases/public/common/translations.ts | 3 +- .../components/add_comment/index.test.tsx | 53 ++++++---- .../public/components/add_comment/index.tsx | 9 +- .../components/create/description.test.tsx | 9 +- .../public/components/create/description.tsx | 4 +- .../public/components/create/form.test.tsx | 31 +++++- .../cases/public/components/create/form.tsx | 98 +++++++++++-------- .../components/markdown_editor/eui_form.tsx | 56 ++--------- .../use_markdown_session_storage.test.tsx | 69 +++++++++++++ .../use_markdown_session_storage.tsx | 66 +++++++++++++ .../components/markdown_editor/utils.test.ts | 31 ++++++ .../components/markdown_editor/utils.ts | 14 +++ .../user_actions/markdown_form.test.tsx | 50 +++++----- .../components/user_actions/markdown_form.tsx | 13 +-- 14 files changed, 363 insertions(+), 143 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/utils.ts diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index a0410fd6fef1f..bc8b0833ff672 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -311,7 +311,8 @@ export const ADD_TAG_CUSTOM_OPTION_LABEL = (searchValue: string) => export const COMMENT_VERSION_CONFLICT_WARNING = i18n.translate( 'xpack.cases.configure.commentVersionConflictWarning', { - defaultMessage: 'This case has been updated. Please refresh before saving additional updates.', + defaultMessage: + 'This comment was updated. Saving your changes will overwrite the updated value.', } ); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 2892e7d5ca1be..d6269f2512cb2 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { waitFor, act } from '@testing-library/react'; +import { waitFor, act, fireEvent } from '@testing-library/react'; import { noop } from 'lodash/fp'; import { noCreateCasesPermissions, TestProviders } from '../../common/mock'; @@ -20,6 +20,8 @@ import { AddComment } from '.'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { timelineIntegrationMock } from '../__mock__/timeline'; import type { CaseAttachmentWithoutOwner } from '../../types'; +import type { AppMockRenderer } from '../../common/mock'; +import { createAppMockRenderer } from '../../common/mock'; jest.mock('../../containers/use_create_attachments'); @@ -52,10 +54,6 @@ describe('AddComment ', () => { beforeEach(() => { jest.clearAllMocks(); useCreateAttachmentsMock.mockImplementation(() => defaultResponse); - Object.defineProperty(window, 'sessionStorage', { - value: { clear: jest.fn(), removeItem: jest.fn(), getItem: jest.fn(), setItem: jest.fn() }, - writable: true, - }); }); it('should post comment on submit click', async () => { @@ -212,23 +210,40 @@ describe('AddComment ', () => { expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)'); }); }); +}); + +describe('draft comment ', () => { + let appMockRenderer: AppMockRenderer; + let store: Record = {}; + + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + jest.clearAllMocks(); + Object.defineProperty(window, 'sessionStorage', { + value: { + clear: jest.fn().mockImplementation(() => (store = {})), + getItem: jest.fn().mockImplementation((key: string) => store[key]), + setItem: jest.fn().mockImplementation((key: string, value: string) => (store[key] = value)), + removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), + }, + writable: true, + }); + }); it('should clear session storage on submit', async () => { - const wrapper = mount( - - - - ); + const result = appMockRenderer.render(); + const draftKey = `cases.caseView.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; - wrapper - .find(`[data-test-subj="add-comment"] textarea`) - .first() - .simulate('change', { target: { value: sampleData.comment } }); + fireEvent.change(result.getByLabelText('caseComment'), { + target: { value: sampleData.comment }, + }); - expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); + expect(result.getByTestId(`add-comment`)).toBeTruthy(); + + fireEvent.click(result.getByTestId('submit-comment')); + + const removeItemSpy = jest.spyOn(window.sessionStorage, 'removeItem'); - wrapper.find(`button[data-test-subj="submit-comment"]`).first().simulate('click'); await waitFor(() => { expect(onCommentSaving).toBeCalled(); expect(createAttachments).toBeCalledWith({ @@ -237,8 +252,8 @@ describe('AddComment ', () => { data: [sampleData], updateCase: onCommentPosted, }); - expect(window.sessionStorage.removeItem).toHaveBeenCalled(); - expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(''); + expect(removeItemSpy).toHaveBeenCalledWith(draftKey); + expect(result.getByLabelText('caseComment').textContent).toBe(''); }); }); }); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 26ad096f999e4..e2722bac17c4c 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -30,6 +30,7 @@ import { useCreateAttachments } from '../../containers/use_create_attachments'; import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; +import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; @@ -73,7 +74,7 @@ export const AddComment = React.memo( const [focusOnContext, setFocusOnContext] = useState(false); const { permissions, owner } = useCasesContext(); const { isLoading, createAttachments } = useCreateAttachments(); - const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor`; + const draftStorageKey = getMarkdownEditorStorageKey(caseId, id); const { form } = useForm({ defaultValue: initialCommentValue, @@ -120,7 +121,7 @@ export const AddComment = React.memo( data: [{ ...data, type: CommentType.user }], updateCase: onCommentPosted, }); - storage.remove(draftCommentStorageKey); + storage.remove(draftStorageKey); reset(); } }, [ @@ -132,7 +133,7 @@ export const AddComment = React.memo( onCommentPosted, reset, storage, - draftCommentStorageKey, + draftStorageKey, ]); /** @@ -178,7 +179,7 @@ export const AddComment = React.memo( componentProps={{ ref: editorRef, id, - draftCommentStorageKey, + draftStorageKey, idAria: 'caseComment', isDisabled: isLoading, dataTestSubj: 'add-comment', diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx index 3e9adb8961cc2..e7128572d2935 100644 --- a/x-pack/plugins/cases/public/components/create/description.test.tsx +++ b/x-pack/plugins/cases/public/components/create/description.test.tsx @@ -22,6 +22,11 @@ jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); describe('Description', () => { let globalForm: FormHook; let appMockRender: AppMockRenderer; + const draftStorageKey = `cases.caseView.createCase.description.markdownEditor`; + const defaultProps = { + draftStorageKey, + isLoading: false, + }; const MockHookWrapperComponent: React.FC = ({ children }) => { const { form } = useForm({ @@ -44,7 +49,7 @@ describe('Description', () => { it('it renders', async () => { const result = appMockRender.render( - + ); @@ -54,7 +59,7 @@ describe('Description', () => { it('it changes the description', async () => { const result = appMockRender.render( - + ); diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index 437ff5c3751c1..48ae59f9e6e7e 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -17,11 +17,12 @@ import { ID as LensPluginId } from '../markdown_editor/plugins/lens/constants'; interface Props { isLoading: boolean; + draftStorageKey: string; } export const fieldName = 'description'; -const DescriptionComponent: React.FC = ({ isLoading }) => { +const DescriptionComponent: React.FC = ({ isLoading, draftStorageKey }) => { const { draftComment, hasIncomingLensState, openLensModal, clearDraftComment } = useLensDraftComment(); const { setFieldValue } = useFormContext(); @@ -62,6 +63,7 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { caseTitle: title, caseTags: tags, disabledUiPlugins, + draftStorageKey, }} /> ); diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 0f05f7a25ad21..137c734566bc0 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act, render, within } from '@testing-library/react'; +import { act, render, within, fireEvent } from '@testing-library/react'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { NONE_CONNECTOR_ID } from '../../../common/api'; @@ -53,6 +53,7 @@ const casesFormProps: CreateCaseFormProps = { describe('CreateCaseForm', () => { let globalForm: FormHook; + let store: Record = {}; const MockHookWrapperComponent: React.FC<{ testProviderProps?: unknown }> = ({ children, @@ -78,6 +79,15 @@ describe('CreateCaseForm', () => { useGetTagsMock.mockReturnValue({ data: ['test'] }); useGetConnectorsMock.mockReturnValue({ isLoading: false, data: connectorsMock }); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + Object.defineProperty(window, 'sessionStorage', { + value: { + clear: jest.fn().mockImplementation(() => (store = {})), + getItem: jest.fn().mockImplementation((key: string) => store[key]), + setItem: jest.fn().mockImplementation((key: string, value: string) => (store[key] = value)), + removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), + }, + writable: true, + }); }); it('renders with steps', async () => { @@ -212,4 +222,23 @@ describe('CreateCaseForm', () => { expect(titleInput).toHaveValue('title'); expect(descriptionInput).toHaveValue('description'); }); + + it('should clear session storage key on cancel', () => { + const result = render( + + + + ); + + const removeItemSpy = jest.spyOn(window.sessionStorage, 'removeItem'); + const cancelBtn = result.getByTestId('create-case-cancel'); + + fireEvent.click(cancelBtn); + + expect(casesFormProps.onCancel).toHaveBeenCalled(); + expect(removeItemSpy).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 4ec587667e18d..9b45beaaee731 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -16,6 +16,7 @@ import { import styled, { css } from 'styled-components'; import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Title } from './title'; import { Description, fieldName as descriptionFieldName } from './description'; @@ -29,6 +30,7 @@ import type { CasesTimelineIntegration } from '../timeline_context'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { InsertTimeline } from '../insert_timeline'; import type { UseCreateAttachments } from '../../containers/use_create_attachments'; +import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; import { SubmitCaseButton } from './submit_button'; import { FormContext } from './form_context'; import { useCasesFeatures } from '../../common/use_cases_features'; @@ -73,6 +75,8 @@ export interface CreateCaseFormProps extends Pick; } +const draftStorageKey = getMarkdownEditorStorageKey('createCase', 'description'); + const empty: ActionConnector[] = []; export const CreateCaseFormFields: React.FC = React.memo( ({ connectors, isLoadingConnectors, withSteps }) => { @@ -110,7 +114,7 @@ export const CreateCaseFormFields: React.FC = React.m )} - + @@ -184,45 +188,59 @@ export const CreateCaseForm: React.FC = React.memo( timelineIntegration, attachments, initialValue, - }) => ( - - - - - - - - {i18n.CANCEL} - - - - - - - - - - - ) + }) => { + const storage = useMemo(() => new Storage(window.sessionStorage), []); + + const handleOnCancel = (): void => { + storage.remove(draftStorageKey); + onCancel(); + }; + + const handleOnSuccess = (theCase: Case): Promise => { + storage.remove(draftStorageKey); + return onSuccess(theCase); + }; + + return ( + + + + + + + + {i18n.CANCEL} + + + + + + + + + + + ); + } ); CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 59dd96e515da0..9f918605b9166 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,18 +5,17 @@ * 2.0. */ -import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import styled from 'styled-components'; import type { EuiMarkdownEditorProps } from '@elastic/eui'; import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import useDebounce from 'react-use/lib/useDebounce'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import * as i18n from '../../common/translations'; import type { MarkdownEditorRef } from './editor'; import { MarkdownEditor } from './editor'; import { CommentEditorContext } from './context'; +import { useMarkdownSessionStorage } from './use_markdown_session_storage'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -27,13 +26,11 @@ type MarkdownEditorFormProps = EuiMarkdownEditorProps & { bottomRightContent?: React.ReactNode; caseTitle?: string; caseTags?: string[]; - draftCommentStorageKey?: string; + draftStorageKey: string; disabledUiPlugins?: string[]; initialValue?: string; }; -const STORAGE_DEBOUNCE_TIME = 500; - const BottomContentWrapper = styled(EuiFlexGroup)` ${({ theme }) => ` padding: ${theme.eui.euiSizeM} 0; @@ -51,16 +48,18 @@ export const MarkdownEditorForm = React.memo( bottomRightContent, caseTitle, caseTags, - draftCommentStorageKey, + draftStorageKey, disabledUiPlugins, initialValue, }, ref ) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [showVersionConflictWarning, setShowVersionConflictWarning] = useState(false); - const storage = useMemo(() => new Storage(window.sessionStorage), []); - const isFirstRender = useRef(true); + const { hasConflicts } = useMarkdownSessionStorage({ + field, + sessionKey: draftStorageKey, + initialValue, + }); const commentEditorContextValue = useMemo( () => ({ @@ -72,39 +71,6 @@ export const MarkdownEditorForm = React.memo( [id, field.value, caseTitle, caseTags] ); - useEffect(() => { - const storageDraftComment = draftCommentStorageKey && storage.get(draftCommentStorageKey); - if (storageDraftComment && storageDraftComment !== '') { - field.setValue(storageDraftComment); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (initialValue && initialValue !== field.value) { - setShowVersionConflictWarning(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialValue]); - - useDebounce( - () => { - if (isFirstRender.current) { - isFirstRender.current = false; - return; - } - if (draftCommentStorageKey) { - if (field.value !== '') { - storage.set(draftCommentStorageKey, field.value); - } else { - storage.remove(draftCommentStorageKey); - } - } - }, - STORAGE_DEBOUNCE_TIME, - [field.value] - ); - return ( { + let store: Record = {}; + const draftStorageKey = `cases.caseView.caseId.markdown-id.markdownEditor`; + const initialValue = ''; + const fieldProps: Record = { + value: 'new comment', + setValue: jest.fn(), + path: 'content', + }; + const field: FieldHook = ; + + beforeAll(() => { + Object.defineProperty(window as any, 'sessionStorage', { + value: { + clear: jest.fn().mockImplementation(() => (store = {})), + getItem: jest.fn().mockImplementation((key: string) => store[key]), + setItem: jest.fn().mockImplementation((key: string, value: string) => (store[key] = value)), + removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), + }, + writable: true, + }); + }); + + it('should add new key to session storage', () => { + const { result } = renderHook(() => + useMarkdownSessionStorage({ + field, + sessionKey: draftStorageKey, + initialValue, + }) + ); + + expect(result.current.hasConflicts).toBeFalsy(); + expect(window.sessionStorage.setItem).toHaveBeenCalledWith(draftStorageKey, field.value); + }); + + it('should return value if key exists', () => { + const { result } = renderHook(() => + useMarkdownSessionStorage({ + field, + sessionKey: draftStorageKey, + initialValue, + }) + ); + + const sessionValue = window.sessionStorage.getItem(draftStorageKey); + + expect(result.current.hasConflicts).toBeFalsy(); + expect(sessionValue).toEqual(field.value); + }); + + afterAll(() => { + delete (window as any).sessionStorage; + }); +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx new file mode 100644 index 0000000000000..6164ce228e472 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState } from 'react'; +import { isEmpty } from 'lodash'; +import useDebounce from 'react-use/lib/useDebounce'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; +import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; + +const STORAGE_DEBOUNCE_TIME = 500; + +interface SessionStorageType { + field: FieldHook; + sessionKey: string; + initialValue: string | undefined; +} + +export const useMarkdownSessionStorage = ({ + field, + sessionKey, + initialValue, +}: SessionStorageType) => { + const [hasConflicts, setHasConflicts] = useState(false); + const isFirstRender = useRef(true); + const initialValueRef = useRef(initialValue); + + const [sessionValue, setSessionValue] = useSessionStorage(sessionKey); + + if (!isEmpty(sessionValue) && isFirstRender.current) { + console.log('first render true', sessionValue); + isFirstRender.current = false; + field.setValue(sessionValue); + } + + if (initialValue !== initialValueRef.current && initialValue !== field.value) { + console.log('initialValue', initialValue, initialValueRef.current); + initialValueRef.current = initialValue; + setHasConflicts(true); + } + + useDebounce( + () => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + + if (sessionKey) { + console.log('usedebounce sessionkey', field.value) + if (field.value !== '') { + setSessionValue(field.value); + } else { + setSessionValue(''); + } + } + }, + STORAGE_DEBOUNCE_TIME, + [field.value] + ); + + return { hasConflicts }; +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts b/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts new file mode 100644 index 0000000000000..13613300f03bb --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMarkdownEditorStorageKey } from './utils'; + +describe('getMarkdownEditorStorageKey', () => { + it('should return correct session key', () => { + const caseId = 'case-id'; + const commentId = 'comment-id'; + const sessionKey = getMarkdownEditorStorageKey(caseId, commentId); + expect(sessionKey).toEqual(`cases.caseView.${caseId}.${commentId}.markdownEditor`); + }); + + it('should return default key when comment id is empty ', () => { + const caseId = 'case-id'; + const commentId = ''; + const sessionKey = getMarkdownEditorStorageKey(caseId, commentId); + expect(sessionKey).toEqual(`cases.markdown`); + }); + + it('should return default key when case id is empty ', () => { + const caseId = ''; + const commentId = 'comment-id'; + const sessionKey = getMarkdownEditorStorageKey(caseId, commentId); + expect(sessionKey).toEqual(`cases.markdown`); + }); +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/utils.ts b/x-pack/plugins/cases/public/components/markdown_editor/utils.ts new file mode 100644 index 0000000000000..e942b7ca63eb5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getMarkdownEditorStorageKey = (caseId: string, commentId: string): string => { + if (caseId === '' || commentId === '') { + return 'cases.markdown'; + } + + return `cases.caseView.${caseId}.${commentId}.markdownEditor`; +}; diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index 0c88c15011e96..4fa50d6efd43d 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -10,20 +10,20 @@ import { mount } from 'enzyme'; import { UserActionMarkdown } from './markdown_form'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer, TestProviders } from '../../common/mock'; -import { waitFor } from '@testing-library/react'; +import { waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; const onChangeEditable = jest.fn(); const onSaveContent = jest.fn(); const newValue = 'Hello from Tehas'; const hyperlink = `[hyperlink](http://elastic.co)`; -const draftCommentStorageKey = `xpack.cases.caseView.caseId.markdown-id.markdownEditor`; +const draftStorageKey = `cases.caseView.caseId.markdown-id.markdownEditor`; const defaultProps = { content: `A link to a timeline ${hyperlink}`, id: 'markdown-id', caseId: 'caseId', isEditable: true, - draftCommentStorageKey, + draftStorageKey, onChangeEditable, onSaveContent, }; @@ -200,44 +200,48 @@ describe('UserActionMarkdown ', () => { expect(result.container.querySelector('textarea')!.value).not.toEqual(oldContent); }); }); + describe('draft comment ', () => { + let appMockRenderer: AppMockRenderer; + let store: Record = {}; + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); Object.defineProperty(window, 'sessionStorage', { - value: { clear: jest.fn(), removeItem: jest.fn(), getItem: jest.fn(), setItem: jest.fn() }, + value: { + clear: jest.fn().mockImplementation(() => (store = {})), + getItem: jest.fn().mockImplementation((key: string) => store[key]), + setItem: jest + .fn() + .mockImplementation((key: string, value: string) => (store[key] = value)), + removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), + }, writable: true, }); }); it('Save button click clears session storage', async () => { - const wrapper = mount( - - - - ); + const result = appMockRenderer.render(); - wrapper - .find(`.euiMarkdownEditorTextArea`) - .first() - .simulate('change', { - target: { value: newValue }, - }); + fireEvent.change(result.getByTestId('euiMarkdownEditorTextArea'), { + target: { value: newValue }, + }); - wrapper.find(`button[data-test-subj="user-action-save-markdown"]`).first().simulate('click'); + const removeItemSpy = jest.spyOn(window.sessionStorage, 'removeItem'); + + fireEvent.click(result.getByTestId(`user-action-save-markdown`)); await waitFor(() => { expect(onSaveContent).toHaveBeenCalledWith(newValue); - expect(window.sessionStorage.removeItem).toHaveBeenCalled(); + expect(removeItemSpy).toHaveBeenCalledWith(draftStorageKey); expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); }); }); it('Cancel button click clears session storage', async () => { - const wrapper = mount( - - - - ); - wrapper.find(`[data-test-subj="user-action-cancel-markdown"]`).first().simulate('click'); + const result = appMockRenderer.render(); + + fireEvent.click(result.getByTestId('user-action-cancel-markdown')); expect(window.sessionStorage.removeItem).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 937cac7de7a80..49a17cb9edad4 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -15,6 +15,7 @@ import * as i18n from '../case_view/translations'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; +import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; export const ContentWrapper = styled.div` padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`}; @@ -47,23 +48,23 @@ const UserActionMarkdownComponent = forwardRef< }); const fieldName = 'content'; - const draftCommentStorageKey = `xpack.cases.caseView.${caseId}.${id}.markdownEditor`; + const draftStorageKey = getMarkdownEditorStorageKey(caseId, id); const { setFieldValue, submit } = form; const handleCancelAction = useCallback(() => { - storage.remove(draftCommentStorageKey); onChangeEditable(id); - }, [id, onChangeEditable, storage, draftCommentStorageKey]); + storage.remove(draftStorageKey); + }, [id, onChangeEditable, storage, draftStorageKey]); const handleSaveAction = useCallback(async () => { const { isValid, data } = await submit(); if (isValid && data.content !== content) { onSaveContent(data.content); - storage.remove(draftCommentStorageKey); } onChangeEditable(id); - }, [content, id, onChangeEditable, onSaveContent, submit, storage, draftCommentStorageKey]); + storage.remove(draftStorageKey); + }, [content, id, onChangeEditable, onSaveContent, submit, storage, draftStorageKey]); const setComment = useCallback( (newComment) => { @@ -117,7 +118,7 @@ const UserActionMarkdownComponent = forwardRef< 'aria-label': 'Cases markdown editor', value: content, id, - draftCommentStorageKey, + draftStorageKey, bottomRightContent: EditorButtons, initialValue: content, }} From d97471d150107064d05446b56466e2780970a35a Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 7 Dec 2022 10:26:28 +0100 Subject: [PATCH 15/32] fix: removed console logs --- .../markdown_editor/use_markdown_session_storage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx index 6164ce228e472..134c44d22f101 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx @@ -31,13 +31,11 @@ export const useMarkdownSessionStorage = ({ const [sessionValue, setSessionValue] = useSessionStorage(sessionKey); if (!isEmpty(sessionValue) && isFirstRender.current) { - console.log('first render true', sessionValue); isFirstRender.current = false; field.setValue(sessionValue); } if (initialValue !== initialValueRef.current && initialValue !== field.value) { - console.log('initialValue', initialValue, initialValueRef.current); initialValueRef.current = initialValue; setHasConflicts(true); } @@ -50,7 +48,6 @@ export const useMarkdownSessionStorage = ({ } if (sessionKey) { - console.log('usedebounce sessionkey', field.value) if (field.value !== '') { setSessionValue(field.value); } else { From 02f076d17b9717c0c542ea367f2632d2e5c143f8 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 7 Dec 2022 13:45:58 +0100 Subject: [PATCH 16/32] conflicts resolved --- .../cases/public/components/create/form.tsx | 15 ++++++++------- .../components/user_actions/markdown_form.tsx | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 309f753c183a0..02f5de90d57c7 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -191,18 +191,19 @@ export const CreateCaseForm: React.FC = React.memo( attachments, initialValue, }) => { + const handleOnConfirmationCallback = (): void => { + ; + storage.remove(draftStorageKey); + onCancel(); + }; + const { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal } = useCancelCreationAction({ - onConfirmationCallback: onCancel, + onConfirmationCallback: handleOnConfirmationCallback }); const storage = useMemo(() => new Storage(window.sessionStorage), []); - const handleOnConfirmModal = (): void => { - storage.remove(draftStorageKey); - onConfirmModal(); - }; - const handleOnSuccess = (theCase: Case): Promise => { storage.remove(draftStorageKey); return onSuccess(theCase); @@ -240,7 +241,7 @@ export const CreateCaseForm: React.FC = React.memo( {showConfirmationModal && ( )} diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 6d0a716a92851..6dd0114fce839 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useRef, useMemo } from 'react'; import styled from 'styled-components'; import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; From a75e76869ec1823a16146c726ae7644e4fbdd909 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 7 Dec 2022 12:52:09 +0000 Subject: [PATCH 17/32] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- x-pack/plugins/cases/public/components/create/form.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 02f5de90d57c7..492b33b2ee254 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -192,14 +192,13 @@ export const CreateCaseForm: React.FC = React.memo( initialValue, }) => { const handleOnConfirmationCallback = (): void => { - ; - storage.remove(draftStorageKey); - onCancel(); - }; + storage.remove(draftStorageKey); + onCancel(); + }; const { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal } = useCancelCreationAction({ - onConfirmationCallback: handleOnConfirmationCallback + onConfirmationCallback: handleOnConfirmationCallback, }); const storage = useMemo(() => new Storage(window.sessionStorage), []); From 51b00aa13a87e8dcdf00154a84e9c1dea7c530f4 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:54:47 +0100 Subject: [PATCH 18/32] fix: remove mock values from jest --- .../components/add_comment/index.test.tsx | 15 +--- .../public/components/add_comment/index.tsx | 5 -- .../public/components/create/form.test.tsx | 17 ++--- .../cases/public/components/create/form.tsx | 14 ++-- .../use_markdown_session_storage.test.tsx | 69 ------------------- .../user_actions/markdown_form.test.tsx | 18 +---- .../components/user_actions/markdown_form.tsx | 20 ++++-- 7 files changed, 26 insertions(+), 132 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index d6269f2512cb2..6367d5faef41f 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -214,20 +214,10 @@ describe('AddComment ', () => { describe('draft comment ', () => { let appMockRenderer: AppMockRenderer; - let store: Record = {}; beforeEach(() => { appMockRenderer = createAppMockRenderer(); jest.clearAllMocks(); - Object.defineProperty(window, 'sessionStorage', { - value: { - clear: jest.fn().mockImplementation(() => (store = {})), - getItem: jest.fn().mockImplementation((key: string) => store[key]), - setItem: jest.fn().mockImplementation((key: string, value: string) => (store[key] = value)), - removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), - }, - writable: true, - }); }); it('should clear session storage on submit', async () => { @@ -239,11 +229,11 @@ describe('draft comment ', () => { }); expect(result.getByTestId(`add-comment`)).toBeTruthy(); + sessionStorage.setItem(draftKey, sampleData.comment); + expect(sessionStorage.getItem(draftKey)).toBe(sampleData.comment); fireEvent.click(result.getByTestId('submit-comment')); - const removeItemSpy = jest.spyOn(window.sessionStorage, 'removeItem'); - await waitFor(() => { expect(onCommentSaving).toBeCalled(); expect(createAttachments).toBeCalledWith({ @@ -252,7 +242,6 @@ describe('draft comment ', () => { data: [sampleData], updateCase: onCommentPosted, }); - expect(removeItemSpy).toHaveBeenCalledWith(draftKey); expect(result.getByLabelText('caseComment').textContent).toBe(''); }); }); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index ff57ca4002f47..9ba76599c2053 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -12,7 +12,6 @@ import React, { useImperativeHandle, useEffect, useState, - useMemo, } from 'react'; import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; @@ -24,7 +23,6 @@ import { UseField, useFormData, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import { CommentType } from '../../../common/api'; import { useCreateAttachments } from '../../containers/use_create_attachments'; import type { Case } from '../../containers/types'; @@ -69,7 +67,6 @@ export const AddComment = React.memo( { id, caseId, onCommentPosted, onCommentSaving, showLoading = true, statusActionButton }, ref ) => { - const storage = useMemo(() => new Storage(window.sessionStorage), []); const editorRef = useRef(null); const [focusOnContext, setFocusOnContext] = useState(false); const { permissions, owner } = useCasesContext(); @@ -121,7 +118,6 @@ export const AddComment = React.memo( data: [{ ...data, type: CommentType.user }], updateCase: onCommentPosted, }); - storage.remove(draftStorageKey); reset(); } }, [ @@ -132,7 +128,6 @@ export const AddComment = React.memo( owner, onCommentPosted, reset, - storage, draftStorageKey, ]); diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 137c734566bc0..648f4b9c2509f 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -53,7 +53,6 @@ const casesFormProps: CreateCaseFormProps = { describe('CreateCaseForm', () => { let globalForm: FormHook; - let store: Record = {}; const MockHookWrapperComponent: React.FC<{ testProviderProps?: unknown }> = ({ children, @@ -79,15 +78,6 @@ describe('CreateCaseForm', () => { useGetTagsMock.mockReturnValue({ data: ['test'] }); useGetConnectorsMock.mockReturnValue({ isLoading: false, data: connectorsMock }); useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - Object.defineProperty(window, 'sessionStorage', { - value: { - clear: jest.fn().mockImplementation(() => (store = {})), - getItem: jest.fn().mockImplementation((key: string) => store[key]), - setItem: jest.fn().mockImplementation((key: string, value: string) => (store[key] = value)), - removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), - }, - writable: true, - }); }); it('renders with steps', async () => { @@ -232,13 +222,14 @@ describe('CreateCaseForm', () => { /> ); - - const removeItemSpy = jest.spyOn(window.sessionStorage, 'removeItem'); + const draftStorageKey = `cases.caseView.createCase.description.markdownEditor`; const cancelBtn = result.getByTestId('create-case-cancel'); fireEvent.click(cancelBtn); + fireEvent.click(result.getByTestId('confirmModalConfirmButton')); + expect(casesFormProps.onCancel).toHaveBeenCalled(); - expect(removeItemSpy).toHaveBeenCalled(); + expect(sessionStorage.getItem(draftStorageKey)).toBe('undefined'); }); }); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 02f5de90d57c7..32419542e33f2 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -16,7 +16,6 @@ import { import styled, { css } from 'styled-components'; import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import { Title } from './title'; import { Description, fieldName as descriptionFieldName } from './description'; @@ -192,20 +191,17 @@ export const CreateCaseForm: React.FC = React.memo( initialValue, }) => { const handleOnConfirmationCallback = (): void => { - ; - storage.remove(draftStorageKey); - onCancel(); - }; + onCancel(); + window.sessionStorage.removeItem(draftStorageKey); + }; const { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal } = useCancelCreationAction({ - onConfirmationCallback: handleOnConfirmationCallback + onConfirmationCallback: handleOnConfirmationCallback, }); - const storage = useMemo(() => new Storage(window.sessionStorage), []); - const handleOnSuccess = (theCase: Case): Promise => { - storage.remove(draftStorageKey); + window.sessionStorage.removeItem(draftStorageKey); return onSuccess(theCase); }; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx deleted file mode 100644 index 07bd9f6bbf1a8..0000000000000 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; - -import { useMarkdownSessionStorage } from './use_markdown_session_storage'; - -describe('useMarkdownSessionStorage', () => { - let store: Record = {}; - const draftStorageKey = `cases.caseView.caseId.markdown-id.markdownEditor`; - const initialValue = ''; - const fieldProps: Record = { - value: 'new comment', - setValue: jest.fn(), - path: 'content', - }; - const field: FieldHook = ; - - beforeAll(() => { - Object.defineProperty(window as any, 'sessionStorage', { - value: { - clear: jest.fn().mockImplementation(() => (store = {})), - getItem: jest.fn().mockImplementation((key: string) => store[key]), - setItem: jest.fn().mockImplementation((key: string, value: string) => (store[key] = value)), - removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), - }, - writable: true, - }); - }); - - it('should add new key to session storage', () => { - const { result } = renderHook(() => - useMarkdownSessionStorage({ - field, - sessionKey: draftStorageKey, - initialValue, - }) - ); - - expect(result.current.hasConflicts).toBeFalsy(); - expect(window.sessionStorage.setItem).toHaveBeenCalledWith(draftStorageKey, field.value); - }); - - it('should return value if key exists', () => { - const { result } = renderHook(() => - useMarkdownSessionStorage({ - field, - sessionKey: draftStorageKey, - initialValue, - }) - ); - - const sessionValue = window.sessionStorage.getItem(draftStorageKey); - - expect(result.current.hasConflicts).toBeFalsy(); - expect(sessionValue).toEqual(field.value); - }); - - afterAll(() => { - delete (window as any).sessionStorage; - }); -}); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index bbda9e84acd63..ed97cfa703972 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -227,21 +227,9 @@ describe('UserActionMarkdown ', () => { describe('draft comment ', () => { let appMockRenderer: AppMockRenderer; - let store: Record = {}; beforeEach(() => { appMockRenderer = createAppMockRenderer(); - Object.defineProperty(window, 'sessionStorage', { - value: { - clear: jest.fn().mockImplementation(() => (store = {})), - getItem: jest.fn().mockImplementation((key: string) => store[key]), - setItem: jest - .fn() - .mockImplementation((key: string, value: string) => (store[key] = value)), - removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), - }, - writable: true, - }); }); it('Save button click clears session storage', async () => { @@ -251,14 +239,12 @@ describe('UserActionMarkdown ', () => { target: { value: newValue }, }); - const removeItemSpy = jest.spyOn(window.sessionStorage, 'removeItem'); - fireEvent.click(result.getByTestId(`user-action-save-markdown`)); await waitFor(() => { expect(onSaveContent).toHaveBeenCalledWith(newValue); - expect(removeItemSpy).toHaveBeenCalledWith(draftStorageKey); expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); + expect(sessionStorage.getItem(draftStorageKey)).toBe('undefined'); }); }); @@ -267,7 +253,7 @@ describe('UserActionMarkdown ', () => { fireEvent.click(result.getByTestId('user-action-cancel-markdown')); - expect(window.sessionStorage.removeItem).toHaveBeenCalled(); + expect(sessionStorage.getItem(draftStorageKey)).toBe(null); }); }); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 6dd0114fce839..719373c599872 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import React, { forwardRef, useCallback, useImperativeHandle, useRef, useMemo } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; import styled from 'styled-components'; import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; @@ -39,7 +38,6 @@ const UserActionMarkdownComponent = forwardRef< >(({ id, content, caseId, isEditable, onChangeEditable, onSaveContent }, ref) => { const editorRef = useRef(); const initialState = { content }; - const storage = useMemo(() => new Storage(window.sessionStorage), []); const { form } = useForm({ defaultValue: initialState, options: { stripEmptyFields: false }, @@ -52,8 +50,8 @@ const UserActionMarkdownComponent = forwardRef< const handleCancelAction = useCallback(() => { onChangeEditable(id); - storage.remove(draftStorageKey); - }, [id, onChangeEditable, storage, draftStorageKey]); + window.sessionStorage.removeItem(draftStorageKey); + }, [id, onChangeEditable, window.sessionStorage, draftStorageKey]); const handleSaveAction = useCallback(async () => { const { isValid, data } = await submit(); @@ -62,8 +60,16 @@ const UserActionMarkdownComponent = forwardRef< onSaveContent(data.content); } onChangeEditable(id); - storage.remove(draftStorageKey); - }, [content, id, onChangeEditable, onSaveContent, submit, storage, draftStorageKey]); + window.sessionStorage.removeItem(draftStorageKey); + }, [ + content, + id, + onChangeEditable, + onSaveContent, + submit, + window.sessionStorage, + draftStorageKey, + ]); const setComment = useCallback( (newComment) => { From 15a273f90ecffdda674bcaac8a0a2f183ea050eb Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 7 Dec 2022 17:00:50 +0100 Subject: [PATCH 19/32] fix linting --- .../cases/public/components/add_comment/index.tsx | 11 +---------- .../public/components/user_actions/markdown_form.tsx | 12 ++---------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 9ba76599c2053..65f5804cc2633 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -120,16 +120,7 @@ export const AddComment = React.memo( }); reset(); } - }, [ - submit, - onCommentSaving, - createAttachments, - caseId, - owner, - onCommentPosted, - reset, - draftStorageKey, - ]); + }, [submit, onCommentSaving, createAttachments, caseId, owner, onCommentPosted, reset]); /** * Focus on the text area when a quote has been added. diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 719373c599872..3c34f245b36aa 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -51,7 +51,7 @@ const UserActionMarkdownComponent = forwardRef< const handleCancelAction = useCallback(() => { onChangeEditable(id); window.sessionStorage.removeItem(draftStorageKey); - }, [id, onChangeEditable, window.sessionStorage, draftStorageKey]); + }, [id, onChangeEditable, draftStorageKey]); const handleSaveAction = useCallback(async () => { const { isValid, data } = await submit(); @@ -61,15 +61,7 @@ const UserActionMarkdownComponent = forwardRef< } onChangeEditable(id); window.sessionStorage.removeItem(draftStorageKey); - }, [ - content, - id, - onChangeEditable, - onSaveContent, - submit, - window.sessionStorage, - draftStorageKey, - ]); + }, [content, id, onChangeEditable, onSaveContent, submit, draftStorageKey]); const setComment = useCallback( (newComment) => { From e01c9fcec93b51964d3e22992bf07a7b031f276c Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 7 Dec 2022 17:55:47 +0100 Subject: [PATCH 20/32] lint fix --- .../plugins/cases/public/components/add_comment/index.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 6367d5faef41f..1ec318f740217 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { waitFor, act, fireEvent } from '@testing-library/react'; import { noop } from 'lodash/fp'; -import { noCreateCasesPermissions, TestProviders } from '../../common/mock'; +import { noCreateCasesPermissions, TestProviders, createAppMockRenderer } from '../../common/mock'; import { CommentType } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; @@ -21,7 +21,6 @@ import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { timelineIntegrationMock } from '../__mock__/timeline'; import type { CaseAttachmentWithoutOwner } from '../../types'; import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer } from '../../common/mock'; jest.mock('../../containers/use_create_attachments'); From 2b70e7fc86f4d79960b9270cff2ac7fa4b2f96ff Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:04:59 +0100 Subject: [PATCH 21/32] Fix FTR e2e test fail on CI pipeline --- .../test/functional_with_es_ssl/apps/cases/view_case.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index 12f2d76eaf9be..52fd9fc1be5c7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -271,7 +271,13 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('actions', () => { - createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + beforeEach(async () => { + await createAndNavigateToCase(getPageObject, getService); + }); + + afterEach(async () => { + await cases.api.deleteAllCases(); + }); it('deletes the case successfully', async () => { await cases.singleCase.deleteCase(); From 29f1a7054ababe6cd3f41123f3c347bab998d02d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 8 Dec 2022 13:10:26 +0000 Subject: [PATCH 22/32] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index 52fd9fc1be5c7..cf8b6bd62a6a1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -274,7 +274,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { beforeEach(async () => { await createAndNavigateToCase(getPageObject, getService); }); - + afterEach(async () => { await cases.api.deleteAllCases(); }); From d738484c6e3c4e139523a4b466e669ee1ce961af Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Thu, 8 Dec 2022 15:42:39 +0100 Subject: [PATCH 23/32] fix: add single case addition and deletion to draft comments describe --- .../functional_with_es_ssl/apps/cases/view_case.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index cf8b6bd62a6a1..a08b46328efd2 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -183,6 +183,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('draft comments', () => { + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); + it('persists new comment when status is updated in dropdown', async () => { const commentArea = await find.byCssSelector( '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' @@ -271,13 +273,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); describe('actions', () => { - beforeEach(async () => { - await createAndNavigateToCase(getPageObject, getService); - }); - - afterEach(async () => { - await cases.api.deleteAllCases(); - }); + createOneCaseBeforeDeleteAllAfter(getPageObject, getService); it('deletes the case successfully', async () => { await cases.singleCase.deleteCase(); From f6bbdb9e066163bbffe4ee3c6883b5a15e6834f2 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:16:50 +0100 Subject: [PATCH 24/32] Fix: added unit tests for more scenarios --- .../components/add_comment/index.test.tsx | 40 +++- .../public/components/add_comment/index.tsx | 4 +- .../public/components/create/form.test.tsx | 55 +++-- .../cases/public/components/create/form.tsx | 26 ++- .../use_markdown_session_storage.test.tsx | 215 ++++++++++++++++++ .../use_markdown_session_storage.tsx | 21 +- .../components/markdown_editor/utils.test.ts | 23 +- .../components/markdown_editor/utils.ts | 14 +- .../user_actions/markdown_form.test.tsx | 95 +++++++- .../components/user_actions/markdown_form.tsx | 9 +- .../cases/public/components/utils.test.ts | 28 ++- .../plugins/cases/public/components/utils.ts | 4 + 12 files changed, 472 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 1ec318f740217..fde8288d85adc 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -213,23 +213,40 @@ describe('AddComment ', () => { describe('draft comment ', () => { let appMockRenderer: AppMockRenderer; + const appId = 'testAppId'; beforeEach(() => { appMockRenderer = createAppMockRenderer(); jest.clearAllMocks(); }); + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + it('should clear session storage on submit', async () => { const result = appMockRenderer.render(); - const draftKey = `cases.caseView.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; + const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; fireEvent.change(result.getByLabelText('caseComment'), { target: { value: sampleData.comment }, }); - expect(result.getByTestId(`add-comment`)).toBeTruthy(); - sessionStorage.setItem(draftKey, sampleData.comment); - expect(sessionStorage.getItem(draftKey)).toBe(sampleData.comment); + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitFor(() => { + expect(result.getByLabelText('caseComment')).toHaveValue(sessionStorage.getItem(draftKey)); + }); fireEvent.click(result.getByTestId('submit-comment')); @@ -242,6 +259,21 @@ describe('draft comment ', () => { updateCase: onCommentPosted, }); expect(result.getByLabelText('caseComment').textContent).toBe(''); + expect(sessionStorage.getItem(draftKey)).toBe(''); + }); + }); + + describe('existing storage key', () => { + const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; + + beforeEach(() => { + sessionStorage.setItem(draftKey, 'value set in storage'); + }); + + it('should have draft comment same as existing session storage', async () => { + const result = appMockRenderer.render(); + + expect(result.getByLabelText('caseComment')).toHaveValue('value set in storage'); }); }); }); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 65f5804cc2633..7a1ca9869fdcc 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -69,9 +69,9 @@ export const AddComment = React.memo( ) => { const editorRef = useRef(null); const [focusOnContext, setFocusOnContext] = useState(false); - const { permissions, owner } = useCasesContext(); + const { permissions, owner, appId } = useCasesContext(); const { isLoading, createAttachments } = useCreateAttachments(); - const draftStorageKey = getMarkdownEditorStorageKey(caseId, id); + const draftStorageKey = getMarkdownEditorStorageKey(appId, caseId, id); const { form } = useForm({ defaultValue: initialCommentValue, diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 648f4b9c2509f..f98cae684be69 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { act, render, within, fireEvent } from '@testing-library/react'; +import { waitFor } from '@testing-library/dom'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { NONE_CONNECTOR_ID } from '../../../common/api'; @@ -213,23 +214,47 @@ describe('CreateCaseForm', () => { expect(descriptionInput).toHaveValue('description'); }); - it('should clear session storage key on cancel', () => { - const result = render( - - - - ); - const draftStorageKey = `cases.caseView.createCase.description.markdownEditor`; - const cancelBtn = result.getByTestId('create-case-cancel'); + describe('draft comment ', () => { + it('should clear session storage key on cancel', () => { + const result = render( + + + + ); + const draftStorageKey = `cases.caseView.createCase.description.markdownEditor`; + + const cancelBtn = result.getByTestId('create-case-cancel'); - fireEvent.click(cancelBtn); + fireEvent.click(cancelBtn); - fireEvent.click(result.getByTestId('confirmModalConfirmButton')); + fireEvent.click(result.getByTestId('confirmModalConfirmButton')); - expect(casesFormProps.onCancel).toHaveBeenCalled(); - expect(sessionStorage.getItem(draftStorageKey)).toBe('undefined'); + expect(casesFormProps.onCancel).toHaveBeenCalled(); + expect(sessionStorage.getItem(draftStorageKey)).toBe(null); + }); + + it('should clear session storage key on submit', () => { + const result = render( + + + + ); + const draftStorageKey = `cases.kibana.createCase.description.markdownEditor`; + + const submitBtn = result.getByTestId('create-case-submit'); + + fireEvent.click(submitBtn); + + waitFor(() => { + expect(casesFormProps.onSuccess).toHaveBeenCalled(); + expect(sessionStorage.getItem(draftStorageKey)).toBe(null); + }); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 32419542e33f2..4e5a148286493 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -28,6 +28,7 @@ import type { Case } from '../../containers/types'; import type { CasesTimelineIntegration } from '../timeline_context'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { InsertTimeline } from '../insert_timeline'; +import { removeItemFromSessionStorate } from '../utils'; import type { UseCreateAttachments } from '../../containers/use_create_attachments'; import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; import { SubmitCaseButton } from './submit_button'; @@ -63,6 +64,8 @@ export interface CreateCaseFormFieldsProps { connectors: ActionConnector[]; isLoadingConnectors: boolean; withSteps: boolean; + owner: string[]; + draftStorageKey: string; } export interface CreateCaseFormProps extends Pick, 'withSteps'> { onCancel: () => void; @@ -76,16 +79,12 @@ export interface CreateCaseFormProps extends Pick; } -const draftStorageKey = getMarkdownEditorStorageKey('createCase', 'description'); - const empty: ActionConnector[] = []; export const CreateCaseFormFields: React.FC = React.memo( - ({ connectors, isLoadingConnectors, withSteps }) => { + ({ connectors, isLoadingConnectors, withSteps, owner, draftStorageKey }) => { const { isSubmitting } = useFormContext(); const { isSyncAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures(); - const { owner } = useCasesContext(); - const availableOwners = useAvailableCasesOwners(); const canShowCaseSolutionSelection = !owner.length && availableOwners.length; @@ -121,7 +120,13 @@ export const CreateCaseFormFields: React.FC = React.m ), }), - [isSubmitting, caseAssignmentAuthorized, canShowCaseSolutionSelection, availableOwners] + [ + isSubmitting, + caseAssignmentAuthorized, + canShowCaseSolutionSelection, + availableOwners, + draftStorageKey, + ] ); const secondStep = useMemo( @@ -190,9 +195,12 @@ export const CreateCaseForm: React.FC = React.memo( attachments, initialValue, }) => { + const { owner, appId } = useCasesContext(); + const draftStorageKey = getMarkdownEditorStorageKey(appId, 'createCase', 'description'); + const handleOnConfirmationCallback = (): void => { onCancel(); - window.sessionStorage.removeItem(draftStorageKey); + removeItemFromSessionStorate(draftStorageKey); }; const { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal } = @@ -201,7 +209,7 @@ export const CreateCaseForm: React.FC = React.memo( }); const handleOnSuccess = (theCase: Case): Promise => { - window.sessionStorage.removeItem(draftStorageKey); + removeItemFromSessionStorate(draftStorageKey); return onSuccess(theCase); }; @@ -217,6 +225,8 @@ export const CreateCaseForm: React.FC = React.memo( connectors={empty} isLoadingConnectors={false} withSteps={withSteps} + owner={owner} + draftStorageKey={draftStorageKey} /> { + const field = { + label: '', + helpText: '', + type: '', + value: 'test', + errors: [], + onChange: jest.fn(), + setValue: jest.fn(), + setErrors: jest.fn(), + reset: jest.fn(), + } as unknown as FieldHook; + + const sessionKey = 'testKey'; + const initialValue = ''; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return hasConflicts as false', async () => { + const { result, waitFor } = renderHook(() => + useMarkdownSessionStorage({ field, sessionKey, initialValue }) + ); + + await waitFor(() => { + expect(result.current.hasConflicts).toBe(false); + }); + }); + + it('should not update the session value when it is first render ', async () => { + const { waitFor } = renderHook( + (props) => { + return useMarkdownSessionStorage(props); + }, + { + initialProps: { field, sessionKey, initialValue }, + } + ); + + await waitFor(() => { + expect(sessionStorage.getItem(sessionKey)).toBe(null); + }); + }); + + it('should set seeion storage when field has value and session key is not created yet', async () => { + const specialCharsValue = '!{tooltip[Hello again](This is tooltip!)}'; + const { waitFor, result } = renderHook( + (props) => { + return useMarkdownSessionStorage(props); + }, + { + initialProps: { field: { ...field, value: specialCharsValue }, sessionKey, initialValue }, + } + ); + + expect(sessionStorage.getItem(sessionKey)).toBe(null); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitForComponentToUpdate(); + + await waitFor(() => { + expect(result.current.hasConflicts).toBe(false); + expect(sessionStorage.getItem(sessionKey)).toBe(specialCharsValue); + }); + }); + + it('should update session value ', async () => { + const { result, rerender, waitFor } = renderHook( + (props) => { + return useMarkdownSessionStorage(props); + }, + { + initialProps: { field, sessionKey, initialValue }, + } + ); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + // await waitForComponentToUpdate(); + + rerender({ field: { ...field, value: 'new value' }, sessionKey, initialValue }); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitForComponentToUpdate(); + + await waitFor(() => { + expect(result.current.hasConflicts).toBe(false); + expect(sessionStorage.getItem(sessionKey)).toBe('new value'); + }); + }); + + it('should return has conflict true', async () => { + const { result, rerender, waitFor } = renderHook( + (props) => { + return useMarkdownSessionStorage(props); + }, + { initialProps: { field, sessionKey, initialValue } } + ); + + rerender({ field, sessionKey, initialValue: 'updated' }); + + await waitFor(() => { + expect(result.current.hasConflicts).toBe(true); + }); + }); + + describe('existing session key', () => { + beforeEach(() => { + sessionStorage.setItem(sessionKey, 'exisitng session storage value'); + }); + + afterEach(() => { + sessionStorage.removeItem(sessionKey); + }); + + it('should set field value if seesion already exists and it is a first render', async () => { + const { waitFor, result } = renderHook( + (props) => { + return useMarkdownSessionStorage(props); + }, + { + initialProps: { field, sessionKey, initialValue }, + } + ); + + await waitForComponentToUpdate(); + + await waitFor(() => { + expect(field.setValue).toHaveBeenCalled(); + }); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitForComponentToUpdate(); + + await waitFor(() => { + expect(result.current.hasConflicts).toBe(false); + expect(field.value).toBe(sessionStorage.getItem(sessionKey)); + }); + }); + + it('should update existing session key if field value changed', async () => { + const { waitFor, rerender, result } = renderHook< + SessionStorageType, + { hasConflicts: boolean } + >( + (props) => { + return useMarkdownSessionStorage(props); + }, + { + initialProps: { field, sessionKey, initialValue }, + } + ); + + await waitForComponentToUpdate(); + + await waitFor(() => { + expect(field.setValue).toHaveBeenCalled(); + }); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + rerender({ field: { ...field, value: 'new value' }, sessionKey, initialValue }); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitForComponentToUpdate(); + + await waitFor(() => { + expect(result.current.hasConflicts).toBe(false); + expect(sessionStorage.getItem(sessionKey)).toBe('new value'); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx index 134c44d22f101..aacf350e87107 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx @@ -8,12 +8,11 @@ import { useRef, useState } from 'react'; import { isEmpty } from 'lodash'; import useDebounce from 'react-use/lib/useDebounce'; -import useSessionStorage from 'react-use/lib/useSessionStorage'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; const STORAGE_DEBOUNCE_TIME = 500; -interface SessionStorageType { +export interface SessionStorageType { field: FieldHook; sessionKey: string; initialValue: string | undefined; @@ -28,7 +27,7 @@ export const useMarkdownSessionStorage = ({ const isFirstRender = useRef(true); const initialValueRef = useRef(initialValue); - const [sessionValue, setSessionValue] = useSessionStorage(sessionKey); + const sessionValue = sessionStorage.getItem(sessionKey) ?? ''; if (!isEmpty(sessionValue) && isFirstRender.current) { isFirstRender.current = false; @@ -43,17 +42,19 @@ export const useMarkdownSessionStorage = ({ useDebounce( () => { if (isFirstRender.current) { + if (isEmpty(sessionValue) && !isEmpty(field.value)) { + /* this condition is used to for lens draft comment, + when user selects and visualization and comes back to Markdown editor, + it is a first render for Markdown editor, however field has value of visualization which is not stored in session + hence saving this item in session storage + */ + sessionStorage.setItem(sessionKey, field.value); + } isFirstRender.current = false; return; } - if (sessionKey) { - if (field.value !== '') { - setSessionValue(field.value); - } else { - setSessionValue(''); - } - } + sessionStorage.setItem(sessionKey, field.value); }, STORAGE_DEBOUNCE_TIME, [field.value] diff --git a/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts b/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts index 13613300f03bb..11a94424f8e62 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts @@ -9,23 +9,34 @@ import { getMarkdownEditorStorageKey } from './utils'; describe('getMarkdownEditorStorageKey', () => { it('should return correct session key', () => { + const appId = 'security-solution'; const caseId = 'case-id'; const commentId = 'comment-id'; - const sessionKey = getMarkdownEditorStorageKey(caseId, commentId); - expect(sessionKey).toEqual(`cases.caseView.${caseId}.${commentId}.markdownEditor`); + const sessionKey = getMarkdownEditorStorageKey(appId, caseId, commentId); + expect(sessionKey).toEqual(`cases.${appId}.${caseId}.${commentId}.markdownEditor`); }); it('should return default key when comment id is empty ', () => { + const appId = 'security-solution'; const caseId = 'case-id'; const commentId = ''; - const sessionKey = getMarkdownEditorStorageKey(caseId, commentId); - expect(sessionKey).toEqual(`cases.markdown`); + const sessionKey = getMarkdownEditorStorageKey(appId, caseId, commentId); + expect(sessionKey).toEqual(`cases.${appId}.${caseId}.comment.markdownEditor`); }); it('should return default key when case id is empty ', () => { + const appId = 'security-solution'; const caseId = ''; const commentId = 'comment-id'; - const sessionKey = getMarkdownEditorStorageKey(caseId, commentId); - expect(sessionKey).toEqual(`cases.markdown`); + const sessionKey = getMarkdownEditorStorageKey(appId, caseId, commentId); + expect(sessionKey).toEqual(`cases.${appId}.case.${commentId}.markdownEditor`); + }); + + it('should return default key when app id is empty ', () => { + const appId = ''; + const caseId = 'case-id'; + const commentId = 'comment-id'; + const sessionKey = getMarkdownEditorStorageKey(appId, caseId, commentId); + expect(sessionKey).toEqual(`cases.kibana.${caseId}.${commentId}.markdownEditor`); }); }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/utils.ts b/x-pack/plugins/cases/public/components/markdown_editor/utils.ts index e942b7ca63eb5..a895503b43276 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/utils.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/utils.ts @@ -5,10 +5,14 @@ * 2.0. */ -export const getMarkdownEditorStorageKey = (caseId: string, commentId: string): string => { - if (caseId === '' || commentId === '') { - return 'cases.markdown'; - } +export const getMarkdownEditorStorageKey = ( + appId: string, + caseId: string, + commentId: string +): string => { + const appIdKey = appId !== '' ? appId : 'kibana'; + const caseIdKey = caseId !== '' ? caseId : 'case'; + const commentIdKey = commentId !== '' ? commentId : 'comment'; - return `cases.caseView.${caseId}.${commentId}.markdownEditor`; + return `cases.${appIdKey}.${caseIdKey}.${commentIdKey}.markdownEditor`; }; diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index ed97cfa703972..e91adce5ad1d2 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -7,10 +7,13 @@ import React from 'react'; import { mount } from 'enzyme'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import type { Content } from './schema'; +import { schema } from './schema'; import { UserActionMarkdown } from './markdown_form'; import type { AppMockRenderer } from '../../common/mock'; import { createAppMockRenderer, TestProviders } from '../../common/mock'; -import { waitFor, fireEvent } from '@testing-library/react'; +import { waitFor, fireEvent, render, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; const onChangeEditable = jest.fn(); const onSaveContent = jest.fn(); @@ -18,7 +21,7 @@ const onSaveContent = jest.fn(); const newValue = 'Hello from Tehas'; const emptyValue = ''; const hyperlink = `[hyperlink](http://elastic.co)`; -const draftStorageKey = `cases.caseView.caseId.markdown-id.markdownEditor`; +const draftStorageKey = `cases.testAppId.caseId.markdown-id.markdownEditor`; const defaultProps = { content: `A link to a timeline ${hyperlink}`, id: 'markdown-id', @@ -226,34 +229,110 @@ describe('UserActionMarkdown ', () => { }); describe('draft comment ', () => { - let appMockRenderer: AppMockRenderer; + const content = 'test content'; + const initialState = { content }; + const MockHookWrapperComponent: React.FC<{ testProviderProps?: unknown }> = ({ + children, + testProviderProps = {}, + }) => { + const { form } = useForm({ + defaultValue: initialState, + options: { stripEmptyFields: false }, + schema, + }); + + return ( + +
{children}
+
+ ); + }; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + sessionStorage.removeItem(draftStorageKey); + }); + + afterAll(() => { + jest.useRealTimers(); + }); beforeEach(() => { - appMockRenderer = createAppMockRenderer(); + jest.clearAllMocks(); }); it('Save button click clears session storage', async () => { - const result = appMockRenderer.render(); + const result = render( + + + + ); fireEvent.change(result.getByTestId('euiMarkdownEditorTextArea'), { target: { value: newValue }, }); + act(() => { + jest.advanceTimersByTime(1000); + }); + + expect(sessionStorage.getItem(draftStorageKey)).toBe(newValue); + fireEvent.click(result.getByTestId(`user-action-save-markdown`)); await waitFor(() => { expect(onSaveContent).toHaveBeenCalledWith(newValue); expect(onChangeEditable).toHaveBeenCalledWith(defaultProps.id); - expect(sessionStorage.getItem(draftStorageKey)).toBe('undefined'); + expect(sessionStorage.getItem(draftStorageKey)).toBe(null); }); }); it('Cancel button click clears session storage', async () => { - const result = appMockRenderer.render(); + const result = render( + + + + ); + + expect(sessionStorage.getItem(draftStorageKey)).toBe(null); + + fireEvent.change(result.getByTestId('euiMarkdownEditorTextArea'), { + target: { value: newValue }, + }); + + act(() => { + jest.advanceTimersByTime(1000); + }); + + await waitFor(() => { + expect(sessionStorage.getItem(draftStorageKey)).toBe(newValue); + }); fireEvent.click(result.getByTestId('user-action-cancel-markdown')); - expect(sessionStorage.getItem(draftStorageKey)).toBe(null); + await waitFor(() => { + expect(sessionStorage.getItem(draftStorageKey)).toBe(null); + }); + }); + + describe('existing storage key', () => { + beforeEach(() => { + sessionStorage.setItem(draftStorageKey, 'value set in storage'); + }); + + it('should have session storage value same as draft comment', async () => { + const result = render( + + + + ); + + expect(result.container.querySelector('textarea')!.value).toEqual('value set in storage'); + }); }); }); }); diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 3c34f245b36aa..980f73fef8f95 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -12,6 +12,8 @@ import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/h import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; +import { removeItemFromSessionStorate } from '../utils'; +import { useCasesContext } from '../cases_context/use_cases_context'; import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; import { UserActionMarkdownFooter } from './markdown_form_footer'; @@ -45,12 +47,13 @@ const UserActionMarkdownComponent = forwardRef< }); const fieldName = 'content'; - const draftStorageKey = getMarkdownEditorStorageKey(caseId, id); + const { appId } = useCasesContext(); + const draftStorageKey = getMarkdownEditorStorageKey(appId, caseId, id); const { setFieldValue, submit } = form; const handleCancelAction = useCallback(() => { onChangeEditable(id); - window.sessionStorage.removeItem(draftStorageKey); + removeItemFromSessionStorate(draftStorageKey); }, [id, onChangeEditable, draftStorageKey]); const handleSaveAction = useCallback(async () => { @@ -60,7 +63,7 @@ const UserActionMarkdownComponent = forwardRef< onSaveContent(data.content); } onChangeEditable(id); - window.sessionStorage.removeItem(draftStorageKey); + removeItemFromSessionStorate(draftStorageKey); }, [content, id, onChangeEditable, onSaveContent, submit, draftStorageKey]); const setComment = useCallback( diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts index 99ec0213ff4ad..537b547566a73 100644 --- a/x-pack/plugins/cases/public/components/utils.test.ts +++ b/x-pack/plugins/cases/public/components/utils.test.ts @@ -7,7 +7,12 @@ import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; -import { connectorDeprecationValidator, getConnectorIcon, isDeprecatedConnector } from './utils'; +import { + connectorDeprecationValidator, + getConnectorIcon, + isDeprecatedConnector, + removeItemFromSessionStorate, +} from './utils'; describe('Utils', () => { const connector = { @@ -85,4 +90,25 @@ describe('Utils', () => { ).toBe(false); }); }); + + describe('removeItemFromSessionStorate', () => { + const sessionKey = 'testKey'; + const sessionValue = 'test value'; + + it('successfully removes key from session storage', () => { + sessionStorage.setItem(sessionKey, sessionValue); + + expect(sessionStorage.getItem(sessionKey)).toBe(sessionValue); + + removeItemFromSessionStorate(sessionKey); + + expect(sessionStorage.getItem(sessionKey)).toBe(null); + }); + + it('is null if key is not in session storage', () => { + removeItemFromSessionStorate(sessionKey); + + expect(sessionStorage.getItem(sessionKey)).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 1a5f8134563ff..f65966e170b80 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -93,3 +93,7 @@ export const getConnectorIcon = ( export const isDeprecatedConnector = (connector?: CaseActionConnector): boolean => { return connector?.isDeprecated ?? false; }; + +export const removeItemFromSessionStorate = (key: string) => { + window.sessionStorage.removeItem(key); +}; From a2dc3a32a70d50dba6cdb41cf6b9fc22d9045d69 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:39:34 +0100 Subject: [PATCH 25/32] prevent useSessionStorage hook to JSON serialize stored values --- .../use_markdown_session_storage.test.tsx | 6 ++---- .../markdown_editor/use_markdown_session_storage.tsx | 9 +++++---- .../components/user_actions/markdown_form.test.tsx | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx index 108a6b6bd7dcb..aa72b61133a01 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx @@ -64,7 +64,7 @@ describe('useMarkdownSessionStorage', () => { ); await waitFor(() => { - expect(sessionStorage.getItem(sessionKey)).toBe(null); + expect(sessionStorage.getItem(sessionKey)).toBe(''); }); }); @@ -79,7 +79,7 @@ describe('useMarkdownSessionStorage', () => { } ); - expect(sessionStorage.getItem(sessionKey)).toBe(null); + expect(sessionStorage.getItem(sessionKey)).toBe(''); act(() => { jest.advanceTimersByTime(1000); @@ -107,8 +107,6 @@ describe('useMarkdownSessionStorage', () => { jest.advanceTimersByTime(1000); }); - // await waitForComponentToUpdate(); - rerender({ field: { ...field, value: 'new value' }, sessionKey, initialValue }); act(() => { diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx index aacf350e87107..2b92afbe7355a 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx @@ -8,6 +8,7 @@ import { useRef, useState } from 'react'; import { isEmpty } from 'lodash'; import useDebounce from 'react-use/lib/useDebounce'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; const STORAGE_DEBOUNCE_TIME = 500; @@ -27,7 +28,7 @@ export const useMarkdownSessionStorage = ({ const isFirstRender = useRef(true); const initialValueRef = useRef(initialValue); - const sessionValue = sessionStorage.getItem(sessionKey) ?? ''; + const [sessionValue, setSessionValue] = useSessionStorage(sessionKey, '', true); if (!isEmpty(sessionValue) && isFirstRender.current) { isFirstRender.current = false; @@ -48,13 +49,13 @@ export const useMarkdownSessionStorage = ({ it is a first render for Markdown editor, however field has value of visualization which is not stored in session hence saving this item in session storage */ - sessionStorage.setItem(sessionKey, field.value); + + setSessionValue(field.value); } isFirstRender.current = false; return; } - - sessionStorage.setItem(sessionKey, field.value); + setSessionValue(field.value); }, STORAGE_DEBOUNCE_TIME, [field.value] diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index e91adce5ad1d2..126b043ce87af 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -298,7 +298,7 @@ describe('UserActionMarkdown ', () => { ); - expect(sessionStorage.getItem(draftStorageKey)).toBe(null); + expect(sessionStorage.getItem(draftStorageKey)).toBe(''); fireEvent.change(result.getByTestId('euiMarkdownEditorTextArea'), { target: { value: newValue }, From 9775b947cb49b9f11cd95e14c2e2e94654acb0f3 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 13 Dec 2022 10:37:22 +0100 Subject: [PATCH 26/32] fix: type check fixed for form_context --- .../cases/public/components/create/form_context.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index ff65444c914c3..3e51ada9e63c7 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -91,6 +91,8 @@ const defaultCreateCaseForm: CreateCaseFormFieldsProps = { isLoadingConnectors: false, connectors: [], withSteps: true, + owner: ['securitySolution'], + draftStorageKey: 'cases.kibana.createCase.description.markdownEditor', }; const defaultPostPushToService = { From 1a1d54f18dbdd72e55bcc2f8c0d1adb922aa3a7b Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 13 Dec 2022 11:37:37 +0100 Subject: [PATCH 27/32] remove session storage key after each test --- .../cases/public/components/create/form_context.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 3e51ada9e63c7..94a7628d1b7c4 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -180,6 +180,10 @@ describe('Create case', () => { jest.clearAllMocks(); }); + afterEach(() => { + sessionStorage.removeItem(defaultCreateCaseForm.draftStorageKey); + }) + describe('Step 1 - Case Fields', () => { it('renders correctly', async () => { mockedContext.render( From 6a39c82f912a2ce4c821f193cfba4514d47245cd Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 13 Dec 2022 10:43:14 +0000 Subject: [PATCH 28/32] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- .../cases/public/components/create/form_context.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 94a7628d1b7c4..e9ee476baaec5 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -182,7 +182,7 @@ describe('Create case', () => { afterEach(() => { sessionStorage.removeItem(defaultCreateCaseForm.draftStorageKey); - }) + }); describe('Step 1 - Case Fields', () => { it('renders correctly', async () => { From acbcdfe4de875d74fd693048d013a2ca9792cfdb Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 13 Dec 2022 14:27:36 +0100 Subject: [PATCH 29/32] fix: add existing storage key test case, weird issue of comment stays in Markdown after added --- .../components/add_comment/index.test.tsx | 14 +++-- .../public/components/add_comment/index.tsx | 21 +++++++- .../public/components/create/form.test.tsx | 7 ++- .../components/create/form_context.test.tsx | 54 +++++++++++++++++++ .../use_markdown_session_storage.test.tsx | 1 + .../user_actions/markdown_form.test.tsx | 6 ++- .../cases/public/components/utils.test.ts | 4 ++ 7 files changed, 98 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index fde8288d85adc..9e6de10cf380a 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -48,6 +48,8 @@ const sampleData: CaseAttachmentWithoutOwner = { comment: 'what a cool comment', type: CommentType.user as const, }; +const appId = 'testAppId'; +const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; describe('AddComment ', () => { beforeEach(() => { @@ -55,6 +57,10 @@ describe('AddComment ', () => { useCreateAttachmentsMock.mockImplementation(() => defaultResponse); }); + afterEach(() => { + sessionStorage.removeItem(draftKey); + }); + it('should post comment on submit click', async () => { const wrapper = mount( @@ -213,7 +219,6 @@ describe('AddComment ', () => { describe('draft comment ', () => { let appMockRenderer: AppMockRenderer; - const appId = 'testAppId'; beforeEach(() => { appMockRenderer = createAppMockRenderer(); @@ -234,7 +239,6 @@ describe('draft comment ', () => { it('should clear session storage on submit', async () => { const result = appMockRenderer.render(); - const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; fireEvent.change(result.getByLabelText('caseComment'), { target: { value: sampleData.comment }, @@ -264,12 +268,14 @@ describe('draft comment ', () => { }); describe('existing storage key', () => { - const draftKey = `cases.${appId}.${addCommentProps.caseId}.${addCommentProps.id}.markdownEditor`; - beforeEach(() => { sessionStorage.setItem(draftKey, 'value set in storage'); }); + afterEach(() => { + sessionStorage.removeItem(draftKey); + }); + it('should have draft comment same as existing session storage', async () => { const result = appMockRenderer.render(); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 7a1ca9869fdcc..d21b18d90ec99 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -29,6 +29,7 @@ import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; +import { removeItemFromSessionStorate } from '../utils'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; @@ -118,9 +119,25 @@ export const AddComment = React.memo( data: [{ ...data, type: CommentType.user }], updateCase: onCommentPosted, }); - reset(); + + removeItemFromSessionStorate(draftStorageKey); + + /* had to add to this check that session key is removed to avaoid weird issue that on reset + Markdown editor form adds session storage comment back if it is not removed immediately */ + if (!sessionStorage.getItem(draftStorageKey)) { + reset(); + } } - }, [submit, onCommentSaving, createAttachments, caseId, owner, onCommentPosted, reset]); + }, [ + submit, + onCommentSaving, + createAttachments, + caseId, + owner, + onCommentPosted, + reset, + draftStorageKey, + ]); /** * Focus on the text area when a quote has been added. diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index f98cae684be69..d95f264089643 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -54,6 +54,7 @@ const casesFormProps: CreateCaseFormProps = { describe('CreateCaseForm', () => { let globalForm: FormHook; + const draftStorageKey = `cases.caseView.createCase.description.markdownEditor`; const MockHookWrapperComponent: React.FC<{ testProviderProps?: unknown }> = ({ children, @@ -81,6 +82,10 @@ describe('CreateCaseForm', () => { useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); + afterEach(() => { + sessionStorage.removeItem(draftStorageKey); + }); + it('renders with steps', async () => { const wrapper = mount( @@ -224,7 +229,6 @@ describe('CreateCaseForm', () => { /> ); - const draftStorageKey = `cases.caseView.createCase.description.markdownEditor`; const cancelBtn = result.getByTestId('create-case-cancel'); @@ -245,7 +249,6 @@ describe('CreateCaseForm', () => { /> ); - const draftStorageKey = `cases.kibana.createCase.description.markdownEditor`; const submitBtn = result.getByTestId('create-case-submit'); diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index e9ee476baaec5..ab9c5226d4c53 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -786,4 +786,58 @@ describe('Create case', () => { expect(screen.queryByTestId('createCaseAssigneesComboBox')).toBeNull(); }); }); + + describe('draft comment', () => { + describe('existing storage key', () => { + beforeEach(() => { + sessionStorage.setItem(defaultCreateCaseForm.draftStorageKey, 'value set in storage'); + }); + + afterEach(() => { + sessionStorage.removeItem(defaultCreateCaseForm.draftStorageKey); + }); + + it('should have session storage value same as draft comment', async () => { + mockedContext.render( + + + + + ); + + await waitForFormToRender(screen); + const descriptionInput = within(screen.getByTestId('caseDescription')).getByTestId( + 'euiMarkdownEditorTextArea' + ); + + expect(descriptionInput).toHaveValue('value set in storage'); + }); + }); + + describe('set storage key', () => { + afterEach(() => { + sessionStorage.removeItem(defaultCreateCaseForm.draftStorageKey); + }); + + it('should have session storage value same as draft comment', async () => { + mockedContext.render( + + + + + ); + + await waitForFormToRender(screen); + const descriptionInput = within(screen.getByTestId('caseDescription')).getByTestId( + 'euiMarkdownEditorTextArea' + ); + + await waitFor(() => { + expect(descriptionInput).toHaveValue( + sessionStorage.getItem(defaultCreateCaseForm.draftStorageKey) + ); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx index aa72b61133a01..957669683fcc1 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx @@ -33,6 +33,7 @@ describe('useMarkdownSessionStorage', () => { afterEach(() => { jest.clearAllTimers(); + sessionStorage.removeItem(sessionKey); }); afterAll(() => { diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index 126b043ce87af..44ac346199e09 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -37,6 +37,10 @@ describe('UserActionMarkdown ', () => { jest.clearAllMocks(); }); + afterEach(() => { + sessionStorage.removeItem(draftStorageKey); + }); + it('Renders markdown correctly when not in edit mode', async () => { const wrapper = mount( @@ -254,11 +258,11 @@ describe('UserActionMarkdown ', () => { afterEach(() => { jest.clearAllTimers(); - sessionStorage.removeItem(draftStorageKey); }); afterAll(() => { jest.useRealTimers(); + sessionStorage.removeItem(draftStorageKey); }); beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts index 537b547566a73..a2c8c7f11d93b 100644 --- a/x-pack/plugins/cases/public/components/utils.test.ts +++ b/x-pack/plugins/cases/public/components/utils.test.ts @@ -95,6 +95,10 @@ describe('Utils', () => { const sessionKey = 'testKey'; const sessionValue = 'test value'; + afterEach(() => { + sessionStorage.removeItem(sessionKey); + }); + it('successfully removes key from session storage', () => { sessionStorage.setItem(sessionKey, sessionValue); From cbfb2c604804e1eaa39333675fa4dfe6d48b3b3d Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:50:15 +0100 Subject: [PATCH 30/32] typos fixed --- .../plugins/cases/public/components/add_comment/index.tsx | 6 +++--- x-pack/plugins/cases/public/components/create/form.tsx | 6 +++--- .../markdown_editor/use_markdown_session_storage.test.tsx | 6 +++--- .../cases/public/components/markdown_editor/utils.test.ts | 2 +- .../cases/public/components/markdown_editor/utils.ts | 2 +- .../public/components/user_actions/markdown_form.tsx | 6 +++--- x-pack/plugins/cases/public/components/utils.test.ts | 8 ++++---- x-pack/plugins/cases/public/components/utils.ts | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index d21b18d90ec99..7ae460925698c 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -29,7 +29,7 @@ import type { Case } from '../../containers/types'; import type { EuiMarkdownEditorRef } from '../markdown_editor'; import { MarkdownEditorForm } from '../markdown_editor'; import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; -import { removeItemFromSessionStorate } from '../utils'; +import { removeItemFromSessionStorage } from '../utils'; import * as i18n from './translations'; import type { AddCommentFormSchema } from './schema'; @@ -120,9 +120,9 @@ export const AddComment = React.memo( updateCase: onCommentPosted, }); - removeItemFromSessionStorate(draftStorageKey); + removeItemFromSessionStorage(draftStorageKey); - /* had to add to this check that session key is removed to avaoid weird issue that on reset + /* had to add to this check that session key is removed to avoid weird issue that on reset Markdown editor form adds session storage comment back if it is not removed immediately */ if (!sessionStorage.getItem(draftStorageKey)) { reset(); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 4e5a148286493..7be18f167ab1f 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -28,7 +28,7 @@ import type { Case } from '../../containers/types'; import type { CasesTimelineIntegration } from '../timeline_context'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; import { InsertTimeline } from '../insert_timeline'; -import { removeItemFromSessionStorate } from '../utils'; +import { removeItemFromSessionStorage } from '../utils'; import type { UseCreateAttachments } from '../../containers/use_create_attachments'; import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; import { SubmitCaseButton } from './submit_button'; @@ -200,7 +200,7 @@ export const CreateCaseForm: React.FC = React.memo( const handleOnConfirmationCallback = (): void => { onCancel(); - removeItemFromSessionStorate(draftStorageKey); + removeItemFromSessionStorage(draftStorageKey); }; const { showConfirmationModal, onOpenModal, onConfirmModal, onCancelModal } = @@ -209,7 +209,7 @@ export const CreateCaseForm: React.FC = React.memo( }); const handleOnSuccess = (theCase: Case): Promise => { - removeItemFromSessionStorate(draftStorageKey); + removeItemFromSessionStorage(draftStorageKey); return onSuccess(theCase); }; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx index 957669683fcc1..cef69256dd4b6 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx @@ -69,7 +69,7 @@ describe('useMarkdownSessionStorage', () => { }); }); - it('should set seeion storage when field has value and session key is not created yet', async () => { + it('should set session storage when field has value and session key is not created yet', async () => { const specialCharsValue = '!{tooltip[Hello again](This is tooltip!)}'; const { waitFor, result } = renderHook( (props) => { @@ -139,14 +139,14 @@ describe('useMarkdownSessionStorage', () => { describe('existing session key', () => { beforeEach(() => { - sessionStorage.setItem(sessionKey, 'exisitng session storage value'); + sessionStorage.setItem(sessionKey, 'existing session storage value'); }); afterEach(() => { sessionStorage.removeItem(sessionKey); }); - it('should set field value if seesion already exists and it is a first render', async () => { + it('should set field value if session already exists and it is a first render', async () => { const { waitFor, result } = renderHook( (props) => { return useMarkdownSessionStorage(props); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts b/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts index 11a94424f8e62..ef1de4a1bc327 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/utils.test.ts @@ -37,6 +37,6 @@ describe('getMarkdownEditorStorageKey', () => { const caseId = 'case-id'; const commentId = 'comment-id'; const sessionKey = getMarkdownEditorStorageKey(appId, caseId, commentId); - expect(sessionKey).toEqual(`cases.kibana.${caseId}.${commentId}.markdownEditor`); + expect(sessionKey).toEqual(`cases.cases.${caseId}.${commentId}.markdownEditor`); }); }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/utils.ts b/x-pack/plugins/cases/public/components/markdown_editor/utils.ts index a895503b43276..1fb81875b926b 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/utils.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/utils.ts @@ -10,7 +10,7 @@ export const getMarkdownEditorStorageKey = ( caseId: string, commentId: string ): string => { - const appIdKey = appId !== '' ? appId : 'kibana'; + const appIdKey = appId !== '' ? appId : 'cases'; const caseIdKey = caseId !== '' ? caseId : 'case'; const commentIdKey = commentId !== '' ? commentId : 'comment'; diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx index 980f73fef8f95..60b1a2bda3b6a 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.tsx @@ -12,7 +12,7 @@ import { Form, useForm, UseField } from '@kbn/es-ui-shared-plugin/static/forms/h import type { Content } from './schema'; import { schema } from './schema'; import { MarkdownRenderer, MarkdownEditorForm } from '../markdown_editor'; -import { removeItemFromSessionStorate } from '../utils'; +import { removeItemFromSessionStorage } from '../utils'; import { useCasesContext } from '../cases_context/use_cases_context'; import { getMarkdownEditorStorageKey } from '../markdown_editor/utils'; import { UserActionMarkdownFooter } from './markdown_form_footer'; @@ -53,7 +53,7 @@ const UserActionMarkdownComponent = forwardRef< const handleCancelAction = useCallback(() => { onChangeEditable(id); - removeItemFromSessionStorate(draftStorageKey); + removeItemFromSessionStorage(draftStorageKey); }, [id, onChangeEditable, draftStorageKey]); const handleSaveAction = useCallback(async () => { @@ -63,7 +63,7 @@ const UserActionMarkdownComponent = forwardRef< onSaveContent(data.content); } onChangeEditable(id); - removeItemFromSessionStorate(draftStorageKey); + removeItemFromSessionStorage(draftStorageKey); }, [content, id, onChangeEditable, onSaveContent, submit, draftStorageKey]); const setComment = useCallback( diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts index a2c8c7f11d93b..faf9cb2af727b 100644 --- a/x-pack/plugins/cases/public/components/utils.test.ts +++ b/x-pack/plugins/cases/public/components/utils.test.ts @@ -11,7 +11,7 @@ import { connectorDeprecationValidator, getConnectorIcon, isDeprecatedConnector, - removeItemFromSessionStorate, + removeItemFromSessionStorage, } from './utils'; describe('Utils', () => { @@ -91,7 +91,7 @@ describe('Utils', () => { }); }); - describe('removeItemFromSessionStorate', () => { + describe('removeItemFromSessionStorage', () => { const sessionKey = 'testKey'; const sessionValue = 'test value'; @@ -104,13 +104,13 @@ describe('Utils', () => { expect(sessionStorage.getItem(sessionKey)).toBe(sessionValue); - removeItemFromSessionStorate(sessionKey); + removeItemFromSessionStorage(sessionKey); expect(sessionStorage.getItem(sessionKey)).toBe(null); }); it('is null if key is not in session storage', () => { - removeItemFromSessionStorate(sessionKey); + removeItemFromSessionStorage(sessionKey); expect(sessionStorage.getItem(sessionKey)).toBe(null); }); diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index f65966e170b80..b026dce662340 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -94,6 +94,6 @@ export const isDeprecatedConnector = (connector?: CaseActionConnector): boolean return connector?.isDeprecated ?? false; }; -export const removeItemFromSessionStorate = (key: string) => { +export const removeItemFromSessionStorage = (key: string) => { window.sessionStorage.removeItem(key); }; From 426ab55106b49bd6b30f5d7f4ffe594349f8e919 Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Tue, 13 Dec 2022 18:40:46 +0100 Subject: [PATCH 31/32] fix --- x-pack/plugins/cases/public/components/add_comment/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 7ae460925698c..2de363d68e498 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -123,7 +123,7 @@ export const AddComment = React.memo( removeItemFromSessionStorage(draftStorageKey); /* had to add to this check that session key is removed to avoid weird issue that on reset - Markdown editor form adds session storage comment back if it is not removed immediately */ + Markdown editor form adds session storage comment back if it is not removed immediately. */ if (!sessionStorage.getItem(draftStorageKey)) { reset(); } From 4de1551ae0f437490696f12c4cc0d1c310c8779d Mon Sep 17 00:00:00 2001 From: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com> Date: Wed, 14 Dec 2022 13:18:02 +0100 Subject: [PATCH 32/32] fix: description text updated, isFristrender check removed from debounce block, unit tests updated --- .../cases/public/common/translations.ts | 11 +++++------ .../public/components/add_comment/index.tsx | 6 +----- .../components/create/form_context.test.tsx | 10 ++++++++-- .../components/markdown_editor/eui_form.tsx | 6 +++++- .../use_markdown_session_storage.test.tsx | 8 ++++++-- .../use_markdown_session_storage.tsx | 18 ++++-------------- .../user_actions/markdown_form.test.tsx | 2 +- 7 files changed, 30 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index cf71eb950b1ba..ead5a92bd6c7c 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -311,13 +311,12 @@ export const ADD_TAG_CUSTOM_OPTION_LABEL = (searchValue: string) => values: { searchValue }, }); -export const COMMENT_VERSION_CONFLICT_WARNING = i18n.translate( - 'xpack.cases.configure.commentVersionConflictWarning', - { +export const VERSION_CONFLICT_WARNING = (markdownId: string) => + i18n.translate('xpack.cases.configure.commentVersionConflictWarning', { defaultMessage: - 'This comment was updated. Saving your changes will overwrite the updated value.', - } -); + 'This {markdownId} was updated. Saving your changes will overwrite the updated value.', + values: { markdownId }, + }); /** * EUI checkbox replace {searchValue} with the current diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 2de363d68e498..d8e63da8158f2 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -122,11 +122,7 @@ export const AddComment = React.memo( removeItemFromSessionStorage(draftStorageKey); - /* had to add to this check that session key is removed to avoid weird issue that on reset - Markdown editor form adds session storage comment back if it is not removed immediately. */ - if (!sessionStorage.getItem(draftStorageKey)) { - reset(); - } + reset({ defaultValue: {} }); } }, [ submit, diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index ab9c5226d4c53..1f7551b466b8a 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { Screen } from '@testing-library/react'; -import { waitFor, within, screen } from '@testing-library/react'; +import { waitFor, within, screen, act } from '@testing-library/react'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { CaseSeverity, CommentType, ConnectorTypes } from '../../../common/api'; @@ -810,7 +810,13 @@ describe('Create case', () => { 'euiMarkdownEditorTextArea' ); - expect(descriptionInput).toHaveValue('value set in storage'); + jest.useFakeTimers(); + + act(() => jest.advanceTimersByTime(1000)); + + await waitFor(() => expect(descriptionInput).toHaveValue('value set in storage')); + + jest.useRealTimers(); }); }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 9f918605b9166..4e00b0f8b9b19 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -61,6 +61,10 @@ export const MarkdownEditorForm = React.memo( initialValue, }); + const conflictWarningText = i18n.VERSION_CONFLICT_WARNING( + id === 'description' ? id : 'comment' + ); + const commentEditorContextValue = useMemo( () => ({ editorId: id, @@ -78,7 +82,7 @@ export const MarkdownEditorForm = React.memo( describedByIds={idAria ? [idAria] : undefined} fullWidth error={errorMessage} - helpText={hasConflicts ? i18n.COMMENT_VERSION_CONFLICT_WARNING : field.helpText} + helpText={hasConflicts ? conflictWarningText : field.helpText} isInvalid={isInvalid} label={field.label} labelAppend={field.labelAppend} diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx index cef69256dd4b6..7de2e83cf234d 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.test.tsx @@ -54,7 +54,7 @@ describe('useMarkdownSessionStorage', () => { }); }); - it('should not update the session value when it is first render ', async () => { + it('should update the session value with field value when it is first render', async () => { const { waitFor } = renderHook( (props) => { return useMarkdownSessionStorage(props); @@ -64,8 +64,12 @@ describe('useMarkdownSessionStorage', () => { } ); + act(() => { + jest.advanceTimersByTime(1000); + }); + await waitFor(() => { - expect(sessionStorage.getItem(sessionKey)).toBe(''); + expect(sessionStorage.getItem(sessionKey)).toBe(field.value); }); }); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx index 2b92afbe7355a..e33fed6729858 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_markdown_session_storage.tsx @@ -31,10 +31,13 @@ export const useMarkdownSessionStorage = ({ const [sessionValue, setSessionValue] = useSessionStorage(sessionKey, '', true); if (!isEmpty(sessionValue) && isFirstRender.current) { - isFirstRender.current = false; field.setValue(sessionValue); } + if (isFirstRender.current) { + isFirstRender.current = false; + } + if (initialValue !== initialValueRef.current && initialValue !== field.value) { initialValueRef.current = initialValue; setHasConflicts(true); @@ -42,19 +45,6 @@ export const useMarkdownSessionStorage = ({ useDebounce( () => { - if (isFirstRender.current) { - if (isEmpty(sessionValue) && !isEmpty(field.value)) { - /* this condition is used to for lens draft comment, - when user selects and visualization and comes back to Markdown editor, - it is a first render for Markdown editor, however field has value of visualization which is not stored in session - hence saving this item in session storage - */ - - setSessionValue(field.value); - } - isFirstRender.current = false; - return; - } setSessionValue(field.value); }, STORAGE_DEBOUNCE_TIME, diff --git a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx index 44ac346199e09..972a520d2085c 100644 --- a/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/markdown_form.test.tsx @@ -335,7 +335,7 @@ describe('UserActionMarkdown ', () => { ); - expect(result.container.querySelector('textarea')!.value).toEqual('value set in storage'); + expect(result.getByText('value set in storage')).toBeInTheDocument(); }); }); });