Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENG-2871: move components to a new view and replace with a hexagon #5065

Merged
merged 1 commit into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 73 additions & 13 deletions app/web/src/components/ModelingView/ModelingRightClickMenu.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
<template>
<DropdownMenu
v-if="selectedEdge"
ref="contextMenuRef"
:items="rightClickMenuItemsEdge"
variant="editor"
/>
<DropdownMenu
v-else
ref="contextMenuRef"
:items="rightClickMenuItems"
variant="editor"
/>
<div>
<DropdownMenu
v-if="selectedEdge"
ref="contextMenuRef"
:items="rightClickMenuItemsEdge"
variant="editor"
/>
<DropdownMenu
v-else
ref="contextMenuRef"
:items="rightClickMenuItems"
variant="editor"
/>
<Modal
ref="modalRef"
type="save"
size="sm"
saveLabel="Create"
title="Create View"
@save="create"
>
<VormInput
ref="labelRef"
v-model="viewName"
required
label="View Name"
@enterPressed="create"
/>
</Modal>
</div>
</template>

<script lang="ts" setup>
import * as _ from "lodash-es";
import {
DropdownMenu,
DropdownMenuItemObjectDef,
Modal,
VormInput,
} from "@si/vue-lib/design-system";
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
Expand Down Expand Up @@ -197,6 +217,43 @@ const anyViews = computed(() =>
selectedComponents.value.some((c) => c instanceof DiagramViewData),
);

const modalRef = ref<InstanceType<typeof Modal>>();
const labelRef = ref<InstanceType<typeof VormInput>>();
const viewName = ref("");
const newView = () => {
modalRef.value?.open();
};

const create = async () => {
if (!viewStore.selectedViewId) return;
if (!viewName.value) {
labelRef.value?.setError("Name is required");
} else {
const components: Record<ComponentId, IRect> = {};
selectedComponents.value.forEach((component) => {
const geo =
component.def.componentType === ComponentType.Component
? viewStore.components[component.def.id]
: viewStore.groups[component.def.id];
if (geo) components[component.def.id] = geo;
});
const resp = await viewStore.CREATE_VIEW_AND_MOVE(
viewName.value,
viewStore.selectedViewId,
components,
);
if (resp.result.success) {
modalRef.value?.close();

viewName.value = "";
} else if (resp.result.statusCode === 409) {
labelRef.value?.setError(
`${viewName.value} is already in use. Please choose another name`,
);
}
}
};

