@@ -299,38 +256,26 @@ const UserProfileRoles = ({
-
-
@@ -340,11 +285,6 @@ const UserProfileRoles = ({
data-testid="user-profile-edit-roles-cancel-button"
iconLeading={}
size="xs"
- style={{
- position: 'absolute',
- bottom: '0px',
- right: '38px',
- }}
onClick={handleCloseEditRole}
/>
}
isLoading={isLoading}
size="xs"
- style={{ position: 'absolute', bottom: '0px' }}
onClick={handleRolesSave}
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoConfigurationFormArrayFieldTemplate.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoConfigurationFormArrayFieldTemplate.tsx
index 00b5c5053ccf..b92f68a9570f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoConfigurationFormArrayFieldTemplate.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoConfigurationFormArrayFieldTemplate.tsx
@@ -12,49 +12,22 @@
*/
import {
+ Autocomplete,
Badge,
Grid,
- Tooltip,
Typography,
} from '@openmetadata/ui-core-components';
import { FieldProps } from '@rjsf/utils';
-import { Select } from 'antd';
import classNames from 'classnames';
import { isArray, isEmpty, isObject, startCase } from 'lodash';
-import type { CustomTagProps } from 'rc-select/lib/BaseSelect';
-import { useCallback, useMemo } from 'react';
+import { Key, KeyboardEvent, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { ReactComponent as CloseIcon } from '../../../assets/svg/close.svg';
import { DEPRECATED_SSO_PROPERTIES } from '../../../constants/Services.constant';
import { useClipboard } from '../../../hooks/useClipBoard';
import { splitCSV } from '../../../utils/CSV/CSVPureUtils';
import { isValidUrl } from '../../../utils/SSOUtils';
import './sso-configuration-form-array-field-template.less';
-const SsoCustomTagRenderer = (props: CustomTagProps) => {
- const { label, closable, onClose } = props;
- const labelStr = (label as string) ?? '';
-
- const labelNode = labelStr ? (
-
- {label}
-
- ) : (
-
{label}
- );
-
- return (
-
- {labelNode}
- {closable && (
-
- )}
-
- );
-};
-
const getArrayFieldValue = (
formData: FieldProps['formData'],
isScopeField: boolean
@@ -113,8 +86,8 @@ const SsoConfigurationFormArrayFieldTemplate = (props: FieldProps) => {
isArray(props.schema.items.enum)
) {
return (props.schema.items.enum as string[]).map((option) => ({
+ id: option,
label: option,
- value: option,
}));
}
@@ -256,44 +229,38 @@ const SsoConfigurationFormArrayFieldTemplate = (props: FieldProps) => {
)}
-
-
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoRolesSelectField.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoRolesSelectField.tsx
index d98398279aeb..ddae004c2c50 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoRolesSelectField.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SsoRolesSelectField.tsx
@@ -11,13 +11,16 @@
* limitations under the License.
*/
-import { Grid, Typography } from '@openmetadata/ui-core-components';
+import {
+ Autocomplete,
+ Grid,
+ Typography,
+} from '@openmetadata/ui-core-components';
import { FieldProps } from '@rjsf/utils';
-import { Select } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { debounce, startCase } from 'lodash';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Key, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { searchRoles } from '../../../rest/rolesAPIV1';
import { showErrorToast } from '../../../utils/ToastUtils';
@@ -119,27 +122,35 @@ const SsoRolesSelectField = (props: FieldProps) => {
{startCase(props.name)}
-
-
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form-array-field-template.less b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form-array-field-template.less
index 90c1e693c674..b48487642db8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form-array-field-template.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form-array-field-template.less
@@ -44,130 +44,6 @@
}
}
-.sso-select-container {
- position: relative;
- overflow: hidden;
-
- .ant-select-selection-overflow {
- padding-left: var(--om-space-2) !important;
- padding-right: @size-md;
- gap: @size-xxs;
- padding-bottom: 0;
- }
-
- .ant-select-selection-item {
- display: flex;
- align-items: center;
- gap: @size-xxs;
- background: @blue-29;
- border: 1px solid @blue-30;
- border-radius: var(--om-radius-md);
- margin: 0;
- min-height: @size-lg;
- height: auto;
- color: @grey-900;
- font-size: @size-sm;
- font-weight: @font-medium;
- word-break: break-all;
- word-wrap: break-word;
- white-space: normal;
- padding: @size-xxs @size-xs;
-
- .ant-select-selection-item-content {
- flex: 1;
- word-break: break-all;
- word-wrap: break-word;
- white-space: normal;
- line-height: 1.4;
- color: @tag-text;
- font-weight: @font-regular;
- font-size: @size-sm;
- }
-
- .ant-select-selection-item-remove {
- margin-left: @size-xxs;
- display: flex;
- align-items: flex-start;
- justify-content: center;
- align-self: flex-start;
- margin: auto;
- flex-shrink: 0;
-
- background-color: transparent;
- border: none;
-
- &:hover {
- background-color: transparent;
- }
- }
- }
-
- .ant-select-selector {
- background: @background-color;
- border: 1px solid @grey-300;
- border-radius: @border-rad-xs;
-
- box-shadow: 0px 1px 2px 0px @grey-27;
-
- &:hover {
- border-color: @primary-6;
- }
-
- &:focus {
- border-color: @primary-color;
- box-shadow: @box-shadow-ant-select;
- outline: none;
- }
-
- .ant-select-selection-search {
- margin: 0;
-
- .ant-select-selection-search-input {
- padding-left: 0;
- margin-left: 0;
- text-indent: 0;
- box-sizing: border-box;
- }
- }
-
- .ant-select-selection-placeholder {
- color: @grey-500;
- font-size: @font-size-base;
- font-weight: var(--om-font-weight-regular);
- line-height: @size-mlg;
- margin-left: var(--om-space-2);
- }
- }
-
- // Hover state for the select
- .ant-select:hover .ant-select-selector {
- border-color: @primary-6;
- }
-
- // Focused state for the select
- .ant-select-focused .ant-select-selector {
- border-color: @primary-color;
- box-shadow: @box-shadow-ant-select;
- outline: none;
- }
-
- // Override Ant Design's default input positioning for tags mode
- .ant-select-selection-overflow {
- padding-left: 0;
- padding-bottom: 0;
- }
-
- .ant-select-selection-search-input {
- padding-left: 0;
- margin-left: 0;
- text-indent: 0;
- }
-
- .ant-select:not(.ant-select-customize-input) .ant-select-selector {
- border-radius: @border-rad-xs;
- padding: @size-xs @size-sm;
- }
-}
.array-field-label {
color: @grey-700;
font-weight: 500;
@@ -178,27 +54,6 @@
}
}
-// Custom dropdown arrow icon for array field selects
-.sso-select-container {
- .ant-select .ant-select-arrow {
- .anticon {
- display: none;
- }
-
- &::after {
- content: '';
- display: block;
- width: @size-sm;
- height: @size-xs;
- margin-right: @size-xxs;
- background-image: url('../../../assets/svg/ic-dropdown-arrow.svg');
- background-size: @size-sm @size-xs;
- background-repeat: no-repeat;
- background-position: center;
- }
- }
-}
-
// Add box shadow to any input fields in array field templates
.sso-array-field {
input[type='text'],
@@ -221,7 +76,6 @@
}
// Apply proper blue focus styles to select container
-.sso-select-container,
.sso-array-field {
.ant-select {
&:hover .ant-select-selector {
@@ -295,10 +149,6 @@
}
// Specific override for ant-select show-search input
-.sso-select-container
- .ant-select-show-search.ant-select:not(.ant-select-customize-input)
- .ant-select-selector
- input,
.sso-array-field
.ant-select-show-search.ant-select:not(.ant-select-customize-input)
.ant-select-selector
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.component.tsx
index 9a089da92a0f..118b78cdd461 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.component.tsx
@@ -10,30 +10,53 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Input, InputProps } from 'antd';
+import { Box, Input } from '@openmetadata/ui-core-components';
import { useTranslation } from 'react-i18next';
-import './color-picker.style.less';
-const ColorPicker = (props: InputProps) => {
+export interface ColorPickerProps {
+ id?: string;
+ /** Names both controls, so each theme colour is distinguishable. */
+ label: string;
+ value?: string;
+ onBlur?: () => void;
+ onChange?: (value: string) => void;
+}
+
+const ColorPicker = ({
+ id,
+ label,
+ value = '',
+ onBlur,
+ onChange,
+}: ColorPickerProps) => {
const { t } = useTranslation();
- const { id, ...rest } = props;
+ const testId = id ? `${id}-color` : 'color';
return (
-
-
+ {/* Core has no colour swatch control; a core Input squeezes the native
+ swatch into a thin stripe, so the swatch stays a native colour input. */}
+ onChange?.(e.target.value)}
/>
-
-
+
+
+
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.test.tsx
index b34b5fc6ab61..d4ff13dbead2 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/ColorPicker.test.tsx
@@ -10,21 +10,50 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { render, screen } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
import ColorPicker from './ColorPicker.component';
describe('ColorPicker component', () => {
it('component should render', async () => {
- render(
);
+ render(
);
expect(await screen.findByTestId('color-picker')).toBeInTheDocument();
expect(await screen.findByTestId('color-input')).toBeInTheDocument();
});
it('Should have same value in color picker and input box', async () => {
- render(
);
+ render(
);
expect(await screen.findByTestId('color-picker')).toHaveValue('#000000');
expect(await screen.findByTestId('color-input')).toHaveValue('#000000');
});
+
+ it('reports a typed hex code as a plain string', () => {
+ const onChange = jest.fn();
+ render(
+
+ );
+
+ fireEvent.change(screen.getByTestId('primaryColor-color-input'), {
+ target: { value: '#123456' },
+ });
+
+ expect(onChange).toHaveBeenCalledWith('#123456');
+ });
+
+ it('names the hex box and the swatch after the field', () => {
+ render(
);
+
+ expect(screen.getByTestId('color-input')).toHaveAccessibleName(
+ 'Primary Color'
+ );
+ expect(screen.getByTestId('color-picker')).toHaveAccessibleName(
+ 'label.select-field'
+ );
+ });
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/color-picker.style.less b/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/color-picker.style.less
deleted file mode 100644
index 49b340eb0766..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/ColorPicker/color-picker.style.less
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright 2023 Collate.
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-.ant-input-group .ant-input.style-color-picker {
- width: 15%;
-}
-.ant-input-group .ant-input.style-color-input {
- width: 85%;
-}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomUnitSelect/CustomUnitSelect.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomUnitSelect/CustomUnitSelect.test.tsx
deleted file mode 100644
index 062904f78073..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomUnitSelect/CustomUnitSelect.test.tsx
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * Copyright 2025 Collate.
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
-import { UnitOfMeasurement } from '../../../generated/entity/data/metric';
-import { getCustomUnitsOfMeasurement } from '../../../rest/metricsAPI';
-import { showErrorToast } from '../../../utils/ToastUtils';
-import CustomUnitSelect from './CustomUnitSelect';
-
-// Mock dependencies
-jest.mock('../../../rest/metricsAPI', () => ({
- getCustomUnitsOfMeasurement: jest.fn(),
-}));
-
-jest.mock('../../../utils/ToastUtils', () => ({
- showErrorToast: jest.fn(),
-}));
-
-jest.mock('react-i18next', () => ({
- useTranslation: () => ({
- t: (key: string) => {
- const translations: Record
= {
- 'label.enter-custom-unit-of-measurement':
- 'Enter custom unit of measurement',
- 'label.add': 'Add',
- };
-
- return translations[key] || key;
- },
- }),
-}));
-
-const mockCustomUnits = ['Custom Unit 1', 'Custom Unit 2', 'Custom Unit 3'];
-
-describe('CustomUnitSelect Component', () => {
- const mockOnChange = jest.fn();
- const mockGetCustomUnits = getCustomUnitsOfMeasurement as jest.Mock;
- const mockShowErrorToast = showErrorToast as jest.Mock;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockGetCustomUnits.mockResolvedValue(mockCustomUnits);
- });
-
- describe('Basic Rendering', () => {
- it('should render component with default props', async () => {
- render();
-
- expect(
- screen.getByTestId('unit-of-measurement-select')
- ).toBeInTheDocument();
- });
-
- it('should render with custom data-testid', async () => {
- render();
-
- expect(screen.getByTestId('custom-unit-select')).toBeInTheDocument();
- });
-
- it('should render with placeholder text', async () => {
- render();
-
- expect(screen.getByText('Select unit')).toBeInTheDocument();
- });
-
- it('should be disabled when disabled prop is true', async () => {
- render();
-
- const select = screen.getByRole('combobox');
-
- expect(select).toBeDisabled();
- });
- });
-
- describe('API Integration', () => {
- it('should fetch custom units on mount', async () => {
- render();
-
- await waitFor(() => {
- expect(mockGetCustomUnits).toHaveBeenCalledTimes(1);
- });
- });
-
- it('should handle API error gracefully', async () => {
- const error = new Error('API Error');
- mockGetCustomUnits.mockRejectedValue(error);
-
- render();
-
- await waitFor(() => {
- expect(mockShowErrorToast).toHaveBeenCalledWith(error);
- });
- });
-
- it('should handle empty API response', async () => {
- mockGetCustomUnits.mockResolvedValue([]);
-
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- expect(screen.getByText('Count')).toBeInTheDocument();
- });
- });
-
- it('should handle null API response', async () => {
- mockGetCustomUnits.mockResolvedValue(null);
-
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- });
- });
- });
-
- describe('Options Display', () => {
- it('should display standard unit options', async () => {
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- expect(screen.getByText('Count')).toBeInTheDocument();
- expect(screen.queryByText('Other')).not.toBeInTheDocument();
- });
- });
-
- it('should display custom units from API', async () => {
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Custom Unit 1')).toBeInTheDocument();
- expect(screen.getByText('Custom Unit 2')).toBeInTheDocument();
- });
- });
-
- it('should show add custom unit interface', async () => {
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(
- screen.getByPlaceholderText('Enter custom unit of measurement')
- ).toBeInTheDocument();
- expect(
- screen.getByRole('button', { name: /add/i })
- ).toBeInTheDocument();
- });
- });
- });
-
- describe('Unit Selection', () => {
- it('should handle standard unit selection', async () => {
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByText('Percentage'));
-
- expect(mockOnChange).toHaveBeenCalledWith(
- UnitOfMeasurement.Percentage,
- undefined
- );
- });
-
- it('should handle custom unit selection', async () => {
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Custom Unit 1')).toBeInTheDocument();
- });
-
- fireEvent.click(screen.getByText('Custom Unit 1'));
-
- expect(mockOnChange).toHaveBeenCalledWith(
- UnitOfMeasurement.Other,
- 'Custom Unit 1'
- );
- });
-
- it('should display selected standard unit', async () => {
- render();
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- });
- });
-
- it('should display selected custom unit', async () => {
- render(
-
- );
-
- await waitFor(() => {
- expect(screen.getByText('My Custom Unit')).toBeInTheDocument();
- });
- });
-
- it('should handle onChange being undefined', async () => {
- render();
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- });
-
- expect(() => {
- fireEvent.click(screen.getByText('Percentage'));
- }).not.toThrow();
- });
- });
-
- describe('Custom Value Initialization', () => {
- it('should initialize with custom value', async () => {
- render(
-
- );
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- expect(screen.getByText('Initial Custom Unit')).toBeInTheDocument();
- });
- });
-
- it('should not duplicate existing custom value', async () => {
- mockGetCustomUnits.mockResolvedValue(['Existing Unit']);
-
- render(
-
- );
-
- await waitFor(() => {
- // The test passes if component renders without error
- expect(
- screen.getByTestId('unit-of-measurement-select')
- ).toBeInTheDocument();
- });
-
- fireEvent.mouseDown(screen.getByRole('combobox'));
-
- await waitFor(() => {
- // Should show options are available
- expect(screen.getByRole('listbox')).toBeInTheDocument();
- });
- });
- });
-
- describe('Performance and Memoization', () => {
- it('should use memoized options', async () => {
- const { rerender } = render();
-
- // Initial render should call API
- await waitFor(() => {
- expect(mockGetCustomUnits).toHaveBeenCalledTimes(1);
- });
-
- // Rerender with same props should not call API again
- rerender();
-
- // Should still only have been called once
- expect(mockGetCustomUnits).toHaveBeenCalledTimes(1);
- });
-
- it('should handle component updates efficiently', async () => {
- const { rerender } = render(
-
- );
-
- await waitFor(() => {
- expect(screen.getByText('Count')).toBeInTheDocument();
- });
-
- // Change value
- rerender();
-
- await waitFor(() => {
- expect(screen.getByText('Percentage')).toBeInTheDocument();
- });
-
- // API should still only be called once
- expect(mockGetCustomUnits).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('Accessibility and Props', () => {
- it('should support custom placeholder', () => {
- render();
-
- expect(screen.getByText('Choose measurement unit')).toBeInTheDocument();
- });
-
- it('should support disabled state', () => {
- render();
-
- expect(screen.getByRole('combobox')).toBeDisabled();
- });
-
- it('should support search functionality', () => {
- render();
-
- expect(screen.getByRole('combobox')).toHaveAttribute('type', 'search');
- });
-
- it('should disable search when showSearch is false', () => {
- render();
-
- const select = screen.getByTestId('unit-of-measurement-select');
-
- expect(select).toBeInTheDocument();
- });
-
- it('should use custom testid', () => {
- render();
-
- expect(screen.getByTestId('my-custom-select')).toBeInTheDocument();
- });
- });
-});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomUnitSelect/CustomUnitSelect.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomUnitSelect/CustomUnitSelect.tsx
deleted file mode 100644
index 76a26bdd0f48..000000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomUnitSelect/CustomUnitSelect.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright 2025 Collate.
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { PlusOutlined } from '@ant-design/icons';
-import { Button, Divider } from '@openmetadata/ui-core-components';
-import { Input, Select } from 'antd';
-import { AxiosError } from 'axios';
-import { startCase } from 'lodash';
-import {
- FC,
- ReactNode,
- useCallback,
- useEffect,
- useMemo,
- useState,
-} from 'react';
-import { useTranslation } from 'react-i18next';
-import { UnitOfMeasurement } from '../../../generated/entity/data/metric';
-import { getCustomUnitsOfMeasurement } from '../../../rest/metricsAPI';
-import { showErrorToast } from '../../../utils/ToastUtils';
-
-interface CustomUnitSelectProps {
- value?: string;
- customValue?: string;
- onChange?: (
- unitOfMeasurement: string,
- customUnitOfMeasurement?: string
- ) => void;
- placeholder?: string;
- disabled?: boolean;
- showSearch?: boolean;
- dataTestId?: string;
-}
-
-const renderCustomUnitDropdown = (
- menu: ReactNode,
- {
- t,
- newCustomUnit,
- onNameChange,
- onAddCustomUnit,
- }: {
- t: ReturnType['t'];
- newCustomUnit: string;
- onNameChange: (e: React.ChangeEvent) => void;
- onAddCustomUnit: () => void;
- }
-) => (
- <>
- {menu}
-
-
- {
- e.stopPropagation();
- if (e.key === 'Enter') {
- onAddCustomUnit();
- }
- }}
- />
- }
- onClick={onAddCustomUnit}>
- {t('label.add')}
-
-
- >
-);
-
-const CustomUnitSelect: FC = ({
- value,
- customValue,
- onChange,
- placeholder,
- disabled = false,
- showSearch = true,
- dataTestId = 'unit-of-measurement-select',
-}) => {
- const { t } = useTranslation();
- const [customUnits, setCustomUnits] = useState([]);
- const [userAddedUnits, setUserAddedUnits] = useState([]);
- const [newCustomUnit, setNewCustomUnit] = useState('');
- const [loading, setLoading] = useState(false);
-
- // Memoized computed values
- const { allCustomUnits, allOptions } = useMemo(() => {
- // Combine custom units with deduplication
- const combinedCustomUnits = [
- ...new Set([...customUnits, ...userAddedUnits]),
- ];
-
- // Standard unit options
- const standardOptions = Object.values(UnitOfMeasurement)
- .filter((unit) => unit !== UnitOfMeasurement.Other)
- .map((unit) => ({
- label: startCase(unit.toLowerCase()),
- value: unit,
- }));
-
- // Custom options from combined units
- const customOptions = combinedCustomUnits.map((unit) => ({
- label: unit,
- value: unit,
- }));
-
- // Combined options - standard units first, then custom units
- const combinedOptions = [...standardOptions, ...customOptions];
-
- return {
- allCustomUnits: combinedCustomUnits,
- allOptions: combinedOptions,
- };
- }, [customUnits, userAddedUnits]);
-
- const fetchCustomUnits = useCallback(async () => {
- try {
- setLoading(true);
- const units = await getCustomUnitsOfMeasurement();
- setCustomUnits(units || []);
- } catch (error) {
- showErrorToast(error as AxiosError);
- } finally {
- setLoading(false);
- }
- }, []);
-
- // Fetch custom units from backend on component mount
- useEffect(() => {
- fetchCustomUnits();
- }, []);
-
- // Initialize with current custom value if it exists
- useEffect(() => {
- if (customValue && !allCustomUnits.includes(customValue)) {
- setUserAddedUnits([customValue]);
- }
- }, [customValue, allCustomUnits]);
-
- const handleAddCustomUnit = () => {
- const trimmedUnit = newCustomUnit.trim();
- if (trimmedUnit) {
- // Check if it already exists in either list
- if (!allCustomUnits.includes(trimmedUnit)) {
- setUserAddedUnits([...userAddedUnits, trimmedUnit]);
- }
- // Select the newly added custom unit
- onChange?.(UnitOfMeasurement.Other, trimmedUnit);
- setNewCustomUnit('');
- }
- };
-
- const handleChange = (selectedValue: string) => {
- // Check if it's a custom unit (from backend or user-added)
- const isCustomUnit = allCustomUnits.includes(selectedValue);
-
- if (isCustomUnit) {
- // Custom unit selected - set unitOfMeasurement to OTHER
- onChange?.(UnitOfMeasurement.Other, selectedValue);
- } else {
- // Standard unit selected - clear customUnitOfMeasurement
- onChange?.(selectedValue, undefined);
- }
- };
-
- const handleNameChange = (e: React.ChangeEvent) => {
- setNewCustomUnit(e.target.value);
- };
-
- // Determine display value
- const displayValue = value === UnitOfMeasurement.Other ? customValue : value;
-
- return (
-