diff --git a/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/StructuredPropertyInput.tsx b/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/StructuredPropertyInput.tsx index 81fb79d6ba..1e3c6dbea0 100644 --- a/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/StructuredPropertyInput.tsx +++ b/datahub-web-react/src/app/entity/shared/components/styled/StructuredProperty/StructuredPropertyInput.tsx @@ -1,12 +1,13 @@ -import React from 'react'; import { PropertyCardinality, StdDataType, StructuredPropertyEntity } from '@src/types.generated'; -import SingleSelectInput from './SingleSelectInput'; -import MultiSelectInput from './MultiSelectInput'; -import StringInput from './StringInput'; -import RichTextInput from './RichTextInput'; -import DateInput from './DateInput'; -import NumberInput from './NumberInput'; +import React from 'react'; +import StructuredPropertySearchSelectUrnInput from '../../../entityForm/prompts/StructuredPropertyPrompt/UrnInput/StructuredPropertySearchSelectUrnInput'; import UrnInput from '../../../entityForm/prompts/StructuredPropertyPrompt/UrnInput/UrnInput'; +import DateInput from './DateInput'; +import MultiSelectInput from './MultiSelectInput'; +import NumberInput from './NumberInput'; +import RichTextInput from './RichTextInput'; +import SingleSelectInput from './SingleSelectInput'; +import StringInput from './StringInput'; interface Props { structuredProperty: StructuredPropertyEntity; @@ -14,6 +15,7 @@ interface Props { selectSingleValue: (value: string | number) => void; toggleSelectedValue: (value: string | number) => void; updateSelectedValues: (value: (string | number | null)[]) => void; + canUseSearchSelectUrnInput?: boolean; } export default function StructuredPropertyInput({ @@ -22,6 +24,7 @@ export default function StructuredPropertyInput({ selectedValues, toggleSelectedValue, updateSelectedValues, + canUseSearchSelectUrnInput = false, }: Props) { const { allowedValues, cardinality, valueType } = structuredProperty.definition; @@ -66,10 +69,17 @@ export default function StructuredPropertyInput({ updateSelectedValues={updateSelectedValues} /> )} - {!allowedValues && valueType.info.type === StdDataType.Urn && ( + {!allowedValues && valueType.info.type === StdDataType.Urn && canUseSearchSelectUrnInput && ( + + )} + {!allowedValues && valueType.info.type === StdDataType.Urn && !canUseSearchSelectUrnInput && ( )} diff --git a/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/StructuredPropertySearchSelectUrnInput.tsx b/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/StructuredPropertySearchSelectUrnInput.tsx new file mode 100644 index 0000000000..6cda6de703 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/StructuredPropertySearchSelectUrnInput.tsx @@ -0,0 +1,36 @@ +import { SearchSelectUrnInput } from '@src/app/entityV2/shared/components/styled/search/SearchSelectUrnInput'; +import React, { useMemo } from 'react'; +import { EntityType, PropertyCardinality, StructuredPropertyEntity } from '../../../../../../../types.generated'; + +interface StructuredPropertySearchSelectUrnInputProps { + structuredProperty: StructuredPropertyEntity; + selectedValues: string[]; + updateSelectedValues: (values: string[] | number[]) => void; +} + +// Wrapper component that extracts information from StructuredProperty +export default function StructuredPropertySearchSelectUrnInput({ + structuredProperty, + selectedValues, + updateSelectedValues, +}: StructuredPropertySearchSelectUrnInputProps) { + // Get the allowed entity types from the structured property + const allowedEntityTypes = useMemo(() => { + return ( + structuredProperty.definition.typeQualifier?.allowedTypes?.map( + (allowedType) => allowedType.info.type as EntityType, + ) || [] + ); + }, [structuredProperty]); + + const isMultiple = structuredProperty.definition.cardinality === PropertyCardinality.Multiple; + + return ( + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/UrnInput.tsx b/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/UrnInput.tsx index 54d53c7560..62ccc9ee2e 100644 --- a/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/UrnInput.tsx +++ b/datahub-web-react/src/app/entity/shared/entityForm/prompts/StructuredPropertyPrompt/UrnInput/UrnInput.tsx @@ -3,8 +3,8 @@ import { Select } from 'antd'; import React from 'react'; import styled from 'styled-components'; import { StructuredPropertyEntity } from '../../../../../../../types.generated'; -import useUrnInput from './useUrnInput'; import SelectedEntity from './SelectedEntity'; +import useUrnInput from './useUrnInput'; const EntitySelect = styled(Select)` width: 75%; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx index a3ab1bc63a..c8eb8eb095 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx @@ -1,11 +1,16 @@ +import { Button } from '@src/alchemy-components'; import analytics, { EventType } from '@src/app/analytics'; +import { ModalButtonContainer } from '@src/app/shared/button/styledComponents'; import { Modal, message } from 'antd'; import React, { useEffect, useMemo } from 'react'; import styled from 'styled-components'; -import { Button } from '@src/alchemy-components'; -import { ModalButtonContainer } from '@src/app/shared/button/styledComponents'; import { useUpsertStructuredPropertiesMutation } from '../../../../../../graphql/structuredProperties.generated'; -import { EntityType, PropertyValueInput, StructuredPropertyEntity } from '../../../../../../types.generated'; +import { + EntityType, + PropertyValueInput, + StdDataType, + StructuredPropertyEntity, +} from '../../../../../../types.generated'; import handleGraphQLError from '../../../../../shared/handleGraphQLError'; import StructuredPropertyInput from '../../../components/styled/StructuredProperty/StructuredPropertyInput'; import { useEditStructuredProperty } from '../../../components/styled/StructuredProperty/useEditStructuredProperty'; @@ -27,6 +32,9 @@ interface Props { isAddMode?: boolean; } +const SEARCH_SELECT_MODAL_WIDTH = 1400; +const DEFAULT_MODAL_WIDTH = 650; + export default function EditStructuredPropertyModal({ isOpen, structuredProperty, @@ -44,6 +52,7 @@ export default function EditStructuredPropertyModal({ const { selectedValues, selectSingleValue, toggleSelectedValue, updateSelectedValues, setSelectedValues } = useEditStructuredProperty(initialValues); const [upsertStructuredProperties] = useUpsertStructuredPropertiesMutation(); + const { allowedValues } = structuredProperty.definition; useEffect(() => { setSelectedValues(initialValues); @@ -99,12 +108,15 @@ export default function EditStructuredPropertyModal({ }); } + const isUrnInput = structuredProperty.definition.valueType.info.type === StdDataType.Urn && !allowedValues; + return ( + ), + Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, + Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( + + ), + // No need to mock colors and typography anymore, we use the actual ones + }; +}); + +describe('SearchSelectUrnInput', () => { + const mockUpdateSelectedValues = vi.fn(); + + beforeEach(() => { + mockUpdateSelectedValues.mockClear(); + mockSetSelectedEntities.mockClear(); + }); + + it('renders correctly with no initial selection', () => { + render( + + + + + , + ); + + expect(screen.getByText('Search Values')).toBeInTheDocument(); + expect(screen.getByTestId('mock-search-select')).toBeInTheDocument(); + expect(screen.getByText('Selected Values')).toBeInTheDocument(); + expect(screen.getByText('No values selected')).toBeInTheDocument(); + }); + + it('renders with initial selected values', () => { + const initialUrns = ['urn:li:dataset:1', 'urn:li:dataset:2']; + render( + + + + + , + ); + + expect(screen.getByText('Selected Values')).toBeInTheDocument(); + expect(screen.getByTestId('entity-link-urn:li:dataset:1')).toHaveTextContent('Entity 1'); + expect(screen.getByTestId('entity-link-urn:li:dataset:2')).toHaveTextContent('Entity 2'); + expect(screen.queryByText('No values selected')).not.toBeInTheDocument(); + }); + + it('calls updateSelectedValues when a value is removed', () => { + const initialUrns = ['urn:li:dataset:1', 'urn:li:dataset:2']; + render( + + + + + , + ); + + // Find the remove button for the first entity + const removeButton = screen.getAllByTestId('mock-icon-x')[0]; // Assuming order matches initialUrns + fireEvent.click(removeButton); + + // Check if updateSelectedValues was called with the remaining URN + // It gets called twice: once on initial load (useEffect) and once on removal + expect(mockUpdateSelectedValues).toHaveBeenCalledTimes(2); + expect(mockUpdateSelectedValues).toHaveBeenLastCalledWith(['urn:li:dataset:2']); + + // Check if the removed entity is no longer rendered + expect(screen.queryByTestId('entity-link-urn:li:dataset:1')).not.toBeInTheDocument(); + expect(screen.getByTestId('entity-link-urn:li:dataset:2')).toBeInTheDocument(); + }); + + it('calls updateSelectedValues when SearchSelect updates selection', () => { + const initialUrns: string[] = []; + const newEntity: EntityAndType = { urn: 'urn:li:dataset:3', type: EntityType.Dataset }; + render( + + + + + , + ); + + // Ensure the SearchSelect mock rendered and called our spy with the callback + expect(mockSetSelectedEntities).toHaveBeenCalled(); + + // Get the actual callback function from the spy's arguments + const capturedSetSelectedEntities = mockSetSelectedEntities.mock.calls[0][0]; + expect(capturedSetSelectedEntities).toBeInstanceOf(Function); // Verify it's a function + + // Simulate SearchSelect calling its setSelectedEntities prop within act + act(() => { + capturedSetSelectedEntities([newEntity]); + }); + + // Check if updateSelectedValues was called with the new URN + // It gets called once on initial load (useEffect) and once when the selection changes + expect(mockUpdateSelectedValues).toHaveBeenCalledTimes(2); + expect(mockUpdateSelectedValues).toHaveBeenLastCalledWith([newEntity.urn]); + + // Check if the new entity is rendered + expect(screen.getByTestId('entity-link-urn:li:dataset:3')).toHaveTextContent('Entity 3'); + }); +});