Skip to content

Commit 4f787b4

Browse files
authored
feat(ui): improve JSON copy functionality in state explorer - AB-242 (#1107)
* refactor: replace inline object checks with isPlainObject utility function * feat(selectionToJson): implement selection parsing logic for JSON subset extraction - AB-242 * feat(SharedJsonViewer): add copy to clipboard selection functionality * fix(SharedJsonViewer): avoid copying empty objects when a simple value is selected * fix(SharedJsonViewer): remove unnecessary space argument in prettyJson
1 parent 6a75bdc commit 4f787b4

File tree

8 files changed

+742
-11
lines changed

8 files changed

+742
-11
lines changed

src/ui/src/builder/useComponentClipboard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isPlainObject } from "@/utils/object";
12
import { useLogger } from "@/composables/useLogger";
23
import { Component } from "@/writerTypes";
34

@@ -8,8 +9,7 @@ type ClipboardData = {
89

910
function isClipboardData(data: unknown): data is ClipboardData {
1011
return (
11-
typeof data === "object" &&
12-
data !== null &&
12+
isPlainObject(data) &&
1313
"type" in data &&
1414
data.type === "writer/clipboard" &&
1515
"components" in data &&

src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewer.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isPlainObject } from "@/utils/object";
12
import type {
23
JsonData,
34
JsonValue,
@@ -18,7 +19,7 @@ export function isJSONArray(data: JsonData): data is JsonData[] {
1819
export function isJSONObject(
1920
data: JsonData,
2021
): data is { [x: string]: JsonData } {
21-
return !isJSONArray(data) && typeof data === "object" && data !== null;
22+
return isPlainObject(data);
2223
}
2324

2425
export function getJSONLength(data: JsonData): number {

src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewer.vue

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
:open="isRootOpen"
77
:data="data"
88
@toggle="$emit('toggle', { path: [], open: $event })"
9+
@copy="onCopyToClipboard($event, data)"
910
>
1011
<SharedJsonViewerObject
1112
:data="data"
@@ -20,6 +21,7 @@
2021
:path="path"
2122
:initial-depth="initialDepth"
2223
@toggle="$emit('toggle', $event)"
24+
@copy="onCopyToClipboard($event, data)"
2325
/>
2426
</template>
2527
<SharedJsonViewerValue v-else-if="isJSONValue(data)" :data="data" />
@@ -50,6 +52,8 @@ export type JsonViewerTogglePayload = { path: JsonPath; open: boolean };
5052
* This component will detect the shape of the JSON and redirect the right dedicated component.
5153
*/
5254
import { PropType, computed } from "vue";
55+
import { useClipboard } from "@vueuse/core";
56+
import { isPlainObject } from "@/utils/object";
5357
import {
5458
isJSONArray,
5559
isJSONObject,
@@ -62,6 +66,7 @@ import SharedJsonViewerValue from "./SharedJsonViewerValue.vue";
6266
import SharedJsonViewerChildrenCounter from "./SharedJsonViewerChildrenCounter.vue";
6367
import SharedControlBar from "../SharedControlBar.vue";
6468
import { defineAsyncComponentWithLoader } from "@/utils/defineAsyncComponentWithLoader";
69+
import { selectionToJson } from "./selectionToJson";
6570
6671
const SharedCopyClipboardButton = defineAsyncComponentWithLoader({
6772
loader: () => import("@/components/shared/SharedCopyClipboardButton.vue"),
@@ -99,8 +104,49 @@ const isRoot = computed(() => props.path.length === 0 && !props.hideRoot);
99104
const isRootOpen = computed(
100105
() => props.initialDepth === -1 || props.initialDepth > 0,
101106
);
102-
const dataAsString = computed(() => {
103-
if (props.data === undefined) return JSON.stringify(null);
104-
return JSON.stringify(props.data);
105-
});
107+
108+
function prettyJson(input: unknown) {
109+
if (input === undefined) {
110+
return JSON.stringify(null);
111+
}
112+
113+
try {
114+
return JSON.stringify(input, null, 2);
115+
} catch {
116+
return JSON.stringify(null);
117+
}
118+
}
119+
120+
const dataAsString = computed(() => prettyJson(props.data));
121+
122+
const { copy } = useClipboard();
123+
124+
function isEmptySubset(subset: Record<string, unknown> | unknown[]): boolean {
125+
if (Array.isArray(subset)) return subset.length === 0;
126+
if (isPlainObject(subset)) return Object.keys(subset).length === 0;
127+
return false;
128+
}
129+
130+
function onCopyToClipboard(
131+
event: ClipboardEvent,
132+
arrayOrObject: Record<string, JsonData> | JsonData[],
133+
) {
134+
event.stopPropagation();
135+
136+
const selectionText = (window.getSelection()?.toString() ?? "").trim();
137+
138+
if (!selectionText) {
139+
return; // no selection → do nothing
140+
}
141+
142+
const subset = selectionToJson(arrayOrObject, selectionText);
143+
144+
if (isEmptySubset(subset)) {
145+
return; // nothing meaningful recognized → allow native copy of raw text
146+
}
147+
148+
event.preventDefault();
149+
150+
copy(prettyJson(subset)); // copy subset
151+
}
106152
</script>

src/ui/src/components/shared/SharedJsonViewer/SharedJsonViewerObject.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ import SharedJsonViewerValue from "./SharedJsonViewerValue.vue";
4747
4848
const props = defineProps({
4949
data: {
50-
type: [Object, Array] as PropType<JsonData>,
50+
type: [Object, Array] as PropType<
51+
Record<string, JsonData> | JsonData[]
52+
>,
5153
required: true,
5254
},
5355
path: {

0 commit comments

Comments
 (0)