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
1 change: 1 addition & 0 deletions aas-web-ui/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ declare module 'vue' {
ImagePreview: typeof import('./components/Plugins/ImagePreview.vue')['default']
InvalidElement: typeof import('./components/SubmodelElements/InvalidElement.vue')['default']
JSONArrayProperty: typeof import('./components/Plugins/SubmodelElements/JSONArrayProperty.vue')['default']
JsonInsert: typeof import('./components/EditorComponents/JsonInsert.vue')['default']
JSONPreview: typeof import('./components/Plugins/JSONPreview.vue')['default']
LastSync: typeof import('./components/UIComponents/LastSync.vue')['default']
LineChart: typeof import('./components/Widgets/LineChart.vue')['default']
Expand Down
226 changes: 226 additions & 0 deletions aas-web-ui/src/components/EditorComponents/JsonInsert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
<template>
<v-dialog v-model="jsonInsertDialog" width="860" persistent>
<v-card>
<v-card-title>Insert {{ type }} from JSON</v-card-title>
<v-divider></v-divider>
<v-card-text class="bg-card pa-3">
<v-card>
<v-textarea
v-model="jsonInput"
:error-messages="jsonInputErrors"
variant="outlined"
rows="10"
hide-details
@update:model-value="clearErrorMessages" />
</v-card>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="closeDialog">Cancel</v-btn>
<v-btn color="primary" @click="insertJson">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script lang="ts" setup>
import type { JsonValue } from '@aas-core-works/aas-core3.0-typescript/jsonization';
import { jsonization, types as aasTypes } from '@aas-core-works/aas-core3.0-typescript';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useAASRepositoryClient } from '@/composables/Client/AASRepositoryClient';
import { useSMRepositoryClient } from '@/composables/Client/SMRepositoryClient';
import { useAASStore } from '@/store/AASDataStore';
import { useNavigationStore } from '@/store/NavigationStore';
import { extractEndpointHref } from '@/utils/AAS/DescriptorUtils';
import { base64Decode, base64Encode } from '@/utils/EncodeDecodeUtils';

const props = defineProps<{
modelValue: boolean;
type: 'Submodel' | 'SubmodelElement';
parentElement?: any;
}>();

const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void;
}>();

// Vue Router
const router = useRouter();

// Stores
const aasStore = useAASStore();
const navigationStore = useNavigationStore();

// Composables
const { postSubmodel, postSubmodelElement } = useSMRepositoryClient();
const { putAas } = useAASRepositoryClient();

// Data
const jsonInsertDialog = ref(false);
const jsonInput = ref<string | null>(null);
const jsonInputErrors = ref<string[]>([]);

// Computed Properties
const selectedAAS = computed(() => aasStore.getSelectedAAS); // Get the selected AAS from Store
const submodelRepoUrl = computed(() => navigationStore.getSubmodelRepoURL);

watch(
() => props.modelValue,
(value) => {
jsonInsertDialog.value = value;
}
);

watch(
() => jsonInsertDialog.value,
(value) => {
emit('update:modelValue', value);
}
);

function insertJson(): void {
if (!jsonInput.value || !isValidJson(jsonInput.value)) {
jsonInputErrors.value = ['Invalid JSON input'];
return;
}

// Parse JSON to Submodel/SubmodelElement
if (props.type === 'Submodel') {
insertSubmodel(JSON.parse(jsonInput.value));
} else {
insertSubmodelElement(JSON.parse(jsonInput.value));
}
}

async function insertSubmodel(json: JsonValue): Promise<void> {
// Parse JSON to Submodel
const instanceOrError = jsonization.submodelFromJsonable(json);
if (instanceOrError.error !== null) {
navigationStore.dispatchSnackbar({
status: true,
timeout: 4000,
color: 'error',
btnColor: 'buttonText',
text: 'Error parsing Submodel: ' + instanceOrError.error,
});
return;
}
const submodel = instanceOrError.mustValue();

// Create Submodel
await postSubmodel(submodel);
// Add Submodel Reference to AAS
await addSubmodelReferenceToAas(submodel);
// Fetch and dispatch Submodel
const path = submodelRepoUrl.value + '/' + base64Encode(submodel.id);
const aasEndpoint = extractEndpointHref(selectedAAS.value, 'AAS-3.0');
router.push({ query: { aas: aasEndpoint, path: path } });

closeDialog();
navigationStore.dispatchTriggerTreeviewReload();
}

