Wizard: add support of react js to the js template
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import kotlinx.css.*
|
||||
import styled.StyleSheet
|
||||
|
||||
object WelcomeStyles : StyleSheet("WelcomeStyles", isStatic = true) {
|
||||
val textContainer by css {
|
||||
padding(5.px)
|
||||
|
||||
backgroundColor = rgb(8, 97, 22)
|
||||
color = rgb(56, 246, 137)
|
||||
}
|
||||
|
||||
val textInput by css {
|
||||
margin(vertical = 5.px)
|
||||
|
||||
fontSize = 14.px
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>JS Client</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="${moduleName}.js"></script>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import react.dom.render
|
||||
import kotlin.browser.document
|
||||
import kotlin.browser.window
|
||||
|
||||
fun main() {
|
||||
window.onload = {
|
||||
render(document.getElementById("root")) {
|
||||
child(Welcome::class) {
|
||||
attrs {
|
||||
name = "Kotlin/JS"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import kotlinx.html.InputType
|
||||
import kotlinx.html.js.onChangeFunction
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import react.RBuilder
|
||||
import react.RComponent
|
||||
import react.RProps
|
||||
import react.RState
|
||||
#if($useStyledComponents)
|
||||
import styled.css
|
||||
import styled.styledDiv
|
||||
import styled.styledInput
|
||||
#else
|
||||
import react.dom.div
|
||||
import react.dom.input
|
||||
#end
|
||||
|
||||
interface WelcomeProps : RProps {
|
||||
var name: String
|
||||
}
|
||||
|
||||
data class WelcomeState(val name: String) : RState
|
||||
|
||||
class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) {
|
||||
|
||||
init {
|
||||
state = WelcomeState(props.name)
|
||||
}
|
||||
|
||||
override fun RBuilder.render() {
|
||||
#if($useStyledComponents)
|
||||
styledDiv {
|
||||
css {
|
||||
+WelcomeStyles.textContainer
|
||||
}
|
||||
#else
|
||||
div {
|
||||
#end
|
||||
+"Hello, ${state.name}"
|
||||
}
|
||||
#if($useStyledComponents)
|
||||
styledInput {
|
||||
css {
|
||||
+WelcomeStyles.textInput
|
||||
}
|
||||
#else
|
||||
input {
|
||||
#end
|
||||
attrs {
|
||||
type = InputType.text
|
||||
value = state.name
|
||||
onChangeFunction = { event ->
|
||||
setState(
|
||||
WelcomeState(name = (event.target as HTMLInputElement).value)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -6,6 +6,7 @@ import org.jetbrains.kotlin.tools.projectWizard.plugins.printer.BuildFilePrinter
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.printer.GradlePrinter
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.printer.MavenPrinter
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.ModuleKind
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Sourceset
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.SourcesetType
|
||||
import org.jetbrains.kotlin.tools.projectWizard.templates.Template
|
||||
@@ -67,4 +68,10 @@ data class MultiplatformModuleIR(
|
||||
)
|
||||
}.listNl(needFirstIndent = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MultiplatformModuleIR.neighbourTargetModules() =
|
||||
originalModule.parent
|
||||
?.takeIf { it.kind == ModuleKind.multiplatform }
|
||||
?.subModules
|
||||
.orEmpty()
|
||||
+6
-5
@@ -1,11 +1,8 @@
|
||||
package org.jetbrains.kotlin.tools.projectWizard.plugins
|
||||
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.Context
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.Plugin
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.TaskRunningContext
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.*
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.entity.StringValidators
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.entity.reference
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.pathParser
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.service.FileSystemWizardService
|
||||
import org.jetbrains.kotlin.tools.projectWizard.ir.buildsystem.PomIR
|
||||
import org.jetbrains.kotlin.tools.projectWizard.phases.GenerationPhase
|
||||
@@ -39,9 +36,13 @@ class StructurePlugin(context: Context) : Plugin(context) {
|
||||
}
|
||||
}
|
||||
|
||||
val TaskRunningContext.projectPath
|
||||
val ValuesReadingContext.projectPath
|
||||
get() = StructurePlugin::projectPath.reference.settingValue
|
||||
|
||||
val ValuesReadingContext.projectName
|
||||
get() = StructurePlugin::name.reference.settingValue
|
||||
|
||||
|
||||
fun TaskRunningContext.pomIR() = PomIR(
|
||||
artifactId = StructurePlugin::artifactId.reference.settingValue,
|
||||
groupId = StructurePlugin::groupId.reference.settingValue,
|
||||
|
||||
+3
-1
@@ -15,6 +15,8 @@ import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ProjectKind
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.printer.BuildFilePrinter
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.printer.printBuildFile
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.projectPath
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.templates.TemplatePlugin
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.templates.TemplatesPlugin
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.DisplayableSettingItem
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Repository
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.updateBuildFiles
|
||||
@@ -44,7 +46,7 @@ abstract class BuildSystemPlugin(context: Context) : Plugin(context) {
|
||||
|
||||
val takeRepositoriesFromDependencies by pipelineTask(GenerationPhase.PROJECT_GENERATION) {
|
||||
runBefore(BuildSystemPlugin::createModules)
|
||||
runAfter(KotlinPlugin::createModules)
|
||||
runAfter(TemplatesPlugin::postApplyTemplatesToModules)
|
||||
|
||||
withAction {
|
||||
updateBuildFiles { buildFile ->
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ class ModulesToIRsConverter(
|
||||
}
|
||||
|
||||
val moduleIr = SingleplatformModuleIR(
|
||||
module.name,
|
||||
if (modulePath == projectPath) projectName else module.name,
|
||||
modulePath,
|
||||
dependenciesIRs,
|
||||
module.template,
|
||||
|
||||
+13
-2
@@ -8,6 +8,8 @@ import org.jetbrains.kotlin.tools.projectWizard.ir.buildsystem.*
|
||||
import org.jetbrains.kotlin.tools.projectWizard.phases.GenerationPhase
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.buildSystem.BuildSystemPlugin
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.KotlinPlugin
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.projectName
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.projectPath
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.updateBuildFiles
|
||||
import org.jetbrains.kotlin.tools.projectWizard.templates.*
|
||||
import org.jetbrains.kotlin.tools.projectWizard.transformers.interceptors.InterceptionPoint
|
||||
@@ -111,7 +113,11 @@ class TemplatesPlugin(context: Context) : Plugin(context) {
|
||||
): TaskResult<Unit> {
|
||||
val template = module.template ?: return UNIT_SUCCESS
|
||||
val settings = with(template) { settingsAsMap(module.originalModule) }
|
||||
val allSettings = settings + interceptionPointSettings.mapKeys { it.key.name }
|
||||
val allSettings: Map<String, Any> = mutableMapOf<String, Any>().apply {
|
||||
putAll(settings)
|
||||
putAll(interceptionPointSettings.mapKeys { it.key.name })
|
||||
putAll(defaultSettings(module))
|
||||
}
|
||||
return with(template) { getFileTemplates(module) }.map { (fileTemplateDescriptor, filePath) ->
|
||||
val path = generatePathForFileTemplate(module, filePath)
|
||||
val fileTemplate = FileTemplate(
|
||||
@@ -123,11 +129,16 @@ class TemplatesPlugin(context: Context) : Plugin(context) {
|
||||
}.sequenceIgnore()
|
||||
}
|
||||
|
||||
private fun TaskRunningContext.defaultSettings(moduleIR: ModuleIR) = mapOf(
|
||||
"projectName" to projectName,
|
||||
"moduleName" to moduleIR.name
|
||||
)
|
||||
|
||||
private fun generatePathForFileTemplate(module: ModuleIR, filePath: FilePath) = when (module) {
|
||||
is SingleplatformModuleIR -> {
|
||||
when (filePath) {
|
||||
is SrcFilePath -> SRC_DIR / filePath.sourcesetType.toString() / KOTLIN_DIR
|
||||
is ResourcesFilePath -> RESOURCES_DIR / filePath.sourcesetType.toString()
|
||||
is ResourcesFilePath -> SRC_DIR / filePath.sourcesetType.toString() / RESOURCES_DIR
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ data class BintrayRepository(val repository: String) : CustomMavenRepository {
|
||||
|
||||
object Repositories {
|
||||
val KTOR_BINTRAY = BintrayRepository("kotlin/ktor")
|
||||
val KOTLIN_JS_WRAPPERS_BINTRAY = BintrayRepository("kotlin/kotlin-js-wrappers")
|
||||
val KOTLIN_EAP_BINTRAY = BintrayRepository("kotlin/kotlin-eap")
|
||||
val KOTLIN_DEV_BINTRAY = BintrayRepository("kotlin/kotlin-dev")
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,12 +1,19 @@
|
||||
package org.jetbrains.kotlin.tools.projectWizard.templates
|
||||
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.asPath
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.SourcesetType
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
|
||||
// Should be used to create any kind of files in the generated project
|
||||
// Except build files as they will be generated using IR
|
||||
data class FileTemplateDescriptor(val templateId: String, val relativePath: Path)
|
||||
data class FileTemplateDescriptor(val templateId: String, val relativePath: Path) {
|
||||
constructor(templateId: String) : this(
|
||||
templateId,
|
||||
Paths.get(templateId).fileName.toString().removeSuffix(".vm").asPath()
|
||||
)
|
||||
}
|
||||
|
||||
sealed class FilePath {
|
||||
abstract val sourcesetType: SourcesetType
|
||||
|
||||
+113
-10
@@ -8,16 +8,21 @@ package org.jetbrains.kotlin.tools.projectWizard.templates
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.TaskRunningContext
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.asPath
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.buildList
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.entity.TemplateSetting
|
||||
import org.jetbrains.kotlin.tools.projectWizard.core.safeAs
|
||||
import org.jetbrains.kotlin.tools.projectWizard.ir.buildsystem.*
|
||||
import org.jetbrains.kotlin.tools.projectWizard.ir.buildsystem.gradle.*
|
||||
import org.jetbrains.kotlin.tools.projectWizard.ir.buildsystem.gradle.multiplatform.DefaultTargetConfigurationIR
|
||||
import org.jetbrains.kotlin.tools.projectWizard.library.MavenArtifact
|
||||
import org.jetbrains.kotlin.tools.projectWizard.library.NpmArtifact
|
||||
import org.jetbrains.kotlin.tools.projectWizard.moduleConfigurators.JsBrowserTargetConfigurator
|
||||
import org.jetbrains.kotlin.tools.projectWizard.phases.GenerationPhase
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleSubType
|
||||
import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleType
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.DisplayableSettingItem
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.DefaultRepository
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Repositories
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.SourcesetType
|
||||
import org.jetbrains.kotlin.tools.projectWizard.settings.version.Version
|
||||
import org.jetbrains.kotlin.tools.projectWizard.transformers.interceptors.TemplateInterceptor
|
||||
@@ -32,19 +37,62 @@ class SimpleJsClientTemplate : Template() {
|
||||
override fun isApplicableTo(module: Module): Boolean =
|
||||
module.configurator == JsBrowserTargetConfigurator
|
||||
|
||||
override fun TaskRunningContext.getRequiredLibraries(module: ModuleIR): List<DependencyIR> =
|
||||
val renderEngine by enumSetting<RenderEngine>("Rendering engine", GenerationPhase.PROJECT_GENERATION) {
|
||||
defaultValue = RenderEngine.REACT_WITH_STYLED
|
||||
}
|
||||
|
||||
override val settings: List<TemplateSetting<*, *>> = listOf(renderEngine)
|
||||
|
||||
override fun TaskRunningContext.getRequiredLibraries(module: ModuleIR): List<DependencyIR> = withSettingsOf(module.originalModule) {
|
||||
buildList {
|
||||
+ArtifactBasedLibraryDependencyIR(
|
||||
MavenArtifact(DefaultRepository.JCENTER, "org.jetbrains.kotlinx", "kotlinx-html-js"),
|
||||
Version.fromString("0.6.12"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
|
||||
if (renderEngine.reference.settingValue != RenderEngine.KOTLINX_HTML) {
|
||||
+Dependencies.KOTLIN_REACT
|
||||
+Dependencies.KOTLIN_REACT_DOM
|
||||
+Dependencies.NPM_REACT
|
||||
+Dependencies.NPM_REACT_DOM
|
||||
if (renderEngine.reference.settingValue == RenderEngine.REACT_WITH_STYLED) {
|
||||
+Dependencies.NPM_REACT_IS
|
||||
+Dependencies.KOTLIN_STYLED
|
||||
+Dependencies.NPM_STYLED_COMPONENTS
|
||||
+Dependencies.NPM_INLINE_STYLE_PREFIXER
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun TaskRunningContext.getFileTemplates(module: ModuleIR): List<FileTemplateDescriptorWithPath> =
|
||||
withSettingsOf(module.originalModule) {
|
||||
buildList {
|
||||
val hasKtorServNeighbourTarget = module.safeAs<MultiplatformModuleIR>()
|
||||
?.neighbourTargetModules()
|
||||
.orEmpty()
|
||||
.any { module ->
|
||||
module.template is KtorServerTemplate
|
||||
}
|
||||
if (!hasKtorServNeighbourTarget) {
|
||||
+(FileTemplateDescriptor("$id/index.html.vm") asResourceOf SourcesetType.main)
|
||||
}
|
||||
if (renderEngine.reference.settingValue == RenderEngine.KOTLINX_HTML) {
|
||||
+(FileTemplateDescriptor("$id/client.kt.vm") asSrcOf SourcesetType.main)
|
||||
+(FileTemplateDescriptor("$id/TestClient.kt.vm", "TestClient.kt".asPath()) asSrcOf SourcesetType.test)
|
||||
} else {
|
||||
+(FileTemplateDescriptor("$id/reactClient.kt.vm", "client.kt".asPath()) asSrcOf SourcesetType.main)
|
||||
+(FileTemplateDescriptor("$id/reactComponent.kt.vm", "welcome.kt".asPath()) asSrcOf SourcesetType.main)
|
||||
}
|
||||
|
||||
if (renderEngine.reference.settingValue == RenderEngine.REACT_WITH_STYLED) {
|
||||
+(FileTemplateDescriptor("$id/WelcomeStyles.kt.vm") asSrcOf SourcesetType.main)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun TaskRunningContext.getFileTemplates(module: ModuleIR): List<FileTemplateDescriptorWithPath> = buildList {
|
||||
+(FileTemplateDescriptor("$id/client.kt.vm", "client.kt".asPath()) asSrcOf SourcesetType.main)
|
||||
+(FileTemplateDescriptor("$id/TestClient.kt.vm", "TestClient.kt".asPath()) asSrcOf SourcesetType.test)
|
||||
}
|
||||
|
||||
override fun createInterceptors(module: ModuleIR): List<TemplateInterceptor> = buildList {
|
||||
+interceptTemplate(KtorServerTemplate()) {
|
||||
@@ -78,7 +126,12 @@ class SimpleJsClientTemplate : Template() {
|
||||
if (value.isNotEmpty()) return@interceptAtPoint value
|
||||
buildList {
|
||||
+value
|
||||
+"""script(src = "/static/output.js") {}"""
|
||||
+"""
|
||||
div {
|
||||
id = "root"
|
||||
}
|
||||
""".trimIndent()
|
||||
+"""script(src = "/static/$JS_OUTPUT_FILE_NAME") {}"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +141,7 @@ class SimpleJsClientTemplate : Template() {
|
||||
target.safeAs<DefaultTargetConfigurationIR>()?.targetAccess?.type == ModuleSubType.jvm
|
||||
} as? DefaultTargetConfigurationIR ?: return@transformBuildFile null
|
||||
val jvmTargetName = jvmTarget.targetName
|
||||
val webPackTaskName = "${jsSourcesetName}BrowserWebpack"
|
||||
val webPackTaskName = "$jsSourcesetName$WEBPACK_TASK_SUFFIX"
|
||||
val jvmJarTaskAccess = GradleByNameTaskAccessIR("${jvmTargetName}Jar", "Jar")
|
||||
|
||||
val jvmJarTaskConfiguration = run {
|
||||
@@ -133,13 +186,12 @@ class SimpleJsClientTemplate : Template() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun TaskRunningContext.getIrsToAddToBuildFile(module: ModuleIR): List<BuildSystemIR> = buildList {
|
||||
+RepositoryIR(DefaultRepository.JCENTER)
|
||||
if (module is MultiplatformModuleIR) {
|
||||
+GradleImportIR("org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack")
|
||||
val taskAccessIR = GradleByNameTaskAccessIR(
|
||||
"${module.name}BrowserWebpack",
|
||||
"${module.name}$WEBPACK_TASK_SUFFIX",
|
||||
WEBPACK_TASK_CLASS
|
||||
)
|
||||
|
||||
@@ -157,5 +209,56 @@ class SimpleJsClientTemplate : Template() {
|
||||
companion object {
|
||||
private const val JS_OUTPUT_FILE_NAME = "output.js"
|
||||
private const val WEBPACK_TASK_CLASS = "KotlinWebpack"
|
||||
private const val WEBPACK_TASK_SUFFIX = "BrowserProductionWebpack"
|
||||
}
|
||||
}
|
||||
|
||||
private object Dependencies {
|
||||
val KOTLIN_REACT = ArtifactBasedLibraryDependencyIR(
|
||||
MavenArtifact(Repositories.KOTLIN_JS_WRAPPERS_BINTRAY, "org.jetbrains", "kotlin-react"),
|
||||
Version.fromString("16.9.0-pre.89-kotlin-1.3.60"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
val KOTLIN_REACT_DOM = ArtifactBasedLibraryDependencyIR(
|
||||
MavenArtifact(Repositories.KOTLIN_JS_WRAPPERS_BINTRAY, "org.jetbrains", "kotlin-react-dom"),
|
||||
Version.fromString("16.9.0-pre.89-kotlin-1.3.60"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
val KOTLIN_STYLED = ArtifactBasedLibraryDependencyIR(
|
||||
MavenArtifact(Repositories.KOTLIN_JS_WRAPPERS_BINTRAY, "org.jetbrains", "kotlin-styled"),
|
||||
Version.fromString("1.0.0-pre.89-kotlin-1.3.60"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
|
||||
val NPM_REACT = ArtifactBasedLibraryDependencyIR(
|
||||
NpmArtifact("react"),
|
||||
Version.fromString("16.12.0"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
val NPM_REACT_DOM = ArtifactBasedLibraryDependencyIR(
|
||||
NpmArtifact("react-dom"),
|
||||
Version.fromString("16.12.0"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
val NPM_REACT_IS = ArtifactBasedLibraryDependencyIR(
|
||||
NpmArtifact("react-is"),
|
||||
Version.fromString("16.12.0"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
val NPM_STYLED_COMPONENTS = ArtifactBasedLibraryDependencyIR(
|
||||
NpmArtifact("styled-components"),
|
||||
Version.fromString("5.0.0"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
val NPM_INLINE_STYLE_PREFIXER = ArtifactBasedLibraryDependencyIR(
|
||||
NpmArtifact("inline-style-prefixer"),
|
||||
Version.fromString("5.1.0"),
|
||||
DependencyType.MAIN
|
||||
)
|
||||
}
|
||||
|
||||
enum class RenderEngine(override val text: String) : DisplayableSettingItem {
|
||||
KOTLINX_HTML("Use statically typed Kotlinx.html DSL"),
|
||||
REACT("Use Kotlin-wrapped React library"),
|
||||
REACT_WITH_STYLED("Use Kotlin-wrapped React framework together with Styled Components"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user