/**
* HERE IS THE APPROACH IN GENERAL
* Make sure every "action" (i.e. onSelect) operates on the whole list of selectedComponents
Expand All @@ -219,7 +276,10 @@ const rightClickMenuItems = computed(() => {
items.push({
label: "Move to",
icon: "arrows-out",
submenuItems: viewsSubitems(viewAdd(true)),
submenuItems: viewsSubitems(viewAdd(true)).concat({
label: "Create new View ...",
onSelect: newView,
}),
});
items.push({
label: "Copy to",
Expand Down
40 changes: 40 additions & 0 deletions app/web/src/store/views.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,46 @@ export const useViewsStore = (forceChangeSetId?: ChangeSetId) => {
},
});
},
async CREATE_VIEW_AND_MOVE(
name: string,
sourceViewId: ViewId,
components: Record<ComponentId, IRect>,
) {
let sumX = 0;
let sumY = 0;
const stringGeometries: Record<ComponentId, StringGeometry> = {};
Object.entries(components).forEach(([componentId, geo]) => {
// sum the center coordinates separately
sumX += geo.x;
sumY += geo.y + geo.height / 2;
stringGeometries[componentId] = {
x: Math.round(geo.x).toString(),
y: Math.round(geo.y).toString(),
width: Math.round(geo.width).toString(),
height: Math.round(geo.height).toString(),
};
});
const viewToHexagonGeo = {
x: Math.round(
sumX / Object.keys(stringGeometries).length,
).toString(),
y: Math.round(
sumY / Object.keys(stringGeometries).length,
).toString(),
radius: "250",
};
return new ApiRequest({
method: "post",
url: API_PREFIX.concat(["create_and_move"]),
params: {
name,
sourceViewId,
geometriesByComponentId: stringGeometries,
removeFromOriginalView: true,
placeViewAt: viewToHexagonGeo,
},
});
},
},
async onActivated() {
if (!changeSetId) return;
Expand Down
5 changes: 5 additions & 0 deletions lib/sdf-server/src/service/v2/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use tokio::task::JoinError;

pub mod create_component;
pub mod create_view;
pub mod create_view_and_move;
mod create_view_object;
mod erase_components;
mod erase_view_object;
Expand Down Expand Up @@ -104,6 +105,10 @@ pub fn v2_routes() -> Router<AppState> {
// Func Stuff
.route("/", get(list_views::list_views))
.route("/", post(create_view::create_view))
.route(
"/create_and_move",
post(create_view_and_move::create_view_and_move),
)
.route("/:view_id", put(update_view::update_view))
.route("/:view_id/get_diagram", get(get_diagram::get_diagram))
.route("/:view_id/get_geometry", get(get_diagram::get_geometry))
Expand Down
157 changes: 157 additions & 0 deletions lib/sdf-server/src/service/v2/view/create_view_and_move.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use std::collections::HashMap;

use crate::extract::{AccessBuilder, HandlerContext, PosthogClient};
use crate::service::force_change_set_response::ForceChangeSetResponse;
use crate::service::v2::view::{ViewError, ViewResult};
use crate::tracking::track;
use axum::extract::{Host, OriginalUri, Path};
use axum::Json;
use dal::diagram::geometry::Geometry;
use dal::diagram::view::{View, ViewComponentsUpdateList, ViewId, ViewView};
use dal::{ChangeSet, ChangeSetId, Component, ComponentError, ComponentId, WorkspacePk, WsEvent};
use serde::{Deserialize, Serialize};
use si_frontend_types::{RawGeometry, StringGeometry};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ViewNodeGeometry {
pub x: String,
pub y: String,
pub radius: String,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Request {
pub name: String,
pub source_view_id: ViewId,
pub geometries_by_component_id: HashMap<ComponentId, StringGeometry>,
pub remove_from_original_view: bool,
pub place_view_at: ViewNodeGeometry,
}

pub async fn create_view_and_move(
HandlerContext(builder): HandlerContext,
AccessBuilder(access_builder): AccessBuilder,
PosthogClient(posthog_client): PosthogClient,
OriginalUri(original_uri): OriginalUri,
Host(host_name): Host,
Path((_workspace_pk, change_set_id)): Path<(WorkspacePk, ChangeSetId)>,
Json(Request {
name,
source_view_id,
geometries_by_component_id,
remove_from_original_view,
place_view_at,
}): Json<Request>,
) -> ViewResult<ForceChangeSetResponse<ViewView>> {
let mut ctx = builder
.build(access_builder.build(change_set_id.into()))
.await?;

if View::find_by_name(&ctx, name.as_str()).await?.is_some() {
return Err(ViewError::NameAlreadyInUse(name));
}

let force_change_set_id = ChangeSet::force_new(&mut ctx).await?;

let view = View::new(&ctx, name.clone()).await?;
let view_id = view.id();

let view_view = ViewView::from_view(&ctx, view).await?;

WsEvent::view_created(&ctx, view_view.clone())
.await?
.publish_on_commit(&ctx)
.await?;

let mut updated_components = ViewComponentsUpdateList::new();

let mut successful_erase = false;
let mut latest_error = None;
for (component_id, string_geometry) in geometries_by_component_id.clone() {
let geometry: RawGeometry = string_geometry.try_into()?;

match Component::add_to_view(&ctx, component_id, view_id, geometry.clone()).await {
Ok(_) => {}
Err(err @ ComponentError::ComponentAlreadyInView(_, _)) => {
latest_error = Some(err);
continue;
}
Err(err) => return Err(err)?,
};

successful_erase = true;

updated_components
.entry(view_id)
.or_default()
.added
.insert(component_id.into(), geometry);

if remove_from_original_view {
let old_geometry =
Geometry::get_by_component_and_view(&ctx, component_id, source_view_id).await?;

updated_components
.entry(source_view_id)
.or_default()
.removed
.insert(component_id.into());

Geometry::remove(&ctx, old_geometry.id()).await?
}
}

if let Some(err) = latest_error {
if !successful_erase {
return Err(err)?;
}
}

WsEvent::view_components_update(&ctx, updated_components)
.await?
.publish_on_commit(&ctx)
.await?;

let (Ok(x), Ok(y), Ok(radius)) = (
place_view_at.x.clone().parse::<isize>(),
place_view_at.y.clone().parse::<isize>(),
place_view_at.radius.clone().parse::<isize>(),
) else {
ctx.rollback().await?;
return Err(ViewError::InvalidRequest(
"geometry unable to be parsed from create view object request".into(),
));
};

let geometry = RawGeometry {
x,
y,
width: Some(radius),
height: Some(radius),
};
View::add_to_another_view(&ctx, view_id, source_view_id, geometry.clone()).await?;

WsEvent::view_object_created(&ctx, source_view_id, view_id, geometry)
.await?
.publish_on_commit(&ctx)
.await?;

track(
&posthog_client,
&ctx,
&original_uri,
&host_name,
"create_view",
serde_json::json!({
"how": "/diagram/create_view_and_move",
"view_id": view_id,
"view_name": name,
"change_set_id": ctx.change_set_id(),
}),
);

ctx.commit().await?;

Ok(ForceChangeSetResponse::new(force_change_set_id, view_view))
}
Loading