mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Remove frontends/qt6 views; update import path
Delete duplicated frontend QML files under frontends/qt6 (GodPanel.qml, SettingsView.qml). Adjust qml/qt6/GodPanel.qml to import the local qmllib/MetaBuilder path instead of the MetaBuilder 1.0 module, keeping the canonical QML implementation in qml/qt6.
This commit is contained in:
@@ -1,227 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QmlComponents 1.0
|
|
||||||
import "qmllib/MetaBuilder"
|
|
||||||
import "config/GodPanelConfig.js" as GodPanelConfig
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: godPanel
|
|
||||||
color: Theme.background
|
|
||||||
|
|
||||||
property int currentTab: 0
|
|
||||||
|
|
||||||
// Config summary counts (would come from DBAL in production)
|
|
||||||
property var configCounts: ({
|
|
||||||
schemas: 39, workflows: 12, luaScripts: 8, packages: 62,
|
|
||||||
pages: 27, components: 152, users: 3, snippets: 5,
|
|
||||||
cssClasses: 44, dropdowns: 16, dbBackends: 14
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Data loaded from JSON config ──
|
|
||||||
property var tabModel: []
|
|
||||||
property var levelData: []
|
|
||||||
property var configStatData: []
|
|
||||||
property var tabSources: []
|
|
||||||
|
|
||||||
// ── MD3-inspired palette ──
|
|
||||||
readonly property bool isDark: Theme.mode === "dark"
|
|
||||||
readonly property color accentBlue: "#6366F1"
|
|
||||||
readonly property color accentCyan: "#06B6D4"
|
|
||||||
readonly property color accentViolet: "#8B5CF6"
|
|
||||||
readonly property color accentAmber: "#F59E0B"
|
|
||||||
readonly property color accentRose: "#F43F5E"
|
|
||||||
|
|
||||||
// MD3 tonal surfaces
|
|
||||||
readonly property color surfaceContainer: isDark ? Qt.rgba(1, 1, 1, 0.05) : Qt.rgba(0.31, 0.31, 0.44, 0.06)
|
|
||||||
readonly property color surfaceContainerHigh: isDark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0.31, 0.31, 0.44, 0.10)
|
|
||||||
readonly property color surfaceContainerHighest: isDark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0.31, 0.31, 0.44, 0.14)
|
|
||||||
readonly property color onSurface: Theme.text
|
|
||||||
readonly property color onSurfaceVariant: Theme.textSecondary
|
|
||||||
readonly property color outline: Theme.border
|
|
||||||
readonly property color outlineVariant: isDark ? Qt.rgba(1, 1, 1, 0.06) : Qt.rgba(0, 0, 0, 0.08)
|
|
||||||
readonly property color primaryContainer: isDark ? Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.15) : Qt.rgba(accentBlue.r, accentBlue.g, accentBlue.b, 0.12)
|
|
||||||
|
|
||||||
// Level accent colors for guide cards
|
|
||||||
readonly property var levelAccents: [
|
|
||||||
"#94A3B8", accentBlue, accentCyan, accentViolet, accentRose
|
|
||||||
]
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
var tabs = GodPanelConfig.loadTabs()
|
|
||||||
tabModel = tabs
|
|
||||||
|
|
||||||
// Extract source paths for the Loader-based tabs
|
|
||||||
var sources = []
|
|
||||||
for (var i = 0; i < tabs.length; i++)
|
|
||||||
sources.push(tabs[i].source || "")
|
|
||||||
tabSources = sources
|
|
||||||
|
|
||||||
levelData = GodPanelConfig.loadLevels()
|
|
||||||
|
|
||||||
var rawStats = GodPanelConfig.loadConfigStats()
|
|
||||||
var palette = { accentBlue: accentBlue, accentCyan: accentCyan,
|
|
||||||
accentViolet: accentViolet, accentAmber: accentAmber,
|
|
||||||
accentRose: accentRose }
|
|
||||||
configStatData = GodPanelConfig.resolveConfigStats(rawStats, configCounts, palette)
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 24
|
|
||||||
spacing: 20
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════
|
|
||||||
// HEADER
|
|
||||||
// ═══════════════════════════════════════════════════
|
|
||||||
CGodPanelHeader {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
configCounts: godPanel.configCounts
|
|
||||||
isDark: godPanel.isDark
|
|
||||||
onNavigateLevel: function(level) {
|
|
||||||
if (level === 1) appWindow.currentView = "frontpage"
|
|
||||||
else if (level === 2) appWindow.currentView = "dashboard"
|
|
||||||
else if (level === 3) appWindow.currentView = "admin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════
|
|
||||||
// TAB BAR
|
|
||||||
// ═══════════════════════════════════════════════════
|
|
||||||
CTabBar {
|
|
||||||
id: tabBar
|
|
||||||
Layout.fillWidth: true
|
|
||||||
currentIndex: currentTab
|
|
||||||
onCurrentIndexChanged: currentTab = currentIndex
|
|
||||||
tabs: tabModel
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════
|
|
||||||
// TAB CONTENT
|
|
||||||
// ═══════════════════════════════════════════════════
|
|
||||||
StackLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
currentIndex: currentTab
|
|
||||||
|
|
||||||
// ── 0 - Guide ──
|
|
||||||
Rectangle {
|
|
||||||
color: "transparent"
|
|
||||||
ScrollView {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
contentWidth: availableWidth
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
width: parent.width
|
|
||||||
spacing: 20
|
|
||||||
|
|
||||||
// Intro section
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: guideIntroCol.implicitHeight + 48
|
|
||||||
radius: 16; color: surfaceContainerHigh
|
|
||||||
border.width: 1; border.color: outlineVariant
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: guideIntroCol
|
|
||||||
anchors { left: parent.left; right: parent.right; top: parent.top; margins: 24 }
|
|
||||||
spacing: 16
|
|
||||||
CText { text: "Builder Quick Reference"; font.pixelSize: 22; font.weight: Font.Bold; color: onSurface; Layout.fillWidth: true }
|
|
||||||
CText { text: "MetaBuilder uses a 5-level permission and interface system. Each level unlocks progressively more powerful tools."; font.pixelSize: 14; wrapMode: Text.Wrap; Layout.fillWidth: true; color: onSurfaceVariant; lineHeight: 1.5 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Level cards
|
|
||||||
CText { text: "Access Levels"; font.pixelSize: 18; font.weight: Font.DemiBold; color: onSurface; Layout.fillWidth: true; Layout.topMargin: 4 }
|
|
||||||
Repeater {
|
|
||||||
model: levelData
|
|
||||||
delegate: CLevelReferenceCard {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
levelName: modelData.level; role: modelData.role; description: modelData.desc
|
|
||||||
accent: levelAccents[modelData.accentIndex]; levelNumber: modelData.accentIndex + 1
|
|
||||||
isDark: godPanel.isDark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config summary section
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: configSummaryCol.implicitHeight + 48
|
|
||||||
Layout.topMargin: 8; radius: 16; color: surfaceContainerHigh
|
|
||||||
border.width: 1; border.color: outlineVariant
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: configSummaryCol
|
|
||||||
anchors { left: parent.left; right: parent.right; top: parent.top; margins: 24 }
|
|
||||||
spacing: 16
|
|
||||||
CText { text: "Current Configuration"; font.pixelSize: 16; font.weight: Font.DemiBold; color: onSurface; Layout.fillWidth: true }
|
|
||||||
GridLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
columns: Math.max(2, Math.min(4, Math.floor((parent.width + 12) / 180)))
|
|
||||||
columnSpacing: 12; rowSpacing: 12
|
|
||||||
Repeater {
|
|
||||||
model: configStatData
|
|
||||||
delegate: CConfigStatCard {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
label: modelData.label; value: modelData.value; accent: modelData.accent
|
|
||||||
isDark: godPanel.isDark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CAlert { Layout.fillWidth: true; severity: "info"; text: "Philosophy: 95% JSON config, 5% TypeScript/C++ infrastructure. Entities, workflows, pages, and business logic are all declarative." }
|
|
||||||
Item { Layout.preferredHeight: 16 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tabs 1-12: data-driven Loader tabs ──
|
|
||||||
Repeater {
|
|
||||||
model: 12
|
|
||||||
delegate: Rectangle {
|
|
||||||
color: "transparent"
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent
|
|
||||||
source: (tabSources.length > index + 1) ? tabSources[index + 1] : ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 13 - Settings (Theme + SMTP side by side) ──
|
|
||||||
Rectangle {
|
|
||||||
color: "transparent"
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent; spacing: 20
|
|
||||||
CText { text: "System Settings"; font.pixelSize: 22; font.weight: Font.Bold; color: onSurface; Layout.fillWidth: true }
|
|
||||||
CText { text: "Theme customization and SMTP configuration for outbound email."; font.pixelSize: 14; color: onSurfaceVariant; Layout.fillWidth: true }
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 16
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true; Layout.fillHeight: true; radius: 16
|
|
||||||
color: surfaceContainerHigh; border.width: 1; border.color: outlineVariant
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent; anchors.margins: 20; spacing: 12
|
|
||||||
RowLayout { Layout.fillWidth: true; spacing: 10; CText { text: "Theme Editor"; font.pixelSize: 16; font.weight: Font.DemiBold; color: onSurface } CChip { text: "Visual"; variant: "info" } }
|
|
||||||
Rectangle { Layout.fillWidth: true; height: 1; color: outlineVariant }
|
|
||||||
Loader { Layout.fillWidth: true; Layout.fillHeight: true; source: "ThemeEditor.qml" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true; Layout.fillHeight: true; radius: 16
|
|
||||||
color: surfaceContainerHigh; border.width: 1; border.color: outlineVariant
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent; anchors.margins: 20; spacing: 12
|
|
||||||
RowLayout { Layout.fillWidth: true; spacing: 10; CText { text: "SMTP Configuration"; font.pixelSize: 16; font.weight: Font.DemiBold; color: onSurface } CChip { text: "Email"; variant: "primary" } }
|
|
||||||
Rectangle { Layout.fillWidth: true; height: 1; color: outlineVariant }
|
|
||||||
Loader { Layout.fillWidth: true; Layout.fillHeight: true; source: "SMTPConfigEditor.qml" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QmlComponents 1.0
|
|
||||||
import "qmllib/dbal"
|
|
||||||
import "qmllib/MetaBuilder"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
DBALProvider { id: dbal }
|
|
||||||
property bool useLiveData: dbal.connected
|
|
||||||
|
|
||||||
// ── JSON config ──────────────────────────────────────────────
|
|
||||||
function loadJson(path) {
|
|
||||||
var xhr = new XMLHttpRequest()
|
|
||||||
xhr.open("GET", Qt.resolvedUrl(path), false); xhr.send()
|
|
||||||
return xhr.status === 200 ? JSON.parse(xhr.responseText) : null
|
|
||||||
}
|
|
||||||
property var notificationConfig: loadJson("config/settings-notifications.json") || []
|
|
||||||
property var aboutConfig: loadJson("config/settings-about.json") || []
|
|
||||||
property var fontSizeConfig: loadJson("config/settings-font-sizes.json") || []
|
|
||||||
|
|
||||||
// ── State ────────────────────────────────────────────────────
|
|
||||||
property string displayName: appWindow.currentUser
|
|
||||||
property string userEmail: appWindow.currentUser + "@metabuilder.io"
|
|
||||||
property bool profileSaved: false
|
|
||||||
property string selectedTheme: appWindow.currentTheme
|
|
||||||
property string fontSize: "medium"
|
|
||||||
property var notifValues: ({ emailNotifications: true, desktopNotifications: true, soundAlerts: false })
|
|
||||||
|
|
||||||
function userInitials() {
|
|
||||||
var n = appWindow.currentUser
|
|
||||||
if (!n || n.length === 0) return "??"
|
|
||||||
var p = n.split(" ")
|
|
||||||
return p.length >= 2 ? (p[0][0] + p[1][0]).toUpperCase() : n.substring(0, 2).toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── DBAL persistence ─────────────────────────────────────────
|
|
||||||
function saveProfile() {
|
|
||||||
var onSaved = function() { profileSaved = true; profileSavedTimer.restart() }
|
|
||||||
if (useLiveData) {
|
|
||||||
dbal.update("user", appWindow.currentUser,
|
|
||||||
{ displayName: displayName, email: userEmail },
|
|
||||||
function(r, e) { if (!e) onSaved() })
|
|
||||||
} else { onSaved(); console.log("[Settings] Profile saved (offline):", displayName, userEmail) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePreferences() {
|
|
||||||
if (!useLiveData) return
|
|
||||||
dbal.update("user", appWindow.currentUser, {
|
|
||||||
preferences: { theme: selectedTheme, fontSize: fontSize,
|
|
||||||
emailNotifications: notifValues.emailNotifications,
|
|
||||||
desktopNotifications: notifValues.desktopNotifications,
|
|
||||||
soundAlerts: notifValues.soundAlerts }
|
|
||||||
}, function(r, e) { if (!e) console.log("[Settings] Preferences saved to DBAL") })
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPreferences() {
|
|
||||||
if (!useLiveData) return
|
|
||||||
dbal.findFirst("user", { username: appWindow.currentUser }, function(result, error) {
|
|
||||||
if (error || !result) return
|
|
||||||
var items = result.items || []; if (items.length === 0) return
|
|
||||||
var u = items[0]
|
|
||||||
if (u.displayName) displayName = u.displayName
|
|
||||||
if (u.email) userEmail = u.email
|
|
||||||
if (u.preferences) {
|
|
||||||
if (u.preferences.theme) selectedTheme = u.preferences.theme
|
|
||||||
if (u.preferences.fontSize) fontSize = u.preferences.fontSize
|
|
||||||
var nv = JSON.parse(JSON.stringify(notifValues))
|
|
||||||
if (u.preferences.emailNotifications !== undefined) nv.emailNotifications = u.preferences.emailNotifications
|
|
||||||
if (u.preferences.desktopNotifications !== undefined) nv.desktopNotifications = u.preferences.desktopNotifications
|
|
||||||
if (u.preferences.soundAlerts !== undefined) nv.soundAlerts = u.preferences.soundAlerts
|
|
||||||
notifValues = nv
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: { if (useLiveData) loadPreferences() }
|
|
||||||
onUseLiveDataChanged: { if (useLiveData) loadPreferences() }
|
|
||||||
Timer { id: profileSavedTimer; interval: 3000; onTriggered: profileSaved = false }
|
|
||||||
|
|
||||||
// ── UI ───────────────────────────────────────────────────────
|
|
||||||
ScrollView {
|
|
||||||
anchors.fill: parent; anchors.margins: 24; clip: true
|
|
||||||
ColumnLayout {
|
|
||||||
width: parent.width; spacing: 20
|
|
||||||
|
|
||||||
CText { variant: "h3"; text: "Settings" }
|
|
||||||
|
|
||||||
// Profile
|
|
||||||
CSettingsSection {
|
|
||||||
title: "Profile"
|
|
||||||
FlexRow {
|
|
||||||
Layout.fillWidth: true; Layout.topMargin: 4; spacing: 16
|
|
||||||
CAvatar { size: "lg"; initials: userInitials() }
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true; spacing: 4
|
|
||||||
CText { variant: "subtitle1"; text: appWindow.currentUser; font.bold: true }
|
|
||||||
CText { variant: "body2"; text: appWindow.currentRole + " \u00b7 Level " + appWindow.currentLevel; opacity: 0.7 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CTextField { Layout.fillWidth: true; Layout.topMargin: 8; label: "Display Name"; placeholderText: "Enter display name"; text: displayName; onTextChanged: displayName = text }
|
|
||||||
CTextField { Layout.fillWidth: true; label: "Email"; placeholderText: "Enter email address"; text: userEmail; onTextChanged: userEmail = text }
|
|
||||||
FlexRow {
|
|
||||||
Layout.fillWidth: true; spacing: 12
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
CAlert { visible: profileSaved; severity: "success"; text: "Profile saved successfully" }
|
|
||||||
CButton { text: "Save Profile"; variant: "primary"; onClicked: saveProfile() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Appearance
|
|
||||||
CSettingsSection {
|
|
||||||
title: "Appearance"
|
|
||||||
CThemePicker {
|
|
||||||
Layout.fillWidth: true; currentTheme: root.selectedTheme
|
|
||||||
onThemeSelected: function(name) {
|
|
||||||
root.selectedTheme = name; appWindow.currentTheme = name
|
|
||||||
if (typeof Theme.setTheme === "function") Theme.setTheme(name)
|
|
||||||
savePreferences()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CText { variant: "subtitle2"; text: "Font Size"; Layout.topMargin: 8 }
|
|
||||||
FlexRow {
|
|
||||||
Layout.fillWidth: true; spacing: 8
|
|
||||||
Repeater {
|
|
||||||
model: fontSizeConfig
|
|
||||||
delegate: CButton {
|
|
||||||
text: modelData.label; variant: fontSize === modelData.id ? "primary" : "default"; size: "sm"
|
|
||||||
onClicked: { fontSize = modelData.id; savePreferences() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notifications
|
|
||||||
CSettingsSection {
|
|
||||||
title: "Notifications"
|
|
||||||
CNotificationToggles {
|
|
||||||
Layout.fillWidth: true; model: notificationConfig; values: notifValues
|
|
||||||
onToggled: function(id, value) {
|
|
||||||
var nv = JSON.parse(JSON.stringify(notifValues)); nv[id] = value; notifValues = nv; savePreferences()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection
|
|
||||||
CSettingsSection {
|
|
||||||
title: "Connection"
|
|
||||||
CConnectionTest { Layout.fillWidth: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// About
|
|
||||||
CSettingsSection {
|
|
||||||
title: "About"
|
|
||||||
Repeater {
|
|
||||||
model: aboutConfig
|
|
||||||
delegate: FlexRow {
|
|
||||||
Layout.fillWidth: true; spacing: 12
|
|
||||||
CText { variant: "body2"; text: modelData.label; opacity: 0.6; Layout.preferredWidth: 120 }
|
|
||||||
CText { variant: "body1"; text: modelData.value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FlexRow {
|
|
||||||
Layout.fillWidth: true; spacing: 12
|
|
||||||
CText { variant: "body2"; text: "Platform"; opacity: 0.6; Layout.preferredWidth: 120 }
|
|
||||||
CText { variant: "body1"; text: Qt.platform.os }
|
|
||||||
}
|
|
||||||
CDivider { Layout.fillWidth: true }
|
|
||||||
FlexRow {
|
|
||||||
Layout.fillWidth: true; spacing: 12
|
|
||||||
CButton { text: "View Documentation"; variant: "default"; size: "sm"; onClicked: Qt.openUrlExternally("https://github.com/nicholasgriffintn/metabuilder") }
|
|
||||||
CButton { text: "Report Issue"; variant: "ghost"; size: "sm"; onClicked: Qt.openUrlExternally("https://github.com/nicholasgriffintn/metabuilder/issues") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CText { variant: "caption"; text: useLiveData ? "Connected to DBAL \u2014 preferences synced" : "Offline \u2014 preferences stored locally"; opacity: 0.4 }
|
|
||||||
Item { Layout.preferredHeight: 20 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QmlComponents 1.0
|
import QmlComponents 1.0
|
||||||
import MetaBuilder 1.0
|
import "qmllib/MetaBuilder"
|
||||||
import "config/GodPanelConfig.js" as GodPanelConfig
|
import "config/GodPanelConfig.js" as GodPanelConfig
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
Reference in New Issue
Block a user