Alles und nix

This commit is contained in:
Timo Reichl 2025-04-25 23:23:54 +02:00
parent c374ef8074
commit 0c84fb7b6d
38 changed files with 1932 additions and 938 deletions

View File

@ -1,120 +0,0 @@
<template>
<div class="component-container" :class="direction">
<div class="drop-zone" :class="computedDropClass" @dragover.prevent @dragenter="onDragEnter" @drop="onDrop($event, index)"></div>
<component :is="resolvedComponent" v-bind="element" @update-items="updateSchema" />
<div class="drop-zone" :class="computedDropClass" @dragover.prevent @dragenter="onDragEnter" @drop="onDrop($event, index + 1)"></div>
</div>
</template>
<script>
import { defineComponent, defineAsyncComponent } from "vue";
const InputTextRenderer = defineAsyncComponent(() => import("@/components/formDesigner/formRenderer/InputTextRenderer.vue"));
const InputNumberRenderer = defineAsyncComponent(() => import("@/components/formDesigner/formRenderer/InputNumberRenderer.vue"));
const InputDateRenderer = defineAsyncComponent(() => import("@/components/formDesigner/formRenderer/InputDateRenderer.vue"));
const LabelRenderer = defineAsyncComponent(() => import("@/components/formDesigner/formRenderer/LabelRenderer.vue"));
const ButtonRenderer = defineAsyncComponent(() => import("@/components/formDesigner/formRenderer/ButtonRenderer.vue"));
const FlexLayoutRenderer = defineAsyncComponent(() => import("@/components/formDesigner/formRenderer/FlexLayoutRenderer.vue"));
export default defineComponent({
name: "ComponentRenderer",
components: {
InputTextRenderer,
InputNumberRenderer,
InputDateRenderer,
LabelRenderer,
ButtonRenderer,
FlexLayoutRenderer, // Jetzt wird es erst geladen, wenn nötig
},
props: {
element: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
direction: {
type: String,
required: true,
},
},
computed: {
resolvedComponent() {
const componentMap = {
FlexLayout: "FlexLayoutRenderer",
Label: "LabelRenderer",
InputText: "InputTextRenderer",
InputNumber: "InputNumberRenderer",
InputDate: "InputDateRenderer",
Button: "ButtonRenderer",
};
return componentMap[this.element.type] || null;
},
computedDropClass() {
return this.direction === "horizontal" ? "drop-zone-horizontal" : "drop-zone-vertical";
},
},
methods: {
updateSchema(newItems) {
this.$emit("update-schema", newItems); // 🔥 Leitet die Änderung weiter
},
onDragEnter(event) {
event.preventDefault();
},
onDrop(event, position) {
event.preventDefault();
const droppedType = event.dataTransfer.getData("text/plain");
const newElement = this.createElementByType(droppedType);
if (newElement) {
this.$emit("insert-item", { position, newElement });
}
},
createElementByType(type) {
switch (type) {
case "InputText":
return { type: "InputText", label: "Neues Textfeld", placeholder: "Hier eingeben..." };
case "InputNumber":
return { type: "InputNumber", label: "Neue Zahl", placeholder: "0" };
case "InputDate":
return { type: "InputDate", label: "Neues Datum" };
case "Button":
return { type: "Button", label: "Klicken" };
case "Label":
return { type: "Label", value: "Neues Label" };
case "FlexLayout":
return { type: "FlexLayout", direction: "vertical", items: [] };
default:
console.warn("⚠ Unbekannter Typ:", type);
return null;
}
},
},
});
</script>
<style scoped>
.component-container {
display: flex;
align-items: center;
}
/* Drop-Zonen für horizontale Layouts */
.drop-zone-horizontal {
width: 10px;
height: 100%;
background: rgba(0, 0, 255, 0.3);
cursor: pointer;
}
/* Drop-Zonen für vertikale Layouts */
.drop-zone-vertical {
height: 10px;
width: 100%;
background: rgba(0, 0, 255, 0.3);
cursor: pointer;
}
</style>

View File

@ -1,144 +0,0 @@
<template>
<div id="accordion-1" class="accordion" role="tablist">
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-1" aria-expanded="false" aria-controls="accordion-1 .item-1">Steuerelemente</button></h2>
<div class="accordion-collapse collapse item-1" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row ms-0 me-0" draggable="true" @dragstart="startDrag($event, 'InputText')">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-input-cursor-text pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2"
></path>
<path d="M10 5h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4zM6 5V4H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4v-1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"></path>
</svg>
<p class="mb-0" style="font-size: 14px">Textfeld</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div class="row ms-0 me-0" draggable="true" @dragstart="startDrag($event, 'InputNumber')">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-0-square pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path
d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99ZM8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"
></path>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"></path>
</svg>
<p class="mb-0" style="font-size: 14px">Nummernfeld</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div class="row ms-0 me-0" draggable="true" @dragstart="startDrag($event, 'InputDate')">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-calendar3 pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857V3.857z"></path>
<path
d="M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Datumsfeld</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div class="row ms-0 me-0" draggable="true" @dragstart="startDrag($event, 'Button')">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-menu-button pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 1.5A1.5 1.5 0 0 1 1.5 0h8A1.5 1.5 0 0 1 11 1.5v2A1.5 1.5 0 0 1 9.5 5h-8A1.5 1.5 0 0 1 0 3.5zM1.5 1a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 0-.5-.5z"></path>
<path
d="m7.823 2.823-.396-.396A.25.25 0 0 1 7.604 2h.792a.25.25 0 0 1 .177.427l-.396.396a.25.25 0 0 1-.354 0zM0 8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm1 3v2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2zm14-1V8a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2zM2 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 4a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Button</p>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-2" aria-expanded="false" aria-controls="accordion-1 .item-2">Layoutelemente</button></h2>
<div class="accordion-collapse collapse item-2" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row ms-0 me-0" draggable="true" @dragstart="startDrag($event, 'FlexLayout')">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-grid-1x2 pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path d="M6 1H1v14h5zm9 0h-5v5h5zm0 9v5h-5v-5zM0 1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1zm9 0a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1zm1 8a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1z"></path>
</svg>
<p class="mb-0" style="font-size: 14px">Flexibles Layout</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div class="row ms-0 me-0" draggable="true" @dragstart="startDrag($event, 'Label')">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-alphabet-uppercase pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path
d="M1.226 10.88H0l2.056-6.26h1.42l2.047 6.26h-1.29l-.48-1.61H1.707l-.48 1.61ZM2.76 5.818h-.054l-.75 2.532H3.51zm3.217 5.062V4.62h2.56c1.09 0 1.808.582 1.808 1.54 0 .762-.444 1.22-1.05 1.372v.055c.736.074 1.365.587 1.365 1.528 0 1.119-.89 1.766-2.133 1.766h-2.55ZM7.18 5.55v1.675h.8c.812 0 1.171-.308 1.171-.853 0-.51-.328-.822-.898-.822zm0 2.537V9.95h.903c.951 0 1.342-.312 1.342-.909 0-.591-.382-.954-1.095-.954H7.18Zm5.089-.711v.775c0 1.156.49 1.803 1.347 1.803.705 0 1.163-.454 1.212-1.096H16v.12C15.942 10.173 14.95 11 13.607 11c-1.648 0-2.573-1.073-2.573-2.849v-.78c0-1.775.934-2.871 2.573-2.871 1.347 0 2.34.849 2.393 2.087v.115h-1.172c-.05-.665-.516-1.156-1.212-1.156-.849 0-1.347.67-1.347 1.83Z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Label</p>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" role="tab"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-3" aria-expanded="false" aria-controls="accordion-1 .item-3">Templates</button></h2>
<div class="accordion-collapse collapse item-3" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body">
<div class="row ms-0 me-0">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-chevron-down pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"></path>
</svg>
<p class="mb-0" style="font-size: 14px">Accordion</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div class="row ms-0 me-0">
<div class="col d-xxl-flex justify-content-xxl-start align-items-xxl-center">
<svg class="bi bi-floppy pe-0 me-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path d="M11 2H9v3h2z"></path>
<path
d="M1.5 0h11.586a1.5 1.5 0 0 1 1.06.44l1.415 1.414A1.5 1.5 0 0 1 16 2.914V14.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13A1.5 1.5 0 0 1 1.5 0M1 1.5v13a.5.5 0 0 0 .5.5H2v-4.5A1.5 1.5 0 0 1 3.5 9h9a1.5 1.5 0 0 1 1.5 1.5V15h.5a.5.5 0 0 0 .5-.5V2.914a.5.5 0 0 0-.146-.353l-1.415-1.415A.5.5 0 0 0 13.086 1H13v4.5A1.5 1.5 0 0 1 11.5 7h-7A1.5 1.5 0 0 1 3 5.5V1H1.5a.5.5 0 0 0-.5.5m3 4a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5V1H4zM3 15h10v-4.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Gespeicherter Eintrag 1</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "FormDesignerToolsComponent",
// Verwendete Komponenten
components: {},
// Props für die Komponente
props: {},
// Daten der Komponente
data() {},
// Computed Properties
computed: {},
// Methoden
methods: {
startDrag(event, type) {
console.log("Dragging:", type);
event.dataTransfer.setData("text/plain", type); // Speichere das Objekt als JSON
},
},
// Lifecycle-Hooks
};
</script>
<style scoped>
.accordion-button:focus {
box-shadow: none;
}
</style>

View File

