mirror of
https://github.com/beekeeper-studio/beekeeper-studio.git
synced 2026-03-13 10:12:54 +08:00
Merge pull request #2931 from beekeeper-studio/use-ui-kit-sqltexteditor
Use UI Kit SQL Text Editor in the main app
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
@import "@beekeeperstudio/ui-kit/style.css";
|
||||
@import "codemirror/lib/codemirror";
|
||||
@import "codemirror/addon/hint/show-hint";
|
||||
@import "noty/lib/noty";
|
||||
|
||||
@@ -130,8 +130,11 @@
|
||||
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
padding-bottom: 0.25rem;
|
||||
.BksSqlTextEditor {
|
||||
min-height: 0;
|
||||
.CodeMirror {
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-linenumbers {
|
||||
|
||||
@@ -1,77 +1,78 @@
|
||||
|
||||
.CodeMirror {
|
||||
font-family: $font-family-mono, monospace;
|
||||
background: $query-editor-bg!important;
|
||||
color: $text-dark!important;
|
||||
padding: 0.75rem 0;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.CodeMirror-code {
|
||||
line-height: 1.3!important;
|
||||
}
|
||||
.CodeMirror-gutters {
|
||||
border: 0!important;
|
||||
background: $query-editor-bg!important;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
color: $brand-primary;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
// Monokai
|
||||
// ---------------------------------------
|
||||
.cm-s-monokai {
|
||||
span.cm-keyword {
|
||||
color: $brand-pink;
|
||||
:not(.BksTextEditor) {
|
||||
.CodeMirror {
|
||||
font-family: $font-family-mono, monospace;
|
||||
background: $query-editor-bg!important;
|
||||
color: $text-dark!important;
|
||||
padding: 0.75rem 0;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
}
|
||||
span.cm-variable-2 {
|
||||
color: $brand-secondary;
|
||||
.CodeMirror-code {
|
||||
line-height: 1.3!important;
|
||||
}
|
||||
div.CodeMirror-selected {
|
||||
background: rgba($theme-base, 0.25);
|
||||
.CodeMirror-gutters {
|
||||
border: 0!important;
|
||||
background: $query-editor-bg!important;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: rgba($theme-base, 0.15);
|
||||
}
|
||||
.CodeMirror-line .cm-number,
|
||||
.CodeMirror-line .cm-string {
|
||||
.CodeMirror-foldmarker {
|
||||
color: $brand-primary;
|
||||
text-shadow: none;
|
||||
}
|
||||
.CodeMirror-cursor {
|
||||
border-color: $text-dark;
|
||||
}
|
||||
span.cm-bracket {
|
||||
color: $text;
|
||||
}
|
||||
}
|
||||
|
||||
// Addon: Hints
|
||||
// --------------------------------------
|
||||
.CodeMirror-hints {
|
||||
font-family: $font-family-mono, monospace;
|
||||
background: lighten($theme-bg, 5%);
|
||||
color: $text-dark;
|
||||
padding: $gutter-h 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
@include card-shadow;
|
||||
li {
|
||||
&:hover {
|
||||
background: rgba($theme-base, 0.05);
|
||||
// Monokai
|
||||
// ---------------------------------------
|
||||
.cm-s-monokai {
|
||||
span.cm-keyword {
|
||||
color: $brand-pink;
|
||||
}
|
||||
&.CodeMirror-hint-active {
|
||||
background: darken($theme-secondary, 25%);
|
||||
color: white;
|
||||
span.cm-variable-2 {
|
||||
color: $brand-secondary;
|
||||
}
|
||||
div.CodeMirror-selected {
|
||||
background: rgba($theme-base, 0.25);
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: rgba($theme-base, 0.15);
|
||||
}
|
||||
.CodeMirror-line .cm-number,
|
||||
.CodeMirror-line .cm-string {
|
||||
color: $brand-primary;
|
||||
}
|
||||
.CodeMirror-cursor {
|
||||
border-color: $text-dark;
|
||||
}
|
||||
span.cm-bracket {
|
||||
color: $text;
|
||||
}
|
||||
}
|
||||
}
|
||||
.CodeMirror-hint {
|
||||
padding: ($gutter-h * 0.5) $gutter-h;
|
||||
border-radius: 0;
|
||||
color: $text-dark;
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
background-color: transparent;
|
||||
|
||||
// Addon: Hints
|
||||
// --------------------------------------
|
||||
.CodeMirror-hints {
|
||||
font-family: $font-family-mono, monospace;
|
||||
background: lighten($theme-bg, 5%);
|
||||
color: $text-dark;
|
||||
padding: $gutter-h 0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
@include card-shadow;
|
||||
li {
|
||||
&:hover {
|
||||
background: rgba($theme-base, 0.05);
|
||||
}
|
||||
&.CodeMirror-hint-active {
|
||||
background: darken($theme-secondary, 25%);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.CodeMirror-hint {
|
||||
padding: ($gutter-h * 0.5) $gutter-h;
|
||||
border-radius: 0;
|
||||
color: $text-dark;
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,3 +117,25 @@ $row-handle-selected: lighten($query-editor-bg, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.BksTextEditor {
|
||||
--bks-text-editor-bg-color: #{$query-editor-bg};
|
||||
--bks-text-editor-fg-color: #{$text-dark};
|
||||
--bks-text-editor-gutter-bg-color: #{$query-editor-bg};
|
||||
--bks-text-editor-keyword-fg-color: #{$brand-pink};
|
||||
--bks-text-editor-variable-2-fg-color: #{$brand-secondary};
|
||||
--bks-text-editor-selected-bg-color: #{rgba($theme-base, 0.25)};
|
||||
--bks-text-editor-linenumber-fg-color: #{rgba($theme-base, 0.15)};
|
||||
--bks-text-editor-number-fg-color: #{$brand-primary};
|
||||
--bks-text-editor-string-fg-color: #{$brand-primary};
|
||||
--bks-text-editor-cursor-bg-color: #{$text-dark};
|
||||
--bks-text-editor-selected-bg-color: #{rgba($theme-base, 0.25)};
|
||||
}
|
||||
|
||||
.BksTextEditor-hints {
|
||||
--bks-text-editor-context-menu-bg-color: #{lighten($theme-bg, 5%)};
|
||||
--bks-text-editor-context-menu-fg-color: #{$text-dark};
|
||||
--bks-text-editor-context-menu-item-bg-color-active: #{darken($theme-secondary, 25%)};
|
||||
--bks-text-editor-context-menu-item-bg-color-hover: #{rgba($theme-base, 0.05)};
|
||||
--bks-text-editor-context-menu-item-fg-color-active: #ffffff;
|
||||
}
|
||||
|
||||
@@ -580,3 +580,7 @@ x-switch {
|
||||
background: rgba(darken($theme-bg, 8%), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.BksTextEditor {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -482,59 +482,23 @@ x-buttons {
|
||||
}
|
||||
}
|
||||
|
||||
.BksTextEditor {
|
||||
--bks-text-editor-string-fg-color: #{$brand-success};
|
||||
--bks-text-editor-number-fg-color: #{$brand-warning};
|
||||
--bks-text-editor-keyword-fg-color: #{$brand-pink};
|
||||
--bks-text-editor-variable-2-fg-color: #{$brand-secondary};
|
||||
--bks-text-editor-selected-bg-color: #{rgba($theme-base, 0.25)};
|
||||
--bks-text-editor-linenumber-fg-color: #{rgba($theme-base, 0.25)};
|
||||
--bks-text-editor-cursor-bg-color: #{$text-dark};
|
||||
--bks-text-editor-bracket-fg-color: #{$text};
|
||||
|
||||
// CodeMirror
|
||||
// ----------------------------------
|
||||
.CodeMirror-hints {
|
||||
background: darken($theme-bg, 5%)!important;
|
||||
li {
|
||||
&.CodeMirror-hint-active {
|
||||
background: $brand-info;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai {
|
||||
span {
|
||||
.cm-string {
|
||||
color: $brand-success;
|
||||
}
|
||||
}
|
||||
.CodeMirror-line {
|
||||
.cm-number {
|
||||
color: $brand-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cm-s-monokai {
|
||||
font-weight: 500;
|
||||
span.cm-keyword {
|
||||
color: $brand-pink;
|
||||
}
|
||||
span.cm-variable-2 {
|
||||
color: $brand-secondary;
|
||||
}
|
||||
div.CodeMirror-selected {
|
||||
background: rgba($theme-secondary, 0.25);
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
color: rgba($theme-base, 0.25);
|
||||
}
|
||||
.CodeMirror-line .cm-number {
|
||||
color: $brand-warning;
|
||||
}
|
||||
.CodeMirror-line .cm-string {
|
||||
color: $brand-success;
|
||||
}
|
||||
.CodeMirror-cursor {
|
||||
border-color: $text-dark;
|
||||
}
|
||||
span.cm-bracket {
|
||||
color: $text;
|
||||
}
|
||||
}
|
||||
|
||||
.BksTextEditor-hints {
|
||||
--bks-text-editor-context-menu-bg-color: #{darken($theme-bg, 5%)};
|
||||
--bks-text-editor-context-menu-item-bg-color-active: #{$brand-info};
|
||||
--bks-text-editor-context-menu-item-fg-color-active: #ffffff;
|
||||
}
|
||||
|
||||
// Noty
|
||||
|
||||
@@ -32,24 +32,33 @@
|
||||
</div>
|
||||
</div>
|
||||
<sql-text-editor
|
||||
v-model="unsavedText"
|
||||
v-bind.sync="editor"
|
||||
:value="unsavedText"
|
||||
:height="editor.height"
|
||||
:read-only="editor.readOnly"
|
||||
:focus="focusingElement === 'text-editor'"
|
||||
@update:focus="updateTextEditorFocus"
|
||||
:markers="editorMarkers"
|
||||
:connection-type="connectionType"
|
||||
:extra-keybindings="keybindings"
|
||||
:formatter-dialect="formatterDialect"
|
||||
:identifier-dialect="identifierDialect"
|
||||
:keybindings="keybindings"
|
||||
:vim-config="vimConfig"
|
||||
@initialized="handleEditorInitialized"
|
||||
:keymap="userKeymap"
|
||||
:entities="entities"
|
||||
:columns-getter="columnsGetter"
|
||||
:default-schema="defaultSchema"
|
||||
:mode="dialectData.textEditorMode"
|
||||
@bks-initialized="handleEditorInitialized"
|
||||
@bks-value-change="unsavedText = $event.value"
|
||||
@bks-blur="onTextEditorBlur?.()"
|
||||
@bks-query-selection-change="handleQuerySelectionChange"
|
||||
/>
|
||||
<span class="expand" />
|
||||
<div class="toolbar text-right">
|
||||
<div
|
||||
class="toolbar text-right"
|
||||
ref="toolbar"
|
||||
>
|
||||
<div class="editor-help expand" />
|
||||
<div class="expand" />
|
||||
<div
|
||||
class="actions btn-group"
|
||||
ref="actions"
|
||||
>
|
||||
<div class="actions btn-group">
|
||||
<x-button
|
||||
v-if="showDryRun"
|
||||
class="btn btn-flat btn-small dry-run-btn"
|
||||
@@ -323,12 +332,11 @@
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { identify } from 'sql-query-identifier'
|
||||
|
||||
import { splitQueries, isTextSelected } from '../lib/db/sql_tools'
|
||||
import { EditorMarker } from '@/lib/editor/utils'
|
||||
import ProgressBar from './editor/ProgressBar.vue'
|
||||
import ResultTable from './editor/ResultTable.vue'
|
||||
import ShortcutHints from './editor/ShortcutHints.vue'
|
||||
import SQLTextEditor from '@/components/common/texteditor/SQLTextEditor.vue'
|
||||
import SqlTextEditor from "@beekeeperstudio/ui-kit/vue/sql-text-editor"
|
||||
|
||||
import QueryEditorStatusBar from './editor/QueryEditorStatusBar.vue'
|
||||
import rawlog from '@bksLogger'
|
||||
@@ -338,6 +346,10 @@
|
||||
import { PropType } from 'vue'
|
||||
import { TransportOpenTab, findQuery } from '@/common/transport/TransportOpenTab'
|
||||
import { blankFavoriteQuery } from '@/common/transport'
|
||||
import { TableOrView } from "@/lib/db/models";
|
||||
import { FormatterDialect, dialectFor } from "@shared/lib/dialects/models"
|
||||
import { findSqlQueryIdentifierDialect } from "@/lib/editor/CodeMirrorPlugins";
|
||||
import { registerQueryMagic } from "@/lib/editor/CodeMirrorPlugins";
|
||||
|
||||
const log = rawlog.scope('query-editor')
|
||||
const isEmpty = (s) => _.isEmpty(_.trim(s))
|
||||
@@ -345,7 +357,7 @@
|
||||
|
||||
export default {
|
||||
// this.queryText holds the current editor value, always
|
||||
components: { ResultTable, ProgressBar, ShortcutHints, QueryEditorStatusBar, ErrorAlert, MergeManager, SqlTextEditor: SQLTextEditor },
|
||||
components: { ResultTable, ProgressBar, ShortcutHints, QueryEditorStatusBar, ErrorAlert, MergeManager, SqlTextEditor },
|
||||
props: {
|
||||
tab: Object as PropType<TransportOpenTab>,
|
||||
active: Boolean
|
||||
@@ -375,7 +387,6 @@
|
||||
tableHeight: 0,
|
||||
savePrompt: false,
|
||||
lastWord: null,
|
||||
marker: null,
|
||||
queryParameterValues: {},
|
||||
queryForExecution: null,
|
||||
executeTime: 0,
|
||||
@@ -395,6 +406,9 @@
|
||||
*/
|
||||
focusElement: 'none',
|
||||
focusingElement: 'none',
|
||||
|
||||
individualQueries: [],
|
||||
currentlySelectedQuery: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -469,43 +483,6 @@
|
||||
result() {
|
||||
return this.results[this.selectedResult]
|
||||
},
|
||||
individualQueries() {
|
||||
if (!this.unsavedText) return []
|
||||
return splitQueries(this.unsavedText, this.identifyDialect)
|
||||
},
|
||||
currentlySelectedQueryIndex() {
|
||||
const queries = this.individualQueries
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
// Find a query in between anchor and head cursors
|
||||
if (this.editor.cursorIndex !== this.editor.cursorIndexAnchor) {
|
||||
const isSelected = isTextSelected(queries[i].start, queries[i].end, this.editor.cursorIndexAnchor, this.editor.cursorIndex)
|
||||
if (isSelected) return i
|
||||
}
|
||||
// Otherwise, find a query that sits before the cursor
|
||||
else if (this.editor.cursorIndex <= queries[i].end + 1) return i
|
||||
}
|
||||
return null
|
||||
},
|
||||
currentlySelectedQuery() {
|
||||
if (this.currentlySelectedQueryIndex === null) return null
|
||||
return this.individualQueries[this.currentlySelectedQueryIndex]
|
||||
},
|
||||
currentQueryPosition() {
|
||||
if(!this.editor.initialized || !this.currentlySelectedQuery || !this.individualQueries) {
|
||||
return null
|
||||
}
|
||||
const qi = this.currentlySelectedQueryIndex
|
||||
const previousQuery = qi === 0 ? null : this.individualQueries[qi - 1]
|
||||
// adding 1 to account for semicolon
|
||||
const start = previousQuery ? previousQuery.end + 1: 0
|
||||
const end = this.currentlySelectedQuery.end
|
||||
|
||||
return {
|
||||
from: start,
|
||||
to: end + 1
|
||||
}
|
||||
|
||||
},
|
||||
rowCount() {
|
||||
return this.result && this.result.rows ? this.result.rows.length : 0
|
||||
},
|
||||
@@ -602,13 +579,21 @@
|
||||
},
|
||||
editorMarkers() {
|
||||
const markers = []
|
||||
if (this.marker) markers.push(this.marker)
|
||||
if (this.errorMarker) markers.push(this.errorMarker)
|
||||
return markers
|
||||
},
|
||||
showResultTable() {
|
||||
return this.rowCount > 0
|
||||
},
|
||||
entities() {
|
||||
return this.tables.map((t: TableOrView) => ({ schema: t.schema, name: t.name }))
|
||||
},
|
||||
formatterDialect() {
|
||||
return FormatterDialect(dialectFor(this.connectionType))
|
||||
},
|
||||
identifierDialect() {
|
||||
return findSqlQueryIdentifierDialect(this.connectionType)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
error() {
|
||||
@@ -659,24 +644,6 @@
|
||||
this.$modal.hide(`save-modal-${this.tab.id}`)
|
||||
}
|
||||
},
|
||||
currentQueryPosition() {
|
||||
this.marker = null
|
||||
|
||||
if(!this.individualQueries || this.individualQueries.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.currentQueryPosition) {
|
||||
return
|
||||
}
|
||||
const { from, to } = this.currentQueryPosition
|
||||
|
||||
const editorText = this.unsavedText
|
||||
// const lines = editorText.split(/\n/)
|
||||
|
||||
const [markStart, markEnd] = this.locationFromPosition(editorText, from, to)
|
||||
this.marker = { from: markStart, to: markEnd, type: 'highlight' } as EditorMarker
|
||||
},
|
||||
async focusElement(element, oldElement) {
|
||||
if (oldElement === 'text-editor' && element !== 'text-editor') {
|
||||
await this.blurTextEditor()
|
||||
@@ -748,7 +715,16 @@
|
||||
})
|
||||
})
|
||||
},
|
||||
handleEditorInitialized() {
|
||||
handleEditorInitialized(detail) {
|
||||
this.editor.initialized = true
|
||||
|
||||
detail.codemirror.on("cursorActivity", (cm) => {
|
||||
this.editor.selection = cm.getSelection()
|
||||
this.editor.cursorIndex = cm.getDoc().indexFromPos(cm.getCursor())
|
||||
});
|
||||
|
||||
registerQueryMagic(() => this.defaultSchema, () => this.tables, detail.codemirror)
|
||||
|
||||
// this gives the dom a chance to kick in and render these
|
||||
// before we try to read their heights
|
||||
this.$nextTick(() => {
|
||||
@@ -798,7 +774,7 @@
|
||||
},
|
||||
updateEditorHeight() {
|
||||
let height = this.$refs.topPanel.clientHeight
|
||||
height -= this.$refs.actions.clientHeight
|
||||
height -= this.$refs.toolbar.clientHeight
|
||||
this.editor.height = height
|
||||
},
|
||||
triggerSave() {
|
||||
@@ -995,11 +971,6 @@
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
updateTextEditorFocus(focused: boolean) {
|
||||
if (!focused) {
|
||||
this.onTextEditorBlur?.()
|
||||
}
|
||||
},
|
||||
async switchPaneFocus(_event?: KeyboardEvent, target?: 'text-editor' | 'table') {
|
||||
if (target) {
|
||||
this.focusElement = target
|
||||
@@ -1033,6 +1004,29 @@
|
||||
this.focusingElement = 'none'
|
||||
})
|
||||
},
|
||||
async columnsGetter(tableName: string) {
|
||||
let table = this.tables.find(
|
||||
(t: TableOrView) => t.name === tableName || `${t.schema}.${t.name}` === tableName
|
||||
);
|
||||
|
||||
if (!table) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only refresh columns if we don't have them cached.
|
||||
if (!table.columns?.length) {
|
||||
await this.$store.dispatch("updateTableColumns", table);
|
||||
table = this.tables.find(
|
||||
(t: TableOrView) => t.name === tableName || `${t.schema}.${t.name}` === tableName
|
||||
);
|
||||
}
|
||||
|
||||
return table?.columns.map((c) => c.columnName);
|
||||
},
|
||||
handleQuerySelectionChange({ queries, selectedQuery }) {
|
||||
this.individualQueries = queries;
|
||||
this.currentlySelectedQuery = selectedQuery;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
if (this.shouldInitialize) this.initialize()
|
||||
|
||||
@@ -222,7 +222,7 @@ export default {
|
||||
},
|
||||
hasUsedDropdown: {
|
||||
get() {
|
||||
return this.settings?.hideResultsDropdown.value ?? false
|
||||
return this.settings?.hideResultsDropdown?.value ?? false
|
||||
},
|
||||
set(value) {
|
||||
this.$store.dispatch('settings/save', { key: 'hideResultsDropdown', value })
|
||||
|
||||
@@ -35,7 +35,7 @@ export function autoquoteHandler(
|
||||
}
|
||||
}
|
||||
|
||||
function findSqlQueryIdentifierDialect(connectionType: string) {
|
||||
export function findSqlQueryIdentifierDialect(connectionType: string) {
|
||||
const mappings = {
|
||||
sqlserver: "mssql",
|
||||
sqlite: "sqlite",
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.BksTextEditor-hints {
|
||||
.BksTextEditor-hints.CodeMirror-hints {
|
||||
--bks-text-editor-context-menu-bg-color: #{darken($theme-bg, 5%)};
|
||||
--bks-text-editor-context-menu-fg-color: #{$text-dark};
|
||||
--bks-text-editor-context-menu-item-bg-color-active: #{$brand-info};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"url": "https://beekeeperstudio.io"
|
||||
},
|
||||
"private": false,
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/beekeeper-studio/beekeeper-studio.git",
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"bks:build": "yarn workspace beekeeper-studio electron:build",
|
||||
"bks:dev": "yarn workspace beekeeper-studio electron:serve",
|
||||
"bks:build": "yarn lib:build && yarn workspace beekeeper-studio electron:build",
|
||||
"bks:dev": "yarn lib:build && yarn workspace beekeeper-studio electron:serve",
|
||||
"bks:config:build": "yarn workspace beekeeper-studio config:build",
|
||||
"lib:dev": "yarn workspace @bks/ui-kit lib:dev",
|
||||
"lib:dev": "yarn workspace @beekeeperstudio/ui-kit lib:dev",
|
||||
"lib:build": "yarn workspace @beekeeperstudio/ui-kit build",
|
||||
"sqltools:build": "yarn workspace sqltools build",
|
||||
"sqltools:dev": "yarn workspace sqltools serve",
|
||||
"electron:build": "yarn bks:build",
|
||||
|
||||
Reference in New Issue
Block a user