Unify issue assignee mention options
This commit is contained in:
@@ -428,12 +428,45 @@ private data class PilotSkillDetailData(
|
|||||||
val files: List<Models.SkillFile>,
|
val files: List<Models.SkillFile>,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class PilotAssigneeOption(
|
private data class PilotMentionOption(
|
||||||
val id: String?,
|
val id: String?,
|
||||||
val type: String?,
|
val type: String?,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun pilotMentionOptions(
|
||||||
|
includeEmpty: Boolean,
|
||||||
|
emptyLabel: String,
|
||||||
|
currentUser: Models.User?,
|
||||||
|
members: List<Models.Member>,
|
||||||
|
agents: List<Models.Agent>,
|
||||||
|
squads: List<Models.Squad>,
|
||||||
|
query: String = "",
|
||||||
|
): List<PilotMentionOption> {
|
||||||
|
val normalizedQuery = query.trim().lowercase()
|
||||||
|
return Models.issueAssignees(includeEmpty, emptyLabel, currentUser, members, agents, squads)
|
||||||
|
.filter { option ->
|
||||||
|
normalizedQuery.isBlank() ||
|
||||||
|
option.name.lowercase().contains(normalizedQuery) ||
|
||||||
|
option.id.orEmpty().lowercase().contains(normalizedQuery)
|
||||||
|
}
|
||||||
|
.map { PilotMentionOption(it.id, it.type, it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pilotMentionTypeLabel(type: String?, zh: Boolean): String = when (type) {
|
||||||
|
"member" -> if (zh) "人" else "Person"
|
||||||
|
"agent" -> "Agent"
|
||||||
|
"squad" -> if (zh) "小队" else "Squad"
|
||||||
|
else -> if (zh) "无" else "None"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pilotMentionTypeColor(type: String?): Color = when (type) {
|
||||||
|
"member" -> Color(0xFF16A34A)
|
||||||
|
"agent" -> MulticaColors.Accent
|
||||||
|
"squad" -> MulticaColors.Warning
|
||||||
|
else -> MulticaColors.Muted
|
||||||
|
}
|
||||||
|
|
||||||
private val PILOT_REACTION_EMOJIS = listOf("👍", "❤️", "👀", "🎉")
|
private val PILOT_REACTION_EMOJIS = listOf("👍", "❤️", "👀", "🎉")
|
||||||
private const val PILOT_INBOX_PAGE_SIZE = 20
|
private const val PILOT_INBOX_PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -6773,6 +6806,8 @@ private fun IssueFormMenuRow(
|
|||||||
items = options.map { option ->
|
items = options.map { option ->
|
||||||
MulticaCupertinoActionSheetItem(
|
MulticaCupertinoActionSheetItem(
|
||||||
title = option.label,
|
title = option.label,
|
||||||
|
badgeText = option.badgeText,
|
||||||
|
badgeColor = option.badgeColor,
|
||||||
contentDescription = "$contentDescription ${option.label}",
|
contentDescription = "$contentDescription ${option.label}",
|
||||||
selected = option.selected,
|
selected = option.selected,
|
||||||
onClick = option.onClick,
|
onClick = option.onClick,
|
||||||
@@ -6786,6 +6821,8 @@ private fun IssueFormMenuRow(
|
|||||||
|
|
||||||
private data class IssueFormMenuOption(
|
private data class IssueFormMenuOption(
|
||||||
val label: String,
|
val label: String,
|
||||||
|
val badgeText: String? = null,
|
||||||
|
val badgeColor: Color = MulticaColors.Accent,
|
||||||
val selected: Boolean,
|
val selected: Boolean,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
)
|
)
|
||||||
@@ -6840,14 +6877,14 @@ private fun PilotIssueForm(
|
|||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
val options = loaded.getOrThrow()
|
val options = loaded.getOrThrow()
|
||||||
val assignees = Models.issueAssignees(
|
val assignees = pilotMentionOptions(
|
||||||
true,
|
true,
|
||||||
if (zh) "未分配" else "Unassigned",
|
if (zh) "未分配" else "Unassigned",
|
||||||
session.user,
|
session.user,
|
||||||
options.members,
|
options.members,
|
||||||
options.agents,
|
options.agents,
|
||||||
options.squads,
|
options.squads,
|
||||||
).map { PilotAssigneeOption(it.id, it.type, it.name) }
|
)
|
||||||
var title by remember(issue?.id) { mutableStateOf(issue?.title.orEmpty()) }
|
var title by remember(issue?.id) { mutableStateOf(issue?.title.orEmpty()) }
|
||||||
var description by remember(issue?.id) { mutableStateOf(issue?.description.orEmpty()) }
|
var description by remember(issue?.id) { mutableStateOf(issue?.description.orEmpty()) }
|
||||||
var dueDate by remember(issue?.id) { mutableStateOf(issue?.dueDate.orEmpty()) }
|
var dueDate by remember(issue?.id) { mutableStateOf(issue?.dueDate.orEmpty()) }
|
||||||
@@ -6856,7 +6893,6 @@ private fun PilotIssueForm(
|
|||||||
var projectIndex by remember(issue?.id, options.projects.size) {
|
var projectIndex by remember(issue?.id, options.projects.size) {
|
||||||
mutableIntStateOf((options.projects.indexOfFirst { it.id == issue?.projectId } + 1).coerceAtLeast(0))
|
mutableIntStateOf((options.projects.indexOfFirst { it.id == issue?.projectId } + 1).coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
var teamIndex by remember(issue?.id, options.squads.size) { mutableIntStateOf(0) }
|
|
||||||
var statusIndex by remember(issue?.id) {
|
var statusIndex by remember(issue?.id) {
|
||||||
mutableIntStateOf(Models.STATUS_VALUES.indexOf(issue?.status ?: "todo").coerceAtLeast(0))
|
mutableIntStateOf(Models.STATUS_VALUES.indexOf(issue?.status ?: "todo").coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
@@ -6944,11 +6980,11 @@ private fun PilotIssueForm(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val projectId = if (projectIndex == 0) null else options.projects[projectIndex - 1].id
|
val projectId = if (projectIndex == 0) null else options.projects[projectIndex - 1].id
|
||||||
val teamId = if (!editing && teamIndex > 0) options.squads[teamIndex - 1].id else null
|
|
||||||
val dueDateText = if (dueDateEnabled) dueDate.trim() else ""
|
val dueDateText = if (dueDateEnabled) dueDate.trim() else ""
|
||||||
val status = Models.STATUS_VALUES[statusIndex]
|
val status = Models.STATUS_VALUES[statusIndex]
|
||||||
val priority = Models.PRIORITY_VALUES[priorityIndex]
|
val priority = Models.PRIORITY_VALUES[priorityIndex]
|
||||||
val selectedAssignee = assignees[assigneeIndex]
|
val selectedAssignee = assignees[assigneeIndex]
|
||||||
|
val teamId = if (!editing && selectedAssignee.type == "squad") selectedAssignee.id else null
|
||||||
val assignee = if (selectedAssignee.id == null) null else {
|
val assignee = if (selectedAssignee.id == null) null else {
|
||||||
Models.Assignee(selectedAssignee.id, selectedAssignee.type, selectedAssignee.name)
|
Models.Assignee(selectedAssignee.id, selectedAssignee.type, selectedAssignee.name)
|
||||||
}
|
}
|
||||||
@@ -7187,31 +7223,6 @@ private fun PilotIssueForm(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (!editing) {
|
|
||||||
IssueFormMenuRow(
|
|
||||||
eyebrow = if (zh) "小队" else "Squad",
|
|
||||||
title = if (teamIndex == 0) {
|
|
||||||
if (zh) "不选择小队" else "No squad"
|
|
||||||
} else {
|
|
||||||
options.squads[teamIndex - 1].name
|
|
||||||
},
|
|
||||||
subtitle = null,
|
|
||||||
contentDescription = "Issue Form Squad",
|
|
||||||
options = listOf(
|
|
||||||
IssueFormMenuOption(
|
|
||||||
label = if (zh) "不选择小队" else "No squad",
|
|
||||||
selected = teamIndex == 0,
|
|
||||||
onClick = { teamIndex = 0 },
|
|
||||||
)
|
|
||||||
) + options.squads.mapIndexed { index, squad ->
|
|
||||||
IssueFormMenuOption(
|
|
||||||
label = squad.name,
|
|
||||||
selected = teamIndex == index + 1,
|
|
||||||
onClick = { teamIndex = index + 1 },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IssueFormMenuRow(
|
IssueFormMenuRow(
|
||||||
eyebrow = if (zh) "负责人" else "Assignee",
|
eyebrow = if (zh) "负责人" else "Assignee",
|
||||||
title = assignees[assigneeIndex].name,
|
title = assignees[assigneeIndex].name,
|
||||||
@@ -7220,6 +7231,8 @@ private fun PilotIssueForm(
|
|||||||
options = assignees.mapIndexed { index, assignee ->
|
options = assignees.mapIndexed { index, assignee ->
|
||||||
IssueFormMenuOption(
|
IssueFormMenuOption(
|
||||||
label = assignee.name,
|
label = assignee.name,
|
||||||
|
badgeText = pilotMentionTypeLabel(assignee.type, zh),
|
||||||
|
badgeColor = pilotMentionTypeColor(assignee.type),
|
||||||
selected = assigneeIndex == index,
|
selected = assigneeIndex == index,
|
||||||
onClick = { assigneeIndex = index },
|
onClick = { assigneeIndex = index },
|
||||||
)
|
)
|
||||||
@@ -10860,34 +10873,7 @@ private fun IssueCommentReplyBox(
|
|||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val replyActionModifier = Modifier.size(42.dp)
|
val replyActionModifier = Modifier.size(42.dp)
|
||||||
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||||
val query = mentionQuery.trim().lowercase()
|
pilotMentionOptions(false, "", null, members, agents.filter { it.archivedAt.isBlank() }, squads, mentionQuery)
|
||||||
val agentItems = agents
|
|
||||||
.filter { it.archivedAt.isBlank() }
|
|
||||||
.filter { agent ->
|
|
||||||
query.isBlank() ||
|
|
||||||
agent.name.lowercase().contains(query) ||
|
|
||||||
agent.description.lowercase().contains(query) ||
|
|
||||||
agent.id.lowercase().contains(query)
|
|
||||||
}
|
|
||||||
.sortedBy { it.name.lowercase() }
|
|
||||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "agent") }
|
|
||||||
val memberItems = members
|
|
||||||
.filter { member ->
|
|
||||||
query.isBlank() ||
|
|
||||||
member.displayName.lowercase().contains(query) ||
|
|
||||||
member.email.lowercase().contains(query)
|
|
||||||
}
|
|
||||||
.sortedBy { it.displayName.lowercase() }
|
|
||||||
.map { Triple(it.id, it.displayName, "member") }
|
|
||||||
val squadItems = squads
|
|
||||||
.filter { squad ->
|
|
||||||
query.isBlank() ||
|
|
||||||
squad.name.lowercase().contains(query) ||
|
|
||||||
squad.description.lowercase().contains(query)
|
|
||||||
}
|
|
||||||
.sortedBy { it.name.lowercase() }
|
|
||||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "squad") }
|
|
||||||
agentItems + memberItems + squadItems
|
|
||||||
}
|
}
|
||||||
LaunchedEffect(parentId) {
|
LaunchedEffect(parentId) {
|
||||||
delay(180)
|
delay(180)
|
||||||
@@ -11029,12 +11015,15 @@ Column(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
mentionItems.forEach { (id, name, type) ->
|
mentionItems.forEach { item ->
|
||||||
IssueMentionUnifiedRow(
|
IssueMentionUnifiedRow(
|
||||||
name = name,
|
name = item.name,
|
||||||
mentionType = type,
|
mentionType = item.type.orEmpty(),
|
||||||
contentDescription = "Issue Comment Reply Mention $type Row $id",
|
zh = zh,
|
||||||
|
contentDescription = "Issue Comment Reply Mention ${item.type} Row ${item.id}",
|
||||||
onClick = {
|
onClick = {
|
||||||
|
val name = item.name
|
||||||
|
val id = item.id.orEmpty()
|
||||||
val insertion = AgentMentionMarkdown.displayText(name, id)
|
val insertion = AgentMentionMarkdown.displayText(name, id)
|
||||||
val separator = if (draft.isBlank() || draft.endsWith(" ") || draft.endsWith("\n")) "" else " "
|
val separator = if (draft.isBlank() || draft.endsWith(" ") || draft.endsWith("\n")) "" else " "
|
||||||
onDraftChange(draft + separator + insertion + " ")
|
onDraftChange(draft + separator + insertion + " ")
|
||||||
@@ -11125,6 +11114,7 @@ private fun IssueMentionAgentWebRow(
|
|||||||
private fun IssueMentionUnifiedRow(
|
private fun IssueMentionUnifiedRow(
|
||||||
name: String,
|
name: String,
|
||||||
mentionType: String,
|
mentionType: String,
|
||||||
|
zh: Boolean,
|
||||||
contentDescription: String,
|
contentDescription: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -11133,21 +11123,13 @@ private fun IssueMentionUnifiedRow(
|
|||||||
"squad" -> name.take(1).uppercase().ifBlank { "S" }
|
"squad" -> name.take(1).uppercase().ifBlank { "S" }
|
||||||
else -> name.take(1).uppercase().ifBlank { "A" }
|
else -> name.take(1).uppercase().ifBlank { "A" }
|
||||||
}
|
}
|
||||||
val iconColor = when (mentionType) {
|
val iconColor = pilotMentionTypeColor(mentionType)
|
||||||
"member" -> MulticaColors.Text
|
|
||||||
"squad" -> MulticaColors.Accent
|
|
||||||
else -> MulticaColors.Accent
|
|
||||||
}
|
|
||||||
val iconBg = when (mentionType) {
|
val iconBg = when (mentionType) {
|
||||||
"member" -> MulticaColors.AccentSoft.copy(alpha = 0.12f)
|
"member" -> MulticaColors.AccentSoft.copy(alpha = 0.12f)
|
||||||
"squad" -> MulticaColors.AccentSoft.copy(alpha = 0.36f)
|
"squad" -> MulticaColors.AccentSoft.copy(alpha = 0.36f)
|
||||||
else -> MulticaColors.AccentSoft.copy(alpha = 0.24f)
|
else -> MulticaColors.AccentSoft.copy(alpha = 0.24f)
|
||||||
}
|
}
|
||||||
val typeLabel = when (mentionType) {
|
val typeLabel = pilotMentionTypeLabel(mentionType, zh)
|
||||||
"member" -> "member"
|
|
||||||
"squad" -> "squad"
|
|
||||||
else -> "agent"
|
|
||||||
}
|
|
||||||
val mentionMarkdown = when (mentionType) {
|
val mentionMarkdown = when (mentionType) {
|
||||||
"member" -> AgentMentionMarkdown.memberMarkdown(name, "")
|
"member" -> AgentMentionMarkdown.memberMarkdown(name, "")
|
||||||
"squad" -> AgentMentionMarkdown.squadMarkdown(name, "")
|
"squad" -> AgentMentionMarkdown.squadMarkdown(name, "")
|
||||||
@@ -11244,34 +11226,7 @@ private fun IssueCommentInputBar(
|
|||||||
configuration.screenHeightDp,
|
configuration.screenHeightDp,
|
||||||
).dp
|
).dp
|
||||||
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||||
val query = mentionQuery.trim().lowercase()
|
pilotMentionOptions(false, "", null, members, agents.filter { it.archivedAt.isBlank() }, squads, mentionQuery)
|
||||||
val agentItems = agents
|
|
||||||
.filter { it.archivedAt.isBlank() }
|
|
||||||
.filter { agent ->
|
|
||||||
query.isBlank() ||
|
|
||||||
agent.name.lowercase().contains(query) ||
|
|
||||||
agent.description.lowercase().contains(query) ||
|
|
||||||
agent.id.lowercase().contains(query)
|
|
||||||
}
|
|
||||||
.sortedBy { it.name.lowercase() }
|
|
||||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "agent") }
|
|
||||||
val memberItems = members
|
|
||||||
.filter { member ->
|
|
||||||
query.isBlank() ||
|
|
||||||
member.displayName.lowercase().contains(query) ||
|
|
||||||
member.email.lowercase().contains(query)
|
|
||||||
}
|
|
||||||
.sortedBy { it.displayName.lowercase() }
|
|
||||||
.map { Triple(it.id, it.displayName, "member") }
|
|
||||||
val squadItems = squads
|
|
||||||
.filter { squad ->
|
|
||||||
query.isBlank() ||
|
|
||||||
squad.name.lowercase().contains(query) ||
|
|
||||||
squad.description.lowercase().contains(query)
|
|
||||||
}
|
|
||||||
.sortedBy { it.name.lowercase() }
|
|
||||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "squad") }
|
|
||||||
agentItems + memberItems + squadItems
|
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -11437,12 +11392,15 @@ Column(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
mentionItems.forEach { (id, name, type) ->
|
mentionItems.forEach { item ->
|
||||||
IssueMentionUnifiedRow(
|
IssueMentionUnifiedRow(
|
||||||
name = name,
|
name = item.name,
|
||||||
mentionType = type,
|
mentionType = item.type.orEmpty(),
|
||||||
contentDescription = "Issue Comment Mention $type Row $id",
|
zh = zh,
|
||||||
|
contentDescription = "Issue Comment Mention ${item.type} Row ${item.id}",
|
||||||
onClick = {
|
onClick = {
|
||||||
|
val name = item.name
|
||||||
|
val id = item.id.orEmpty()
|
||||||
val insertion = AgentMentionMarkdown.displayText(name, id)
|
val insertion = AgentMentionMarkdown.displayText(name, id)
|
||||||
val separator = if (draft.isBlank() || draft.endsWith(" ") || draft.endsWith("\n")) "" else " "
|
val separator = if (draft.isBlank() || draft.endsWith(" ") || draft.endsWith("\n")) "" else " "
|
||||||
onDraftChange(draft + separator + insertion + " ")
|
onDraftChange(draft + separator + insertion + " ")
|
||||||
|
|||||||
@@ -1021,6 +1021,8 @@ data class MulticaCupertinoActionSheetItem(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val subtitle: String? = null,
|
val subtitle: String? = null,
|
||||||
val icon: ImageVector? = null,
|
val icon: ImageVector? = null,
|
||||||
|
val badgeText: String? = null,
|
||||||
|
val badgeColor: Color = MulticaColors.Accent,
|
||||||
val contentDescription: String? = null,
|
val contentDescription: String? = null,
|
||||||
val selected: Boolean = false,
|
val selected: Boolean = false,
|
||||||
val destructive: Boolean = false,
|
val destructive: Boolean = false,
|
||||||
@@ -1093,6 +1095,24 @@ fun MulticaCupertinoActionSheet(
|
|||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!item.badgeText.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = item.badgeText,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(999.dp))
|
||||||
|
.background(item.badgeColor.copy(alpha = 0.16f))
|
||||||
|
.border(1.dp, item.badgeColor.copy(alpha = 0.32f), RoundedCornerShape(999.dp))
|
||||||
|
.padding(horizontal = 7.dp, vertical = 3.dp),
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
color = item.badgeColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = item.title,
|
||||||
|
|||||||
Reference in New Issue
Block a user