From 696ba2f1566697afe409d5772131f04b06182d73 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 13 Aug 2024 11:19:41 +0800 Subject: [PATCH] Allow input text when exceed max length Signed-off-by: Lin Wang --- .../workspace_description_field.test.tsx | 24 ++++---- .../fields/workspace_description_field.tsx | 23 ++++--- .../fields/workspace_name_field.test.tsx | 19 +++--- .../fields/workspace_name_field.tsx | 22 ++++--- .../workspace_form/use_workspace_form.ts | 7 +-- .../workspace_create_action_panel.test.tsx | 60 +++++++++++++++++++ .../workspace_create_action_panel.tsx | 13 +++- .../workspace_form/workspace_form.tsx | 2 +- 8 files changed, 127 insertions(+), 43 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.test.tsx diff --git a/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.test.tsx b/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.test.tsx index 38aa84ba869e..e8795bef7a76 100644 --- a/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.test.tsx +++ b/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.test.tsx @@ -10,7 +10,7 @@ import { MAX_WORKSPACE_DESCRIPTION_LENGTH } from '../../../../common/constants'; import { WorkspaceDescriptionField } from './workspace_description_field'; describe('', () => { - it('should call onChange when the new value is within MAX_WORKSPACE_DESCRIPTION_LENGTH', () => { + it('should call onChange when the new value', () => { const onChangeMock = jest.fn(); const value = 'test'; @@ -20,20 +20,24 @@ describe('', () => { fireEvent.change(textarea, { target: { value: 'new value' } }); expect(onChangeMock).toHaveBeenCalledWith('new value'); - }); - - it('should not call onChange when the new value exceeds MAX_WORKSPACE_DESCRIPTION_LENGTH', () => { - const onChangeMock = jest.fn(); - const value = 'a'.repeat(MAX_WORKSPACE_DESCRIPTION_LENGTH); - render(); - - const textarea = screen.getByPlaceholderText('Describe the workspace'); fireEvent.change(textarea, { target: { value: 'a'.repeat(MAX_WORKSPACE_DESCRIPTION_LENGTH + 1) }, }); - expect(onChangeMock).not.toHaveBeenCalled(); + expect(onChangeMock).toHaveBeenCalledWith('a'.repeat(MAX_WORKSPACE_DESCRIPTION_LENGTH + 1)); + }); + + it('should render the correct number of characters left when value larger than MAX_WORKSPACE_DESCRIPTION_LENGTH', () => { + render( + + ); + + const helpText = screen.getByText(new RegExp(`-1.+characters left\.`)); + expect(helpText).toBeInTheDocument(); }); it('should render the correct number of characters left when value is empty', () => { diff --git a/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.tsx b/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.tsx index 3bad252543c2..599445cd25d2 100644 --- a/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.tsx +++ b/src/plugins/workspace/public/components/workspace_form/fields/workspace_description_field.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiCompressedFormRow, EuiCompressedTextArea } from '@elastic/eui'; +import { EuiCompressedFormRow, EuiCompressedTextArea, EuiTextColor } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React, { useCallback } from 'react'; @@ -24,13 +24,12 @@ export const WorkspaceDescriptionField = ({ }: WorkspaceDescriptionFieldProps) => { const handleChange = useCallback( (e) => { - const newValue = e.currentTarget.value; - if (newValue.length <= MAX_WORKSPACE_DESCRIPTION_LENGTH) { - onChange(newValue); - } + onChange(e.currentTarget.value); }, [onChange] ); + const leftCharacters = MAX_WORKSPACE_DESCRIPTION_LENGTH - (value?.length ?? 0); + const charactersOverflow = leftCharacters < 0; return ( optional } - isInvalid={!!error} + isInvalid={!!error || charactersOverflow} error={error} - helpText={<>{MAX_WORKSPACE_DESCRIPTION_LENGTH - (value?.length ?? 0)} characters left.} + helpText={ + + {i18n.translate('workspace.form.description.charactersLeft', { + defaultMessage: '{leftCharacters} characters left.', + values: { + leftCharacters, + }, + })} + + } > ); diff --git a/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.test.tsx b/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.test.tsx index 2305aac8f1da..50779124f3f8 100644 --- a/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.test.tsx +++ b/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.test.tsx @@ -10,7 +10,7 @@ import { MAX_WORKSPACE_NAME_LENGTH } from '../../../../common/constants'; import { WorkspaceNameField } from './workspace_name_field'; describe('', () => { - it('should call onChange when the new value is within MAX_WORKSPACE_NAME_LENGTH', () => { + it('should call onChange when the new value', () => { const onChangeMock = jest.fn(); const value = 'test'; @@ -20,18 +20,19 @@ describe('', () => { fireEvent.change(input, { target: { value: 'new value' } }); expect(onChangeMock).toHaveBeenCalledWith('new value'); - }); - it('should not call onChange when the new value exceeds MAX_WORKSPACE_NAME_LENGTH', () => { - const onChangeMock = jest.fn(); - const value = 'a'.repeat(MAX_WORKSPACE_NAME_LENGTH); + fireEvent.change(input, { target: { value: 'a'.repeat(MAX_WORKSPACE_NAME_LENGTH + 1) } }); - render(); + expect(onChangeMock).toHaveBeenCalledWith('a'.repeat(MAX_WORKSPACE_NAME_LENGTH + 1)); + }); - const input = screen.getByPlaceholderText('Enter a name'); - fireEvent.change(input, { target: { value: 'a'.repeat(MAX_WORKSPACE_NAME_LENGTH + 1) } }); + it('should render the correct number of characters left when value greater than MAX_WORKSPACE_NAME_LENGTH', () => { + render( + + ); - expect(onChangeMock).not.toHaveBeenCalled(); + const helpText = screen.getByText(new RegExp(`-1.+characters left\.`)); + expect(helpText).toBeInTheDocument(); }); it('should render the correct number of characters left when value is empty', () => { diff --git a/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx b/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx index 54ab6ec9a177..e8ddf6b4e3ea 100644 --- a/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx +++ b/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiCompressedFieldText, EuiCompressedFormRow } from '@elastic/eui'; +import { EuiCompressedFieldText, EuiCompressedFormRow, EuiTextColor } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React, { useCallback } from 'react'; @@ -24,13 +24,12 @@ export const WorkspaceNameField = ({ }: WorkspaceNameFieldProps) => { const handleChange = useCallback( (e) => { - const newValue = e.currentTarget.value; - if (newValue.length <= MAX_WORKSPACE_NAME_LENGTH) { - onChange(newValue); - } + onChange(e.currentTarget.value); }, [onChange] ); + const leftCharacters = MAX_WORKSPACE_NAME_LENGTH - (value?.length ?? 0); + const charactersOverflow = leftCharacters < 0; return ( - {MAX_WORKSPACE_NAME_LENGTH - (value?.length ?? 0)} characters left.
+ + {i18n.translate('workspace.form.name.charactersLeft', { + defaultMessage: '{leftCharacters} characters left.', + values: { + leftCharacters, + }, + })} + +
{i18n.translate('workspace.form.workspaceDetails.name.helpText', { defaultMessage: 'Use a unique name for the workspace. Valid characters are a-z, A-Z, 0-9, (), [], _ (underscore), - (hyphen) and (space).', })} } - isInvalid={!!error} + isInvalid={!!error || charactersOverflow} error={error} >
); diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index 7a2dfb3a29ca..8b03d246d568 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -4,12 +4,7 @@ */ import { useCallback, useState, FormEventHandler, useRef, useMemo } from 'react'; -import { - htmlIdGenerator, - EuiFieldTextProps, - EuiTextAreaProps, - EuiColorPickerProps, -} from '@elastic/eui'; +import { htmlIdGenerator, EuiColorPickerProps } from '@elastic/eui'; import { useApplications } from '../../hooks'; import { diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.test.tsx new file mode 100644 index 000000000000..6f1dbc58bf9e --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { applicationServiceMock } from '../../../../../core/public/mocks'; +import { + MAX_WORKSPACE_DESCRIPTION_LENGTH, + MAX_WORKSPACE_NAME_LENGTH, +} from '../../../common/constants'; +import { WorkspaceCreateActionPanel } from './workspace_create_action_panel'; + +const mockApplication = applicationServiceMock.createStartContract(); + +describe('WorkspaceCreateActionPanel', () => { + const formId = 'workspaceForm'; + const formData = { + name: 'Test Workspace', + description: 'This is a test workspace', + }; + + it('should disable the "Create Workspace" button when name exceeds the maximum length', () => { + const longName = 'a'.repeat(MAX_WORKSPACE_NAME_LENGTH + 1); + render( + + ); + const createButton = screen.getByText('Create workspace'); + expect(createButton.closest('button')).toBeDisabled(); + }); + + it('should disable the "Create Workspace" button when description exceeds the maximum length', () => { + const longDescription = 'a'.repeat(MAX_WORKSPACE_DESCRIPTION_LENGTH + 1); + render( + + ); + const createButton = screen.getByText('Create workspace'); + expect(createButton.closest('button')).toBeDisabled(); + }); + + it('should enable the "Create Workspace" button when name and description are within the maximum length', () => { + render( + + ); + const createButton = screen.getByText('Create workspace'); + expect(createButton.closest('button')).not.toBeDisabled(); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.tsx index a7824ffac428..0b914c0a7658 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_create_action_panel.tsx @@ -6,21 +6,31 @@ import { EuiSmallButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React, { useState, useCallback } from 'react'; -import { ApplicationStart } from 'opensearch-dashboards/public'; +import type { ApplicationStart } from 'opensearch-dashboards/public'; +import type { WorkspaceFormData } from './types'; import { WorkspaceCancelModal } from './workspace_cancel_modal'; +import { + MAX_WORKSPACE_DESCRIPTION_LENGTH, + MAX_WORKSPACE_NAME_LENGTH, +} from '../../../common/constants'; interface WorkspaceCreateActionPanelProps { formId: string; + formData: Partial>; application: ApplicationStart; } export const WorkspaceCreateActionPanel = ({ formId, + formData, application, }: WorkspaceCreateActionPanelProps) => { const [isCancelModalVisible, setIsCancelModalVisible] = useState(false); const closeCancelModal = useCallback(() => setIsCancelModalVisible(false), []); const showCancelModal = useCallback(() => setIsCancelModalVisible(true), []); + const createButtonDisabled = + (formData.name?.length ?? 0) > MAX_WORKSPACE_NAME_LENGTH || + (formData.description?.length ?? 0) > MAX_WORKSPACE_DESCRIPTION_LENGTH; return ( <> @@ -41,6 +51,7 @@ export const WorkspaceCreateActionPanel = ({ type="submit" form={formId} data-test-subj="workspaceForm-bottomBar-createButton" + disabled={createButtonDisabled} > {i18n.translate('workspace.form.bottomBar.createWorkspace', { defaultMessage: 'Create workspace', diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx index 80d1adab3553..fcedc87a1d42 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -126,7 +126,7 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { )} {operationType === WorkspaceOperationType.Create && ( - + )} {operationType === WorkspaceOperationType.Update && (