feat(tasks): вложенные задачи (подзадачи) + per-task меню «⋯»
Паритет веб-версии Задач: - CalDAV: RELATED-TO;RELTYPE=PARENT в модели/парсере/билдере VTODO (DavTask.parentUid); createTask принимает parentUid; updateTask и toggleComplete сохраняют связь с родителем. - TasksRepository/VM: createSubtask(parent), deleteTask(task) для любой задачи (не только открытой детали). - UI: у задачи меню «⋯» → «Добавить вложенную задачу» / «Удалить задачу»; подзадачи рисуются под родителем с отступом 24dp; диалог ввода названия подзадачи. Подзадачи исключены из верхнего уровня (не дублируются). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,8 @@ data class DavTask(
|
|||||||
val description: String,
|
val description: String,
|
||||||
val calendarName: String,
|
val calendarName: String,
|
||||||
val calendarHref: String,
|
val calendarHref: String,
|
||||||
|
/** UID родительской задачи (RELATED-TO;RELTYPE=PARENT) — для подзадач; пусто у корневых. */
|
||||||
|
val parentUid: String = "",
|
||||||
) {
|
) {
|
||||||
val isCompleted: Boolean
|
val isCompleted: Boolean
|
||||||
get() = status.equals("COMPLETED", ignoreCase = true) || percentComplete >= 100
|
get() = status.equals("COMPLETED", ignoreCase = true) || percentComplete >= 100
|
||||||
@@ -87,6 +89,11 @@ object CalDavClient {
|
|||||||
private val icsDue = Pattern.compile("DUE[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
private val icsDue = Pattern.compile("DUE[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
||||||
private val icsStatus = Pattern.compile("STATUS:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
private val icsStatus = Pattern.compile("STATUS:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
||||||
private val icsUid = Pattern.compile("UID:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
private val icsUid = Pattern.compile("UID:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
||||||
|
// RELATED-TO с необязательным RELTYPE=PARENT (или без параметров) → UID родителя.
|
||||||
|
private val icsRelatedTo = Pattern.compile(
|
||||||
|
"RELATED-TO(?:;[^:\\r\\n]*)?:([^\\r\\n]+)",
|
||||||
|
Pattern.CASE_INSENSITIVE,
|
||||||
|
)
|
||||||
private val icsDtStamp = Pattern.compile("DTSTAMP[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
private val icsDtStamp = Pattern.compile("DTSTAMP[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
||||||
private val icsLastModified = Pattern.compile("LAST-MODIFIED[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
private val icsLastModified = Pattern.compile("LAST-MODIFIED[^:]*:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
||||||
private val icsPriority = Pattern.compile("PRIORITY:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
private val icsPriority = Pattern.compile("PRIORITY:([^\\r\\n]+)", Pattern.CASE_INSENSITIVE)
|
||||||
@@ -634,6 +641,7 @@ object CalDavClient {
|
|||||||
dueDate: LocalDate? = null,
|
dueDate: LocalDate? = null,
|
||||||
priority: Int = 0,
|
priority: Int = 0,
|
||||||
description: String = "",
|
description: String = "",
|
||||||
|
parentUid: String = "",
|
||||||
): DavTask {
|
): DavTask {
|
||||||
val uid = "${UUID.randomUUID()}@f7cloud.mobile"
|
val uid = "${UUID.randomUUID()}@f7cloud.mobile"
|
||||||
val ics = buildVTodoIcs(
|
val ics = buildVTodoIcs(
|
||||||
@@ -644,6 +652,7 @@ object CalDavClient {
|
|||||||
priority = priority,
|
priority = priority,
|
||||||
percentComplete = 0,
|
percentComplete = 0,
|
||||||
description = description,
|
description = description,
|
||||||
|
parentUid = parentUid,
|
||||||
)
|
)
|
||||||
val url = calendar.href.trimEnd('/') + "/$uid.ics"
|
val url = calendar.href.trimEnd('/') + "/$uid.ics"
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
@@ -670,6 +679,7 @@ object CalDavClient {
|
|||||||
description = description,
|
description = description,
|
||||||
calendarName = calendar.displayName,
|
calendarName = calendar.displayName,
|
||||||
calendarHref = calendar.href,
|
calendarHref = calendar.href,
|
||||||
|
parentUid = parentUid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,6 +701,7 @@ object CalDavClient {
|
|||||||
priority = priority,
|
priority = priority,
|
||||||
percentComplete = percentComplete,
|
percentComplete = percentComplete,
|
||||||
description = description,
|
description = description,
|
||||||
|
parentUid = task.parentUid, // не терять связь с родителем при правке подзадачи
|
||||||
)
|
)
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url(task.href)
|
.url(task.href)
|
||||||
@@ -938,6 +949,7 @@ object CalDavClient {
|
|||||||
description = extractIcs(icsDescription, ics),
|
description = extractIcs(icsDescription, ics),
|
||||||
calendarName = calendarName,
|
calendarName = calendarName,
|
||||||
calendarHref = calendarHref,
|
calendarHref = calendarHref,
|
||||||
|
parentUid = extractIcs(icsRelatedTo, ics).trim(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,6 +961,7 @@ object CalDavClient {
|
|||||||
priority: Int,
|
priority: Int,
|
||||||
percentComplete: Int,
|
percentComplete: Int,
|
||||||
description: String,
|
description: String,
|
||||||
|
parentUid: String = "",
|
||||||
): String {
|
): String {
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
return buildString {
|
return buildString {
|
||||||
@@ -976,6 +989,9 @@ object CalDavClient {
|
|||||||
if (description.isNotBlank()) {
|
if (description.isNotBlank()) {
|
||||||
appendLine("DESCRIPTION:${escapeIcsText(description)}")
|
appendLine("DESCRIPTION:${escapeIcsText(description)}")
|
||||||
}
|
}
|
||||||
|
if (parentUid.isNotBlank()) {
|
||||||
|
appendLine("RELATED-TO;RELTYPE=PARENT:${escapeIcsText(parentUid)}")
|
||||||
|
}
|
||||||
appendLine("END:VTODO")
|
appendLine("END:VTODO")
|
||||||
appendLine("END:VCALENDAR")
|
appendLine("END:VCALENDAR")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.animation.animateContentSize
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -345,7 +346,10 @@ fun TasksRow(
|
|||||||
onToggleComplete: () -> Unit,
|
onToggleComplete: () -> Unit,
|
||||||
onOpen: () -> Unit,
|
onOpen: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onAddSubtask: (() -> Unit)? = null,
|
||||||
|
onDelete: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
var menuOpen by remember { mutableStateOf(false) }
|
||||||
val hasMeta = showMeta && (task.listName.isNotBlank() || task.percentComplete in 1..99 || task.due.isNotBlank())
|
val hasMeta = showMeta && (task.listName.isNotBlank() || task.percentComplete in 1..99 || task.due.isNotBlank())
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -401,6 +405,43 @@ fun TasksRow(
|
|||||||
color = task.dueDateColor(),
|
color = task.dueDateColor(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Меню «⋯»: вложенная задача / удалить (как в веб-версии)
|
||||||
|
if (onAddSubtask != null || onDelete != null) {
|
||||||
|
Box {
|
||||||
|
Text(
|
||||||
|
"⋯",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = F7Colors.TextSecondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = { menuOpen = true },
|
||||||
|
),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
)
|
||||||
|
androidx.compose.material3.DropdownMenu(
|
||||||
|
expanded = menuOpen,
|
||||||
|
onDismissRequest = { menuOpen = false },
|
||||||
|
containerColor = androidx.compose.ui.graphics.Color.White,
|
||||||
|
) {
|
||||||
|
onAddSubtask?.let { add ->
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text("Добавить вложенную задачу", color = F7Colors.TextPrimary) },
|
||||||
|
onClick = { menuOpen = false; add() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onDelete?.let { del ->
|
||||||
|
androidx.compose.material3.DropdownMenuItem(
|
||||||
|
text = { Text("Удалить задачу", color = F7Colors.Error) },
|
||||||
|
onClick = { menuOpen = false; del() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasMeta) {
|
if (hasMeta) {
|
||||||
@@ -1064,3 +1105,42 @@ private fun SmartListSettingRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Диалог создания вложенной задачи (подзадачи) — по кнопке «Добавить вложенную задачу». */
|
||||||
|
@Composable
|
||||||
|
fun TaskSubtaskDialog(
|
||||||
|
parentTitle: String,
|
||||||
|
onConfirm: (String) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(shape = RoundedCornerShape(16.dp), color = Color.White) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Text(
|
||||||
|
"Вложенная задача",
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = F7Colors.TextPrimary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"к «$parentTitle»",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = F7Colors.TextSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
F7OutlinedField(value = text, onValueChange = { text = it }, label = "Название")
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
F7SecondaryButton("Отмена", onClick = onDismiss, modifier = Modifier.weight(1f))
|
||||||
|
F7PrimaryButton(
|
||||||
|
text = "Создать",
|
||||||
|
onClick = { onConfirm(text.trim()) },
|
||||||
|
enabled = text.isNotBlank(),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ data class TaskItem(
|
|||||||
val description: String,
|
val description: String,
|
||||||
val listHref: String,
|
val listHref: String,
|
||||||
val listName: String,
|
val listName: String,
|
||||||
|
val parentUid: String = "",
|
||||||
) {
|
) {
|
||||||
val isCompleted: Boolean
|
val isCompleted: Boolean
|
||||||
get() = status.equals("COMPLETED", ignoreCase = true) || percentComplete >= 100
|
get() = status.equals("COMPLETED", ignoreCase = true) || percentComplete >= 100
|
||||||
@@ -46,6 +47,7 @@ data class TaskItem(
|
|||||||
description = description,
|
description = description,
|
||||||
calendarName = listName,
|
calendarName = listName,
|
||||||
calendarHref = listHref,
|
calendarHref = listHref,
|
||||||
|
parentUid = parentUid,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ class TasksRepository {
|
|||||||
dueDate: LocalDate? = null,
|
dueDate: LocalDate? = null,
|
||||||
priority: Int = 0,
|
priority: Int = 0,
|
||||||
description: String = "",
|
description: String = "",
|
||||||
|
parentUid: String = "",
|
||||||
): TaskItem {
|
): TaskItem {
|
||||||
val client = authedClient(session)
|
val client = authedClient(session)
|
||||||
val calendar = calendarForHref(session, listHref)
|
val calendar = calendarForHref(session, listHref)
|
||||||
@@ -90,6 +93,7 @@ class TasksRepository {
|
|||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
priority = priority,
|
priority = priority,
|
||||||
description = description,
|
description = description,
|
||||||
|
parentUid = parentUid,
|
||||||
).toTaskItem()
|
).toTaskItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +161,7 @@ class TasksRepository {
|
|||||||
description = description,
|
description = description,
|
||||||
listHref = calendarHref,
|
listHref = calendarHref,
|
||||||
listName = calendarName,
|
listName = calendarName,
|
||||||
|
parentUid = parentUid,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun calendarSlug(href: String): String {
|
private fun calendarSlug(href: String): String {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -37,6 +40,8 @@ fun TasksScreen(
|
|||||||
) {
|
) {
|
||||||
val vm: TasksViewModel = viewModel()
|
val vm: TasksViewModel = viewModel()
|
||||||
val state by vm.state.collectAsState()
|
val state by vm.state.collectAsState()
|
||||||
|
// Родитель для создаваемой подзадачи (открывает диалог ввода названия).
|
||||||
|
var subtaskParent by remember { mutableStateOf<TaskItem?>(null) }
|
||||||
|
|
||||||
// Стрелка «назад» нижней панели на Задачах → показать панель списков (закрыть открытый список).
|
// Стрелка «назад» нижней панели на Задачах → показать панель списков (закрыть открытый список).
|
||||||
LaunchedEffect(showListsRequest) {
|
LaunchedEffect(showListsRequest) {
|
||||||
@@ -109,6 +114,8 @@ fun TasksScreen(
|
|||||||
onReload = { vm.refreshTasks(session) },
|
onReload = { vm.refreshTasks(session) },
|
||||||
onToggleCompletedExpanded = vm::toggleCompletedExpanded,
|
onToggleCompletedExpanded = vm::toggleCompletedExpanded,
|
||||||
onDeleteCompleted = { vm.deleteCompletedTasks(session) },
|
onDeleteCompleted = { vm.deleteCompletedTasks(session) },
|
||||||
|
onAddSubtask = { subtaskParent = it },
|
||||||
|
onDeleteTask = { vm.deleteTask(session, it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +156,18 @@ fun TasksScreen(
|
|||||||
onDismiss = vm::closeSettings,
|
onDismiss = vm::closeSettings,
|
||||||
onSwipeBack = vm::closeSettings,
|
onSwipeBack = vm::closeSettings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Диалог «Добавить вложенную задачу»
|
||||||
|
subtaskParent?.let { parent ->
|
||||||
|
TaskSubtaskDialog(
|
||||||
|
parentTitle = parent.summary,
|
||||||
|
onConfirm = { title ->
|
||||||
|
vm.createSubtask(session, parent, title)
|
||||||
|
subtaskParent = null
|
||||||
|
},
|
||||||
|
onDismiss = { subtaskParent = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -168,7 +187,15 @@ private fun TasksMainContent(
|
|||||||
onReload: () -> Unit,
|
onReload: () -> Unit,
|
||||||
onToggleCompletedExpanded: () -> Unit,
|
onToggleCompletedExpanded: () -> Unit,
|
||||||
onDeleteCompleted: () -> Unit,
|
onDeleteCompleted: () -> Unit,
|
||||||
|
onAddSubtask: (TaskItem) -> Unit,
|
||||||
|
onDeleteTask: (TaskItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
// Подзадачи (RELATED-TO) — рисуем под родителем с отступом; из верхнего уровня исключаем.
|
||||||
|
val childrenByParent = remember(state.visibleActiveGroups) {
|
||||||
|
state.visibleActiveGroups.flatMap { it.tasks }
|
||||||
|
.filter { it.parentUid.isNotBlank() }
|
||||||
|
.groupBy { it.parentUid }
|
||||||
|
}
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
TasksToolbar(
|
TasksToolbar(
|
||||||
serverUrl = session.serverUrl,
|
serverUrl = session.serverUrl,
|
||||||
@@ -218,13 +245,27 @@ private fun TasksMainContent(
|
|||||||
modifier = Modifier.padding(bottom = 4.dp),
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(group.tasks, key = { it.uid }) { task ->
|
items(group.tasks.filter { it.parentUid.isBlank() }, key = { it.uid }) { task ->
|
||||||
TasksRow(
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
task = task,
|
TasksRow(
|
||||||
serverUrl = session.serverUrl,
|
task = task,
|
||||||
onToggleComplete = { onToggleComplete(task) },
|
serverUrl = session.serverUrl,
|
||||||
onOpen = { onOpenTask(task) },
|
onToggleComplete = { onToggleComplete(task) },
|
||||||
)
|
onOpen = { onOpenTask(task) },
|
||||||
|
onAddSubtask = { onAddSubtask(task) },
|
||||||
|
onDelete = { onDeleteTask(task) },
|
||||||
|
)
|
||||||
|
childrenByParent[task.uid].orEmpty().forEach { sub ->
|
||||||
|
TasksRow(
|
||||||
|
task = sub,
|
||||||
|
serverUrl = session.serverUrl,
|
||||||
|
onToggleComplete = { onToggleComplete(sub) },
|
||||||
|
onOpen = { onOpenTask(sub) },
|
||||||
|
onDelete = { onDeleteTask(sub) },
|
||||||
|
modifier = Modifier.padding(start = 24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ class TasksViewModel(
|
|||||||
summary: String,
|
summary: String,
|
||||||
dueInput: String,
|
dueInput: String,
|
||||||
priority: Int,
|
priority: Int,
|
||||||
|
parentUid: String = "",
|
||||||
) {
|
) {
|
||||||
val listHref = _state.value.selectedListHref ?: return
|
val listHref = _state.value.selectedListHref ?: return
|
||||||
if (summary.isBlank()) {
|
if (summary.isBlank()) {
|
||||||
@@ -218,6 +219,7 @@ class TasksViewModel(
|
|||||||
summary = summary.trim(),
|
summary = summary.trim(),
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
priority = priority,
|
priority = priority,
|
||||||
|
parentUid = parentUid,
|
||||||
)
|
)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
_state.update {
|
_state.update {
|
||||||
@@ -301,6 +303,30 @@ class TasksViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Удаление конкретной задачи из списка (per-task меню «⋯»), не только открытой детали. */
|
||||||
|
fun deleteTask(session: AuthSession, task: TaskItem) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_state.update { it.copy(saving = true, error = null) }
|
||||||
|
runCatching { repository.deleteTask(session, task) }
|
||||||
|
.onSuccess {
|
||||||
|
_state.update {
|
||||||
|
it.copy(saving = false, detailTask = it.detailTask?.takeIf { d -> d.uid != task.uid })
|
||||||
|
}
|
||||||
|
_state.value.selectedListHref?.let { loadTasks(session, it) }
|
||||||
|
}
|
||||||
|
.onFailure { t ->
|
||||||
|
_state.update {
|
||||||
|
it.copy(saving = false, error = t.message ?: "Не удалось удалить", unauthorized = t is UnauthorizedException)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Создать подзадачу к [parent] с заголовком [summary]. */
|
||||||
|
fun createSubtask(session: AuthSession, parent: TaskItem, summary: String) {
|
||||||
|
createTask(session, summary, "", 0, parentUid = parent.uid)
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteDetail(session: AuthSession) {
|
fun deleteDetail(session: AuthSession) {
|
||||||
val task = _state.value.detailTask ?: return
|
val task = _state.value.detailTask ?: return
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
Reference in New Issue
Block a user