Compare commits
0
Commits
No files matched your search
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="com.nbadal.ktlint.KtlintProjectSettings">
|
|
||||||
<ktlintRulesetVersion>DEFAULT</ktlintRulesetVersion>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -19,7 +19,6 @@ 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")
|
||||||
@@ -40,7 +39,6 @@ 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 {
|
||||||
@@ -49,8 +47,6 @@ 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)
|
||||||
|
|||||||
+9
-22
@@ -1,52 +1,39 @@
|
|||||||
### 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: {{apiVersion}}
|
Anytype-Version: 0.1
|
||||||
|
|
||||||
### Get Type
|
### Get Type
|
||||||
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/{{taskTypeId}}
|
GET http://localhost:31009/v1/spaces/{{spaceId}}/types/bafyreidtsobmbfmf6gl7bfwyd3ik3zs4m7t6e7p46y4lonsz3chkgeeabi
|
||||||
Authorization: Bearer {{apiKey}}
|
Authorization: Bearer {{apiKey}}
|
||||||
Anytype-Version: {{apiVersion}}
|
Anytype-Version: 0.1
|
||||||
|
|
||||||
### 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: {{apiVersion}}
|
Anytype-Version: 0.1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"types": ["project"]
|
"types": ["project"]
|
||||||
}
|
}
|
||||||
|
|
||||||
### Get Property Tags (Values) Priority
|
### Get Property
|
||||||
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/{{priorityPropertyId}}/tags
|
GET http://localhost:31009/v1/spaces/{{spaceId}}/properties/bafyreibs7xr3zphtvjt3t4bk3jhcxlllo6jskrsatnuvkrcs65k3kfuany
|
||||||
Authorization: Bearer {{apiKey}}
|
Authorization: Bearer {{apiKey}}
|
||||||
Anytype-Version: {{apiVersion}}
|
Anytype-Version: 0.1
|
||||||
|
|
||||||
### 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": "With Template - II",
|
"name": "Test Task II",
|
||||||
"body": "And here comes the body",
|
"body": "Created via HTTP client",
|
||||||
"template_id": "bafyreidivup2xrqn4xbgimukkgst3ao24izqnpxdesbkdtnc5owvhf6i6i",
|
|
||||||
"type_key": "ot-task"
|
"type_key": "ot-task"
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"local": {
|
"local": {
|
||||||
"apiKey": "<API-Key>",
|
"apiKey": "<API-Key>",
|
||||||
"apiVersion": "2025-11-08",
|
"spaceId": "<Space-Id>"
|
||||||
"spaceId": "<Space-Id>",
|
|
||||||
"taskTypeId": "<taskTypeId>",
|
|
||||||
"priorityPropertyId": "<priorityPropertyId>"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,19 @@ fun main() {
|
|||||||
onSettings = { isSettingsVisible = true },
|
onSettings = { isSettingsVisible = true },
|
||||||
onExit = { exitApplication() },
|
onExit = { exitApplication() },
|
||||||
)
|
)
|
||||||
logger.info("AnyTask started. Press ${HotkeyManager.DESCRIPTION} to open the task window.")
|
client.getSpaces()
|
||||||
|
.onSuccess { spaces ->
|
||||||
|
logger.info("Available spaces:")
|
||||||
|
spaces.forEach { logger.info("Space: ${it.id} ${it.name}") }
|
||||||
|
val space = spaces.find { space -> space.id == configResult.config.spaceId }
|
||||||
|
if (space == null) {
|
||||||
|
logger.error("Selected space not found: ${configResult.config.spaceId}")
|
||||||
|
exitProcess(-1)
|
||||||
|
} else {
|
||||||
|
logger.info("Selected space: ${space.name} (${space.id})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { logger.error("Failed to load spaces: ${it.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
@@ -117,6 +129,7 @@ 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)
|
||||||
@@ -153,7 +166,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("Create Task").also { it.addActionListener { onShow() } })
|
add(MenuItem("Open").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,20 +1,32 @@
|
|||||||
package ch.ergon.anytask.api
|
package ch.ergon.anytask.api
|
||||||
|
|
||||||
import ch.ergon.anytask.api.dto.*
|
import ch.ergon.anytask.api.dto.CreateObjectRequest
|
||||||
import io.ktor.client.*
|
import ch.ergon.anytask.api.dto.CreateObjectResponse
|
||||||
import io.ktor.client.call.*
|
import ch.ergon.anytask.api.dto.ObjectType
|
||||||
import io.ktor.client.engine.cio.*
|
import ch.ergon.anytask.api.dto.SearchObject
|
||||||
import io.ktor.client.plugins.*
|
import ch.ergon.anytask.api.dto.SearchObjectsResponse
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import ch.ergon.anytask.api.dto.Space
|
||||||
import io.ktor.client.plugins.defaultRequest
|
import ch.ergon.anytask.api.dto.SpacesResponse
|
||||||
import io.ktor.client.request.*
|
import ch.ergon.anytask.api.dto.TypesResponse
|
||||||
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 kotlinx.serialization.json.JsonElement
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.HttpSend
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.client.plugins.plugin
|
||||||
|
import io.ktor.client.request.bearerAuth
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.parameter
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import io.ktor.http.isSuccess
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.apache.logging.log4j.LogManager
|
import org.apache.logging.log4j.LogManager
|
||||||
import kotlin.time.DurationUnit.MILLISECONDS
|
import kotlin.time.DurationUnit.MILLISECONDS
|
||||||
import kotlin.time.toDuration
|
import kotlin.time.toDuration
|
||||||
@@ -33,24 +45,13 @@ class AnyTypeApi(
|
|||||||
|
|
||||||
private val http = HttpClient(CIO) {
|
private val http = HttpClient(CIO) {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json { ignoreUnknownKeys = true; explicitNulls = false })
|
json(Json { ignoreUnknownKeys = true })
|
||||||
}
|
|
||||||
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)
|
||||||
@@ -66,6 +67,7 @@ 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
|
||||||
@@ -74,6 +76,7 @@ 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
|
||||||
@@ -83,45 +86,29 @@ 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? = null,
|
details: String = "",
|
||||||
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?.takeIf { it.isNotBlank() },
|
body = details,
|
||||||
typeKey = typeKey,
|
typeKey = typeKey,
|
||||||
templateId = templateId,
|
|
||||||
properties = properties,
|
properties = properties,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class CreateObjectRequest(
|
data class CreateObjectRequest(
|
||||||
val name: String,
|
val name: String,
|
||||||
val body: String? = null,
|
val body: String,
|
||||||
@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(),
|
||||||
)
|
)
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package ch.ergon.anytask.api.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Tag(
|
|
||||||
val id: String,
|
|
||||||
val key: String = "",
|
|
||||||
val name: String,
|
|
||||||
val color: String = "",
|
|
||||||
)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package ch.ergon.anytask.api.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TagsResponse(
|
|
||||||
val data: List<Tag>,
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package ch.ergon.anytask.api.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Template(
|
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
val archived: Boolean = false,
|
|
||||||
val icon: Icon? = null,
|
|
||||||
val layout: String = "",
|
|
||||||
val `object`: String = "",
|
|
||||||
val properties: List<Property> = emptyList(),
|
|
||||||
val snippet: String = "",
|
|
||||||
@SerialName("space_id") val spaceId: String = "",
|
|
||||||
val type: ObjectType? = null,
|
|
||||||
)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package ch.ergon.anytask.api.dto
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TemplatesResponse(
|
|
||||||
val data: List<Template>,
|
|
||||||
)
|
|
||||||
@@ -17,10 +17,10 @@ data class Connection(
|
|||||||
@Serializable
|
@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,7 +29,6 @@ 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,9 +15,6 @@ 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,7 +30,6 @@ 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
|
||||||
@@ -41,15 +40,12 @@ 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
|
||||||
@@ -58,85 +54,30 @@ 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
|
||||||
|
|
||||||
suspend fun loadSpaces(key: String) {
|
// Load types on start
|
||||||
spaces = emptyList()
|
|
||||||
isLoadingSpaces = true
|
|
||||||
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key)
|
|
||||||
try {
|
|
||||||
tempClient.getSpaces()
|
|
||||||
.onSuccess { spaces = it }
|
|
||||||
.onFailure { errorMessage = "Failed to load spaces: ${it.message}" }
|
|
||||||
} finally {
|
|
||||||
tempClient.close()
|
|
||||||
}
|
|
||||||
isLoadingSpaces = false
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadTypes(key: String, spaceId: String) {
|
|
||||||
types = emptyList()
|
|
||||||
isLoadingTypes = true
|
|
||||||
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId)
|
|
||||||
try {
|
|
||||||
tempClient.getTypes()
|
|
||||||
.onSuccess { types = it }
|
|
||||||
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
|
|
||||||
} finally {
|
|
||||||
tempClient.close()
|
|
||||||
}
|
|
||||||
isLoadingTypes = false
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadTemplates(key: String, spaceId: String, typeId: String) {
|
|
||||||
templates = emptyList()
|
|
||||||
isLoadingTemplates = true
|
|
||||||
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId)
|
|
||||||
try {
|
|
||||||
tempClient.getTemplates(typeId)
|
|
||||||
.onSuccess { templates = it }
|
|
||||||
.onFailure { errorMessage = "Failed to load templates: ${it.message}" }
|
|
||||||
} finally {
|
|
||||||
tempClient.close()
|
|
||||||
}
|
|
||||||
isLoadingTemplates = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
loadSpaces(apiKey)
|
isLoadingTypes = true
|
||||||
if (selectedSpaceId.isNotEmpty()) {
|
client.getTypes()
|
||||||
loadTypes(apiKey, selectedSpaceId)
|
.onSuccess { types = it }
|
||||||
}
|
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
|
||||||
}
|
isLoadingTypes = false
|
||||||
|
|
||||||
// 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 }
|
||||||
@@ -144,9 +85,8 @@ fun SettingsWindow(
|
|||||||
|
|
||||||
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 = selectedSpaceId,
|
spaceId = config.spaceId,
|
||||||
taskTypeKey = selectedTypeKey,
|
taskTypeKey = selectedTypeKey,
|
||||||
templateId = selectedTemplateId,
|
|
||||||
properties = propertyConfigs.toList(),
|
properties = propertyConfigs.toList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,62 +96,13 @@ 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 + reload button
|
// ApiKey
|
||||||
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))
|
||||||
|
|
||||||
@@ -222,26 +113,12 @@ 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))
|
Spacer(modifier = Modifier.height(16.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)
|
||||||
@@ -364,6 +241,7 @@ 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))
|
||||||
}
|
}
|
||||||
@@ -382,6 +260,11 @@ 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)
|
||||||
@@ -403,6 +286,7 @@ 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 }
|
||||||
@@ -439,6 +323,15 @@ 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",
|
||||||
@@ -454,13 +347,14 @@ 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,7 +49,6 @@ 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
|
||||||
@@ -63,7 +62,6 @@ 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() }
|
||||||
|
|
||||||
@@ -81,7 +79,6 @@ 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 {
|
||||||
@@ -146,7 +143,6 @@ 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(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,19 +173,7 @@ fun TaskWindow(client: AnyTypeApi, config: Configuration, isVisible: Boolean, on
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(isVisible) {
|
LaunchedEffect(isVisible) { if (isVisible) focusRequester.requestFocus() }
|
||||||
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
|
||||||
@@ -198,14 +182,13 @@ 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,
|
||||||
tags = tags,
|
options = prop.possibleValues,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -247,17 +230,16 @@ private fun PropertyField(
|
|||||||
private fun SelectField(
|
private fun SelectField(
|
||||||
label: String,
|
label: String,
|
||||||
value: String,
|
value: String,
|
||||||
tags: List<Tag>,
|
options: List<String>,
|
||||||
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(selectedName.ifEmpty { "Select..." })
|
Text(value.ifEmpty { "Select..." })
|
||||||
}
|
}
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
||||||
@@ -266,12 +248,12 @@ private fun SelectField(
|
|||||||
}) {
|
}) {
|
||||||
Text("(none)", style = MaterialTheme.typography.caption)
|
Text("(none)", style = MaterialTheme.typography.caption)
|
||||||
}
|
}
|
||||||
tags.forEach { tag ->
|
options.forEach { option ->
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
||||||
onValueChange(tag.id)
|
onValueChange(option)
|
||||||
expanded = false
|
expanded = false
|
||||||
}) {
|
}) {
|
||||||
Text(tag.name)
|
Text(option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
<?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="${LOG_PATTERN}" disableAnsi="false"/>
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
</Console>
|
</Console>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="trace">
|
<Root level="info">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
</Root>
|
</Root>
|
||||||
</Loggers>
|
</Loggers>
|
||||||
|
|||||||