/** * 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 { useState, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { UMLEditorState, UML_EDITOR_TAB, } from '../../../../stores/editor/editor-state/element-editor-state/UMLEditorState.js'; import { CORE_DND_TYPE, type UMLEditorElementDropTarget, type ElementDragSource, } from '../../../../stores/editor/utils/DnDUtils.js'; import { useDrop } from 'react-dnd'; import { clsx, CustomSelectorInput, createFilter, ResizablePanel, ResizablePanelSplitter, ResizablePanelGroup, ResizablePanelSplitterLine, BlankPanelContent, getCollapsiblePanelGroupProps, InputWithInlineValidation, LockIcon, PlusIcon, ArrowCircleRightIcon, LongArrowRightIcon, PanelDropZone, Panel, InfoCircleIcon, PanelHeader, PanelContentLists, } from '@finos/legend-art'; import { getElementIcon } from '../../../ElementIconUtils.js'; import { prettyCONSTName, guaranteeType } from '@finos/legend-shared'; import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js'; import { StereotypeDragPreviewLayer, StereotypeSelector, } from './StereotypeSelector.js'; import { TaggedValueDragPreviewLayer, TaggedValueEditor, } from './TaggedValueEditor.js'; import { PropertyEditor } from './PropertyEditor.js'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { type Association, type Property, type StereotypeReference, type TaggedValue, MULTIPLICITY_INFINITE, Profile, Class, PrimitiveType, Unit, StereotypeExplicitReference, stub_Profile, stub_TaggedValue, stub_Tag, stub_Stereotype, getFirstAssociatedProperty, getSecondAssociatedProperty, } from '@finos/legend-graph'; import { property_setName, property_setMultiplicity, annotatedElement_deleteStereotype, annotatedElement_addTaggedValue, annotatedElement_addStereotype, annotatedElement_deleteTaggedValue, association_changePropertyType, } from '../../../../stores/graph-modifier/DomainGraphModifierHelper.js'; import { CLASS_PROPERTY_TYPE, getClassPropertyType, } from '../../../../stores/editor/utils/ModelClassifierUtils.js'; import { useApplicationNavigationContext } from '@finos/legend-application'; import { buildElementOption, type PackageableElementOption, } from '@finos/legend-lego/graph-editor'; import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../../__lib__/LegendStudioApplicationNavigationContext.js'; import { LEGEND_STUDIO_DOCUMENTATION_KEY } from '../../../../__lib__/LegendStudioDocumentation.js'; const AssociationPropertyBasicEditor = observer( (props: { association: Association; property: Property; selectProperty: () => void; isReadOnly: boolean; }) => { const { association, property, selectProperty, isReadOnly } = props; const editorStore = useEditorStore(); const isPropertyDuplicated = (val: Property): boolean => { if ( association.properties[0].name === val.name && association.properties[1].name === val.name ) { return true; } return false; }; const isPropertyInvalid = (): boolean => { const sytemAssociation = editorStore.graphManagerState.graph.systemModel.getOwnNullableAssociation( association.path, ); if ( !sytemAssociation && editorStore.graphManagerState.graph.systemModel.getOwnNullableClass( property.genericType.value.rawType.path, ) ) { return true; } return false; }; // Name const changeValue: React.ChangeEventHandler = (event) => { property_setName(property, event.target.value); }; // Generic Type const [isEditingType, setIsEditingType] = useState(false); // TODO: make this so that association can only refer to classes from the same graph space const propertyTypeOptions = editorStore.graphManagerState.usableAssociationPropertyClasses.map( buildElementOption, ); const propertyType = property.genericType.value.rawType; const propertyTypeName = getClassPropertyType(propertyType); const filterOption = createFilter({ ignoreCase: true, ignoreAccents: false, stringify: (option: { data: PackageableElementOption }): string => option.data.value.path, }); const selectedPropertyType = { value: propertyType, label: propertyType.name, } as PackageableElementOption; const changePropertyType = (val: PackageableElementOption): void => { association_changePropertyType( association, property, guaranteeType( val.value, Class, `Association property type can only be 'class'`, ), ); setIsEditingType(false); }; // Multiplicity const [lowerBound, setLowerBound] = useState( property.multiplicity.lowerBound, ); const [upperBound, setUpperBound] = useState( property.multiplicity.upperBound ?? MULTIPLICITY_INFINITE, ); const updateMultiplicity = ( lower: number | string, upper: number | string, ): void => { const lBound = typeof lower === 'number' ? lower : parseInt(lower, 10); const uBound = upper === MULTIPLICITY_INFINITE ? undefined : typeof upper === 'number' ? upper : parseInt(upper, 10); if (!isNaN(lBound) && (uBound === undefined || !isNaN(uBound))) { property_setMultiplicity( property, editorStore.graphManagerState.graph.getMultiplicity(lBound, uBound), ); } }; const changeLowerBound: React.ChangeEventHandler = ( event, ) => { setLowerBound(event.target.value); updateMultiplicity(event.target.value, upperBound); }; const changeUpperBound: React.ChangeEventHandler = ( event, ) => { setUpperBound(event.target.value); updateMultiplicity(lowerBound, event.target.value); }; // Other const openElement = (): void => { if (!(propertyType instanceof PrimitiveType)) { editorStore.graphEditorMode.openElement( propertyType instanceof Unit ? propertyType.measure : propertyType, ); } }; const seeErrorDocumentation = (): void => editorStore.applicationStore.assistantService.openDocumentationEntry( LEGEND_STUDIO_DOCUMENTATION_KEY.QUESTION_WHY_DO_I_SEE_ERROR_WITH_ASSOCIATION_PROPERTY_TYPE, ); return (
{!isReadOnly && isEditingType && ( )} {!isReadOnly && !isEditingType && (
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && (
{getElementIcon(propertyType, editorStore)}
)}
{propertyType.name}
setIsEditingType(true)} > Click to edit
{isPropertyInvalid() && ( )} {propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && ( )}
)} {isReadOnly && (
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && (
{getElementIcon(propertyType, editorStore)}
)}
{propertyType.name}
{isPropertyInvalid() && ( )} {propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && ( )}
)}
..
); }, ); export const AssociationEditor = observer( (props: { association: Association }) => { const { association } = props; const editorStore = useEditorStore(); const editorState = editorStore.tabManagerState.getCurrentEditorState(UMLEditorState); const isReadOnly = editorState.isReadOnly; // Selected property const [selectedProperty, setSelectedProperty] = useState< Property | undefined >(); const selectProperty = (e: Property): (() => void) => (): void => setSelectedProperty(e); // Tab const selectedTab = editorState.selectedTab; const tabs = [ UML_EDITOR_TAB.PROPERTIES, UML_EDITOR_TAB.TAGGED_VALUES, UML_EDITOR_TAB.STEREOTYPES, ]; const changeTab = (tab: UML_EDITOR_TAB): (() => void) => (): void => { editorState.setSelectedTab(tab); setSelectedProperty(undefined); }; let addButtonTitle = ''; switch (selectedTab) { case UML_EDITOR_TAB.TAGGED_VALUES: addButtonTitle = 'Add tagged value'; break; case UML_EDITOR_TAB.STEREOTYPES: addButtonTitle = 'Add stereotype'; break; default: break; } const add = (): void => { if (!isReadOnly) { if (selectedTab === UML_EDITOR_TAB.TAGGED_VALUES) { annotatedElement_addTaggedValue( association, stub_TaggedValue(stub_Tag(stub_Profile())), ); } else if (selectedTab === UML_EDITOR_TAB.STEREOTYPES) { annotatedElement_addStereotype( association, StereotypeExplicitReference.create(stub_Stereotype(stub_Profile())), ); } } }; // Tagged value and Stereotype const _deleteStereotype = (val: StereotypeReference): (() => void) => (): void => annotatedElement_deleteStereotype(association, val); const _deleteTaggedValue = (val: TaggedValue): (() => void) => (): void => annotatedElement_deleteTaggedValue(association, val); // Property const deselectProperty = (): void => setSelectedProperty(undefined); // Drag and Drop const handleDropTaggedValue = useCallback( (item: UMLEditorElementDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Profile) { annotatedElement_addTaggedValue( association, stub_TaggedValue(stub_Tag(item.data.packageableElement)), ); } }, [association, isReadOnly], ); const [{ isTaggedValueDragOver }, taggedValueDropConnector] = useDrop< ElementDragSource, void, { isTaggedValueDragOver: boolean } >( () => ({ accept: [CORE_DND_TYPE.PROJECT_EXPLORER_PROFILE], drop: (item) => handleDropTaggedValue(item), collect: (monitor) => ({ isTaggedValueDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropTaggedValue], ); const handleDropStereotype = useCallback( (item: UMLEditorElementDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Profile) { annotatedElement_addStereotype( association, StereotypeExplicitReference.create( stub_Stereotype(item.data.packageableElement), ), ); } }, [association, isReadOnly], ); const [{ isStereotypeDragOver }, stereotypeDropConnector] = useDrop< ElementDragSource, void, { isStereotypeDragOver: boolean } >( () => ({ accept: [CORE_DND_TYPE.PROJECT_EXPLORER_PROFILE], drop: (item) => handleDropStereotype(item), collect: (monitor) => ({ isStereotypeDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropStereotype], ); useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.ASSOCIATION_EDITOR, ); // layout const propertyEditorCollapsiblePanelGroupProps = getCollapsiblePanelGroupProps(!selectedProperty, { size: 250, }); return (
{isReadOnly && (
)}
association
{association.name}
{tabs.map((tab) => (
{prettyCONSTName(tab)}
))}
{selectedTab === UML_EDITOR_TAB.PROPERTIES && ( )} {selectedTab === UML_EDITOR_TAB.TAGGED_VALUES && ( {association.taggedValues.map((taggedValue) => ( ))} )} {selectedTab === UML_EDITOR_TAB.STEREOTYPES && ( {association.stereotypes.map((stereotype) => ( ))} )}
{selectedProperty ? ( ) : (
No property selected
)}
); }, );