Skip to content

Commit 2dbcbdf

Browse files
authored
Merge pull request #465 from theleftbit/fix-error-description
Fix error description
2 parents f6b92c4 + aef0acc commit 2dbcbdf

File tree

4 files changed

+385
-26
lines changed

4 files changed

+385
-26
lines changed

Sources/BSWInterfaceKit/Skip/BSWAsyncButton.kt

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,21 +117,6 @@ fun ProvideAsyncButtonOperationIdentifierKey(
117117
)
118118
}
119119

120-
fun normalizeAsyncButtonErrorMessage(raw: String?): String {
121-
if (raw.isNullOrBlank()) return "Something went wrong"
122-
val trimmed = raw.trim()
123-
124-
val optionalQuoted = Regex("""Optional\("(.+)"\)""")
125-
.find(trimmed)?.groupValues?.getOrNull(1)
126-
if (!optionalQuoted.isNullOrBlank()) return optionalQuoted
127-
128-
val optionalPlain = Regex("""Optional\((.+)\)""")
129-
.find(trimmed)?.groupValues?.getOrNull(1)
130-
if (!optionalPlain.isNullOrBlank()) return optionalPlain.trim().trim('"')
131-
132-
return trimmed
133-
}
134-
135120
/**
136121
* Shared async state holder used by Android button wrappers.
137122
*
@@ -173,7 +158,7 @@ class AsyncButtonController internal constructor(
173158
fun rememberAsyncButtonController(
174159
action: suspend () -> Unit,
175160
errorMessageResolver: (Throwable?) -> String = { throwable ->
176-
normalizeAsyncButtonErrorMessage(throwable?.localizedMessage ?: throwable?.message)
161+
normalizeAsyncButtonErrorMessage(throwable)
177162
}
178163
): AsyncButtonController {
179164
val loadingConfig = LocalAsyncButtonLoadingConfiguration.current
@@ -283,7 +268,7 @@ fun BSWAsyncButton(
283268
disableTextColor: Color? = null,
284269
action: suspend () -> Unit,
285270
errorMessageResolver: (Throwable?) -> String = { throwable ->
286-
normalizeAsyncButtonErrorMessage(throwable?.localizedMessage ?: throwable?.message)
271+
normalizeAsyncButtonErrorMessage(throwable)
287272
},
288273
progressView: @Composable (BSWAsyncButtonLoadingConfiguration.Style) -> Unit = { style ->
289274
DefaultAsyncButtonProgressView(style = style)

Sources/BSWInterfaceKit/Skip/BSWAsyncView.kt

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,10 @@ fun BSWDefaultAsyncErrorView(
208208
}
209209

210210
private fun Throwable.toDisplayMessage(): String {
211-
val preferred = localizedMessage ?: message
212-
val fallback = preferred ?: toString()
213-
214-
val optionalRegex = Regex("""errorDescription:\s*Optional\("(.+)"\)""")
215-
optionalRegex.find(fallback)?.groupValues?.getOrNull(1)?.let { extracted ->
216-
if (extracted.isNotBlank()) return extracted
217-
}
218-
219-
return fallback
211+
return extractAsyncButtonErrorMessage(this)
212+
?: localizedMessage
213+
?: message
214+
?: toString()
220215
}
221216

222217
/**
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package bswinterface.kit
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.material3.AlertDialog
8+
import androidx.compose.material3.CircularProgressIndicator
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Surface
11+
import androidx.compose.material3.Text
12+
import androidx.compose.material3.TextButton
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.LaunchedEffect
15+
import androidx.compose.runtime.Stable
16+
import androidx.compose.runtime.getValue
17+
import androidx.compose.runtime.mutableLongStateOf
18+
import androidx.compose.runtime.mutableStateOf
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.rememberCoroutineScope
21+
import androidx.compose.runtime.setValue
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.text.style.TextAlign
25+
import androidx.compose.ui.unit.dp
26+
import androidx.compose.ui.window.Dialog
27+
import androidx.compose.ui.window.DialogProperties
28+
import kotlinx.coroutines.CancellationException
29+
import kotlinx.coroutines.launch
30+
31+
sealed interface AsyncBlockingTaskConfirmationStrategy {
32+
data object NotRequired : AsyncBlockingTaskConfirmationStrategy
33+
34+
data class ConfirmWith(
35+
val title: String,
36+
val message: String,
37+
val confirmButtonTitle: String,
38+
val cancelButtonTitle: String,
39+
val isDestructiveAction: Boolean = false
40+
) : AsyncBlockingTaskConfirmationStrategy
41+
}
42+
43+
internal data class BlockingTaskRequest<Input>(
44+
val id: Long,
45+
val input: Input
46+
)
47+
48+
@Stable
49+
class BlockingTaskState<Input> internal constructor() {
50+
private var requestID by mutableLongStateOf(0L)
51+
52+
internal var pendingRequest by mutableStateOf<BlockingTaskRequest<Input>?>(null)
53+
internal var isRunning by mutableStateOf(false)
54+
internal var errorText by mutableStateOf<String?>(null)
55+
56+
fun trigger(input: Input) {
57+
requestID += 1
58+
pendingRequest = BlockingTaskRequest(id = requestID, input = input)
59+
}
60+
61+
fun dismissError() {
62+
errorText = null
63+
}
64+
}
65+
66+
@Composable
67+
fun <Input> rememberBlockingTaskState(): BlockingTaskState<Input> {
68+
return remember { BlockingTaskState() }
69+
}
70+
71+
@Composable
72+
fun <Input> PerformBlockingTask(
73+
state: BlockingTaskState<Input>,
74+
loadingTitle: String,
75+
confirmationStrategy: AsyncBlockingTaskConfirmationStrategy = AsyncBlockingTaskConfirmationStrategy.NotRequired,
76+
errorMessage: (Throwable?) -> String = { throwable -> normalizeAsyncButtonErrorMessage(throwable) },
77+
task: suspend (Input) -> Unit,
78+
content: @Composable () -> Unit
79+
) {
80+
PerformBlockingTaskHost(
81+
state = state,
82+
loadingTitle = loadingTitle,
83+
confirmationStrategy = confirmationStrategy,
84+
errorMessage = errorMessage,
85+
task = task
86+
)
87+
content()
88+
}
89+
90+
@Composable
91+
fun <Input> PerformBlockingTaskHost(
92+
state: BlockingTaskState<Input>,
93+
loadingTitle: String,
94+
confirmationStrategy: AsyncBlockingTaskConfirmationStrategy = AsyncBlockingTaskConfirmationStrategy.NotRequired,
95+
errorMessage: (Throwable?) -> String = { throwable -> normalizeAsyncButtonErrorMessage(throwable) },
96+
task: suspend (Input) -> Unit
97+
) {
98+
val scope = rememberCoroutineScope()
99+
var confirmationRequest by remember(state) { mutableStateOf<BlockingTaskRequest<Input>?>(null) }
100+
101+
fun launchTask(request: BlockingTaskRequest<Input>) {
102+
state.isRunning = true
103+
state.errorText = null
104+
scope.launch {
105+
try {
106+
task(request.input)
107+
} catch (cancellationException: CancellationException) {
108+
throw cancellationException
109+
} catch (throwable: Throwable) {
110+
state.errorText = errorMessage(throwable)
111+
} finally {
112+
state.isRunning = false
113+
}
114+
}
115+
}
116+
117+
LaunchedEffect(state.pendingRequest?.id, state.isRunning, confirmationStrategy) {
118+
val request = state.pendingRequest ?: return@LaunchedEffect
119+
if (state.isRunning) return@LaunchedEffect
120+
121+
state.pendingRequest = null
122+
when (confirmationStrategy) {
123+
AsyncBlockingTaskConfirmationStrategy.NotRequired -> launchTask(request)
124+
is AsyncBlockingTaskConfirmationStrategy.ConfirmWith -> confirmationRequest = request
125+
}
126+
}
127+
128+
if (state.isRunning) {
129+
Dialog(
130+
onDismissRequest = {},
131+
properties = DialogProperties(
132+
dismissOnBackPress = false,
133+
dismissOnClickOutside = false
134+
)
135+
) {
136+
Surface(
137+
shape = RoundedCornerShape(20.dp),
138+
color = MaterialTheme.colorScheme.surface
139+
) {
140+
Column(
141+
modifier = Modifier.padding(horizontal = 28.dp, vertical = 24.dp),
142+
horizontalAlignment = Alignment.CenterHorizontally,
143+
verticalArrangement = Arrangement.spacedBy(16.dp)
144+
) {
145+
CircularProgressIndicator()
146+
Text(
147+
text = loadingTitle,
148+
style = MaterialTheme.typography.bodyMedium,
149+
textAlign = TextAlign.Center
150+
)
151+
}
152+
}
153+
}
154+
}
155+
156+
if (state.errorText != null) {
157+
AlertDialog(
158+
onDismissRequest = { state.dismissError() },
159+
confirmButton = {
160+
TextButton(onClick = { state.dismissError() }) {
161+
Text("OK")
162+
}
163+
},
164+
text = {
165+
Text(
166+
text = state.errorText.orEmpty(),
167+
textAlign = TextAlign.Center
168+
)
169+
}
170+
)
171+
}
172+
173+
val confirmation = confirmationRequest
174+
if (confirmation != null && confirmationStrategy is AsyncBlockingTaskConfirmationStrategy.ConfirmWith) {
175+
AlertDialog(
176+
onDismissRequest = { confirmationRequest = null },
177+
title = { Text(confirmationStrategy.title) },
178+
text = { Text(confirmationStrategy.message) },
179+
confirmButton = {
180+
TextButton(
181+
onClick = {
182+
confirmationRequest = null
183+
launchTask(confirmation)
184+
}
185+
) {
186+
Text(
187+
text = confirmationStrategy.confirmButtonTitle,
188+
color = if (confirmationStrategy.isDestructiveAction) {
189+
MaterialTheme.colorScheme.error
190+
} else {
191+
MaterialTheme.colorScheme.primary
192+
}
193+
)
194+
}
195+
},
196+
dismissButton = {
197+
TextButton(onClick = { confirmationRequest = null }) {
198+
Text(confirmationStrategy.cancelButtonTitle)
199+
}
200+
}
201+
)
202+
}
203+
}

0 commit comments

Comments
 (0)