Compare commits

...
10 Commits
Author SHA1 Message Date
boxi bd01936bf6 cleanUp log 2026-05-15 23:54:06 +02:00
boxi c9541eb618 cleanUp HTTP-Client-Calls 2026-05-15 20:49:49 +02:00
boxi 027b49d90e Merge remote-tracking branch 'origin/main'
# Conflicts:
#	httpClient/anyType.http
2026-05-15 20:48:40 +02:00
boxi 6dba8aaa2e fixed API 2026-05-15 20:45:58 +02:00
boxi d74e243cf7 fixed API 2026-05-15 20:39:49 +02:00
Thomas Boksberger af0d8b77a5 load selectValues from Tags-endpoint 2026-04-07 07:56:38 +02:00
boxi 2434383953 init Template use 2026-04-05 20:50:14 +02:00
boxi 27797679ac enhance httpClientFiles 2026-04-04 00:34:26 +02:00
boxi 95753f617b fixed inital Config 2026-04-04 00:01:12 +02:00
boxi e8d2408c8e fixed inital Config 2026-04-04 00:00:37 +02:00
16 changed files with 307 additions and 102 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
<?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,6 +19,7 @@ repositories {
dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.materialIconsExtended)
// Global hotkeys
implementation("com.github.kwhat:jnativehook:2.2.2")
@@ -39,6 +40,7 @@ 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 {
@@ -47,6 +49,8 @@ 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)
+22 -9
View File
@@ -1,39 +1,52 @@
### 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: 0.1
Anytype-Version: {{apiVersion}}
### Get Type
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/bafyreidtsobmbfmf6gl7bfwyd3ik3zs4m7t6e7p46y4lonsz3chkgeeabi
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/{{taskTypeId}}
Authorization: Bearer {{apiKey}}
Anytype-Version: 0.1
Anytype-Version: {{apiVersion}}
### Get objects of type
POST http://localhost:31009/v1/spaces/{{spaceId}}/search?type=project
Authorization: Bearer {{apiKey}}
Anytype-Version: 0.1
Anytype-Version: {{apiVersion}}
Content-Type: application/json
{
"types": ["project"]
}
### Get Property
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/bafyreibs7xr3zphtvjt3t4bk3jhcxlllo6jskrsatnuvkrcs65k3kfuany
### Get Property Tags (Values) Priority
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/{{priorityPropertyId}}/tags
Authorization: Bearer {{apiKey}}
Anytype-Version: 0.1
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}}
### Create Task
POST http://localhost:31009/v1/spaces/{{spaceId}}/objects
Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Content-Type: application/json
{
"name": "Test Task II",
"body": "Created via HTTP client",
"name": "With Template - II",
"body": "And here comes the body",
"template_id": "bafyreidivup2xrqn4xbgimukkgst3ao24izqnpxdesbkdtnc5owvhf6i6i",
"type_key": "ot-task"
}
@@ -1,6 +1,9 @@
{
"local": {
"apiKey": "<API-Key>",
"spaceId": "<Space-Id>"
"apiVersion": "2025-11-08",
"spaceId": "<Space-Id>",
"taskTypeId": "<taskTypeId>",
"priorityPropertyId": "<priorityPropertyId>"
}
}
+2 -15
View File
@@ -65,19 +65,7 @@ fun main() {
onSettings = { isSettingsVisible = true },
onExit = { exitApplication() },
)
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}") }
logger.info("AnyTask started. Press ${HotkeyManager.DESCRIPTION} to open the task window.")
}
DisposableEffect(Unit) {
@@ -129,7 +117,6 @@ fun main() {
}
SettingsWindow(
config = configResult.config,
client = client,
onSave = { newConfig ->
saveConfig(newConfig, configResult.sourceFile)
configResult = ConfigResult(newConfig, configResult.sourceFile)
@@ -166,7 +153,7 @@ private fun setupSystemTray(onShow: () -> Unit, onSettings: () -> Unit, onExit:
val icon = createTrayIcon(size)
val menu = PopupMenu().apply {
add(MenuItem("Open").also { it.addActionListener { onShow() } })
add(MenuItem("Create Task").also { it.addActionListener { onShow() } })
add(MenuItem("Settings").also { it.addActionListener { onSettings() } })
addSeparator()
add(MenuItem("Quit").also { it.addActionListener { onExit() } })
@@ -1,32 +1,20 @@
package ch.ergon.anytask.api
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 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 kotlinx.serialization.json.JsonElement
import org.apache.logging.log4j.LogManager
import kotlin.time.DurationUnit.MILLISECONDS
import kotlin.time.toDuration
@@ -45,13 +33,24 @@ class AnyTypeApi(
private val http = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
json(Json { ignoreUnknownKeys = true; explicitNulls = false })
}
defaultRequest {
header("Anytype-Version", "2025-11-08")
}
}.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)
@@ -67,7 +66,6 @@ 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
@@ -76,7 +74,6 @@ 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
@@ -86,29 +83,45 @@ 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 = "",
details: String? = null,
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,
body = details?.takeIf { it.isNotBlank() },
typeKey = typeKey,
templateId = templateId,
properties = properties,
)
)
@@ -7,7 +7,8 @@ import kotlinx.serialization.json.JsonElement
@Serializable
data class CreateObjectRequest(
val name: String,
val body: String,
val body: String? = null,
@SerialName("type_key") val typeKey: String,
@SerialName("template_id") val templateId: String? = null,
val properties: List<JsonElement> = emptyList(),
)
@@ -0,0 +1,11 @@
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 = "",
)
@@ -0,0 +1,8 @@
package ch.ergon.anytask.api.dto
import kotlinx.serialization.Serializable
@Serializable
data class TagsResponse(
val data: List<Tag>,
)
@@ -0,0 +1,18 @@
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,
)
@@ -0,0 +1,8 @@
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,6 +29,7 @@ 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,6 +15,9 @@ 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,6 +30,7 @@ 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
@@ -40,12 +41,15 @@ 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
@@ -54,39 +58,95 @@ 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
// Load types on start
LaunchedEffect(Unit) {
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
client.getTypes()
.onSuccess { types = it }
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
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
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)
}
}
val selectedType = types.find { it.key == selectedTypeKey }
val availableProperties = selectedType?.properties.orEmpty()
fun buildConfig(): Configuration = Configuration(
connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl),
spaceId = config.spaceId,
spaceId = selectedSpaceId,
taskTypeKey = selectedTypeKey,
templateId = selectedTemplateId,
properties = propertyConfigs.toList(),
)
@@ -96,13 +156,62 @@ fun SettingsWindow(
Text("Settings", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(16.dp))
// ApiKey
OutlinedTextField(
value = apiKey,
onValueChange = { apiKey = it },
label = { Text("API Key") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
// 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,
)
Spacer(modifier = Modifier.height(12.dp))
@@ -113,12 +222,26 @@ 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(16.dp))
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))
}
// Properties section
Text("Properties", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold)
@@ -241,7 +364,6 @@ 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))
}
@@ -260,11 +382,6 @@ 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)
@@ -286,7 +403,6 @@ 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 }
@@ -323,15 +439,6 @@ 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",
@@ -347,14 +454,13 @@ 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,6 +49,7 @@ 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
@@ -62,6 +63,7 @@ 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() }
@@ -79,6 +81,7 @@ 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 {
@@ -143,6 +146,7 @@ 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(),
)
}
}
@@ -173,7 +177,19 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
}
}
LaunchedEffect(isVisible) { if (isVisible) focusRequester.requestFocus() }
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 }
}
}
}
}
}
@Composable
@@ -182,13 +198,14 @@ 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,
options = prop.possibleValues,
tags = tags,
onValueChange = onValueChange,
)
}
@@ -230,16 +247,17 @@ private fun PropertyField(
private fun SelectField(
label: String,
value: String,
options: List<String>,
tags: List<Tag>,
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(value.ifEmpty { "Select..." })
Text(selectedName.ifEmpty { "Select..." })
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(onClick = {
@@ -248,12 +266,12 @@ private fun SelectField(
}) {
Text("(none)", style = MaterialTheme.typography.caption)
}
options.forEach { option ->
tags.forEach { tag ->
DropdownMenuItem(onClick = {
onValueChange(option)
onValueChange(tag.id)
expanded = false
}) {
Text(option)
Text(tag.name)
}
}
}
+7 -2
View File
@@ -1,12 +1,17 @@
<?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="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<Root level="trace">
<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.