refactor(qt6): component extraction batch 3 — WorkflowEditor, editors, remaining splits

WorkflowEditor (1432→631): CWorkflowCanvas, CNodePalette, CNodePropertiesPanel, CConnectionLayer, CWorkflowToolbar
+ CssClassManager, DatabaseManager, DropdownConfigManager, MediaServicePanel,
  PageRoutesManager, UserManagement split into extracted components
+ Theme editor: ThemeLivePreview, ThemeSpacingRadius, ThemeTypography
+ SMTP editor: CSmtpTemplateEditor, CSmtpTemplateList, CSmtpTestEmailForm

Net: -2,549 lines from view files into reusable components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 09:57:44 +00:00
parent 3886ecf4b5
commit e6d06f3fa3
12 changed files with 1170 additions and 3719 deletions

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
import "qmllib/MetaBuilder"
Rectangle {
id: root
@@ -12,87 +13,21 @@ Rectangle {
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── Mock data ──────────────────────────────────────────────────────
// ── State ──────────────────────────────────────────────────────────
property var cssClasses: [
{
name: "card-primary",
usageCount: 14,
properties: [
{ prop: "background-color", value: "#1976d2" },
{ prop: "border-radius", value: "12px" },
{ prop: "padding", value: "16px" },
{ prop: "color", value: "#ffffff" }
]
},
{
name: "card-dark",
usageCount: 9,
properties: [
{ prop: "background-color", value: "#1e1e2e" },
{ prop: "border-radius", value: "8px" },
{ prop: "color", value: "#cdd6f4" },
{ prop: "padding", value: "20px" }
]
},
{
name: "text-accent",
usageCount: 22,
properties: [
{ prop: "color", value: "#f59e0b" },
{ prop: "font-size", value: "14px" },
{ prop: "opacity", value: "0.95" }
]
},
{
name: "btn-glow",
usageCount: 6,
properties: [
{ prop: "background-color", value: "#7c3aed" },
{ prop: "border-radius", value: "24px" },
{ prop: "box-shadow", value: "0 0 12px rgba(124,58,237,0.5)" },
{ prop: "color", value: "#ffffff" },
{ prop: "padding", value: "10px 24px" }
]
},
{
name: "surface-elevated",
usageCount: 17,
properties: [
{ prop: "background-color", value: "#2a2a3c" },
{ prop: "border-radius", value: "6px" },
{ prop: "box-shadow", value: "0 4px 12px rgba(0,0,0,0.3)" },
{ prop: "padding", value: "12px" }
]
},
{
name: "badge-live",
usageCount: 3,
properties: [
{ prop: "background-color", value: "#ef4444" },
{ prop: "border-radius", value: "999px" },
{ prop: "color", value: "#ffffff" },
{ prop: "font-size", value: "11px" },
{ prop: "padding", value: "2px 8px" }
]
},
{
name: "panel-glass",
usageCount: 8,
properties: [
{ prop: "background-color", value: "rgba(255,255,255,0.06)" },
{ prop: "border-radius", value: "16px" },
{ prop: "opacity", value: "0.9" },
{ prop: "padding", value: "24px" }
]
}
{ name: "card-primary", usageCount: 14, properties: [{ prop: "background-color", value: "#1976d2" }, { prop: "border-radius", value: "12px" }, { prop: "padding", value: "16px" }, { prop: "color", value: "#ffffff" }] },
{ name: "card-dark", usageCount: 9, properties: [{ prop: "background-color", value: "#1e1e2e" }, { prop: "border-radius", value: "8px" }, { prop: "color", value: "#cdd6f4" }, { prop: "padding", value: "20px" }] },
{ name: "text-accent", usageCount: 22, properties: [{ prop: "color", value: "#f59e0b" }, { prop: "font-size", value: "14px" }, { prop: "opacity", value: "0.95" }] },
{ name: "btn-glow", usageCount: 6, properties: [{ prop: "background-color", value: "#7c3aed" }, { prop: "border-radius", value: "24px" }, { prop: "box-shadow", value: "0 0 12px rgba(124,58,237,0.5)" }, { prop: "color", value: "#ffffff" }, { prop: "padding", value: "10px 24px" }] },
{ name: "surface-elevated", usageCount: 17, properties: [{ prop: "background-color", value: "#2a2a3c" }, { prop: "border-radius", value: "6px" }, { prop: "box-shadow", value: "0 4px 12px rgba(0,0,0,0.3)" }, { prop: "padding", value: "12px" }] },
{ name: "badge-live", usageCount: 3, properties: [{ prop: "background-color", value: "#ef4444" }, { prop: "border-radius", value: "999px" }, { prop: "color", value: "#ffffff" }, { prop: "font-size", value: "11px" }, { prop: "padding", value: "2px 8px" }] },
{ name: "panel-glass", usageCount: 8, properties: [{ prop: "background-color", value: "rgba(255,255,255,0.06)" }, { prop: "border-radius", value: "16px" }, { prop: "opacity", value: "0.9" }, { prop: "padding", value: "24px" }] }
]
property int selectedClassIndex: 0
property bool showAddClassDialog: false
property bool showDeleteConfirm: false
property string newClassName: ""
property bool showSuggestions: false
property int editingPropertyIndex: -1
property var propertySuggestions: [
"color", "background-color", "border-radius", "padding",
@@ -101,117 +36,53 @@ Rectangle {
]
// ── Helpers ─────────────────────────────────────────────────────────
function selectedClass() {
if (selectedClassIndex >= 0 && selectedClassIndex < cssClasses.length)
return cssClasses[selectedClassIndex]
if (selectedClassIndex >= 0 && selectedClassIndex < cssClasses.length) return cssClasses[selectedClassIndex]
return null
}
function updateClasses(arr) {
cssClasses = arr
if (useLiveData) saveCssClass(selectedClassIndex)
}
function updateClasses(arr) { cssClasses = arr; if (useLiveData) saveCssClass(selectedClassIndex) }
function addPropertyToSelected() {
var cls = cssClasses.slice()
var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice()
props.push({ prop: "property", value: "value" })
entry.properties = props
cls[selectedClassIndex] = entry
updateClasses(cls)
var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice(); props.push({ prop: "property", value: "value" })
entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls)
}
function removePropertyFromSelected(propIndex) {
var cls = cssClasses.slice()
var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice()
props.splice(propIndex, 1)
entry.properties = props
cls[selectedClassIndex] = entry
updateClasses(cls)
var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice(); props.splice(propIndex, 1)
entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls)
}
function setPropertyName(propIndex, name) {
var cls = cssClasses.slice()
var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice()
props[propIndex] = { prop: name, value: props[propIndex].value }
entry.properties = props
cls[selectedClassIndex] = entry
updateClasses(cls)
var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice(); props[propIndex] = { prop: name, value: props[propIndex].value }
entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls)
}
function setPropertyValue(propIndex, val) {
var cls = cssClasses.slice()
var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice()
props[propIndex] = { prop: props[propIndex].prop, value: val }
entry.properties = props
cls[selectedClassIndex] = entry
updateClasses(cls)
var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex])
var props = entry.properties.slice(); props[propIndex] = { prop: props[propIndex].prop, value: val }
entry.properties = props; cls[selectedClassIndex] = entry; updateClasses(cls)
}
function setClassName(name) {
var cls = cssClasses.slice()
var entry = Object.assign({}, cls[selectedClassIndex])
entry.name = name
cls[selectedClassIndex] = entry
updateClasses(cls)
var cls = cssClasses.slice(); var entry = Object.assign({}, cls[selectedClassIndex])
entry.name = name; cls[selectedClassIndex] = entry; updateClasses(cls)
}
function addClass(name) {
var newClass = { name: name, usageCount: 0, properties: [{ prop: "color", value: "#ffffff" }] }
if (useLiveData) {
dbal.execute("core/css-classes/create", { data: newClass }, function(r, e) { if (!e) loadCssClasses() })
}
var cls = cssClasses.slice()
cls.push(newClass)
cssClasses = cls
selectedClassIndex = cls.length - 1
if (useLiveData) dbal.execute("core/css-classes/create", { data: newClass }, function(r, e) { if (!e) loadCssClasses() })
var cls = cssClasses.slice(); cls.push(newClass); cssClasses = cls; selectedClassIndex = cls.length - 1
}
function deleteSelectedClass() {
if (cssClasses.length <= 1) return
if (useLiveData && cssClasses[selectedClassIndex].id) {
dbal.execute("core/css-classes/delete", { id: cssClasses[selectedClassIndex].id }, function(r, e) { if (!e) loadCssClasses() })
}
var cls = cssClasses.slice()
cls.splice(selectedClassIndex, 1)
cssClasses = cls
if (selectedClassIndex >= cls.length)
selectedClassIndex = cls.length - 1
}
function resolvePreviewColor(properties, name, fallback) {
for (var i = 0; i < properties.length; i++) {
if (properties[i].prop === name)
return properties[i].value
}
return fallback
}
function resolvePreviewRadius(properties) {
for (var i = 0; i < properties.length; i++) {
if (properties[i].prop === "border-radius")
return parseInt(properties[i].value) || 0
}
return 0
}
function resolvePreviewOpacity(properties) {
for (var i = 0; i < properties.length; i++) {
if (properties[i].prop === "opacity")
return parseFloat(properties[i].value) || 1.0
}
return 1.0
}
function swatchColor(properties) {
var bg = resolvePreviewColor(properties, "background-color", "")
if (bg) return bg
return resolvePreviewColor(properties, "color", Theme.surface)
if (useLiveData && cssClasses[selectedClassIndex].id) dbal.execute("core/css-classes/delete", { id: cssClasses[selectedClassIndex].id }, function(r, e) { if (!e) loadCssClasses() })
var cls = cssClasses.slice(); cls.splice(selectedClassIndex, 1); cssClasses = cls
if (selectedClassIndex >= cls.length) selectedClassIndex = cls.length - 1
}
// ── DBAL Integration ─────────────────────────────────────────────
@@ -240,486 +111,82 @@ Rectangle {
}
// ── Layout ──────────────────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
spacing: 16
anchors.margins: 20
// Header
FlexRow {
Layout.fillWidth: true
spacing: 12
Layout.fillWidth: true; spacing: 12
CText { variant: "h3"; text: "CSS Class Manager" }
Item { Layout.fillWidth: true }
CBadge { text: cssClasses.length + " classes" }
CButton {
text: "Add Class"
variant: "primary"
size: "sm"
onClicked: {
newClassName = ""
showAddClassDialog = true
}
}
CButton { text: "Add Class"; variant: "primary"; size: "sm"; onClicked: { newClassName = ""; showAddClassDialog = true } }
}
CDivider { Layout.fillWidth: true }
// Main split
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16
// ── Left: class list ────────────────────────────────────────
CCard {
Layout.preferredWidth: 280
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
CText { variant: "h4"; text: "Classes" }
ListView {
id: classListView
Layout.fillWidth: true
Layout.fillHeight: true
model: cssClasses
spacing: 4
clip: true
delegate: CListItem {
width: classListView.width
title: modelData.name
subtitle: modelData.properties.length + " properties"
selected: index === selectedClassIndex
// Swatch + usage badge row
Row {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Rectangle {
width: 16; height: 16
radius: 3
color: swatchColor(modelData.properties)
border.color: Theme.border
border.width: 1
}
CBadge { text: modelData.usageCount.toString() }
}
onClicked: {
selectedClassIndex = index
showSuggestions = false
editingPropertyIndex = -1
}
}
}
}
CssClassSidebar {
Layout.preferredWidth: 280; Layout.fillHeight: true
cssClasses: root.cssClasses
selectedIndex: root.selectedClassIndex
onItemClicked: function(idx) { root.selectedClassIndex = idx }
}
// ── Right: editor + preview ─────────────────────────────────
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16
// Editor card
CCard {
Layout.fillWidth: true
Layout.fillHeight: true
CssPropertyEditor {
Layout.fillWidth: true; Layout.fillHeight: true
visible: selectedClass() !== null
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// Class name + delete
FlexRow {
Layout.fillWidth: true
spacing: 12
CTextField {
id: classNameField
Layout.preferredWidth: 260
label: "Class Name"
placeholderText: ".class-name"
text: selectedClass() ? selectedClass().name : ""
onTextChanged: {
if (selectedClass() && text !== selectedClass().name)
setClassName(text)
}
}
Item { Layout.fillWidth: true }
CButton {
text: "Delete Class"
variant: "danger"
size: "sm"
onClicked: showDeleteConfirm = true
}
}
CDivider { Layout.fillWidth: true }
// Properties header
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "h4"; text: "Properties" }
Item { Layout.fillWidth: true }
CButton {
text: "Add Property"
variant: "primary"
size: "sm"
onClicked: addPropertyToSelected()
}
}
// Property list
ListView {
id: propertyListView
Layout.fillWidth: true
Layout.fillHeight: true
model: selectedClass() ? selectedClass().properties : []
spacing: 6
clip: true
delegate: Item {
width: propertyListView.width
height: 48
RowLayout {
anchors.fill: parent
spacing: 8
// Property name field with suggestions
Item {
Layout.preferredWidth: 200
Layout.fillHeight: true
CTextField {
id: propNameField
anchors.fill: parent
label: index === 0 ? "Property" : ""
placeholderText: "property-name"
text: modelData.prop
onTextChanged: {
if (text !== modelData.prop)
setPropertyName(index, text)
}
onActiveFocusChanged: {
if (activeFocus) {
editingPropertyIndex = index
showSuggestions = true
} else {
// Delay hide so clicks on suggestions register
suggestHideTimer.start()
}
}
}
// Suggestions dropdown
Rectangle {
visible: showSuggestions && editingPropertyIndex === index
anchors.top: propNameField.bottom
anchors.left: propNameField.left
width: propNameField.width
height: Math.min(suggestCol.implicitHeight + 8, 180)
z: 100
color: Theme.paper
border.color: Theme.border
border.width: 1
radius: 6
clip: true
Flickable {
anchors.fill: parent
anchors.margins: 4
contentHeight: suggestCol.implicitHeight
clip: true
Column {
id: suggestCol
width: parent.width
spacing: 2
Repeater {
model: propertySuggestions
Rectangle {
width: suggestCol.width
height: 26
radius: 4
color: suggestMa.containsMouse ? Theme.surface : "transparent"
CText {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
variant: "body2"
text: modelData
}
MouseArea {
id: suggestMa
anchors.fill: parent
hoverEnabled: true
onClicked: {
setPropertyName(editingPropertyIndex, modelData)
showSuggestions = false
}
}
}
}
}
}
}
}
// Value field
CTextField {
Layout.fillWidth: true
Layout.fillHeight: true
label: index === 0 ? "Value" : ""
placeholderText: "#000000"
text: modelData.value
onTextChanged: {
if (text !== modelData.value)
setPropertyValue(index, text)
}
}
// Color swatch for color-like values
Rectangle {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
radius: 4
border.color: Theme.border
border.width: 1
color: {
var v = modelData.value
if (v.charAt(0) === "#" || v.indexOf("rgb") === 0)
return v
return "transparent"
}
visible: {
var p = modelData.prop
return p === "color" || p === "background-color"
}
}
// Remove property button
CButton {
text: "x"
variant: "ghost"
size: "sm"
onClicked: removePropertyFromSelected(index)
}
}
}
}
}
selectedClass: root.selectedClass()
propertySuggestions: root.propertySuggestions
onClassNameChanged: function(name) { setClassName(name) }
onDeleteClassClicked: showDeleteConfirm = true
onAddPropertyClicked: addPropertyToSelected()
onRemovePropertyClicked: function(idx) { removePropertyFromSelected(idx) }
onPropertyNameChanged: function(idx, name) { setPropertyName(idx, name) }
onPropertyValueChanged: function(idx, val) { setPropertyValue(idx, val) }
}
// ── Preview card ────────────────────────────────────────
CCard {
Layout.fillWidth: true
Layout.preferredHeight: 160
CssClassPreview {
Layout.fillWidth: true; Layout.preferredHeight: 160
visible: selectedClass() !== null
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
CText { variant: "h4"; text: "Preview" }
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.surface
radius: 6
border.color: Theme.border
border.width: 1
// Checkerboard background for transparency
Grid {
anchors.fill: parent
rows: Math.ceil(height / 12)
columns: Math.ceil(width / 12)
clip: true
Repeater {
model: Math.ceil(parent.parent.width / 12) * Math.ceil(parent.parent.height / 12)
Rectangle {
width: 12; height: 12
color: (Math.floor(index / Math.ceil(parent.parent.width / 12)) + index) % 2 === 0
? "#2a2a2a" : "#333333"
}
}
}
// Rendered preview element
Rectangle {
id: previewRect
anchors.centerIn: parent
width: Math.min(parent.width - 40, 280)
height: Math.min(parent.height - 20, 72)
radius: selectedClass() ? resolvePreviewRadius(selectedClass().properties) : 0
opacity: selectedClass() ? resolvePreviewOpacity(selectedClass().properties) : 1.0
color: selectedClass()
? resolvePreviewColor(selectedClass().properties, "background-color", Theme.surface)
: Theme.surface
CText {
anchors.centerIn: parent
variant: "body1"
text: selectedClass() ? "." + selectedClass().name : ""
color: selectedClass()
? resolvePreviewColor(selectedClass().properties, "color", Theme.text)
: Theme.text
font.pixelSize: {
if (!selectedClass()) return 14
var props = selectedClass().properties
for (var i = 0; i < props.length; i++) {
if (props[i].prop === "font-size")
return parseInt(props[i].value) || 14
}
return 14
}
}
}
}
}
selectedClass: root.selectedClass()
}
}
}
}
// ── Timer to delay hiding suggestions ───────────────────────────────
Timer {
id: suggestHideTimer
interval: 200
onTriggered: showSuggestions = false
}
// ── Add Class Dialog ────────────────────────────────────────────────
CDialog {
id: addClassDialog
visible: showAddClassDialog
title: "Add New Class"
visible: showAddClassDialog; title: "Add New Class"
ColumnLayout {
spacing: 16
width: 320
CText {
variant: "body1"
text: "Enter a name for the new CSS class."
}
CTextField {
id: newClassNameField
Layout.fillWidth: true
label: "Class Name"
placeholderText: "my-class"
text: newClassName
onTextChanged: newClassName = text
}
spacing: 16; width: 320
CText { variant: "body1"; text: "Enter a name for the new CSS class." }
CTextField { Layout.fillWidth: true; label: "Class Name"; placeholderText: "my-class"; text: newClassName; onTextChanged: newClassName = text }
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
size: "sm"
onClicked: showAddClassDialog = false
}
CButton {
text: "Create"
variant: "primary"
size: "sm"
enabled: newClassName.trim().length > 0
onClicked: {
addClass(newClassName.trim())
showAddClassDialog = false
}
}
Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true }
CButton { text: "Cancel"; variant: "ghost"; size: "sm"; onClicked: showAddClassDialog = false }
CButton { text: "Create"; variant: "primary"; size: "sm"; enabled: newClassName.trim().length > 0; onClicked: { addClass(newClassName.trim()); showAddClassDialog = false } }
}
}
}
// ── Delete Confirmation Dialog ──────────────────────────────────────
CDialog {
id: deleteConfirmDialog
visible: showDeleteConfirm
title: "Delete Class"
visible: showDeleteConfirm; title: "Delete Class"
ColumnLayout {
spacing: 16
width: 320
CAlert {
Layout.fillWidth: true
severity: "warning"
text: selectedClass()
? "Are you sure you want to delete \"." + selectedClass().name + "\"? This action cannot be undone."
: "No class selected."
}
CText {
variant: "body2"
text: selectedClass()
? "This class is used in " + selectedClass().usageCount + " place(s)."
: ""
visible: selectedClass() !== null && selectedClass().usageCount > 0
}
spacing: 16; width: 320
CAlert { Layout.fillWidth: true; severity: "warning"; text: selectedClass() ? "Are you sure you want to delete \"." + selectedClass().name + "\"? This action cannot be undone." : "No class selected." }
CText { variant: "body2"; text: selectedClass() ? "This class is used in " + selectedClass().usageCount + " place(s)." : ""; visible: selectedClass() !== null && selectedClass().usageCount > 0 }
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
size: "sm"
onClicked: showDeleteConfirm = false
}
CButton {
text: "Delete"
variant: "danger"
size: "sm"
onClicked: {
deleteSelectedClass()
showDeleteConfirm = false
}
}
Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true }
CButton { text: "Cancel"; variant: "ghost"; size: "sm"; onClicked: showDeleteConfirm = false }
CButton { text: "Delete"; variant: "danger"; size: "sm"; onClicked: { deleteSelectedClass(); showDeleteConfirm = false } }
}
}
}

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
import "qmllib/MetaBuilder"
Rectangle {
id: root
@@ -10,73 +11,8 @@ Rectangle {
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
property var mockBackends: JSON.parse(JSON.stringify(backends))
function loadAdapterStatus() {
dbal.execute("core/adapters", {}, function(result, error) {
if (!error && result && result.adapters && result.adapters.length > 0) {
var liveBackends = []
for (var i = 0; i < result.adapters.length; i++) {
var a = result.adapters[i]
liveBackends.push({
name: a.name || "",
key: a.key || "",
status: a.status || "disconnected",
description: a.description || "",
connectionString: a.connectionString || "",
records: a.records || 0,
sizeKb: a.sizeKb || 0,
lastBackup: a.lastBackup || "Never"
})
}
backends = liveBackends
if (selectedBackendIndex >= liveBackends.length)
selectedBackendIndex = 0
if (activeBackendIndex >= liveBackends.length)
activeBackendIndex = 0
}
// On error or empty result, keep existing mock backends as fallback
})
}
function testConnectionLive(index) {
var backend = backends[index]
testingIndex = index
if (useLiveData) {
dbal.execute("core/test-connection", { adapter: backend.key }, function(result, error) {
var newResults = Object.assign({}, testResults)
if (!error && result && result.success) {
newResults[index] = "success"
} else {
newResults[index] = "error"
}
testResults = newResults
testingIndex = -1
})
} else {
// Fall back to mock timer
testTimer.targetIndex = index
testTimer.start()
}
}
function checkHealth() {
if (useLiveData) {
dbal.ping()
}
}
onUseLiveDataChanged: {
if (useLiveData) loadAdapterStatus()
}
Component.onCompleted: {
loadAdapterStatus()
}
// ── State ──────────────────────────────────────────────────────────
property int selectedBackendIndex: 2
property int activeBackendIndex: 2
@@ -86,7 +22,6 @@ Rectangle {
property int adapterPattern: 0
property bool showExportDialog: false
property bool showImportDialog: false
property var adapterPatterns: ["read-through", "write-through", "cache-aside", "dual-write"]
property var backends: [
@@ -106,18 +41,10 @@ Rectangle {
{ name: "Generic", key: "generic", status: "disconnected", description: "Custom adapter via DATABASE_URL with driver query params", connectionString: "generic://localhost:9999/custom?driver=mydriver", records: 0, sizeKb: 0, lastBackup: "Never" }
]
// ── Mock testing state ─────────────────────────────────────────────
// ── Testing state ─────────────────────────────────────────────
property var testingIndex: -1
property var testResults: ({})
function testConnection(index) {
testingIndex = index
testResults = Object.assign({}, testResults)
delete testResults[index]
testTimer.targetIndex = index
testTimer.start()
}
Timer {
id: testTimer
property int targetIndex: -1
@@ -125,75 +52,63 @@ Rectangle {
onTriggered: {
var backend = backends[targetIndex]
var result = (backend.status === "connected") ? "success" : (backend.status === "error" ? "error" : "warning")
var newResults = Object.assign({}, testResults)
newResults[targetIndex] = result
testResults = newResults
testingIndex = -1
var newResults = Object.assign({}, testResults); newResults[targetIndex] = result
testResults = newResults; testingIndex = -1
}
}
function formatSize(kb) {
if (kb < 1024) return kb + " KB"
return (kb / 1024).toFixed(1) + " MB"
function formatSize(kb) { return kb < 1024 ? kb + " KB" : (kb / 1024).toFixed(1) + " MB" }
function totalRecords() { var s = 0; for (var i = 0; i < backends.length; i++) s += backends[i].records; return s }
function totalSize() { var s = 0; for (var i = 0; i < backends.length; i++) s += backends[i].sizeKb; return s }
function connectedCount() { var c = 0; for (var i = 0; i < backends.length; i++) if (backends[i].status === "connected") c++; return c }
function testConnectionLive(index) {
testingIndex = index
if (useLiveData) {
dbal.execute("core/test-connection", { adapter: backends[index].key }, function(result, error) {
var newResults = Object.assign({}, testResults)
newResults[index] = (!error && result && result.success) ? "success" : "error"
testResults = newResults; testingIndex = -1
})
} else { testTimer.targetIndex = index; testTimer.start() }
}
function totalRecords() {
var sum = 0
for (var i = 0; i < backends.length; i++) sum += backends[i].records
return sum
function loadAdapterStatus() {
dbal.execute("core/adapters", {}, function(result, error) {
if (!error && result && result.adapters && result.adapters.length > 0) {
var liveBackends = []
for (var i = 0; i < result.adapters.length; i++) {
var a = result.adapters[i]
liveBackends.push({ name: a.name || "", key: a.key || "", status: a.status || "disconnected", description: a.description || "",
connectionString: a.connectionString || "", records: a.records || 0, sizeKb: a.sizeKb || 0, lastBackup: a.lastBackup || "Never" })
}
backends = liveBackends
if (selectedBackendIndex >= liveBackends.length) selectedBackendIndex = 0
if (activeBackendIndex >= liveBackends.length) activeBackendIndex = 0
}
})
}
function totalSize() {
var sum = 0
for (var i = 0; i < backends.length; i++) sum += backends[i].sizeKb
return sum
}
onUseLiveDataChanged: { if (useLiveData) loadAdapterStatus() }
Component.onCompleted: { loadAdapterStatus() }
function connectedCount() {
var count = 0
for (var i = 0; i < backends.length; i++)
if (backends[i].status === "connected") count++
return count
}
// ── Export Dialog ──────────────────────────────────────────────────
// ── Export/Import Dialogs ────────────────────────────────────────
Dialog {
id: exportDialog
visible: showExportDialog
title: "Export Database"
modal: true
anchors.centerIn: parent
width: 420
standardButtons: Dialog.Ok | Dialog.Cancel
onAccepted: showExportDialog = false
onRejected: showExportDialog = false
id: exportDialog; visible: showExportDialog; title: "Export Database"; modal: true; anchors.centerIn: parent; width: 420
standardButtons: Dialog.Ok | Dialog.Cancel; onAccepted: showExportDialog = false; onRejected: showExportDialog = false
ColumnLayout {
spacing: 12
width: parent.width
spacing: 12; width: parent.width
CText { variant: "body1"; text: "Export the active database (" + backends[activeBackendIndex].name + ") to a JSON dump file." }
CTextField { label: "Output path"; text: "/tmp/dbal-export-" + backends[activeBackendIndex].key + ".json"; Layout.fillWidth: true }
CAlert { severity: "success"; text: "Export includes all tenants and entity data (" + backends[activeBackendIndex].records + " records)." }
}
}
// ── Import Dialog ──────────────────────────────────────────────────
Dialog {
id: importDialog
visible: showImportDialog
title: "Import Database"
modal: true
anchors.centerIn: parent
width: 420
standardButtons: Dialog.Ok | Dialog.Cancel
onAccepted: showImportDialog = false
onRejected: showImportDialog = false
id: importDialog; visible: showImportDialog; title: "Import Database"; modal: true; anchors.centerIn: parent; width: 420
standardButtons: Dialog.Ok | Dialog.Cancel; onAccepted: showImportDialog = false; onRejected: showImportDialog = false
ColumnLayout {
spacing: 12
width: parent.width
spacing: 12; width: parent.width
CText { variant: "body1"; text: "Import a JSON dump into the active backend (" + backends[activeBackendIndex].name + ")." }
CTextField { label: "Import file"; placeholderText: "/path/to/dbal-export.json"; Layout.fillWidth: true }
CAlert { severity: "warning"; text: "Existing records with matching IDs will be overwritten." }
@@ -202,337 +117,66 @@ Rectangle {
// ── Main Layout ────────────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
anchors.fill: parent; anchors.margins: 20; spacing: 16
// ── Header ────────────────────────────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
Layout.fillWidth: true; spacing: 12
CText { variant: "h3"; text: "Database Manager" }
CStatusBadge { status: "success"; text: connectedCount() + " / " + backends.length + " connected" }
CBadge {
text: useLiveData ? "Connected to DBAL" : "Mock Data"
color: useLiveData ? Theme.success : Theme.warning
}
CBadge { text: useLiveData ? "Connected to DBAL" : "Mock Data"; color: useLiveData ? Theme.success : Theme.warning }
Item { Layout.fillWidth: true }
CButton { text: "Export"; variant: "ghost"; onClicked: showExportDialog = true }
CButton { text: "Import"; variant: "ghost"; onClicked: showImportDialog = true }
}
// ── Stats Row ─────────────────────────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Total Records" }
CText { variant: "h4"; text: totalRecords().toLocaleString() }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Total Size" }
CText { variant: "h4"; text: formatSize(totalSize()) }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Active Backend" }
CText { variant: "h4"; text: backends[activeBackendIndex].name }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: 72
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 4
CText { variant: "caption"; text: "Adapter Pattern" }
CText { variant: "h4"; text: adapterPatterns[adapterPattern] }
}
}
CDatabaseStatsRow {
totalRecords: root.totalRecords().toLocaleString()
totalSize: root.formatSize(root.totalSize())
activeBackend: backends[activeBackendIndex].name
adapterPattern: adapterPatterns[root.adapterPattern]
}
// ── Body: Sidebar + Detail ────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16
// ── Backend List (sidebar) ────────────────────────────────
CCard {
Layout.preferredWidth: 300
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
CText { variant: "subtitle1"; text: "Backends (14)" }
CDivider { Layout.fillWidth: true }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: backends
spacing: 4
clip: true
delegate: CListItem {
width: parent ? parent.width : 268
title: modelData.name
subtitle: modelData.key
selected: index === selectedBackendIndex
leadingIcon: modelData.status === "connected" ? "check_circle" : (modelData.status === "error" ? "error" : "radio_button_unchecked")
onClicked: selectedBackendIndex = index
CStatusBadge {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
status: modelData.status === "connected" ? "success" : (modelData.status === "error" ? "error" : "warning")
text: modelData.status
}
}
}
}
CBackendListSidebar {
backends: root.backends
selectedIndex: selectedBackendIndex
onBackendSelected: function(index) { selectedBackendIndex = index }
}
// ── Detail Panel ──────────────────────────────────────────
CCard {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16
Flickable {
anchors.fill: parent
anchors.margins: 16
contentHeight: detailColumn.implicitHeight
clip: true
CBackendDetailPanel {
backend: backends[selectedBackendIndex]
isActive: selectedBackendIndex === activeBackendIndex
testingIndex: root.testingIndex
backendIndex: selectedBackendIndex
testResult: testResults[selectedBackendIndex]
onTestConnectionRequested: testConnectionLive(selectedBackendIndex)
onSetActiveRequested: activeBackendIndex = selectedBackendIndex
}
CCard {
Layout.fillWidth: true
ColumnLayout {
id: detailColumn
width: parent.width
spacing: 16
anchors.fill: parent; anchors.margins: 16; spacing: 12
// ── Backend Header ────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h4"; text: backends[selectedBackendIndex].name }
CStatusBadge {
status: backends[selectedBackendIndex].status === "connected" ? "success" : (backends[selectedBackendIndex].status === "error" ? "error" : "warning")
text: backends[selectedBackendIndex].status
}
Item { Layout.fillWidth: true }
CBadge {
text: selectedBackendIndex === activeBackendIndex ? "ACTIVE" : "INACTIVE"
accent: selectedBackendIndex === activeBackendIndex
}
}
CText {
variant: "body1"
text: backends[selectedBackendIndex].description
wrapMode: Text.Wrap
Layout.fillWidth: true
}
CDivider { Layout.fillWidth: true }
// ── Connection ────────────────────────────────
CText { variant: "subtitle1"; text: "Connection" }
CTextField {
label: "Connection String"
text: backends[selectedBackendIndex].connectionString
Layout.fillWidth: true
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CButton {
text: testingIndex === selectedBackendIndex ? "Testing..." : "Test Connection"
variant: "primary"
enabled: testingIndex === -1
onClicked: testConnectionLive(selectedBackendIndex)
}
CButton {
text: selectedBackendIndex === activeBackendIndex ? "Active Backend" : "Set as Active"
variant: selectedBackendIndex === activeBackendIndex ? "ghost" : "primary"
enabled: selectedBackendIndex !== activeBackendIndex && backends[selectedBackendIndex].status === "connected"
onClicked: activeBackendIndex = selectedBackendIndex
}
Item { Layout.fillWidth: true }
Loader {
active: testResults[selectedBackendIndex] !== undefined
sourceComponent: CStatusBadge {
status: testResults[selectedBackendIndex] === "success" ? "success" : "error"
text: testResults[selectedBackendIndex] === "success" ? "Connection OK" : "Connection Failed"
}
}
}
CDivider { Layout.fillWidth: true }
// ── Storage Stats ─────────────────────────────
CText { variant: "subtitle1"; text: "Storage Statistics" }
FlexRow {
Layout.fillWidth: true
spacing: 12
CPaper {
Layout.fillWidth: true
implicitHeight: 60
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
CText { variant: "caption"; text: "Records" }
CText { variant: "h4"; text: backends[selectedBackendIndex].records.toLocaleString() }
}
}
CPaper {
Layout.fillWidth: true
implicitHeight: 60
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
CText { variant: "caption"; text: "Size" }
CText { variant: "h4"; text: formatSize(backends[selectedBackendIndex].sizeKb) }
}
}
CPaper {
Layout.fillWidth: true
implicitHeight: 60
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
CText { variant: "caption"; text: "Last Backup" }
CText { variant: "body2"; text: backends[selectedBackendIndex].lastBackup }
}
}
}
CDivider { Layout.fillWidth: true }
// ── Configuration ─────────────────────────────
CText { variant: "subtitle1"; text: "Environment Configuration" }
CTextField {
label: "DATABASE_URL"
text: databaseUrl
onTextChanged: databaseUrl = text
placeholderText: "sqlite:///path/to/db or postgres://user:pass@host/db"
Layout.fillWidth: true
}
CTextField {
label: "DBAL_CACHE_URL (Redis)"
text: cacheUrl
onTextChanged: cacheUrl = text
placeholderText: "redis://localhost:6379/0?ttl=300&pattern=read-through"
Layout.fillWidth: true
}
CTextField {
label: "DBAL_SEARCH_URL (Elasticsearch)"
text: searchUrl
onTextChanged: searchUrl = text
placeholderText: "http://localhost:9200?index=dbal_search&refresh=true"
Layout.fillWidth: true
}
CTextField { label: "DATABASE_URL"; text: databaseUrl; onTextChanged: databaseUrl = text; placeholderText: "sqlite:///path/to/db or postgres://user:pass@host/db"; Layout.fillWidth: true }
CTextField { label: "DBAL_CACHE_URL (Redis)"; text: cacheUrl; onTextChanged: cacheUrl = text; placeholderText: "redis://localhost:6379/0?ttl=300&pattern=read-through"; Layout.fillWidth: true }
CTextField { label: "DBAL_SEARCH_URL (Elasticsearch)"; text: searchUrl; onTextChanged: searchUrl = text; placeholderText: "http://localhost:9200?index=dbal_search&refresh=true"; Layout.fillWidth: true }
CDivider { Layout.fillWidth: true }
// ── Multi-Adapter Pattern ─────────────────────
CText { variant: "subtitle1"; text: "Multi-Adapter Pattern" }
CText { variant: "body2"; text: "Select how the primary, cache, and search adapters coordinate data flow." }
FlexRow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: adapterPatterns
delegate: CButton {
text: modelData
variant: index === adapterPattern ? "primary" : "ghost"
onClicked: adapterPattern = index
}
}
CAdapterPatternSelector {
selectedPattern: root.adapterPattern
onPatternChanged: function(index) { root.adapterPattern = index }
}
CPaper {
Layout.fillWidth: true
implicitHeight: patternDesc.implicitHeight + 24
CText {
id: patternDesc
anchors.fill: parent
anchors.margins: 12
variant: "body2"
wrapMode: Text.Wrap
text: {
var descriptions = [
"Read-through: Reads check cache first. On miss, the cache fetches from the primary DB, stores the result, and returns it. Best for read-heavy workloads.",
"Write-through: Every write goes to both cache and primary DB synchronously. Guarantees consistency at the cost of write latency.",
"Cache-aside: Application manages cache explicitly. Reads check cache, fetch from DB on miss and populate cache. Writes go directly to DB and invalidate cache.",
"Dual-write: Writes are sent to two backends simultaneously (e.g., primary DB + search index). Requires conflict resolution strategy."
]
return descriptions[adapterPattern]
}
}
}
// ── Spacer at bottom ──────────────────────────
Item { height: 16 }
}
}

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
import "qmllib/MetaBuilder"
Rectangle {
id: root
@@ -19,103 +20,13 @@ Rectangle {
property string newDropdownDescription: ""
property var dropdowns: [
{
name: "user_roles",
description: "Assignable user roles for access control",
allowCustom: false,
required: true,
options: [
{ label: "Administrator", value: "admin" },
{ label: "Moderator", value: "moderator" },
{ label: "Editor", value: "editor" },
{ label: "Viewer", value: "viewer" },
{ label: "Guest", value: "guest" }
]
},
{
name: "content_status",
description: "Publication lifecycle status for content items",
allowCustom: false,
required: true,
options: [
{ label: "Draft", value: "draft" },
{ label: "In Review", value: "in_review" },
{ label: "Published", value: "published" },
{ label: "Archived", value: "archived" }
]
},
{
name: "priority_levels",
description: "Task and issue priority classifications",
allowCustom: false,
required: true,
options: [
{ label: "Critical", value: "critical" },
{ label: "High", value: "high" },
{ label: "Medium", value: "medium" },
{ label: "Low", value: "low" },
{ label: "None", value: "none" }
]
},
{
name: "categories",
description: "General-purpose content categorization tags",
allowCustom: true,
required: false,
options: [
{ label: "Technology", value: "technology" },
{ label: "Design", value: "design" },
{ label: "Business", value: "business" },
{ label: "Science", value: "science" },
{ label: "Education", value: "education" },
{ label: "Entertainment", value: "entertainment" }
]
},
{
name: "languages",
description: "Supported interface and content languages",
allowCustom: false,
required: true,
options: [
{ label: "English", value: "en" },
{ label: "Spanish", value: "es" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Japanese", value: "ja" },
{ label: "Chinese", value: "zh" },
{ label: "Portuguese", value: "pt" }
]
},
{
name: "themes",
description: "Available UI theme presets",
allowCustom: true,
required: false,
options: [
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
{ label: "System Default", value: "system" },
{ label: "High Contrast", value: "high_contrast" }
]
},
{
name: "database_backends",
description: "Supported DBAL database adapter backends",
allowCustom: false,
required: true,
options: [
{ label: "SQLite", value: "sqlite" },
{ label: "PostgreSQL", value: "postgres" },
{ label: "MySQL", value: "mysql" },
{ label: "MariaDB", value: "mariadb" },
{ label: "MongoDB", value: "mongodb" },
{ label: "Redis", value: "redis" },
{ label: "CockroachDB", value: "cockroachdb" },
{ label: "SurrealDB", value: "surrealdb" },
{ label: "Supabase", value: "supabase" },
{ label: "In-Memory", value: "memory" }
]
}
{ name: "user_roles", description: "Assignable user roles for access control", allowCustom: false, required: true, options: [{ label: "Administrator", value: "admin" }, { label: "Moderator", value: "moderator" }, { label: "Editor", value: "editor" }, { label: "Viewer", value: "viewer" }, { label: "Guest", value: "guest" }] },
{ name: "content_status", description: "Publication lifecycle status for content items", allowCustom: false, required: true, options: [{ label: "Draft", value: "draft" }, { label: "In Review", value: "in_review" }, { label: "Published", value: "published" }, { label: "Archived", value: "archived" }] },
{ name: "priority_levels", description: "Task and issue priority classifications", allowCustom: false, required: true, options: [{ label: "Critical", value: "critical" }, { label: "High", value: "high" }, { label: "Medium", value: "medium" }, { label: "Low", value: "low" }, { label: "None", value: "none" }] },
{ name: "categories", description: "General-purpose content categorization tags", allowCustom: true, required: false, options: [{ label: "Technology", value: "technology" }, { label: "Design", value: "design" }, { label: "Business", value: "business" }, { label: "Science", value: "science" }, { label: "Education", value: "education" }, { label: "Entertainment", value: "entertainment" }] },
{ name: "languages", description: "Supported interface and content languages", allowCustom: false, required: true, options: [{ label: "English", value: "en" }, { label: "Spanish", value: "es" }, { label: "French", value: "fr" }, { label: "German", value: "de" }, { label: "Japanese", value: "ja" }, { label: "Chinese", value: "zh" }, { label: "Portuguese", value: "pt" }] },
{ name: "themes", description: "Available UI theme presets", allowCustom: true, required: false, options: [{ label: "Light", value: "light" }, { label: "Dark", value: "dark" }, { label: "System Default", value: "system" }, { label: "High Contrast", value: "high_contrast" }] },
{ name: "database_backends", description: "Supported DBAL database adapter backends", allowCustom: false, required: true, options: [{ label: "SQLite", value: "sqlite" }, { label: "PostgreSQL", value: "postgres" }, { label: "MySQL", value: "mysql" }, { label: "MariaDB", value: "mariadb" }, { label: "MongoDB", value: "mongodb" }, { label: "Redis", value: "redis" }, { label: "CockroachDB", value: "cockroachdb" }, { label: "SurrealDB", value: "surrealdb" }, { label: "Supabase", value: "supabase" }, { label: "In-Memory", value: "memory" }] }
]
function selectedDropdown() {
@@ -124,82 +35,51 @@ Rectangle {
}
function updateDropdown(index, updated) {
var copy = dropdowns.slice()
copy[index] = updated
dropdowns = copy
var copy = dropdowns.slice(); copy[index] = updated; dropdowns = copy
if (useLiveData) saveDropdown(index)
}
function updateSelectedField(field, value) {
if (selectedIndex < 0) return
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex]))
dd[field] = value
updateDropdown(selectedIndex, dd)
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])); dd[field] = value; updateDropdown(selectedIndex, dd)
}
function addOption() {
if (selectedIndex < 0) return
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex]))
dd.options.push({ label: "New Option", value: "new_option_" + dd.options.length })
updateDropdown(selectedIndex, dd)
dd.options.push({ label: "New Option", value: "new_option_" + dd.options.length }); updateDropdown(selectedIndex, dd)
}
function removeOption(optIndex) {
if (selectedIndex < 0) return
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex]))
dd.options.splice(optIndex, 1)
updateDropdown(selectedIndex, dd)
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])); dd.options.splice(optIndex, 1); updateDropdown(selectedIndex, dd)
}
function moveOption(optIndex, direction) {
if (selectedIndex < 0) return
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex]))
var targetIndex = optIndex + direction
if (targetIndex < 0 || targetIndex >= dd.options.length) return
var temp = dd.options[optIndex]
dd.options[optIndex] = dd.options[targetIndex]
dd.options[targetIndex] = temp
updateDropdown(selectedIndex, dd)
var t = optIndex + direction; if (t < 0 || t >= dd.options.length) return
var tmp = dd.options[optIndex]; dd.options[optIndex] = dd.options[t]; dd.options[t] = tmp; updateDropdown(selectedIndex, dd)
}
function updateOptionField(optIndex, field, value) {
if (selectedIndex < 0) return
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex]))
dd.options[optIndex][field] = value
updateDropdown(selectedIndex, dd)
var dd = JSON.parse(JSON.stringify(dropdowns[selectedIndex])); dd.options[optIndex][field] = value; updateDropdown(selectedIndex, dd)
}
function addDropdown() {
if (newDropdownName.trim() === "") return
var newDd = {
name: newDropdownName.trim().toLowerCase().replace(/ /g, "_"),
description: newDropdownDescription.trim() || "No description",
allowCustom: false,
required: false,
options: [{ label: "Option 1", value: "option_1" }]
}
if (useLiveData) {
dbal.execute("core/dropdown-configs/create", { data: newDd }, function(r, e) { if (!e) loadDropdowns() })
}
var copy = dropdowns.slice()
copy.push(newDd)
dropdowns = copy
selectedIndex = dropdowns.length - 1
newDropdownName = ""
newDropdownDescription = ""
addDialogOpen = false
var newDd = { name: newDropdownName.trim().toLowerCase().replace(/ /g, "_"), description: newDropdownDescription.trim() || "No description", allowCustom: false, required: false, options: [{ label: "Option 1", value: "option_1" }] }
if (useLiveData) dbal.execute("core/dropdown-configs/create", { data: newDd }, function(r, e) { if (!e) loadDropdowns() })
var copy = dropdowns.slice(); copy.push(newDd); dropdowns = copy
selectedIndex = dropdowns.length - 1; newDropdownName = ""; newDropdownDescription = ""; addDialogOpen = false
}
function deleteSelectedDropdown() {
if (selectedIndex < 0) return
if (useLiveData && dropdowns[selectedIndex].id) {
dbal.execute("core/dropdown-configs/delete", { id: dropdowns[selectedIndex].id }, function(r, e) { if (!e) loadDropdowns() })
}
var copy = dropdowns.slice()
copy.splice(selectedIndex, 1)
dropdowns = copy
selectedIndex = copy.length > 0 ? Math.min(selectedIndex, copy.length - 1) : -1
deleteDialogOpen = false
if (useLiveData && dropdowns[selectedIndex].id) dbal.execute("core/dropdown-configs/delete", { id: dropdowns[selectedIndex].id }, function(r, e) { if (!e) loadDropdowns() })
var copy = dropdowns.slice(); copy.splice(selectedIndex, 1); dropdowns = copy
selectedIndex = copy.length > 0 ? Math.min(selectedIndex, copy.length - 1) : -1; deleteDialogOpen = false
}
// ── DBAL Integration ─────────────────────────────────────────────
@@ -232,83 +112,31 @@ Rectangle {
anchors.margins: 20
spacing: 16
// Header
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h3"; text: "Dropdown Configuration Manager" }
Item { Layout.fillWidth: true }
CBadge { text: dropdowns.length + " dropdowns" }
}
CText {
variant: "body2"
text: "Configure dropdown/select field options used across all packages and entities."
color: Theme.text
opacity: 0.7
}
CText { variant: "body2"; text: "Configure dropdown/select field options used across all packages and entities."; color: Theme.text; opacity: 0.7 }
CDivider { Layout.fillWidth: true }
// Main content: sidebar + editor
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
// Left sidebar: dropdown list
CCard {
DropdownSidebar {
Layout.preferredWidth: 320
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "h4"; text: "Dropdowns" }
Item { Layout.fillWidth: true }
CButton {
text: "+ Add"
variant: "primary"
size: "sm"
onClicked: addDialogOpen = true
}
}
CDivider { Layout.fillWidth: true }
ListView {
id: dropdownList
Layout.fillWidth: true
Layout.fillHeight: true
model: dropdowns
spacing: 4
clip: true
delegate: CListItem {
width: dropdownList.width
title: modelData.name
subtitle: modelData.description
selected: index === selectedIndex
onClicked: selectedIndex = index
CBadge {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData.options.length + ""
}
}
}
}
dropdowns: root.dropdowns
selectedIndex: root.selectedIndex
onItemClicked: function(idx) { root.selectedIndex = idx }
onAddClicked: addDialogOpen = true
}
// Right panel: editor + preview
CCard {
Layout.fillWidth: true
Layout.fillHeight: true
@@ -318,396 +146,58 @@ Rectangle {
anchors.margins: 16
spacing: 12
// No selection state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: selectedIndex < 0
CText {
anchors.centerIn: parent
variant: "body1"
text: "Select a dropdown from the list to edit its configuration."
color: Theme.text
opacity: 0.5
}
Layout.fillWidth: true; Layout.fillHeight: true; visible: selectedIndex < 0
CText { anchors.centerIn: parent; variant: "body1"; text: "Select a dropdown from the list to edit its configuration."; color: Theme.text; opacity: 0.5 }
}
// Editor content (visible when a dropdown is selected)
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 12
visible: selectedIndex >= 0
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 12; visible: selectedIndex >= 0
// Editor header
FlexRow {
Layout.fillWidth: true
spacing: 12
CText {
variant: "h4"
text: selectedDropdown() ? selectedDropdown().name : ""
}
Layout.fillWidth: true; spacing: 12
CText { variant: "h4"; text: selectedDropdown() ? selectedDropdown().name : "" }
Item { Layout.fillWidth: true }
CButton {
text: "Delete"
variant: "danger"
size: "sm"
onClicked: deleteDialogOpen = true
}
CButton { text: "Delete"; variant: "danger"; size: "sm"; onClicked: deleteDialogOpen = true }
}
CDivider { Layout.fillWidth: true }
// Scrollable editor area
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: editorColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
Layout.fillWidth: true; Layout.fillHeight: true
contentHeight: editorColumn.implicitHeight; clip: true; boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: editorColumn
width: parent.width
spacing: 16
// Name and description fields
CText { variant: "body2"; text: "General"; font.bold: true }
RowLayout {
DropdownGeneralForm {
Layout.fillWidth: true
spacing: 12
CTextField {
label: "Name"
placeholderText: "dropdown_name"
text: selectedDropdown() ? selectedDropdown().name : ""
Layout.fillWidth: true
onTextChanged: {
if (selectedDropdown() && text !== selectedDropdown().name) {
updateSelectedField("name", text)
}
}
}
CTextField {
label: "Description"
placeholderText: "What is this dropdown for?"
text: selectedDropdown() ? selectedDropdown().description : ""
Layout.fillWidth: true
onTextChanged: {
if (selectedDropdown() && text !== selectedDropdown().description) {
updateSelectedField("description", text)
}
}
}
}
// Toggles
RowLayout {
Layout.fillWidth: true
spacing: 24
RowLayout {
spacing: 8
Switch {
id: allowCustomSwitch
checked: selectedDropdown() ? selectedDropdown().allowCustom : false
onCheckedChanged: {
if (selectedDropdown() && checked !== selectedDropdown().allowCustom) {
updateSelectedField("allowCustom", checked)
}
}
}
CText {
variant: "body2"
text: "Allow custom values"
}
}
RowLayout {
spacing: 8
Switch {
id: requiredSwitch
checked: selectedDropdown() ? selectedDropdown().required : false
onCheckedChanged: {
if (selectedDropdown() && checked !== selectedDropdown().required) {
updateSelectedField("required", checked)
}
}
}
CText {
variant: "body2"
text: "Required"
}
}
Item { Layout.fillWidth: true }
CBadge {
text: (selectedDropdown() && selectedDropdown().required ? "Required" : "Optional")
accent: selectedDropdown() ? selectedDropdown().required : false
}
CBadge {
text: (selectedDropdown() && selectedDropdown().allowCustom ? "Custom allowed" : "Fixed options")
visible: true
}
dropdown: selectedDropdown()
onFieldChanged: function(field, value) { updateSelectedField(field, value) }
}
CDivider { Layout.fillWidth: true }
// Options header
FlexRow {
DropdownOptionsEditor {
Layout.fillWidth: true
spacing: 8
CText { variant: "body2"; text: "Options"; font.bold: true }
CBadge { text: selectedDropdown() ? selectedDropdown().options.length + " items" : "0 items" }
Item { Layout.fillWidth: true }
CButton {
text: "+ Add Option"
variant: "primary"
size: "sm"
onClicked: addOption()
}
}
// Options list
Repeater {
id: optionsRepeater
model: selectedDropdown() ? selectedDropdown().options : []
CPaper {
Layout.fillWidth: true
implicitHeight: optionRow.implicitHeight + 24
RowLayout {
id: optionRow
anchors.fill: parent
anchors.margins: 12
spacing: 8
// Index indicator
CText {
variant: "caption"
text: (index + 1) + "."
Layout.preferredWidth: 24
color: Theme.text
opacity: 0.5
}
CTextField {
label: "Label"
placeholderText: "Display label"
text: modelData.label
Layout.fillWidth: true
onTextChanged: {
if (text !== modelData.label) {
updateOptionField(index, "label", text)
}
}
}
CTextField {
label: "Value"
placeholderText: "stored_value"
text: modelData.value
Layout.fillWidth: true
onTextChanged: {
if (text !== modelData.value) {
updateOptionField(index, "value", text)
}
}
}
// Reorder buttons
ColumnLayout {
spacing: 2
CButton {
text: "\u25B2"
variant: "ghost"
size: "sm"
enabled: index > 0
onClicked: moveOption(index, -1)
}
CButton {
text: "\u25BC"
variant: "ghost"
size: "sm"
enabled: selectedDropdown() ? index < selectedDropdown().options.length - 1 : false
onClicked: moveOption(index, 1)
}
}
CButton {
text: "X"
variant: "danger"
size: "sm"
onClicked: removeOption(index)
}
}
}
dropdown: selectedDropdown()
onAddOptionClicked: addOption()
onRemoveOptionClicked: function(idx) { removeOption(idx) }
onMoveOptionClicked: function(idx, dir) { moveOption(idx, dir) }
onOptionFieldChanged: function(idx, field, val) { updateOptionField(idx, field, val) }
}
CDivider { Layout.fillWidth: true }
// Preview section
CText { variant: "body2"; text: "Preview"; font.bold: true }
CPaper {
DropdownPreview {
Layout.fillWidth: true
implicitHeight: previewColumn.implicitHeight + 32
ColumnLayout {
id: previewColumn
anchors.fill: parent
anchors.margins: 16
spacing: 12
CText {
variant: "caption"
text: "This is how the dropdown will render in forms:"
color: Theme.text
opacity: 0.6
}
// Simulated dropdown label
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CText {
variant: "body2"
text: (selectedDropdown() ? selectedDropdown().name : "") + (selectedDropdown() && selectedDropdown().required ? " *" : "")
font.bold: true
}
// Simulated select box
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
color: Theme.surface
border.color: Theme.border
border.width: 1
radius: 4
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
CText {
variant: "body1"
text: selectedDropdown() && selectedDropdown().options.length > 0
? selectedDropdown().options[0].label
: "No options"
Layout.fillWidth: true
color: Theme.text
}
CText {
variant: "body2"
text: "\u25BE"
color: Theme.text
opacity: 0.5
}
}
}
CText {
variant: "caption"
text: selectedDropdown() ? selectedDropdown().description : ""
color: Theme.text
opacity: 0.5
}
}
CDivider { Layout.fillWidth: true }
// Simulated expanded dropdown list
CText {
variant: "caption"
text: "Expanded view:"
color: Theme.text
opacity: 0.6
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: previewOptionsList.implicitHeight + 8
color: Theme.surface
border.color: Theme.border
border.width: 1
radius: 4
ColumnLayout {
id: previewOptionsList
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 4
spacing: 0
Repeater {
model: selectedDropdown() ? selectedDropdown().options : []
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
color: previewOptionMouse.containsMouse ? Theme.primary : "transparent"
opacity: previewOptionMouse.containsMouse ? 0.12 : 1.0
radius: 2
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
CText {
variant: "body1"
text: modelData.label
Layout.fillWidth: true
}
CText {
variant: "caption"
text: modelData.value
color: Theme.text
opacity: 0.4
}
}
MouseArea {
id: previewOptionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
// Metadata summary
FlexRow {
Layout.fillWidth: true
spacing: 8
CBadge { text: selectedDropdown() ? selectedDropdown().options.length + " options" : "0 options" }
CBadge {
text: selectedDropdown() && selectedDropdown().required ? "Required" : "Optional"
accent: selectedDropdown() ? selectedDropdown().required : false
}
CBadge {
text: selectedDropdown() && selectedDropdown().allowCustom ? "Custom values OK" : "Fixed options only"
}
}
}
dropdown: selectedDropdown()
}
// Bottom spacer
Item { Layout.preferredHeight: 16 }
}
}
@@ -719,102 +209,32 @@ Rectangle {
// Add Dropdown Dialog
CDialog {
id: addDialog
visible: addDialogOpen
title: "Add New Dropdown"
visible: addDialogOpen; title: "Add New Dropdown"
ColumnLayout {
spacing: 16
width: 400
CText {
variant: "body2"
text: "Create a new dropdown configuration. The name will be normalized to snake_case."
}
CTextField {
label: "Dropdown Name"
placeholderText: "e.g. ticket_types"
text: newDropdownName
Layout.fillWidth: true
onTextChanged: newDropdownName = text
}
CTextField {
label: "Description"
placeholderText: "What will this dropdown be used for?"
text: newDropdownDescription
Layout.fillWidth: true
onTextChanged: newDropdownDescription = text
}
CAlert {
severity: "info"
text: "A default option will be added. You can configure options after creation."
Layout.fillWidth: true
}
spacing: 16; width: 400
CText { variant: "body2"; text: "Create a new dropdown configuration. The name will be normalized to snake_case." }
CTextField { label: "Dropdown Name"; placeholderText: "e.g. ticket_types"; text: newDropdownName; Layout.fillWidth: true; onTextChanged: newDropdownName = text }
CTextField { label: "Description"; placeholderText: "What will this dropdown be used for?"; text: newDropdownDescription; Layout.fillWidth: true; onTextChanged: newDropdownDescription = text }
CAlert { severity: "info"; text: "A default option will be added. You can configure options after creation."; Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
onClicked: {
addDialogOpen = false
newDropdownName = ""
newDropdownDescription = ""
}
}
CButton {
text: "Create"
variant: "primary"
enabled: newDropdownName.trim() !== ""
onClicked: addDropdown()
}
Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true }
CButton { text: "Cancel"; variant: "ghost"; onClicked: { addDialogOpen = false; newDropdownName = ""; newDropdownDescription = "" } }
CButton { text: "Create"; variant: "primary"; enabled: newDropdownName.trim() !== ""; onClicked: addDropdown() }
}
}
}
// Delete Confirmation Dialog
CDialog {
id: deleteDialog
visible: deleteDialogOpen
title: "Delete Dropdown"
visible: deleteDialogOpen; title: "Delete Dropdown"
ColumnLayout {
spacing: 16
width: 400
CAlert {
severity: "warning"
text: "This action cannot be undone. Any forms referencing this dropdown will need to be updated."
Layout.fillWidth: true
}
CText {
variant: "body1"
text: selectedDropdown()
? "Are you sure you want to delete \"" + selectedDropdown().name + "\" with " + selectedDropdown().options.length + " options?"
: ""
wrapMode: Text.WordWrap
}
spacing: 16; width: 400
CAlert { severity: "warning"; text: "This action cannot be undone. Any forms referencing this dropdown will need to be updated."; Layout.fillWidth: true }
CText { variant: "body1"; text: selectedDropdown() ? "Are you sure you want to delete \"" + selectedDropdown().name + "\" with " + selectedDropdown().options.length + " options?" : ""; wrapMode: Text.WordWrap }
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
onClicked: deleteDialogOpen = false
}
CButton {
text: "Delete"
variant: "danger"
onClicked: deleteSelectedDropdown()
}
Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true }
CButton { text: "Cancel"; variant: "ghost"; onClicked: deleteDialogOpen = false }
CButton { text: "Delete"; variant: "danger"; onClicked: deleteSelectedDropdown() }
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
import "qmllib/MetaBuilder"
Rectangle {
id: root
@@ -16,26 +17,9 @@ Rectangle {
property bool addDialogVisible: false
property bool deleteDialogVisible: false
property string newPath: ""
property string newTitle: ""
property int newLevel: 1
property string newLayout: "default"
property var layoutOptions: ["default", "sidebar", "dashboard", "blank"]
property var levelOptions: [1, 2, 3, 4, 5]
property var mockRoutes: [
{ path: "/", title: "Home", level: 1, layout: "default", enabled: true, permissions: "public" },
{ path: "/dashboard", title: "Dashboard", level: 1, layout: "dashboard", enabled: true, permissions: "authenticated" },
{ path: "/admin", title: "Admin Panel", level: 3, layout: "sidebar", enabled: true, permissions: "role:admin" },
{ path: "/forum", title: "Forum", level: 1, layout: "sidebar", enabled: true, permissions: "authenticated" },
{ path: "/gallery", title: "Gallery", level: 1, layout: "default", enabled: true, permissions: "public" },
{ path: "/profile", title: "Profile", level: 1, layout: "sidebar", enabled: true, permissions: "authenticated" },
{ path: "/settings", title: "Settings", level: 2, layout: "sidebar", enabled: true, permissions: "authenticated" },
{ path: "/god-panel", title: "God Panel", level: 4, layout: "dashboard", enabled: true, permissions: "role:god" },
{ path: "/supergod", title: "Super God", level: 5, layout: "blank", enabled: false, permissions: "role:supergod" }
]
property var routes: [
{ path: "/", title: "Home", level: 1, layout: "default", enabled: true, permissions: "public" },
{ path: "/dashboard", title: "Dashboard", level: 1, layout: "dashboard", enabled: true, permissions: "authenticated" },
@@ -54,551 +38,141 @@ Rectangle {
route[field] = value
updated[index] = route
routes = updated
if (selectedIndex === index) {
selectedIndex = -1
selectedIndex = index
}
if (selectedIndex === index) { selectedIndex = -1; selectedIndex = index }
if (useLiveData) saveRoute(index)
}
function addRoute() {
if (newPath.length === 0 || newTitle.length === 0) return
var newRoute = {
path: newPath,
title: newTitle,
level: newLevel,
layout: newLayout,
enabled: true,
permissions: "authenticated"
}
function addRoute(path, title, level, layout) {
var newRoute = { path: path, title: title, level: level, layout: layout, enabled: true, permissions: "authenticated" }
if (useLiveData) {
dbal.create("ui_page", newRoute, function(result, error) {
if (!error) loadRoutes()
})
dbal.create("ui_page", newRoute, function(result, error) { if (!error) loadRoutes() })
} else {
var updated = routes.slice()
updated.push(newRoute)
routes = updated
var updated = routes.slice(); updated.push(newRoute); routes = updated
}
newPath = ""
newTitle = ""
newLevel = 1
newLayout = "default"
addDialogVisible = false
}
function deleteRoute() {
if (selectedIndex < 0 || selectedIndex >= routes.length) return
if (useLiveData && routes[selectedIndex].id) {
dbal.remove("ui_page", routes[selectedIndex].id, function(result, error) {
if (!error) loadRoutes()
})
dbal.remove("ui_page", routes[selectedIndex].id, function(result, error) { if (!error) loadRoutes() })
} else {
var updated = routes.slice()
updated.splice(selectedIndex, 1)
routes = updated
var updated = routes.slice(); updated.splice(selectedIndex, 1); routes = updated
}
selectedIndex = -1
deleteDialogVisible = false
selectedIndex = -1; deleteDialogVisible = false
}
function moveRoute(fromIndex, direction) {
var toIndex = fromIndex + direction
if (toIndex < 0 || toIndex >= routes.length) return
var updated = routes.slice()
var temp = updated[fromIndex]
updated[fromIndex] = updated[toIndex]
updated[toIndex] = temp
routes = updated
selectedIndex = toIndex
}
function levelColor(level) {
if (level <= 1) return "#4caf50"
if (level === 2) return "#8bc34a"
if (level === 3) return "#ff9800"
if (level === 4) return "#f44336"
return "#9c27b0"
var temp = updated[fromIndex]; updated[fromIndex] = updated[toIndex]; updated[toIndex] = temp
routes = updated; selectedIndex = toIndex
}
// ── DBAL Integration ─────────────────────────────────────────────
function loadRoutes() {
dbal.list("ui_page", { take: 100 }, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var r = result.items[i]
parsed.push({
id: r.id || undefined,
path: r.path || r.route || "",
title: r.title || r.name || "",
level: r.level || 1,
layout: r.layout || "default",
enabled: r.enabled !== undefined ? r.enabled : true,
permissions: r.permissions || "public"
})
parsed.push({ id: r.id || undefined, path: r.path || r.route || "", title: r.title || r.name || "",
level: r.level || 1, layout: r.layout || "default", enabled: r.enabled !== undefined ? r.enabled : true, permissions: r.permissions || "public" })
}
routes = parsed
}
// On error or empty result, keep existing mock routes as fallback
})
}
onUseLiveDataChanged: {
if (useLiveData) loadRoutes()
}
Component.onCompleted: {
loadRoutes()
}
// ── CRUD wiring ──────────────────────────────────────────────────
function saveRoute(index) {
if (!useLiveData) return
var route = routes[index]
var data = { path: route.path, title: route.title, level: route.level, layout: route.layout, enabled: route.enabled, permissions: route.permissions }
if (route.id) {
dbal.update("ui_page", route.id, data, function(result, error) {
if (!error) loadRoutes()
})
} else {
dbal.create("ui_page", data, function(result, error) {
if (!error) loadRoutes()
})
}
if (route.id) dbal.update("ui_page", route.id, data, function(r, e) { if (!e) loadRoutes() })
else dbal.create("ui_page", data, function(r, e) { if (!e) loadRoutes() })
}
onUseLiveDataChanged: { if (useLiveData) loadRoutes() }
Component.onCompleted: { loadRoutes() }
// ── UI ───────────────────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
anchors.fill: parent; anchors.margins: 20; spacing: 16
FlexRow {
Layout.fillWidth: true
spacing: 12
Layout.fillWidth: true; spacing: 12
CText { variant: "h3"; text: "Page Routes Manager" }
Item { Layout.fillWidth: true }
CBadge { text: routes.length + " routes" }
CButton {
text: "Add Route"
variant: "primary"
size: "sm"
onClicked: addDialogVisible = true
}
CButton { text: "Add Route"; variant: "primary"; size: "sm"; onClicked: addDialogVisible = true }
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16
CCard {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 580
Layout.fillWidth: true; Layout.fillHeight: true; Layout.preferredWidth: 580
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 0
Rectangle {
Layout.fillWidth: true
height: 40
color: Theme.surface
radius: 4
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 0
CText {
variant: "caption"
text: "ORDER"
Layout.preferredWidth: 60
}
CText {
variant: "caption"
text: "PATH"
Layout.preferredWidth: 120
}
CText {
variant: "caption"
text: "TITLE"
Layout.preferredWidth: 120
}
CText {
variant: "caption"
text: "LEVEL"
Layout.preferredWidth: 60
}
CText {
variant: "caption"
text: "LAYOUT"
Layout.preferredWidth: 100
}
CText {
variant: "caption"
text: "STATUS"
Layout.fillWidth: true
}
}
}
anchors.fill: parent; anchors.margins: 16; spacing: 0
CRouteTableHeader { }
CDivider { Layout.fillWidth: true }
ListView {
id: routeList
Layout.fillWidth: true
Layout.fillHeight: true
model: routes
clip: true
spacing: 1
Layout.fillWidth: true; Layout.fillHeight: true
model: routes; clip: true; spacing: 1
delegate: Rectangle {
width: routeList.width
height: 48
color: index === selectedIndex ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : (routeHover.hovered ? Theme.surface : "transparent")
radius: 4
HoverHandler { id: routeHover }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: selectedIndex = (selectedIndex === index ? -1 : index)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 0
RowLayout {
Layout.preferredWidth: 60
spacing: 2
CButton {
text: "\u25B2"
variant: "ghost"
size: "sm"
enabled: index > 0
onClicked: moveRoute(index, -1)
}
CButton {
text: "\u25BC"
variant: "ghost"
size: "sm"
enabled: index < routes.length - 1
onClicked: moveRoute(index, 1)
}
}
CText {
variant: "body2"
text: modelData.path
Layout.preferredWidth: 120
color: Theme.primary
}
CText {
variant: "body2"
text: modelData.title
Layout.preferredWidth: 120
}
Rectangle {
Layout.preferredWidth: 60
height: 24
width: 32
radius: 12
color: levelColor(modelData.level)
CText {
anchors.centerIn: parent
variant: "caption"
text: modelData.level.toString()
color: "#ffffff"
}
}
CChip {
text: modelData.layout
Layout.preferredWidth: 100
}
Rectangle {
Layout.fillWidth: true
height: 10
width: 10
radius: 5
color: modelData.enabled ? "#4caf50" : "#9e9e9e"
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 10
Layout.preferredHeight: 10
}
}
delegate: CRouteTableRow {
routeData: modelData
isSelected: index === selectedIndex
routeIndex: index
routeCount: routes.length
onClicked: selectedIndex = (selectedIndex === index ? -1 : index)
onMoveUp: moveRoute(index, -1)
onMoveDown: moveRoute(index, 1)
}
}
}
}
CCard {
Layout.preferredWidth: 340
Layout.fillHeight: true
CRouteEditPanel {
Layout.preferredWidth: 340; Layout.fillHeight: true
visible: selectedIndex >= 0 && selectedIndex < routes.length
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 14
CText { variant: "h4"; text: "Edit Route" }
CDivider { Layout.fillWidth: true }
CText { variant: "caption"; text: "Path" }
CTextField {
Layout.fillWidth: true
text: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].path : ""
onTextChanged: {
if (selectedIndex >= 0 && selectedIndex < routes.length && text !== routes[selectedIndex].path) {
updateRoute(selectedIndex, "path", text)
}
}
}
CText { variant: "caption"; text: "Page Title" }
CTextField {
Layout.fillWidth: true
text: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].title : ""
onTextChanged: {
if (selectedIndex >= 0 && selectedIndex < routes.length && text !== routes[selectedIndex].title) {
updateRoute(selectedIndex, "title", text)
}
}
}
CText { variant: "caption"; text: "Required Level (1-5)" }
CSelect {
Layout.fillWidth: true
model: levelOptions
currentIndex: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].level - 1 : 0
onCurrentIndexChanged: {
if (selectedIndex >= 0 && selectedIndex < routes.length) {
var newLvl = currentIndex + 1
if (newLvl !== routes[selectedIndex].level) {
updateRoute(selectedIndex, "level", newLvl)
}
}
}
}
CText { variant: "caption"; text: "Layout Type" }
CSelect {
Layout.fillWidth: true
model: layoutOptions
currentIndex: {
if (selectedIndex >= 0 && selectedIndex < routes.length) {
return layoutOptions.indexOf(routes[selectedIndex].layout)
}
return 0
}
onCurrentIndexChanged: {
if (selectedIndex >= 0 && selectedIndex < routes.length) {
var newLayoutVal = layoutOptions[currentIndex]
if (newLayoutVal !== routes[selectedIndex].layout) {
updateRoute(selectedIndex, "layout", newLayoutVal)
}
}
}
}
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "body2"; text: "Enabled" }
Item { Layout.fillWidth: true }
CSwitch {
checked: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].enabled : false
onCheckedChanged: {
if (selectedIndex >= 0 && selectedIndex < routes.length && checked !== routes[selectedIndex].enabled) {
updateRoute(selectedIndex, "enabled", checked)
}
}
}
}
CDivider { Layout.fillWidth: true }
CText { variant: "caption"; text: "Permission Rules" }
CTextField {
Layout.fillWidth: true
text: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].permissions : ""
onTextChanged: {
if (selectedIndex >= 0 && selectedIndex < routes.length && text !== routes[selectedIndex].permissions) {
updateRoute(selectedIndex, "permissions", text)
}
}
}
CAlert {
Layout.fillWidth: true
severity: selectedIndex >= 0 && selectedIndex < routes.length && routes[selectedIndex].level >= 4 ? "warning" : "info"
text: selectedIndex >= 0 && selectedIndex < routes.length && routes[selectedIndex].level >= 4
? "High privilege route (level " + routes[selectedIndex].level + ")"
: "Standard access route"
}
Item { Layout.fillHeight: true }
CButton {
text: "Delete Route"
variant: "danger"
size: "sm"
Layout.fillWidth: true
onClicked: deleteDialogVisible = true
}
}
route: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex] : null
layoutOptions: root.layoutOptions; levelOptions: root.levelOptions
onFieldChanged: function(field, value) { updateRoute(selectedIndex, field, value) }
onDeleteRequested: deleteDialogVisible = true
}
CCard {
Layout.preferredWidth: 340
Layout.fillHeight: true
Layout.preferredWidth: 340; Layout.fillHeight: true
visible: selectedIndex < 0 || selectedIndex >= routes.length
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
anchors.fill: parent; anchors.margins: 16; spacing: 12
Item { Layout.fillHeight: true }
CText {
variant: "body2"
text: "Select a route from the table to edit its configuration."
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
CText { variant: "body2"; text: "Select a route from the table to edit its configuration."; Layout.fillWidth: true; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap }
Item { Layout.fillHeight: true }
}
}
}
}
CDialog {
CAddRouteDialog {
id: addDialog
visible: addDialogVisible
title: "Add New Route"
ColumnLayout {
spacing: 12
width: 320
CText { variant: "caption"; text: "Path" }
CTextField {
Layout.fillWidth: true
placeholderText: "/new-page"
text: newPath
onTextChanged: newPath = text
}
CText { variant: "caption"; text: "Page Title" }
CTextField {
Layout.fillWidth: true
placeholderText: "New Page"
text: newTitle
onTextChanged: newTitle = text
}
CText { variant: "caption"; text: "Required Level" }
CSelect {
Layout.fillWidth: true
model: levelOptions
currentIndex: newLevel - 1
onCurrentIndexChanged: newLevel = currentIndex + 1
}
CText { variant: "caption"; text: "Layout Type" }
CSelect {
Layout.fillWidth: true
model: layoutOptions
currentIndex: layoutOptions.indexOf(newLayout)
onCurrentIndexChanged: newLayout = layoutOptions[currentIndex]
}
FlexRow {
Layout.fillWidth: true
spacing: 12
CButton {
text: "Cancel"
variant: "ghost"
onClicked: {
addDialogVisible = false
newPath = ""
newTitle = ""
newLevel = 1
newLayout = "default"
}
}
Item { Layout.fillWidth: true }
CButton {
text: "Add Route"
variant: "primary"
enabled: newPath.length > 0 && newTitle.length > 0
onClicked: addRoute()
}
}
}
layoutOptions: root.layoutOptions; levelOptions: root.levelOptions
onAddRoute: function(path, title, level, layout) { root.addRoute(path, title, level, layout) }
}
CDialog {
CDeleteConfirmDialog {
id: deleteDialog
visible: deleteDialogVisible
title: "Delete Route"
ColumnLayout {
spacing: 16
width: 320
CAlert {
Layout.fillWidth: true
severity: "warning"
text: selectedIndex >= 0 && selectedIndex < routes.length
? "Are you sure you want to delete \"" + routes[selectedIndex].path + "\"?"
: "No route selected."
}
CText {
variant: "body2"
text: "This action cannot be undone. The route will be permanently removed from the configuration."
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
FlexRow {
Layout.fillWidth: true
spacing: 12
CButton {
text: "Cancel"
variant: "ghost"
onClicked: deleteDialogVisible = false
}
Item { Layout.fillWidth: true }
CButton {
text: "Delete"
variant: "danger"
onClicked: deleteRoute()
}
}
}
itemName: selectedIndex >= 0 && selectedIndex < routes.length ? routes[selectedIndex].path : ""
description: "This action cannot be undone. The route will be permanently removed from the configuration."
onConfirmed: deleteRoute()
}
}

View File

@@ -3,15 +3,14 @@ import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
import "qmllib/MetaBuilder"
Rectangle {
id: root
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── Local state ──────────────────────────────────────────────────
@@ -26,757 +25,178 @@ Rectangle {
property string activeRoleFilter: "all"
property int nextUid: 5
// Dialog state
property bool createDialogOpen: false
property bool editDialogOpen: false
property bool deleteDialogOpen: false
property int editIndex: -1
property int deleteIndex: -1
// Form fields
property string formUsername: ""
property string formEmail: ""
property string formPassword: ""
property string formRole: "user"
property bool formActive: true
readonly property var roles: ["user", "admin", "god", "supergod"]
property var mockUsers: JSON.parse(JSON.stringify(users))
// ── DBAL Integration ─────────────────────────────────────────────
function loadUsers() {
dbal.list("user", { take: 50 }, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var u = result.items[i]
parsed.push({
uid: u.id || u.uid || (i + 1),
username: u.username || "",
email: u.email || "",
role: u.role || "user",
level: levelForRole(u.role || "user"),
status: u.status || "active",
created: u.createdAt ? u.createdAt.slice(0, 10) : (u.created || "")
})
parsed.push({ uid: u.id || u.uid || (i + 1), username: u.username || "", email: u.email || "", role: u.role || "user", level: levelForRole(u.role || "user"), status: u.status || "active", created: u.createdAt ? u.createdAt.slice(0, 10) : (u.created || "") })
}
users = parsed
nextUid = parsed.length + 1
users = parsed; nextUid = parsed.length + 1
}
// On error or empty result, keep existing mock users as fallback
})
}
onUseLiveDataChanged: {
if (useLiveData) loadUsers()
}
Component.onCompleted: {
loadUsers()
}
onUseLiveDataChanged: { if (useLiveData) loadUsers() }
Component.onCompleted: { loadUsers() }
// ── Helpers ──────────────────────────────────────────────────────
function initials(name) {
var parts = name.split("")
return (parts[0] || "").toUpperCase() + (parts[1] || "").toUpperCase()
function levelForRole(role) {
if (role === "user") return 2; if (role === "admin") return 3
if (role === "god") return 4; if (role === "supergod") return 5; return 1
}
function roleColor(role) {
if (role === "supergod") return "#e040fb"
if (role === "god") return "#ff9800"
if (role === "admin") return "#2196f3"
return "#4caf50"
}
function statusString(s) { return s === "active" ? "success" : "warning" }
function countByRole(role) {
return users.filter(function(u) { return u.role === role }).length
}
function countByRole(role) { return users.filter(function(u) { return u.role === role }).length }
function filteredUsers() {
var q = searchText.toLowerCase()
return users.filter(function(u) {
var matchesRole = activeRoleFilter === "all" || u.role === activeRoleFilter
var matchesSearch = q === "" ||
u.username.toLowerCase().indexOf(q) !== -1 ||
u.email.toLowerCase().indexOf(q) !== -1 ||
u.role.toLowerCase().indexOf(q) !== -1
var matchesSearch = q === "" || u.username.toLowerCase().indexOf(q) !== -1 || u.email.toLowerCase().indexOf(q) !== -1 || u.role.toLowerCase().indexOf(q) !== -1
return matchesRole && matchesSearch
})
}
function clearForm() {
formUsername = ""
formEmail = ""
formPassword = ""
formRole = "user"
formActive = true
function findUserIndex(uid) {
for (var i = 0; i < users.length; i++) { if (users[i].uid === uid) return i }
return -1
}
function openCreateDialog() {
clearForm()
createDialogOpen = true
}
function openEditDialog(idx) {
var u = users[idx]
formUsername = u.username
formEmail = u.email
formPassword = ""
formRole = u.role
formActive = u.status === "active"
editIndex = idx
editDialogOpen = true
}
function openDeleteDialog(idx) {
deleteIndex = idx
deleteDialogOpen = true
}
function levelForRole(role) {
if (role === "user") return 2
if (role === "admin") return 3
if (role === "god") return 4
if (role === "supergod") return 5
return 1
}
function createUser() {
if (formUsername === "" || formEmail === "") return
var userData = {
username: formUsername,
email: formEmail,
role: formRole,
status: formActive ? "active" : "inactive"
}
function createUser(userData) {
if (useLiveData) {
dbal.create("user", userData, function(result, error) {
if (!error) {
loadUsers()
} else {
createUserLocally(userData)
}
if (!error) loadUsers(); else createUserLocally(userData)
createDialogOpen = false
clearForm()
})
} else {
createUserLocally(userData)
createDialogOpen = false
clearForm()
}
} else { createUserLocally(userData); createDialogOpen = false }
}
function createUserLocally(userData) {
var newUser = {
uid: nextUid,
username: userData.username,
email: userData.email,
role: userData.role,
level: levelForRole(userData.role),
status: userData.status,
created: new Date().toISOString().slice(0, 10)
}
var copy = users.slice()
copy.push(newUser)
users = copy
nextUid++
copy.push({ uid: nextUid, username: userData.username, email: userData.email, role: userData.role, level: levelForRole(userData.role), status: userData.status, created: new Date().toISOString().slice(0, 10) })
users = copy; nextUid++
}
function saveEdit() {
function saveEdit(userData) {
if (editIndex < 0) return
var userData = {
username: formUsername,
email: formEmail,
role: formRole,
status: formActive ? "active" : "inactive"
}
if (useLiveData) {
var userId = users[editIndex].uid
dbal.update("user", userId, userData, function(result, error) {
if (!error) {
loadUsers()
} else {
saveEditLocally(userData)
}
dbal.update("user", users[editIndex].uid, userData, function(result, error) {
if (!error) loadUsers(); else saveEditLocally(userData)
editDialogOpen = false
clearForm()
})
} else {
saveEditLocally(userData)
editDialogOpen = false
clearForm()
}
} else { saveEditLocally(userData); editDialogOpen = false }
}
function saveEditLocally(userData) {
var copy = users.slice()
copy[editIndex] = Object.assign({}, copy[editIndex], {
username: userData.username,
email: userData.email,
role: userData.role,
level: levelForRole(userData.role),
status: userData.status
})
copy[editIndex] = Object.assign({}, copy[editIndex], { username: userData.username, email: userData.email, role: userData.role, level: levelForRole(userData.role), status: userData.status })
users = copy
}
function confirmDelete() {
if (deleteIndex < 0) return
if (useLiveData) {
var userId = users[deleteIndex].uid
dbal.remove("user", userId, function(result, error) {
if (!error) {
loadUsers()
} else {
confirmDeleteLocally()
}
deleteDialogOpen = false
deleteIndex = -1
dbal.remove("user", users[deleteIndex].uid, function(result, error) {
if (!error) loadUsers(); else { var c = users.slice(); c.splice(deleteIndex, 1); users = c }
deleteDialogOpen = false; deleteIndex = -1
})
} else {
confirmDeleteLocally()
deleteDialogOpen = false
deleteIndex = -1
var c = users.slice(); c.splice(deleteIndex, 1); users = c
deleteDialogOpen = false; deleteIndex = -1
}
}
function confirmDeleteLocally() {
var copy = users.slice()
copy.splice(deleteIndex, 1)
users = copy
}
// ── Main layout ──────────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
// Header
FlexRow {
Layout.fillWidth: true
spacing: 12
Layout.fillWidth: true; spacing: 12
CText { variant: "h3"; text: "User Management" }
Item { Layout.fillWidth: true }
CButton {
text: "Create User"
variant: "primary"
onClicked: openCreateDialog()
}
CButton { text: "Create User"; variant: "primary"; onClicked: createDialogOpen = true }
}
// ── Stats bar ────────────────────────────────────────────────
FlexRow {
UserStatsBar {
Layout.fillWidth: true
spacing: 12
CCard {
Layout.fillWidth: true
implicitHeight: statCol1.implicitHeight + 32
ColumnLayout {
id: statCol1
anchors.fill: parent
anchors.margins: 16
spacing: 4
CText { variant: "caption"; text: "Total Users" }
CText { variant: "h4"; text: String(users.length) }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: statCol2.implicitHeight + 32
ColumnLayout {
id: statCol2
anchors.fill: parent
anchors.margins: 16
spacing: 4
CText { variant: "caption"; text: "Admins" }
CText { variant: "h4"; text: String(countByRole("admin")) }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: statCol3.implicitHeight + 32
ColumnLayout {
id: statCol3
anchors.fill: parent
anchors.margins: 16
spacing: 4
CText { variant: "caption"; text: "Gods" }
CText { variant: "h4"; text: String(countByRole("god")) }
}
}
CCard {
Layout.fillWidth: true
implicitHeight: statCol4.implicitHeight + 32
ColumnLayout {
id: statCol4
anchors.fill: parent
anchors.margins: 16
spacing: 4
CText { variant: "caption"; text: "SuperGods" }
CText { variant: "h4"; text: String(countByRole("supergod")) }
}
}
totalUsers: users.length
adminCount: countByRole("admin")
godCount: countByRole("god")
superGodCount: countByRole("supergod")
}
// ── Search & filter ──────────────────────────────────────────
CCard {
UserSearchFilter {
Layout.fillWidth: true
implicitHeight: filterCol.implicitHeight + 32
ColumnLayout {
id: filterCol
anchors.fill: parent
anchors.margins: 16
spacing: 12
CTextField {
Layout.fillWidth: true
label: "Search"
placeholderText: "Filter by username, email, or role..."
text: searchText
onTextChanged: searchText = text
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CChip {
text: "All"
selected: activeRoleFilter === "all"
onClicked: activeRoleFilter = "all"
}
CChip {
text: "User"
selected: activeRoleFilter === "user"
onClicked: activeRoleFilter = "user"
}
CChip {
text: "Admin"
selected: activeRoleFilter === "admin"
onClicked: activeRoleFilter = "admin"
}
CChip {
text: "God"
selected: activeRoleFilter === "god"
onClicked: activeRoleFilter = "god"
}
CChip {
text: "SuperGod"
selected: activeRoleFilter === "supergod"
onClicked: activeRoleFilter = "supergod"
}
}
}
searchText: root.searchText
activeRoleFilter: root.activeRoleFilter
onSearchChanged: function(text) { root.searchText = text }
onRoleFilterChanged: function(role) { root.activeRoleFilter = role }
}
// ── User table ──────────────────────────────────────────────
CCard {
UserTable {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 0
// Table header
Rectangle {
Layout.fillWidth: true
height: 40
color: Theme.surface
radius: 4
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
CText { variant: "caption"; text: "AVATAR"; Layout.preferredWidth: 56 }
CText { variant: "caption"; text: "USERNAME"; Layout.preferredWidth: 120 }
CText { variant: "caption"; text: "EMAIL"; Layout.fillWidth: true }
CText { variant: "caption"; text: "ROLE"; Layout.preferredWidth: 100 }
CText { variant: "caption"; text: "LEVEL"; Layout.preferredWidth: 50 }
CText { variant: "caption"; text: "STATUS"; Layout.preferredWidth: 90 }
CText { variant: "caption"; text: "CREATED"; Layout.preferredWidth: 100 }
CText { variant: "caption"; text: "ACTIONS"; Layout.preferredWidth: 140 }
}
}
CDivider { Layout.fillWidth: true }
// Table body
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: filteredUsers()
clip: true
spacing: 0
delegate: Rectangle {
width: parent ? parent.width : 600
height: 56
color: index % 2 === 0 ? "transparent" : Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.3)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 8
// Avatar
Item {
Layout.preferredWidth: 56
Layout.preferredHeight: 40
CAvatar {
anchors.centerIn: parent
initials: modelData.username.substring(0, 2).toUpperCase()
}
}
// Username
CText {
variant: "body1"
text: modelData.username
Layout.preferredWidth: 120
elide: Text.ElideRight
}
// Email
CText {
variant: "body2"
text: modelData.email
Layout.fillWidth: true
elide: Text.ElideRight
}
// Role badge
Item {
Layout.preferredWidth: 100
Layout.preferredHeight: 40
CBadge {
anchors.verticalCenter: parent.verticalCenter
text: modelData.role
color: roleColor(modelData.role)
}
}
// Level
CText {
variant: "body2"
text: "L" + modelData.level
Layout.preferredWidth: 50
}
// Status
Item {
Layout.preferredWidth: 90
Layout.preferredHeight: 40
CStatusBadge {
anchors.verticalCenter: parent.verticalCenter
status: statusString(modelData.status)
text: modelData.status
}
}
// Created
CText {
variant: "caption"
text: modelData.created
Layout.preferredWidth: 100
}
// Actions
FlexRow {
Layout.preferredWidth: 140
spacing: 6
CButton {
text: "Edit"
variant: "ghost"
size: "sm"
onClicked: {
// Find the real index in the users array
for (var i = 0; i < users.length; i++) {
if (users[i].uid === modelData.uid) {
openEditDialog(i)
break
}
}
}
}
CButton {
text: "Delete"
variant: "danger"
size: "sm"
onClicked: {
for (var i = 0; i < users.length; i++) {
if (users[i].uid === modelData.uid) {
openDeleteDialog(i)
break
}
}
}
}
}
}
}
}
// Empty state
CText {
visible: filteredUsers().length === 0
Layout.fillWidth: true
Layout.topMargin: 24
variant: "body2"
text: "No users match the current filter."
horizontalAlignment: Text.AlignHCenter
}
users: filteredUsers()
allUsers: root.users
onEditClicked: function(uid) {
var idx = findUserIndex(uid); if (idx < 0) return
var u = users[idx]
editFormDialog.formUsername = u.username; editFormDialog.formEmail = u.email
editFormDialog.formPassword = ""; editFormDialog.formRole = u.role
editFormDialog.formActive = u.status === "active"
editIndex = idx; editDialogOpen = true
}
onDeleteClicked: function(uid) {
deleteIndex = findUserIndex(uid); deleteDialogOpen = true
}
}
}
// ── Create User Dialog ───────────────────────────────────────────
CDialog {
id: createDialog
UserFormDialog {
visible: createDialogOpen
title: "Create User"
ColumnLayout {
spacing: 14
width: 380
CTextField {
Layout.fillWidth: true
label: "Username"
placeholderText: "Enter username"
text: formUsername
onTextChanged: formUsername = text
}
CTextField {
Layout.fillWidth: true
label: "Email"
placeholderText: "Enter email address"
text: formEmail
onTextChanged: formEmail = text
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CTextField {
Layout.fillWidth: true
label: "Password"
placeholderText: "Enter password"
text: formPassword
echoMode: TextInput.Password
onTextChanged: formPassword = text
}
CBadge { text: "SHA-512 hashed"; badgeColor: "#607d8b" }
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
CText { variant: "caption"; text: "Role" }
FlexRow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: roles
CChip {
text: modelData
selected: formRole === modelData
onClicked: formRole = modelData
}
}
}
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "body2"; text: "Active" }
CSwitch {
checked: formActive
onCheckedChanged: formActive = checked
}
}
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
onClicked: { createDialogOpen = false; clearForm() }
}
CButton {
text: "Create"
variant: "primary"
enabled: formUsername !== "" && formEmail !== "" && formPassword !== ""
onClicked: createUser()
}
}
}
isEdit: false
roles: root.roles
onAccepted: createUser({ username: formUsername, email: formEmail, role: formRole, status: formActive ? "active" : "inactive" })
onCancelled: createDialogOpen = false
}
// ── Edit User Dialog ─────────────────────────────────────────────
CDialog {
id: editDialog
UserFormDialog {
id: editFormDialog
visible: editDialogOpen
title: "Edit User"
ColumnLayout {
spacing: 14
width: 380
CTextField {
Layout.fillWidth: true
label: "Username"
placeholderText: "Enter username"
text: formUsername
onTextChanged: formUsername = text
}
CTextField {
Layout.fillWidth: true
label: "Email"
placeholderText: "Enter email address"
text: formEmail
onTextChanged: formEmail = text
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CTextField {
Layout.fillWidth: true
label: "Password"
placeholderText: "Leave blank to keep current"
text: formPassword
echoMode: TextInput.Password
onTextChanged: formPassword = text
}
CBadge { text: "SHA-512 hashed"; badgeColor: "#607d8b" }
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
CText { variant: "caption"; text: "Role" }
FlexRow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: roles
CChip {
text: modelData
selected: formRole === modelData
onClicked: formRole = modelData
}
}
}
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "body2"; text: "Active" }
CSwitch {
checked: formActive
onCheckedChanged: formActive = checked
}
}
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
onClicked: { editDialogOpen = false; clearForm() }
}
CButton {
text: "Save Changes"
variant: "primary"
enabled: formUsername !== "" && formEmail !== ""
onClicked: saveEdit()
}
}
}
isEdit: true
roles: root.roles
onAccepted: saveEdit({ username: formUsername, email: formEmail, role: formRole, status: formActive ? "active" : "inactive" })
onCancelled: editDialogOpen = false
}
// ── Delete Confirmation Dialog ───────────────────────────────────
CDialog {
id: deleteDialog
visible: deleteDialogOpen
title: "Delete User"
visible: deleteDialogOpen; title: "Delete User"
ColumnLayout {
spacing: 16
width: 380
spacing: 16; width: 380
CAlert {
Layout.fillWidth: true
severity: "error"
Layout.fillWidth: true; severity: "error"
text: deleteIndex >= 0 && deleteIndex < users.length
? "Are you sure you want to delete \"" + users[deleteIndex].username + "\"? This action cannot be undone."
: "Confirm deletion?"
}
FlexRow {
Layout.fillWidth: true
spacing: 8
Item { Layout.fillWidth: true }
CButton {
text: "Cancel"
variant: "ghost"
onClicked: { deleteDialogOpen = false; deleteIndex = -1 }
}
CButton {
text: "Delete"
variant: "danger"
onClicked: confirmDelete()
}
Layout.fillWidth: true; spacing: 8; Item { Layout.fillWidth: true }
CButton { text: "Cancel"; variant: "ghost"; onClicked: { deleteDialogOpen = false; deleteIndex = -1 } }
CButton { text: "Delete"; variant: "danger"; onClicked: confirmDelete() }
}
}
}

