Skip to content

Commit

Permalink
Initial copy-paste implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
paulocsanz committed Dec 18, 2023
1 parent e5ca95f commit d101386
Show file tree
Hide file tree
Showing 12 changed files with 572 additions and 21 deletions.
10 changes: 8 additions & 2 deletions app/web/src/components/ComponentOutline/ComponentOutline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ const emit = defineEmits<{
// while we've avoided events for most things (selection, panning, etc)
// we still have an emit for this one because the parent (WorkspaceModelAndView) owns the right click menu
// and needs the raw MouseEvent
(e: "right-click-item", ev: MouseEvent): void;
(
e: "right-click-item",
ev: { mouse: MouseEvent; component: FullComponent },
): void;
}>();
const componentsStore = useComponentsStore();
Expand Down Expand Up @@ -168,6 +171,9 @@ function onSearchUpdated(newFilterString: string) {
}
function itemClickHandler(e: MouseEvent, id: ComponentId, tabSlug?: string) {
const component = componentsStore.componentsById[id];
if (!component) throw new Error("component not found");
const shiftKeyBehavior = () => {
const selectedComponentIds = componentsStore.selectedComponentIds;
Expand Down Expand Up @@ -223,7 +229,7 @@ function itemClickHandler(e: MouseEvent, id: ComponentId, tabSlug?: string) {
componentsStore.setSelectedComponentId(id);
}
}
emit("right-click-item", e);
emit("right-click-item", { mouse: e, component });
} else if (e.shiftKey) {
e.preventDefault();
shiftKeyBehavior();
Expand Down
27 changes: 27 additions & 0 deletions app/web/src/components/ModelingDiagram/ModelingDiagram.vue
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
clearSelection();
if (insertElementActive.value) componentsStore.cancelInsert();
componentsStore.copyingFrom = null;
if (dragSelectActive.value) endDragSelect(false);
}
if (!props.readOnly && (e.key === "Delete" || e.key === "Backspace")) {
Expand Down Expand Up @@ -622,6 +623,7 @@ function onMouseDown(ke: KonvaEventObject<MouseEvent>) {
// in order to ignore clicks with a tiny bit of movement
if (dragToPanArmed.value || e.button === 1) beginDragToPan();
else if (insertElementActive.value) triggerInsertElement();
else if (pasteElementsActive.value) triggerPasteElements();
else handleMouseDownSelection();
}

Expand All @@ -639,6 +641,8 @@ function onMouseUp(e: MouseEvent) {
// TODO: probably change this - its a bit hacky...
else if (insertElementActive.value && pointerIsWithinGrid.value)
triggerInsertElement();
else if (pasteElementsActive.value && pointerIsWithinGrid.value)
triggerPasteElements();
else handleMouseUpSelection();
}

Expand Down Expand Up @@ -779,6 +783,7 @@ const cursor = computed(() => {
if (drawEdgeActive.value) return "cell";
if (dragElementsActive.value) return "move";
if (insertElementActive.value) return "copy"; // not sure about this...
if (pasteElementsActive.value) return "copy";
if (
resizeElementActive.value ||
hoveredElementMeta.value?.type === "resize"
Expand Down Expand Up @@ -2061,6 +2066,27 @@ async function endDrawEdge() {
}
}
const pasteElementsActive = computed(() => {
return (
componentsStore.copyingFrom &&
componentsStore.selectedComponentIds.length > 0
);
});
async function triggerPasteElements() {
if (!pasteElementsActive.value)
throw new Error("paste element mode must be active");
if (!gridPointerPos.value)
throw new Error("Cursor must be in grid to paste element");
if (!componentsStore.copyingFrom)
throw new Error("Copy cursor must be in grid to paste element");
componentsStore.PASTE_COMPONENTS(componentsStore.selectedComponentIds, {
x: gridPointerPos.value.x - componentsStore.copyingFrom.x,
y: gridPointerPos.value.y - componentsStore.copyingFrom.y,
});
componentsStore.copyingFrom = null;
}
// ELEMENT ADDITION
const insertElementActive = computed(
() => !!componentsStore.selectedInsertSchemaId,
Expand Down Expand Up @@ -2260,6 +2286,7 @@ function recenterOnElement(panTarget: DiagramElementData) {
const helpModalRef = ref();
onMounted(() => {
componentsStore.copyingFrom = null;
componentsStore.eventBus.on("panToComponent", panToComponent);
});
onBeforeUnmount(() => {
Expand Down
35 changes: 34 additions & 1 deletion app/web/src/components/ModelingView/ModelingRightClickMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { computed, ref } from "vue";
import plur from "plur";
import { useComponentsStore } from "@/store/components.store";
import { useFixesStore } from "@/store/fixes.store";
import { useFeatureFlagsStore } from "@/store/feature_flags.store";
const contextMenuRef = ref<InstanceType<typeof DropdownMenu>>();
const componentsStore = useComponentsStore();
const fixesStore = useFixesStore();
const featureFlagsStore = useFeatureFlagsStore();
const {
selectedComponentId,
Expand Down Expand Up @@ -69,6 +71,15 @@ const rightClickMenuItems = computed(() => {
});
}
} else if (selectedComponentId.value && selectedComponent.value) {
if (featureFlagsStore.COPY_PASTE) {
items.push({
label: `Copy`,
icon: "clipboard-copy",
onSelect: triggerCopySelection,
disabled,
});
}
// single selected component
if (selectedComponent.value.changeStatus === "deleted") {
items.push({
Expand All @@ -90,6 +101,15 @@ const rightClickMenuItems = computed(() => {
});
}
} else if (selectedComponentIds.value.length) {
if (featureFlagsStore.COPY_PASTE) {
items.push({
label: `Copy ${selectedComponentIds.value.length} Components`,
icon: "clipboard-copy",
onSelect: triggerCopySelection,
disabled,
});
}
// Multiple selected components
if (deletableSelectedComponents.value.length > 0) {
items.push({
Expand Down Expand Up @@ -129,15 +149,28 @@ const rightClickMenuItems = computed(() => {
return items;
});
function triggerCopySelection() {
componentsStore.copyingFrom = elementPos.value;
elementPos.value = null;
}
const modelingEventBus = componentsStore.eventBus;
function triggerDeleteSelection() {
modelingEventBus.emit("deleteSelection");
elementPos.value = null;
}
function triggerRestoreSelection() {
modelingEventBus.emit("restoreSelection");
elementPos.value = null;
}
function open(e?: MouseEvent, anchorToMouse?: boolean) {
const elementPos = ref<{ x: number; y: number } | null>(null);
function open(
e: MouseEvent,
anchorToMouse: boolean,
elementPosition?: { x: number; y: number },
) {
if (elementPosition) elementPos.value = elementPosition;
contextMenuRef.value?.open(e, anchorToMouse);
}
defineExpose({ open });
Expand Down
21 changes: 17 additions & 4 deletions app/web/src/components/Workspace/WorkspaceModelAndView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import * as _ from "lodash-es";
import { computed, onMounted, ref } from "vue";
import { ResizablePanel } from "@si/vue-lib/design-system";
import ComponentDetails from "@/components/ComponentDetails.vue";
import { useComponentsStore } from "@/store/components.store";
import { useComponentsStore, FullComponent } from "@/store/components.store";
import { useFixesStore } from "@/store/fixes.store";
import { useChangeSetsStore } from "@/store/change_sets.store";
import FixProgressOverlay from "@/components/FixProgressOverlay.vue";
Expand Down Expand Up @@ -130,11 +130,24 @@ const selectedComponent = computed(() => componentsStore.selectedComponent);
// });
// });
// Nodes that are not resizable have dynamic height based on its rendering objects, we cannot infer that here and honestly it's not a big deal
// So let's hardcode something reasonable that doesn't make the user too much confused when they paste a copy
const NODE_HEIGHT = 200;
function onRightClickElement(rightClickEventInfo: RightClickElementEvent) {
contextMenuRef.value?.open(rightClickEventInfo.e, true);
let position;
if ("position" in rightClickEventInfo.element.def) {
position = _.cloneDeep(rightClickEventInfo.element.def.position);
position.y +=
(rightClickEventInfo.element.def.size?.height ?? NODE_HEIGHT) / 2;
}
contextMenuRef.value?.open(rightClickEventInfo.e, true, position);
}
function onOutlineRightClick(e: MouseEvent) {
contextMenuRef.value?.open(e, true);
function onOutlineRightClick(ev: {
mouse: MouseEvent;
component: FullComponent;
}) {
contextMenuRef.value?.open(ev.mouse, true, ev.component.position);
}
</script>
24 changes: 24 additions & 0 deletions app/web/src/store/components.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export const useComponentsStore = (forceChangeSetId?: ChangeSetId) => {
>,
rawNodeAddMenu: [] as MenuItem[],

copyingFrom: null as { x: number; y: number } | null,
selectedComponentIds: [] as ComponentId[],
selectedEdgeId: null as EdgeId | null,
selectedComponentDetailsTab: null as string | null,
Expand Down Expand Up @@ -1049,6 +1050,29 @@ export const useComponentsStore = (forceChangeSetId?: ChangeSetId) => {
});
},

async PASTE_COMPONENTS(
componentIds: ComponentId[],
offset: { x: number; y: number },
) {
if (changeSetsStore.creatingChangeSet)
throw new Error("race, wait until the change set is created");
if (changeSetId === nilId())
changeSetsStore.creatingChangeSet = true;

return new ApiRequest({
method: "post",
url: "diagram/paste_components",
keyRequestStatusBy: componentIds,
params: {
componentIds,
offsetX: offset.x,
offsetY: offset.y,
...visibilityParams,
},
onSuccess: (response) => {},
});
},

async DELETE_COMPONENTS(componentIds: ComponentId[]) {
if (changeSetsStore.creatingChangeSet)
throw new Error("race, wait until the change set is created");
Expand Down
1 change: 1 addition & 0 deletions app/web/src/store/feature_flags.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const FLAG_MAPPING = {
MUTLIPLAYER_CHANGESET_APPLY: "multiplayer_changeset_apply_flow",
ABANDON_CHANGESET: "abandon_changeset",
CONNECTION_ANNOTATIONS: "socket_connection_annotations",
COPY_PASTE: "copy_paste",
};

type FeatureFlags = keyof typeof FLAG_MAPPING;
Expand Down
Loading

0 comments on commit d101386

Please sign in to comment.