async function insertSubmodelElement(json: JsonValue): Promise<void> {
const instanceOrError = jsonization.submodelElementFromJsonable(json);
if (instanceOrError.error !== null) {
navigationStore.dispatchSnackbar({
status: true,
timeout: 4000,
color: 'error',
btnColor: 'buttonText',
text: 'Error parsing SubmodelElement: ' + instanceOrError.error,
});
return;
}
const submodelElement = instanceOrError.mustValue();

if (props.parentElement.modelType === 'Submodel') {
// Create the property on the parent Submodel
await postSubmodelElement(submodelElement, props.parentElement.id);

const aasEndpoint = extractEndpointHref(selectedAAS.value, 'AAS-3.0');

// Navigate to the new property
router.push({
query: {
aas: aasEndpoint,
path: props.parentElement.path + '/submodel-elements/' + submodelElement.idShort,
},
});
} else {
// Extract the submodel ID and the idShortPath from the parentElement path
const splitted = props.parentElement.path.split('/submodel-elements/');
const submodelId = base64Decode(splitted[0].split('/submodels/')[1]);
const idShortPath = splitted[1];

// Create the property on the parent element
await postSubmodelElement(submodelElement, submodelId, idShortPath);

const aasEndpoint = extractEndpointHref(selectedAAS.value, 'AAS-3.0');

// Navigate to the new property
if (props.parentElement.modelType === 'SubmodelElementCollection') {
router.push({
query: {
aas: aasEndpoint,
path: props.parentElement.path + '.' + submodelElement.idShort,
},
});
}
}

closeDialog();
navigationStore.dispatchTriggerTreeviewReload();
}

async function addSubmodelReferenceToAas(submodel: aasTypes.Submodel): Promise<void> {
if (selectedAAS.value === null) return;
const localAAS = { ...selectedAAS.value };
const instanceOrError = jsonization.assetAdministrationShellFromJsonable(localAAS);
if (instanceOrError.error !== null) {
console.error('Error parsing AAS: ', instanceOrError.error);
return;
}
Comment on lines +182 to +185
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AAS parsing error is only logged to console. Consider showing a user-friendly error message or handling this error more gracefully since it affects the core functionality.

Copilot uses AI. Check for mistakes.

const aas = instanceOrError.mustValue();
// Create new SubmodelReference
const submodelReference = new aasTypes.Reference(aasTypes.ReferenceTypes.ExternalReference, [
new aasTypes.Key(aasTypes.KeyTypes.Submodel, submodel.id),
]);
// Check if Submodels are null
if (aas.submodels === null || aas.submodels === undefined) {
aas.submodels = [submodelReference];
localAAS.submodels = [jsonization.toJsonable(submodelReference)];
} else {
aas.submodels.push(submodelReference);
localAAS.submodels.push(jsonization.toJsonable(submodelReference));
}
await putAas(aas);

// Update AAS in Store
aasStore.dispatchSelectedAAS(localAAS);
}

function isValidJson(jsonString: string): boolean {
try {
JSON.parse(jsonString);
return true;
} catch {
return false;
}
}

function closeDialog(): void {
clearForm();
jsonInsertDialog.value = false;
}

function clearForm(): void {
jsonInput.value = null;
}

