mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-77929 Move uv project creation from custom section to top-level in new project panel
(cherry picked from commit 6bb6eda2b554de46fbdf4616eb6d9b17fd1949f9) IJ-MR-178621 GitOrigin-RevId: 537158b52cf7e3f6cb49eb669fccafb229f65b56
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5cca358308
commit
0d76aa8817
@@ -524,6 +524,7 @@ sdk.create.interpreter.type=Interpreter type:
|
||||
sdk.create.type.project.venv=Project venv
|
||||
sdk.create.type.base.conda=Base conda
|
||||
sdk.create.type.custom=Custom environment
|
||||
sdk.create.type.project.uv=Uv
|
||||
sdk.create.python.version=Python version:
|
||||
sdk.create.conda.executable.path=Path to conda:
|
||||
sdk.create.conda.missing.text=No conda executable found
|
||||
@@ -531,6 +532,7 @@ sdk.create.conda.install.fix=Install Miniconda
|
||||
sdk.create.error.base.broken=Base Python {0} is broken, virtual environment can not be created on top of it
|
||||
sdk.create.simple.venv.hint=Python virtual environment will be created in the project root: {0}
|
||||
sdk.create.simple.conda.hint=To create a new conda environment or choose an existing one, proceed with Custom environment
|
||||
sdk.create.simple.uv.hint=Uv environment will be created in the project root: {0}
|
||||
sdk.create.custom.develop.on=Develop on:
|
||||
sdk.create.custom.env.creation.type=Environment:
|
||||
sdk.create.custom.generate.new=Generate new
|
||||
|
||||
@@ -101,7 +101,6 @@ class NewPythonProjectStep(parent: NewProjectWizardStep, val createPythonModuleS
|
||||
builder.row { cell(onShowTrigger) }
|
||||
|
||||
val sdkPanelBuilder = PythonSdkPanelBuilderAndSdkCreator(
|
||||
onlyAllowedInterpreterTypes = null,
|
||||
errorSink = ShowingMessageErrorSync,
|
||||
module = null,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,6 @@ import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer
|
||||
import com.jetbrains.python.newProjectWizard.impl.PyV3UIServicesProd
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.sdk.refreshPaths
|
||||
import com.jetbrains.python.statistics.version
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -48,7 +47,6 @@ import kotlin.reflect.jvm.jvmName
|
||||
abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>(
|
||||
private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS,
|
||||
private val typeSpecificUI: PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>?,
|
||||
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
|
||||
private val _newProjectName: @NlsSafe String? = null,
|
||||
private val supportsNotEmptyModuleStructure: Boolean = false,
|
||||
) : DirectoryProjectGenerator<PyV3BaseProjectSettings>, PyProjectTypeGenerator {
|
||||
@@ -106,7 +104,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
|
||||
|
||||
override fun createPeer(): ProjectGeneratorPeer<PyV3BaseProjectSettings> =
|
||||
PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, allowedInterpreterTypes, uiServices)
|
||||
PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, uiServices)
|
||||
|
||||
override fun validate(baseDirPath: String): ValidationResult =
|
||||
when (val pathOrError = validatePath(baseDirPath)) {
|
||||
|
||||
@@ -10,13 +10,11 @@ import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings
|
||||
import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
|
||||
import com.jetbrains.python.newProjectWizard.PyV3UIServices
|
||||
import com.jetbrains.python.newProjectWizard.impl.projectPath.ProjectPathImpl
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import javax.swing.JComponent
|
||||
|
||||
internal class PyV3GeneratorPeer<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>(
|
||||
baseSettings: PyV3BaseProjectSettings,
|
||||
private val specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
|
||||
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>?,
|
||||
private val uiServices: PyV3UIServices,
|
||||
) : ProjectGeneratorPeer<PyV3BaseProjectSettings> {
|
||||
private val settings = baseSettings
|
||||
@@ -24,7 +22,7 @@ internal class PyV3GeneratorPeer<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecifi
|
||||
|
||||
|
||||
override fun getComponent(projectPathField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent {
|
||||
panel = PyV3UI(settings, ProjectPathImpl(projectPathField, uiServices), specificUiAndSettings, allowedInterpreterTypes)
|
||||
panel = PyV3UI(settings, ProjectPathImpl(projectPathField, uiServices), specificUiAndSettings)
|
||||
return panel.mainPanel
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider
|
||||
import com.jetbrains.python.sdk.add.v2.PySdkCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSdkPanelBuilderAndSdkCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import javax.swing.JComponent
|
||||
|
||||
@@ -21,11 +20,9 @@ internal class PyV3UI<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>
|
||||
baseSettings: PyV3BaseProjectSettings,
|
||||
projectNameProvider: ProjectPathProvider,
|
||||
specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
|
||||
allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
|
||||
) {
|
||||
|
||||
private val sdkPanelBuilderAndSdkCreator: PythonSdkPanelBuilderAndSdkCreator = PythonSdkPanelBuilderAndSdkCreator(
|
||||
onlyAllowedInterpreterTypes = allowedInterpreterTypes,
|
||||
errorSink = ShowingMessageErrorSync,
|
||||
module = null,
|
||||
)
|
||||
|
||||
@@ -24,8 +24,6 @@ import com.jetbrains.python.sdk.add.v2.hatch.HatchExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.hatch.HatchNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.poetry.EnvironmentCreatorPoetry
|
||||
import com.jetbrains.python.sdk.add.v2.poetry.PoetryExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.uv.EnvironmentCreatorUv
|
||||
import com.jetbrains.python.sdk.add.v2.uv.UvExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.venv.EnvironmentCreatorVenv
|
||||
import com.jetbrains.python.sdk.add.v2.venv.PythonExistingEnvironmentSelector
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -65,7 +63,6 @@ class PythonAddCustomInterpreter<P : PathHolder>(
|
||||
CONDA to { CondaNewEnvironmentCreator(model, errorSink) },
|
||||
PIPENV to { EnvironmentCreatorPip(model, errorSink) },
|
||||
POETRY to { EnvironmentCreatorPoetry(model, module, errorSink) },
|
||||
UV to { EnvironmentCreatorUv(model, module, errorSink) },
|
||||
HATCH to { HatchNewEnvironmentCreator(model, errorSink) },
|
||||
).filterKeys { it.isFSSupported(model.fileSystem) }.mapValues { it.value() }
|
||||
|
||||
@@ -74,7 +71,6 @@ class PythonAddCustomInterpreter<P : PathHolder>(
|
||||
put(CONDA) { CondaExistingEnvironmentSelector(model, errorSink) }
|
||||
if (!limitExistingEnvironments) {
|
||||
put(POETRY) { PoetryExistingEnvironmentSelector(model, module) }
|
||||
put(UV) { UvExistingEnvironmentSelector(model, module) }
|
||||
put(HATCH) { HatchExistingEnvironmentSelector(model) }
|
||||
}
|
||||
}.filterKeys { it.isFSSupported(model.fileSystem) }.mapValues { it.value() }
|
||||
|
||||
@@ -25,6 +25,8 @@ import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
|
||||
import com.jetbrains.python.sdk.add.v2.conda.selectCondaEnvironment
|
||||
import com.jetbrains.python.sdk.add.v2.uv.UvInterpreterSection
|
||||
import com.jetbrains.python.sdk.add.v2.uv.uvCreator
|
||||
import com.jetbrains.python.sdk.add.v2.venv.setupVirtualenv
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
@@ -60,21 +62,15 @@ interface PySdkPanelBuilder {
|
||||
* If `onlyAllowedInterpreterTypes` then only these types are displayed. All types displayed otherwise
|
||||
*/
|
||||
internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
|
||||
private val errorSink: ErrorSink,
|
||||
private val module: Module? = null,
|
||||
private val limitExistingEnvironments: Boolean = true,
|
||||
) : PySdkPanelBuilder, PySdkCreator {
|
||||
private val propertyGraph = PropertyGraph()
|
||||
private val allowedInterpreterTypes = (onlyAllowedInterpreterTypes ?: PythonInterpreterSelectionMode.entries).also {
|
||||
assert(it.isNotEmpty()) {
|
||||
"When provided, onlyAllowedInterpreterTypes shouldn't be empty"
|
||||
}
|
||||
}
|
||||
|
||||
private val initMutex = Mutex()
|
||||
|
||||
private var selectedMode = propertyGraph.property(this.allowedInterpreterTypes.first())
|
||||
private var selectedMode = propertyGraph.property(PythonInterpreterSelectionMode.entries.first())
|
||||
private var _projectVenv = propertyGraph.booleanProperty(selectedMode, PROJECT_VENV)
|
||||
private var _baseConda = propertyGraph.booleanProperty(selectedMode, BASE_CONDA)
|
||||
private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM)
|
||||
@@ -82,12 +78,17 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
|
||||
private lateinit var pythonBaseVersionComboBox: PythonInterpreterComboBox<PathHolder.Eel>
|
||||
private lateinit var executablePath: ValidatedPathField<Version, PathHolder.Eel, ValidatedPath.Executable<PathHolder.Eel>>
|
||||
private lateinit var uvSection: UvInterpreterSection
|
||||
|
||||
private suspend fun updateVenvLocationHint(): Unit = withContext(Dispatchers.EDT) {
|
||||
val get = selectedMode.get()
|
||||
val projectPath = model.projectPathFlows.projectPathWithDefault.first().resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME).toString()
|
||||
if (get == PROJECT_VENV) venvHint.set(message("sdk.create.simple.venv.hint", projectPath))
|
||||
else if (get == BASE_CONDA && PROJECT_VENV in allowedInterpreterTypes) venvHint.set(message("sdk.create.simple.conda.hint"))
|
||||
when (get) {
|
||||
PROJECT_VENV -> venvHint.set(message("sdk.create.simple.venv.hint", projectPath))
|
||||
BASE_CONDA -> venvHint.set(message("sdk.create.simple.conda.hint"))
|
||||
PROJECT_UV -> venvHint.set(message("sdk.create.simple.uv.hint", projectPath))
|
||||
CUSTOM -> venvHint.set("")
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var custom: PythonAddCustomInterpreter<PathHolder.Eel>
|
||||
@@ -96,6 +97,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
override fun buildPanel(outerPanel: Panel, projectPathFlows: ProjectPathFlows) {
|
||||
model = PythonLocalAddInterpreterModel(projectPathFlows, FileSystem.Eel(localEel))
|
||||
model.navigator.selectionMode = selectedMode
|
||||
uvSection = UvInterpreterSection(model, module, selectedMode, propertyGraph)
|
||||
|
||||
custom = PythonAddCustomInterpreter(
|
||||
model = model,
|
||||
@@ -107,9 +109,9 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode)
|
||||
|
||||
with(outerPanel) {
|
||||
if (allowedInterpreterTypes.size > 1) { // No need to show control with only one selection
|
||||
if (PythonInterpreterSelectionMode.entries.size > 1) { // No need to show control with only one selection
|
||||
row(message("sdk.create.interpreter.type")) {
|
||||
segmentedButton(allowedInterpreterTypes) { text = message(it.nameKey) }
|
||||
segmentedButton(PythonInterpreterSelectionMode.entries) { text = message(it.nameKey) }
|
||||
.bind(selectedMode)
|
||||
}
|
||||
}
|
||||
@@ -140,16 +142,18 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
)
|
||||
}.visibleIf(_baseConda)
|
||||
|
||||
uvSection.setupUI(this, validationRequestor)
|
||||
|
||||
row("") {
|
||||
comment("").bindText(venvHint)
|
||||
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull()))
|
||||
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull()) or uvSection.hintVisiblePredicate() or _custom)
|
||||
|
||||
rowsRange {
|
||||
custom.setupUI(this, validationRequestor)
|
||||
}.visibleIf(_custom)
|
||||
}
|
||||
|
||||
model.navigator.restoreLastState(allowedInterpreterTypes) // restore the last UI state before init to prevent visual loading lags
|
||||
model.navigator.restoreLastState(PythonInterpreterSelectionMode.entries) // restore the last UI state before init to prevent visual loading lags
|
||||
}
|
||||
|
||||
override fun onShownInitialization(scopingComponent: Component) {
|
||||
@@ -171,6 +175,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
model.projectPathFlows.projectPathWithDefault.onEach { updateVenvLocationHint() }.launchIn(scope)
|
||||
selectedMode.afterChange(scope.asDisposable()) { scope.launch { updateVenvLocationHint() } }
|
||||
|
||||
uvSection.onShown(scope)
|
||||
custom.onShown(scope)
|
||||
}
|
||||
|
||||
@@ -192,6 +197,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
model.setupVirtualenv(venvFolder, moduleOrProject)
|
||||
}
|
||||
BASE_CONDA -> model.selectCondaEnvironment(base = true)
|
||||
PROJECT_UV -> model.uvCreator(module).getOrCreateSdkWithBackground(moduleOrProject)
|
||||
CUSTOM -> custom.currentSdkManager.getOrCreateSdkWithBackground(moduleOrProject)
|
||||
}.getOr { return it }
|
||||
|
||||
@@ -210,6 +216,15 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
isWSLContext = false,
|
||||
creationMode = InterpreterCreationMode.SIMPLE
|
||||
)
|
||||
PROJECT_UV -> InterpreterStatisticsInfo(
|
||||
type = InterpreterType.UV,
|
||||
target = InterpreterTarget.LOCAL,
|
||||
globalSitePackage = false,
|
||||
makeAvailableToAllProjects = false,
|
||||
previouslyConfigured = false,
|
||||
isWSLContext = false,
|
||||
creationMode = InterpreterCreationMode.SIMPLE
|
||||
)
|
||||
BASE_CONDA -> InterpreterStatisticsInfo(
|
||||
type = InterpreterType.BASE_CONDA,
|
||||
target = InterpreterTarget.LOCAL,
|
||||
|
||||
@@ -151,6 +151,7 @@ enum class PythonSupportedEnvironmentManagers(
|
||||
|
||||
enum class PythonInterpreterSelectionMode(val nameKey: String) {
|
||||
PROJECT_VENV("sdk.create.type.project.venv"),
|
||||
PROJECT_UV("sdk.create.type.project.uv"),
|
||||
BASE_CONDA("sdk.create.type.base.conda"),
|
||||
CUSTOM("sdk.create.type.custom")
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
|
||||
import com.jetbrains.python.sdk.uv.impl.setUvExecutable
|
||||
import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,6 +45,16 @@ import java.nio.file.Paths
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
|
||||
/**
|
||||
* Creates a UV environment creator for the given model.
|
||||
*
|
||||
* @param module The module context for environment creation. Can be null when creating an interpreter
|
||||
* at the project level (not associated with a specific module). When null, the creator
|
||||
* will navigate to the generic Python existing environment selector instead of the
|
||||
* UV-specific selector if a .venv directory already exists.
|
||||
*/
|
||||
internal fun PythonMutableTargetAddInterpreterModel<PathHolder.Eel>.uvCreator(module: Module?): EnvironmentCreatorUv<PathHolder.Eel> =
|
||||
EnvironmentCreatorUv(this, module, ShowingMessageErrorSync)
|
||||
|
||||
internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
@@ -53,7 +64,7 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
override val interpreterType: InterpreterType = InterpreterType.UV
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
|
||||
private val executableFlow = MutableStateFlow(model.state.uvExecutable.get())
|
||||
private lateinit var pythonVersion: ObservableMutableProperty<Version?>
|
||||
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
|
||||
private lateinit var versionComboBox: ComboBox<Version?>
|
||||
|
||||
private val loading = AtomicBooleanProperty(false)
|
||||
@@ -67,7 +78,6 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
row(message("sdk.create.python.version")) {
|
||||
pythonVersion = propertyGraph.property(null)
|
||||
versionComboBox = comboBox(listOf<Version?>(null), textListCellRenderer {
|
||||
it?.let { "${it.major}.${it.minor}" } ?: message("python.sdk.uv.default.version")
|
||||
})
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.uv
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.isNotNull
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.booleanProperty
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Encapsulates all UV-specific UI and lifecycle wiring for the interpreter dialog.
|
||||
*/
|
||||
internal class UvInterpreterSection(
|
||||
private val model: PythonMutableTargetAddInterpreterModel<PathHolder.Eel>,
|
||||
module: Module?,
|
||||
private val selectedMode: ObservableMutableProperty<PythonInterpreterSelectionMode>,
|
||||
propertyGraph: PropertyGraph,
|
||||
) {
|
||||
private val uvCreator: EnvironmentCreatorUv<PathHolder.Eel> = model.uvCreator(module)
|
||||
|
||||
private val _uv = propertyGraph.booleanProperty(selectedMode, PythonInterpreterSelectionMode.PROJECT_UV)
|
||||
|
||||
fun setupUI(outerPanel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
outerPanel.rowsRange {
|
||||
uvCreator.setupUI(this, validationRequestor)
|
||||
}.visibleIf(_uv)
|
||||
}
|
||||
|
||||
fun onShown(scope: CoroutineScope) {
|
||||
selectUvIfExists()
|
||||
uvCreator.onShown(scope)
|
||||
}
|
||||
|
||||
fun hintVisiblePredicate() = _uv and model.state.uvExecutable.isNotNull()
|
||||
|
||||
private fun selectUvIfExists() {
|
||||
if (model.state.uvExecutable.get() != null
|
||||
&& selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) {
|
||||
selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user