mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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:
@@ -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 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
119
frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateEditor.qml
Normal file
119
frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateEditor.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateList.qml
Normal file
42
frontends/qt6/qmllib/MetaBuilder/CSmtpTemplateList.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
frontends/qt6/qmllib/MetaBuilder/CSmtpTestEmailForm.qml
Normal file
98
frontends/qt6/qmllib/MetaBuilder/CSmtpTestEmailForm.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
256
frontends/qt6/qmllib/MetaBuilder/ThemeLivePreview.qml
Normal file
256
frontends/qt6/qmllib/MetaBuilder/ThemeLivePreview.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
frontends/qt6/qmllib/MetaBuilder/ThemeSpacingRadius.qml
Normal file
182
frontends/qt6/qmllib/MetaBuilder/ThemeSpacingRadius.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml
Normal file
94
frontends/qt6/qmllib/MetaBuilder/ThemeTypography.qml
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user