Compare commits

..
0 Commits
16 changed files with 103 additions and 308 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.nbadal.ktlint.KtlintProjectSettings">
<ktlintRulesetVersion>DEFAULT</ktlintRulesetVersion>
</component>
</project>
-4
View File
@@ -19,7 +19,6 @@ repositories {
dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.materialIconsExtended)
// Global hotkeys
implementation("com.github.kwhat:jnativehook:2.2.2")
@@ -40,7 +39,6 @@ dependencies {
// Logging
implementation("org.apache.logging.log4j:log4j-api:2.24.3")
implementation("org.apache.logging.log4j:log4j-core:2.24.3")
implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3")
}
compose.desktop {
@@ -49,8 +47,6 @@ compose.desktop {
// Allow access to AWT Classes to control the window
jvmArgs("--add-opens", "java.desktop/com.apple.eawt=ALL-UNNAMED")
// Skiko / JNativeHook load native libraries via System.load() — JDK 22+ requires explicit native access
jvmArgs("--enable-native-access=ALL-UNNAMED")
nativeDistributions {
targetFormats(TargetFormat.Dmg)
+9 -22
View File
@@ -1,52 +1,39 @@
### Get Spaces
GET http://localhost:31009/v1/spaces
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
### Get Types
GET http://localhost:31009/v1/spaces/{{spaceId}}/types
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Anytype-Version: 0.1
### Get Type
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/{{taskTypeId}}
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/bafyreidtsobmbfmf6gl7bfwyd3ik3zs4m7t6e7p46y4lonsz3chkgeeabi
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Anytype-Version: 0.1
### Get objects of type
POST http://localhost:31009/v1/spaces/{{spaceId}}/search?type=project
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Anytype-Version: 0.1
Content-Type: application/json
{
"types": ["project"]
}
### Get Property Tags (Values) Priority
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/{{priorityPropertyId}}/tags
### Get Property
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/bafyreibs7xr3zphtvjt3t4bk3jhcxlllo6jskrsatnuvkrcs65k3kfuany
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
### Get Property Values Project
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/{{projectPropertyId}}/tags
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
### Get Templates
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/{{taskTypeId}}/templates
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Anytype-Version: 0.1
### Create Task
POST http://localhost:31009/v1/spaces/{{spaceId}}/objects
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Content-Type: application/json
{
"name": "With Template - II",
"body": "And here comes the body",
"template_id": "bafyreidivup2xrqn4xbgimukkgst3ao24izqnpxdesbkdtnc5owvhf6i6i",
"name": "Test Task II",
"body": "Created via HTTP client",
"type_key": "ot-task"
}
@@ -1,9 +1,6 @@
{
"local": {
"apiKey": "<API-Key>",
"apiVersion": "2025-11-08",
"spaceId": "<Space-Id>",
"taskTypeId": "<taskTypeId>",
"priorityPropertyId": "<priorityPropertyId>"
"spaceId": "<Space-Id>"
}
}
+15 -2
View File
@@ -65,7 +65,19 @@ fun main() {
onSettings = { isSettingsVisible = true },
onExit = { exitApplication() },
)
logger.info("AnyTask started. Press ${HotkeyManager.DESCRIPTION} to open the task window.")
client.getSpaces()
.onSuccess { spaces ->
logger.info("Available spaces:")
spaces.forEach { logger.info("Space: ${it.id} ${it.name}") }
val space = spaces.find { space -> space.id == configResult.config.spaceId }
if (space == null) {
logger.error("Selected space not found: ${configResult.config.spaceId}")
exitProcess(-1)
} else {
logger.info("Selected space: ${space.name} (${space.id})")
}
}
.onFailure { logger.error("Failed to load spaces: ${it.message}") }
}
DisposableEffect(Unit) {
@@ -117,6 +129,7 @@ fun main() {
}
SettingsWindow(
config = configResult.config,
client = client,
onSave = { newConfig ->
saveConfig(newConfig, configResult.sourceFile)
configResult = ConfigResult(newConfig, configResult.sourceFile)
@@ -153,7 +166,7 @@ private fun setupSystemTray(onShow: () -> Unit, onSettings: () -> Unit, onExit:
val icon = createTrayIcon(size)
val menu = PopupMenu().apply {
add(MenuItem("Create Task").also { it.addActionListener { onShow() } })
add(MenuItem("Open").also { it.addActionListener { onShow() } })
add(MenuItem("Settings").also { it.addActionListener { onSettings() } })
addSeparator()
add(MenuItem("Quit").also { it.addActionListener { onExit() } })
@@ -1,20 +1,32 @@
package ch.ergon.anytask.api
import ch.ergon.anytask.api.dto.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.OutgoingContent
import io.ktor.http.content.TextContent
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import ch.ergon.anytask.api.dto.CreateObjectRequest
import ch.ergon.anytask.api.dto.CreateObjectResponse
import ch.ergon.anytask.api.dto.ObjectType
import ch.ergon.anytask.api.dto.SearchObject
import ch.ergon.anytask.api.dto.SearchObjectsResponse
import ch.ergon.anytask.api.dto.Space
import ch.ergon.anytask.api.dto.SpacesResponse
import ch.ergon.anytask.api.dto.TypesResponse
import kotlinx.serialization.json.JsonElement
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.plugin
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import org.apache.logging.log4j.LogManager
import kotlin.time.DurationUnit.MILLISECONDS
import kotlin.time.toDuration
@@ -33,24 +45,13 @@ class AnyTypeApi(
private val http = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true; explicitNulls = false })
}
defaultRequest {
header("Anytype-Version", "2025-11-08")
json(Json { ignoreUnknownKeys = true })
}
}.also { client ->
client.plugin(HttpSend).intercept { request ->
val method = request.method.value
val url = request.url.buildString()
logger.info("→ $method $url")
if (logger.isTraceEnabled) {
val bodyText = when (val body = request.body) {
is TextContent -> body.text
is OutgoingContent.ByteArrayContent -> body.bytes().decodeToString()
else -> null
}
if (bodyText != null) logger.trace("Request body: $bodyText")
}
val start = System.currentTimeMillis()
val call = execute(request)
val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS)
@@ -66,6 +67,7 @@ class AnyTypeApi(
suspend fun getSpaces(): Result<List<Space>> =
runCatching {
val response = http.get("$baseUrl/v1/spaces") {
header("Anytype-Version", "0.1")
bearerAuth(apiKey)
}
response.body<SpacesResponse>().data
@@ -74,6 +76,7 @@ class AnyTypeApi(
suspend fun getTypes(): Result<List<ObjectType>> =
runCatching {
val response = http.get("$baseUrl/v1/spaces/$spaceId/types") {
header("Anytype-Version", "0.1")
bearerAuth(apiKey)
}
response.body<TypesResponse>().data
@@ -83,45 +86,29 @@ class AnyTypeApi(
runCatching {
val response = http.post("$baseUrl/v1/spaces/$spaceId/search") {
contentType(ContentType.Application.Json)
header("Anytype-Version", "0.1")
bearerAuth(apiKey)
setBody(mapOf("types" to listOf(typeKey)))
}
response.body<SearchObjectsResponse>().data
}
suspend fun getTags(propertyId: String): Result<List<Tag>> =
runCatching {
val response = http.get("$baseUrl/v1/spaces/$spaceId/properties/$propertyId/tags") {
bearerAuth(apiKey)
}
response.body<TagsResponse>().data
}
suspend fun getTemplates(typeId: String): Result<List<Template>> =
runCatching {
val response = http.get("$baseUrl/v1/spaces/$spaceId/types/$typeId/templates") {
bearerAuth(apiKey)
}
response.body<TemplatesResponse>().data
}
suspend fun createObject(
title: String,
details: String? = null,
details: String = "",
typeKey: String,
templateId: String? = null,
properties: List<JsonElement> = emptyList(),
): Result<String> =
runCatching {
val response = http.post("$baseUrl/v1/spaces/$spaceId/objects") {
contentType(ContentType.Application.Json)
header("Anytype-Version", "0.1")
bearerAuth(apiKey)
setBody(
CreateObjectRequest(
name = title,
body = details?.takeIf { it.isNotBlank() },
body = details,
typeKey = typeKey,
templateId = templateId,
properties = properties,
)
)
@@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonElement
@Serializable
data class CreateObjectRequest(
val name: String,
val body: String? = null,
val body: String,
@SerialName("type_key") val typeKey: String,
@SerialName("template_id") val templateId: String? = null,
val properties: List<JsonElement> = emptyList(),
)
@@ -1,11 +0,0 @@
package ch.ergon.anytask.api.dto
import kotlinx.serialization.Serializable
@Serializable
data class Tag(
val id: String,
val key: String = "",
val name: String,
val color: String = "",
)
@@ -1,8 +0,0 @@
package ch.ergon.anytask.api.dto
import kotlinx.serialization.Serializable
@Serializable
data class TagsResponse(
val data: List<Tag>,
)
@@ -1,18 +0,0 @@
package ch.ergon.anytask.api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Template(
val id: String,
val name: String,
val archived: Boolean = false,
val icon: Icon? = null,
val layout: String = "",
val `object`: String = "",
val properties: List<Property> = emptyList(),
val snippet: String = "",
@SerialName("space_id") val spaceId: String = "",
val type: ObjectType? = null,
)
@@ -1,8 +0,0 @@
package ch.ergon.anytask.api.dto
import kotlinx.serialization.Serializable
@Serializable
data class TemplatesResponse(
val data: List<Template>,
)
@@ -17,10 +17,10 @@ data class Connection(
@Serializable
data class PropertyConfig(
val key: String,
val id: String = "",
val name: String = "",
val format: String = "",
val indicator: String = "",
@SerialName("possible_values") val possibleValues: List<String> = emptyList(),
@SerialName("object_type_key") val objectTypeKey: String = "",
)
@@ -29,7 +29,6 @@ data class Configuration(
val connection: Connection = Connection(),
val spaceId: String = "",
@SerialName("task_type_key") val taskTypeKey: String = "",
@SerialName("template_id") val templateId: String = "",
val properties: List<PropertyConfig> = emptyList(),
)
@@ -15,9 +15,6 @@ import java.util.logging.Logger
*/
object HotkeyManager {
/** Human-readable description of the hotkey combination handled by [isHotkey]. */
const val DESCRIPTION: String = "Ctrl+Shift+Space"
private val logger = LogManager.getLogger(HotkeyManager::class.java)
private var listener: NativeKeyListener? = null
@@ -30,7 +30,6 @@ import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -41,15 +40,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import ch.ergon.anytask.api.AnyTypeApi
import ch.ergon.anytask.api.dto.ObjectType
import ch.ergon.anytask.api.dto.Property
import ch.ergon.anytask.api.dto.Space
import ch.ergon.anytask.api.dto.Template
import ch.ergon.anytask.config.Configuration
import ch.ergon.anytask.config.Connection
import ch.ergon.anytask.config.PropertyConfig
@@ -58,85 +54,30 @@ import kotlinx.coroutines.launch
@Composable
fun SettingsWindow(
config: Configuration,
client: AnyTypeApi,
onSave: (Configuration) -> Unit,
onCancel: () -> Unit,
) {
val scope = rememberCoroutineScope()
var apiKey by remember { mutableStateOf(config.connection.apiKey) }
var loadedApiKey by remember { mutableStateOf(config.connection.apiKey) }
var selectedSpaceId by remember { mutableStateOf(config.spaceId) }
var selectedTypeKey by remember { mutableStateOf(config.taskTypeKey) }
var selectedTemplateId by remember { mutableStateOf(config.templateId) }
val propertyConfigs = remember { mutableStateListOf<PropertyConfig>().also { it.addAll(config.properties) } }
var spaces by remember { mutableStateOf<List<Space>>(emptyList()) }
var isLoadingSpaces by remember { mutableStateOf(false) }
var types by remember { mutableStateOf<List<ObjectType>>(emptyList()) }
var isLoadingTypes by remember { mutableStateOf(false) }
var templates by remember { mutableStateOf<List<Template>>(emptyList()) }
var isLoadingTemplates by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
// Property edit state
var editingIndex by remember { mutableStateOf<Int?>(null) } // null=none, -1=new
suspend fun loadSpaces(key: String) {
spaces = emptyList()
isLoadingSpaces = true
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key)
try {
tempClient.getSpaces()
.onSuccess { spaces = it }
.onFailure { errorMessage = "Failed to load spaces: ${it.message}" }
} finally {
tempClient.close()
}
isLoadingSpaces = false
}
suspend fun loadTypes(key: String, spaceId: String) {
types = emptyList()
isLoadingTypes = true
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId)
try {
tempClient.getTypes()
.onSuccess { types = it }
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
} finally {
tempClient.close()
}
isLoadingTypes = false
}
suspend fun loadTemplates(key: String, spaceId: String, typeId: String) {
templates = emptyList()
isLoadingTemplates = true
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId)
try {
tempClient.getTemplates(typeId)
.onSuccess { templates = it }
.onFailure { errorMessage = "Failed to load templates: ${it.message}" }
} finally {
tempClient.close()
}
isLoadingTemplates = false
}
// Initial load
// Load types on start
LaunchedEffect(Unit) {
loadSpaces(apiKey)
if (selectedSpaceId.isNotEmpty()) {
loadTypes(apiKey, selectedSpaceId)
}
}
// Load templates when type changes
LaunchedEffect(selectedTypeKey, types) {
val type = types.find { it.key == selectedTypeKey }
if (type != null && selectedSpaceId.isNotEmpty()) {
loadTemplates(apiKey, selectedSpaceId, type.id)
}
isLoadingTypes = true
client.getTypes()
.onSuccess { types = it }
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
isLoadingTypes = false
}
val selectedType = types.find { it.key == selectedTypeKey }
@@ -144,9 +85,8 @@ fun SettingsWindow(
fun buildConfig(): Configuration = Configuration(
connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl),
spaceId = selectedSpaceId,
spaceId = config.spaceId,
taskTypeKey = selectedTypeKey,
templateId = selectedTemplateId,
properties = propertyConfigs.toList(),
)
@@ -156,62 +96,13 @@ fun SettingsWindow(
Text("Settings", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(16.dp))
// ApiKey + reload button
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = apiKey,
onValueChange = { apiKey = it },
label = { Text("API Key") },
singleLine = true,
modifier = Modifier.weight(1f).onFocusChanged { focusState ->
if (!focusState.isFocused && apiKey != loadedApiKey) {
scope.launch {
loadedApiKey = apiKey
selectedSpaceId = ""
selectedTypeKey = ""
selectedTemplateId = ""
templates = emptyList()
propertyConfigs.clear()
editingIndex = null
types = emptyList()
loadSpaces(apiKey)
}
}
},
)
IconButton(onClick = {
scope.launch {
loadedApiKey = apiKey
selectedSpaceId = ""
selectedTypeKey = ""
selectedTemplateId = ""
templates = emptyList()
propertyConfigs.clear()
editingIndex = null
types = emptyList()
loadSpaces(apiKey)
}
}) {
Icon(Icons.Default.Refresh, contentDescription = "Reload spaces")
}
}
Spacer(modifier = Modifier.height(12.dp))
// Space dropdown
DropdownSelector(
label = "Space",
selectedValue = spaces.find { it.id == selectedSpaceId }?.name ?: selectedSpaceId,
options = spaces.map { it.id to it.name },
onSelect = { id ->
selectedSpaceId = id
selectedTypeKey = ""
selectedTemplateId = ""
templates = emptyList()
propertyConfigs.clear()
editingIndex = null
scope.launch { loadTypes(apiKey, id) }
},
isLoading = isLoadingSpaces,
// ApiKey
OutlinedTextField(
value = apiKey,
onValueChange = { apiKey = it },
label = { Text("API Key") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
@@ -222,26 +113,12 @@ fun SettingsWindow(
options = types.map { it.key to it.name },
onSelect = { key ->
selectedTypeKey = key
selectedTemplateId = ""
templates = emptyList()
propertyConfigs.clear()
editingIndex = null
},
isLoading = isLoadingTypes,
)
Spacer(modifier = Modifier.height(12.dp))
// Template dropdown
if (selectedTypeKey.isNotEmpty()) {
DropdownSelector(
label = "Template",
selectedValue = templates.find { it.id == selectedTemplateId }?.name ?: selectedTemplateId,
options = listOf("" to "(none)") + templates.map { it.id to it.name },
onSelect = { id -> selectedTemplateId = id },
isLoading = isLoadingTemplates,
)
Spacer(modifier = Modifier.height(16.dp))
}
Spacer(modifier = Modifier.height(16.dp))
// Properties section
Text("Properties", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold)
@@ -364,6 +241,7 @@ private fun PropertyTable(
Text("Name", fontWeight = FontWeight.Bold, modifier = Modifier.weight(2f))
Text("Type", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1.5f))
Text("Ind.", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.7f))
Text("Values", fontWeight = FontWeight.Bold, modifier = Modifier.weight(2f))
Text("Obj Type", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1.5f))
Spacer(modifier = Modifier.width(40.dp))
}
@@ -382,6 +260,11 @@ private fun PropertyTable(
Text(prop.name, modifier = Modifier.weight(2f), maxLines = 1)
Text(prop.format, modifier = Modifier.weight(1.5f), maxLines = 1)
Text(prop.indicator, modifier = Modifier.weight(0.7f), maxLines = 1)
Text(
prop.possibleValues.joinToString(", "),
modifier = Modifier.weight(2f),
maxLines = 1,
)
Text(prop.objectTypeKey, modifier = Modifier.weight(1.5f), maxLines = 1)
IconButton(onClick = { onDelete(index) }) {
Icon(Icons.Default.Delete, contentDescription = "Delete", tint = Color.Gray)
@@ -403,6 +286,7 @@ private fun PropertyEditPanel(
) {
var selectedPropertyKey by remember(existing) { mutableStateOf(existing?.key ?: "") }
var indicator by remember(existing) { mutableStateOf(existing?.indicator ?: "") }
var possibleValuesText by remember(existing) { mutableStateOf(existing?.possibleValues?.joinToString("\n") ?: "") }
var objectTypeKey by remember(existing) { mutableStateOf(existing?.objectTypeKey ?: "") }
val selectedProperty = availableProperties.find { it.key == selectedPropertyKey }
@@ -439,6 +323,15 @@ private fun PropertyEditPanel(
// Format-specific fields
when (format) {
"select", "multi_select" -> {
OutlinedTextField(
value = possibleValuesText,
onValueChange = { possibleValuesText = it },
label = { Text("Possible Values (one per line)") },
modifier = Modifier.fillMaxWidth().height(120.dp),
maxLines = 10,
)
}
"object", "objects" -> {
DropdownSelector(
label = "Object Type",
@@ -454,13 +347,14 @@ private fun PropertyEditPanel(
TextButton(onClick = onCancel) { Text("Cancel") }
Button(
onClick = {
val values = possibleValuesText.lines().map { it.trim() }.filter { it.isNotEmpty() }
onConfirm(
PropertyConfig(
key = selectedPropertyKey,
id = selectedProperty?.id ?: existing?.id ?: "",
name = selectedProperty?.name ?: existing?.name ?: "",
format = format,
indicator = indicator,
possibleValues = values,
objectTypeKey = objectTypeKey,
)
)
@@ -49,7 +49,6 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import ch.ergon.anytask.api.AnyTypeApi
import ch.ergon.anytask.api.dto.SearchObject
import ch.ergon.anytask.api.dto.Tag
import ch.ergon.anytask.config.Configuration
import ch.ergon.anytask.api.dto.buildPropertyValue
import ch.ergon.anytask.config.PropertyConfig
@@ -63,7 +62,6 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val propertyValues = remember { mutableStateMapOf<String, String>() }
val propertyTags = remember { mutableStateMapOf<String, List<Tag>>() }
val focusRequester = remember { FocusRequester() }
@@ -81,7 +79,6 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
title = title.trim(),
details = details.trim(),
typeKey = typeKey,
templateId = config.templateId.ifEmpty { null },
properties = propElements,
)
.onSuccess {
@@ -146,7 +143,6 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
value = propertyValues[prop.key] ?: "",
onValueChange = { propertyValues[prop.key] = it },
client = client,
tags = propertyTags[prop.key] ?: emptyList(),
)
}
}
@@ -177,19 +173,7 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
}
}
LaunchedEffect(isVisible) {
if (isVisible) {
focusRequester.requestFocus()
config.properties
.filter { it.format in listOf("select", "multi_select") && it.id.isNotEmpty() }
.forEach { prop ->
launch {
client.getTags(prop.id)
.onSuccess { propertyTags[prop.key] = it }
}
}
}
}
LaunchedEffect(isVisible) { if (isVisible) focusRequester.requestFocus() }
}
@Composable
@@ -198,14 +182,13 @@ private fun PropertyField(
value: String,
onValueChange: (String) -> Unit,
client: AnyTypeApi,
tags: List<Tag> = emptyList(),
) {
when (prop.format) {
"select", "multi_select" -> {
SelectField(
label = prop.name,
value = value,
tags = tags,
options = prop.possibleValues,
onValueChange = onValueChange,
)
}
@@ -247,17 +230,16 @@ private fun PropertyField(
private fun SelectField(
label: String,
value: String,
tags: List<Tag>,
options: List<String>,
onValueChange: (String) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
val selectedName = tags.find { it.id == value }?.name ?: value
Column {
Text(label, style = MaterialTheme.typography.caption)
Box {
OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
Text(selectedName.ifEmpty { "Select..." })
Text(value.ifEmpty { "Select..." })
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(onClick = {
@@ -266,12 +248,12 @@ private fun SelectField(
}) {
Text("(none)", style = MaterialTheme.typography.caption)
}
tags.forEach { tag ->
options.forEach { option ->
DropdownMenuItem(onClick = {
onValueChange(tag.id)
onValueChange(option)
expanded = false
}) {
Text(tag.name)
Text(option)
}
}
}
+2 -7
View File
@@ -1,17 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="CLR">{FATAL=bright red, ERROR=red, WARN=bright yellow, INFO=Normal, DEBUG=white, TRACE=white}</Property>
<Property name="MSG_CLR">{FATAL=bright red, ERROR=Normal, WARN=Normal, INFO=Normal, DEBUG=Normal, TRACE=white}</Property>
<Property name="LOG_PATTERN">%d{yy-MM-dd HH:mm:ss.SSS} %style{[%t]}{magenta} %highlight{%-5level}${CLR} %style{%logger{36}}{cyan} %highlight{: %msg%n}${MSG_CLR}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false"/>
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="trace">
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.