function clearErrorMessages(): void {
jsonInputErrors.value = [];
}
</script>
29 changes: 27 additions & 2 deletions aas-web-ui/src/components/SubmodelTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,20 @@
</v-list-item>
</template>
<span>Create a new Submodel</span>
</v-tooltip></v-list
>
</v-tooltip>
<!-- Open JSON insert dialog -->
<v-tooltip open-delay="600" location="end">
<template #activator="{ props }">
<v-list-item slim v-bind="props" @click="openJsonInsertDialog('Submodel')">
<template #prepend>
<v-icon size="small">mdi-code-json</v-icon>
</template>
Submodel from JSON
</v-list-item>
</template>
<span>Create a new Submodel from JSON</span>
</v-tooltip>
</v-list>
</v-sheet>
</v-menu>
</template>
Expand Down Expand Up @@ -139,6 +151,7 @@
@open-edit-submodel-element-dialog="openEditSubmodelElementDialogForElement"
@open-add-submodel-element-dialog="openAddSubmodelElementDialog"
@open-edit-dialog="openEditDialog(false, $event)"
@open-json-insert-dialog="openJsonInsertDialog('SubmodelElement', $event)"
@show-delete-dialog="openDeleteDialog"></Treeview>
</template>
<v-empty-state
Expand Down Expand Up @@ -217,6 +230,8 @@
:sml="submodelElementToEdit"></ListForm>
<!-- Dialog for creating/editing Submodel -->
<SubmodelForm v-model="editDialog" :new-sm="newSubmodel" :submodel="submodelToEdit"></SubmodelForm>
<!-- Dialog for inserting JSON -->
<JsonInsert v-model="jsonInsertDialog" :type="jsonInsertType" :parent-element="elementToAddSME"></JsonInsert>
<!-- Dialog for deleting SM/SME -->
<DeleteDialog v-model="deleteDialog" :element="elementToDelete"></DeleteDialog>
</template>
Expand Down Expand Up @@ -274,6 +289,8 @@
const elementToAddSME = ref<any | undefined>(undefined); // Variable to store the Element where the new SME is added inside
const submodelElementPath = ref<string | undefined>(undefined); // Variable to store the Element where the new SME is added inside
const submodelElementToEdit = ref<any | undefined>(undefined); // Variable to store the Element where the new SME is added inside
const jsonInsertDialog = ref(false); // Variable to store if the JSON Insert Dialog should be shown
const jsonInsertType = ref<'Submodel' | 'SubmodelElement'>('Submodel'); // Variable to store the ModelType of the JSON to be inserted

// Computed Properties
const isMobile = computed(() => navigationStore.getIsMobile); // Check if the current Device is a Mobile Device
Expand Down Expand Up @@ -489,6 +506,14 @@
}
}

function openJsonInsertDialog(type: 'Submodel' | 'SubmodelElement', element?: any): void {
jsonInsertDialog.value = true;
jsonInsertType.value = type;
if (type === 'SubmodelElement' && element) {
elementToAddSME.value = element;
}
}

function openDeleteDialog(element: any): void {
deleteDialog.value = true;
elementToDelete.value = element;
Expand Down
22 changes: 22 additions & 0 deletions aas-web-ui/src/components/UIComponents/Treeview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@
</template>
<v-list-item-subtitle>Add Submodel Element</v-list-item-subtitle>
</v-list-item>
<!-- Open Insert SubmodelElement from JSON dialog -->
<v-list-item @click="$emit('openJsonInsertDialog', item)">
<template #prepend>
<v-icon size="x-small">mdi-code-json</v-icon>
</template>
<v-list-item-subtitle>Submodel Element from JSON</v-list-item-subtitle>
</v-list-item>
<v-divider></v-divider>
<!-- Open Submodel edit dialog -->
<v-list-item @click="$emit('openEditDialog', item)">
Expand Down Expand Up @@ -213,6 +220,19 @@
</template>
<v-list-item-subtitle>Add Submodel Element</v-list-item-subtitle>
</v-list-item>
<!-- Open Insert SubmodelElement from JSON dialog -->
<v-list-item
v-if="
item.modelType === 'SubmodelElementCollection' ||
item.modelType === 'SubmodelElementList' ||
item.modelType === 'Entity'
"
@click="$emit('openJsonInsertDialog', item)">
<template #prepend>
<v-icon size="x-small">mdi-code-json</v-icon>
</template>
<v-list-item-subtitle>Submodel Element from JSON</v-list-item-subtitle>
</v-list-item>
<v-divider
v-if="
item.modelType === 'SubmodelElementCollection' ||
Expand Down Expand Up @@ -261,6 +281,7 @@
:depth="depth + 1"
@open-add-submodel-element-dialog="$emit('openAddSubmodelElementDialog', $event)"
@open-edit-submodel-element-dialog="$emit('openEditSubmodelElementDialog', $event)"
@open-json-insert-dialog="$emit('openJsonInsertDialog', $event)"
@show-delete-dialog="$emit('showDeleteDialog', $event)"></Treeview>
</template>
</div>
Expand Down Expand Up @@ -303,6 +324,7 @@
(event: 'openEditDialog', item: any): void;
(event: 'showDeleteDialog', item: any): void;
(event: 'openAddSubmodelElementDialog', item: any): void;
(event: 'openJsonInsertDialog', item: any): void;
(event: 'openEditSubmodelElementDialog', item: any): void;
}>();

Expand Down
Loading