Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 11 additions & 68 deletions aas-web-ui/src/components/SubmodelElementJSONView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,13 @@
/* eslint-enable simple-import-sort/imports */
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useAASStore } from '@/store/AASDataStore';
import { useClipboardUtil } from '@/composables/ClipboardUtil';

// Stores
const aasStore = useAASStore();

const { cleanObjectRecursively } = useClipboardUtil();

// Reactive variables
const jsonContent = ref<string>('');
const formattedJson = ref<string>('');
Expand All @@ -145,8 +148,8 @@
const selectedNode = computed(() => aasStore.getSelectedNode);

const lineCount = computed(() => {
if (!formattedJson.value) return 0;
return formattedJson.value.split('\n').length;
if (!jsonContent.value) return 0;
return jsonContent.value.split('\n').length;
});

onMounted(() => {
Expand Down Expand Up @@ -193,24 +196,6 @@
}
});

function removePathProperties(obj: unknown): unknown {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
return obj.map((item) => removePathProperties(item));
}

const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
if (key !== 'path') {
result[key] = removePathProperties(value);
}
}
return result;
}

function processSelectedNode(): void {
loading.value = true;
error.value = null;
Expand All @@ -229,23 +214,8 @@
// Create a copy of the selected node
const nodeCopy = JSON.parse(JSON.stringify(selectedNode.value));

// Remove timestamp property from top level
if ('timestamp' in nodeCopy) {
delete nodeCopy.timestamp;
}

// Remove conceptDescriptions property from top level
if ('conceptDescriptions' in nodeCopy) {
delete nodeCopy.conceptDescriptions;
}

// Remove endpoints property from top level
if ('endpoints' in nodeCopy) {
delete nodeCopy.endpoints;
}

// Remove all path properties recursively
const cleanedNode = removePathProperties(nodeCopy);
// Clean the selected node
const cleanedNode = cleanObjectRecursively(nodeCopy);

jsonContent.value = JSON.stringify(cleanedNode, null, 2);

Expand Down Expand Up @@ -315,53 +285,26 @@
});
}

function formatJSON(json: string): string {
try {
// Check if input is valid
if (!json || typeof json !== 'string') {
return '';
}

const trimmedJson = json.trim();
if (!trimmedJson) {
return '';
}

try {
const obj = JSON.parse(trimmedJson);
return JSON.stringify(obj, null, 2);
} catch (parseError) {
console.warn('JSON parsing warning:', parseError);
return trimmedJson;
}
} catch (e) {
console.error('Error formatting JSON:', e);
return json;
}
}

