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.
353 lines
11 KiB
353 lines
11 KiB
package org.calculate.taigamobile.ui.screens.kanban
|
|
|
|
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.material3.MaterialTheme
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material.ripple.rememberRipple
|
|
import androidx.compose.runtime.*
|
|
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.LocalDensity
|
|
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.flowlayout.FlowRow
|
|
import com.google.accompanist.insets.navigationBarsHeight
|
|
import org.calculate.taigamobile.R
|
|
import org.calculate.taigamobile.domain.entities.*
|
|
import org.calculate.taigamobile.ui.components.DropdownSelector
|
|
import org.calculate.taigamobile.ui.components.buttons.PlusButton
|
|
import org.calculate.taigamobile.ui.components.texts.CommonTaskTitle
|
|
import org.calculate.taigamobile.ui.theme.*
|
|
import org.calculate.taigamobile.ui.utils.surfaceColorAtElevation
|
|
import org.calculate.taigamobile.ui.utils.toColor
|
|
import java.time.LocalDateTime
|
|
|
|
@Composable
|
|
fun KanbanBoard(
|
|
statuses: List<Status>,
|
|
stories: List<CommonTaskExtended> = emptyList(),
|
|
team: List<User> = emptyList(),
|
|
swimlanes: List<Swimlane?>,
|
|
selectSwimlane: (Swimlane?) -> Unit = {},
|
|
selectedSwimlane: Swimlane? = null,
|
|
navigateToStory: (id: Long, ref: Int) -> Unit = { _, _ -> },
|
|
navigateToCreateTask: (statusId: Long, swimlaneId: Long?) -> Unit = { _, _ -> }
|
|
) {
|
|
val cellOuterPadding = 8.dp
|
|
val cellPadding = 8.dp
|
|
val cellWidth = 280.dp
|
|
val backgroundCellColor = MaterialTheme.colorScheme.surfaceColorAtElevation(kanbanBoardTonalElevation)
|
|
|
|
/*
|
|
swimlanes.takeIf { it.isNotEmpty() }?.let {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.padding(cellOuterPadding)
|
|
) {
|
|
Text(
|
|
text = stringResource(R.string.swimlane_title),
|
|
style = MaterialTheme.typography.titleLarge
|
|
)
|
|
|
|
Spacer(Modifier.width(8.dp))
|
|
|
|
DropdownSelector(
|
|
items = swimlanes,
|
|
selectedItem = selectedSwimlane,
|
|
onItemSelected = selectSwimlane,
|
|
itemContent = {
|
|
Text(
|
|
text = it?.name ?: stringResource(R.string.unclassifed),
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = it?.let { MaterialTheme.colorScheme.onSurface } ?: MaterialTheme.colorScheme.primary
|
|
)
|
|
},
|
|
selectedItemContent = {
|
|
Text(
|
|
text = it?.name ?: stringResource(R.string.unclassifed),
|
|
style = MaterialTheme.typography.titleLarge,
|
|
color = MaterialTheme.colorScheme.primary
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
val storiesToDisplay = stories.filter { it.swimlane?.id == selectedSwimlane?.id }
|
|
|
|
Row(
|
|
Modifier
|
|
.fillMaxSize()
|
|
.horizontalScroll(rememberScrollState())
|
|
) {
|
|
|
|
Spacer(Modifier.width(cellPadding))
|
|
|
|
statuses.forEach { status ->
|
|
val statusStories = storiesToDisplay.filter { it.status == status }
|
|
|
|
Column {
|
|
Header(
|
|
text = status.name,
|
|
storiesCount = statusStories.size,
|
|
cellWidth = cellWidth,
|
|
cellOuterPadding = cellOuterPadding,
|
|
stripeColor = status.color.toColor(),
|
|
backgroundColor = backgroundCellColor,
|
|
onAddClick = { navigateToCreateTask(status.id, selectedSwimlane?.id) }
|
|
)
|
|
|
|
LazyColumn(
|
|
Modifier
|
|
.fillMaxHeight()
|
|
.width(cellWidth)
|
|
.background(backgroundCellColor)
|
|
.padding(cellPadding)
|
|
) {
|
|
items(statusStories) {
|
|
StoryItem(
|
|
story = it,
|
|
assignees = it.assignedIds.mapNotNull { id -> team.find { it.id == id } },
|
|
onTaskClick = { navigateToStory(it.id, it.ref) }
|
|
)
|
|
}
|
|
|
|
item {
|
|
Spacer(Modifier.navigationBarsHeight(8.dp))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun Header(
|
|
text: String,
|
|
storiesCount: Int,
|
|
cellWidth: Dp,
|
|
cellOuterPadding: Dp,
|
|
stripeColor: Color,
|
|
backgroundColor: Color,
|
|
onAddClick: () -> Unit
|
|
) = Row(
|
|
modifier = Modifier
|
|
.padding(end = cellOuterPadding, bottom = cellOuterPadding)
|
|
.width(cellWidth)
|
|
.background(
|
|
color = backgroundColor,
|
|
shape = MaterialTheme.shapes.small.copy(
|
|
bottomStart = CornerSize(0.dp),
|
|
bottomEnd = CornerSize(0.dp)
|
|
)
|
|
),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
val textStyle = MaterialTheme.typography.titleMedium
|
|
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.weight(0.8f, fill = false)
|
|
) {
|
|
Spacer(
|
|
Modifier
|
|
.padding(start = 10.dp)
|
|
.size(
|
|
width = 10.dp,
|
|
height = with(LocalDensity.current) { textStyle.fontSize.toDp() + 2.dp }
|
|
)
|
|
.background(stripeColor)
|
|
)
|
|
|
|
Text(
|
|
text = stringResource(R.string.status_with_number_template).format(
|
|
text.uppercase(), storiesCount
|
|
),
|
|
style = textStyle,
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis,
|
|
modifier = Modifier.padding(8.dp)
|
|
)
|
|
}
|
|
|
|
PlusButton(
|
|
tint = MaterialTheme.colorScheme.outline,
|
|
onClick = onAddClick,
|
|
modifier = Modifier.weight(0.2f)
|
|
)
|
|
}
|
|
|
|
@OptIn(ExperimentalCoilApi::class)
|
|
@Composable
|
|
private fun StoryItem(
|
|
story: CommonTaskExtended,
|
|
assignees: List<User>,
|
|
onTaskClick: () -> Unit
|
|
) = Surface(
|
|
modifier = Modifier.fillMaxWidth().padding(4.dp),
|
|
shape = MaterialTheme.shapes.small,
|
|
shadowElevation = cardShadowElevation
|
|
) {
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth()
|
|
.clickable(
|
|
onClick = onTaskClick,
|
|
indication = rememberRipple(),
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
)
|
|
.padding(12.dp)
|
|
) {
|
|
story.epicsShortInfo.forEach {
|
|
val textStyle = MaterialTheme.typography.bodySmall
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Spacer(
|
|
Modifier
|
|
.size(with(LocalDensity.current) { textStyle.fontSize.toDp() })
|
|
.background(it.color.toColor(), CircleShape)
|
|
)
|
|
|
|
Spacer(Modifier.width(4.dp))
|
|
|
|
Text(
|
|
text = it.title,
|
|
style = textStyle
|
|
)
|
|
}
|
|
|
|
Spacer(Modifier.height(4.dp))
|
|
}
|
|
|
|
Spacer(Modifier.height(4.dp))
|
|
|
|
CommonTaskTitle(
|
|
ref = story.ref,
|
|
title = story.title,
|
|
isInactive = story.isClosed,
|
|
tags = story.tags,
|
|
isBlocked = story.blockedNote != null
|
|
)
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
FlowRow(
|
|
mainAxisSpacing = 4.dp,
|
|
crossAxisSpacing = 4.dp
|
|
) {
|
|
assignees.forEach {
|
|
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(28.dp)
|
|
.clip(CircleShape)
|
|
.weight(0.2f, fill = false)
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
@Preview(showBackground = true)
|
|
@Composable
|
|
fun KanbanBoardPreview() = TaigaMobileTheme {
|
|
KanbanBoard(
|
|
swimlanes = listOf(
|
|
Swimlane(0, "Name", 0),
|
|
Swimlane(0, "Another name", 1)
|
|
),
|
|
statuses = listOf(
|
|
Status(
|
|
id = 0,
|
|
name = stringResource(R.string. status_new),
|
|
color = "#70728F",
|
|
type = StatusType.Status
|
|
),
|
|
Status(
|
|
id = 1,
|
|
name = stringResource(R.string.status_in_progress),
|
|
color = "#E47C40",
|
|
type = StatusType.Status
|
|
),
|
|
Status(
|
|
id = 1,
|
|
name = stringResource(R.string.status_done),
|
|
color = "#A8E440",
|
|
type = StatusType.Status
|
|
),
|
|
Status(
|
|
id = 1,
|
|
name = stringResource(R.string.status_ready_for_test),
|
|
color = "#A9AABC",
|
|
type = StatusType.Status
|
|
),
|
|
),
|
|
stories = List(5) {
|
|
CommonTaskExtended(
|
|
id = 0,
|
|
status = Status(
|
|
id = 1,
|
|
name = "In progress",
|
|
color = "#E47C40",
|
|
type = StatusType.Status
|
|
),
|
|
createdDateTime = LocalDateTime.now(),
|
|
sprint = null,
|
|
assignedIds = List(10) { it.toLong() },
|
|
watcherIds = emptyList(),
|
|
creatorId = 0,
|
|
ref = 1,
|
|
title = "Sample title",
|
|
isClosed = false,
|
|
description = "",
|
|
epicsShortInfo = List(3) { EpicShortInfo(0, "Some title", 1, "#A8E440") },
|
|
projectSlug = "",
|
|
userStoryShortInfo = null,
|
|
version = 0,
|
|
color = null,
|
|
type = null,
|
|
priority = null,
|
|
severity = null,
|
|
taskType = CommonTaskType.UserStory,
|
|
swimlane = null,
|
|
dueDate = null,
|
|
dueDateStatus = DueDateStatus.NotSet,
|
|
url = ""
|
|
)
|
|
},
|
|
team = List(10) {
|
|
User(
|
|
_id = it.toLong(),
|
|
fullName = "Name Name",
|
|
photo = "https://avatars.githubusercontent.com/u/36568187?v=4",
|
|
bigPhoto = null,
|
|
username = "username"
|
|
)
|
|
}
|
|
)
|
|
}
|