You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
564 lines
19 KiB
564 lines
19 KiB
package org.calculate.taigamobile.ui.screens.sprint
|
|
|
|
import androidx.annotation.StringRes
|
|
import androidx.compose.foundation.*
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.foundation.shape.CornerSize
|
|
import androidx.compose.material.ripple.rememberRipple
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
import androidx.compose.ui.unit.Dp
|
|
import androidx.compose.ui.unit.dp
|
|
import coil.annotation.ExperimentalCoilApi
|
|
import coil.compose.rememberImagePainter
|
|
import com.google.accompanist.insets.navigationBarsHeight
|
|
import org.calculate.taigamobile.R
|
|
import org.calculate.taigamobile.domain.entities.*
|
|
import org.calculate.taigamobile.ui.components.buttons.PlusButton
|
|
import org.calculate.taigamobile.ui.components.lists.CommonTaskItem
|
|
import org.calculate.taigamobile.ui.components.texts.CommonTaskTitle
|
|
import org.calculate.taigamobile.ui.theme.TaigaMobileTheme
|
|
import org.calculate.taigamobile.ui.theme.cardShadowElevation
|
|
import org.calculate.taigamobile.ui.theme.kanbanBoardTonalElevation
|
|
import org.calculate.taigamobile.ui.utils.NavigateToTask
|
|
import org.calculate.taigamobile.ui.utils.clickableUnindicated
|
|
import org.calculate.taigamobile.ui.utils.surfaceColorAtElevation
|
|
import org.calculate.taigamobile.ui.utils.toColor
|
|
import java.time.LocalDateTime
|
|
|
|
@Composable
|
|
fun SprintKanban(
|
|
statuses: List<Status>,
|
|
storiesWithTasks: Map<CommonTask, List<CommonTask>>,
|
|
storylessTasks: List<CommonTask> = emptyList(),
|
|
issues: List<CommonTask> = emptyList(),
|
|
navigateToTask: NavigateToTask = { _, _, _ -> },
|
|
navigateToCreateTask: (type: CommonTaskType, parentId: Long?) -> Unit = { _, _ -> }
|
|
) = Column(
|
|
modifier = Modifier.horizontalScroll(rememberScrollState())
|
|
) {
|
|
val cellOuterPadding = 8.dp
|
|
val cellPadding = 8.dp
|
|
val cellWidth = 280.dp
|
|
val userStoryHeadingWidth = cellWidth - 20.dp
|
|
val minCellHeight = 80.dp
|
|
val backgroundCellColor = MaterialTheme.colorScheme.surfaceColorAtElevation(kanbanBoardTonalElevation)
|
|
val screenWidth = LocalContext.current.resources.configuration.screenWidthDp.dp
|
|
val totalWidth = cellWidth * statuses.size + userStoryHeadingWidth + cellPadding * statuses.size
|
|
|
|
Row(Modifier.padding(start = cellPadding, top = cellPadding)) {
|
|
Header(
|
|
text = stringResource(R.string.userstory),
|
|
cellWidth = userStoryHeadingWidth,
|
|
cellPadding = cellPadding,
|
|
stripeColor = backgroundCellColor,
|
|
backgroundColor = Color.Transparent
|
|
)
|
|
|
|
statuses.forEach {
|
|
Header(
|
|
text = it.name,
|
|
cellWidth = cellWidth,
|
|
cellPadding = cellPadding,
|
|
stripeColor = it.color.toColor(),
|
|
backgroundColor = backgroundCellColor
|
|
)
|
|
}
|
|
}
|
|
|
|
LazyColumn {
|
|
// stories with tasks
|
|
storiesWithTasks.forEach { (story, tasks) ->
|
|
item {
|
|
Row(
|
|
Modifier
|
|
.height(IntrinsicSize.Max)
|
|
.padding(start = cellPadding)
|
|
) {
|
|
UserStoryItem(
|
|
cellPadding = cellPadding,
|
|
cellWidth = userStoryHeadingWidth,
|
|
minCellHeight = minCellHeight,
|
|
userStory = story,
|
|
onAddClick = { navigateToCreateTask(CommonTaskType.Task, story.id) },
|
|
onUserStoryClick = { navigateToTask(story.id, story.taskType, story.ref) }
|
|
)
|
|
|
|
statuses.forEach { status ->
|
|
Cell(
|
|
cellWidth = cellWidth,
|
|
cellOuterPadding = cellOuterPadding,
|
|
cellPadding = cellPadding,
|
|
backgroundCellColor = backgroundCellColor
|
|
) {
|
|
tasks.filter { it.status == status }.forEach {
|
|
TaskItem(
|
|
task = it,
|
|
onTaskClick = { navigateToTask(it.id, it.taskType, it.ref) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// storyless tasks
|
|
item {
|
|
Row(
|
|
Modifier
|
|
.height(IntrinsicSize.Max)
|
|
.padding(start = cellPadding)
|
|
) {
|
|
CategoryItem(
|
|
titleId = R.string.tasks_without_story,
|
|
cellPadding = cellPadding,
|
|
cellWidth = userStoryHeadingWidth,
|
|
minCellHeight = minCellHeight,
|
|
onAddClick = { navigateToCreateTask(CommonTaskType.Task, null) },
|
|
)
|
|
|
|
statuses.forEach { status ->
|
|
Cell(
|
|
cellWidth = cellWidth,
|
|
cellOuterPadding = cellOuterPadding,
|
|
cellPadding = cellPadding,
|
|
backgroundCellColor = backgroundCellColor
|
|
) {
|
|
storylessTasks.filter { it.status == status }.forEach {
|
|
TaskItem(
|
|
task = it,
|
|
onTaskClick = { navigateToTask(it.id, it.taskType, it.ref) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
item {
|
|
Spacer(
|
|
Modifier.height(4.dp)
|
|
.padding(start = cellPadding)
|
|
.width(totalWidth)
|
|
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
|
)
|
|
}
|
|
|
|
// issues
|
|
item {
|
|
IssueHeader(
|
|
width = screenWidth,
|
|
padding = cellPadding,
|
|
backgroundColor = backgroundCellColor,
|
|
onAddClick = { navigateToCreateTask(CommonTaskType.Issue, null) }
|
|
)
|
|
}
|
|
|
|
items(issues) {
|
|
Row(Modifier.width(totalWidth)) {
|
|
Row(
|
|
Modifier.width(screenWidth)
|
|
.padding(vertical = 4.dp)
|
|
.background(backgroundCellColor)
|
|
) {
|
|
CommonTaskItem(
|
|
commonTask = it,
|
|
navigateToTask = navigateToTask
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
item {
|
|
Spacer(Modifier.navigationBarsHeight(8.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun Header(
|
|
text: String,
|
|
cellWidth: Dp,
|
|
cellPadding: Dp,
|
|
stripeColor: Color,
|
|
backgroundColor: Color,
|
|
) = Column(
|
|
modifier = Modifier
|
|
.padding(end = cellPadding, bottom = cellPadding)
|
|
.width(cellWidth)
|
|
.background(
|
|
color = backgroundColor,
|
|
shape = MaterialTheme.shapes.small.copy(
|
|
bottomStart = CornerSize(0.dp),
|
|
bottomEnd = CornerSize(0.dp)
|
|
)
|
|
),
|
|
horizontalAlignment = Alignment.Start
|
|
) {
|
|
Text(
|
|
text = text.uppercase(),
|
|
style = MaterialTheme.typography.titleMedium,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(8.dp)
|
|
)
|
|
|
|
Spacer(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.height(4.dp)
|
|
.background(stripeColor)
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun IssueHeader(
|
|
width: Dp,
|
|
padding: Dp,
|
|
backgroundColor: Color,
|
|
onAddClick: () -> Unit
|
|
) = Row(
|
|
modifier = Modifier
|
|
.width(width)
|
|
.padding(padding)
|
|
.clip(MaterialTheme.shapes.extraSmall)
|
|
.background(backgroundColor)
|
|
.padding(horizontal = 6.dp, vertical = 4.dp),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
text = stringResource(R.string.sprint_issues).uppercase(),
|
|
modifier = Modifier.weight(0.8f, fill = false)
|
|
)
|
|
|
|
PlusButton(
|
|
tint = MaterialTheme.colorScheme.outline,
|
|
onClick = onAddClick,
|
|
modifier = Modifier.weight(0.2f)
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun UserStoryItem(
|
|
cellPadding: Dp,
|
|
cellWidth: Dp,
|
|
minCellHeight: Dp,
|
|
userStory: CommonTask,
|
|
onAddClick: () -> Unit,
|
|
onUserStoryClick: () -> Unit
|
|
) = Row(
|
|
modifier = Modifier
|
|
.padding(end = cellPadding, bottom = cellPadding)
|
|
.width(cellWidth)
|
|
.heightIn(min = minCellHeight),
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth().weight(0.8f, fill = false)
|
|
) {
|
|
CommonTaskTitle(
|
|
ref = userStory.ref,
|
|
title = userStory.title,
|
|
indicatorColorsHex = userStory.colors,
|
|
isInactive = userStory.isClosed,
|
|
tags = userStory.tags,
|
|
isBlocked = userStory.blockedNote != null,
|
|
modifier = Modifier.padding(top = 4.dp)
|
|
.clickableUnindicated(onClick = onUserStoryClick)
|
|
)
|
|
|
|
Text(
|
|
text = userStory.status.name,
|
|
color = userStory.status.color.toColor(),
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
|
|
PlusButton(
|
|
tint = MaterialTheme.colorScheme.outline,
|
|
onClick = onAddClick,
|
|
modifier = Modifier.weight(0.2f)
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun CategoryItem(
|
|
@StringRes titleId: Int,
|
|
cellPadding: Dp,
|
|
cellWidth: Dp,
|
|
minCellHeight: Dp,
|
|
onAddClick: () -> Unit,
|
|
) = Column(
|
|
modifier = Modifier
|
|
.padding(end = cellPadding, bottom = cellPadding)
|
|
.width(cellWidth)
|
|
.heightIn(min = minCellHeight)
|
|
) {
|
|
Row(
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Text(
|
|
text = stringResource(titleId),
|
|
modifier = Modifier
|
|
.weight(0.8f, fill = false)
|
|
.padding(top = 4.dp)
|
|
)
|
|
|
|
PlusButton(
|
|
tint = MaterialTheme.colorScheme.outline,
|
|
onClick = onAddClick,
|
|
modifier = Modifier.weight(0.2f)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun Cell(
|
|
cellWidth: Dp,
|
|
cellOuterPadding: Dp,
|
|
cellPadding: Dp,
|
|
backgroundCellColor: Color,
|
|
content: @Composable ColumnScope.() -> Unit
|
|
) = Column(
|
|
modifier = Modifier
|
|
.fillMaxHeight()
|
|
.padding(end = cellOuterPadding, bottom = cellOuterPadding)
|
|
.width(cellWidth)
|
|
.background(backgroundCellColor)
|
|
.padding(cellPadding),
|
|
content = content
|
|
)
|
|
|
|
@OptIn(ExperimentalCoilApi::class)
|
|
@Composable
|
|
private fun TaskItem(
|
|
task: CommonTask,
|
|
onTaskClick: () -> Unit
|
|
) = Surface(
|
|
modifier = Modifier.fillMaxWidth().padding(4.dp),
|
|
shape = MaterialTheme.shapes.small,
|
|
shadowElevation = cardShadowElevation
|
|
) {
|
|
Row(
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
modifier = Modifier.fillMaxWidth()
|
|
.clickable(
|
|
onClick = onTaskClick,
|
|
indication = rememberRipple(),
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
)
|
|
.padding(12.dp)
|
|
) {
|
|
Column(Modifier.weight(0.8f, fill = false)) {
|
|
CommonTaskTitle(
|
|
ref = task.ref,
|
|
title = task.title,
|
|
indicatorColorsHex = task.colors,
|
|
isInactive = task.isClosed,
|
|
tags = task.tags,
|
|
isBlocked = task.blockedNote != null
|
|
)
|
|
|
|
Text(
|
|
text = task.assignee?.fullName?.let {
|
|
stringResource(R.string.assignee_pattern).format(it)
|
|
} ?: stringResource(R.string.unassigned),
|
|
color = MaterialTheme.colorScheme.primary,
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
|
|
task.assignee?.let {
|
|
Image(
|
|
painter = rememberImagePainter(
|
|
data = it.avatarUrl ?: R.drawable.default_avatar,
|
|
builder = {
|
|
error(R.drawable.default_avatar)
|
|
crossfade(true)
|
|
}
|
|
),
|
|
contentDescription = null,
|
|
contentScale = ContentScale.Crop,
|
|
modifier = Modifier.size(32.dp)
|
|
.clip(CircleShape)
|
|
.weight(0.2f, fill = false)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Preview(showBackground = true)
|
|
@Composable
|
|
fun SprintKanbanPreview() = TaigaMobileTheme {
|
|
SprintKanban(
|
|
statuses = listOf(
|
|
Status(
|
|
id = 0,
|
|
//name = "New",
|
|
name = stringResource(R.string.status_new),
|
|
color = "#70728F",
|
|
type = StatusType.Status
|
|
),
|
|
Status(
|
|
id = 1,
|
|
//name = "In progress",
|
|
name = stringResource(R.string.status_in_progress),
|
|
color = "#E47C40",
|
|
type = StatusType.Status
|
|
),
|
|
Status(
|
|
id = 1,
|
|
//name = "Done",
|
|
name = stringResource(R.string.status_done),
|
|
color = "#A8E440",
|
|
type = StatusType.Status
|
|
),
|
|
Status(
|
|
id = 1,
|
|
//name = "Archived",
|
|
name = stringResource(R.string.status_ready_for_test),
|
|
color = "#A9AABC",
|
|
type = StatusType.Status
|
|
),
|
|
),
|
|
storiesWithTasks = List(5) {
|
|
CommonTask(
|
|
id = it.toLong(),
|
|
createdDate = LocalDateTime.now(),
|
|
title = "Very cool story",
|
|
ref = 100,
|
|
status = Status(
|
|
id = 1,
|
|
//name = "In progress",
|
|
name = stringResource(R.string.status_in_progress),
|
|
color = "#E47C40",
|
|
type = StatusType.Status
|
|
),
|
|
assignee = User(
|
|
_id = it.toLong(),
|
|
fullName = "Name Name",
|
|
photo = "https://avatars.githubusercontent.com/u/36568187?v=4",
|
|
bigPhoto = null,
|
|
username = "username"
|
|
),
|
|
projectInfo = Project(0, "", ""),
|
|
taskType = CommonTaskType.UserStory,
|
|
isClosed = false
|
|
) to listOf(
|
|
CommonTask(
|
|
id = it.toLong(),
|
|
createdDate = LocalDateTime.now(),
|
|
title = "Very cool story Very cool story Very cool story",
|
|
ref = 100,
|
|
status = Status(
|
|
id = 1,
|
|
//name = "In progress",
|
|
name = stringResource(R.string.status_in_progress),
|
|
color = "#E47C40",
|
|
type = StatusType.Status
|
|
),
|
|
assignee = User(
|
|
_id = it.toLong(),
|
|
fullName = "Name Name",
|
|
photo = "https://avatars.githubusercontent.com/u/36568187?v=4",
|
|
bigPhoto = null,
|
|
username = "username"
|
|
),
|
|
projectInfo = Project(0, "", ""),
|
|
taskType = CommonTaskType.Task,
|
|
isClosed = false
|
|
),
|
|
CommonTask(
|
|
id = it.toLong() + 2,
|
|
createdDate = LocalDateTime.now(),
|
|
title = "Very cool story",
|
|
ref = 100,
|
|
status = Status(
|
|
id = 1,
|
|
name = stringResource(R.string.status_in_progress),
|
|
color = "#E47C40",
|
|
type = StatusType.Status
|
|
),
|
|
assignee = User(
|
|
_id = it.toLong(),
|
|
fullName = "Name Name",
|
|
photo = "https://avatars.githubusercontent.com/u/36568187?v=4",
|
|
bigPhoto = null,
|
|
username = "username"
|
|
),
|
|
projectInfo = Project(0, "", ""),
|
|
taskType = CommonTaskType.Task,
|
|
isClosed = false
|
|
),
|
|
CommonTask(
|
|
id = it.toLong() + 2,
|
|
createdDate = LocalDateTime.now(),
|
|
title = "Very cool story",
|
|
ref = 100,
|
|
status = Status(
|
|
id = 0,
|
|
//name = "New",
|
|
name = stringResource(R.string.status_new),
|
|
color = "#70728F",
|
|
type = StatusType.Status
|
|
),
|
|
assignee = User(
|
|
_id = it.toLong(),
|
|
fullName = "Name Name",
|
|
photo = "https://avatars.githubusercontent.com/u/36568187?v=4",
|
|
bigPhoto = null,
|
|
username = "username"
|
|
),
|
|
projectInfo = Project(0, "", ""),
|
|
taskType = CommonTaskType.Task,
|
|
isClosed = false
|
|
)
|
|
)
|
|
}.toMap(),
|
|
issues = List(10) {
|
|
CommonTask(
|
|
id = it.toLong() + 1,
|
|
createdDate = LocalDateTime.now(),
|
|
title = "Very cool story",
|
|
ref = 100,
|
|
status = Status(
|
|
id = 0,
|
|
//name = "New",
|
|
name = stringResource(R.string.status_new),
|
|
color = "#70728F",
|
|
type = StatusType.Status
|
|
),
|
|
assignee = User(
|
|
_id = it.toLong(),
|
|
fullName = "Name Name",
|
|
photo = "https://avatars.githubusercontent.com/u/36568187?v=4",
|
|
bigPhoto = null,
|
|
username = "username"
|
|
),
|
|
projectInfo = Project(0, "", ""),
|
|
taskType = CommonTaskType.Issue,
|
|
isClosed = false
|
|
)
|
|
}
|
|
)
|
|
}
|