function highlightJson(): void {
if (!jsonContent.value) {
formattedJson.value = '';
return;
}

try {
const formatted = formatJSON(jsonContent.value);

// Apply syntax highlighting using Prism
// Apply syntax highlighting using Prism directly on the already cleaned and formatted jsonContent
if (
typeof window !== 'undefined' &&
window.Prism &&
window.Prism.highlight &&
window.Prism.languages.json
) {
formattedJson.value = window.Prism.highlight(formatted, window.Prism.languages.json, 'json');
formattedJson.value = window.Prism.highlight(jsonContent.value, window.Prism.languages.json, 'json');
} else if (Prism && Prism.highlight && Prism.languages.json) {
formattedJson.value = Prism.highlight(formatted, Prism.languages.json, 'json');
formattedJson.value = Prism.highlight(jsonContent.value, Prism.languages.json, 'json');
} else {
// Fallback to unformatted JSON if Prism is not available
formattedJson.value = formatted;
formattedJson.value = jsonContent.value;
console.warn('Prism highlighting not available, using plain text');
}
} catch (e) {
Expand Down
24 changes: 22 additions & 2 deletions aas-web-ui/src/components/UIComponents/Treeview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@
</template>
<v-list-item-subtitle>Copy Submodel Endpoint</v-list-item-subtitle>
</v-list-item>
<!-- Copy SM as JSON -->
<v-list-item
@click.stop="copyJsonToClipboard(item, 'Submodel', copyJsonIconAsRef)">
<template #prepend>
<v-icon size="x-small">{{ copyJsonIcon }} </v-icon>
</template>
<v-list-item-subtitle>Copy Submodel as JSON</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-sheet>
</v-menu>
Expand Down Expand Up @@ -254,7 +262,7 @@
<v-list-item-subtitle>Delete {{ item.modelType }}</v-list-item-subtitle>
</v-list-item>
<v-divider></v-divider>
<!-- Copy SM endpoint to clipboard -->
<!-- Copy SME endpoint to clipboard -->
<v-list-item @click="copyToClipboard(item.path, 'Path', copyIconAsRef)">
<template #prepend>
<v-icon size="x-small">{{ copyIcon }} </v-icon>
Expand All @@ -263,6 +271,16 @@
>Copy {{ item.modelType }} Endpoint</v-list-item-subtitle
>
</v-list-item>
<!-- Copy SME as JSON -->
<v-list-item
@click.stop="copyJsonToClipboard(item, item.modelType, copyJsonIconAsRef)">
<template #prepend>
<v-icon size="x-small">{{ copyJsonIcon }} </v-icon>
</template>
<v-list-item-subtitle
>Copy {{ item.modelType }} as JSON</v-list-item-subtitle
>
</v-list-item>
</v-list>
</v-sheet>
</v-menu>
Expand Down Expand Up @@ -301,7 +319,7 @@

// Composables
const { nameToDisplay } = useReferableUtils();
const { copyToClipboard } = useClipboardUtil();
const { copyToClipboard, copyJsonToClipboard } = useClipboardUtil();

// Stores
const navigationStore = useNavigationStore();
Expand Down Expand Up @@ -330,12 +348,14 @@

// Data
const copyIcon = ref<string>('mdi-clipboard-file-outline');
const copyJsonIcon = ref<string>('mdi-clipboard-text-outline');

// Computed Properties
const selectedNode = computed(() => aasStore.getSelectedNode);
const editorMode = computed(() => ['AASEditor', 'SMEditor'].includes(route.name as string));
const isMobile = computed(() => navigationStore.getIsMobile);
const copyIconAsRef = computed(() => copyIcon);
const copyJsonIconAsRef = computed(() => copyJsonIcon);

function toggleTree(smOrSme: any): void {
smOrSme.showChildren = !smOrSme.showChildren;
Expand Down
121 changes: 119 additions & 2 deletions aas-web-ui/src/composables/ClipboardUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ export function useClipboardUtil() {
iconReference.value = 'mdi-check';

// copy value to clipboard
navigator.clipboard.writeText(value);
try {
navigator.clipboard.writeText(value);
} catch {
navigationStore.dispatchSnackbar({
status: true,
timeout: 4000,
color: 'false',
btnColor: 'buttonText',
text: 'Failed to copy JSON to Clipboard.',
});
}

// set the clipboard tooltip to false after 1.5 seconds
setTimeout(() => {
Expand All @@ -27,5 +37,112 @@ export function useClipboardUtil() {
});
}

return { copyToClipboard };
function copyJsonToClipboard(value: unknown, valueName: string, iconReference: { value: string }): void {
if (!value) return;

// Clean the JSON object recursively
const cleanedValue = cleanObjectRecursively(value);

iconReference.value = 'mdi-check';

// copy value to clipboard
try {
navigator.clipboard.writeText(JSON.stringify(cleanedValue, null, 2));
} catch {
navigationStore.dispatchSnackbar({
status: true,
timeout: 4000,
color: 'false',
btnColor: 'buttonText',
text: 'Failed to copy JSON to Clipboard.',
});
}

// set the clipboard tooltip to false after 1.5 seconds
setTimeout(() => {
iconReference.value = 'mdi-clipboard-text-outline';
}, 2000);

// open Snackbar to inform the user that the path was copied to the clipboard
navigationStore.dispatchSnackbar({
status: true,
timeout: 2000,
color: 'success',
btnColor: 'buttonText',
text:
(valueName.trim() !== ''
? valueName
: typeof cleanedValue === 'object' && cleanedValue !== null && 'modelType' in cleanedValue
? (cleanedValue as { modelType?: string }).modelType || 'JSON'
: 'JSON') + ' copied to Clipboard.',
});
}

function cleanObjectRecursively(obj: unknown): unknown {
if (obj === null || obj === undefined) {
return obj;
}

// If it's an array, recursively clean each element
if (Array.isArray(obj)) {
return obj.map((item) => cleanObjectRecursively(item));
}

// If it's an object, create a copy and recursively clean it
if (typeof obj === 'object') {
const cleaned = { ...(obj as Record<string, unknown>) };

// Remove tree-specific properties that were added by prepareForTree
delete cleaned.showChildren;
delete cleaned.parent;
delete cleaned.path;
delete cleaned.timestamp;
delete cleaned.conceptDescriptions;
delete cleaned.idLower;
delete cleaned.idShortLower;
delete cleaned.nameLower;
delete cleaned.descLower;
delete cleaned.endpoints;

// Remove id property for all elements except Submodels
if (cleaned.modelType !== 'Submodel') {
delete cleaned.id;
}

// Restore original structure based on modelType
if (cleaned.modelType === 'Submodel' && Array.isArray(cleaned.children)) {
// For Submodels, children should go back to submodelElements
cleaned.submodelElements = cleanObjectRecursively(cleaned.children);
delete cleaned.children;
} else if (
['SubmodelElementCollection', 'SubmodelElementList'].includes(cleaned.modelType as string) &&
Array.isArray(cleaned.children)
) {
// For Collections and Lists, children should go back to value
cleaned.value = cleanObjectRecursively(cleaned.children);
delete cleaned.children;
} else if (cleaned.modelType === 'Entity' && Array.isArray(cleaned.children)) {
// For Entities, children should go back to statements
cleaned.statements = cleanObjectRecursively(cleaned.children);
delete cleaned.children;
} else {
// Remove children property if it exists but doesn't match any known pattern
delete cleaned.children;
}

// Recursively clean all remaining properties
for (const key in cleaned) {
if (Object.prototype.hasOwnProperty.call(cleaned, key)) {
cleaned[key] = cleanObjectRecursively(cleaned[key]);
}
}

return cleaned;
}

// For primitive types, return as is
return obj;
}

return { copyToClipboard, copyJsonToClipboard, cleanObjectRecursively };
}
Loading