@ -1,135 +0,0 @@
<template>
<div class="flex-layout" :class="computedDirection">
<component-renderer v-for="(item, key) in items" :key="key" :element="item" :index="key" :direction="direction" @update-schema="updateChildItems(key, $event)" @insert-item="insertItem" />
</div>
</template>
<script>
import { defineComponent } from "vue";
import ComponentRenderer from "@/components/formDesigner/ComponentRenderer.vue";
export default defineComponent({
name: "FlexLayoutRenderer",
components: { ComponentRenderer },
data() {
return {
dragOverActive: false,
};
},
props: {
direction: {
type: String,
required: true,
},
items: {
type: Array,
required: true,
},
},
computed: {
computedDirection() {
return this.direction === "horizontal" ? "horizontal" : "vertical";
},
computedDropClass() {
return this.direction === "horizontal" ? "drop-zone-horizontal" : "drop-zone-vertical";
},
},
methods: {
updateChildItems(index, newItems) {
const updatedItems = [...this.items];
updatedItems[index] = { ...updatedItems[index], items: newItems };
this.$emit("update-items", updatedItems);
},
onDragOver(event) {
event.preventDefault();
this.dragOverActive = true;
},
onDragEnter(event) {
event.preventDefault();
},
onDrop(event, position) {
event.preventDefault();
console.log(`📌 onDrop aufgerufen bei Position: ${position}`); // 🔥 Debugging-Log
const droppedType = event.dataTransfer.getData("text/plain");
console.log(`📦 Gedropptes Element: ${droppedType}`); // 🔥 Zeigt an, was gedroppt wurde
const newElement = this.createElementByType(droppedType);
if (newElement) {
console.log(`✅ Neues Element erstellt:`, newElement);
this.insertItem({ position, newElement });
} else {
console.warn("⚠ Kein gültiges Element erstellt!");
}
},
insertItem({ position, newElement }) {
console.log(`🔧 Inserting at position: ${position}`, newElement); // 🔥 Debugging-Log
const updatedItems = [...this.items];
// 🛠 Stelle sicher, dass `position` innerhalb der Liste bleibt
if (position < 0) position = 0;
if (position > updatedItems.length) position = updatedItems.length;
updatedItems.splice(position, 0, newElement); // 🔥 Element an richtiger Stelle einfügen
console.log("🔄 Updated Items:", updatedItems); // 🔥 Zeigt die neue Liste in der Konsole
this.$emit("update-items", updatedItems); // 🔥 Änderungen an Parent weitergeben
},
createElementByType(type) {
switch (type) {
case "InputText":
return { type: "InputText", label: "Neues Textfeld", placeholder: "Hier eingeben..." };
case "InputNumber":
return { type: "InputNumber", label: "Neue Zahl", placeholder: "0" };
case "InputDate":
return { type: "InputDate", label: "Neues Datum" };
case "Button":
return { type: "Button", label: "Klicken" };
case "Label":
return { type: "Label", value: "Neues Label" };
case "FlexLayout":
return { type: "FlexLayout", direction: "vertical", items: [] };
default:
console.warn("⚠ Unbekannter Typ:", type);
return null;
}
},
},
});
</script>
<style scoped>
.flex-layout {
display: flex;
gap: 12px;
min-height: 50px;
}
/* Layouts richtig ausrichten */
.horizontal {
flex-direction: row;
}
.vertical {
flex-direction: column;
}
/* Drop-Zonen für horizontale Layouts (zwischen Spalten) */
.drop-zone-horizontal {
width: 8px; /* Dünne vertikale Linie für horizontale Anordnung */
height: auto; /* ✅ Höhe passt sich dem Inhalt an */
align-self: center; /* ✅ Zentriert die Drop-Zone vertikal */
background: rgba(0, 0, 255, 0.3);
cursor: pointer;
}
/* Drop-Zonen für vertikale Layouts (zwischen Reihen) */
.drop-zone-vertical {
height: 6px; /* Dünne horizontale Linie für vertikale Anordnung */
width: 100%;
background: rgba(0, 0, 255, 0.3);
margin: 2px 0; /* ✅ Kleinere Abstände */
cursor: pointer;
}
</style>

View File