View File

@@ -0,0 +1,119 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CCard {
id: root
Layout.fillWidth: true
Layout.fillHeight: true
property bool hasSelection: false
property string templateName: ""
property string templateSubject: ""
property string templateBody: ""
signal nameChanged(string value)
signal subjectChanged(string value)
signal bodyChanged(string value)
signal saveRequested()
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "h4"; text: "Template Editor" }
Item { Layout.fillWidth: true }
CButton {
visible: root.hasSelection
text: "Save Template"
variant: "primary"
size: "sm"
onClicked: root.saveRequested()
}
}
CDivider { Layout.fillWidth: true }
CText {
visible: !root.hasSelection
variant: "body2"
text: "Select a template from the list to edit."
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 40
}
ColumnLayout {
visible: root.hasSelection
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 12
CTextField {
Layout.fillWidth: true
label: "Template Name"
placeholderText: "e.g. Welcome Email"
text: root.templateName
onTextChanged: root.nameChanged(text)
}
CTextField {
Layout.fillWidth: true
label: "Subject Template"
placeholderText: "e.g. Welcome to {{app_name}}"
text: root.templateSubject
onTextChanged: root.subjectChanged(text)
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "caption"; text: "Body Template" }
Item { Layout.fillWidth: true }
CText {
variant: "caption"
text: "Supports {{variable}} placeholders"
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 180
color: Theme.surface
border.color: Theme.border
border.width: 1
radius: 4
ScrollView {
anchors.fill: parent
anchors.margins: 8
TextArea {
text: root.templateBody
wrapMode: Text.Wrap
color: Theme.text
font.pixelSize: 13
font.family: "monospace"
background: null
onTextChanged: root.bodyChanged(text)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CCard {
id: root
Layout.preferredWidth: 280
Layout.fillHeight: true
property var templates: []
property int selectedIndex: -1
signal templateSelected(int index)
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
CText { variant: "h4"; text: "Templates" }
CText { variant: "caption"; text: root.templates.length + " templates" }
CDivider { Layout.fillWidth: true }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 200
model: root.templates
spacing: 2
clip: true
delegate: CListItem {
width: parent ? parent.width : 248
title: modelData.name
subtitle: modelData.id
selected: root.selectedIndex === index
onClicked: root.templateSelected(index)
}
}
}
}

View File

@@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CCard {
id: root
Layout.fillWidth: true
property string recipient: ""
property string subject: "MetaBuilder SMTP Test"
property string body: "This is a test email from MetaBuilder."
property bool sending: false
signal recipientChanged(string value)
signal subjectChanged(string value)
signal bodyChanged(string value)
signal sendRequested()
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
CText { variant: "h4"; text: "Send Test Email" }
CDivider { Layout.fillWidth: true }
RowLayout {
Layout.fillWidth: true
spacing: 12
CTextField {
Layout.fillWidth: true
label: "Recipient"
placeholderText: "test@example.com"
text: root.recipient
onTextChanged: root.recipientChanged(text)
}
CTextField {
Layout.fillWidth: true
label: "Subject"
placeholderText: "Test subject"
text: root.subject
onTextChanged: root.subjectChanged(text)
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CText { variant: "caption"; text: "Body" }
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 100
color: Theme.surface
border.color: Theme.border
border.width: 1
radius: 4
ScrollView {
anchors.fill: parent
anchors.margins: 8
TextArea {
text: root.body
wrapMode: Text.Wrap
color: Theme.text
font.pixelSize: 13
background: null
onTextChanged: root.bodyChanged(text)
}
}
}
}
FlexRow {
Layout.fillWidth: true
spacing: 12
CButton {
text: root.sending ? "Sending..." : "Send Test Email"
variant: "primary"
size: "sm"
enabled: !root.sending && root.recipient.length > 0
onClicked: root.sendRequested()
}
CText {
visible: root.recipient.length === 0
variant: "caption"
text: "Enter a recipient to enable sending"
}
}
}
}

View File

@@ -0,0 +1,256 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CCard {
id: livePreview
Layout.fillWidth: true
property string customPrimary: "#000000"
property string customBackground: "#000000"
property string customSurface: "#000000"
property string customPaper: "#000000"
property string customText: "#000000"
property string customTextSecondary: "#000000"
property string customBorder: "#000000"
property string customError: "#000000"
property string customWarning: "#000000"
property string customSuccess: "#000000"
property string customInfo: "#000000"
property string fontFamily: "Inter"
property int baseFontSize: 14
property int radiusSmall: 4
property int radiusMedium: 8
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h4"; text: "Live Preview" }
Item { Layout.fillWidth: true }
CBadge { text: "Interactive" }
}
CText { variant: "caption"; text: "A sample UI rendered with your current theme configuration" }
CDivider { Layout.fillWidth: true }
// Preview container
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 340
radius: radiusMedium
color: customBackground
border.width: 1
border.color: customBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 14
// Preview header bar
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: radiusSmall
color: customSurface
RowLayout {
anchors.fill: parent
anchors.leftMargin: 14
anchors.rightMargin: 14
spacing: 12
Text {
text: "MetaBuilder"
font.pixelSize: baseFontSize + 2
font.weight: Font.Bold
font.family: fontFamily
color: customText
}
Item { Layout.fillWidth: true }
Repeater {
model: ["Dashboard", "Settings", "Help"]
Text {
text: modelData
font.pixelSize: baseFontSize - 1
font.family: fontFamily
color: customTextSecondary
}
}
}
}
// Preview content area
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 12
// Preview card 1 - Status
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: radiusMedium
color: customPaper
border.width: 1
border.color: customBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 8
Text {
text: "Status"
font.pixelSize: baseFontSize
font.weight: Font.Bold
font.family: fontFamily
color: customText
}
Rectangle {
Layout.fillWidth: true
height: 1
color: customBorder
}
Repeater {
model: [
{ label: "DBAL", col: customSuccess },
{ label: "Auth", col: customSuccess },
{ label: "Storage", col: customWarning }
]
RowLayout {
spacing: 8
Rectangle {
width: 8; height: 8; radius: 4
color: modelData.col
}
Text {
text: modelData.label
font.pixelSize: baseFontSize - 2
font.family: fontFamily
color: customTextSecondary
}
}
}
Item { Layout.fillHeight: true }
Rectangle {
Layout.fillWidth: true
height: 30
radius: radiusSmall
color: customPrimary
Text {
anchors.centerIn: parent
text: "View Details"
font.pixelSize: baseFontSize - 2
font.family: fontFamily
color: "#ffffff"
}
}
}
}
// Preview card 2 - Activity
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: radiusMedium
color: customPaper
border.width: 1
border.color: customBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 8
Text {
text: "Activity"
font.pixelSize: baseFontSize
font.weight: Font.Bold
font.family: fontFamily
color: customText
}
Rectangle {
Layout.fillWidth: true
height: 1
color: customBorder
}
Repeater {
model: [
{ msg: "User signed in", t: "2m ago" },
{ msg: "Package installed", t: "5m ago" },
{ msg: "Schema updated", t: "1h ago" }
]
ColumnLayout {
spacing: 2
Text {
text: modelData.msg
font.pixelSize: baseFontSize - 2
font.family: fontFamily
color: customText
}
Text {
text: modelData.t
font.pixelSize: baseFontSize - 4
font.family: fontFamily
color: customTextSecondary
}
}
}
Item { Layout.fillHeight: true }
Rectangle {
Layout.fillWidth: true
height: 24
radius: radiusSmall
color: Qt.alpha(customError, 0.15)
Text {
anchors.centerIn: parent
text: "1 alert"
font.pixelSize: baseFontSize - 4
font.family: fontFamily
color: customError
}
}
Rectangle {
Layout.fillWidth: true
height: 24
radius: radiusSmall
color: Qt.alpha(customInfo, 0.15)
Text {
anchors.centerIn: parent
text: "3 notifications"
font.pixelSize: baseFontSize - 4
font.family: fontFamily
color: customInfo
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,182 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
ColumnLayout {
id: spacingRadiusRoot
Layout.fillWidth: true
spacing: 20
property int baseSpacing: 8
property int radiusSmall: 4
property int radiusMedium: 8
property int radiusLarge: 16
signal baseSpacingChanged(int value)
signal radiusSmallChanged(int value)
signal radiusMediumChanged(int value)
signal radiusLargeChanged(int value)
// Spacing section
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Spacing" }
CText { variant: "caption"; text: "Base spacing unit used as a multiplier across the layout system" }
CDivider { Layout.fillWidth: true }
RowLayout {
Layout.fillWidth: true
spacing: 16
CTextField {
Layout.preferredWidth: 120
label: "Base Spacing (px)"
placeholderText: "8"
text: baseSpacing.toString()
onTextChanged: {
var val = parseInt(text)
if (!isNaN(val) && val > 0 && val <= 32) {
baseSpacingChanged(val)
}
}
}
// Spacing preview
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CText { variant: "caption"; text: "Preview: spacing scale" }
RowLayout {
spacing: 8
Repeater {
model: [1, 2, 3, 4, 6]
ColumnLayout {
spacing: 4
Rectangle {
width: baseSpacing * modelData
height: baseSpacing * modelData
radius: 3
color: Theme.primary
opacity: 0.3 + (index * 0.15)
}
Text {
text: (baseSpacing * modelData) + "px"
font.pixelSize: 10
color: Theme.textSecondary
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}
}
}
// Border Radius section
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Border Radius" }
CText { variant: "caption"; text: "Control corner rounding for small, medium, and large elements" }
CDivider { Layout.fillWidth: true }
RowLayout {
Layout.fillWidth: true
spacing: 16
CTextField {
Layout.preferredWidth: 100
label: "Small (px)"
placeholderText: "4"
text: radiusSmall.toString()
onTextChanged: {
var val = parseInt(text)
if (!isNaN(val) && val >= 0) {
radiusSmallChanged(val)
}
}
}
CTextField {
Layout.preferredWidth: 100
label: "Medium (px)"
placeholderText: "8"
text: radiusMedium.toString()
onTextChanged: {
var val = parseInt(text)
if (!isNaN(val) && val >= 0) {
radiusMediumChanged(val)
}
}
}
CTextField {
Layout.preferredWidth: 100
label: "Large (px)"
placeholderText: "16"
text: radiusLarge.toString()
onTextChanged: {
var val = parseInt(text)
if (!isNaN(val) && val >= 0) {
radiusLargeChanged(val)
}
}
}
Item { Layout.fillWidth: true }
// Radius preview
RowLayout {
spacing: 16
Repeater {
model: [
{ label: "Sm", r: radiusSmall },
{ label: "Md", r: radiusMedium },
{ label: "Lg", r: radiusLarge }
]
ColumnLayout {
spacing: 4
Rectangle {
width: 48
height: 48
radius: modelData.r
color: "transparent"
border.width: 2
border.color: Theme.primary
}
Text {
text: modelData.label + " (" + modelData.r + "px)"
font.pixelSize: 10
color: Theme.textSecondary
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CCard {
id: typographyCard
Layout.fillWidth: true
property string fontFamily: "Inter"
property int baseFontSize: 14
signal fontFamilyChanged(string family)
signal baseFontSizeChanged(int size)
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Typography" }
CText { variant: "caption"; text: "Configure font family and base size for the entire interface" }
CDivider { Layout.fillWidth: true }
RowLayout {
Layout.fillWidth: true
spacing: 16
ColumnLayout {
Layout.fillWidth: true
spacing: 8
CTextField {
Layout.fillWidth: true
label: "Font Family"
placeholderText: "e.g., Inter, Roboto, system-ui"
text: fontFamily
onTextChanged: fontFamilyChanged(text)
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
CText { variant: "body2"; text: "Base Font Size: " + baseFontSize + "px" }
Slider {
Layout.fillWidth: true
from: 10
to: 24
stepSize: 1
value: baseFontSize
onValueChanged: baseFontSizeChanged(value)
background: Rectangle {
x: parent.leftPadding
y: parent.topPadding + parent.availableHeight / 2 - height / 2
width: parent.availableWidth
height: 4
radius: 2
color: Theme.border
Rectangle {
width: parent.parent.visualPosition * parent.width
height: parent.height
radius: 2
color: Theme.primary
}
}
handle: Rectangle {
x: parent.leftPadding + parent.visualPosition * (parent.availableWidth - width)
y: parent.topPadding + parent.availableHeight / 2 - height / 2
width: 18
height: 18
radius: 9
color: Theme.primary
border.width: 2
border.color: Theme.background
}
}
RowLayout {
spacing: 4
CText { variant: "caption"; text: "10px" }
Item { Layout.fillWidth: true }
CText { variant: "caption"; text: "24px" }
}
}
}
}
}