/** * 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, useEffect, useCallback, useRef } from 'react'; import { observer } from 'mobx-react-lite'; import { isNonNullable, prettyCONSTName } from '@finos/legend-shared'; import { CORE_DND_TYPE, type ElementDragSource, type UMLEditorElementDropTarget, } from '../../../../stores/editor/utils/DnDUtils.js'; import { clsx, CustomSelectorInput, createFilter, ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, ResizablePanelSplitterLine, BlankPanelContent, getCollapsiblePanelGroupProps, InputWithInlineValidation, LockIcon, PlusIcon, TimesIcon, LongArrowRightIcon, ArrowCircleRightIcon, FireIcon, StickArrowCircleRightIcon, PanelEntryDragHandle, DragPreviewLayer, useDragPreviewLayer, PanelDropZone, Panel, PanelDnDEntry, PanelContentLists, InfoCircleIcon, } from '@finos/legend-art'; import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js'; import { PropertyEditor } from './PropertyEditor.js'; import { StereotypeSelector, StereotypeDragPreviewLayer, } from './StereotypeSelector.js'; import { TaggedValueEditor, TaggedValueDragPreviewLayer, } from './TaggedValueEditor.js'; import { UML_EDITOR_TAB } from '../../../../stores/editor/editor-state/element-editor-state/UMLEditorState.js'; import { ClassEditorState } from '../../../../stores/editor/editor-state/element-editor-state/ClassEditorState.js'; import { flowResult } from 'mobx'; import { type DropTargetMonitor, useDrop, useDrag } from 'react-dnd'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { type StereotypeReference, type GenericTypeReference, type TaggedValue, type Constraint, type Property, type DerivedProperty, MULTIPLICITY_INFINITE, Class, GenericType, Profile, Type, PrimitiveType, Unit, StereotypeExplicitReference, GenericTypeExplicitReference, Association, stub_TaggedValue, stub_Tag, stub_Profile, stub_Stereotype, stub_Constraint, stub_Property, stub_DerivedProperty, getAllClassProperties, getAllSuperclasses, getAllClassConstraints, getAllClassDerivedProperties, isElementReadOnly, getMultiplicityPrettyDescription, } from '@finos/legend-graph'; import { ApplicationNavigationContextData, useApplicationNavigationContext, useApplicationStore, } from '@finos/legend-application'; import { buildElementOption, getPackageableElementOptionFormatter, type PackageableElementOption, } from '@finos/legend-lego/graph-editor'; import { getElementIcon } from '../../../ElementIconUtils.js'; import type { ClassPreviewRenderer } from '../../../../stores/LegendStudioApplicationPlugin.js'; import { class_addProperty, class_deleteDerivedProperty, class_addDerivedProperty, class_addContraint, class_addSuperType, annotatedElement_addTaggedValue, annotatedElement_addStereotype, annotatedElement_deleteStereotype, annotatedElement_deleteTaggedValue, class_deleteConstraint, class_deleteSuperType, class_deleteProperty, class_deleteSubclass, class_addSubclass, constraint_setName, property_setName, property_setGenericType, property_setMultiplicity, class_swapProperties, class_swapDerivedProperties, class_swapConstraints, class_swapSuperTypes, setGenericTypeReferenceValue, } from '../../../../stores/graph-modifier/DomainGraphModifierHelper.js'; import { CLASS_PROPERTY_TYPE, getClassPropertyType, } from '../../../../stores/editor/utils/ModelClassifierUtils.js'; import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../../__lib__/LegendStudioApplicationNavigationContext.js'; import { InlineLambdaEditor } from '@finos/legend-query-builder'; type ClassPropertyDragSource = { property: Property; }; const CLASS_PROPERTY_DND_TYPE = 'CLASS_PROPERTY'; const PropertyBasicEditor = observer( (props: { _class: Class; editorState: ClassEditorState; property: Property; deleteProperty: () => void; isReadOnly: boolean; }) => { const ref = useRef(null); const handleRef = useRef(null); const { property, _class, editorState, deleteProperty, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const isInheritedProperty = property._OWNER instanceof Class && property._OWNER !== _class; const isPropertyFromAssociation = property._OWNER instanceof Association; const isIndirectProperty = isInheritedProperty || isPropertyFromAssociation; const isPropertyDuplicated = (val: Property): boolean => _class.properties.filter((p) => p.name === val.name).length >= 2; const selectProperty = (): void => editorState.setSelectedProperty(property); // Name const changeValue: React.ChangeEventHandler = (event) => { property_setName(property, event.target.value); }; // Generic Type const [isEditingType, setIsEditingType] = useState(false); const propertyTypeOptions = editorStore.graphManagerState.usableClassPropertyTypes.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, }; const changePropertyType = (val: PackageableElementOption): void => { property_setGenericType(property, new GenericType(val.value)); 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); }; // Drag and Drop const handleHover = useCallback( (item: ClassPropertyDragSource): void => { const draggingProperty = item.property; const hoveredProperty = property; class_swapProperties(_class, draggingProperty, hoveredProperty); }, [_class, property], ); const [{ isBeingDraggedProperty }, dropConnector] = useDrop< ClassPropertyDragSource, void, { isBeingDraggedProperty: Property | undefined } >( () => ({ accept: [CLASS_PROPERTY_DND_TYPE], hover: (item) => handleHover(item), collect: ( monitor, ): { isBeingDraggedProperty: Property | undefined; } => ({ isBeingDraggedProperty: monitor.getItem()?.property, }), }), [handleHover], ); const isBeingDragged = property === isBeingDraggedProperty; const [, dragConnector, dragPreviewConnector] = useDrag( () => ({ type: CLASS_PROPERTY_DND_TYPE, item: () => ({ property: property, }), }), [property], ); dragConnector(handleRef); dropConnector(ref); useDragPreviewLayer(dragPreviewConnector); // Other const openElement = (): void => { if (!(propertyType instanceof PrimitiveType)) { editorStore.graphEditorMode.openElement( propertyType instanceof Unit ? propertyType.measure : propertyType, ); } }; // NOTE: for now we do not allow directly modifying inherited and associated properties, // we would make the user go to the supertype or the association where the property comes from const visitOwner = (): void => editorStore.graphEditorMode.openElement(property._OWNER); return ( } showPlaceholder={isBeingDragged} className="property-basic-editor__container" > {!isIndirectProperty && ( )}
{isIndirectProperty && (
{property.name}
)} {!isIndirectProperty && (
)} {!isIndirectProperty && !isReadOnly && isEditingType && ( )} {!isIndirectProperty && !isReadOnly && !isEditingType && (
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && (
{getElementIcon(propertyType, editorStore)}
)}
{propertyType.name}
setIsEditingType(true)} > Click to edit
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && ( )}
)} {(isIndirectProperty || isReadOnly) && (
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && (
{getElementIcon(propertyType, editorStore)}
)}
{propertyType.name}
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && ( )}
)}
..
{!isIndirectProperty && ( )} {isIndirectProperty && ( )} {isIndirectProperty && (
)} {!isIndirectProperty && !isReadOnly && ( )}
); }, ); type ClassDerivedPropertyDragSource = { derivedProperty: DerivedProperty; }; const CLASS_DERIVED_PROPERTY_DND_TYPE = 'CLASS_DERIVED_PROPERTY'; const DerivedPropertyBasicEditor = observer( (props: { _class: Class; editorState: ClassEditorState; derivedProperty: DerivedProperty; deleteDerivedProperty: () => void; isReadOnly: boolean; }) => { const ref = useRef(null); const handleRef = useRef(null); const { derivedProperty, _class, deleteDerivedProperty, editorState, isReadOnly, } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const hasParserError = editorState.classState.derivedPropertyStates.some( (state) => state.parserError, ); const dpState = editorState.classState.getDerivedPropertyState(derivedProperty); const isInheritedProperty = derivedProperty._OWNER !== _class; const selectDerivedProperty = (): void => editorState.setSelectedProperty(derivedProperty); // Name const changeValue: React.ChangeEventHandler = (event) => property_setName(derivedProperty, event.target.value); // Generic Type const [isEditingType, setIsEditingType] = useState(false); const propertyTypeOptions = editorStore.graphManagerState.usableClassPropertyTypes.map( buildElementOption, ); const propertyType = derivedProperty.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, }; const changePropertyType = (val: PackageableElementOption): void => { property_setGenericType(derivedProperty, new GenericType(val.value)); setIsEditingType(false); }; // Multiplicity const [lowerBound, setLowerBound] = useState( derivedProperty.multiplicity.lowerBound, ); const [upperBound, setUpperBound] = useState( derivedProperty.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( derivedProperty, 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); }; // Drag and Drop const handleHover = useCallback( ( item: ClassDerivedPropertyDragSource, monitor: DropTargetMonitor, ): void => { const draggingProperty = item.derivedProperty; const hoveredProperty = derivedProperty; const dragIndex = _class.derivedProperties.findIndex( (e) => e === item.derivedProperty, ); const hoverIndex = _class.derivedProperties.findIndex( (e) => e === derivedProperty, ); // move the item being hovered on when the dragged item position is beyond the its middle point const hoverBoundingReact = ref.current?.getBoundingClientRect(); const distanceThreshold = ((hoverBoundingReact?.bottom ?? 0) - (hoverBoundingReact?.top ?? 0)) / 2; const dragDistance = (monitor.getClientOffset()?.y ?? 0) - (hoverBoundingReact?.top ?? 0); if (dragIndex < hoverIndex && dragDistance < distanceThreshold) { return; } class_swapDerivedProperties(_class, draggingProperty, hoveredProperty); }, [_class, derivedProperty], ); const [{ isBeingDraggedDerivedProperty }, dropConnector] = useDrop< ClassDerivedPropertyDragSource, void, { isBeingDraggedDerivedProperty: DerivedProperty | undefined } >( () => ({ accept: [CLASS_DERIVED_PROPERTY_DND_TYPE], hover: (item, monitor): void => handleHover(item, monitor), collect: (monitor) => ({ isBeingDraggedDerivedProperty: monitor.getItem() ?.derivedProperty, }), }), [handleHover], ); const isBeingDragged = derivedProperty === isBeingDraggedDerivedProperty; const [, dragConnector, dragPreviewConnector] = useDrag( () => ({ type: CLASS_DERIVED_PROPERTY_DND_TYPE, item: () => ({ derivedProperty: derivedProperty, }), }), [derivedProperty], ); dragConnector(handleRef); dropConnector(ref); useDragPreviewLayer(dragPreviewConnector); // Action const onLambdaEditorFocus = (): void => applicationStore.navigationContextService.push( ApplicationNavigationContextData.createTransient( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_DERIVED_PROPERTY_LAMBDA_EDITOR, ), ); const openElement = (): void => { if (!(propertyType instanceof PrimitiveType)) { editorStore.graphEditorMode.openElement( propertyType instanceof Unit ? propertyType.measure : propertyType, ); } }; const visitOwner = (): void => editorStore.graphEditorMode.openElement(derivedProperty._OWNER); const remove = applicationStore.guardUnhandledError(async () => { await flowResult( dpState.convertLambdaObjectToGrammarString({ pretty: false }), ); deleteDerivedProperty(); }); return ( } className="derived-property-editor__container" showPlaceholder={isBeingDragged} >
{!isInheritedProperty && ( )} {isInheritedProperty && (
{derivedProperty.name}
)} {!isInheritedProperty && ( )} {!isInheritedProperty && !isReadOnly && isEditingType && ( )} {!isInheritedProperty && !isReadOnly && !isEditingType && (
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && (
{getElementIcon(propertyType, editorStore)}
)}
{propertyType.name}
setIsEditingType(true)} > Click to edit
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && ( )}
)} {(isInheritedProperty || isReadOnly) && (
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && (
{getElementIcon(propertyType, editorStore)}
)}
{propertyType.name}
{propertyTypeName !== CLASS_PROPERTY_TYPE.PRIMITIVE && ( )}
)}
..
{!isInheritedProperty && ( )} {isInheritedProperty && ( )} {!isInheritedProperty && !isReadOnly && ( )}
); }, ); type ClassConstraintDragSource = { constraint: Constraint; }; const CLASS_CONSTRAINT_DND_TYPE = 'CLASS_CONSTRAINT'; const ConstraintEditor = observer( (props: { editorState: ClassEditorState; _class: Class; constraint: Constraint; deleteConstraint: () => void; isReadOnly: boolean; }) => { const ref = useRef(null); const handleRef = useRef(null); const { constraint, _class, deleteConstraint, editorState, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const hasParserError = editorState.classState.constraintStates.some( (state) => state.parserError, ); const isInheritedConstraint = constraint._OWNER !== _class; const constraintState = editorState.classState.getConstraintState(constraint); // Name const changeName: React.ChangeEventHandler = (event) => constraint_setName(constraint, event.target.value); // Drag and Drop const handleHover = useCallback( (item: ClassConstraintDragSource): void => { const draggingProperty = item.constraint; const hoveredProperty = constraint; class_swapConstraints(_class, draggingProperty, hoveredProperty); }, [_class, constraint], ); const [{ isBeingDraggedConstraint }, dropConnector] = useDrop< ClassConstraintDragSource, void, { isBeingDraggedConstraint: Constraint | undefined } >( () => ({ accept: [CLASS_CONSTRAINT_DND_TYPE], hover: (item) => handleHover(item), collect: ( monitor, ): { isBeingDraggedConstraint: Constraint | undefined } => ({ isBeingDraggedConstraint: monitor.getItem()?.constraint, }), }), [handleHover], ); const isBeingDragged = constraint === isBeingDraggedConstraint; const [, dragConnector, dragPreviewConnector] = useDrag( () => ({ type: CLASS_CONSTRAINT_DND_TYPE, item: () => ({ constraint: constraint, }), }), [constraint], ); dragConnector(handleRef); dropConnector(ref); useDragPreviewLayer(dragPreviewConnector); // Actions const onLambdaEditorFocus = (): void => applicationStore.navigationContextService.push( ApplicationNavigationContextData.createTransient( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_CONTRAINT_LAMBDA_EDITOR, ), ); const remove = applicationStore.guardUnhandledError(async () => { await flowResult( constraintState.convertLambdaObjectToGrammarString({ pretty: false }), ); deleteConstraint(); }); const visitOwner = (): void => editorStore.graphEditorMode.openElement(constraint._OWNER); return ( } className="constraint-editor__container" showPlaceholder={isBeingDragged} >
{!isInheritedConstraint && ( )} {isInheritedConstraint && (
{constraint.name}
)} {!isInheritedConstraint && ( )} {isInheritedConstraint && ( )} {!isInheritedConstraint && !isReadOnly && ( )}
); }, ); type ClassSuperTypeDragSource = { superType: GenericTypeReference; }; const CLASS_SUPER_TYPE_DND_TYPE = 'CLASS_SUPER_TYPE'; const SuperTypeEditor = observer( (props: { _class: Class; superType: GenericTypeReference; deleteSuperType: () => void; isReadOnly: boolean; }) => { const ref = useRef(null); const handleRef = useRef(null); const { superType, _class, deleteSuperType, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); // Type const superTypeOptions = editorStore.graphManagerState.usableClasses .filter( (c) => c instanceof Class && // Exclude current class c !== _class && // Exclude super types of the class !getAllSuperclasses(_class).includes(c) && // Ensure there is no loop (might be expensive) !getAllSuperclasses(c).includes(_class), ) .map(buildElementOption); // Drag and Drop const handleHover = useCallback( (item: ClassSuperTypeDragSource): void => { const draggingProperty = item.superType; const hoveredProperty = superType; class_swapSuperTypes(_class, draggingProperty, hoveredProperty); }, [_class, superType], ); const [{ isBeingDraggedSupertype }, dropConnector] = useDrop< ClassSuperTypeDragSource, void, { isBeingDraggedSupertype: GenericTypeReference | undefined } >( () => ({ accept: [CLASS_SUPER_TYPE_DND_TYPE], hover: (item) => handleHover(item), collect: (monitor) => ({ isBeingDraggedSupertype: monitor.getItem()?.superType, }), }), [handleHover], ); const isBeingDragged = superType === isBeingDraggedSupertype; const [, dragConnector, dragPreviewConnector] = useDrag( () => ({ type: CLASS_SUPER_TYPE_DND_TYPE, item: () => ({ superType: superType, }), }), [superType], ); dragConnector(handleRef); dropConnector(ref); useDragPreviewLayer(dragPreviewConnector); const rawType = superType.value.rawType; const filterOption = createFilter({ ignoreCase: true, ignoreAccents: false, stringify: (option: { data: PackageableElementOption }): string => option.data.value.path, }); const selectedType = { value: rawType, label: rawType.name, } as PackageableElementOption; const changeType = (val: PackageableElementOption): void => setGenericTypeReferenceValue(superType, new GenericType(val.value)); const visitDerivationSource = (): void => editorStore.graphEditorMode.openElement(rawType); return ( } className="super-type-editor__container" showPlaceholder={isBeingDragged} >
{!isReadOnly && ( )}
); }, ); const PropertiesEditor = observer( (props: { _class: Class; editorState: ClassEditorState }) => { const { _class, editorState } = props; const isReadOnly = editorState.isReadOnly; const deleteProperty = (property: Property): (() => void) => (): void => { class_deleteProperty(_class, property); if (property === editorState.selectedProperty) { editorState.setSelectedProperty(undefined); } }; const indirectProperties = getAllClassProperties(_class) .filter((property) => !_class.properties.includes(property)) .sort((p1, p2) => p1.name.localeCompare(p2.name)) .sort( (p1, p2) => (p1._OWNER === _class ? 1 : 0) - (p2._OWNER === _class ? 1 : 0), ); const handleDropProperty = useCallback( (item: UMLEditorElementDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Type) { class_addProperty( _class, stub_Property(item.data.packageableElement, _class), ); } }, [_class, isReadOnly], ); const [{ isPropertyDragOver }, dropConnector] = useDrop< ElementDragSource, void, { isPropertyDragOver: boolean } >( () => ({ accept: [ CORE_DND_TYPE.PROJECT_EXPLORER_CLASS, CORE_DND_TYPE.PROJECT_EXPLORER_ENUMERATION, ], drop: (item) => handleDropProperty(item), collect: (monitor) => ({ isPropertyDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropProperty], ); useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_EDITOR_PROPERTIES, ); return ( item.property.name === '' ? '(unknown)' : item.property.name } types={[CLASS_PROPERTY_DND_TYPE]} /> {_class.properties.concat(indirectProperties).map((property) => ( ))} ); }, ); const DerviedPropertiesEditor = observer( (props: { _class: Class; editorState: ClassEditorState }) => { const { _class, editorState } = props; const isReadOnly = editorState.isReadOnly; const classState = editorState.classState; const indirectDerivedProperties = getAllClassDerivedProperties(_class) .filter((property) => !_class.derivedProperties.includes(property)) .sort((p1, p2) => p1.name.localeCompare(p2.name)) .sort( (p1, p2) => (p1._OWNER === _class ? 1 : 0) - (p2._OWNER === _class ? 1 : 0), ); const deleteDerivedProperty = (dp: DerivedProperty): (() => void) => (): void => { class_deleteDerivedProperty(_class, dp); classState.deleteDerivedPropertyState(dp); if (dp === editorState.selectedProperty) { editorState.setSelectedProperty(undefined); } }; const handleDropDerivedProperty = useCallback( (item: UMLEditorElementDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Type) { const dp = stub_DerivedProperty(item.data.packageableElement, _class); class_addDerivedProperty(_class, dp); classState.addDerivedPropertyState(dp); } }, [_class, classState, isReadOnly], ); const [{ isDerivedPropertyDragOver }, dropConnector] = useDrop< ElementDragSource, void, { isDerivedPropertyDragOver: boolean } >( () => ({ accept: [ CORE_DND_TYPE.PROJECT_EXPLORER_CLASS, CORE_DND_TYPE.PROJECT_EXPLORER_ENUMERATION, ], drop: (item) => handleDropDerivedProperty(item), collect: (monitor) => ({ isDerivedPropertyDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropDerivedProperty], ); useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_EDITOR_DERIVED_PROPERTIES, ); return ( item.derivedProperty.name === '' ? '(unknown)' : item.derivedProperty.name } types={[CLASS_DERIVED_PROPERTY_DND_TYPE]} /> {_class.derivedProperties .concat(indirectDerivedProperties) .filter((dp): dp is DerivedProperty => Boolean(classState.getNullableDerivedPropertyState(dp)), ) .map((dp) => ( ))} ); }, ); const ConstraintsEditor = observer( (props: { _class: Class; editorState: ClassEditorState }) => { const { _class, editorState } = props; const isReadOnly = editorState.isReadOnly; const classState = editorState.classState; const deleteConstraint = (constraint: Constraint): (() => void) => (): void => { class_deleteConstraint(_class, constraint); classState.deleteConstraintState(constraint); }; const inheritedConstraints = getAllClassConstraints(_class).filter( (constraint) => !_class.constraints.includes(constraint), ); useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_EDITOR_CONSTRAINTS, ); return ( item.constraint.name === '' ? '(unknown)' : item.constraint.name } types={[CLASS_CONSTRAINT_DND_TYPE]} /> {_class.constraints .concat(inheritedConstraints) .filter((constraint): constraint is Constraint => Boolean(classState.getNullableConstraintState(constraint)), ) .map((constraint) => ( ))} ); }, ); const SupertypesEditor = observer( (props: { _class: Class; editorState: ClassEditorState }) => { const { _class, editorState } = props; const isReadOnly = editorState.isReadOnly; const deleteSuperType = (superType: GenericTypeReference): (() => void) => (): void => { class_deleteSuperType(_class, superType); if (superType.value.rawType instanceof Class) { class_deleteSubclass(superType.value.rawType, _class); } }; const handleDropSuperType = useCallback( (item: UMLEditorElementDropTarget): void => { const element = item.data.packageableElement; if ( !isReadOnly && // Have to be a class element instanceof Class && // Must not be the same class element !== _class && // Must not be a supertype of the current class !getAllSuperclasses(_class).includes(element) && // Must not have the current class as a super type !getAllSuperclasses(element).includes(_class) ) { class_addSuperType( _class, GenericTypeExplicitReference.create(new GenericType(element)), ); class_addSubclass(element, _class); } }, [_class, isReadOnly], ); const [{ isSuperTypeDragOver }, dropConnector] = useDrop< ElementDragSource, void, { isSuperTypeDragOver: boolean } >( () => ({ accept: [ CORE_DND_TYPE.PROJECT_EXPLORER_CLASS, CORE_DND_TYPE.PROJECT_EXPLORER_ENUMERATION, ], drop: (item) => handleDropSuperType(item), collect: (monitor) => ({ isSuperTypeDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropSuperType], ); useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_EDITOR_SUPERTYPES, ); return ( item.superType.value.rawType.name } types={[CLASS_SUPER_TYPE_DND_TYPE]} /> {_class.generalizations.map((superType) => ( ))} ); }, ); const TaggedValuesEditor = observer( (props: { _class: Class; editorState: ClassEditorState }) => { const { _class, editorState } = props; const applicationStore = useApplicationStore(); const isReadOnly = editorState.isReadOnly; const deleteTaggedValue = (val: TaggedValue): (() => void) => (): void => annotatedElement_deleteTaggedValue(_class, val); const handleDropTaggedValue = useCallback( (item: UMLEditorElementDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Profile) { annotatedElement_addTaggedValue( _class, stub_TaggedValue(stub_Tag(item.data.packageableElement)), ); } }, [_class, isReadOnly], ); const [{ isTaggedValueDragOver }, dropConnector] = useDrop< ElementDragSource, void, { isTaggedValueDragOver: boolean } >( () => ({ accept: [CORE_DND_TYPE.PROJECT_EXPLORER_PROFILE], drop: (item) => handleDropTaggedValue(item), collect: (monitor) => ({ isTaggedValueDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropTaggedValue], ); return ( {_class.taggedValues.map((taggedValue) => ( ))} ); }, ); const StereotypesEditor = observer( (props: { _class: Class; editorState: ClassEditorState }) => { const { _class, editorState } = props; const applicationStore = useApplicationStore(); const isReadOnly = editorState.isReadOnly; const deleteStereotype = (val: StereotypeReference): (() => void) => (): void => annotatedElement_deleteStereotype(_class, val); const handleDropStereotype = useCallback( (item: UMLEditorElementDropTarget): void => { if (!isReadOnly && item.data.packageableElement instanceof Profile) { annotatedElement_addStereotype( _class, StereotypeExplicitReference.create( stub_Stereotype(item.data.packageableElement), ), ); } }, [_class, isReadOnly], ); const [{ isStereotypeDragOver }, dropConnector] = useDrop< ElementDragSource, void, { isStereotypeDragOver: boolean } >( () => ({ accept: [CORE_DND_TYPE.PROJECT_EXPLORER_PROFILE], drop: (item) => handleDropStereotype(item), collect: (monitor) => ({ isStereotypeDragOver: monitor.isOver({ shallow: true }), }), }), [handleDropStereotype], ); return ( {_class.stereotypes.map((stereotype) => ( ))} ); }, ); export const ClassFormEditor = observer( (props: { _class: Class; editorState: ClassEditorState; onHashChange?: () => void; }) => { const { _class, editorState, onHashChange } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const classHash = isElementReadOnly(_class) ? undefined : applicationStore.notificationService.notifyAndReturnAlternativeOnError( () => _class.hashCode, undefined, ); // attempting to read the hashCode of immutable element will throw an error const classState = editorState.classState; const isReadOnly = editorState.isReadOnly; // Tab const selectedTab = editorState.selectedTab; const tabs = [ UML_EDITOR_TAB.PROPERTIES, UML_EDITOR_TAB.DERIVED_PROPERTIES, UML_EDITOR_TAB.CONSTRAINTS, UML_EDITOR_TAB.SUPER_TYPES, UML_EDITOR_TAB.TAGGED_VALUES, UML_EDITOR_TAB.STEREOTYPES, ]; const changeTab = (tab: UML_EDITOR_TAB): (() => void) => (): void => { editorState.setSelectedTab(tab); editorState.setSelectedProperty(undefined); }; const deselectProperty = (): void => editorState.setSelectedProperty(undefined); const possibleSupertypes = editorStore.graphManagerState.graph.ownClasses.filter( (superType) => // Exclude current class superType !== _class && // Exclude super types of the class !getAllSuperclasses(_class).includes(superType) && // Ensure there is no loop (might be expensive) !getAllSuperclasses(superType).includes(_class), ); // Add button let addButtonTitle = ''; switch (selectedTab) { case UML_EDITOR_TAB.PROPERTIES: addButtonTitle = 'Add property'; break; case UML_EDITOR_TAB.DERIVED_PROPERTIES: addButtonTitle = 'Add derived property'; break; case UML_EDITOR_TAB.CONSTRAINTS: addButtonTitle = 'Add constraint'; break; case UML_EDITOR_TAB.SUPER_TYPES: addButtonTitle = 'Add super type'; break; case UML_EDITOR_TAB.TAGGED_VALUES: addButtonTitle = 'Add tagged value'; break; case UML_EDITOR_TAB.STEREOTYPES: addButtonTitle = 'Add stereotype'; break; default: break; } const isAddButtonDisabled = isReadOnly || (selectedTab === UML_EDITOR_TAB.SUPER_TYPES && !possibleSupertypes.length); const add = (): void => { if (!isReadOnly) { if (selectedTab === UML_EDITOR_TAB.PROPERTIES) { class_addProperty( _class, stub_Property(PrimitiveType.STRING, _class), ); } else if (selectedTab === UML_EDITOR_TAB.DERIVED_PROPERTIES) { const dp = stub_DerivedProperty(PrimitiveType.STRING, _class); class_addDerivedProperty(_class, dp); classState.addDerivedPropertyState(dp); } else if (selectedTab === UML_EDITOR_TAB.CONSTRAINTS) { const constraint = stub_Constraint(_class); class_addContraint(_class, constraint); classState.addConstraintState(constraint); } else if ( selectedTab === UML_EDITOR_TAB.SUPER_TYPES && possibleSupertypes.length ) { const possibleSupertype = possibleSupertypes[0] as Class; class_addSuperType( _class, GenericTypeExplicitReference.create( new GenericType(possibleSupertype), ), ); class_addSubclass(possibleSupertype, _class); } else if (selectedTab === UML_EDITOR_TAB.TAGGED_VALUES) { annotatedElement_addTaggedValue( _class, stub_TaggedValue(stub_Tag(stub_Profile())), ); } else if (selectedTab === UML_EDITOR_TAB.STEREOTYPES) { annotatedElement_addStereotype( _class, StereotypeExplicitReference.create(stub_Stereotype(stub_Profile())), ); } } }; // Generation const generationParentElementPath = editorStore.graphState.graphGenerationState.findGenerationParentPath( _class.path, ); const generationParentElement = generationParentElementPath ? editorStore.graphManagerState.graph.getNullableElement( generationParentElementPath, ) : undefined; const visitGenerationParentElement = (): void => { if (generationParentElement) { editorStore.graphEditorMode.openElement(generationParentElement); } }; // On change handler (this is used for other editors which embeds editor) useEffect(() => { onHashChange?.(); }, [_class, classHash, onHashChange]); // Decorate (add/remove states for derived properties/constraints) and convert lambda objects useEffect(() => { classState.decorate(); flowResult(classState.convertConstraintLambdaObjects()).catch( applicationStore.alertUnhandledError, ); flowResult(classState.convertDerivedPropertyLambdaObjects()).catch( applicationStore.alertUnhandledError, ); }, [applicationStore, classState]); // layout const propertyEditorCollapsiblePanelGroupProps = getCollapsiblePanelGroupProps(!editorState.selectedProperty, { size: 250, }); return (
{isReadOnly && (
)}
class
{_class.name}
{generationParentElement && ( )}
{tabs.map((tab) => (
{prettyCONSTName(tab)}
))}
{selectedTab === UML_EDITOR_TAB.PROPERTIES && ( )} {selectedTab === UML_EDITOR_TAB.DERIVED_PROPERTIES && ( )} {selectedTab === UML_EDITOR_TAB.CONSTRAINTS && ( )} {selectedTab === UML_EDITOR_TAB.SUPER_TYPES && ( )} {selectedTab === UML_EDITOR_TAB.TAGGED_VALUES && ( )} {selectedTab === UML_EDITOR_TAB.STEREOTYPES && ( )}
{editorState.selectedProperty ? ( ) : (
No property selected
)}
); }, ); export const ClassEditor = observer((props: { _class: Class }) => { const { _class } = props; const editorStore = useEditorStore(); const editorState = editorStore.tabManagerState.getCurrentEditorState(ClassEditorState); const classPreviewRenderers = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraClassPreviewRenderers?.() ?? []) .filter(isNonNullable); useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.CLASS_EDITOR, ); return ( {classPreviewRenderers.length !== 0 && (classPreviewRenderers[0] as ClassPreviewRenderer)(_class)} {classPreviewRenderers.length === 0 && ( No preview )} ); });