@ -1,91 +0,0 @@
<template>
<div class="row ms-0 ps-0 mb-4" style="text-align: left">
<div class="col-3 ps-0 pe-0 me-5" style="border: 1px solid rgba(39, 68, 93, 0.39); width: 100%">
<div @click.stop="toggleTable" class="d-inline-flex justify-content-between align-items-center align-items-sm-center align-items-md-center align-items-lg-center align-items-xl-center align-items-xxl-center ps-3" style="width: 100%; height: 50px; text-align: left; background: #71bbb2">
<div class="d-inline-flex align-items-xl-center">
<h1 style="font-size: 22px; text-align: left; width: auto">{{ tableTitleProp }} ({{ viewProp?.length || 0 }})</h1>
<svg class="pointer bi bi-plus-circle-fill ps-0 ms-3" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" style="font-size: 18px; color: var(--bs-primary) !important">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3z"></path>
</svg>
</div>
<div style="text-align: right; width: 50px">
<svg v-if="showTable" @click.stop="toggleTable" :class="tableIcon" class="pointer bi me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" style="font-size: 25px">
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"></path>
</svg>
<svg v-if="!showTable" @click.stop="toggleTable" class="pointer bi bi-chevron-right me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" style="font-size: 25px">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"></path>
</svg>
</div>
</div>
<div v-if="showTable" class="table-responsive">
<table class="table">
<thead>
<tr>
<th style="width: 80%">Name</th>
<th>Änderungsdatum</th>
</tr>
</thead>
<tbody>
<tr class="pro-row" @click="setActivePage('FormDesignerLayout', 'Ansicht (' + row.name + ')', {}, 'process')" v-for="row in viewProp" :key="row.name + '-' + row.changed">
<td>{{ row.name }}</td>
<td>{{ row.changed }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { useUiStore } from "../../store/uiStore.js";
export default {
name: "ProcessTableComponent",
// Verwendete Komponenten
components: {},
// Props für die Komponente
props: {
viewProp: {
type: Array,
},
tableTitleProp: {
type: String,
required: true,
},
},
// Daten der Komponente
data() {
return {
showTable: false,
uiStore: useUiStore(),
};
},
// Computed Propertiesprocesses.data
computed: {
openComponents() {
return this.uiStore.openComponents;
},
},
// Methoden
methods: {
toggleTable() {
this.showTable = !this.showTable;
},
setActivePage(componentName, displayName, props, menuItem) {
this.uiStore.setActivePage(componentName, displayName, props, menuItem);
},
},
// Lifecycle-Hooks
};
</script>
<style scoped>
.pro-row:hover td {
background-color: rgba(113, 187, 178, 0.3);
cursor: pointer;
}
</style>

View File

@ -1,14 +1,37 @@
<template>
<div style="width: 4.5rem; height: 100%; margin-right: 4.5rem; top: 0; background: var(--bs-primary); height: 100dvh">
<div
style="
width: 4.5rem;
height: 100%;
top: 0;
background: var(--bs-primary);
height: 100dvh;
"
class="me-2"
>
<ul class="nav nav-pills flex-column text-center nav-flush mb-auto">
<li v-for="(item, index) in menuItems" :key="index" class="nav-item" @mouseover="showPopover($event)" @mouseleave="hidePopoverWithDelay" :class="{ active: isActive(item.menuItem) }" @click="setActivePage(item.componentName, item.text)">
<li
v-for="(item, index) in menuItems"
:key="index"
class="nav-item"
@mouseover="showPopover($event)"
@mouseleave="hidePopoverWithDelay"
:class="{ active: isActive(item.menuItem) }"
@click="setActivePage(item.componentName, item.text)"
>
<a class="nav-link py-3 border-bottom rounded-0" href="#">
<Icon :icon="item.icon" :style="{ fontSize: '20px' }" />
</a>
</li>
</ul>
<!-- Menu-Popover-->
<div v-if="popoverVisible" class="popover-box" :style="popoverStyle" @mouseenter="popoverVisible = true" @mouseleave="hidePopoverWithDelay">
<div
v-if="popoverVisible"
class="popover-box"
:style="popoverStyle"
@mouseenter="popoverVisible = true"
@mouseleave="hidePopoverWithDelay"
>
<p>{{ popoverText }}</p>
</div>
</div>
@ -31,12 +54,42 @@ export default {
data() {
return {
menuItems: [
{ icon: "carbon:home", text: "Dashboard", componentName: "DevelopmentDashboardComponent", menuItem: "home" },
{ icon: "carbon:ibm-process-mining", text: "Prozesse", componentName: "ProcessComponent", menuItem: "process" },
{ icon: "carbon:user-multiple", text: "Benutzer", componentName: "UserComponent", menuItem: "users" },
{ icon: "carbon:building", text: "Firmen", componentName: "CompanyComponent", menuItem: "company" },
{ icon: "carbon:object-storage", text: "Objekte", componentName: "StorageComponent", menuItem: "storage" },
{ icon: "carbon:enumeration-usage", text: "Enums", componentName: "StatisticsComponent", menuItem: "stats" },
{
icon: "carbon:home",
text: "Dashboard",
componentName: "DevelopmentDashboardComponent",
menuItem: "home",
},
{
icon: "carbon:ibm-process-mining",
text: "Prozesse",
componentName: "ProcessComponent",
menuItem: "process",
},
{
icon: "carbon:user-multiple",
text: "Benutzer",
componentName: "UserComponent",
menuItem: "users",
},
{
icon: "carbon:building",
text: "Firmen",
componentName: "CompanyComponent",
menuItem: "company",
},
{
icon: "carbon:object-storage",
text: "Objekte",
componentName: "StorageComponent",
menuItem: "storage",
},
{
icon: "carbon:enumeration-usage",
text: "Enums",
componentName: "StatisticsComponent",
menuItem: "stats",
},
],
uiStore: useUiStore(),
popoverVisible: false,
@ -48,7 +101,9 @@ export default {
// Computed Properties
computed: {
activeMenuItem() {
const activeComponent = this.uiStore.openComponents.find((comp) => comp.active);
const activeComponent = this.uiStore.openComponents.find(
(comp) => comp.active
);
return activeComponent ? activeComponent.menuItem : null;
},
},
@ -66,7 +121,9 @@ export default {
top: `${rect.top + window.scrollY + 10}px`, // Höhe relativ zur Seite
left: `${rect.right + 10}px`, // 10px rechts vom Element
};
const index = [...triggerElement.parentElement.children].indexOf(triggerElement);
const index = [...triggerElement.parentElement.children].indexOf(
triggerElement
);
this.popoverText = this.menuItems[index]?.text || "Info";
}
this.popoverVisible = true;
@ -110,7 +167,7 @@ export default {
.popover-box {
position: absolute;
background: var(--bs-primary);
background: #142330;
color: white;
padding: 6px;
border-radius: 5px;

View File

@ -2,10 +2,13 @@
<div>
<slot name="topbar"> </slot>
<div class="d-inline-flex">
<slot name="sidebar">test</slot>
<slot name="sidebar"></slot>
<Suspense>
<template #default>
<component :is="activePageComponent" v-bind="currentProps"></component>
<component
:is="activePageComponent"
v-bind="currentProps"
></component>
</template>
<template #fallback>
<p>Lade Seite...</p>
@ -35,18 +38,32 @@ export default {
computed: {
activePageComponent() {
const components = {
Dashboard: defineAsyncComponent(() => import("@/pages/DevelopmentDashboard.vue")),
TestComponent: defineAsyncComponent(() => import("@/pages/TestComponent.vue")),
ProcessOverviewComponent: defineAsyncComponent(() => import("@/pages/ProcessOverviewComponent.vue")),
FormDesignerLayout: defineAsyncComponent(() => import("@/layouts/FormDesignerLayout.vue")),
Dashboard: defineAsyncComponent(() =>
import("@/pages/DevelopmentDashboard.vue")
),
TestComponent: defineAsyncComponent(() =>
import("@/pages/TestComponent.vue")
),
ProcessOverviewComponent: defineAsyncComponent(() =>
import("@/modules/process/pages/ProcessOverviewComponent.vue")
),
FormDesignerLayout: defineAsyncComponent(() =>
import("@/modules/process/layouts/FormDesignerLayout.vue")
),
};
const activeComponent = this.uiStore.openComponents.find((comp) => comp.active);
const activeComponent = this.uiStore.openComponents.find(
(comp) => comp.active
);
return components[activeComponent?.componentName] || components["Dashboard"];
return (
components[activeComponent?.componentName] || components["Dashboard"]
);
},
currentProps() {
const activeComponent = this.uiStore.openComponents.find((comp) => comp.active);
const activeComponent = this.uiStore.openComponents.find(
(comp) => comp.active
);
return activeComponent?.props || {};
},
},

View File

@ -1,272 +0,0 @@
<template>
<div class="container mt-2" style="width: 100vw; height: 100vh">
<div style="height: 100%">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active" role="tab" data-bs-toggle="tab" href="#tab-1">
<svg class="bi bi-file-text pe-0 me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" style="font-size: 16px">
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z"></path>
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1"></path>
</svg>
Verwaltung
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" role="tab" data-bs-toggle="tab" href="#tab-2">
<svg class="bi bi-card-heading pe-0 me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"></path>
<path d="M3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m0-5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5z"></path>
</svg>
Editor
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" role="tab" data-bs-toggle="tab" href="#tab-3">
<svg class="bi bi-diagram-3 pe-0 me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5zM8.5 5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5zM0 11.5A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm4.5.5A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm4.5.5a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"
></path>
</svg>
Objektstruktur
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" role="tab" data-bs-toggle="tab" href="#tab-4">
<svg class="bi bi-filetype-json pe-0 me-2" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M14 4.5V11h-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM4.151 15.29a1.176 1.176 0 0 1-.111-.449h.764a.578.578 0 0 0 .255.384c.07.049.154.087.25.114.095.028.201.041.319.041.164 0 .301-.023.413-.07a.559.559 0 0 0 .255-.193.507.507 0 0 0 .084-.29.387.387 0 0 0-.152-.326c-.101-.08-.256-.144-.463-.193l-.618-.143a1.72 1.72 0 0 1-.539-.214 1.001 1.001 0 0 1-.352-.367 1.068 1.068 0 0 1-.123-.524c0-.244.064-.457.19-.639.128-.181.304-.322.528-.422.225-.1.484-.149.777-.149.304 0 .564.05.779.152.217.102.384.239.5.41.12.17.186.359.2.566h-.75a.56.56 0 0 0-.12-.258.624.624 0 0 0-.246-.181.923.923 0 0 0-.37-.068c-.216 0-.387.05-.512.152a.472.472 0 0 0-.185.384c0 .121.048.22.144.3a.97.97 0 0 0 .404.175l.621.143c.217.05.406.12.566.211a1 1 0 0 1 .375.358c.09.148.135.335.135.56 0 .247-.063.466-.188.656a1.216 1.216 0 0 1-.539.439c-.234.105-.52.158-.858.158-.254 0-.476-.03-.665-.09a1.404 1.404 0 0 1-.478-.252 1.13 1.13 0 0 1-.29-.375Zm-3.104-.033a1.32 1.32 0 0 1-.082-.466h.764a.576.576 0 0 0 .074.27.499.499 0 0 0 .454.246c.19 0 .33-.055.422-.164.091-.11.137-.265.137-.466v-2.745h.791v2.725c0 .44-.119.774-.357 1.005-.237.23-.565.345-.985.345a1.59 1.59 0 0 1-.568-.094 1.145 1.145 0 0 1-.407-.266 1.14 1.14 0 0 1-.243-.39Zm9.091-1.585v.522c0 .256-.039.47-.117.641a.862.862 0 0 1-.322.387.877.877 0 0 1-.47.126.883.883 0 0 1-.47-.126.87.87 0 0 1-.32-.387 1.55 1.55 0 0 1-.117-.641v-.522c0-.258.039-.471.117-.641a.87.87 0 0 1 .32-.387.868.868 0 0 1 .47-.129c.177 0 .333.043.47.129a.862.862 0 0 1 .322.387c.078.17.117.383.117.641m.803.519v-.513c0-.377-.069-.701-.205-.973a1.46 1.46 0 0 0-.59-.63c-.253-.146-.559-.22-.916-.22-.356 0-.662.074-.92.22a1.441 1.441 0 0 0-.589.628c-.137.271-.205.596-.205.975v.513c0 .375.068.699.205.973.137.271.333.48.589.626.258.145.564.217.92.217.357 0 .663-.072.917-.217.256-.146.452-.355.589-.626.136-.274.205-.598.205-.973Zm1.29-.935v2.675h-.746v-3.999h.662l1.752 2.66h.032v-2.66h.75v4h-.656l-1.761-2.676h-.032Z"
></path>
</svg>
JSON
</a>
</li>
</ul>
<div class="tab-content" style="height: 100%">
<div id="tab-1" class="tab-pane active" role="tabpanel">
<div class="row">
<div class="col">
<div class="d-xl-flex justify-content-xl-center align-items-xl-center" style="width: 300px; height: 200px; text-align: center; background: #71bbb2">
<h1 style="font-size: 22px">Menüauswahl</h1>
</div>
</div>
<div class="col">
<div class="d-xl-flex justify-content-xl-center align-items-xl-center" style="width: 300px; height: 200px; text-align: center; background: #71bbb2">
<h1 style="font-size: 22px">Zuordnung</h1>
</div>
</div>
</div>
</div>
<div id="tab-2" class="tab-pane" role="tabpanel" style="height: 100%">
<div class="split-container">
<!-- Linker Bereich mit fixer Höhe oben -->
<div class="panel left" :style="{ width: leftWidth + 'px' }">
<div class="top-panel" :style="{ height: topHeight + 'px' }">
<FormDesignerTreeViewComponent :nodes="formJSON"></FormDesignerTreeViewComponent>
</div>
<div class="horizontal-splitter" @mousedown="startHorizontalResize"></div>
<div class="bottom-panel" :style="{ height: bottomHeight + 'px' }">
<FormDesignerToolsComponent></FormDesignerToolsComponent>
</div>
</div>
<!-- Vertikaler Splitter -->
<div class="splitter" @mousedown="startVerticalResize"></div>
<!-- Mittlerer Bereich -->
<div class="panel middle p-3" :style="{ width: middleWidth + 'px' }">
<FormRendererComponent :schema="formJSON" @update-schema="addElementToForm" />
<slot name="middle"></slot>
</div>
<!-- Zweiter vertikaler Splitter -->
<div class="splitter" @mousedown="startSecondVerticalResize"></div>
<!-- Rechter Bereich -->
<div class="panel right" :style="{ width: rightWidth + 'px' }">
<slot name="right"></slot>
</div>
</div>
</div>
<div id="tab-3" class="tab-pane" role="tabpanel">
<p>Content for tab 3.</p>
</div>
<div id="tab-4" class="tab-pane" role="tabpanel">
<textarea class="pro-textarea" v-model="jsonOutput" readonly></textarea>
</div>
</div>
</div>
</div>
</template>
<script>
import { reactive } from "vue";
import FormDesignerToolsComponent from "@/components/formDesigner/FormDesignerToolsComponent.vue";
import FormDesignerTreeViewComponent from "@/components/formDesigner/FormDesignerTreeViewComponent.vue";
import FormRendererComponent from "@/components/formDesigner/FormRendererComponent.vue";
import FormJSON from "@/dummyData/formularData.json";
export default {
name: "FormDesignerLayout",
// Verwendete Komponenten
components: {
FormDesignerToolsComponent,
FormDesignerTreeViewComponent,
FormRendererComponent,
},
data() {
return {
leftWidth: 400, // Feste Breite für den linken Bereich
rightWidth: 200, // Feste Breite für den rechten Bereich
middleWidth: window.innerWidth - 400, // Dynamisch: Restliche Breite
topHeight: 150, // Fixierte Höhe für den oberen Bereich
bottomHeight: window.innerHeight - 150, // Dynamisch: Resthöhe
isResizingVertical: false,
isResizingSecondVertical: false,
isResizingHorizontal: false,
startX: 0,
startY: 0,
formJSON: reactive([...FormJSON]),
};
},
mounted() {
window.addEventListener("resize", this.updateSizes);
},
beforeUnmount() {
window.removeEventListener("resize", this.updateSizes);
},
methods: {
updateSizes() {
this.middleWidth = window.innerWidth - this.leftWidth - this.rightWidth;
this.bottomHeight = window.innerHeight - this.topHeight;
},
addElementToForm(newSchema) {
console.log("💾 `update-schema` angekommen in FormDesignerLayout! 🔥", newSchema);
this.formJSON = newSchema;
},
// Vertikaler Splitter (zwischen links und Mitte)
startVerticalResize(event) {
this.isResizingVertical = true;
this.startX = event.clientX;
document.addEventListener("mousemove", this.resizeVertical);
document.addEventListener("mouseup", this.stopResize);
},
resizeVertical(event) {
if (!this.isResizingVertical) return;
const delta = event.clientX - this.startX;
const newLeftWidth = this.leftWidth + delta;
if (newLeftWidth >= 100 && this.middleWidth - delta >= 100) {
this.leftWidth = newLeftWidth;
this.middleWidth = window.innerWidth - this.leftWidth - this.rightWidth;
}
this.startX = event.clientX;
},
// Zweiter vertikaler Splitter (zwischen Mitte und Rechts)
startSecondVerticalResize(event) {
this.isResizingSecondVertical = true;
this.startX = event.clientX;
document.addEventListener("mousemove", this.resizeSecondVertical);
document.addEventListener("mouseup", this.stopResize);
},
resizeSecondVertical(event) {
if (!this.isResizingSecondVertical) return;
const delta = event.clientX - this.startX;
const newRightWidth = this.rightWidth - delta;
if (newRightWidth >= 100 && this.middleWidth + delta >= 100) {
this.rightWidth = newRightWidth;
this.middleWidth = window.innerWidth - this.leftWidth - this.rightWidth;
}
this.startX = event.clientX;
},
// Horizontaler Splitter für den linken Bereich
startHorizontalResize(event) {
this.isResizingHorizontal = true;
this.startY = event.clientY;
document.addEventListener("mousemove", this.resizeHorizontal);
document.addEventListener("mouseup", this.stopResize);
},
resizeHorizontal(event) {
if (!this.isResizingHorizontal) return;
const delta = event.clientY - this.startY;
const newTopHeight = this.topHeight + delta;
const newBottomHeight = this.bottomHeight - delta;
if (newTopHeight >= 50 && newBottomHeight >= 50) {
this.topHeight = newTopHeight;
this.bottomHeight = newBottomHeight;
}
this.startY = event.clientY;
},
stopResize() {
this.isResizingVertical = false;
this.isResizingSecondVertical = false;
this.isResizingHorizontal = false;
document.removeEventListener("mousemove", this.resizeVertical);
document.removeEventListener("mousemove", this.resizeSecondVertical);
document.removeEventListener("mousemove", this.resizeHorizontal);
document.removeEventListener("mouseup", this.stopResize);
},
},
computed: {
jsonOutput() {
return JSON.stringify(this.formJSON, null, 2); // JSON sauber formatieren
},
},
};
</script>
<style scoped>
.split-container {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
}
/* Linker Bereich mit fixer Höhe oben */
.left {
display: flex;
flex-direction: column;
}
.top-panel {
height: 150px; /* Feste Höhe */
overflow: auto;
}
.bottom-panel {
overflow: auto;
}
/* Allgemeine Panel-Styling */
.panel {
overflow: auto;
flex-grow: 1;
}
/* Splitter für vertikale Trennung */
.splitter {
width: 6px;
background: #27445d21;
cursor: ew-resize;
user-select: none;
}
/* Splitter für horizontale Trennung */
.horizontal-splitter {
height: 3px;
background: #27445d21;
cursor: ns-resize;
user-select: none;
}
.pro-textarea {
height: 100dvh;
width: 100dvh;
}
</style>

View File

@ -5,8 +5,13 @@ import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap.bundle.min.js";
import "@/assets/custom-bootstrap.css";
import { client } from "@/modules/shared/services/WebSocketService";
(async () => {
await client.connect();
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");
})();

View File

@ -0,0 +1,238 @@
<template>
<div class="component-container" :class="direction">
<component
:is="resolvedComponent"
v-bind="element.props"
@update-items="updateSchema"
@dragover.prevent="onDragOver($event)"
@drop="onDrop($event, index)"
@dragleave="onDragLeave()"
:class="getDropClass"
/>
</div>
</template>
<script>
import { defineComponent, defineAsyncComponent } from "vue";
const InputTextRenderer = defineAsyncComponent(() =>
import(
"@/modules/process/components/formDesigner/renderer/InputTextRenderer.vue"
)
);
const InputNumberRenderer = defineAsyncComponent(() =>
import(
"@/modules/process/components/formDesigner/renderer/InputNumberRenderer.vue"
)
);
const InputDateRenderer = defineAsyncComponent(() =>
import(
"@/modules/process/components/formDesigner/renderer/InputDateRenderer.vue"
)
);
const LabelRenderer = defineAsyncComponent(() =>
import("@/modules/process/components/formDesigner/renderer/LabelRenderer.vue")
);
const ButtonRenderer = defineAsyncComponent(() =>
import(
"@/modules/process/components/formDesigner/renderer/ButtonRenderer.vue"
)
);
const FlexLayoutRenderer = defineAsyncComponent(() =>
import(
"@/modules/process/components/formDesigner/renderer/FlexLayoutRenderer.vue"
)
);
import { InputTextBlock } from "@/modules/process/models/formDesigner/blocks/InputTextBlock";
import { ButtonBlock } from "@/modules/process/models/formDesigner/blocks/ButtonBlock";
import { LabelBlock } from "@/modules/process/models/formDesigner/blocks/LabelBlock";
import { FlexLayoutBlock } from "@/modules/process/models/formDesigner/blocks/FlexLayoutBlock";
import { InputDateBlock } from "@/modules/process/models/formDesigner/blocks/InputDateBlock";
import { InputNumberBlock } from "@/modules/process/models/formDesigner/blocks/InputNumberBlock";
export default defineComponent({
name: "ComponentRenderer",
components: {
InputTextRenderer,
InputNumberRenderer,
InputDateRenderer,
LabelRenderer,
ButtonRenderer,
FlexLayoutRenderer,
},
data() {
return {
dropIndicator: null,
};
},
props: {
element: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
direction: {
type: String,
required: true,
},
},
computed: {
resolvedComponent() {
const componentMap = {
FlexLayout: "FlexLayoutRenderer",
Label: "LabelRenderer",
InputText: "InputTextRenderer",
InputNumber: "InputNumberRenderer",
InputDate: "InputDateRenderer",
Button: "ButtonRenderer",
};
return componentMap[this.element.type] || null;
},
getDropClass() {
if (!this.dropIndicator || this.dropIndicator?.type !== this.element.type)
return "";
switch (this.dropIndicator.position) {
case "above":
return "drop-above";
case "below":
return "drop-below";
case "right":
return "drop-right";
case "left":
return "drop-left";
default:
return "";
}
},
},
methods: {
updateSchema(newItems) {
this.$emit("update-schema", newItems);
},
onDrop(event, index) {
event.stopPropagation();
event.preventDefault();
if (
this.dropIndicator?.position === "right" ||
this.dropIndicator?.position === "below"
) {
index = index + 1;
}
const droppedType = event.dataTransfer.getData("text/plain");
const newElement = this.createElementByType(droppedType);
this.dropIndicator = null;
if (newElement) {
this.$emit("insert-item", { index, newElement });
}
},
onDragOver(event) {
event.stopPropagation();
const bounds = event.currentTarget.getBoundingClientRect();
const offsetX = event.clientX - bounds.left;
const offsetY = event.clientY - bounds.top;
const isHorizontal = this.direction === "horizontal";
const position = isHorizontal
? offsetX < bounds.width / 2
? "left"
: "right"
: offsetY < bounds.height / 2
? "above"
: "below";
const curElement = this.element;
this.dropIndicator = {
type: curElement.type,
position,
};
},
onDragLeave() {
if (
this.dropIndicator &&
this.dropIndicator?.type === this.element.type
) {
this.dropIndicator = null;
}
},
createElementByType(type) {
switch (type) {
case "InputText":
return new InputTextBlock({
label: "Neues Textfeld",
placeholder: "Beispieltext",
});
case "InputNumber":
return new InputNumberBlock({
label: "Neue Nummer",
placeholder: "Hier Nummer eingeben",
});
case "InputDate":
return new InputDateBlock({
label: "Neues Datum",
placeholder: "Hier Datum eingeben",
});
case "Button":
return new ButtonBlock({ label: "Neuer Button" });
case "Label":
return new LabelBlock({ label: "Neues Label" });
case "FlexLayout":
return new FlexLayoutBlock({
items: [new LabelBlock({ label: "Erstes Element" })],
});
default:
console.warn("⚠ Unbekannter Typ:", type);
return null;
}
},
},
});
</script>
<style scoped>
.component-container {
display: flex;
align-items: center;
position: relative;
}
.component-container .drop-above::before,
.component-container .drop-below::after,
.component-container .drop-left::before,
.component-container .drop-right::after {
content: "";
position: absolute;
background: #42b983;
z-index: 10;
}
.component-container .drop-above::before {
top: 0;
left: 0;
right: 0;
height: 2px;
}
.component-container .drop-below::after {
bottom: 0;
left: 0;
right: 0;
height: 2px;
}
.component-container .drop-left::before {
left: 0;
top: 0;
bottom: 0;
width: 2px;
}
.component-container .drop-right::after {
right: 0;
top: 0;
bottom: 0;
width: 2px;
}
</style>

View File

@ -0,0 +1,346 @@
<template>
<div class="mt-2">
<div class="btn-group formBtn m-1" role="group" aria-label="Button group">
<button
type="button"
class="btn"
:class="elementClass"
@click="setActive('element')"
>
Eingabe
</button>
<button
type="button"
class="btn"
:class="layoutClass"
@click="setActive('layout')"
>
Layout
</button>
<button
type="button"
class="btn"
:class="templateClass"
@click="setActive('template')"
>
Template
</button>
</div>
<div class="mt-2 formElements" v-if="elementActive">
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
draggable="true"
@dragstart="startDrag($event, 'InputText')"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-input-cursor-text pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566.174.099.321.198.44.286.119-.088.266-.187.44-.286A4.165 4.165 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.49 3.49 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294.387.221.926.434 1.564.434a.5.5 0 0 1 0 1 4.165 4.165 0 0 1-2.06-.566A4.561 4.561 0 0 1 8 13.65a4.561 4.561 0 0 1-.44.285 4.165 4.165 0 0 1-2.06.566.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.49 3.49 0 0 0-.436-.294A3.166 3.166 0 0 0 5.5 2.5.5.5 0 0 1 5 2"
></path>
<path
d="M10 5h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4zM6 5V4H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4v-1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Textfeld</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
draggable="true"
@dragstart="startDrag($event, 'InputNumber')"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-0-square pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99ZM8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"
></path>
<path
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Nummernfeld</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
draggable="true"
@dragstart="startDrag($event, 'InputDate')"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-calendar3 pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857V3.857z"
></path>
<path
d="M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Datumsfeld</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
draggable="true"
@dragstart="startDrag($event, 'Button')"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-menu-button pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M0 1.5A1.5 1.5 0 0 1 1.5 0h8A1.5 1.5 0 0 1 11 1.5v2A1.5 1.5 0 0 1 9.5 5h-8A1.5 1.5 0 0 1 0 3.5zM1.5 1a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 0-.5-.5z"
></path>
<path
d="m7.823 2.823-.396-.396A.25.25 0 0 1 7.604 2h.792a.25.25 0 0 1 .177.427l-.396.396a.25.25 0 0 1-.354 0zM0 8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm1 3v2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2zm14-1V8a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2zM2 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 4a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Button</p>
</div>
</div>
</div>
<div class="mt-4 formElements" v-if="layoutActive">
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
draggable="true"
@dragstart="startDrag($event, 'FlexLayout')"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-grid-1x2 pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M6 1H1v14h5zm9 0h-5v5h5zm0 9v5h-5v-5zM0 1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1zm9 0a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1zm1 8a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Flexibles Layout</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
draggable="true"
@dragstart="startDrag($event, 'Label')"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-alphabet-uppercase pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M1.226 10.88H0l2.056-6.26h1.42l2.047 6.26h-1.29l-.48-1.61H1.707l-.48 1.61ZM2.76 5.818h-.054l-.75 2.532H3.51zm3.217 5.062V4.62h2.56c1.09 0 1.808.582 1.808 1.54 0 .762-.444 1.22-1.05 1.372v.055c.736.074 1.365.587 1.365 1.528 0 1.119-.89 1.766-2.133 1.766h-2.55ZM7.18 5.55v1.675h.8c.812 0 1.171-.308 1.171-.853 0-.51-.328-.822-.898-.822zm0 2.537V9.95h.903c.951 0 1.342-.312 1.342-.909 0-.591-.382-.954-1.095-.954H7.18Zm5.089-.711v.775c0 1.156.49 1.803 1.347 1.803.705 0 1.163-.454 1.212-1.096H16v.12C15.942 10.173 14.95 11 13.607 11c-1.648 0-2.573-1.073-2.573-2.849v-.78c0-1.775.934-2.871 2.573-2.871 1.347 0 2.34.849 2.393 2.087v.115h-1.172c-.05-.665-.516-1.156-1.212-1.156-.849 0-1.347.67-1.347 1.83Z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Label</p>
</div>
</div>
</div>
<div class="mt-4 formElements" v-if="templateActive">
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-chevron-down pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Accordion</p>
</div>
</div>
<hr class="mb-1 mt-1" />
<div
class="row ms-0 me-0 d-flex formItem align-items-center justify-content-center"
>
<div
class="col d-flex justify-content-xxl-start align-items-xxl-center"
>
<svg
class="bi bi-floppy pe-0 me-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M11 2H9v3h2z"></path>
<path
d="M1.5 0h11.586a1.5 1.5 0 0 1 1.06.44l1.415 1.414A1.5 1.5 0 0 1 16 2.914V14.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 14.5v-13A1.5 1.5 0 0 1 1.5 0M1 1.5v13a.5.5 0 0 0 .5.5H2v-4.5A1.5 1.5 0 0 1 3.5 9h9a1.5 1.5 0 0 1 1.5 1.5V15h.5a.5.5 0 0 0 .5-.5V2.914a.5.5 0 0 0-.146-.353l-1.415-1.415A.5.5 0 0 0 13.086 1H13v4.5A1.5 1.5 0 0 1 11.5 7h-7A1.5 1.5 0 0 1 3 5.5V1H1.5a.5.5 0 0 0-.5.5m3 4a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5V1H4zM3 15h10v-4.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5z"
></path>
</svg>
<p class="mb-0" style="font-size: 14px">Gespeicherter Eintrag 1</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "FormDesignerToolsComponent",
// Verwendete Komponenten
components: {},
// Props für die Komponente
props: {},
// Daten der Komponente
data() {
return {
elementActive: true,
layoutActive: false,
templateActive: false,
};
},
// Computed Properties
computed: {
elementClass() {
if (this.elementActive) {
return "active";
}
return "";
},
layoutClass() {
if (this.layoutActive) {
return "active";
}
return "";
},
templateClass() {
if (this.templateActive) {
return "active";
}
return "";
},
},
// Methoden
methods: {
startDrag(event, type) {
console.log("Dragging:", type);
event.dataTransfer.setData("text/plain", type); // Speichere das Objekt als JSON
},
setActive(type) {
switch (type) {
case "element":
this.elementActive = true;
this.layoutActive = false;
this.templateActive = false;
break;
case "layout":
this.layoutActive = true;
this.elementActive = false;
this.templateActive = false;
break;
case "template":
this.templateActive = true;
this.elementActive = false;
this.layoutActive = false;
break;
default:
break;
}
},
},
// Lifecycle-Hooks
};
</script>
<style scoped>
.accordion-button:focus {
box-shadow: none;
}
.formBtn {
width: 97%;
border: white solid 1px;
}
.formBtn button:not(:last-child) {
border-right: white solid 1px;
}
.formBtn button {
color: white;
}
.formBtn button.active {
color: var(--bs-primary);
background-color: white;
}
.formBtn button:hover {
background-color: #617789;
}
.formElements .formItem {
height: 50px;
text-align: center;
}
.formElements .formItem:hover {
background-color: #617789;
cursor: grab;
}
</style>

View File

@ -1,9 +1,13 @@
<template>
<FlexLayoutRenderer direction="vertical" :items="schema" @update-items="$emit('update-schema', $event)" />
<FlexLayoutRenderer
direction="vertical"
:items="schema"
@update-items="$emit('update-schema', $event)"
/>
</template>
<script>
import FlexLayoutRenderer from "@/components/formDesigner/formRenderer/FlexLayoutRenderer.vue";
import FlexLayoutRenderer from "@/modules/process/components/formDesigner/renderer/FlexLayoutRenderer.vue";
export default {
name: "FormRendererComponent",

View File

@ -0,0 +1,138 @@
<template>
<div class="flex-layout" :class="computedDirection">
<component-renderer
v-for="(item, key) in items"
:key="key"
:element="item"
:index="key"
:direction="direction"
@update-schema="updateChildItems(key, $event)"
@insert-item="insertItem"
/>
</div>
</template>
<script>
import { defineComponent } from "vue";
import ComponentRenderer from "@/modules/process/components/formDesigner/ComponentRenderer.vue";
import { InputTextBlock } from "@/modules/process/models/formDesigner/blocks/InputTextBlock";
import { ButtonBlock } from "@/modules/process/models/formDesigner/blocks/ButtonBlock";
import { LabelBlock } from "@/modules/process/models/formDesigner/blocks/LabelBlock";
import { FlexLayoutBlock } from "@/modules/process/models/formDesigner/blocks/FlexLayoutBlock";
import { InputDateBlock } from "@/modules/process/models/formDesigner/blocks/InputDateBlock";
import { InputNumberBlock } from "@/modules/process/models/formDesigner/blocks/InputNumberBlock";
export default defineComponent({
name: "FlexLayoutRenderer",
components: { ComponentRenderer },
data() {
return {
dragOverActive: false,
};
},
props: {
direction: {
type: String,
required: true,
},
items: {
type: Array,
required: true,
},
},
computed: {
computedDirection() {
return this.direction === "horizontal" ? "horizontal" : "vertical";
},
computedDropClass() {
return this.direction === "horizontal"
? "drop-zone-horizontal"
: "drop-zone-vertical";
},
},
methods: {
updateChildItems(index, newItems) {
const updatedItems = [...this.items];
updatedItems[index] = { ...updatedItems[index], items: newItems };
this.$emit("update-items", updatedItems);
},
insertItem({ index, newElement }) {
const updatedItems = [...this.items];
// 🛠 Stelle sicher, dass `index` innerhalb der Liste bleibt
if (index < 0) index = 0;
if (index > updatedItems.length) index = updatedItems.length;
updatedItems.splice(index, 0, newElement);
this.$emit("update-items", updatedItems);
},
createElementByType(type) {
switch (type) {
case "InputText":
return new InputTextBlock({
label: "Neues Textfeld",
placeholder: "Beispieltext",
});
case "InputNumber":
return new InputNumberBlock({
label: "Neue Nummer",
placeholder: "Hier Nummer eingeben",
});
case "InputDate":
return new InputDateBlock({
label: "Neues Datum",
placeholder: "Hier Datum eingeben",
});
case "Button":
return new ButtonBlock({ label: "Neuer Button" });
case "Label":
return new LabelBlock({ label: "Neues Label" });
case "FlexLayout":
return new FlexLayoutBlock({
items: [new LabelBlock({ label: "Erstes Element" })],
});
default:
console.warn("⚠ Unbekannter Typ:", type);
return null;
}
},
},
});
</script>
<style scoped>
.flex-layout {
display: flex;
gap: 12px;
min-height: 50px;
}
/* Layouts richtig ausrichten */
.horizontal {
flex-direction: row;
}
.vertical {
flex-direction: column;
}
/* Drop-Zonen für horizontale Layouts (zwischen Spalten) */
.drop-zone-horizontal {
width: 8px; /* Dünne vertikale Linie für horizontale Anordnung */
height: auto; /* ✅ Höhe passt sich dem Inhalt an */
align-self: center; /* ✅ Zentriert die Drop-Zone vertikal */
background: rgba(0, 0, 255, 0.3);
cursor: pointer;
}
/* Drop-Zonen für vertikale Layouts (zwischen Reihen) */
.drop-zone-vertical {
height: 6px; /* Dünne horizontale Linie für vertikale Anordnung */
width: 100%;
background: rgba(0, 0, 255, 0.3);
margin: 2px 0; /* ✅ Kleinere Abstände */
cursor: pointer;
}
</style>

View File

@ -1,12 +1,12 @@
<template>
<label>{{ value }}</label>
<label>{{ label }}</label>
</template>
<script>
export default {
name: "LabelRenderer",
props: {
value: String,
label: String,
},
};
</script>

View File

@ -0,0 +1,149 @@
<template>
<div class="row ms-0 ps-0 mb-4" style="text-align: left">
<div
class="col-3 ps-0 pe-0 me-5"
style="border: 1px solid rgba(39, 68, 93, 0.39); width: 100%"
>
<div
@click.stop="toggleTable"
class="d-inline-flex justify-content-between align-items-center align-items-sm-center align-items-md-center align-items-lg-center align-items-xl-center align-items-xxl-center ps-3"
style="width: 100%; height: 50px; text-align: left; background: #71bbb2"
>
<div class="d-inline-flex align-items-xl-center">
<h1 style="font-size: 22px; text-align: left; width: auto">
{{ tableTitleProp }} ({{ viewProp?.length || 0 }})
</h1>
<svg
class="pointer bi bi-plus-circle-fill ps-0 ms-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
style="font-size: 18px; color: var(--bs-primary) !important"
>
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3z"
></path>
</svg>
</div>
<div style="text-align: right; width: 50px">
<svg
v-if="showTable"
@click.stop="toggleTable"
:class="tableIcon"
class="pointer bi me-2"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
style="font-size: 25px"
>
<path
fill-rule="evenodd"
d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"
></path>
</svg>
<svg
v-if="!showTable"
@click.stop="toggleTable"
class="pointer bi bi-chevron-right me-2"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
style="font-size: 25px"
>
<path
fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
></path>
</svg>
</div>
</div>
<div v-if="showTable" class="table-responsive">
<table class="table">
<thead>
<tr>
<th style="width: 80%">Name</th>
<th>Änderungsdatum</th>
</tr>
</thead>
<tbody>
<tr
class="pro-row"
@click="
setActivePage(
'FormDesignerLayout',
'Ansicht (' + row.name + ')',
{},
'process'
)
"
v-for="row in viewProp"
:key="row.name + '-' + row.changed"
>
<td>{{ row.name }}</td>
<td>{{ row.changed }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { useUiStore } from "../../../../store/uiStore.js";
export default {
name: "ProcessTableComponent",
// Verwendete Komponenten
components: {},
// Props für die Komponente
props: {
viewProp: {
type: Array,
},
tableTitleProp: {
type: String,
required: true,
},
},
// Daten der Komponente
data() {
return {
showTable: false,
uiStore: useUiStore(),
};
},
// Computed Propertiesprocesses.data
computed: {
openComponents() {
return this.uiStore.openComponents;
},
},
// Methoden
methods: {
toggleTable() {
this.showTable = !this.showTable;
},
setActivePage(componentName, displayName, props, menuItem) {
this.uiStore.setActivePage(componentName, displayName, props, menuItem);
},
},
// Lifecycle-Hooks
};
</script>
<style scoped>
.pro-row:hover td {
background-color: rgba(113, 187, 178, 0.3);
cursor: pointer;
}
</style>

View File

@ -0,0 +1,397 @@
<template>
<div class="container mt-2" style="width: 100vw; height: 100vh">
<div style="height: 100%; width: calc(100vw - 120px)">
<ul class="nav nav-tabs" style="justify-content: center" role="tablist">
<li class="nav-item" role="presentation">
<a
class="nav-link active"
role="tab"
data-bs-toggle="tab"
href="#tab-1"
>
<svg
class="bi bi-file-text pe-0 me-2"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
style="font-size: 16px"
>
<path
d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z"
></path>
<path
d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1"
></path>
</svg>
Verwaltung
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" role="tab" data-bs-toggle="tab" href="#tab-2">
<svg
class="bi bi-card-heading pe-0 me-2"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"
></path>
<path
d="M3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m0-5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5z"
></path>
</svg>
Editor
</a>
</li>
</ul>
<div class="tab-content" style="height: 100%">
<div id="tab-1" class="tab-pane active" role="tabpanel">
<div class="row">
<div class="col">
<div
class="d-xl-flex justify-content-xl-center align-items-xl-center"
style="
width: 300px;
height: 200px;
text-align: center;
background: #71bbb2;
"
>
<h1 style="font-size: 22px">Menüauswahl</h1>
</div>
</div>
<div class="col">
<div
class="d-xl-flex justify-content-xl-center align-items-xl-center"
style="
width: 300px;
height: 200px;
text-align: center;
background: #71bbb2;
"
>
<h1 style="font-size: 22px">Zuordnung</h1>
</div>
</div>
</div>
</div>
<div id="tab-2" class="tab-pane" role="tabpanel" style="height: 100%">
<div class="split-container">
<!-- Linker Bereich mit fixer Höhe oben -->
<div
class="panel left"
style="background-color: var(--bs-primary); color: white"
:style="{ width: leftWidth + 'px' }"
>
<ul class="nav nav-tabs d-flex formPanel" role="tablist">
<li class="nav-item" role="presentation">
<a
class="nav-link active"
role="tab"
data-bs-toggle="tab"
href="#tab-elements"
>
<svg
class="bi bi-file-text pe-0 me-2"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
style="font-size: 16px"
>
<path
d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z"
></path>
<path
d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1"
></path>
</svg>
Elemente
</a>
</li>
<li class="nav-item" role="presentation">
<a
class="nav-link"
role="tab"
data-bs-toggle="tab"
href="#tab-tree"
>
<svg
class="bi bi-card-heading pe-0 me-2"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"
></path>
<path
d="M3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m0-5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5z"
></path>
</svg>
Baum
</a>
</li>
</ul>
<div class="tab-content" style="height: 100%">
<div id="tab-tree" class="tab-pane" role="tabpanel">
<FormDesignerTreeViewComponent
:nodes="formJSON"
></FormDesignerTreeViewComponent>
</div>
<div id="tab-elements" class="tab-pane active" role="tabpanel">
<FormDesignerToolsComponent></FormDesignerToolsComponent>
</div>
</div>
</div>
<!-- Vertikaler Splitter -->
<div class="splitter" @mousedown="startVerticalResize"></div>
<!-- Mittlerer Bereich -->
<div
class="panel middle p-3"
:style="{ width: middleWidth + 'px' }"
>
<FormRendererComponent
:schema="schema"
@update-schema="addElementToForm"
/>
<slot name="middle"></slot>
</div>
<!-- Zweiter vertikaler Splitter -->
<div class="splitter" @mousedown="startSecondVerticalResize"></div>
<!-- Rechter Bereich -->
<div
class="panel right"
style="background-color: var(--bs-primary); color: white"
:style="{ width: rightWidth + 'px' }"
>
<slot name="right"></slot>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import FormDesignerToolsComponent from "@/modules/process/components/formDesigner/FormDesignerToolsComponent.vue";
import FormDesignerTreeViewComponent from "@/modules/process/components/formDesigner/FormDesignerTreeViewComponent.vue";
import FormRendererComponent from "@/modules/process/components/formDesigner/FormRendererComponent.vue";
import { InputTextBlock } from "@/modules/process/models/formDesigner/blocks/InputTextBlock";
import { ButtonBlock } from "@/modules/process/models/formDesigner/blocks/ButtonBlock";
import { LabelBlock } from "@/modules/process/models/formDesigner/blocks/LabelBlock";
import { FlexLayoutBlock } from "@/modules/process/models/formDesigner/blocks/FlexLayoutBlock";
import { InputDateBlock } from "@/modules/process/models/formDesigner/blocks/InputDateBlock";
import { InputNumberBlock } from "@/modules/process/models/formDesigner/blocks/InputNumberBlock";
export default {
name: "FormDesignerLayout",
// Verwendete Komponenten
components: {
FormDesignerToolsComponent,
FormDesignerTreeViewComponent,
FormRendererComponent,
},
data() {
return {
leftWidth: 400, // Feste Breite für den linken Bereich
rightWidth: 200, // Feste Breite für den rechten Bereich
middleWidth: window.innerWidth - 400, // Dynamisch: Restliche Breite
topHeight: 150, // Fixierte Höhe für den oberen Bereich
bottomHeight: window.innerHeight - 150, // Dynamisch: Resthöhe
isResizingVertical: false,
isResizingSecondVertical: false,
isResizingHorizontal: false,
startX: 0,
startY: 0,
schema: [],
};
},
mounted() {
window.addEventListener("resize", this.updateSizes);
this.schema = [
new InputTextBlock({ label: "Vorname", placeholder: "Dein Name" }),
new ButtonBlock({ label: "Klick mich an" }),
new LabelBlock({ label: "Ich bin ein Label" }),
new FlexLayoutBlock({
items: [
new InputDateBlock({
label: "Geburtstag",
placeholder: "Datumswert",
}),
new InputNumberBlock({
label: "Alter",
placeholder: "Gib dein Alter ein",
}),
],
}),
];
},
beforeUnmount() {
window.removeEventListener("resize", this.updateSizes);
},
methods: {
updateSizes() {
this.middleWidth = window.innerWidth - this.leftWidth - this.rightWidth;
this.bottomHeight = window.innerHeight - this.topHeight;
},
addElementToForm(newSchema) {
this.schema = newSchema;
},
// Vertikaler Splitter (zwischen links und Mitte)
startVerticalResize(event) {
this.isResizingVertical = true;
this.startX = event.clientX;
document.addEventListener("mousemove", this.resizeVertical);
document.addEventListener("mouseup", this.stopResize);
},
resizeVertical(event) {
if (!this.isResizingVertical) return;
const delta = event.clientX - this.startX;
const newLeftWidth = this.leftWidth + delta;
if (newLeftWidth >= 100 && this.middleWidth - delta >= 100) {
this.leftWidth = newLeftWidth;
this.middleWidth = window.innerWidth - this.leftWidth - this.rightWidth;
}
this.startX = event.clientX;
},
// Zweiter vertikaler Splitter (zwischen Mitte und Rechts)
startSecondVerticalResize(event) {
this.isResizingSecondVertical = true;
this.startX = event.clientX;
document.addEventListener("mousemove", this.resizeSecondVertical);
document.addEventListener("mouseup", this.stopResize);
},
resizeSecondVertical(event) {
if (!this.isResizingSecondVertical) return;
const delta = event.clientX - this.startX;
const newRightWidth = this.rightWidth - delta;
if (newRightWidth >= 100 && this.middleWidth + delta >= 100) {
this.rightWidth = newRightWidth;
this.middleWidth = window.innerWidth - this.leftWidth - this.rightWidth;
}
this.startX = event.clientX;
},
// Horizontaler Splitter für den linken Bereich
startHorizontalResize(event) {
this.isResizingHorizontal = true;
this.startY = event.clientY;
document.addEventListener("mousemove", this.resizeHorizontal);
document.addEventListener("mouseup", this.stopResize);
},
resizeHorizontal(event) {
if (!this.isResizingHorizontal) return;
const delta = event.clientY - this.startY;
const newTopHeight = this.topHeight + delta;
const newBottomHeight = this.bottomHeight - delta;
if (newTopHeight >= 50 && newBottomHeight >= 50) {
this.topHeight = newTopHeight;
this.bottomHeight = newBottomHeight;
}
this.startY = event.clientY;
},
stopResize() {
this.isResizingVertical = false;
this.isResizingSecondVertical = false;
this.isResizingHorizontal = false;
document.removeEventListener("mousemove", this.resizeVertical);
document.removeEventListener("mousemove", this.resizeSecondVertical);
document.removeEventListener("mousemove", this.resizeHorizontal);
document.removeEventListener("mouseup", this.stopResize);
},
},
computed: {
jsonOutput() {
return JSON.stringify(this.formJSON, null, 2); // JSON sauber formatieren
},
},
};
</script>
<style scoped>
.split-container {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
}
/* Linker Bereich mit fixer Höhe oben */
.left {
display: flex;
flex-direction: column;
}
.top-panel {
height: 150px; /* Feste Höhe */
overflow: auto;
}
.bottom-panel {
overflow: auto;
}
/* Allgemeine Panel-Styling */
.panel {
overflow: auto;
flex-grow: 1;
}
/* Splitter für vertikale Trennung */
.splitter {
width: 6px;
background: #27445d21;
cursor: ew-resize;
user-select: none;
}
/* Splitter für horizontale Trennung */
.horizontal-splitter {
height: 3px;
background: #27445d21;
cursor: ns-resize;
user-select: none;
}
.pro-textarea {
height: 100dvh;
width: 100dvh;
}
.formPanel a {
border-radius: 0;
border: none;
}
.formPanel .nav-item {
width: 50%;
}
.formPanel .nav-link:not(.active) {
background-color: white;
color: black !important;
}
.formPanel .nav-link.active {
background-color: var(--bs-primary);
color: white !important;
}
</style>

View File

@ -0,0 +1,18 @@
export class FormBlock {
constructor(type, props = {}) {
this.type = type;
this.props = props;
}
toJSON() {
return {
type: this.type,
...this.props,
};
}
// Optional: Logik für spätere Validierung
validate(value) {
return value;
}
}

View File

@ -0,0 +1,27 @@
import { ButtonBlock } from "../../../utils/formular/blocks/ButtonBlock";
import { FlexLayoutBlock } from "../../../utils/formular/blocks/FlexLayoutBlock";
import { InputDateBlock } from "../../../utils/formular/blocks/InputDateBlock";
import { InputNumberBlock } from "../../../utils/formular/blocks/InputNumberBlock";
import { InputTextBlock } from "../../../utils/formular/blocks/InputTextBlock";
import { LabelBlock } from "../../../utils/formular/blocks/LabelBlock";
export class FormBlockFactory {
static createBlock(data) {
switch (data.type) {
case "InputText":
return new InputTextBlock(data);
case "Label":
return new LabelBlock(data);
case "Button":
return new ButtonBlock(data);
case "FlexLayout":
return new FlexLayoutBlock(data);
case "InputDate":
return new InputDateBlock(data);
case "InputNumber":
return new InputNumberBlock(data);
default:
throw new Error(`Unbekannter Baustein-Typ: ${data.type}`);
}
}
}

View File

@ -0,0 +1,13 @@
import { FormBlock } from "../FormBlock";
export class ButtonBlock extends FormBlock {
constructor({ label = "Klick mich" } = {}) {
super("Button", {
label,
});
}
validate() {
console.log("Validier ist noch nicht fertig");
}
}

View File

@ -0,0 +1,14 @@
import { FormBlock } from "../FormBlock";
export class FlexLayoutBlock extends FormBlock {
constructor({ direction = "horizontal", items = [] } = {}) {
super("FlexLayout", {
direction,
items,
});
}
validate() {
console.log("Validier ist noch nicht fertig");
}
}

View File

@ -0,0 +1,14 @@
import { FormBlock } from "../FormBlock";
export class InputDateBlock extends FormBlock {
constructor({ label = "", placeholder = "" } = {}) {
super("InputDate", {
label,
placeholder,
});
}
validate() {
console.log("Validier ist noch nicht fertig");
}
}

View File

@ -0,0 +1,14 @@
import { FormBlock } from "../FormBlock";
export class InputNumberBlock extends FormBlock {
constructor({ label = "", placeholder = "" } = {}) {
super("InputNumber", {
label,
placeholder,
});
}
validate() {
console.log("Validier ist noch nicht fertig");
}
}

View File

@ -0,0 +1,14 @@
import { FormBlock } from "../FormBlock";
export class InputTextBlock extends FormBlock {
constructor({ label = "", placeholder = "" } = {}) {
super("InputText", {
label,
placeholder,
});
}
validate() {
console.log("Validier ist noch nicht fertig");
}
}

View File

@ -0,0 +1,13 @@
import { FormBlock } from "../FormBlock";
export class LabelBlock extends FormBlock {
constructor({ label = "Ich bin ein Label" } = {}) {
super("Label", {
label,
});
}
validate() {
console.log("Validier ist noch nicht fertig");
}
}

View File

@ -0,0 +1,15 @@
import { BaseModel } from "@/modules/shared/models/Basemodel";
export class Process extends BaseModel {
constructor({ id, name, description } = {}) {
super();
this.id = id;
this.name = name;
this.description = description;
this.topic = "system_process";
}
getPublicFields() {
return ["id", "name", "description"];
}
}

View File

@ -0,0 +1,76 @@
<template>
<div class="container mt-2" style="width: 100vw; height: 100vh">
<h1 class="mb-3">{{ processObject.name }}</h1>
<label class="form-label mb-0" style="font-size: 14px">Beschreibung</label>
<textarea
class="form-control-sm mb-3 pb-2 ms-0 me-0"
wrap="hard"
style="width: 100%"
v-model="processObject.description"
></textarea>
<ProcessTableComponent
:viewProp="objectData"
:tableTitleProp="'Ansichten'"
></ProcessTableComponent>
<ProcessTableComponent
:viewProp="processObject.workflows"
:tableTitleProp="'Workflows'"
></ProcessTableComponent>
</div>
</template>
<script>
import ProcessTableComponent from "@/modules/process/components/process/ProcessTableComponent.vue";
import { getObject } from "@/utils/getObject.js";
//import { getSpecificObject } from "@/utils/getSpecificObject";
export default {
name: "ProcessOverviewComponent",
// Verwendete Komponenten
components: {
ProcessTableComponent,
},
// Props für die Komponente
props: {
object: {
type: Object,
required: true,
},
objectId: {
type: String,
required: true,
},
},
// Daten der Komponente
data() {
return {
processObject: {},
objectData: {},
};
},
// Computed Propertiesprocesses.data
computed: {},
// Methoden
methods: {},
// Lifecycle-Hooks
mounted() {
this.processObject = { ...this.object };
console.log(this.processObject);
this.wsHandler = getObject(
"system_processView",
["name", "description"],
(id, updated) => {
this.objectData = { ...this.objectData, [id]: updated };
}
);
},
beforeUnmount() {
this.specificObjectHandler?.unsubscribe();
},
};
</script>
<style scoped></style>

View File

@ -0,0 +1,74 @@
import socketService from "@/modules/shared/services/WebSocketService";
import { reactive } from "vue";
export class BaseModel {
constructor() {
this._activeSubscriptions = new Map();
}
makeReactive() {
return reactive(this);
}
toJSON() {
return this.getPublicFields().reduce((obj, key) => {
obj[key] = this[key];
return obj;
}, {});
}
subscribe(fields = [], callback = null) {
if (!this.topic || !this.id) {
console.warn("BaseModel.subscribe(): 'topic' und 'id' fehlen.");
return;
}
fields.forEach((field) => {
const filter = { _id: this.id };
const key = `${field}`;
const handler = (value) => {
const newValue = Array.isArray(value)
? value[0]?.transientValue
: value?.transientValue ?? value;
if (Object.prototype.hasOwnProperty.call(this, field)) {
// Feld existiert bereits -> Vue kann es tracken
this[field] = newValue;
} else {
// 🧠 Feld existiert noch nicht -> tricksen:
Object.assign(this, { [field]: newValue });
}
if (typeof callback === "function") {
callback(field, newValue);
}
};
this._activeSubscriptions.set(key, { field, filter, handler });
socketService.subscribe(this.topic, field, filter, handler);
});
}
unsubscribe(fields = []) {
if (!this.topic || !this.id) return;
fields.forEach((field) => {
const key = `${field}`;
const sub = this._activeSubscriptions.get(key);
if (sub) {
socketService.unsubscribe(
this.topic,
sub.field,
sub.filter,
sub.handler
);
this._activeSubscriptions.delete(key);
}
});
}
unsubscribeAll() {
this.unsubscribe([...this._activeSubscriptions.keys()]);
}
}

View File

@ -0,0 +1,53 @@
function generateKey(subject, field, filter = {}) {
return `${subject}/${field}/${JSON.stringify(filter)}`;
}
export class SubscriptionManager {
constructor(socketClient) {
this.socketClient = socketClient;
this.subscribers = {}; // { key: [callback1, callback2, ...] }
this.socketClient.onEvent(this._handleMessage.bind(this));
}
_handleMessage(event) {
if (event.type !== "message") return;
const { subject, field, filter = {}, value } = event.payload;
const key = generateKey(subject, field, filter);
if (this.subscribers[key]) {
this.subscribers[key].forEach((cb) => {
if (typeof cb === "function") {
cb(value);
}
});
}
}
subscribe(subject, field, filter = {}, callback) {
const key = generateKey(subject, field, filter);
if (!this.subscribers[key]) {
this.subscribers[key] = [];
console.log({ action: "subscribe", subject, field, filter });
this.socketClient.send({ action: "subscribe", subject, field, filter });
}
this.subscribers[key].push(callback);
}
unsubscribe(subject, field, filter = {}, callback) {
const key = generateKey(subject, field, filter);
const callbacks = this.subscribers[key];
if (!callbacks) return;
this.subscribers[key] = callbacks.filter((cb) => cb !== callback);
if (this.subscribers[key].length === 0) {
this.socketClient.send({ action: "unsubscribe", subject, field, filter });
delete this.subscribers[key];
}
}
}

View File

@ -0,0 +1,61 @@
const VUE_APP_WS_URL = process.env.VUE_APP_WS_URL || "ws://localhost";
export class WebSocketClient {
constructor(url = VUE_APP_WS_URL) {
this.url = url;
this.socket = null;
this.handlers = new Set();
}
connect() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
return Promise.resolve();
}
return new Promise((resolve) => {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log("WebSocket verbunden.");
this.handlers.forEach((h) => h({ type: "open" }));
resolve(); // ✅ Promise wird hier erfüllt
};
this.socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handlers.forEach((h) =>
h({ type: "message", payload: message })
);
} catch (err) {
console.error("WebSocket-Parsing-Fehler:", err);
}
};
this.socket.onclose = () => {
console.warn("WebSocket getrennt. Reconnect in 2s...");
//setTimeout(() => this.connect(), 2000); // <- ruft sich selbst erneut auf
};
this.socket.onerror = (err) => {
console.error("WebSocket-Fehler:", err);
};
});
}
send(data) {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
console.warn("WebSocket nicht bereit:", data);
}
}
onEvent(callback) {
this.handlers.add(callback);
}
removeEvent(callback) {
this.handlers.delete(callback);
}
}

View File

@ -0,0 +1,7 @@
import { WebSocketClient } from "./WebSocketClient";
import { SubscriptionManager } from "./SubscriptionManager";
export const client = new WebSocketClient();
const subscriptionManager = new SubscriptionManager(client);
export default subscriptionManager;

View File

@ -1,32 +1,17 @@
<template>
<div class="container mt-2" style="width: 100vw; height: 100vh">
<h1 class="mb-3">
processes
<svg
class="bi bi-plus-circle-fill ps-0 ms-3"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
viewBox="0 0 16 16"
style="font-size: 20px; color: var(--bs-primary) !important"
>
<path
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.5 4.5a.5.5 0 0 0-1 0v3h-3a.5.5 0 0 0 0 1h3v3a.5.5 0 0 0 1 0v-3h3a.5.5 0 0 0 0-1h-3z"
></path>
</svg>
</h1>
<h1 class="mb-3">Prozesse</h1>
<div class="row" style="text-align: left">
<div
v-for="process in processes"
:key="process.id"
v-for="(object, objectId) in objectData"
:key="objectId"
class="col-3 ps-0 pe-0 me-5 pointer"
style="border: 1px solid rgba(39, 68, 93, 0.39); min-height: 130px"
@click="
setActivePage(
'ProcessOverviewComponent',
processData[process.id]?.name || 'Unknown',
{ process },
object.name || 'Unknown',
{ object: object, objectId: objectId },
'process'
)
"
@ -41,11 +26,11 @@
"
>
<h1 style="font-size: 22px">
{{ processData[process.id]?.name || "Loading..." }}
{{ object.name || "Loading..." }}
</h1>
</div>
<p class="ms-2 pe-2 pt-2 pb-2" style="font-size: 14px">
{{ process.description }}
{{ object.description }}
</p>
</div>
</div>
@ -53,17 +38,16 @@
</template>
<script>
import { useUiStore } from "../store/uiStore.js";
import WebSocketService from "@/services/WebSocketService";
import { useUiStore } from "@/store/uiStore.js";
//import { getObject } from "@/utils/getObject.js";
import socketService from "@/modules/shared/services/WebSocketService";
import { Process } from "@/modules/process/models/process/Process";
export default {
name: "DevelopmentDashboard",
data() {
return {
processes: [],
processData: {},
callbacks: {}, // field-wise callbacks per processId
fields: ["name"], // fields to subscribe
objectData: [],
uiStore: useUiStore(),
};
},
@ -72,66 +56,26 @@ export default {
setActivePage(componentName, displayName, props, menuItem) {
this.uiStore.setActivePage(componentName, displayName, props, menuItem);
},
handleNewProcessId(processId) {
if (this.processes.some((p) => p.id === processId)) return;
this.processes.push({ id: processId });
if (!this.processData[processId]) {
this.processData[processId] = {};
}
if (!this.callbacks[processId]) {
this.callbacks[processId] = {};
}
this.fields.forEach((field) => {
const callback = (value) => {
this.handleUpdate(processId, field, value);
};
this.callbacks[processId][field] = callback;
WebSocketService.subscribe(
"process",
field,
{ id: processId },
callback
);
});
console.log(this.processes);
},
handleUpdate(processId, field, value) {
if (!this.processData[processId]) {
this.processData[processId] = {};
}
this.processData[processId][field] = value;
},
},
mounted() {
this._idCallback = (value) => {
const ids = [...new Set(Array.isArray(value) ? value : [value])];
ids.forEach((id) => this.handleNewProcessId(id));
};
WebSocketService.subscribe("process", "_id", {}, this._idCallback);
},
beforeUnmount() {
Object.entries(this.callbacks).forEach(([processId, fieldMap]) => {
Object.entries(fieldMap).forEach(([field, callback]) => {
WebSocketService.unsubscribe(
"process",
field,
{ id: processId },
callback
);
socketService.subscribe("system_process", "_id", {}, (val) => {
val.forEach((element) => {
let curProcess = new Process({ id: element }).makeReactive();
curProcess.subscribe(["name", "description"]);
this.objectData.push(curProcess);
});
});
WebSocketService.unsubscribe("process", "_id", {}, this._idCallback);
/*this.wsHandler = getObject(
"system_process",
["name", "description"],
(id, updated) => {
this.objectData = { ...this.objectData, [id]: updated };
}
);*/
},
beforeUnmount() {},
};
</script>

View File

@ -1,65 +0,0 @@
<template>
<div class="container mt-2" style="width: 100vw; height: 100vh">
<h1 class="mb-3">{{ processObject.name }}</h1>
<label class="form-label mb-0" style="font-size: 14px">Bemerkung</label>
<textarea class="form-control-sm mb-3 pb-2 ms-0 me-0" wrap="hard" style="width: 100%" v-model="processObject.description"></textarea>
<ProcessTableComponent :viewProp="processObject.views" :tableTitleProp="'Ansichten'"></ProcessTableComponent>
<ProcessTableComponent :viewProp="processObject.workflows" :tableTitleProp="'Workflows'"></ProcessTableComponent>
</div>
</template>
<script>
import processes from "@/dummyData/processes.json";
import ProcessTableComponent from "@/components/process/ProcessTableComponent.vue";
export default {
name: "ProcessOverviewComponent",
// Verwendete Komponenten
components: {
ProcessTableComponent,
},
// Props für die Komponente
props: {
process: {
type: Object,
required: true,
},
},
// Daten der Komponente
data() {
return {
processObject: {},
};
},
// Computed Propertiesprocesses.data
computed: {},
// Methoden
methods: {
getProcess(processId) {
if (processId) {
let currentProcess = processes.processes.find((processEntry) => processEntry.id === processId);
if (currentProcess) {
this.processObject = currentProcess;
} else {
console.error(`Prozess mit ID ${processId} nicht gefunden.`);
}
} else {
console.error("Kein gültiger Prozess-ID übergeben.");
}
},
},
// Lifecycle-Hooks
mounted() {
if (this.process && this.process.id) {
this.getProcess(this.process.id);
} else {
console.error("Prozess ist undefiniert oder hat keine ID.");
}
},
};
</script>
<style scoped></style>

66
src/utils/getObject.js.js Normal file
View File

@ -0,0 +1,66 @@
import WebSocketService from "@/services/WebSocketService";
/**
* Synchronisiert Objekte per WebSocket über ID + Felder
* @param {string} subject
* @param {string[]} fields
* @param {(id: string, data: Object) => void} [onUpdate]
* @returns {{ dataMap: Object, unsubscribe: Function }}
*/
export function getObject(subject, fields = [], onUpdate) {
const dataMap = {};
const callbacks = {};
let idCallback = null;
idCallback = (value) => {
const ids = [...new Set(Array.isArray(value) ? value : [value])];
ids.forEach((id) => {
if (dataMap[id]) return;
dataMap[id] = {};
callbacks[id] = {};
fields.forEach((field) => {
const callback = (val) => {
let safeValue;
if (Array.isArray(val)) {
if (val.length === 1) {
// Nur ein Wert → direkt den Wert extrahieren
safeValue = val[0]?.transientValue;
} else {
// Mehrere Werte → Liste aus transientValue extrahieren
safeValue = val.map((entry) => entry.transientValue);
}
} else {
safeValue = undefined; // oder throw error / fallback
}
dataMap[id][field] = safeValue;
if (typeof onUpdate === "function") {
onUpdate(id, { ...dataMap[id] });
}
};
callbacks[id][field] = callback;
WebSocketService.subscribe(subject, field, { _id: id }, callback);
});
});
};
WebSocketService.subscribe(subject, "_id", {}, idCallback);
return {
dataMap,
unsubscribe: () => {
Object.entries(callbacks).forEach(([id, fieldMap]) => {
Object.entries(fieldMap).forEach(([field, cb]) => {
WebSocketService.unsubscribe(subject, field, { _id: id }, cb);
});
});
WebSocketService.unsubscribe(subject, "_id", {}, idCallback);
},
};
}

View File

@ -0,0 +1,47 @@
import WebSocketService from "@/services/WebSocketService";
/**
* Holt ein spezifisches Objekt per WebSocket anhand eines Filters
* @param {string} subject
* @param {Object} filter
* @param {string[]} fields
* @param {(data: Object) => void} [onUpdate]
* @returns {{ object: Object, unsubscribe: Function }}
*/
export function getSpecificObject(subject, filter = {}, fields = [], onUpdate) {
const object = {};
const callbacks = {};
fields.forEach((field) => {
const callback = (val) => {
let safeValue;
if (Array.isArray(val)) {
safeValue =
val.length === 1
? val[0]?.transientValue
: val.map((v) => v.transientValue);
} else {
safeValue = undefined;
}
object[field] = safeValue;
if (typeof onUpdate === "function") {
onUpdate({ ...object });
}
};
callbacks[field] = callback;
WebSocketService.subscribe(subject, field, filter, callback);
});
return {
object,
unsubscribe: () => {
Object.entries(callbacks).forEach(([field, cb]) => {
WebSocketService.unsubscribe(subject, field, filter, cb);
});
},
};
}