Compare commits
10
Commits
cd57e97cff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd01936bf6 | ||
|
|
c9541eb618 | ||
|
|
027b49d90e | ||
|
|
6dba8aaa2e | ||
|
|
d74e243cf7 | ||
|
|
af0d8b77a5 | ||
|
|
2434383953 | ||
|
|
27797679ac | ||
|
|
95753f617b | ||
|
|
e8d2408c8e |
No files matched your search
Generated
+6
@@ -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>
|
||||||
@@ -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
@@ -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>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
try {
|
||||||
|
tempClient.getTypes()
|
||||||
.onSuccess { types = it }
|
.onSuccess { types = it }
|
||||||
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
|
.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
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = apiKey,
|
value = apiKey,
|
||||||
onValueChange = { apiKey = it },
|
onValueChange = { apiKey = it },
|
||||||
label = { Text("API Key") },
|
label = { Text("API Key") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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(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
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||