Skip to content

Commit 8ebbffd

Browse files
authored
542 add copy and paste functionality to schema diagram (#955)
* Make Ace Editor cursor update not change focus * Add copy and paste functionality to schema diagram * apply formatting changes --------- Co-authored-by: Logende <[email protected]>
1 parent 55daef4 commit 8ebbffd

4 files changed

Lines changed: 325 additions & 9 deletions

File tree

meta_configurator/src/components/panels/code-editor/aceUtility.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export function updateCursorPositionBasedOnPath(
3030
currentPath: Path
3131
) {
3232
const position = determineCursorPosition(editor, editorContent, currentPath);
33-
editor.gotoLine(position.row + 1, position.column, true); // row is 1-based, column is 0-based
33+
editor.clearSelection();
34+
editor.moveCursorToPosition(position);
35+
editor.renderer.scrollCursorIntoView();
3436
}
3537

3638
/**

meta_configurator/src/components/panels/schema-diagram/VueFlowPanel.vue

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ import SchemaExternalReferenceNode from '@/components/panels/schema-diagram/Sche
4848
import $RefParser from '@apidevtools/json-schema-ref-parser';
4949
import {useErrorService} from '@/utility/errorServiceInstance.ts';
5050
import {pathToJsonPointer} from '@/utility/pathUtils.ts';
51-
import {stringToIdentifier, urlStringToIdentifier} from '@/utility/stringToIdentifier.ts';
51+
import {urlStringToIdentifier} from '@/utility/stringToIdentifier.ts';
52+
import {
53+
copySelectedSchemaToClipboard,
54+
pasteSchemaFromClipboard,
55+
} from '@/components/panels/schema-diagram/schemaClipboardUtils';
5256
5357
const emit = defineEmits<{
5458
(e: 'update_current_path', path: Path): void;
@@ -110,6 +114,33 @@ onMounted(() => {
110114
});
111115
});
112116
117+
async function handleCopy(event?: ClipboardEvent) {
118+
await copyToClipboard(event);
119+
event?.preventDefault(); // override default browser copy
120+
}
121+
async function handlePaste(event?: ClipboardEvent) {
122+
await pasteFromClipboard(event);
123+
event?.preventDefault(); // override default browser paste
124+
}
125+
126+
async function copyToClipboard(event?: ClipboardEvent) {
127+
await copySelectedSchemaToClipboard(
128+
event,
129+
schemaData,
130+
selectedData.value,
131+
schemaSession.currentSelectedElement.value
132+
);
133+
}
134+
135+
async function pasteFromClipboard(event?: ClipboardEvent) {
136+
try {
137+
const pastedPath = await pasteSchemaFromClipboard(event, schemaData, selectedData.value);
138+
selectElement(pastedPath);
139+
} catch (err) {
140+
console.error('Failed to read from clipboard:', err);
141+
}
142+
}
143+
113144
// scroll to the current selected element when it changes
114145
watch(
115146
schemaSession.currentSelectedElement,
@@ -424,7 +455,7 @@ function updateExternalReferenceValue(
424455
</script>
425456

426457
<template>
427-
<div class="layout-flow">
458+
<div class="layout-flow" tabindex="0" @copy="handleCopy" @paste="handlePaste">
428459
<VueFlow
429460
:nodes="activeNodes"
430461
:edges="activeEdges"
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import type {ManagedData} from '@/data/managedData';
2+
import {
3+
SchemaElementData,
4+
SchemaObjectAttributeData,
5+
SchemaObjectNodeData,
6+
} from '@/schema/graph-representation/schemaGraphTypes';
7+
import {findAvailableSchemaId} from '@/schema/schemaReadingUtils';
8+
import {
9+
addSchemaEnum,
10+
addSchemaObject,
11+
createIdentifierForExtractedElement,
12+
extractInlinedSchemaElement,
13+
} from '@/schema/schemaManipulationUtils';
14+
import type {Path} from '@/utility/path';
15+
16+
export type ClipboardSchemaPayload = {
17+
name: string;
18+
schema: any;
19+
};
20+
21+
export async function copySelectedSchemaToClipboard(
22+
event: ClipboardEvent | undefined,
23+
schemaData: ManagedData,
24+
selectedData: SchemaElementData | undefined,
25+
selectedPath: Path
26+
) {
27+
if (!selectedData) {
28+
console.log('No element selected to copy');
29+
return;
30+
}
31+
32+
const schema = schemaData.dataAt(selectedPath);
33+
let metaType = selectedData.getNodeType();
34+
35+
if (schema.enum != undefined) {
36+
metaType = 'schemaenum';
37+
}
38+
39+
const dataToCopy = structuredClone(schema);
40+
if (!dataToCopy.type && metaType === 'schemaobject') {
41+
dataToCopy.type = 'object';
42+
}
43+
44+
const clipboardText = JSON.stringify(
45+
{
46+
[getClipboardNameForSelectedSchema(selectedData)]: dataToCopy,
47+
},
48+
null,
49+
2
50+
);
51+
52+
try {
53+
if (event?.clipboardData) {
54+
event.clipboardData.setData('text/plain', clipboardText);
55+
return;
56+
}
57+
await navigator.clipboard.writeText(clipboardText);
58+
console.log('Copied to system clipboard:', dataToCopy);
59+
} catch (err) {
60+
console.error('Failed to write to clipboard:', err);
61+
}
62+
}
63+
64+
export async function pasteSchemaFromClipboard(
65+
event: ClipboardEvent | undefined,
66+
schemaData: ManagedData,
67+
selectedData: SchemaElementData | undefined
68+
) {
69+
const clipboardText =
70+
event?.clipboardData?.getData('text/plain') ?? (await navigator.clipboard.readText());
71+
const payload = normalizeClipboardSchemaPayload(JSON.parse(clipboardText));
72+
73+
if (isComplexSchema(payload.schema)) {
74+
return addComplexSchemaToDefinitions(schemaData, payload);
75+
}
76+
77+
return addPrimitiveSchemaAsAttribute(schemaData, selectedData, payload);
78+
}
79+
80+
export function inferSchemaKind(
81+
schema: any
82+
): 'object' | 'array' | 'enum' | 'attribute' | 'unknown' {
83+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
84+
return 'unknown';
85+
}
86+
87+
if (schema.enum !== undefined) {
88+
return 'enum';
89+
}
90+
if (isPrimitiveArraySchema(schema)) {
91+
return 'attribute';
92+
}
93+
if (schema.type === 'array' || schema.items !== undefined) {
94+
return 'array';
95+
}
96+
if (schema.type === 'object' || schema.properties || schema.$ref) {
97+
return 'object';
98+
}
99+
if (
100+
['string', 'boolean', 'number', 'null', 'integer'].includes(schema.type) ||
101+
schema.const !== undefined
102+
) {
103+
return 'attribute';
104+
}
105+
106+
return 'unknown';
107+
}
108+
109+
function getClipboardNameForSelectedSchema(selectedData: SchemaElementData) {
110+
return createIdentifierForExtractedElement(
111+
selectedData.name,
112+
selectedData.title,
113+
selectedData.fallbackDisplayName
114+
);
115+
}
116+
117+
function normalizeClipboardSchemaPayload(data: any): ClipboardSchemaPayload {
118+
if (isWrappedSchemaPayload(data)) {
119+
const [name, schema] = Object.entries(data)[0] as [string, any];
120+
return {
121+
name: name.trim() || deriveNameFromSchema(schema),
122+
schema,
123+
};
124+
}
125+
126+
return {
127+
name: defaultNameForSchema(data),
128+
schema: data,
129+
};
130+
}
131+
132+
function deriveNameFromSchema(schema: any) {
133+
if (schema?.title && typeof schema.title === 'string') {
134+
return schema.title;
135+
}
136+
if (schema?.type === 'object' || schema?.properties || schema?.$ref) {
137+
return 'object';
138+
}
139+
if (schema?.type === 'array' || schema?.items) {
140+
return 'array';
141+
}
142+
if (schema?.enum !== undefined) {
143+
return 'enum';
144+
}
145+
if (typeof schema?.type === 'string') {
146+
return schema.type;
147+
}
148+
return 'schema';
149+
}
150+
151+
function isWrappedSchemaPayload(data: any) {
152+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
153+
return false;
154+
}
155+
156+
const entries = Object.entries(data);
157+
if (entries.length !== 1) {
158+
return false;
159+
}
160+
161+
return inferSchemaKind(data) === 'unknown' && inferSchemaKind(entries[0]![1]) !== 'unknown';
162+
}
163+
164+
function isComplexSchema(schema: any) {
165+
return ['object', 'array', 'enum'].includes(inferSchemaKind(schema));
166+
}
167+
168+
function defaultNameForSchema(schema: any) {
169+
const schemaKind = inferSchemaKind(schema);
170+
171+
if (schemaKind === 'attribute') {
172+
return 'attr';
173+
}
174+
if (schemaKind === 'object' || schemaKind === 'array') {
175+
return 'object';
176+
}
177+
if (schemaKind === 'enum') {
178+
return 'enum';
179+
}
180+
181+
return 'schema';
182+
}
183+
184+
function addComplexSchemaToDefinitions(schemaData: ManagedData, payload: ClipboardSchemaPayload) {
185+
const preferredName = payload.name.trim() || deriveNameFromSchema(payload.schema);
186+
const schemaKind = inferSchemaKind(payload.schema);
187+
188+
if (schemaKind === 'enum') {
189+
return addSchemaEnum(schemaData, payload.schema, preferredName);
190+
}
191+
192+
const elementPath = addSchemaObject(schemaData, false, payload.schema, preferredName);
193+
194+
if (schemaKind === 'array') {
195+
return extractArrayItemsToDefinition(schemaData, elementPath, payload);
196+
}
197+
198+
return elementPath;
199+
}
200+
201+
function addPrimitiveSchemaAsAttribute(
202+
schemaData: ManagedData,
203+
selectedData: SchemaElementData | undefined,
204+
payload: ClipboardSchemaPayload
205+
) {
206+
const targetObjectPath = getSelectedObjectPathForAttributePaste(selectedData);
207+
if (!targetObjectPath) {
208+
throw new Error('Primitive schemas can only be pasted when an object is selected.');
209+
}
210+
211+
const attributePath = findAvailableSchemaId(
212+
schemaData,
213+
[...targetObjectPath, 'properties'],
214+
payload.name.trim() || 'property',
215+
true
216+
);
217+
218+
schemaData.setDataAt(attributePath, payload.schema);
219+
return attributePath;
220+
}
221+
222+
function getSelectedObjectPathForAttributePaste(selectedData: SchemaElementData | undefined) {
223+
if (selectedData instanceof SchemaObjectNodeData) {
224+
return selectedData.absolutePath;
225+
}
226+
227+
if (selectedData instanceof SchemaObjectAttributeData) {
228+
return selectedData.absolutePath.slice(0, -2);
229+
}
230+
231+
return undefined;
232+
}
233+
234+
function isPrimitiveArraySchema(schema: any) {
235+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
236+
return false;
237+
}
238+
239+
if (!(schema.type === 'array' || schema.items !== undefined)) {
240+
return false;
241+
}
242+
243+
const items = schema.items;
244+
if (!items || typeof items !== 'object' || Array.isArray(items)) {
245+
return false;
246+
}
247+
248+
return inferSchemaKind(items) === 'attribute';
249+
}
250+
251+
function extractArrayItemsToDefinition(
252+
schemaData: ManagedData,
253+
arrayPath: Path,
254+
payload: ClipboardSchemaPayload
255+
) {
256+
const arraySchema = schemaData.dataAt(arrayPath);
257+
if (!arraySchema?.items || typeof arraySchema.items !== 'object' || arraySchema.items.$ref) {
258+
return arrayPath;
259+
}
260+
261+
const itemName = createIdentifierForExtractedElement(
262+
undefined,
263+
arraySchema.items.title,
264+
payload.name + 'Item'
265+
);
266+
267+
return extractInlinedSchemaElement([...arrayPath, 'items'], schemaData, itemName, true);
268+
}

meta_configurator/src/schema/schemaManipulationUtils.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,26 @@ export function addSchemaObject(
153153
return objectPath;
154154
}
155155

156-
export function addSchemaEnum(schemaData: ManagedData) {
157-
const enumPath = findAvailableSchemaId(schemaData, ['$defs'], 'enum');
158-
schemaData.setDataAt(enumPath, {
159-
type: 'string',
160-
enum: ['VAL_1', 'VAL_2'],
161-
});
156+
export function addSchemaEnum(
157+
schemaData: ManagedData,
158+
schema: any = undefined,
159+
identifier: string | undefined = undefined
160+
) {
161+
let enumPath: Path;
162+
163+
if (identifier !== undefined) {
164+
enumPath = findAvailableSchemaId(schemaData, ['$defs'], identifier, true);
165+
} else {
166+
enumPath = findAvailableSchemaId(schemaData, ['$defs'], 'enum');
167+
}
168+
169+
if (schema !== undefined) {
170+
schemaData.setDataAt(enumPath, schema);
171+
} else {
172+
schemaData.setDataAt(enumPath, {
173+
type: 'string',
174+
enum: ['VAL_1', 'VAL_2'],
175+
});
176+
}
162177
return enumPath;
163178
}

0 commit comments

Comments
 (0)