/** * Copyright (c) 2020-present, Goldman Sachs * * 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 { useRef, useState, useEffect, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { Dialog, type SelectComponent, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, clsx, CustomSelectorInput, BlankPanelPlaceholder, createFilter, PURE_EnumerationIcon, PencilIcon, TimesIcon, PlusIcon, PanelDropZone, Panel, PanelContent, ModalTitle, Modal, PanelHeaderActionItem, PanelHeaderActions, PanelHeader, } from '@finos/legend-art'; import { MappingEditorState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; import { TypeTree } from './TypeTree.js'; import { useDrop } from 'react-dnd'; import { type TransformDropTarget, type ElementDragSource, type MappingElementSourceDropTarget, CORE_DND_TYPE, TypeDragSource, } from '../../../../stores/editor/utils/DnDUtils.js'; import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js'; import { noop } from '@finos/legend-shared'; import { MappingElementDecorator, MappingElementDecorationCleaner, } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingElementDecorator.js'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { Type, Enum, Enumeration, type PackageableElement, type SourceValue, type EnumerationMapping, type PackageableElementReference, getEnumValueNames, PackageableElementExplicitReference, PrimitiveType, } from '@finos/legend-graph'; import { enumerationMapping_updateSourceType, enumValueMapping_addSourceValue, enumValueMapping_deleteSourceValue, enumValueMapping_updateSourceValue, } from '../../../../stores/graph-modifier/DSL_Mapping_GraphModifierHelper.js'; import { buildElementOption, getPackageableElementOptionFormatter, type PackageableElementOption, } from '@finos/legend-lego/graph-editor'; const EnumerationMappingSourceSelectorModal = observer( (props: { enumerationMapping: EnumerationMapping; open: boolean; closeModal: () => void; }) => { const { enumerationMapping, closeModal, open } = props; const editorStore = useEditorStore(); const applicationStore = editorStore.applicationStore; const options = [PrimitiveType.INTEGER, PrimitiveType.STRING] .map(buildElementOption) .concat( editorStore.graphManagerState.usableEnumerations.map( buildElementOption, ), ); const sourceSelectorRef = useRef(null); const filterOption = createFilter({ ignoreCase: true, ignoreAccents: false, stringify: (option: { data: PackageableElementOption; }): string => option.data.value.path, }); const sourceType = enumerationMapping.sourceType?.value; const selectedSourceType = sourceType ? { value: sourceType, label: sourceType.name } : null; const changeSourceType = ( val: PackageableElementOption | null, ): void => { const value = val?.value; if (!value || value instanceof Type) { enumerationMapping_updateSourceType( enumerationMapping, value ? PackageableElementExplicitReference.create(value) : undefined, ); } if (value) { closeModal(); } }; const handleEnter = (): void => sourceSelectorRef.current?.focus(); return ( ); }, ); export const SourceValueInput = observer( (props: { sourceValue: SourceValue; expectedType?: Type | undefined; updateSourceValue: (val: Enum | string | undefined) => void; deleteSourceValue: () => void; isReadOnly: boolean; }) => { const { sourceValue, updateSourceValue, deleteSourceValue, expectedType, isReadOnly, } = props; const value = sourceValue.value instanceof Enum ? sourceValue.value.name : (sourceValue.value ?? ''); const onChange: React.ChangeEventHandler = (event) => updateSourceValue(event.target.value); const onBlur: React.ChangeEventHandler = (event) => { const val = event.target.value; // Small validation if sourceType is enumeration // reset if not an enum value set to enum if source value not an enum if ( expectedType instanceof Enumeration && !getEnumValueNames(expectedType).includes(val) ) { updateSourceValue(undefined); } else if ( expectedType instanceof Enumeration && !(sourceValue instanceof Enum) && getEnumValueNames(expectedType).includes(val) ) { const enumValue = expectedType.values.find((e) => e.name === val); updateSourceValue(enumValue); } }; // Drag and Drop const handleDrop = useCallback( (item: TransformDropTarget): void => { if (!isReadOnly) { if (item instanceof TypeDragSource && item.data) { updateSourceValue(value + item.data.label); } } }, [isReadOnly, updateSourceValue, value], ); const [{ canDrop }, dropConnector] = useDrop< TypeDragSource, void, { canDrop: boolean } >( () => ({ accept: CORE_DND_TYPE.TYPE_TREE_ENUM, drop: (item) => handleDrop(item), collect: (monitor) => ({ canDrop: monitor.canDrop(), }), }), [handleDrop], ); const ref = useRef(null); dropConnector(ref); return (
{expectedType?.name ?? 'unknown'}
{!isReadOnly && ( )}
); }, ); const EnumValueMappingEditor = observer( (props: { enumValue: Enum; enumerationMapping: EnumerationMapping; sourceType: PackageableElementReference | undefined; isReadOnly: boolean; }) => { const { enumValue, enumerationMapping, sourceType, isReadOnly } = props; const matchingEnumValueMapping = enumerationMapping.enumValueMappings.find( (evm) => evm.enum.value === enumValue, ); const addSourceValue = (): void => matchingEnumValueMapping ? enumValueMapping_addSourceValue(matchingEnumValueMapping, undefined) : undefined; return (
{enumValue.name}
{matchingEnumValueMapping && ( )}
{matchingEnumValueMapping && (
{matchingEnumValueMapping.sourceValues.map((sourceValue, idx) => ( enumValueMapping_updateSourceValue( matchingEnumValueMapping, idx, val, sourceType?.value, ) } deleteSourceValue={(): void => enumValueMapping_deleteSourceValue( matchingEnumValueMapping, idx, ) } /> ))} {!matchingEnumValueMapping.sourceValues.length && (
No source value. Click{' '}
{' '} to add one.
)}
)}
); }, ); export const EnumerationMappingEditor = observer( (props: { enumerationMapping: EnumerationMapping; isReadOnly: boolean }) => { const { enumerationMapping, isReadOnly } = props; const editorStore = useEditorStore(); const mappingEditorState = editorStore.tabManagerState.getCurrentEditorState(MappingEditorState); const enumeration = enumerationMapping.enumeration; // ID const showId = Object.keys(mappingEditorState.mappingElementsWithSimilarTarget).length > 1; // Source const sourceType = enumerationMapping.sourceType?.value; const [openSourceSelectorModal, setOpenSourceSelectorModal] = useState(false); const showSourceSelectorModal = (): void => isReadOnly ? undefined : setOpenSourceSelectorModal(true); const hideSourceSelectorModal = (): void => setOpenSourceSelectorModal(false); const handleDrop = useCallback( (item: MappingElementSourceDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Type) { enumerationMapping_updateSourceType( enumerationMapping, PackageableElementExplicitReference.create( item.data.packageableElement, ), ); } }, [enumerationMapping, isReadOnly], ); const [{ isDragOver, canDrop }, dropConnector] = useDrop< ElementDragSource, void, { isDragOver: boolean; canDrop: boolean } >( () => ({ accept: CORE_DND_TYPE.PROJECT_EXPLORER_ENUMERATION, drop: (item) => handleDrop(item), collect: (monitor) => ({ isDragOver: monitor.isOver({ shallow: true }), canDrop: monitor.canDrop(), }), }), [handleDrop], ); useEffect(() => { if (!isReadOnly) { new MappingElementDecorator(editorStore).visitEnumerationMapping( enumerationMapping, ); } return isReadOnly ? noop() : (): void => new MappingElementDecorationCleaner( editorStore, ).visitEnumerationMapping(enumerationMapping); }, [enumerationMapping, isReadOnly, editorStore]); return (
{/* Target */}
enumeration mapping
{showId && (
{enumerationMapping.id.isDefault ? 'default ID' : enumerationMapping.id.value}
)}
for
{enumeration.value.name}
{/* Driver */} {sourceType && (
using
{sourceType.name}
)} {!sourceType && (
with no source
)}
ENUMS
{enumeration.value.values.map((enumValue) => ( ))}
source
{sourceType?.name ?? '(none)'}
{sourceType && (
{sourceType instanceof Enumeration && ( )} {/* TODO?: do we need to show anything when the source type is string or integer */}
)} {!sourceType && ( )}
); }, );