Skip to content

Commit 117c03f

Browse files
committed
add copy component from one app to another
1 parent 1740278 commit 117c03f

3 files changed

Lines changed: 216 additions & 91 deletions

File tree

client/packages/lowcoder/src/comps/utils/gridCompOperator.ts

Lines changed: 163 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { SimpleContainerComp } from "comps/comps/containerBase/simpleContainerCo
33
import { GridItemComp } from "comps/comps/gridItemComp";
44
import { remoteComp } from "comps/comps/remoteComp/remoteComp";
55
import { EditorState } from "comps/editorState";
6+
import { NameGenerator } from "comps/utils";
67
import { trans } from "i18n";
78
import {
89
calcPasteBaseXY,
10+
DEFAULT_POSITION_PARAMS,
911
Layout,
1012
LayoutItem,
1113
moveToZero,
@@ -29,15 +31,73 @@ import { genRandomKey } from "./idGenerator";
2931
import { getLatestVersion, getRemoteCompType, parseCompType } from "./remote";
3032
import { APPLICATION_VIEW_URL } from "@lowcoder-ee/constants/routesURL";
3133

32-
export type CopyCompType = {
34+
const CLIPBOARD_TYPE = "lowcoder-components";
35+
const CLIPBOARD_VERSION = 1;
36+
37+
export interface ClipboardGridItem {
38+
compType: string;
39+
comp: any;
40+
name: string;
3341
layout: LayoutItem;
34-
item: Comp;
35-
};
42+
isContainer: boolean;
43+
}
3644

37-
export class GridCompOperator {
38-
private static copyComps: CopyCompType[] = [];
39-
private static sourcePositionParams: PositionParams;
45+
export interface LowcoderClipboardPayload {
46+
type: typeof CLIPBOARD_TYPE;
47+
version: number;
48+
timestamp: number;
49+
gridItems: ClipboardGridItem[];
50+
hookItems: ClipboardHookItem[];
51+
sourcePositionParams: PositionParams;
52+
}
53+
54+
export interface ClipboardHookItem {
55+
compType: string;
56+
comp: any;
57+
name: string;
58+
fullValue: any;
59+
}
60+
61+
async function writeToClipboard(payload: LowcoderClipboardPayload): Promise<boolean> {
62+
try {
63+
const json = JSON.stringify(payload);
64+
await navigator.clipboard.writeText(json);
65+
return true;
66+
} catch {
67+
return false;
68+
}
69+
}
70+
71+
export async function readFromClipboard(): Promise<LowcoderClipboardPayload | null> {
72+
try {
73+
const text = await navigator.clipboard.readText();
74+
if (!text) return null;
75+
const parsed = JSON.parse(text);
76+
if (parsed?.type !== CLIPBOARD_TYPE || !parsed?.version) return null;
77+
return parsed as LowcoderClipboardPayload;
78+
} catch {
79+
return null;
80+
}
81+
}
82+
83+
function buildEmptyPayload(): LowcoderClipboardPayload {
84+
return {
85+
type: CLIPBOARD_TYPE,
86+
version: CLIPBOARD_VERSION,
87+
timestamp: Date.now(),
88+
gridItems: [],
89+
hookItems: [],
90+
sourcePositionParams: DEFAULT_POSITION_PARAMS,
91+
};
92+
}
4093

94+
export function writeHookOnlyToClipboard(hookItems: ClipboardHookItem[]) {
95+
const payload = buildEmptyPayload();
96+
payload.hookItems = hookItems;
97+
writeToClipboard(payload);
98+
}
99+
100+
export class GridCompOperator {
41101
static copyComp(editorState: EditorState, compRecords: Record<string, Comp>) {
42102
const oldUi = editorState.getUIComp().getComp();
43103
if (!oldUi) {
@@ -65,24 +125,54 @@ export class GridCompOperator {
65125
layout: layout[key],
66126
}));
67127

68-
const toCopyComps = Object.values(compMap).filter((item) => !!item.item && !!item.layout);
69-
if (!toCopyComps || _.size(toCopyComps) <= 0) {
128+
const validComps = Object.values(compMap).filter((item) => !!item.item && !!item.layout);
129+
if (!validComps || _.size(validComps) <= 0) {
70130
messageInstance.info(trans("gridCompOperator.selectAtLeastOneComponent"));
71131
return false;
72132
}
73-
this.copyComps = toCopyComps;
74-
this.sourcePositionParams = simpleContainer.children.positionParams.getView();
75133

76-
// log.debug( "copyComp. toCopyComps: ", this.copyComps, " sourcePositionParams: ", this.sourcePositionParams);
134+
const sourcePositionParams = simpleContainer.children.positionParams.getView();
135+
const nameGenerator = editorState.getNameGenerator();
136+
137+
const gridItems: ClipboardGridItem[] = validComps.map((comp) => {
138+
const itemComp = comp.item as GridItemComp;
139+
const compType = itemComp.children.compType.getView();
140+
const name = itemComp.children.name.getView();
141+
const innerComp = itemComp.children.comp;
142+
const isContainerComp = isContainer(innerComp);
143+
const compJSON = isContainerComp
144+
? {
145+
...innerComp.toJsonValue(),
146+
...innerComp.getPasteValue(nameGenerator) as Record<string, any>,
147+
}
148+
: innerComp.toJsonValue();
149+
return { compType, comp: compJSON, name, layout: comp.layout, isContainer: isContainerComp };
150+
});
151+
152+
const payload = buildEmptyPayload();
153+
payload.sourcePositionParams = sourcePositionParams;
154+
payload.gridItems = gridItems;
155+
writeToClipboard(payload);
156+
77157
return true;
78158
}
79159

80-
// FALK TODO: How can we enable Copy and Paste of components across Browser Tabs / Windows?
81-
static pasteComp(editorState: EditorState) {
82-
if (!this.copyComps || _.size(this.copyComps) <= 0 || !this.sourcePositionParams) {
83-
messageInstance.info(trans("gridCompOperator.selectCompFirst"));
160+
static pasteFromPayload(editorState: EditorState, payload: LowcoderClipboardPayload): boolean {
161+
if (payload.gridItems.length === 0) {
84162
return false;
85163
}
164+
return this.doPaste(
165+
editorState,
166+
payload.gridItems,
167+
payload.sourcePositionParams || DEFAULT_POSITION_PARAMS,
168+
);
169+
}
170+
171+
private static doPaste(
172+
editorState: EditorState,
173+
items: ClipboardGridItem[],
174+
sourcePositionParams: PositionParams,
175+
): boolean {
86176
const oldUi = editorState.getUIComp().getComp();
87177
if (!oldUi) {
88178
messageInstance.info(trans("gridCompOperator.notSupport"));
@@ -93,18 +183,8 @@ export class GridCompOperator {
93183
messageInstance.warning(trans("gridCompOperator.noContainerSelected"));
94184
return false;
95185
}
186+
96187
const selectedComps = editorState.selectedComps();
97-
const isSelectingContainer =
98-
_.size(selectedComps) === 1 &&
99-
(Object.values(selectedComps)[0] as GridItemComp)?.children?.comp === selectedContainer;
100-
if (_.size(this.copyComps) === 1) {
101-
const { item } = this.copyComps[0];
102-
// Special case: To paste a container, and the container is currently selected, paste it outside the selected container
103-
if (isContainer((item as GridItemComp).children.comp) && isSelectingContainer) {
104-
selectedContainer =
105-
editorState.findContainer(Object.keys(selectedComps)[0]) ?? selectedContainer;
106-
}
107-
}
108188

109189
const selectedSimpleContainer =
110190
selectedContainer.realSimpleContainer(Object.keys(selectedComps)[0]) ??
@@ -115,43 +195,39 @@ export class GridCompOperator {
115195
const multiAddActions: Array<CustomAction<any>> = [];
116196
const copyLayouts: Layout = {};
117197
const copyCompNames = new Set<string>();
118-
// log.debug("pasteComps. sourceContainer: ", this.sourceContainer, " targetContainer: ", selectedContainer);
119-
this.copyComps.forEach((comp) => {
198+
199+
items.forEach((item) => {
120200
const key = genRandomKey();
121201
const { w, h } = switchLayoutWH(
122-
comp.layout.w,
123-
comp.layout.h,
124-
this.sourcePositionParams,
202+
item.layout.w,
203+
item.layout.h,
204+
sourcePositionParams,
125205
selectedSimpleContainer.children.positionParams.getView()
126206
);
127207
copyLayouts[key] = {
128-
...comp.layout,
208+
...item.layout,
129209
i: key,
130-
x: comp.layout.x + baseX,
131-
y: comp.layout.y + baseY,
210+
x: item.layout.x + baseX,
211+
y: item.layout.y + baseY,
132212
w,
133213
h,
134214
isDragging: true,
135215
};
136-
const itemComp = comp.item as GridItemComp;
137-
const compType = itemComp.children.compType.getView();
138-
const compInfo = parseCompType(compType);
216+
const compInfo = parseCompType(item.compType);
139217
const compName = nameGenerator.genItemName(compInfo.compName);
140-
const compJSONValue = isContainer(itemComp.children.comp)
141-
? {
142-
...itemComp.children.comp.toJsonValue(),
143-
...itemComp.children.comp.getPasteValue(nameGenerator) as Record<string, any>,
144-
}
145-
: itemComp.children.comp.toJsonValue();
218+
const compJSONValue = item.isContainer
219+
? remapContainerPasteValue(item.comp, nameGenerator)
220+
: item.comp;
146221
copyCompNames.add(compName);
147222
multiAddActions.push(
148223
(oldUi.realSimpleContainer() as SimpleContainerComp).children.items.addAction(key, {
149-
compType: compType,
224+
compType: item.compType,
150225
comp: compJSONValue,
151226
name: compName,
152227
})
153228
);
154229
});
230+
155231
selectedSimpleContainer.dispatch(
156232
multiChangeAction({
157233
layout: selectedSimpleContainer.children.layout.changeValueAction({
@@ -281,3 +357,46 @@ export class GridCompOperator {
281357
messageInstance.success(trans("comp.upgradeSuccess"));
282358
}
283359
}
360+
361+
/**
362+
* Remap keys and names inside a serialized container JSON.
363+
* Mirrors what SimpleContainerComp.getPasteValue() does, but operates on
364+
* plain JSON so it works for cross-app clipboard payloads where we don't
365+
* have live comp instances.
366+
*/
367+
function remapContainerPasteValue(compJson: any, nameGenerator: NameGenerator): any {
368+
if (!compJson || typeof compJson !== "object") return compJson;
369+
370+
const items = compJson.items;
371+
const layout = compJson.layout;
372+
if (!items || typeof items !== "object") return compJson;
373+
374+
const keyMap: Record<string, string> = {};
375+
Object.keys(items).forEach((oldKey) => {
376+
keyMap[oldKey] = genRandomKey();
377+
});
378+
379+
const newItems: Record<string, any> = {};
380+
Object.entries(items).forEach(([oldKey, itemValue]: [string, any]) => {
381+
const newKey = keyMap[oldKey];
382+
const compType = itemValue?.compType;
383+
const newName = compType ? nameGenerator.genItemName(compType) : genRandomKey();
384+
const innerComp = itemValue?.comp;
385+
const remappedComp = innerComp?.items
386+
? remapContainerPasteValue(innerComp, nameGenerator)
387+
: innerComp;
388+
newItems[newKey] = { ...itemValue, name: newName, comp: remappedComp };
389+
});
390+
391+
let newLayout = layout;
392+
if (layout && typeof layout === "object") {
393+
const remapped: Record<string, any> = {};
394+
Object.entries(layout).forEach(([oldKey, layoutItem]: [string, any]) => {
395+
const newKey = keyMap[oldKey] || oldKey;
396+
remapped[newKey] = { ...layoutItem, i: newKey };
397+
});
398+
newLayout = remapped;
399+
}
400+
401+
return { ...compJson, items: newItems, layout: newLayout };
402+
}
Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { HookComp } from "comps/hooks/hookComp";
22
import { EditorState } from "comps/editorState";
33
import { singletonHookComp } from "comps/hooks/hookCompTypes";
4-
import { wrapActionExtraInfo, type Comp } from "lowcoder-core";
4+
import { wrapActionExtraInfo } from "lowcoder-core";
55
import { messageInstance } from "lowcoder-design";
66
import { trans } from "i18n";
7+
import {
8+
writeHookOnlyToClipboard,
9+
type ClipboardHookItem,
10+
type LowcoderClipboardPayload,
11+
} from "./gridCompOperator";
712

813
export class HookCompOperator {
9-
private static copyHooks: HookComp[] = [];
10-
11-
static copyComp(editorState: EditorState, compRecords: Record<string, Comp>) {
14+
static copyComp(editorState: EditorState): boolean {
1215
const selectedNames = Array.from(editorState.selectedCompNames);
1316
if (!selectedNames.length) {
1417
return false;
@@ -26,52 +29,50 @@ export class HookCompOperator {
2629
return false;
2730
}
2831

29-
this.copyHooks = selectedHookComps;
32+
const hookItems: ClipboardHookItem[] = selectedHookComps.map((hookComp) => {
33+
const compType = hookComp.children.compType.getView();
34+
const name = hookComp.children.name.getView();
35+
const childComp: any = hookComp.children.comp;
36+
const baseValue = childComp?.toJsonValue ? childComp.toJsonValue() : {};
37+
const pasteValue = childComp?.getPasteValue?.(editorState.getNameGenerator()) ?? {};
38+
const comp = { ...baseValue, ...pasteValue };
39+
const fullValue = hookComp.toJsonValue();
40+
return { compType, comp, name, fullValue };
41+
});
42+
43+
writeHookOnlyToClipboard(hookItems);
3044
messageInstance.success(trans("copySuccess"));
3145
return true;
3246
}
3347

34-
static clearCopy() {
35-
this.copyHooks = [];
36-
}
37-
38-
39-
static pasteComp(editorState: EditorState) {
40-
if (!this.copyHooks.length) {
41-
messageInstance.info(trans("gridCompOperator.selectCompFirst"));
48+
static pasteFromPayload(editorState: EditorState, payload: LowcoderClipboardPayload): boolean {
49+
if (payload.hookItems.length === 0) {
4250
return false;
4351
}
4452

4553
const hooksComp = editorState.getHooksComp();
4654
const nameGenerator = editorState.getNameGenerator();
4755
const newNames = new Set<string>();
4856

49-
this.copyHooks.forEach((hookComp) => {
50-
const compType = hookComp.children.compType.getView();
51-
const newName = nameGenerator.genItemName(compType);
52-
const childComp: any = hookComp.children.comp;
53-
const baseValue = childComp?.toJsonValue ? childComp.toJsonValue() : {};
54-
const pasteValue =
55-
childComp?.getPasteValue?.(nameGenerator) ?? {};
57+
payload.hookItems.forEach((item) => {
58+
const newName = nameGenerator.genItemName(item.compType);
5659

57-
const payload = {
58-
...(hookComp.toJsonValue() as any),
60+
const dispatchPayload = {
61+
...(item.fullValue || {}),
5962
name: newName,
60-
comp: {
61-
...baseValue,
62-
...pasteValue,
63-
},
63+
compType: item.compType,
64+
comp: item.comp,
6465
};
6566

6667
hooksComp.dispatch(
6768
wrapActionExtraInfo(
68-
hooksComp.pushAction(payload),
69+
hooksComp.pushAction(dispatchPayload),
6970
{
7071
compInfos: [
7172
{
7273
type: "add",
7374
compName: newName,
74-
compType,
75+
compType: item.compType,
7576
},
7677
],
7778
}
@@ -85,4 +86,3 @@ export class HookCompOperator {
8586
return true;
8687
}
8788
}
88-

0 commit comments

Comments
 (0)