From c487ec965a6427ab5cb99e8607451b1ce67b9dd7 Mon Sep 17 00:00:00 2001 From: Victor Bustamante Date: Tue, 12 Nov 2024 19:53:02 -0300 Subject: [PATCH] Ability to add view objects to the diagram and manipulate it, and navigate between them Signed-off-by: Victor Bustamante Co-authored-by: John Obelenus --- app/web/src/api/sdf/dal/component.ts | 9 +- app/web/src/api/sdf/dal/schema.ts | 1 + app/web/src/api/sdf/dal/views.ts | 6 + .../components/Actions/ActionRunnerCard.vue | 11 +- .../components/Actions/ActionRunnerCardV2.vue | 11 +- .../src/components/Actions/ActionWidget.vue | 12 +- .../src/components/AssetActionsDetails.vue | 12 +- app/web/src/components/AssetDiffDetails.vue | 14 +- .../AttributesPanel/AttributesPanel.vue | 6 +- .../AttributesPanelCustomInputs.vue | 14 +- .../AttributesPanel/TreeFormItem.vue | 7 +- app/web/src/components/ChangeCard.vue | 10 +- app/web/src/components/ComponentCard.vue | 42 +- .../components/ComponentConnectionsPanel.vue | 34 +- app/web/src/components/ComponentDetails.vue | 14 +- .../src/components/ComponentDetailsCode.vue | 12 +- .../components/ComponentDetailsManagement.vue | 6 +- .../components/ComponentDetailsResource.vue | 12 +- .../DiagramOutline/DiagramOutline.vue | 46 +- .../DiagramOutline/DiagramOutlineNode.vue | 16 +- app/web/src/components/EdgeDetailsPanel.vue | 4 +- app/web/src/components/LeftPanelDrawer.vue | 9 +- .../src/components/ManagementRunPrototype.vue | 17 +- .../ModelingDiagram/DiagramEdge.vue | 14 +- .../ModelingDiagram/DiagramGroup.vue | 33 +- .../ModelingDiagram/DiagramNode.vue | 22 +- .../ModelingDiagram/DiagramView.vue | 173 +++++ .../ModelingDiagram/ModelingDiagram.vue | 356 +++++++--- .../ModelingDiagram/diagram_types.ts | 31 +- .../ModelingView/DeleteSelectionModal.vue | 79 ++- .../ModelingView/EraseSelectionModal.vue | 18 +- .../ModelingView/ModelingRightClickMenu.vue | 392 +++++------ .../ModelingView/RestoreSelectionModal.vue | 33 +- .../components/MultiSelectDetailsPanel.vue | 8 +- app/web/src/components/ViewCard.vue | 132 +++- .../src/components/Workspace/CommandModal.vue | 4 +- .../Workspace/WorkspaceModelAndView.vue | 17 +- .../layout/navbar/ChangeSetPanel.vue | 38 +- app/web/src/store/components.store.ts | 278 -------- app/web/src/store/func/funcs.store.ts | 27 +- app/web/src/store/realtime/realtime_events.ts | 2 + app/web/src/store/views.store.ts | 640 +++++++++++++++++- lib/dal/src/component.rs | 8 +- lib/dal/src/diagram.rs | 109 +-- lib/dal/src/diagram/diagram_object.rs | 105 +++ lib/dal/src/diagram/geometry.rs | 176 ++++- lib/dal/src/diagram/view.rs | 150 +++- lib/dal/src/schema/variant.rs | 3 +- lib/dal/src/standard_connection.rs | 1 + lib/dal/src/workspace_snapshot.rs | 1 + lib/dal/src/workspace_snapshot/edge_weight.rs | 2 + lib/dal/src/workspace_snapshot/graph/v2.rs | 3 +- lib/dal/src/workspace_snapshot/graph/v3.rs | 5 +- lib/dal/src/workspace_snapshot/graph/v4.rs | 6 +- lib/dal/src/workspace_snapshot/node_weight.rs | 44 +- .../node_weight/diagram_object_node_weight.rs | 33 + .../diagram_object_node_weight/v1.rs | 183 +++++ .../node_weight/geometry_node_weight/v1.rs | 207 +++++- .../workspace_snapshot/node_weight/traits.rs | 3 + lib/dal/src/ws_event.rs | 7 +- .../tests/integration_test/deserialize/mod.rs | 12 +- lib/dal/tests/integration_test/management.rs | 4 +- ...erialization-test-data-2024-10-17.snapshot | Bin 2083 -> 0 bytes ...erialization-test-data-2024-11-21.snapshot | Bin 0 -> 2209 bytes .../service/diagram/set_component_position.rs | 2 +- lib/sdf-server/src/service/v2/view.rs | 23 +- .../src/service/v2/view/create_view_object.rs | 123 ++++ .../src/service/v2/view/erase_view_object.rs | 94 +++ .../src/service/v2/view/get_diagram.rs | 62 +- .../service/v2/view/set_component_geometry.rs | 97 --- .../src/service/v2/view/set_geometry.rs | 179 +++++ .../src/design-system/icons/icon_set.ts | 1 + 72 files changed, 3213 insertions(+), 1052 deletions(-) create mode 100644 app/web/src/components/ModelingDiagram/DiagramView.vue create mode 100644 lib/dal/src/diagram/diagram_object.rs create mode 100644 lib/dal/src/workspace_snapshot/node_weight/diagram_object_node_weight.rs create mode 100644 lib/dal/src/workspace_snapshot/node_weight/diagram_object_node_weight/v1.rs delete mode 100644 lib/dal/tests/serialization-test-data-2024-10-17.snapshot create mode 100644 lib/dal/tests/serialization-test-data-2024-11-21.snapshot create mode 100644 lib/sdf-server/src/service/v2/view/create_view_object.rs create mode 100644 lib/sdf-server/src/service/v2/view/erase_view_object.rs delete mode 100644 lib/sdf-server/src/service/v2/view/set_component_geometry.rs create mode 100644 lib/sdf-server/src/service/v2/view/set_geometry.rs diff --git a/app/web/src/api/sdf/dal/component.ts b/app/web/src/api/sdf/dal/component.ts index 108713541b..c196319f3c 100644 --- a/app/web/src/api/sdf/dal/component.ts +++ b/app/web/src/api/sdf/dal/component.ts @@ -1,10 +1,10 @@ -import { Vector2d } from "konva/lib/types"; +import { IRect, Vector2d } from "konva/lib/types"; import { StandardModel } from "@/api/sdf/dal/standard_model"; import { CodeView } from "@/api/sdf/dal/code_view"; import { ActorView } from "@/api/sdf/dal/history_actor"; import { ChangeStatus } from "@/api/sdf/dal/change_set"; import { ComponentType } from "@/api/sdf/dal/schema"; -import { ViewId } from "@/api/sdf/dal/views"; +import { ViewDescription, ViewId } from "@/api/sdf/dal/views"; import { DiagramSocketDef, Size2D, @@ -37,6 +37,11 @@ export interface ViewGeometry { geometry: Vector2d & Partial; } +export interface ViewNodeGeometry { + view: ViewDescription; + geometry: IRect; +} + export interface RawComponent { changeStatus: ChangeStatus; color: string; diff --git a/app/web/src/api/sdf/dal/schema.ts b/app/web/src/api/sdf/dal/schema.ts index f1ecee8893..a9b4701e91 100644 --- a/app/web/src/api/sdf/dal/schema.ts +++ b/app/web/src/api/sdf/dal/schema.ts @@ -22,6 +22,7 @@ export type SchemaId = string; export enum ComponentType { Component = "component", + View = "view", ConfigurationFrameDown = "configurationFrameDown", ConfigurationFrameUp = "configurationFrameUp", AggregationFrame = "aggregationFrame", diff --git a/app/web/src/api/sdf/dal/views.ts b/app/web/src/api/sdf/dal/views.ts index b83cdade6d..89fad8bb43 100644 --- a/app/web/src/api/sdf/dal/views.ts +++ b/app/web/src/api/sdf/dal/views.ts @@ -2,14 +2,19 @@ import { IRect } from "konva/lib/types"; import { ComponentId } from "@/api/sdf/dal/component"; import { DiagramElementUniqueKey, + DiagramViewData, SocketLocationInfo, } from "@/components/ModelingDiagram/diagram_types"; +import { ComponentType } from "./schema"; export type ViewId = string; export type Components = Record; export type Groups = Record; export type Sockets = Record; +export type ViewNode = ViewDescription & + IRect & { componentType: ComponentType.View }; +export type ViewNodes = Record; export interface View { id: ViewId; @@ -17,6 +22,7 @@ export interface View { components: Components; groups: Groups; sockets: Sockets; + viewNodes: ViewNodes; } export interface ViewDescription { diff --git a/app/web/src/components/Actions/ActionRunnerCard.vue b/app/web/src/components/Actions/ActionRunnerCard.vue index 1e42a68e7c..faeb0f08df 100644 --- a/app/web/src/components/Actions/ActionRunnerCard.vue +++ b/app/web/src/components/Actions/ActionRunnerCard.vue @@ -95,6 +95,7 @@ import { TreeNode, useTheme, IconButton } from "@si/vue-lib/design-system"; import { DeprecatedActionRunner } from "@/store/actions.store"; import { useComponentsStore } from "@/store/components.store"; import { useChangeSetsStore } from "@/store/change_sets.store"; +import { useViewsStore } from "@/store/views.store"; import CodeViewer from "../CodeViewer.vue"; import StatusIndicatorIcon from "../StatusIndicatorIcon.vue"; import ActionRunnerDetails from "./ActionRunnerDetails.vue"; @@ -140,10 +141,12 @@ const componentNameTooltip = computed(() => { return {}; }); +const viewStore = useViewsStore(); + function onClick() { const component = componentsStore.allComponentsById[props.runner.componentId]; if (component) { - componentsStore.setSelectedComponentId(props.runner.componentId); + viewStore.setSelectedComponentId(props.runner.componentId); componentsStore.eventBus.emit("panToComponent", { component, center: true, @@ -153,18 +156,18 @@ function onClick() { } const isHover = computed( - () => componentsStore.hoveredComponentId === props.runner.componentId, + () => viewStore.hoveredComponentId === props.runner.componentId, ); function onHoverStart() { if (componentsStore.allComponentsById[props.runner.componentId]) { - componentsStore.setHoveredComponentId(props.runner.componentId); + viewStore.setHoveredComponentId(props.runner.componentId); } } function onHoverEnd() { if (componentsStore.allComponentsById[props.runner.componentId]) { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } } diff --git a/app/web/src/components/Actions/ActionRunnerCardV2.vue b/app/web/src/components/Actions/ActionRunnerCardV2.vue index 620d898376..f1e6741145 100644 --- a/app/web/src/components/Actions/ActionRunnerCardV2.vue +++ b/app/web/src/components/Actions/ActionRunnerCardV2.vue @@ -48,6 +48,7 @@ import { TreeNode, useTheme } from "@si/vue-lib/design-system"; import { ActionProposedView } from "@/store/actions.store"; import { useComponentsStore } from "@/store/components.store"; import { useChangeSetsStore } from "@/store/change_sets.store"; +import { useViewsStore } from "@/store/views.store"; import StatusIndicatorIcon from "../StatusIndicatorIcon.vue"; const changeSetsStore = useChangeSetsStore(); @@ -83,11 +84,13 @@ const componentNameTooltip = computed(() => { return {}; }); +const viewStore = useViewsStore(); + function onClick() { const component = componentsStore.allComponentsById[props.action.componentId || ""]; if (component) { - componentsStore.setSelectedComponentId(props.action.componentId); + viewStore.setSelectedComponentId(props.action.componentId); componentsStore.eventBus.emit("panToComponent", { component, center: true, @@ -97,7 +100,7 @@ function onClick() { } const isHover = computed( - () => componentsStore.hoveredComponentId === props.action.componentId, + () => viewStore.hoveredComponentId === props.action.componentId, ); function onHoverStart() { @@ -105,7 +108,7 @@ function onHoverStart() { props.action.componentId && componentsStore.allComponentsById[props.action.componentId] ) { - componentsStore.setHoveredComponentId(props.action.componentId); + viewStore.setHoveredComponentId(props.action.componentId); } } @@ -114,7 +117,7 @@ function onHoverEnd() { props.action.componentId && componentsStore.allComponentsById[props.action.componentId] ) { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } } diff --git a/app/web/src/components/Actions/ActionWidget.vue b/app/web/src/components/Actions/ActionWidget.vue index 4d7282d903..2f0d740636 100644 --- a/app/web/src/components/Actions/ActionWidget.vue +++ b/app/web/src/components/Actions/ActionWidget.vue @@ -51,7 +51,8 @@ import { useRouter } from "vue-router"; import { storeToRefs } from "pinia"; import { useActionsStore } from "@/store/actions.store"; import { Action } from "@/api/sdf/dal/func"; -import { useComponentsStore } from "@/store/components.store"; +import { useViewsStore } from "@/store/views.store"; +import { ComponentType } from "@/api/sdf/dal/schema"; import StatusIndicatorIcon from "../StatusIndicatorIcon.vue"; import { DiagramGroupData, @@ -68,8 +69,8 @@ const props = defineProps<{ binding: BindingWithDisplayName; }>(); -const componentsStore = useComponentsStore(); -const { selectedComponent } = storeToRefs(componentsStore); +const viewStore = useViewsStore(); +const { selectedComponent } = storeToRefs(viewStore); const actionsStore = useActionsStore(); const router = useRouter(); @@ -92,7 +93,10 @@ function clickHandler() { } function onClickView() { - if (props.binding) { + if ( + props.binding && + selectedComponent.value?.def.componentType !== ComponentType.View + ) { router.push({ name: "workspace-lab-assets", query: { diff --git a/app/web/src/components/AssetActionsDetails.vue b/app/web/src/components/AssetActionsDetails.vue index 5b0df698b6..d603d30f19 100644 --- a/app/web/src/components/AssetActionsDetails.vue +++ b/app/web/src/components/AssetActionsDetails.vue @@ -2,9 +2,7 @@
@@ -43,10 +41,10 @@ import { computed, ref, watch } from "vue"; import * as _ from "lodash-es"; import { TabGroup, TabGroupItem } from "@si/vue-lib/design-system"; -import { useComponentsStore } from "@/store/components.store"; import { useFuncStore } from "@/store/func/funcs.store"; import EmptyStateIcon from "@/components/EmptyStateIcon.vue"; import ActionWidget from "@/components/Actions/ActionWidget.vue"; +import { useViewsStore } from "@/store/views.store"; import ComponentDetailsResource from "./ComponentDetailsResource.vue"; import { DiagramGroupData, @@ -58,17 +56,17 @@ const props = defineProps<{ }>(); const funcStore = useFuncStore(); -const componentsStore = useComponentsStore(); +const viewStore = useViewsStore(); const tabsRef = ref>(); function onTabSelected(newTabSlug?: string) { - componentsStore.setComponentDetailsTab(newTabSlug || null); + viewStore.setComponentDetailsTab(newTabSlug || null); } const bindings = computed(() => funcStore.actionBindingsForSelectedComponent); watch( - () => componentsStore.selectedComponentDetailsTab, + () => viewStore.selectedComponentDetailsTab, (tabSlug) => { if (tabSlug?.startsWith("resource-")) { tabsRef.value?.selectTab(tabSlug); diff --git a/app/web/src/components/AssetDiffDetails.vue b/app/web/src/components/AssetDiffDetails.vue index e0e128e76f..4f911a84eb 100644 --- a/app/web/src/components/AssetDiffDetails.vue +++ b/app/web/src/components/AssetDiffDetails.vue @@ -1,6 +1,10 @@ - diff --git a/app/web/src/components/ComponentDetailsResource.vue b/app/web/src/components/ComponentDetailsResource.vue index ffbc228ad9..30b17e0626 100644 --- a/app/web/src/components/ComponentDetailsResource.vue +++ b/app/web/src/components/ComponentDetailsResource.vue @@ -67,16 +67,18 @@ import { computed, watch } from "vue"; import { ErrorMessage, Timestamp } from "@si/vue-lib/design-system"; import { useComponentsStore } from "@/store/components.store"; import { useChangeSetsStore } from "@/store/change_sets.store"; +import { useViewsStore } from "@/store/views.store"; import CodeViewer from "./CodeViewer.vue"; import ActionRunnerDetails from "./Actions/ActionRunnerDetails.vue"; import StatusIndicatorIcon from "./StatusIndicatorIcon.vue"; import EmptyStateIcon from "./EmptyStateIcon.vue"; const changeSetsStore = useChangeSetsStore(); +const viewStore = useViewsStore(); const componentsStore = useComponentsStore(); const selectedComponentId = computed( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - () => componentsStore.selectedComponentId!, + () => viewStore.selectedComponentId!, ); const resourceReqStatus = componentsStore.getRequestStatus( @@ -85,15 +87,17 @@ const resourceReqStatus = componentsStore.getRequestStatus( ); const selectedComponentResource = computed( - () => componentsStore.selectedComponentResource, + () => + componentsStore.componentResourceById[viewStore.selectedComponentId || ""], ); watch( [() => changeSetsStore.selectedChangeSetLastWrittenAt], () => { if ( - componentsStore.selectedComponent && - componentsStore.selectedComponent.def.changeStatus !== "deleted" + viewStore.selectedComponent && + "changeStatus" in viewStore.selectedComponent.def && + viewStore.selectedComponent.def.changeStatus !== "deleted" ) { componentsStore.FETCH_COMPONENT_RESOURCE(selectedComponentId.value); } diff --git a/app/web/src/components/DiagramOutline/DiagramOutline.vue b/app/web/src/components/DiagramOutline/DiagramOutline.vue index 1a7941751a..b4260e7dcb 100644 --- a/app/web/src/components/DiagramOutline/DiagramOutline.vue +++ b/app/web/src/components/DiagramOutline/DiagramOutline.vue @@ -183,7 +183,13 @@ const viewComponentIds = computed(() => { const rootComponents = computed(() => { return Object.values(componentsStore.allComponentsById).filter((c) => { if (viewComponentIds.value !== null) { - return viewComponentIds.value.includes(c.def.id) && !c.def.parentId; + const ancestorsInView = c.def.ancestorIds?.some((a) => + viewComponentIds.value?.includes(a), + ); + return ( + viewComponentIds.value.includes(c.def.id) && + (!c.def.parentId || !ancestorsInView) + ); } else return !c.def.parentId; }); }); @@ -341,11 +347,11 @@ const filterArrays = [ ]; watch( - () => componentsStore.selectedComponentId, + () => viewStore.selectedComponentId, () => { - if (!componentsStore.selectedComponentId) return; + if (!viewStore.selectedComponentId) return; const el = document.getElementById( - `diagram-outline-node-${componentsStore.selectedComponentId}`, + `diagram-outline-node-${viewStore.selectedComponentId}`, ); el?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, @@ -357,17 +363,17 @@ function itemClickHandler( tabSlug?: string, ) { const shiftKeyBehavior = () => { - const selectedComponentIds = componentsStore.selectedComponentIds; + const selectedComponentIds = viewStore.selectedComponentIds; if (selectedComponentIds.length === 0) { // If nothing is selected, select the current component - componentsStore.setSelectedComponentId(component.def.id); + viewStore.setSelectedComponentId(component.def.id); } else if ( selectedComponentIds.length === 1 && selectedComponentIds[0] === component.def.id ) { // If there's only one component selected and you clicked it, deselect it - componentsStore.setSelectedComponentId(null); + viewStore.setSelectedComponentId(null); } else { // Otherwise, attempt to select components between let components = componentsTreeFlattened.value; @@ -376,7 +382,7 @@ function itemClickHandler( } let indexFrom = components.findIndex((c) => - componentsStore.selectedComponentIds.includes(c.def.id), + viewStore.selectedComponentIds.includes(c.def.id), ); const indexTo = components.findIndex( (c) => c.def.id === component.def.id, @@ -384,19 +390,19 @@ function itemClickHandler( if (indexFrom > indexTo) { indexFrom = _.findLastIndex(components, (c) => - componentsStore.selectedComponentIds.includes(c.def.id), + viewStore.selectedComponentIds.includes(c.def.id), ); const selection = components .slice(indexTo, indexFrom + 1) .map((component) => component.def.id); - componentsStore.setSelectedComponentId(selection); + viewStore.setSelectedComponentId(selection); } else if (indexFrom < indexTo) { const selection = components .slice(indexFrom, indexTo + 1) .map((component) => component.def.id); - componentsStore.setSelectedComponentId(selection); + viewStore.setSelectedComponentId(selection); } else { - componentsStore.setSelectedComponentId(component.def.id); + viewStore.setSelectedComponentId(component.def.id); } } }; @@ -406,15 +412,13 @@ function itemClickHandler( e.preventDefault(); if (e.shiftKey) { shiftKeyBehavior(); - } else if ( - !componentsStore.selectedComponentIds.includes(component.def.id) - ) { + } else if (!viewStore.selectedComponentIds.includes(component.def.id)) { if (e.metaKey) { - componentsStore.setSelectedComponentId(component.def.id, { + viewStore.setSelectedComponentId(component.def.id, { toggle: true, }); } else { - componentsStore.setSelectedComponentId(component.def.id); + viewStore.setSelectedComponentId(component.def.id); } } emit("right-click-item", { mouse: e, component }); @@ -423,11 +427,11 @@ function itemClickHandler( shiftKeyBehavior(); } else if (e.metaKey) { e.preventDefault(); - componentsStore.setSelectedComponentId(component.def.id, { toggle: true }); + viewStore.setSelectedComponentId(component.def.id, { toggle: true }); } else if (e.type === "dblclick") { componentsStore.panTargetComponentId = component.def.id; } else { - componentsStore.setSelectedComponentId(component.def.id, { + viewStore.setSelectedComponentId(component.def.id, { detailsTab: tabSlug, }); componentsStore.panTargetComponentId = component.def.id; @@ -455,7 +459,7 @@ const onKeyDown = (e: KeyboardEvent) => { // Tab goes forwards, Shift-Tab goes backwards if (e.key === "Tab") { - const selectedComponentId = _.last(componentsStore.selectedComponentIds); + const selectedComponentId = _.last(viewStore.selectedComponentIds); if (!selectedComponentId) return; e.preventDefault(); @@ -476,7 +480,7 @@ const onKeyDown = (e: KeyboardEvent) => { } const toSelect = componentIds[toSelectIndex]; if (toSelect) { - componentsStore.setSelectedComponentId(toSelect); + viewStore.setSelectedComponentId(toSelect); } } }; diff --git a/app/web/src/components/DiagramOutline/DiagramOutlineNode.vue b/app/web/src/components/DiagramOutline/DiagramOutlineNode.vue index 7a38d6e7a0..3a3c12835c 100644 --- a/app/web/src/components/DiagramOutline/DiagramOutlineNode.vue +++ b/app/web/src/components/DiagramOutline/DiagramOutlineNode.vue @@ -64,6 +64,12 @@
{{ component.def.schemaName }}: {{ component.def.displayName }}
+
@@ -312,7 +318,7 @@ const childComponents = computed(() => { }); const isSelected = computed(() => - componentsStore.selectedComponentIds.includes(props.component.def.id), + viewStore.selectedComponentIds.includes(props.component.def.id), ); const enableGroupToggle = computed( @@ -335,15 +341,15 @@ function onClick(e: MouseEvent, tabSlug?: string) { } const isHover = computed( - () => componentsStore.hoveredComponentId === props.component.def.id, + () => viewStore.hoveredComponentId === props.component.def.id, ); function onHoverStart() { - componentsStore.setHoveredComponentId(props.component.def.id); + viewStore.setHoveredComponentId(props.component.def.id); } function onHoverEnd() { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } const parentIdPathByComponentId = computed>( @@ -437,7 +443,7 @@ const parentBreadcrumbsText = computed(() => { const upgradeRequestStatus = componentsStore.getRequestStatus("UPGRADE_COMPONENT"); const upgradeComponent = async () => { - componentsStore.setSelectedComponentId(null); + viewStore.setSelectedComponentId(null); await componentsStore.UPGRADE_COMPONENT( props.component.def.id, props.component.def.displayName, diff --git a/app/web/src/components/EdgeDetailsPanel.vue b/app/web/src/components/EdgeDetailsPanel.vue index c6140021e1..6caec0770a 100644 --- a/app/web/src/components/EdgeDetailsPanel.vue +++ b/app/web/src/components/EdgeDetailsPanel.vue @@ -58,6 +58,7 @@ import { } from "@si/vue-lib/design-system"; import { useComponentsStore } from "@/store/components.store"; import { isDevMode } from "@/utils/debug"; +import { useViewsStore } from "@/store/views.store"; import DetailsPanelTimestamps from "./DetailsPanelTimestamps.vue"; import EdgeCard from "./EdgeCard.vue"; import SidebarSubpanelTitle from "./SidebarSubpanelTitle.vue"; @@ -68,9 +69,10 @@ defineProps({ }); const componentsStore = useComponentsStore(); +const viewStore = useViewsStore(); const modelingEventBus = componentsStore.eventBus; -const selectedEdge = computed(() => componentsStore.selectedEdge); +const selectedEdge = computed(() => viewStore.selectedEdge); const emit = defineEmits<{ (e: "openMenu", mouse: MouseEvent): void; diff --git a/app/web/src/components/LeftPanelDrawer.vue b/app/web/src/components/LeftPanelDrawer.vue index bd0f3c9cc2..2a01ae8c0e 100644 --- a/app/web/src/components/LeftPanelDrawer.vue +++ b/app/web/src/components/LeftPanelDrawer.vue @@ -43,6 +43,7 @@ :key="view.id" :view="view" :selected="view.id === viewStore.selectedViewId" + :outlined="view.id === viewStore.outlinerViewId" />
@@ -116,10 +117,14 @@ const create = async () => { labelRef.value?.setError("Name is required"); } else { const resp = await viewStore.CREATE_VIEW(viewName.value); - modalRef.value?.close(); - viewName.value = ""; if (resp.result.success) { viewStore.selectView(resp.result.data.id); + modalRef.value?.close(); + viewName.value = ""; + } else if (resp.result.statusCode === 409) { + labelRef.value?.setError( + `${viewName.value} is already in use. Please choose another name`, + ); } } }; diff --git a/app/web/src/components/ManagementRunPrototype.vue b/app/web/src/components/ManagementRunPrototype.vue index a781340205..f9c08be10f 100644 --- a/app/web/src/components/ManagementRunPrototype.vue +++ b/app/web/src/components/ManagementRunPrototype.vue @@ -54,11 +54,11 @@ import { MgmtPrototype, MgmtPrototypeResult, } from "@/store/func/funcs.store"; -import { useComponentsStore } from "@/store/components.store"; import { FuncRunId } from "@/store/func_runs.store"; import { useManagementRunsStore } from "@/store/management_runs.store"; import { useViewsStore } from "@/store/views.store"; import { ViewId } from "@/api/sdf/dal/views"; +import { ComponentType } from "@/api/sdf/dal/schema"; import { DiagramGroupData, DiagramNodeData, @@ -67,7 +67,7 @@ import StatusIndicatorIcon from "./StatusIndicatorIcon.vue"; import FuncRunTabDropdown from "./FuncRunTabDropdown.vue"; const funcStore = useFuncStore(); -const componentsStore = useComponentsStore(); +const viewStore = useViewsStore(); const router = useRouter(); const toast = useToast(); const managementRunsStore = useManagementRunsStore(); @@ -161,11 +161,12 @@ const runClick = async (e?: MouseEvent) => { }; function onClickView() { - router.push({ - name: "workspace-lab-assets", - query: { - s: `a_${componentsStore.selectedComponent?.def.schemaVariantId}|f_${props.prototype.funcId}`, - }, - }); + if (viewStore.selectedComponent?.def.componentType !== ComponentType.View) + router.push({ + name: "workspace-lab-assets", + query: { + s: `a_${viewStore.selectedComponent?.def.schemaVariantId}|f_${props.prototype.funcId}`, + }, + }); } diff --git a/app/web/src/components/ModelingDiagram/DiagramEdge.vue b/app/web/src/components/ModelingDiagram/DiagramEdge.vue index 0fce4b51ac..a16804a338 100644 --- a/app/web/src/components/ModelingDiagram/DiagramEdge.vue +++ b/app/web/src/components/ModelingDiagram/DiagramEdge.vue @@ -76,9 +76,9 @@ import { getToneColorHex, useTheme, } from "@si/vue-lib/design-system"; -import { useComponentsStore } from "@/store/components.store"; import { isDevMode } from "@/utils/debug"; import { useFeatureFlagsStore } from "@/store/feature_flags.store"; +import { useViewsStore } from "@/store/views.store"; import { SELECTION_COLOR, SOCKET_SIZE } from "./diagram_constants"; import { DiagramEdgeData } from "./diagram_types"; import { pointAlongLinePct, pointAlongLinePx } from "./utils/math"; @@ -160,9 +160,9 @@ const centerPoint = computed(() => { return pointAlongLinePct(props.fromPoint, props.toPoint, 0.5); }); -const selectedComponentId = computed( - () => componentsStore.selectedComponent?.def.id, -); +const viewStore = useViewsStore(); + +const selectedComponentId = computed(() => viewStore.selectedComponent?.def.id); const isFromOrToSelected = computed( () => @@ -198,14 +198,12 @@ const mainLineOpacity = computed(() => { return 0.1; }); -const componentsStore = useComponentsStore(); - function onMouseOver() { - componentsStore.setHoveredEdgeId(props.edge.def.id); + viewStore.setHoveredEdgeId(props.edge.def.id); } function onMouseOut(_e: KonvaEventObject) { - componentsStore.setHoveredEdgeId(null); + viewStore.setHoveredEdgeId(null); } function onMouseDown(_e: KonvaEventObject) { diff --git a/app/web/src/components/ModelingDiagram/DiagramGroup.vue b/app/web/src/components/ModelingDiagram/DiagramGroup.vue index 48e181dd08..8bf6f8486b 100644 --- a/app/web/src/components/ModelingDiagram/DiagramGroup.vue +++ b/app/web/src/components/ModelingDiagram/DiagramGroup.vue @@ -592,7 +592,7 @@ const colors = computed(() => { function onMouseOver(evt: KonvaEventObject, type?: string) { evt.cancelBubble = true; - componentsStore.setHoveredComponentId( + viewStore.setHoveredComponentId( componentId.value, type ? ({ type } as ElementHoverMeta) : undefined, ); @@ -603,40 +603,39 @@ function onResizeHover( evt: KonvaEventObject, ) { evt.cancelBubble = true; - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "resize", direction, }); } function onSocketHoverStart(socket: DiagramSocketData) { - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "socket", socket, }); } function onSocketHoverEnd(_socket: DiagramSocketData) { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } function onMouseOut() { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } function onClick(detailsTabSlug: string) { - componentsStore.setSelectedComponentId(componentId.value, { + viewStore.setSelectedComponentId(componentId.value, { detailsTab: detailsTabSlug, }); } const highlightParent = computed(() => { - if (!componentsStore.hoveredComponent) return false; - if (componentsStore.hoveredComponentMeta?.type !== "parent") return false; + if (!viewStore.hoveredComponent) return false; + if (viewStore.hoveredComponentMeta?.type !== "parent") return false; return ( - componentsStore.hoveredComponent.def.ancestorIds?.includes( - componentId.value, - ) || false + viewStore.hoveredComponent.def.ancestorIds?.includes(componentId.value) || + false ); }); @@ -699,16 +698,16 @@ const fixCursorToText = ref(false); const renameHovered = computed( () => - (componentsStore.hoveredComponentMeta?.type === "rename" && - componentsStore.hoveredComponentId === props.group.def.id) || + (viewStore.hoveredComponentMeta?.type === "rename" && + viewStore.hoveredComponentId === props.group.def.id) || renameHoverState.value, ); const selectedAndRenameHovered = computed( () => props.isSelected && - componentsStore.hoveredComponentMeta?.type === "rename" && - componentsStore.hoveredComponentId === props.group.def.id && + viewStore.hoveredComponentMeta?.type === "rename" && + viewStore.hoveredComponentId === props.group.def.id && renameHoverState.value, ); @@ -741,14 +740,14 @@ function renameIfSelected(e: KonvaEventObject) { rename(); } else if (fixCursorToText.value) { fixCursorToText.value = false; - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "rename", } as ElementHoverMeta); } } function rename() { - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "rename", }); renaming.value = true; diff --git a/app/web/src/components/ModelingDiagram/DiagramNode.vue b/app/web/src/components/ModelingDiagram/DiagramNode.vue index fdf457fbfd..0ff194e56f 100644 --- a/app/web/src/components/ModelingDiagram/DiagramNode.vue +++ b/app/web/src/components/ModelingDiagram/DiagramNode.vue @@ -451,29 +451,29 @@ watch([() => props.isLoading, overlay], () => { function onMouseOver(evt: KonvaEventObject, type?: string) { evt.cancelBubble = true; - componentsStore.setHoveredComponentId( + viewStore.setHoveredComponentId( componentId.value, type ? ({ type } as ElementHoverMeta) : undefined, ); } function onMouseOut() { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } function onSocketHoverStart(socket: DiagramSocketData) { - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "socket", socket, }); } function onSocketHoverEnd(_socket: DiagramSocketData) { - componentsStore.setHoveredComponentId(null); + viewStore.setHoveredComponentId(null); } function onClick(detailsTabSlug: string) { - componentsStore.setSelectedComponentId(componentId.value, { + viewStore.setSelectedComponentId(componentId.value, { detailsTab: detailsTabSlug, }); } @@ -513,16 +513,16 @@ const fixCursorToText = ref(false); const renameHovered = computed( () => - (componentsStore.hoveredComponentMeta?.type === "rename" && - componentsStore.hoveredComponentId === props.node.def.id) || + (viewStore.hoveredComponentMeta?.type === "rename" && + viewStore.hoveredComponentId === props.node.def.id) || renameHoverState.value, ); const selectedAndRenameHovered = computed( () => props.isSelected && - componentsStore.hoveredComponentMeta?.type === "rename" && - componentsStore.hoveredComponentId === props.node.def.id && + viewStore.hoveredComponentMeta?.type === "rename" && + viewStore.hoveredComponentId === props.node.def.id && renameHoverState.value, ); @@ -555,14 +555,14 @@ function renameIfSelected(e: KonvaEventObject) { rename(); } else if (fixCursorToText.value) { fixCursorToText.value = false; - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "rename", } as ElementHoverMeta); } } function rename() { - componentsStore.setHoveredComponentId(componentId.value, { + viewStore.setHoveredComponentId(componentId.value, { type: "rename", }); renaming.value = true; diff --git a/app/web/src/components/ModelingDiagram/DiagramView.vue b/app/web/src/components/ModelingDiagram/DiagramView.vue new file mode 100644 index 0000000000..71738f896d --- /dev/null +++ b/app/web/src/components/ModelingDiagram/DiagramView.vue @@ -0,0 +1,173 @@ + + + diff --git a/app/web/src/components/ModelingDiagram/ModelingDiagram.vue b/app/web/src/components/ModelingDiagram/ModelingDiagram.vue index 0a950b5fde..850a0b550a 100644 --- a/app/web/src/components/ModelingDiagram/ModelingDiagram.vue +++ b/app/web/src/components/ModelingDiagram/ModelingDiagram.vue @@ -117,6 +117,16 @@ overflow hidden */ " /> + c.def.canBeUpgraded, + const component = viewStore.selectedComponents + .filter( + (c): c is DiagramNodeData | DiagramGroupData => + !(c instanceof DiagramViewData), + ) + .pop(); + const containsUpgradeable = viewStore.selectedComponents.some( + (c) => "canBeUpgraded" in c.def && c.def.canBeUpgraded, ); if (containsUpgradeable) { toast("Components that can be upgraded cannot be copied"); @@ -775,7 +792,7 @@ async function onKeyDown(e: KeyboardEvent) { window.localStorage.setItem( CLIPBOARD_LOCALSTORAGE_KEY.value, JSON.stringify({ - componentIds: componentsStore.selectedComponentIds, + componentIds: viewStore.selectedComponentIds, copyingFrom: component.def.isGroup ? { ...viewStore.groups[component.def.id] } : { ...viewStore.components[component.def.id] }, @@ -787,7 +804,7 @@ async function onKeyDown(e: KeyboardEvent) { if (json !== null && json !== "null") { try { const { componentIds, copyingFrom } = JSON.parse(json); - componentsStore.selectedComponentIds = componentIds; + viewStore.selectedComponentIds = componentIds; componentsStore.copyingFrom = copyingFrom; triggerPasteElements(); } catch { @@ -832,16 +849,16 @@ async function onKeyDown(e: KeyboardEvent) { if ( !props.readOnly && e.key === "r" && - componentsStore.selectedComponent?.def.hasResource && + viewStore.selectedComponent?.def && + "hasResource" in viewStore.selectedComponent.def && + viewStore.selectedComponent?.def.hasResource && changeSetsStore.selectedChangeSetId === changeSetsStore.headChangeSetId ) { - componentsStore.REFRESH_RESOURCE_INFO( - componentsStore.selectedComponent.def.id, - ); + componentsStore.REFRESH_RESOURCE_INFO(viewStore.selectedComponent.def.id); } - if (!props.readOnly && e.key === "n" && componentsStore.selectedComponentId) { + if (!props.readOnly && e.key === "n" && viewStore.selectedComponentId) { e.preventDefault(); - renameOnDiagramByComponentId(componentsStore.selectedComponentId); + renameOnDiagramByComponentId(viewStore.selectedComponentId); } } @@ -895,6 +912,7 @@ function onMouseDown(ke: KonvaEventObject) { if (dragToPanArmed.value || e.button === 1) beginDragToPan(); else if (insertElementActive.value) triggerInsertElement(); else if (outlinerAddActive.value) triggerAddToView(); + else if (viewAddActive.value) triggerAddViewToView(); else if (pasteElementsActive.value) triggerPasteElements(); else handleMouseDownSelection(); } @@ -986,10 +1004,11 @@ function checkIfDragStarted(_e: MouseEvent) { } else if (props.readOnly) { // TODO: add controls for each of these modes... return; - } else if ("componentId" in lastMouseDownElement.value.def) { + } else if ("componentType" in lastMouseDownElement.value.def) { if (lastMouseDownHoverMeta.value?.type === "resize") { beginResizeElement(); } else if ( + "changeStatus" in lastMouseDownElement.value.def && lastMouseDownElement.value.def.changeStatus !== "deleted" && lastMouseDownHoverMeta.value?.type === "socket" ) { @@ -1097,12 +1116,10 @@ const hoveredElementKey = computed(() => { // dont recompute this while we're dragging if (dragElementsActive.value) return undefined; - if (componentsStore.hoveredComponentId) { - return getDiagramElementKeyForComponentId( - componentsStore.hoveredComponentId, - ); - } else if (componentsStore.hoveredEdgeId) { - return DiagramEdgeData.generateUniqueKey(componentsStore.hoveredEdgeId); + if (viewStore.hoveredComponentId) { + return getDiagramElementKeyForComponentId(viewStore.hoveredComponentId); + } else if (viewStore.hoveredEdgeId) { + return DiagramEdgeData.generateUniqueKey(viewStore.hoveredEdgeId); } return undefined; }); @@ -1129,7 +1146,7 @@ const hoveredElement = computed(() => { // NOTE - we'll receive 2 events when hovering sockets, one for the node and one for the socket // more detailed info about what inside an element is being hovered (like resize direction, socket, etc) -const hoveredElementMeta = computed(() => componentsStore.hoveredComponentMeta); +const hoveredElementMeta = computed(() => viewStore.hoveredComponentMeta); const disableHoverEvents = computed(() => { if (dragToPanArmed.value || dragToPanActive.value) return true; @@ -1221,14 +1238,17 @@ function panToComponent(payload: { // ELEMENT SELECTION ///////////////////////////////////////////////////////////////////////////////// const currentSelectionKeys = computed(() => { - if (componentsStore.selectedEdgeId) { - return _.compact([ - getDiagramElementKeyForEdgeId(componentsStore.selectedEdgeId), - ]); + if (viewStore.selectedEdgeId) { + return _.compact([getDiagramElementKeyForEdgeId(viewStore.selectedEdgeId)]); } else { return _.compact( - _.map(componentsStore.selectedComponentIds, (componentId) => { - const component = componentsStore.allComponentsById[componentId]; + _.map(viewStore.selectedComponentIds, (componentId) => { + let component: + | DiagramNodeData + | DiagramGroupData + | DiagramViewData + | undefined = componentsStore.allComponentsById[componentId]; + if (!component) component = viewStore.viewNodes[componentId]; return component?.uniqueKey; }), ); @@ -1246,7 +1266,7 @@ function setSelectionByKey( toSelect?: DiagramElementUniqueKey | DiagramElementUniqueKey[], ) { if (!toSelect || !toSelect.length) { - componentsStore.setSelectedComponentId(null); + viewStore.setSelectedComponentId(null); return; } @@ -1254,13 +1274,14 @@ function setSelectionByKey( // TODO: unsure if this edge check works if (els.length === 1 && els[0] instanceof DiagramEdgeData) { - componentsStore.setSelectedEdgeId(els[0].def.id); + viewStore.setSelectedEdgeId(els[0].def.id); } else { - componentsStore.setSelectedComponentId( - // TODO: remove this any... - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _.map(els, (e) => (e.def as any).componentId), - ); + const ids: string[] = []; + els.forEach((e) => { + if ("componentId" in e.def) ids.push(e.def.componentId); + else if ("componentType" in e.def) ids.push(e.def.id); // view + }); + viewStore.setSelectedComponentId(ids); } } @@ -1270,13 +1291,17 @@ function toggleSelectedByKey( ) { const els = _.compact(_.map(_.castArray(toToggle), getElementByKey)); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const elIds = _.map(els, (el) => (el.def as any).componentId); + const elIds: string[] = []; + els.forEach((el) => { + if ("componentId" in el.def) elIds.push(el.def.componentId); + else if ("componentType" in el.def) elIds.push(el.def.id); // view + }); // second true enables "toggle" mode - componentsStore.setSelectedComponentId(elIds, { toggle: true }); + viewStore.setSelectedComponentId(elIds, { toggle: true }); } function clearSelection() { - componentsStore.setSelectedComponentId(null); + viewStore.setSelectedComponentId(null); } function elementIsHovered(el: DiagramElementData) { @@ -1307,7 +1332,7 @@ function handleMouseDownSelection() { // nodes can be multi-selected, so we have some extra behaviour // TODO: other elements may also share this behaviour - if (hoveredElement.value && "componentId" in hoveredElement.value.def) { + if (hoveredElement.value && hoveredElement.value.def) { // when clicking on an element that is NOT currently selected, we act right away // but if the element IS selected, this could be beginning of dragging // so we handle selection on mouseup if the user never fully started to drag @@ -1381,6 +1406,14 @@ function endDragSelect(doSelection = true) { if (inSelectionBox) selectedInBoxKeys.push(DiagramNodeData.generateUniqueKey(nodeKey)); }); + _.each(viewStore.viewNodes, (node, nodeKey) => { + const inSelectionBox = checkRectanglesOverlap( + pointsToRect(dragSelectStartPos.value!, dragSelectEndPos.value!), + node.def, + ); + if (inSelectionBox) + selectedInBoxKeys.push(DiagramViewData.generateUniqueKey(nodeKey)); + }); // if holding shift key, we'll add/toggle the existing selection with what's in the box // NOTE - weird edge cases around what if you let go of shift after beginning the drag which we are ignoring if (lastMouseDownEvent.value?.shiftKey) { @@ -1424,19 +1457,20 @@ const currentSelectionMovableElements = computed(() => { // filter selection for nodes and groups const elements = _.filter( currentSelectionElements.value, - (el) => el && "componentId" in el.def, - ) as unknown as (DiagramNodeData | DiagramGroupData)[]; + (el) => el && "componentType" in el.def, + ) as unknown as (DiagramNodeData | DiagramGroupData | DiagramViewData)[]; // cannot move elements that are actually gone already return elements.filter((e) => { - if (e.def.changeStatus === "deleted") return false; + if ("changeStatus" in e.def && e.def.changeStatus === "deleted") + return false; return true; }); }); const findChildrenByBoundingBox = ( el: DiagramNodeData | DiagramGroupData, -): (DiagramNodeData | DiagramGroupData)[] => { +): (DiagramNodeData | DiagramGroupData | DiagramViewData)[] => { const cRect = el.def.isGroup ? viewStore.groups[el.def.id] : viewStore.components[el.def.id]; @@ -1445,7 +1479,7 @@ const findChildrenByBoundingBox = ( const rect = { ...cRect }; rect.x -= rect.width / 2; - const components: (DiagramGroupData | DiagramNodeData)[] = []; + const nodes: (DiagramGroupData | DiagramNodeData | DiagramViewData)[] = []; const process = ([id, elRect]: [ComponentId, IRect]) => { // i do not fit inside myself if (el.def.id === id) return; @@ -1453,13 +1487,26 @@ const findChildrenByBoundingBox = ( _r.x -= _r.width / 2; if (rectContainsAnother(rect, _r)) { const component = componentsStore.allComponentsById[id]; - if (component) components.push(component); + if (component) nodes.push(component); } }; Object.entries(viewStore.groups).forEach(process); Object.entries(viewStore.components).forEach(process); - return components; + Object.values(viewStore.viewNodes).forEach((viewNode) => { + const _r = { + x: viewNode.def.x, + y: viewNode.def.y, + width: viewNode.def.width, + height: viewNode.def.height, + }; + _r.x -= _r.width / 2; + _r.y -= _r.height / 2; + if (rectContainsAnother(rect, _r)) { + nodes.push(viewNode); + } + }); + return nodes; }; const draggedElementsPositionsPreDrag = ref< @@ -1467,17 +1514,24 @@ const draggedElementsPositionsPreDrag = ref< >({}); const edgeScrolledDuringDrag = ref({ x: 0, y: 0 }); -const draggedChildren = ref<(DiagramNodeData | DiagramGroupData)[]>([]); +const draggedChildren = ref< + (DiagramNodeData | DiagramGroupData | DiagramViewData)[] +>([]); function beginDragElements() { if (!lastMouseDownElement.value) return; dragElementsActive.value = true; edgeScrolledDuringDrag.value = { x: 0, y: 0 }; - const children: Set = new Set(); + const children: Set = + new Set(); currentSelectionMovableElements.value.forEach((el) => { - const childs = findChildrenByBoundingBox(el); - childs.forEach((c) => children.add(c)); + if (el.def.componentType !== ComponentType.View) { + const childs = findChildrenByBoundingBox( + el as DiagramNodeData | DiagramGroupData, + ); + childs.forEach((c) => children.add(c)); + } }); draggedChildren.value = [...children]; @@ -1485,9 +1539,7 @@ function beginDragElements() { draggedElementsPositionsPreDrag.value = currentSelectionMovableElements.value .concat(draggedChildren.value) .reduce((obj, el) => { - const geo = el.def.isGroup - ? viewStore.groups[el.def.id] - : viewStore.components[el.def.id]; + const geo = viewStore.geoFrom(el); if (geo) obj[el.uniqueKey] = { ...geo }; return obj; @@ -1537,9 +1589,7 @@ function onDragElementsMove() { // if we are going to move the element within a new parent we may need to adjust // the position to stay inside of it const parentRect = viewStore.groups[parentOrCandidate.def.id]; - const elRect = el.def.isGroup - ? viewStore.groups[el.def.id] - : viewStore.components[el.def.id]; + const elRect = viewStore.geoFrom(el); if (!parentRect || !elRect) return; const movedElRect = { x: newPosition.x - elRect.width / 2, @@ -1579,15 +1629,25 @@ function onDragElementsMove() { const selectionIds = currentSelectionMovableElements.value.map( (s) => s.def.id, ); - viewStore.MOVE_COMPONENTS( - [ - ...currentSelectionMovableElements.value.concat( - draggedChildren.value.filter((c) => !selectionIds.includes(c.def.id)), - ), - ], - deltaFromLast, - { broadcastToClients: true }, - ); + const _components: (DiagramGroupData | DiagramNodeData)[] = []; + const _views: DiagramViewData[] = []; + [ + ...currentSelectionMovableElements.value.concat( + draggedChildren.value.filter((c) => !selectionIds.includes(c.def.id)), + ), + ].forEach((c) => { + if (c.def.componentType === ComponentType.View) + _views.push(c as DiagramViewData); + else _components.push(c as DiagramGroupData | DiagramNodeData); + }); + if (_components.length > 0) + viewStore.MOVE_COMPONENTS(_components, deltaFromLast, { + broadcastToClients: true, + }); + if (_views.length > 0) + viewStore.MOVE_VIEWS(_views, deltaFromLast, { + broadcastToClients: true, + }); checkDiagramEdgeForScroll(); } @@ -1603,6 +1663,13 @@ function endDragElements() { draggedChildren.value.filter((c) => !selectionIds.includes(c.def.id)), ), ]; + const _components: (DiagramGroupData | DiagramNodeData)[] = []; + const _views: DiagramViewData[] = []; + movedComponents.forEach((c) => { + if (c.def.componentType === ComponentType.View) + _views.push(c as DiagramViewData); + else _components.push(c as DiagramGroupData | DiagramNodeData); + }); const detach = !cursorWithinGroupKey.value; let newParent: DiagramGroupData | undefined; @@ -1624,6 +1691,7 @@ function endDragElements() { const setParents: ComponentId[] = []; nonChildElements.forEach((component) => { + if (!("parentId" in component.def)) return; // if their current parent is NOT in this view, do not re-parent!!! if ( component.def.parentId && @@ -1648,11 +1716,14 @@ function endDragElements() { viewStore.SET_PARENT(setParents, newParent?.def.id ?? null); } - viewStore.MOVE_COMPONENTS( - movedComponents, - { x: 0, y: 0 }, - { writeToChangeSet: true }, - ); + if (_components.length > 0) + viewStore.MOVE_COMPONENTS( + _components, + { x: 0, y: 0 }, + { writeToChangeSet: true }, + ); + if (_views.length > 0) + viewStore.MOVE_VIEWS(_views, { x: 0, y: 0 }, { writeToChangeSet: true }); draggedChildren.value = []; } @@ -1743,9 +1814,7 @@ function alignSelection(direction: Direction) { let alignedX: number | undefined; let alignedY: number | undefined; const positions = _.map(currentSelectionMovableElements.value, (el) => - el.def.isGroup - ? viewStore.groups[el.def.id] - : viewStore.components[el.def.id], + viewStore.geoFrom(el), ).filter(nonNullable); const xPositions = _.map(positions, (p) => p.x); const yPositions = _.map(positions, (p) => p.y); @@ -1754,15 +1823,30 @@ function alignSelection(direction: Direction) { else if (direction === "left") alignedX = _.min(xPositions); else if (direction === "right") alignedX = _.max(xPositions); - viewStore.MOVE_COMPONENTS( - currentSelectionMovableElements.value, - { x: alignedX ?? 0, y: alignedY ?? 0 }, - { writeToChangeSet: true }, - ); + const _components: (DiagramGroupData | DiagramNodeData)[] = []; + const _views: DiagramViewData[] = []; + currentSelectionMovableElements.value.forEach((c) => { + if (c.def.componentType === ComponentType.View) + _views.push(c as DiagramViewData); + else _components.push(c as DiagramGroupData | DiagramNodeData); + }); + if (_components.length) + viewStore.MOVE_COMPONENTS( + _components, + { x: alignedX ?? 0, y: alignedY ?? 0 }, + { writeToChangeSet: true }, + ); + if (_views.length) + viewStore.MOVE_VIEWS( + _views, + { x: alignedX ?? 0, y: alignedY ?? 0 }, + { writeToChangeSet: true }, + ); } type VoidFn = () => void; let debouncedNudgeFn: _.DebouncedFunc | null; +let debouncedNudgeFnViews: _.DebouncedFunc | null; function nudgeSelection(direction: Direction, largeNudge: boolean) { if (!currentSelectionMovableElements.value.length) return; const nudgeSize = largeNudge ? 10 : 1; @@ -1773,22 +1857,41 @@ function nudgeSelection(direction: Direction, largeNudge: boolean) { down: { x: 0, y: 1 * nudgeSize }, }[direction]; - viewStore.MOVE_COMPONENTS( - currentSelectionMovableElements.value, - nudgeVector, - { broadcastToClients: true }, - ); - if (!debouncedNudgeFn) { + const _components: (DiagramGroupData | DiagramNodeData)[] = []; + const _views: DiagramViewData[] = []; + currentSelectionMovableElements.value.forEach((c) => { + if (c.def.componentType === ComponentType.View) + _views.push(c as DiagramViewData); + else _components.push(c as DiagramGroupData | DiagramNodeData); + }); + + if (_components.length > 0) + viewStore.MOVE_COMPONENTS(_components, nudgeVector, { + broadcastToClients: true, + }); + if (!debouncedNudgeFn && _components.length > 0) { debouncedNudgeFn = _.debounce(() => { viewStore.MOVE_COMPONENTS( - currentSelectionMovableElements.value, + _components, { x: 0, y: 0 }, { writeToChangeSet: true }, ); debouncedNudgeFn = null; }, 300); + debouncedNudgeFn(); + } + + if (_views.length > 0) + viewStore.MOVE_VIEWS(_views, nudgeVector, { + broadcastToClients: true, + }); + if (!debouncedNudgeFnViews && _views.length > 0) { + debouncedNudgeFnViews = _.debounce(() => { + viewStore.MOVE_VIEWS(_views, { x: 0, y: 0 }, { writeToChangeSet: true }); + debouncedNudgeFn = null; + }, 300); + debouncedNudgeFnViews(); } - debouncedNudgeFn(); } // we calculate which group (if any) the cursor is within without using hover events @@ -2326,15 +2429,14 @@ async function endDrawEdge() { const pasteElementsActive = computed(() => { return ( - componentsStore.copyingFrom && - componentsStore.selectedComponentIds.length > 0 + componentsStore.copyingFrom && viewStore.selectedComponentIds.length > 0 ); }); // TODO: I dont think we need to compute this // we can do the work directly in the paste function const currentSelectionEnclosure: Ref = computed(() => { - const componentIds = componentsStore.selectedComponentIds; + const componentIds = viewStore.selectedComponentIds; if (componentIds.length === 0) return; @@ -2458,7 +2560,7 @@ async function triggerPasteElements() { selectionOffset.y -= fitOffset.y; } - const pasteTargets = _.map(componentsStore.selectedComponentIds, (id) => { + const pasteTargets = _.map(viewStore.selectedComponentIds, (id) => { const thisGeometry = viewStore.components[id] || viewStore.groups[id]; if (!thisGeometry) throw new Error("Rendered Component not found"); @@ -2507,6 +2609,7 @@ const insertElementActive = computed( ); const outlinerAddActive = computed(() => !!viewStore.addComponentId); +const viewAddActive = computed(() => !!viewStore.addViewId); const HEADER_SIZE = 60; // The height of the component header bar; TODO find a better way to detect this function fitChildInsideParentFrame( @@ -2537,6 +2640,20 @@ function fitChildInsideParentFrame( return [createAtPosition, createAtSize]; } +async function triggerAddViewToView() { + if (!viewAddActive.value || !viewStore.addViewId) + throw new Error("insert element mode must be active"); + if (!gridPointerPos.value) + throw new Error("Cursor must be in grid to insert element"); + + const addingViewId = viewStore.addViewId; + viewStore.addViewId = null; + + const geo = { ...gridPointerPos.value, radius: 250 }; + + viewStore.ADD_VIEW_TO(viewStore.selectedViewId!, addingViewId, geo); +} + async function triggerAddToView() { if (!outlinerAddActive.value) throw new Error("insert element mode must be active"); @@ -2678,12 +2795,12 @@ const groups = computed(() => { // TODO change this to being position comparisons not parentage if ( dragElementsActive.value || - componentsStore.selectedComponentIds.length > 0 + viewStore.selectedComponentIds.length > 0 ) { if ( _.intersection( [g.def.componentId, ...(g.def.ancestorIds || [])], - componentsStore.selectedComponentIds, + viewStore.selectedComponentIds, ).length ) { zIndex += 1000; @@ -2704,7 +2821,13 @@ const sockets = computed(() => { // this will re-compute on every drag until all the position data is removed const allElementsByKey = computed(() => _.keyBy( - [...nodes.value, ...groups.value, ...sockets.value, ...viewStore.edges], + [ + ...nodes.value, + ...groups.value, + ...sockets.value, + ...viewStore.edges, + ...Object.values(viewStore.viewNodes), + ], (e) => e.uniqueKey, ), ); @@ -2717,24 +2840,38 @@ function getElementByKey(key?: DiagramElementUniqueKey) { const selectionRects = computed(() => { const rects = [] as (Size2D & Vector2d)[]; currentSelectionKeys.value.forEach((uniqueKey) => { + const isView = uniqueKey.startsWith("v-"); const isGroup = uniqueKey.startsWith("g-"); const id = uniqueKey.slice(2); // remove the prefix - const rect = viewStore.components[id] || viewStore.groups[id]; - if (rect) { - const r = { - x: rect.x - rect.width / 2, - y: rect.y, - width: rect.width, - height: rect.height, - }; - if (isGroup) { - // deal with top bar height outside the component's - // designated height - const adjust = 28 + GROUP_HEADER_BOTTOM_MARGIN * 2; - r.height += adjust; - r.y -= adjust; + if (isView) { + const rect = viewStore.viewNodes[id]?.def; + if (rect) { + const r = { + x: rect.x - rect.width / 2, + y: rect.y - rect.height / 2, + width: rect.width, + height: rect.height, + }; + rects.push(r); + } + } else { + const rect = viewStore.components[id] || viewStore.groups[id]; + if (rect) { + const r = { + x: rect.x - rect.width / 2, + y: rect.y, + width: rect.width, + height: rect.height, + }; + if (isGroup) { + // deal with top bar height outside the component's + // designated height + const adjust = 28 + GROUP_HEADER_BOTTOM_MARGIN * 2; + r.height += adjust; + r.y -= adjust; + } + rects.push(r); } - rects.push(r); } }); return rects; @@ -2744,7 +2881,12 @@ function getDiagramElementKeyForComponentId( componentId?: ComponentId | null, ): string | undefined { if (!componentId) return; - const component = componentsStore.allComponentsById[componentId]; + let component: + | DiagramNodeData + | DiagramGroupData + | DiagramViewData + | undefined = componentsStore.allComponentsById[componentId]; + if (!component) component = viewStore.viewNodes[componentId]; return component?.uniqueKey; } diff --git a/app/web/src/components/ModelingDiagram/diagram_types.ts b/app/web/src/components/ModelingDiagram/diagram_types.ts index 73c29780fe..83ce05218a 100644 --- a/app/web/src/components/ModelingDiagram/diagram_types.ts +++ b/app/web/src/components/ModelingDiagram/diagram_types.ts @@ -6,6 +6,7 @@ import { useComponentsStore } from "@/store/components.store"; import { ChangeStatus } from "@/api/sdf/dal/change_set"; import { ActorAndTimestamp, ComponentId } from "@/api/sdf/dal/component"; import { ComponentType } from "@/api/sdf/dal/schema"; +import { ViewNode } from "@/api/sdf/dal/views"; import { GROUP_BOTTOM_INTERNAL_PADDING, NODE_HEADER_HEIGHT, @@ -34,7 +35,11 @@ export type SideAndCornerIdentifiers = export type DiagramElementUniqueKey = string; export abstract class DiagramElementData { - abstract get def(): DiagramNodeDef | DiagramSocketDef | DiagramEdgeDef; + abstract get def(): + | DiagramNodeDef + | DiagramSocketDef + | DiagramEdgeDef + | DiagramViewDef; abstract get uniqueKey(): DiagramElementUniqueKey; } @@ -187,6 +192,24 @@ export class DiagramGroupData extends DiagramNodeHasSockets { } } +export class DiagramViewData extends DiagramElementData { + constructor(readonly def: DiagramViewDef) { + super(); + } + + get uniqueKey() { + return `v-${this.def.id}`; + } + + static generateUniqueKey(id: string | number) { + return `v-${id}`; + } + + static componentIdFromUniqueKey(uniqueKey: string): string { + return uniqueKey.replace("v-", ""); + } +} + export class DiagramSocketData extends DiagramElementData { position?: Vector2d; @@ -376,6 +399,12 @@ export type DiagramSocketDef = { // shape }; +export type DiagramViewDef = ViewNode & { + icon: IconNames; + color: string; + schemaName: string; +}; + export type DiagramEdgeDef = { id: DiagramElementId; type?: string; diff --git a/app/web/src/components/ModelingView/DeleteSelectionModal.vue b/app/web/src/components/ModelingView/DeleteSelectionModal.vue index 4f2431a040..ba620f54b8 100644 --- a/app/web/src/components/ModelingView/DeleteSelectionModal.vue +++ b/app/web/src/components/ModelingView/DeleteSelectionModal.vue @@ -5,6 +5,30 @@

You're about to delete the following edge:

+ -