mirror of https://github.com/apache/nifi.git
NIFI-13059: (#8661)
- Adding support for copying and pasting on the canvas. This closes #8661
This commit is contained in:
parent
8d8aebc5be
commit
41e4779bc5
|
@ -54,11 +54,14 @@ import {
|
|||
startCurrentProcessGroup,
|
||||
stopComponents,
|
||||
stopCurrentProcessGroup,
|
||||
stopVersionControlRequest
|
||||
stopVersionControlRequest,
|
||||
copy,
|
||||
paste
|
||||
} from '../state/flow/flow.actions';
|
||||
import { ComponentType } from '../../../state/shared';
|
||||
import {
|
||||
ConfirmStopVersionControlRequest,
|
||||
CopyComponentRequest,
|
||||
DeleteComponentRequest,
|
||||
MoveComponentRequest,
|
||||
OpenChangeVersionDialogRequest,
|
||||
|
@ -76,6 +79,7 @@ import { getComponentStateAndOpenDialog } from '../../../state/component-state/c
|
|||
import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions';
|
||||
import * as d3 from 'd3';
|
||||
import { Client } from '../../../service/client.service';
|
||||
import { CanvasView } from './canvas-view.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||
|
@ -1173,12 +1177,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
|||
}
|
||||
},
|
||||
{
|
||||
condition: (selection: any) => {
|
||||
condition: (selection: d3.Selection<any, any, any, any>) => {
|
||||
return this.canvasUtils.isDisconnected(selection);
|
||||
},
|
||||
clazz: 'fa icon-group',
|
||||
text: 'Group',
|
||||
action: (selection: any) => {
|
||||
action: (selection: d3.Selection<any, any, any, any>) => {
|
||||
const moveComponents: MoveComponentRequest[] = [];
|
||||
selection.each(function (d: any) {
|
||||
moveComponents.push({
|
||||
|
@ -1212,25 +1216,55 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
|||
isSeparator: true
|
||||
},
|
||||
{
|
||||
condition: (selection: any) => {
|
||||
// TODO - isCopyable
|
||||
return false;
|
||||
condition: (selection: d3.Selection<any, any, any, any>) => {
|
||||
return this.canvasUtils.isCopyable(selection);
|
||||
},
|
||||
clazz: 'fa fa-copy',
|
||||
text: 'Copy',
|
||||
action: () => {
|
||||
// TODO - copy
|
||||
action: (selection: d3.Selection<any, any, any, any>) => {
|
||||
const origin = this.canvasUtils.getOrigin(selection);
|
||||
const dimensions = this.canvasView.getSelectionBoundingClientRect(selection);
|
||||
|
||||
const components: CopyComponentRequest[] = [];
|
||||
selection.each((d) => {
|
||||
components.push({
|
||||
id: d.id,
|
||||
type: d.type,
|
||||
uri: d.uri,
|
||||
entity: d
|
||||
});
|
||||
});
|
||||
|
||||
this.store.dispatch(
|
||||
copy({
|
||||
request: {
|
||||
components,
|
||||
origin,
|
||||
dimensions
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
condition: (selection: any) => {
|
||||
// TODO - isPastable
|
||||
return false;
|
||||
condition: () => {
|
||||
return this.canvasUtils.isPastable();
|
||||
},
|
||||
clazz: 'fa fa-paste',
|
||||
text: 'Paste',
|
||||
action: () => {
|
||||
// TODO - paste
|
||||
action: (selection: d3.Selection<any, any, any, any>, event) => {
|
||||
if (event) {
|
||||
const pasteLocation = this.canvasView.getCanvasPosition({ x: event.pageX, y: event.pageY });
|
||||
if (pasteLocation) {
|
||||
this.store.dispatch(
|
||||
paste({
|
||||
request: {
|
||||
pasteLocation
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1325,7 +1359,8 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
|||
constructor(
|
||||
private store: Store<CanvasState>,
|
||||
private canvasUtils: CanvasUtils,
|
||||
private client: Client
|
||||
private client: Client,
|
||||
private canvasView: CanvasView
|
||||
) {
|
||||
this.allMenus = new Map<string, ContextMenuDefinition>();
|
||||
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
selectBreadcrumbs,
|
||||
selectCanvasPermissions,
|
||||
selectConnections,
|
||||
selectCopiedSnippet,
|
||||
selectCurrentProcessGroupId,
|
||||
selectParentProcessGroupId
|
||||
} from '../state/flow/flow.selectors';
|
||||
|
@ -39,7 +40,7 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
|
|||
import { FlowConfiguration } from '../../../state/flow-configuration';
|
||||
import { initialState as initialFlowConfigurationState } from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||
import { VersionControlInformation } from '../state/flow';
|
||||
import { CopiedSnippet, VersionControlInformation } from '../state/flow';
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
|
||||
|
@ -59,6 +60,7 @@ export class CanvasUtils {
|
|||
private flowConfiguration: FlowConfiguration | null = initialFlowConfigurationState.flowConfiguration;
|
||||
private connections: any[] = [];
|
||||
private breadcrumbs: BreadcrumbEntity | null = null;
|
||||
private copiedSnippet: CopiedSnippet | null = null;
|
||||
|
||||
private readonly humanizeDuration: Humanizer;
|
||||
|
||||
|
@ -118,6 +120,13 @@ export class CanvasUtils {
|
|||
.subscribe((breadcrumbs) => {
|
||||
this.breadcrumbs = breadcrumbs;
|
||||
});
|
||||
|
||||
this.store
|
||||
.select(selectCopiedSnippet)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((copiedSnippet) => {
|
||||
this.copiedSnippet = copiedSnippet;
|
||||
});
|
||||
}
|
||||
|
||||
public hasDownstream(selection: any): boolean {
|
||||
|
@ -807,14 +816,13 @@ export class CanvasUtils {
|
|||
*
|
||||
* @argument {selection} selection The selection
|
||||
*/
|
||||
getOrigin(selection: any): Position {
|
||||
const self: CanvasUtils = this;
|
||||
public getOrigin(selection: d3.Selection<any, any, any, any>): Position {
|
||||
let x: number | undefined;
|
||||
let y: number | undefined;
|
||||
|
||||
selection.each(function (this: any, d: any) {
|
||||
const selected: any = d3.select(this);
|
||||
if (!self.isConnection(selected)) {
|
||||
selection.each((d, i, nodes) => {
|
||||
const selected: any = d3.select(nodes[i]);
|
||||
if (!this.isConnection(selected)) {
|
||||
if (x == null || d.position.x < x) {
|
||||
x = d.position.x;
|
||||
}
|
||||
|
@ -831,6 +839,54 @@ export class CanvasUtils {
|
|||
return { x, y };
|
||||
}
|
||||
|
||||
public isCopyable(selection: d3.Selection<any, any, any, any>): boolean {
|
||||
// if nothing is selected return
|
||||
if (selection.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.canRead(selection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// determine how many copyable components are selected
|
||||
const copyable = selection.filter((d, i, nodes) => {
|
||||
const selected = d3.select(nodes[i]);
|
||||
if (this.isConnection(selected)) {
|
||||
const sourceIncluded = !selection
|
||||
.filter((source) => {
|
||||
const sourceComponentId = this.getConnectionSourceComponentId(d);
|
||||
return sourceComponentId === source.id;
|
||||
})
|
||||
.empty();
|
||||
const destinationIncluded = !selection
|
||||
.filter((destination) => {
|
||||
const destinationComponentId = this.getConnectionDestinationComponentId(d);
|
||||
return destinationComponentId === destination.id;
|
||||
})
|
||||
.empty();
|
||||
return sourceIncluded && destinationIncluded;
|
||||
} else {
|
||||
return (
|
||||
this.isProcessor(selected) ||
|
||||
this.isFunnel(selected) ||
|
||||
this.isLabel(selected) ||
|
||||
this.isProcessGroup(selected) ||
|
||||
this.isRemoteProcessGroup(selected) ||
|
||||
this.isInputPort(selected) ||
|
||||
this.isOutputPort(selected)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ensure everything selected is copyable
|
||||
return selection.size() === copyable.size();
|
||||
}
|
||||
|
||||
public isPastable(): boolean {
|
||||
return this.canvasPermissions.canWrite && this.copiedSnippet != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name for this connection.
|
||||
*
|
||||
|
|
|
@ -31,6 +31,7 @@ import { RemoteProcessGroupManager } from './manager/remote-process-group-manage
|
|||
import { ConnectionManager } from './manager/connection-manager.service';
|
||||
import { deselectAllComponents } from '../state/flow/flow.actions';
|
||||
import { CanvasUtils } from './canvas-utils.service';
|
||||
import { Position } from '../state/shared';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -185,7 +186,6 @@ export class CanvasView {
|
|||
|
||||
public isSelectedComponentOnScreen(): boolean {
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
|
||||
if (canvasContainer == null) {
|
||||
return false;
|
||||
}
|
||||
|
@ -245,6 +245,55 @@ export class CanvasView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a bounding box is fully in the current viewable canvas area.
|
||||
*
|
||||
* @param {type} boundingBox Bounding box to check.
|
||||
* @param {boolean} strict If true, the entire bounding box must be in the viewport.
|
||||
* If false, only part of the bounding box must be in the viewport.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isBoundingBoxInViewport(boundingBox: any, strict: boolean): boolean {
|
||||
const selection: any = this.canvasUtils.getSelection();
|
||||
if (selection.size() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
if (!canvasContainer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const yOffset = canvasContainer.getBoundingClientRect().top;
|
||||
|
||||
// scale the translation
|
||||
const translate = [this.x / this.k, this.y / this.k];
|
||||
|
||||
// get the normalized screen width and height
|
||||
const screenWidth = canvasContainer.offsetWidth / this.k;
|
||||
const screenHeight = canvasContainer.offsetHeight / this.k;
|
||||
|
||||
// calculate the screen bounds one screens worth in each direction
|
||||
const screenLeft = -translate[0];
|
||||
const screenTop = -translate[1];
|
||||
const screenRight = screenLeft + screenWidth;
|
||||
const screenBottom = screenTop + screenHeight;
|
||||
|
||||
const left = Math.ceil(boundingBox.x);
|
||||
const right = Math.floor(boundingBox.x + boundingBox.width);
|
||||
const top = Math.ceil(boundingBox.y - yOffset / this.k);
|
||||
const bottom = Math.floor(boundingBox.y - yOffset / this.k + boundingBox.height);
|
||||
|
||||
if (strict) {
|
||||
return !(left < screenLeft || right > screenRight || top < screenTop || bottom > screenBottom);
|
||||
} else {
|
||||
return (
|
||||
((left > screenLeft && left < screenRight) || (right < screenRight && right > screenLeft)) &&
|
||||
((top > screenTop && top < screenBottom) || (bottom < screenBottom && bottom > screenTop))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public updateCanvasVisibility(): void {
|
||||
const self: CanvasView = this;
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
|
@ -354,11 +403,6 @@ export class CanvasView {
|
|||
}
|
||||
|
||||
public centerSelectedComponents(allowTransition: boolean): void {
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
if (canvasContainer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection: any = this.canvasUtils.getSelection();
|
||||
if (selection.empty()) {
|
||||
return;
|
||||
|
@ -368,7 +412,7 @@ export class CanvasView {
|
|||
if (selection.size() === 1) {
|
||||
bbox = this.getSingleSelectionBoundingClientRect(selection);
|
||||
} else {
|
||||
bbox = this.getBulkSelectionBoundingClientRect(selection, canvasContainer);
|
||||
bbox = this.getSelectionBoundingClientRect(selection);
|
||||
}
|
||||
|
||||
this.allowTransition = allowTransition;
|
||||
|
@ -408,8 +452,13 @@ export class CanvasView {
|
|||
/**
|
||||
* Get a BoundingClientRect, normalized to the canvas, that encompasses all nodes in a given selection.
|
||||
*/
|
||||
private getBulkSelectionBoundingClientRect(selection: any, canvasContainer: any): any {
|
||||
const canvasBoundingBox: any = canvasContainer.getBoundingClientRect();
|
||||
public getSelectionBoundingClientRect(selection: any): any {
|
||||
let yOffset = 0;
|
||||
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
if (canvasContainer) {
|
||||
yOffset = canvasContainer.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
const initialBBox: any = {
|
||||
x: Number.MAX_VALUE,
|
||||
|
@ -430,9 +479,9 @@ export class CanvasView {
|
|||
|
||||
// normalize the bounding box with scale and translate
|
||||
bbox.x = (bbox.x - this.x) / this.k;
|
||||
bbox.y = (bbox.y - canvasBoundingBox.top - this.y) / this.k;
|
||||
bbox.y = (bbox.y - yOffset - this.y) / this.k;
|
||||
bbox.right = (bbox.right - this.x) / this.k;
|
||||
bbox.bottom = (bbox.bottom - canvasBoundingBox.top - this.y) / this.k;
|
||||
bbox.bottom = (bbox.bottom - yOffset - this.y) / this.k;
|
||||
|
||||
bbox.width = bbox.right - bbox.x;
|
||||
bbox.height = bbox.bottom - bbox.y;
|
||||
|
@ -442,6 +491,37 @@ export class CanvasView {
|
|||
return bbox;
|
||||
}
|
||||
|
||||
public getCanvasPosition(position: Position): Position | null {
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
if (!canvasContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = canvasContainer.getBoundingClientRect();
|
||||
|
||||
// translate the point onto the canvas
|
||||
const canvasDropPoint = {
|
||||
x: position.x - rect.left,
|
||||
y: position.y - rect.top
|
||||
};
|
||||
|
||||
// if the position is over the canvas fire an event to add the new item
|
||||
if (
|
||||
canvasDropPoint.x >= 0 &&
|
||||
canvasDropPoint.x < rect.width &&
|
||||
canvasDropPoint.y >= 0 &&
|
||||
canvasDropPoint.y < rect.height
|
||||
) {
|
||||
// adjust the x and y coordinates accordingly
|
||||
const x = canvasDropPoint.x / this.k - this.x / this.k;
|
||||
const y = canvasDropPoint.y / this.k - this.y / this.k;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private centerBoundingBox(boundingBox: any): void {
|
||||
let scale: number = this.k;
|
||||
if (boundingBox.scale != null) {
|
||||
|
@ -460,7 +540,7 @@ export class CanvasView {
|
|||
* @param {type} boundingBox
|
||||
* @returns {number[]}
|
||||
*/
|
||||
private getCenterForBoundingBox(boundingBox: any): number[] {
|
||||
public getCenterForBoundingBox(boundingBox: any): number[] {
|
||||
let scale: number = this.k;
|
||||
if (boundingBox.scale != null) {
|
||||
scale = boundingBox.scale;
|
||||
|
|
|
@ -35,7 +35,6 @@ import {
|
|||
ReplayLastProvenanceEventRequest,
|
||||
RunOnceRequest,
|
||||
SaveToVersionControlRequest,
|
||||
Snippet,
|
||||
StartComponentRequest,
|
||||
StartProcessGroupRequest,
|
||||
StopComponentRequest,
|
||||
|
@ -257,33 +256,6 @@ export class FlowService implements PropertyDescriptorRetriever {
|
|||
return this.httpClient.delete(this.nifiCommon.stripProtocol(deleteComponent.uri), { params });
|
||||
}
|
||||
|
||||
createSnippet(snippet: Snippet): Observable<any> {
|
||||
return this.httpClient.post(`${FlowService.API}/snippets`, {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||
snippet
|
||||
});
|
||||
}
|
||||
|
||||
moveSnippet(snippetId: string, groupId: string): Observable<any> {
|
||||
const payload: any = {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||
snippet: {
|
||||
id: snippetId,
|
||||
parentGroupId: groupId
|
||||
}
|
||||
};
|
||||
return this.httpClient.put(`${FlowService.API}/snippets/${snippetId}`, payload);
|
||||
}
|
||||
|
||||
deleteSnippet(snippetId: string): Observable<any> {
|
||||
const params = new HttpParams({
|
||||
fromObject: {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged()
|
||||
}
|
||||
});
|
||||
return this.httpClient.delete(`${FlowService.API}/snippets/${snippetId}`, { params });
|
||||
}
|
||||
|
||||
replayLastProvenanceEvent(request: ReplayLastProvenanceEventRequest): Observable<any> {
|
||||
return this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`, request);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Snippet, SnippetComponentRequest } from '../state/flow';
|
||||
import { ClusterConnectionService } from '../../../service/cluster-connection.service';
|
||||
import { ComponentType } from '../../../state/shared';
|
||||
import { Client } from '../../../service/client.service';
|
||||
import { Position } from '../state/shared';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SnippetService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private client: Client,
|
||||
private clusterConnectionService: ClusterConnectionService
|
||||
) {}
|
||||
|
||||
marshalSnippet(components: SnippetComponentRequest[], processGroupId: string): Snippet {
|
||||
return components.reduce(
|
||||
(snippet, component) => {
|
||||
switch (component.type) {
|
||||
case ComponentType.Processor:
|
||||
snippet.processors[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.InputPort:
|
||||
snippet.inputPorts[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.OutputPort:
|
||||
snippet.outputPorts[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.ProcessGroup:
|
||||
snippet.processGroups[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.RemoteProcessGroup:
|
||||
snippet.remoteProcessGroups[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.Funnel:
|
||||
snippet.funnels[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.Label:
|
||||
snippet.labels[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.Connection:
|
||||
snippet.connections[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
}
|
||||
return snippet;
|
||||
},
|
||||
{
|
||||
parentGroupId: processGroupId,
|
||||
processors: {},
|
||||
funnels: {},
|
||||
inputPorts: {},
|
||||
outputPorts: {},
|
||||
remoteProcessGroups: {},
|
||||
processGroups: {},
|
||||
connections: {},
|
||||
labels: {}
|
||||
} as Snippet
|
||||
);
|
||||
}
|
||||
|
||||
createSnippet(snippet: Snippet): Observable<any> {
|
||||
return this.httpClient.post(`${SnippetService.API}/snippets`, {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||
snippet
|
||||
});
|
||||
}
|
||||
|
||||
moveSnippet(snippetId: string, groupId: string): Observable<any> {
|
||||
const payload: any = {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||
snippet: {
|
||||
id: snippetId,
|
||||
parentGroupId: groupId
|
||||
}
|
||||
};
|
||||
return this.httpClient.put(`${SnippetService.API}/snippets/${snippetId}`, payload);
|
||||
}
|
||||
|
||||
copySnippet(snippetId: string, pasteLocation: Position, groupId: string): Observable<any> {
|
||||
const payload: any = {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||
originX: pasteLocation.x,
|
||||
originY: pasteLocation.y,
|
||||
snippetId
|
||||
};
|
||||
return this.httpClient.post(`${SnippetService.API}/process-groups/${groupId}/snippet-instance`, payload);
|
||||
}
|
||||
|
||||
deleteSnippet(snippetId: string): Observable<any> {
|
||||
const params = new HttpParams({
|
||||
fromObject: {
|
||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged()
|
||||
}
|
||||
});
|
||||
return this.httpClient.delete(`${SnippetService.API}/snippets/${snippetId}`, { params });
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ import {
|
|||
ChangeVersionDialogRequest,
|
||||
ComponentEntity,
|
||||
ConfirmStopVersionControlRequest,
|
||||
CopiedSnippet,
|
||||
CopyRequest,
|
||||
CreateComponentRequest,
|
||||
CreateComponentResponse,
|
||||
CreateConnection,
|
||||
|
@ -64,6 +66,8 @@ import {
|
|||
OpenGroupComponentsDialogRequest,
|
||||
OpenLocalChangesDialogRequest,
|
||||
OpenSaveVersionDialogRequest,
|
||||
PasteRequest,
|
||||
PasteResponse,
|
||||
RefreshRemoteProcessGroupRequest,
|
||||
ReplayLastProvenanceEventRequest,
|
||||
RpgManageRemotePortsRequest,
|
||||
|
@ -476,6 +480,14 @@ export const moveComponents = createAction(
|
|||
props<{ request: MoveComponentsRequest }>()
|
||||
);
|
||||
|
||||
export const copy = createAction(`${CANVAS_PREFIX} Copy`, props<{ request: CopyRequest }>());
|
||||
|
||||
export const copySuccess = createAction(`${CANVAS_PREFIX} Copy Success`, props<{ copiedSnippet: CopiedSnippet }>());
|
||||
|
||||
export const paste = createAction(`${CANVAS_PREFIX} Paste`, props<{ request: PasteRequest }>());
|
||||
|
||||
export const pasteSuccess = createAction(`${CANVAS_PREFIX} Paste Success`, props<{ response: PasteResponse }>());
|
||||
|
||||
/*
|
||||
Delete Component Actions
|
||||
*/
|
||||
|
|
|
@ -39,14 +39,17 @@ import {
|
|||
tap
|
||||
} from 'rxjs';
|
||||
import {
|
||||
CopyComponentRequest,
|
||||
CreateProcessGroupDialogRequest,
|
||||
DeleteComponentResponse,
|
||||
GroupComponentsDialogRequest,
|
||||
ImportFromRegistryDialogRequest,
|
||||
LoadProcessGroupRequest,
|
||||
LoadProcessGroupResponse,
|
||||
MoveComponentRequest,
|
||||
SaveVersionDialogRequest,
|
||||
SaveVersionRequest,
|
||||
SelectedComponent,
|
||||
Snippet,
|
||||
StopVersionControlRequest,
|
||||
StopVersionControlResponse,
|
||||
|
@ -61,6 +64,7 @@ import { Action, Store } from '@ngrx/store';
|
|||
import {
|
||||
selectAnySelectedComponentIds,
|
||||
selectChangeVersionRequest,
|
||||
selectCopiedSnippet,
|
||||
selectCurrentParameterContext,
|
||||
selectCurrentProcessGroupId,
|
||||
selectMaxZIndex,
|
||||
|
@ -119,6 +123,8 @@ import { LocalChangesDialog } from '../../ui/canvas/items/flow/local-changes-dia
|
|||
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
|
||||
import { ExtensionTypesService } from '../../../../service/extension-types.service';
|
||||
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
|
||||
import { SnippetService } from '../../service/snippet.service';
|
||||
import { selectTransform } from '../transform/transform.selectors';
|
||||
|
||||
@Injectable()
|
||||
export class FlowEffects {
|
||||
|
@ -134,6 +140,7 @@ export class FlowEffects {
|
|||
private birdseyeView: BirdseyeView,
|
||||
private connectionManager: ConnectionManager,
|
||||
private clusterConnectionService: ClusterConnectionService,
|
||||
private snippetService: SnippetService,
|
||||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
private propertyTableHelperService: PropertyTableHelperService,
|
||||
|
@ -1816,53 +1823,11 @@ export class FlowEffects {
|
|||
map((action) => action.request),
|
||||
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||
mergeMap(([request, processGroupId]) => {
|
||||
const components: any[] = request.components;
|
||||
const components: MoveComponentRequest[] = request.components;
|
||||
const snippet = this.snippetService.marshalSnippet(components, processGroupId);
|
||||
|
||||
const snippet: Snippet = components.reduce(
|
||||
(snippet, component) => {
|
||||
switch (component.type) {
|
||||
case ComponentType.Processor:
|
||||
snippet.processors[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.InputPort:
|
||||
snippet.inputPorts[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.OutputPort:
|
||||
snippet.outputPorts[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.ProcessGroup:
|
||||
snippet.processGroups[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.RemoteProcessGroup:
|
||||
snippet.remoteProcessGroups[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.Funnel:
|
||||
snippet.funnels[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.Label:
|
||||
snippet.labels[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
case ComponentType.Connection:
|
||||
snippet.connections[component.id] = this.client.getRevision(component.entity);
|
||||
break;
|
||||
}
|
||||
return snippet;
|
||||
},
|
||||
{
|
||||
parentGroupId: processGroupId,
|
||||
processors: {},
|
||||
funnels: {},
|
||||
inputPorts: {},
|
||||
outputPorts: {},
|
||||
remoteProcessGroups: {},
|
||||
processGroups: {},
|
||||
connections: {},
|
||||
labels: {}
|
||||
} as Snippet
|
||||
);
|
||||
|
||||
return from(this.flowService.createSnippet(snippet)).pipe(
|
||||
switchMap((response) => this.flowService.moveSnippet(response.snippet.id, request.groupId)),
|
||||
return from(this.snippetService.createSnippet(snippet)).pipe(
|
||||
switchMap((response) => this.snippetService.moveSnippet(response.snippet.id, request.groupId)),
|
||||
map(() => {
|
||||
const deleteResponses: DeleteComponentResponse[] = [];
|
||||
|
||||
|
@ -1884,6 +1849,157 @@ export class FlowEffects {
|
|||
)
|
||||
);
|
||||
|
||||
copy$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.copy),
|
||||
map((action) => action.request),
|
||||
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||
switchMap(([request, processGroupId]) => {
|
||||
const components: CopyComponentRequest[] = request.components;
|
||||
const snippet = this.snippetService.marshalSnippet(components, processGroupId);
|
||||
return of(
|
||||
FlowActions.copySuccess({
|
||||
copiedSnippet: {
|
||||
snippet,
|
||||
dimensions: request.dimensions,
|
||||
origin: request.origin
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
paste$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.paste),
|
||||
map((action) => action.request),
|
||||
concatLatestFrom(() => [
|
||||
this.store.select(selectCopiedSnippet).pipe(isDefinedAndNotNull()),
|
||||
this.store.select(selectCurrentProcessGroupId),
|
||||
this.store.select(selectTransform)
|
||||
]),
|
||||
switchMap(([request, copiedSnippet, processGroupId, transform]) =>
|
||||
from(this.snippetService.createSnippet(copiedSnippet.snippet)).pipe(
|
||||
switchMap((response) => {
|
||||
let pasteLocation = request.pasteLocation;
|
||||
const snippetOrigin = copiedSnippet.origin;
|
||||
const dimensions = copiedSnippet.dimensions;
|
||||
|
||||
if (!pasteLocation) {
|
||||
// if the copied snippet is from a different group or the original items are not in the viewport, center the pasted snippet
|
||||
if (
|
||||
copiedSnippet.snippet.parentGroupId != processGroupId ||
|
||||
!this.canvasView.isBoundingBoxInViewport(dimensions, false)
|
||||
) {
|
||||
const center = this.canvasView.getCenterForBoundingBox(dimensions);
|
||||
pasteLocation = {
|
||||
x: center[0] - transform.translate.x / transform.scale,
|
||||
y: center[1] - transform.translate.y / transform.scale
|
||||
};
|
||||
} else {
|
||||
pasteLocation = {
|
||||
x: snippetOrigin.x + 25,
|
||||
y: snippetOrigin.y + 25
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return from(
|
||||
this.snippetService.copySnippet(response.snippet.id, pasteLocation, processGroupId)
|
||||
).pipe(map((response) => FlowActions.pasteSuccess({ response })));
|
||||
}),
|
||||
catchError((error) => of(FlowActions.flowSnackbarError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
pasteSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.pasteSuccess),
|
||||
map((action) => action.response),
|
||||
switchMap((response) => {
|
||||
this.canvasView.updateCanvasVisibility();
|
||||
this.birdseyeView.refresh();
|
||||
|
||||
const components: SelectedComponent[] = [];
|
||||
components.push(
|
||||
...response.flow.labels.map((label) => {
|
||||
return {
|
||||
id: label.id,
|
||||
componentType: ComponentType.Label
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.funnels.map((funnel) => {
|
||||
return {
|
||||
id: funnel.id,
|
||||
componentType: ComponentType.Funnel
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.remoteProcessGroups.map((remoteProcessGroups) => {
|
||||
return {
|
||||
id: remoteProcessGroups.id,
|
||||
componentType: ComponentType.RemoteProcessGroup
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.inputPorts.map((inputPorts) => {
|
||||
return {
|
||||
id: inputPorts.id,
|
||||
componentType: ComponentType.InputPort
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.outputPorts.map((outputPorts) => {
|
||||
return {
|
||||
id: outputPorts.id,
|
||||
componentType: ComponentType.OutputPort
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.processGroups.map((processGroup) => {
|
||||
return {
|
||||
id: processGroup.id,
|
||||
componentType: ComponentType.ProcessGroup
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.processors.map((processor) => {
|
||||
return {
|
||||
id: processor.id,
|
||||
componentType: ComponentType.Processor
|
||||
};
|
||||
})
|
||||
);
|
||||
components.push(
|
||||
...response.flow.connections.map((connection) => {
|
||||
return {
|
||||
id: connection.id,
|
||||
componentType: ComponentType.Connection
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return of(
|
||||
FlowActions.selectComponents({
|
||||
request: {
|
||||
components
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
deleteComponent$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.deleteComponents),
|
||||
|
@ -1962,8 +2078,8 @@ export class FlowEffects {
|
|||
} as Snippet
|
||||
);
|
||||
|
||||
return from(this.flowService.createSnippet(snippet)).pipe(
|
||||
switchMap((response) => this.flowService.deleteSnippet(response.snippet.id)),
|
||||
return from(this.snippetService.createSnippet(snippet)).pipe(
|
||||
switchMap((response) => this.snippetService.deleteSnippet(response.snippet.id)),
|
||||
map(() => {
|
||||
const deleteResponses: DeleteComponentResponse[] = [];
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
changeVersionComplete,
|
||||
changeVersionSuccess,
|
||||
clearFlowApiError,
|
||||
copySuccess,
|
||||
createComponentComplete,
|
||||
createComponentSuccess,
|
||||
createConnection,
|
||||
|
@ -41,6 +42,7 @@ import {
|
|||
loadProcessorSuccess,
|
||||
loadRemoteProcessGroupSuccess,
|
||||
navigateWithoutTransform,
|
||||
pasteSuccess,
|
||||
pollChangeVersionSuccess,
|
||||
pollRevertChangesSuccess,
|
||||
requestRefreshRemoteProcessGroup,
|
||||
|
@ -143,6 +145,7 @@ export const initialState: FlowState = {
|
|||
parameterProviderBulletins: [],
|
||||
reportingTaskBulletins: []
|
||||
},
|
||||
copiedSnippet: null,
|
||||
dragging: false,
|
||||
saving: false,
|
||||
versionSaving: false,
|
||||
|
@ -324,6 +327,51 @@ export const flowReducer = createReducer(
|
|||
});
|
||||
});
|
||||
}),
|
||||
on(copySuccess, (state, { copiedSnippet }) => ({
|
||||
...state,
|
||||
copiedSnippet
|
||||
})),
|
||||
on(pasteSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
const labels: any[] | null = getComponentCollection(draftState, ComponentType.Label);
|
||||
if (labels) {
|
||||
labels.push(...response.flow.labels);
|
||||
}
|
||||
const funnels: any[] | null = getComponentCollection(draftState, ComponentType.Funnel);
|
||||
if (funnels) {
|
||||
funnels.push(...response.flow.funnels);
|
||||
}
|
||||
const remoteProcessGroups: any[] | null = getComponentCollection(
|
||||
draftState,
|
||||
ComponentType.RemoteProcessGroup
|
||||
);
|
||||
if (remoteProcessGroups) {
|
||||
remoteProcessGroups.push(...response.flow.remoteProcessGroups);
|
||||
}
|
||||
const inputPorts: any[] | null = getComponentCollection(draftState, ComponentType.InputPort);
|
||||
if (inputPorts) {
|
||||
inputPorts.push(...response.flow.inputPorts);
|
||||
}
|
||||
const outputPorts: any[] | null = getComponentCollection(draftState, ComponentType.OutputPort);
|
||||
if (outputPorts) {
|
||||
outputPorts.push(...response.flow.outputPorts);
|
||||
}
|
||||
const processGroups: any[] | null = getComponentCollection(draftState, ComponentType.ProcessGroup);
|
||||
if (processGroups) {
|
||||
processGroups.push(...response.flow.processGroups);
|
||||
}
|
||||
const processors: any[] | null = getComponentCollection(draftState, ComponentType.Processor);
|
||||
if (processors) {
|
||||
processors.push(...response.flow.processors);
|
||||
}
|
||||
const connections: any[] | null = getComponentCollection(draftState, ComponentType.Connection);
|
||||
if (connections) {
|
||||
connections.push(...response.flow.connections);
|
||||
}
|
||||
|
||||
draftState.copiedSnippet = null;
|
||||
});
|
||||
}),
|
||||
on(setDragging, (state, { dragging }) => ({
|
||||
...state,
|
||||
dragging
|
||||
|
|
|
@ -42,6 +42,8 @@ export const selectCurrentProcessGroupId = createSelector(selectFlowState, (stat
|
|||
|
||||
export const selectRefreshRpgDetails = createSelector(selectFlowState, (state: FlowState) => state.refreshRpgDetails);
|
||||
|
||||
export const selectCopiedSnippet = createSelector(selectFlowState, (state: FlowState) => state.copiedSnippet);
|
||||
|
||||
export const selectCurrentParameterContext = createSelector(
|
||||
selectFlowState,
|
||||
(state: FlowState) => state.flow.processGroupFlow.parameterContext
|
||||
|
|
|
@ -423,18 +423,36 @@ export interface UpdatePositionsRequest {
|
|||
connectionUpdates: UpdateComponentRequest[];
|
||||
}
|
||||
|
||||
export interface MoveComponentRequest {
|
||||
export interface SnippetComponentRequest {
|
||||
id: string;
|
||||
uri: string;
|
||||
type: ComponentType;
|
||||
entity: any;
|
||||
}
|
||||
|
||||
export interface MoveComponentRequest extends SnippetComponentRequest {}
|
||||
|
||||
export interface MoveComponentsRequest {
|
||||
components: MoveComponentRequest[];
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export interface CopyComponentRequest extends SnippetComponentRequest {}
|
||||
|
||||
export interface CopyRequest {
|
||||
components: CopyComponentRequest[];
|
||||
origin: Position;
|
||||
dimensions: any;
|
||||
}
|
||||
|
||||
export interface PasteRequest {
|
||||
pasteLocation?: Position;
|
||||
}
|
||||
|
||||
export interface PasteResponse {
|
||||
flow: Flow;
|
||||
}
|
||||
|
||||
export interface DeleteComponentRequest {
|
||||
id: string;
|
||||
uri: string;
|
||||
|
@ -490,6 +508,12 @@ export interface Snippet {
|
|||
};
|
||||
}
|
||||
|
||||
export interface CopiedSnippet {
|
||||
snippet: Snippet;
|
||||
origin: Position;
|
||||
dimensions: any;
|
||||
}
|
||||
|
||||
/*
|
||||
Tooltips
|
||||
*/
|
||||
|
@ -613,6 +637,7 @@ export interface FlowState {
|
|||
error: string | null;
|
||||
versionSaving: boolean;
|
||||
changeVersionRequest: FlowUpdateRequestEntity | null;
|
||||
copiedSnippet: CopiedSnippet | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
||||
|
||||
|
|
|
@ -116,8 +116,8 @@
|
|||
color="primary"
|
||||
class="mr-2"
|
||||
type="button"
|
||||
[disabled]="!canPaste(selection)"
|
||||
(click)="paste(selection)">
|
||||
[disabled]="!canPaste()"
|
||||
(click)="paste()">
|
||||
<i class="fa fa-paste"></i>
|
||||
</button>
|
||||
<button
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import {
|
||||
copy,
|
||||
deleteComponents,
|
||||
getParameterContextsAndOpenGroupComponentsDialog,
|
||||
navigateToEditComponent,
|
||||
navigateToEditCurrentProcessGroup,
|
||||
navigateToManageComponentPolicies,
|
||||
paste,
|
||||
setOperationCollapsed,
|
||||
startComponents,
|
||||
startCurrentProcessGroup,
|
||||
|
@ -34,6 +36,7 @@ import { CanvasUtils } from '../../../../service/canvas-utils.service';
|
|||
import { initialState } from '../../../../state/flow/flow.reducer';
|
||||
import { Storage } from '../../../../../../service/storage.service';
|
||||
import {
|
||||
CopyComponentRequest,
|
||||
DeleteComponentRequest,
|
||||
MoveComponentRequest,
|
||||
StartComponentRequest,
|
||||
|
@ -43,6 +46,8 @@ import {
|
|||
import { BreadcrumbEntity } from '../../../../state/shared';
|
||||
import { ComponentType } from '../../../../../../state/shared';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import * as d3 from 'd3';
|
||||
import { CanvasView } from '../../../../service/canvas-view.service';
|
||||
|
||||
@Component({
|
||||
selector: 'operation-control',
|
||||
|
@ -63,6 +68,7 @@ export class OperationControl {
|
|||
constructor(
|
||||
private store: Store<CanvasState>,
|
||||
public canvasUtils: CanvasUtils,
|
||||
private canvasView: CanvasView,
|
||||
private storage: Storage
|
||||
) {
|
||||
try {
|
||||
|
@ -335,22 +341,45 @@ export class OperationControl {
|
|||
}
|
||||
}
|
||||
|
||||
canCopy(selection: any): boolean {
|
||||
// TODO - isCopyable
|
||||
return false;
|
||||
canCopy(selection: d3.Selection<any, any, any, any>): boolean {
|
||||
return this.canvasUtils.isCopyable(selection);
|
||||
}
|
||||
|
||||
copy(selection: any): void {
|
||||
// TODO - copy
|
||||
copy(selection: d3.Selection<any, any, any, any>): void {
|
||||
const components: CopyComponentRequest[] = [];
|
||||
selection.each((d) => {
|
||||
components.push({
|
||||
id: d.id,
|
||||
type: d.type,
|
||||
uri: d.uri,
|
||||
entity: d
|
||||
});
|
||||
});
|
||||
|
||||
const origin = this.canvasUtils.getOrigin(selection);
|
||||
const dimensions = this.canvasView.getSelectionBoundingClientRect(selection);
|
||||
|
||||
this.store.dispatch(
|
||||
copy({
|
||||
request: {
|
||||
components,
|
||||
origin,
|
||||
dimensions
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
canPaste(selection: any): boolean {
|
||||
// TODO - isPastable
|
||||
return false;
|
||||
canPaste(): boolean {
|
||||
return this.canvasUtils.isPastable();
|
||||
}
|
||||
|
||||
paste(selection: any): void {
|
||||
// TODO - paste
|
||||
paste(): void {
|
||||
this.store.dispatch(
|
||||
paste({
|
||||
request: {}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
canGroup(selection: any): boolean {
|
||||
|
|
|
@ -19,14 +19,12 @@ import { Component, Input } from '@angular/core';
|
|||
import { CdkDrag, CdkDragEnd } from '@angular/cdk/drag-drop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CanvasState } from '../../../../state';
|
||||
import { INITIAL_SCALE, INITIAL_TRANSLATE } from '../../../../state/transform/transform.reducer';
|
||||
import { selectTransform } from '../../../../state/transform/transform.selectors';
|
||||
import { createComponentRequest, setDragging } from '../../../../state/flow/flow.actions';
|
||||
import { Client } from '../../../../../../service/client.service';
|
||||
import { selectDragging } from '../../../../state/flow/flow.selectors';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Position } from '../../../../state/shared';
|
||||
import { ComponentType } from '../../../../../../state/shared';
|
||||
import { CanvasView } from '../../../../service/canvas-view.service';
|
||||
|
||||
@Component({
|
||||
selector: 'new-canvas-item',
|
||||
|
@ -45,21 +43,11 @@ export class NewCanvasItem {
|
|||
|
||||
private hovering = false;
|
||||
|
||||
private scale: number = INITIAL_SCALE;
|
||||
private translate: Position = INITIAL_TRANSLATE;
|
||||
|
||||
constructor(
|
||||
private client: Client,
|
||||
private canvasView: CanvasView,
|
||||
private store: Store<CanvasState>
|
||||
) {
|
||||
this.store
|
||||
.select(selectTransform)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((transform) => {
|
||||
this.scale = transform.scale;
|
||||
this.translate = transform.translate;
|
||||
});
|
||||
|
||||
this.store
|
||||
.select(selectDragging)
|
||||
.pipe(takeUntilDestroyed())
|
||||
|
@ -93,27 +81,10 @@ export class NewCanvasItem {
|
|||
}
|
||||
|
||||
onDragEnded(event: CdkDragEnd): void {
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
const rect = canvasContainer.getBoundingClientRect();
|
||||
const dropPoint = event.dropPoint;
|
||||
|
||||
// translate the drop point onto the canvas
|
||||
const canvasDropPoint = {
|
||||
x: dropPoint.x - rect.left,
|
||||
y: dropPoint.y - rect.top
|
||||
};
|
||||
|
||||
// if the position is over the canvas fire an event to add the new item
|
||||
if (
|
||||
canvasDropPoint.x >= 0 &&
|
||||
canvasDropPoint.x < rect.width &&
|
||||
canvasDropPoint.y >= 0 &&
|
||||
canvasDropPoint.y < rect.height
|
||||
) {
|
||||
// adjust the x and y coordinates accordingly
|
||||
const x = canvasDropPoint.x / this.scale - this.translate.x / this.scale;
|
||||
const y = canvasDropPoint.y / this.scale - this.translate.y / this.scale;
|
||||
|
||||
const position = this.canvasView.getCanvasPosition(dropPoint);
|
||||
if (position) {
|
||||
this.store.dispatch(
|
||||
createComponentRequest({
|
||||
request: {
|
||||
|
@ -122,7 +93,7 @@ export class NewCanvasItem {
|
|||
version: 0
|
||||
},
|
||||
type: this.type,
|
||||
position: { x, y }
|
||||
position
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -44,7 +44,11 @@
|
|||
<b>History</b>
|
||||
<ul class="px-2">
|
||||
@for (previousValue of propertyHistory.previousValues; track previousValue) {
|
||||
<li>{{ previousValue.previousValue }} - {{ previousValue.timestamp }} ({{ previousValue.userIdentity }})</li>
|
||||
<li>
|
||||
{{ previousValue.previousValue }} - {{ previousValue.timestamp }} ({{
|
||||
previousValue.userIdentity
|
||||
}})
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue