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 { dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation(compose.materialIconsExtended)
// Global hotkeys // Global hotkeys
implementation("com.github.kwhat:jnativehook:2.2.2") implementation("com.github.kwhat:jnativehook:2.2.2")
@@ -39,6 +40,7 @@ dependencies {
// Logging // Logging
implementation("org.apache.logging.log4j:log4j-api:2.24.3") 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-core:2.24.3")
implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3")
} }
compose.desktop { compose.desktop {
@@ -47,6 +49,8 @@ compose.desktop {
// Allow access to AWT Classes to control the window // Allow access to AWT Classes to control the window
jvmArgs("--add-opens", "java.desktop/com.apple.eawt=ALL-UNNAMED") 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 { nativeDistributions {
targetFormats(TargetFormat.Dmg) targetFormats(TargetFormat.Dmg)
+22 -9
View File
@@ -1,39 +1,52 @@
### Get Spaces ### Get Spaces
GET http://localhost:31009/v1/spaces GET http://localhost:31009/v1/spaces
Authorization: Bearer {{apiKey}} Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
### Get Types ### Get Types
GET http://localhost:31009/v1/spaces/{{spaceId}}/types GET http://localhost:31009/v1/spaces/{{spaceId}}/types
Authorization: Bearer {{apiKey}} Authorization: Bearer {{apiKey}}
Anytype-Version: 0.1 Anytype-Version: {{apiVersion}}
### Get Type ### Get Type
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/bafyreidtsobmbfmf6gl7bfwyd3ik3zs4m7t6e7p46y4lonsz3chkgeeabi GET http://localhost:31009/v1/spaces/{{spaceId}}/types/{{taskTypeId}}
Authorization: Bearer {{apiKey}} Authorization: Bearer {{apiKey}}
Anytype-Version: 0.1 Anytype-Version: {{apiVersion}}
### Get objects of type ### Get objects of type
POST http://localhost:31009/v1/spaces/{{spaceId}}/search?type=project POST http://localhost:31009/v1/spaces/{{spaceId}}/search?type=project
Authorization: Bearer {{apiKey}} Authorization: Bearer {{apiKey}}
Anytype-Version: 0.1 Anytype-Version: {{apiVersion}}
Content-Type: application/json Content-Type: application/json
{ {
"types": ["project"] "types": ["project"]
} }
### Get Property ### Get Property Tags (Values) Priority
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/bafyreibs7xr3zphtvjt3t4bk3jhcxlllo6jskrsatnuvkrcs65k3kfuany GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/{{priorityPropertyId}}/tags
Authorization: Bearer {{apiKey}} 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 ### Create Task
POST http://localhost:31009/v1/spaces/{{spaceId}}/objects POST http://localhost:31009/v1/spaces/{{spaceId}}/objects
Authorization: Bearer {{apiKey}} Authorization: Bearer {{apiKey}}
Anytype-Version: {{apiVersion}}
Content-Type: application/json Content-Type: application/json
{ {
"name": "Test Task II", "name": "With Template - II",
"body": "Created via HTTP client", "body": "And here comes the body",
"template_id": "bafyreidivup2xrqn4xbgimukkgst3ao24izqnpxdesbkdtnc5owvhf6i6i",
"type_key": "ot-task" "type_key": "ot-task"
} }
@@ -1,6 +1,9 @@
{ {
"local": { "local": {
"apiKey": "<API-Key>", "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 }, onSettings = { isSettingsVisible = true },
onExit = { exitApplication() }, onExit = { exitApplication() },
) )
client.getSpaces() logger.info("AnyTask started. Press ${HotkeyManager.DESCRIPTION} to open the task window.")
.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) { DisposableEffect(Unit) {
@@ -129,7 +117,6 @@ fun main() {
} }
SettingsWindow( SettingsWindow(
config = configResult.config, config = configResult.config,
client = client,
onSave = { newConfig -> onSave = { newConfig ->
saveConfig(newConfig, configResult.sourceFile) saveConfig(newConfig, configResult.sourceFile)
configResult = ConfigResult(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 icon = createTrayIcon(size)
val menu = PopupMenu().apply { 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() } }) add(MenuItem("Settings").also { it.addActionListener { onSettings() } })
addSeparator() addSeparator()
add(MenuItem("Quit").also { it.addActionListener { onExit() } }) add(MenuItem("Quit").also { it.addActionListener { onExit() } })
@@ -1,32 +1,20 @@
package ch.ergon.anytask.api package ch.ergon.anytask.api
import ch.ergon.anytask.api.dto.CreateObjectRequest import ch.ergon.anytask.api.dto.*
import ch.ergon.anytask.api.dto.CreateObjectResponse import io.ktor.client.*
import ch.ergon.anytask.api.dto.ObjectType import io.ktor.client.call.*
import ch.ergon.anytask.api.dto.SearchObject import io.ktor.client.engine.cio.*
import ch.ergon.anytask.api.dto.SearchObjectsResponse import io.ktor.client.plugins.*
import ch.ergon.anytask.api.dto.Space import io.ktor.client.plugins.contentnegotiation.*
import ch.ergon.anytask.api.dto.SpacesResponse import io.ktor.client.plugins.defaultRequest
import ch.ergon.anytask.api.dto.TypesResponse import io.ktor.client.request.*
import kotlinx.serialization.json.JsonElement import io.ktor.client.statement.*
import io.ktor.client.HttpClient import io.ktor.http.*
import io.ktor.client.call.body import io.ktor.http.content.OutgoingContent
import io.ktor.client.engine.cio.CIO import io.ktor.http.content.TextContent
import io.ktor.client.plugins.HttpSend import io.ktor.serialization.kotlinx.json.*
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import kotlin.time.DurationUnit.MILLISECONDS import kotlin.time.DurationUnit.MILLISECONDS
import kotlin.time.toDuration import kotlin.time.toDuration
@@ -45,13 +33,24 @@ class AnyTypeApi(
private val http = HttpClient(CIO) { private val http = HttpClient(CIO) {
install(ContentNegotiation) { install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true }) json(Json { ignoreUnknownKeys = true; explicitNulls = false })
}
defaultRequest {
header("Anytype-Version", "2025-11-08")
} }
}.also { client -> }.also { client ->
client.plugin(HttpSend).intercept { request -> client.plugin(HttpSend).intercept { request ->
val method = request.method.value val method = request.method.value
val url = request.url.buildString() val url = request.url.buildString()
logger.info("→ $method $url") 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 start = System.currentTimeMillis()
val call = execute(request) val call = execute(request)
val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS) val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS)
@@ -67,7 +66,6 @@ class AnyTypeApi(
suspend fun getSpaces(): Result<List<Space>> = suspend fun getSpaces(): Result<List<Space>> =
runCatching { runCatching {
val response = http.get("$baseUrl/v1/spaces") { val response = http.get("$baseUrl/v1/spaces") {
header("Anytype-Version", "0.1")
bearerAuth(apiKey) bearerAuth(apiKey)
} }
response.body<SpacesResponse>().data response.body<SpacesResponse>().data
@@ -76,7 +74,6 @@ class AnyTypeApi(
suspend fun getTypes(): Result<List<ObjectType>> = suspend fun getTypes(): Result<List<ObjectType>> =
runCatching { runCatching {
val response = http.get("$baseUrl/v1/spaces/$spaceId/types") { val response = http.get("$baseUrl/v1/spaces/$spaceId/types") {
header("Anytype-Version", "0.1")
bearerAuth(apiKey) bearerAuth(apiKey)
} }
response.body<TypesResponse>().data response.body<TypesResponse>().data
@@ -86,29 +83,45 @@ class AnyTypeApi(
runCatching { runCatching {
val response = http.post("$baseUrl/v1/spaces/$spaceId/search") { val response = http.post("$baseUrl/v1/spaces/$spaceId/search") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
header("Anytype-Version", "0.1")
bearerAuth(apiKey) bearerAuth(apiKey)
setBody(mapOf("types" to listOf(typeKey))) setBody(mapOf("types" to listOf(typeKey)))
} }
response.body<SearchObjectsResponse>().data 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( suspend fun createObject(
title: String, title: String,
details: String = "", details: String? = null,
typeKey: String, typeKey: String,
templateId: String? = null,
properties: List<JsonElement> = emptyList(), properties: List<JsonElement> = emptyList(),
): Result<String> = ): Result<String> =
runCatching { runCatching {
val response = http.post("$baseUrl/v1/spaces/$spaceId/objects") { val response = http.post("$baseUrl/v1/spaces/$spaceId/objects") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
header("Anytype-Version", "0.1")
bearerAuth(apiKey) bearerAuth(apiKey)
setBody( setBody(
CreateObjectRequest( CreateObjectRequest(
name = title, name = title,
body = details, body = details?.takeIf { it.isNotBlank() },
typeKey = typeKey, typeKey = typeKey,
templateId = templateId,
properties = properties, properties = properties,
) )
) )
@@ -7,7 +7,8 @@ import kotlinx.serialization.json.JsonElement
@Serializable @Serializable
data class CreateObjectRequest( data class CreateObjectRequest(
val name: String, val name: String,
val body: String, val body: String? = null,
@SerialName("type_key") val typeKey: String, @SerialName("type_key") val typeKey: String,
@SerialName("template_id") val templateId: String? = null,
val properties: List<JsonElement> = emptyList(), 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 @Serializable
data class PropertyConfig( data class PropertyConfig(
val key: String, val key: String,
val id: String = "",
val name: String = "", val name: String = "",
val format: String = "", val format: String = "",
val indicator: String = "", val indicator: String = "",
@SerialName("possible_values") val possibleValues: List<String> = emptyList(),
@SerialName("object_type_key") val objectTypeKey: String = "", @SerialName("object_type_key") val objectTypeKey: String = "",
) )
@@ -29,6 +29,7 @@ data class Configuration(
val connection: Connection = Connection(), val connection: Connection = Connection(),
val spaceId: String = "", val spaceId: String = "",
@SerialName("task_type_key") val taskTypeKey: String = "", @SerialName("task_type_key") val taskTypeKey: String = "",
@SerialName("template_id") val templateId: String = "",
val properties: List<PropertyConfig> = emptyList(), val properties: List<PropertyConfig> = emptyList(),
) )
@@ -15,6 +15,9 @@ import java.util.logging.Logger
*/ */
object HotkeyManager { 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 val logger = LogManager.getLogger(HotkeyManager::class.java)
private var listener: NativeKeyListener? = null 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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -40,12 +41,15 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.ergon.anytask.api.AnyTypeApi import ch.ergon.anytask.api.AnyTypeApi
import ch.ergon.anytask.api.dto.ObjectType import ch.ergon.anytask.api.dto.ObjectType
import ch.ergon.anytask.api.dto.Property 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.Configuration
import ch.ergon.anytask.config.Connection import ch.ergon.anytask.config.Connection
import ch.ergon.anytask.config.PropertyConfig import ch.ergon.anytask.config.PropertyConfig
@@ -54,39 +58,95 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun SettingsWindow( fun SettingsWindow(
config: Configuration, config: Configuration,
client: AnyTypeApi,
onSave: (Configuration) -> Unit, onSave: (Configuration) -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var apiKey by remember { mutableStateOf(config.connection.apiKey) } 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 selectedTypeKey by remember { mutableStateOf(config.taskTypeKey) }
var selectedTemplateId by remember { mutableStateOf(config.templateId) }
val propertyConfigs = remember { mutableStateListOf<PropertyConfig>().also { it.addAll(config.properties) } } 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 types by remember { mutableStateOf<List<ObjectType>>(emptyList()) }
var isLoadingTypes by remember { mutableStateOf(false) } 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) } var errorMessage by remember { mutableStateOf<String?>(null) }
// Property edit state // Property edit state
var editingIndex by remember { mutableStateOf<Int?>(null) } // null=none, -1=new var editingIndex by remember { mutableStateOf<Int?>(null) } // null=none, -1=new
// Load types on start suspend fun loadSpaces(key: String) {
LaunchedEffect(Unit) { 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 isLoadingTypes = true
client.getTypes() val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId)
.onSuccess { types = it } try {
.onFailure { errorMessage = "Failed to load types: ${it.message}" } tempClient.getTypes()
.onSuccess { types = it }
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
} finally {
tempClient.close()
}
isLoadingTypes = false 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 selectedType = types.find { it.key == selectedTypeKey }
val availableProperties = selectedType?.properties.orEmpty() val availableProperties = selectedType?.properties.orEmpty()
fun buildConfig(): Configuration = Configuration( fun buildConfig(): Configuration = Configuration(
connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl), connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl),
spaceId = config.spaceId, spaceId = selectedSpaceId,
taskTypeKey = selectedTypeKey, taskTypeKey = selectedTypeKey,
templateId = selectedTemplateId,
properties = propertyConfigs.toList(), properties = propertyConfigs.toList(),
) )
@@ -96,13 +156,62 @@ fun SettingsWindow(
Text("Settings", style = MaterialTheme.typography.h6) Text("Settings", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// ApiKey // ApiKey + reload button
OutlinedTextField( Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
value = apiKey, OutlinedTextField(
onValueChange = { apiKey = it }, value = apiKey,
label = { Text("API Key") }, onValueChange = { apiKey = it },
singleLine = true, label = { Text("API Key") },
modifier = Modifier.fillMaxWidth(), 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)) Spacer(modifier = Modifier.height(12.dp))
@@ -113,12 +222,26 @@ fun SettingsWindow(
options = types.map { it.key to it.name }, options = types.map { it.key to it.name },
onSelect = { key -> onSelect = { key ->
selectedTypeKey = key selectedTypeKey = key
selectedTemplateId = ""
templates = emptyList()
propertyConfigs.clear() propertyConfigs.clear()
editingIndex = null editingIndex = null
}, },
isLoading = isLoadingTypes, 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 // Properties section
Text("Properties", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold) 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("Name", fontWeight = FontWeight.Bold, modifier = Modifier.weight(2f))
Text("Type", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1.5f)) Text("Type", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1.5f))
Text("Ind.", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.7f)) 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)) Text("Obj Type", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1.5f))
Spacer(modifier = Modifier.width(40.dp)) Spacer(modifier = Modifier.width(40.dp))
} }
@@ -260,11 +382,6 @@ private fun PropertyTable(
Text(prop.name, modifier = Modifier.weight(2f), maxLines = 1) Text(prop.name, modifier = Modifier.weight(2f), maxLines = 1)
Text(prop.format, modifier = Modifier.weight(1.5f), maxLines = 1) Text(prop.format, modifier = Modifier.weight(1.5f), maxLines = 1)
Text(prop.indicator, modifier = Modifier.weight(0.7f), 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) Text(prop.objectTypeKey, modifier = Modifier.weight(1.5f), maxLines = 1)
IconButton(onClick = { onDelete(index) }) { IconButton(onClick = { onDelete(index) }) {
Icon(Icons.Default.Delete, contentDescription = "Delete", tint = Color.Gray) 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 selectedPropertyKey by remember(existing) { mutableStateOf(existing?.key ?: "") }
var indicator by remember(existing) { mutableStateOf(existing?.indicator ?: "") } 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 ?: "") } var objectTypeKey by remember(existing) { mutableStateOf(existing?.objectTypeKey ?: "") }
val selectedProperty = availableProperties.find { it.key == selectedPropertyKey } val selectedProperty = availableProperties.find { it.key == selectedPropertyKey }
@@ -323,15 +439,6 @@ private fun PropertyEditPanel(
// Format-specific fields // Format-specific fields
when (format) { 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" -> { "object", "objects" -> {
DropdownSelector( DropdownSelector(
label = "Object Type", label = "Object Type",
@@ -347,14 +454,13 @@ private fun PropertyEditPanel(
TextButton(onClick = onCancel) { Text("Cancel") } TextButton(onClick = onCancel) { Text("Cancel") }
Button( Button(
onClick = { onClick = {
val values = possibleValuesText.lines().map { it.trim() }.filter { it.isNotEmpty() }
onConfirm( onConfirm(
PropertyConfig( PropertyConfig(
key = selectedPropertyKey, key = selectedPropertyKey,
id = selectedProperty?.id ?: existing?.id ?: "",
name = selectedProperty?.name ?: existing?.name ?: "", name = selectedProperty?.name ?: existing?.name ?: "",
format = format, format = format,
indicator = indicator, indicator = indicator,
possibleValues = values,
objectTypeKey = objectTypeKey, objectTypeKey = objectTypeKey,
) )
) )
@@ -49,6 +49,7 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.ergon.anytask.api.AnyTypeApi import ch.ergon.anytask.api.AnyTypeApi
import ch.ergon.anytask.api.dto.SearchObject import ch.ergon.anytask.api.dto.SearchObject
import ch.ergon.anytask.api.dto.Tag
import ch.ergon.anytask.config.Configuration import ch.ergon.anytask.config.Configuration
import ch.ergon.anytask.api.dto.buildPropertyValue import ch.ergon.anytask.api.dto.buildPropertyValue
import ch.ergon.anytask.config.PropertyConfig 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 isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
val propertyValues = remember { mutableStateMapOf<String, String>() } val propertyValues = remember { mutableStateMapOf<String, String>() }
val propertyTags = remember { mutableStateMapOf<String, List<Tag>>() }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -79,6 +81,7 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
title = title.trim(), title = title.trim(),
details = details.trim(), details = details.trim(),
typeKey = typeKey, typeKey = typeKey,
templateId = config.templateId.ifEmpty { null },
properties = propElements, properties = propElements,
) )
.onSuccess { .onSuccess {
@@ -143,6 +146,7 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
value = propertyValues[prop.key] ?: "", value = propertyValues[prop.key] ?: "",
onValueChange = { propertyValues[prop.key] = it }, onValueChange = { propertyValues[prop.key] = it },
client = client, 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 @Composable
@@ -182,13 +198,14 @@ private fun PropertyField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
client: AnyTypeApi, client: AnyTypeApi,
tags: List<Tag> = emptyList(),
) { ) {
when (prop.format) { when (prop.format) {
"select", "multi_select" -> { "select", "multi_select" -> {
SelectField( SelectField(
label = prop.name, label = prop.name,
value = value, value = value,
options = prop.possibleValues, tags = tags,
onValueChange = onValueChange, onValueChange = onValueChange,
) )
} }
@@ -230,16 +247,17 @@ private fun PropertyField(
private fun SelectField( private fun SelectField(
label: String, label: String,
value: String, value: String,
options: List<String>, tags: List<Tag>,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val selectedName = tags.find { it.id == value }?.name ?: value
Column { Column {
Text(label, style = MaterialTheme.typography.caption) Text(label, style = MaterialTheme.typography.caption)
Box { Box {
OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) { OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
Text(value.ifEmpty { "Select..." }) Text(selectedName.ifEmpty { "Select..." })
} }
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
@@ -248,12 +266,12 @@ private fun SelectField(
}) { }) {
Text("(none)", style = MaterialTheme.typography.caption) Text("(none)", style = MaterialTheme.typography.caption)
} }
options.forEach { option -> tags.forEach { tag ->
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
onValueChange(option) onValueChange(tag.id)
expanded = false expanded = false
}) { }) {
Text(option) Text(tag.name)
} }
} }
} }
+7 -2
View File
@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN"> <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> <Appenders>
<Console name="Console" target="SYSTEM_OUT"> <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> </Console>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="info"> <Root level="trace">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Root> </Root>
</Loggers> </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.