Add squad mentions and assignee support
This commit is contained in:
@@ -748,6 +748,18 @@ final class ApiClient {
|
|||||||
return parseArray(extractArray(json, "agents"), Models.Agent::new);
|
return parseArray(extractArray(json, "agents"), Models.Agent::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
List<Models.Squad> squads(String workspaceId) throws Exception {
|
||||||
|
return squads(workspaceId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Models.Squad> squads(String workspaceId, boolean includeArchived) throws Exception {
|
||||||
|
JSONObject query = query(null, "workspace_id", workspaceId);
|
||||||
|
if (includeArchived) query.put("include_archived", "true");
|
||||||
|
JSONObject json = requestObject("GET", "/api/squads", query, null);
|
||||||
|
return parseArray(extractArray(json, "squads"), Models.Squad::new);
|
||||||
|
}
|
||||||
|
|
||||||
List<Models.Runtime> runtimes(String workspaceId) throws Exception {
|
List<Models.Runtime> runtimes(String workspaceId) throws Exception {
|
||||||
JSONObject json = requestObject("GET", "/api/runtimes", query(null, "workspace_id", workspaceId), null);
|
JSONObject json = requestObject("GET", "/api/runtimes", query(null, "workspace_id", workspaceId), null);
|
||||||
return parseArray(extractArray(json, "runtimes"), Models.Runtime::new);
|
return parseArray(extractArray(json, "runtimes"), Models.Runtime::new);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -367,6 +369,7 @@ private data class PilotIssueDetailData(
|
|||||||
val projects: List<Models.Project>,
|
val projects: List<Models.Project>,
|
||||||
val members: List<Models.Member>,
|
val members: List<Models.Member>,
|
||||||
val agents: List<Models.Agent>,
|
val agents: List<Models.Agent>,
|
||||||
|
val squads: List<Models.Squad>,
|
||||||
val issueLabels: List<Models.IssueLabel>,
|
val issueLabels: List<Models.IssueLabel>,
|
||||||
val allLabels: List<Models.IssueLabel>,
|
val allLabels: List<Models.IssueLabel>,
|
||||||
val subscribers: List<Models.IssueSubscriber>,
|
val subscribers: List<Models.IssueSubscriber>,
|
||||||
@@ -381,6 +384,7 @@ private data class PilotIssueFormOptions(
|
|||||||
val projects: List<Models.Project>,
|
val projects: List<Models.Project>,
|
||||||
val members: List<Models.Member>,
|
val members: List<Models.Member>,
|
||||||
val agents: List<Models.Agent>,
|
val agents: List<Models.Agent>,
|
||||||
|
val squads: List<Models.Squad>,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class PilotChatMessagesData(
|
private data class PilotChatMessagesData(
|
||||||
@@ -5622,6 +5626,7 @@ private fun PilotIssues(
|
|||||||
var movingBoardIssueId by remember(personal, workspaceId) { mutableStateOf<String?>(null) }
|
var movingBoardIssueId by remember(personal, workspaceId) { mutableStateOf<String?>(null) }
|
||||||
var assigneeMembers by remember(workspaceId) { mutableStateOf<List<Models.Member>>(emptyList()) }
|
var assigneeMembers by remember(workspaceId) { mutableStateOf<List<Models.Member>>(emptyList()) }
|
||||||
var assigneeAgents by remember(workspaceId) { mutableStateOf<List<Models.Agent>>(emptyList()) }
|
var assigneeAgents by remember(workspaceId) { mutableStateOf<List<Models.Agent>>(emptyList()) }
|
||||||
|
var assigneeSquads by remember(workspaceId) { mutableStateOf<List<Models.Squad>>(emptyList()) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(personal, myIssueScope, issues) {
|
LaunchedEffect(personal, myIssueScope, issues) {
|
||||||
@@ -5733,6 +5738,7 @@ private fun PilotIssues(
|
|||||||
}.onSuccess { (members, agents) ->
|
}.onSuccess { (members, agents) ->
|
||||||
assigneeMembers = members
|
assigneeMembers = members
|
||||||
assigneeAgents = agents
|
assigneeAgents = agents
|
||||||
|
assigneeSquads = runCatching { api.squads(workspaceId) }.getOrElse { emptyList() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6070,6 +6076,7 @@ private fun PilotIssues(
|
|||||||
issue,
|
issue,
|
||||||
assigneeMembers,
|
assigneeMembers,
|
||||||
assigneeAgents,
|
assigneeAgents,
|
||||||
|
assigneeSquads,
|
||||||
currentUser,
|
currentUser,
|
||||||
)
|
)
|
||||||
if (isNotBlank()) append(" · ")
|
if (isNotBlank()) append(" · ")
|
||||||
@@ -6772,6 +6779,7 @@ private fun PilotIssueForm(
|
|||||||
projects = api.projects(session.workspace.id, 500, 0).items,
|
projects = api.projects(session.workspace.id, 500, 0).items,
|
||||||
members = api.members(session.workspace.id),
|
members = api.members(session.workspace.id),
|
||||||
agents = api.agents(session.workspace.id),
|
agents = api.agents(session.workspace.id),
|
||||||
|
squads = runCatching { api.squads(session.workspace.id) }.getOrElse { emptyList() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6792,6 +6800,7 @@ private fun PilotIssueForm(
|
|||||||
session.user,
|
session.user,
|
||||||
options.members,
|
options.members,
|
||||||
options.agents,
|
options.agents,
|
||||||
|
options.squads,
|
||||||
).map { PilotAssigneeOption(it.id, it.type, it.name) }
|
).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()) }
|
||||||
@@ -8868,6 +8877,7 @@ private fun IssueCommentCard(
|
|||||||
authorAgentStatus: String? = null,
|
authorAgentStatus: String? = null,
|
||||||
members: List<Models.Member> = emptyList(),
|
members: List<Models.Member> = emptyList(),
|
||||||
agents: List<Models.Agent> = emptyList(),
|
agents: List<Models.Agent> = emptyList(),
|
||||||
|
squads: List<Models.Squad> = emptyList(),
|
||||||
currentUser: Models.User? = null,
|
currentUser: Models.User? = null,
|
||||||
onIssueIdClick: ((String) -> Unit)? = null,
|
onIssueIdClick: ((String) -> Unit)? = null,
|
||||||
onIssueReferenceClick: ((String) -> Unit)? = null,
|
onIssueReferenceClick: ((String) -> Unit)? = null,
|
||||||
@@ -8962,6 +8972,7 @@ private fun IssueCommentCard(
|
|||||||
renderedContent,
|
renderedContent,
|
||||||
members = members,
|
members = members,
|
||||||
agents = agents,
|
agents = agents,
|
||||||
|
squads = squads,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
attachments = comment.attachments,
|
attachments = comment.attachments,
|
||||||
onIssueIdClick = onIssueIdClick,
|
onIssueIdClick = onIssueIdClick,
|
||||||
@@ -9387,6 +9398,7 @@ private fun PilotIssueDetail(
|
|||||||
projects = core.projects,
|
projects = core.projects,
|
||||||
members = core.members,
|
members = core.members,
|
||||||
agents = core.agents,
|
agents = core.agents,
|
||||||
|
squads = emptyList(),
|
||||||
issueLabels = emptyList(),
|
issueLabels = emptyList(),
|
||||||
allLabels = emptyList(),
|
allLabels = emptyList(),
|
||||||
subscribers = emptyList(),
|
subscribers = emptyList(),
|
||||||
@@ -9417,9 +9429,11 @@ private fun PilotIssueDetail(
|
|||||||
val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id
|
val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id
|
||||||
val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull()
|
val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull()
|
||||||
val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() }
|
val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() }
|
||||||
|
val squads = runCatching { api.squads(workspaceId) }.getOrElse { emptyList() }
|
||||||
initialData.copy(
|
initialData.copy(
|
||||||
parentIssue = parentIssue,
|
parentIssue = parentIssue,
|
||||||
projects = projects,
|
projects = projects,
|
||||||
|
squads = squads,
|
||||||
issueLabels = issueLabels,
|
issueLabels = issueLabels,
|
||||||
allLabels = allLabels,
|
allLabels = allLabels,
|
||||||
subscribers = subscribers,
|
subscribers = subscribers,
|
||||||
@@ -9651,7 +9665,7 @@ private fun PilotIssueDetail(
|
|||||||
Models.Assignee(
|
Models.Assignee(
|
||||||
assigneeId,
|
assigneeId,
|
||||||
data.issue.assigneeType ?: "member",
|
data.issue.assigneeType ?: "member",
|
||||||
Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
|
Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, data.squads, currentUser),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -9803,7 +9817,7 @@ private fun PilotIssueDetail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendNewComment() {
|
fun sendNewComment() {
|
||||||
val content = AgentMentionMarkdown.markdownFromDraft(newCommentDraft.trim(), data.agents)
|
val content = AgentMentionMarkdown.markdownFromDraft(newCommentDraft.trim(), data.agents, data.members, data.squads)
|
||||||
if (sendingNewComment || content.isEmpty() && pendingCommentAttachments.isEmpty()) return
|
if (sendingNewComment || content.isEmpty() && pendingCommentAttachments.isEmpty()) return
|
||||||
sendingNewComment = true
|
sendingNewComment = true
|
||||||
newCommentMessage = null
|
newCommentMessage = null
|
||||||
@@ -9826,7 +9840,7 @@ private fun PilotIssueDetail(
|
|||||||
|
|
||||||
fun sendReply(thread: IssueCommentThreads.Thread) {
|
fun sendReply(thread: IssueCommentThreads.Thread) {
|
||||||
val parentId = thread.root.id
|
val parentId = thread.root.id
|
||||||
val explicitContent = AgentMentionMarkdown.markdownFromDraft(replyDraft.trim(), data.agents)
|
val explicitContent = AgentMentionMarkdown.markdownFromDraft(replyDraft.trim(), data.agents, data.members, data.squads)
|
||||||
val content = IssueCommentThreads.replyContentForThread(thread, explicitContent, data.agents)
|
val content = IssueCommentThreads.replyContentForThread(thread, explicitContent, data.agents)
|
||||||
if (sendingReply || (content.isEmpty() && pendingReplyAttachments.isEmpty())) return
|
if (sendingReply || (content.isEmpty() && pendingReplyAttachments.isEmpty())) return
|
||||||
sendingReply = true
|
sendingReply = true
|
||||||
@@ -9925,12 +9939,14 @@ private fun PilotIssueDetail(
|
|||||||
contentDescription = "Issue Comment Bottom Haze Composer $WEB_SSOT_COMMENT_INPUT"
|
contentDescription = "Issue Comment Bottom Haze Composer $WEB_SSOT_COMMENT_INPUT"
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (replyingThread != null) {
|
if (replyingThread != null) {
|
||||||
IssueCommentReplyBox(
|
IssueCommentReplyBox(
|
||||||
parentId = replyingThread.root.id,
|
parentId = replyingThread.root.id,
|
||||||
draft = replyDraft,
|
draft = replyDraft,
|
||||||
pendingAttachments = pendingReplyAttachments,
|
pendingAttachments = pendingReplyAttachments,
|
||||||
agents = data.agents,
|
agents = data.agents,
|
||||||
|
members = data.members,
|
||||||
|
squads = data.squads,
|
||||||
uploadingAttachment = uploadingReplyAttachment,
|
uploadingAttachment = uploadingReplyAttachment,
|
||||||
sending = sendingReply,
|
sending = sendingReply,
|
||||||
zh = zh,
|
zh = zh,
|
||||||
@@ -9954,6 +9970,8 @@ private fun PilotIssueDetail(
|
|||||||
draft = newCommentDraft,
|
draft = newCommentDraft,
|
||||||
pendingAttachments = pendingCommentAttachments,
|
pendingAttachments = pendingCommentAttachments,
|
||||||
agents = data.agents,
|
agents = data.agents,
|
||||||
|
members = data.members,
|
||||||
|
squads = data.squads,
|
||||||
uploadingAttachment = uploadingCommentAttachment,
|
uploadingAttachment = uploadingCommentAttachment,
|
||||||
sending = sendingNewComment,
|
sending = sendingNewComment,
|
||||||
zh = zh,
|
zh = zh,
|
||||||
@@ -9988,7 +10006,7 @@ private fun PilotIssueDetail(
|
|||||||
IssueDetailCompactMetadata(
|
IssueDetailCompactMetadata(
|
||||||
issue = data.issue,
|
issue = data.issue,
|
||||||
issueLabels = data.issueLabels,
|
issueLabels = data.issueLabels,
|
||||||
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
|
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, data.squads, currentUser),
|
||||||
project = projectName,
|
project = projectName,
|
||||||
zh = zh,
|
zh = zh,
|
||||||
)
|
)
|
||||||
@@ -10026,6 +10044,7 @@ private fun PilotIssueDetail(
|
|||||||
data.issue.description,
|
data.issue.description,
|
||||||
members = data.members,
|
members = data.members,
|
||||||
agents = data.agents,
|
agents = data.agents,
|
||||||
|
squads = data.squads,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
attachments = data.attachments,
|
attachments = data.attachments,
|
||||||
onIssueIdClick = { onOpenIssue(it) },
|
onIssueIdClick = { onOpenIssue(it) },
|
||||||
@@ -10185,6 +10204,7 @@ private fun PilotIssueDetail(
|
|||||||
authorAgentStatus = commentAuthorAgentStatus(comment, data.agents),
|
authorAgentStatus = commentAuthorAgentStatus(comment, data.agents),
|
||||||
members = data.members,
|
members = data.members,
|
||||||
agents = data.agents,
|
agents = data.agents,
|
||||||
|
squads = data.squads,
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
onIssueIdClick = { onOpenIssue(it) },
|
onIssueIdClick = { onOpenIssue(it) },
|
||||||
onIssueReferenceClick = { openIssueReference(it) },
|
onIssueReferenceClick = { openIssueReference(it) },
|
||||||
@@ -10741,6 +10761,8 @@ private fun IssueCommentReplyBox(
|
|||||||
draft: String,
|
draft: String,
|
||||||
pendingAttachments: List<Models.Attachment>,
|
pendingAttachments: List<Models.Attachment>,
|
||||||
agents: List<Models.Agent>,
|
agents: List<Models.Agent>,
|
||||||
|
members: List<Models.Member>,
|
||||||
|
squads: List<Models.Squad>,
|
||||||
uploadingAttachment: Boolean,
|
uploadingAttachment: Boolean,
|
||||||
sending: Boolean,
|
sending: Boolean,
|
||||||
zh: Boolean,
|
zh: Boolean,
|
||||||
@@ -10757,10 +10779,10 @@ private fun IssueCommentReplyBox(
|
|||||||
val focusRequester = remember(parentId) { FocusRequester() }
|
val focusRequester = remember(parentId) { FocusRequester() }
|
||||||
val keyboard = LocalSoftwareKeyboardController.current
|
val keyboard = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val replyActionModifier = Modifier.size(42.dp)
|
val replyActionModifier = Modifier.size(42.dp)
|
||||||
val mentionAgents = remember(agents, mentionQuery) {
|
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||||
val query = mentionQuery.trim().lowercase()
|
val query = mentionQuery.trim().lowercase()
|
||||||
agents
|
val agentItems = agents
|
||||||
.filter { it.archivedAt.isBlank() }
|
.filter { it.archivedAt.isBlank() }
|
||||||
.filter { agent ->
|
.filter { agent ->
|
||||||
query.isBlank() ||
|
query.isBlank() ||
|
||||||
@@ -10769,7 +10791,24 @@ private fun IssueCommentReplyBox(
|
|||||||
agent.id.lowercase().contains(query)
|
agent.id.lowercase().contains(query)
|
||||||
}
|
}
|
||||||
.sortedBy { it.name.lowercase() }
|
.sortedBy { it.name.lowercase() }
|
||||||
.take(8)
|
.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)
|
||||||
@@ -10871,11 +10910,12 @@ private fun IssueCommentReplyBox(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
.semantics { contentDescription = "Issue Comment Reply Mention Web Picker" },
|
.semantics { contentDescription = "Issue Comment Reply Mention Web Picker" },
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (zh) "选择 Agent" else "Mention agent",
|
text = if (zh) "选择 @对象" else "Mention someone",
|
||||||
modifier = Modifier.semantics { contentDescription = "Issue Comment Mention Web Header" },
|
modifier = Modifier.semantics { contentDescription = "Issue Comment Mention Web Header" },
|
||||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
||||||
color = MulticaColors.Text,
|
color = MulticaColors.Text,
|
||||||
@@ -10885,22 +10925,30 @@ private fun IssueCommentReplyBox(
|
|||||||
onValueChange = { mentionQuery = it },
|
onValueChange = { mentionQuery = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentDescription = "Issue Comment Reply Mention Search",
|
contentDescription = "Issue Comment Reply Mention Search",
|
||||||
label = if (zh) "搜索 Agent" else "Search agents",
|
label = if (zh) "搜索人、Agent、小队" else "Search people, agents, squads",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
if (mentionAgents.isEmpty()) {
|
if (mentionItems.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = if (zh) "没有可提及的 Agent" else "No agents available",
|
text = if (zh) "没有可提及的对象" else "No mentionable items",
|
||||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp),
|
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp),
|
||||||
color = MulticaColors.Muted,
|
color = MulticaColors.Muted,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mentionAgents.forEach { agent ->
|
Column(
|
||||||
IssueMentionAgentWebRow(
|
modifier = Modifier
|
||||||
agent = agent,
|
.fillMaxWidth()
|
||||||
contentDescription = "Issue Comment Reply Mention Agent Row ${agent.id}",
|
.heightIn(max = 240.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
mentionItems.forEach { (id, name, type) ->
|
||||||
|
IssueMentionUnifiedRow(
|
||||||
|
name = name,
|
||||||
|
mentionType = type,
|
||||||
|
contentDescription = "Issue Comment Reply Mention $type Row $id",
|
||||||
onClick = {
|
onClick = {
|
||||||
val insertion = AgentMentionMarkdown.displayText(agent.name, agent.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 + " ")
|
||||||
mentionPickerOpen = false
|
mentionPickerOpen = false
|
||||||
@@ -10913,6 +10961,7 @@ private fun IssueCommentReplyBox(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -10985,11 +11034,110 @@ private fun IssueMentionAgentWebRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IssueMentionUnifiedRow(
|
||||||
|
name: String,
|
||||||
|
mentionType: String,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val iconLetter = when (mentionType) {
|
||||||
|
"member" -> name.take(1).uppercase().ifBlank { "M" }
|
||||||
|
"squad" -> name.take(1).uppercase().ifBlank { "S" }
|
||||||
|
else -> name.take(1).uppercase().ifBlank { "A" }
|
||||||
|
}
|
||||||
|
val iconColor = when (mentionType) {
|
||||||
|
"member" -> MulticaColors.Text
|
||||||
|
"squad" -> MulticaColors.Accent
|
||||||
|
else -> MulticaColors.Accent
|
||||||
|
}
|
||||||
|
val iconBg = when (mentionType) {
|
||||||
|
"member" -> MulticaColors.AccentSoft.copy(alpha = 0.12f)
|
||||||
|
"squad" -> MulticaColors.AccentSoft.copy(alpha = 0.36f)
|
||||||
|
else -> MulticaColors.AccentSoft.copy(alpha = 0.24f)
|
||||||
|
}
|
||||||
|
val typeLabel = when (mentionType) {
|
||||||
|
"member" -> "member"
|
||||||
|
"squad" -> "squad"
|
||||||
|
else -> "agent"
|
||||||
|
}
|
||||||
|
val mentionMarkdown = when (mentionType) {
|
||||||
|
"member" -> AgentMentionMarkdown.memberMarkdown(name, "")
|
||||||
|
"squad" -> AgentMentionMarkdown.squadMarkdown(name, "")
|
||||||
|
else -> AgentMentionMarkdown.markdown(name, "")
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.semantics(mergeDescendants = true) {
|
||||||
|
this.contentDescription = contentDescription
|
||||||
|
semanticsOnClick(label = name) {
|
||||||
|
onClick()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = MulticaColors.SurfaceElevated.copy(alpha = 0.78f),
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
shadowElevation = 0.dp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(1.dp, MulticaColors.Border.copy(alpha = 0.58f), RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(27.dp)
|
||||||
|
.clip(RoundedCornerShape(999.dp))
|
||||||
|
.background(iconBg),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = iconLetter,
|
||||||
|
style = MaterialTheme.typography.labelMedium.copy(fontSize = 11.sp, lineHeight = 13.sp, fontWeight = FontWeight.SemiBold),
|
||||||
|
color = iconColor,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = MaterialTheme.typography.titleSmall.copy(fontSize = 13.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MulticaColors.Text,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = typeLabel,
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp, lineHeight = 12.sp),
|
||||||
|
color = MulticaColors.Muted,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MulticaColors.TextTertiary,
|
||||||
|
modifier = Modifier.size(17.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IssueCommentInputBar(
|
private fun IssueCommentInputBar(
|
||||||
draft: String,
|
draft: String,
|
||||||
pendingAttachments: List<Models.Attachment>,
|
pendingAttachments: List<Models.Attachment>,
|
||||||
agents: List<Models.Agent>,
|
agents: List<Models.Agent>,
|
||||||
|
members: List<Models.Member>,
|
||||||
|
squads: List<Models.Squad>,
|
||||||
uploadingAttachment: Boolean,
|
uploadingAttachment: Boolean,
|
||||||
sending: Boolean,
|
sending: Boolean,
|
||||||
zh: Boolean,
|
zh: Boolean,
|
||||||
@@ -11000,9 +11148,9 @@ private fun IssueCommentInputBar(
|
|||||||
) {
|
) {
|
||||||
var mentionPickerOpen by remember { mutableStateOf(false) }
|
var mentionPickerOpen by remember { mutableStateOf(false) }
|
||||||
var mentionQuery by remember { mutableStateOf("") }
|
var mentionQuery by remember { mutableStateOf("") }
|
||||||
val mentionAgents = remember(agents, mentionQuery) {
|
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||||
val query = mentionQuery.trim().lowercase()
|
val query = mentionQuery.trim().lowercase()
|
||||||
agents
|
val agentItems = agents
|
||||||
.filter { it.archivedAt.isBlank() }
|
.filter { it.archivedAt.isBlank() }
|
||||||
.filter { agent ->
|
.filter { agent ->
|
||||||
query.isBlank() ||
|
query.isBlank() ||
|
||||||
@@ -11011,7 +11159,24 @@ private fun IssueCommentInputBar(
|
|||||||
agent.id.lowercase().contains(query)
|
agent.id.lowercase().contains(query)
|
||||||
}
|
}
|
||||||
.sortedBy { it.name.lowercase() }
|
.sortedBy { it.name.lowercase() }
|
||||||
.take(12)
|
.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
|
||||||
@@ -11114,11 +11279,12 @@ private fun IssueCommentInputBar(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
.semantics { contentDescription = "Issue Comment Mention Web Picker" },
|
.semantics { contentDescription = "Issue Comment Mention Web Picker" },
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (zh) "选择 Agent" else "Mention agent",
|
text = if (zh) "选择 @对象" else "Mention someone",
|
||||||
modifier = Modifier.semantics { contentDescription = "Issue Comment Mention Web Header" },
|
modifier = Modifier.semantics { contentDescription = "Issue Comment Mention Web Header" },
|
||||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
||||||
color = MulticaColors.Text,
|
color = MulticaColors.Text,
|
||||||
@@ -11128,22 +11294,30 @@ private fun IssueCommentInputBar(
|
|||||||
onValueChange = { mentionQuery = it },
|
onValueChange = { mentionQuery = it },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentDescription = "Issue Comment Mention Search",
|
contentDescription = "Issue Comment Mention Search",
|
||||||
label = if (zh) "搜索 Agent" else "Search agents",
|
label = if (zh) "搜索人、Agent、小队" else "Search people, agents, squads",
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
if (mentionAgents.isEmpty()) {
|
if (mentionItems.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = if (zh) "没有可提及的 Agent" else "No agents available",
|
text = if (zh) "没有可提及的对象" else "No mentionable items",
|
||||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp),
|
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp),
|
||||||
color = MulticaColors.Muted,
|
color = MulticaColors.Muted,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mentionAgents.forEach { agent ->
|
Column(
|
||||||
IssueMentionAgentWebRow(
|
modifier = Modifier
|
||||||
agent = agent,
|
.fillMaxWidth()
|
||||||
contentDescription = "Issue Comment Mention Agent Row ${agent.id}",
|
.heightIn(max = 240.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
mentionItems.forEach { (id, name, type) ->
|
||||||
|
IssueMentionUnifiedRow(
|
||||||
|
name = name,
|
||||||
|
mentionType = type,
|
||||||
|
contentDescription = "Issue Comment Mention $type Row $id",
|
||||||
onClick = {
|
onClick = {
|
||||||
val insertion = AgentMentionMarkdown.displayText(agent.name, agent.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 + " ")
|
||||||
mentionPickerOpen = false
|
mentionPickerOpen = false
|
||||||
@@ -11156,6 +11330,7 @@ private fun IssueCommentInputBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -11583,6 +11758,7 @@ private fun MarkdownBlock(
|
|||||||
markdown: String,
|
markdown: String,
|
||||||
members: List<Models.Member> = emptyList(),
|
members: List<Models.Member> = emptyList(),
|
||||||
agents: List<Models.Agent> = emptyList(),
|
agents: List<Models.Agent> = emptyList(),
|
||||||
|
squads: List<Models.Squad> = emptyList(),
|
||||||
currentUser: Models.User? = null,
|
currentUser: Models.User? = null,
|
||||||
attachments: List<Models.Attachment> = emptyList(),
|
attachments: List<Models.Attachment> = emptyList(),
|
||||||
onIssueIdClick: ((String) -> Unit)? = null,
|
onIssueIdClick: ((String) -> Unit)? = null,
|
||||||
@@ -11591,7 +11767,7 @@ private fun MarkdownBlock(
|
|||||||
val textColor = MulticaColors.Text.toArgb()
|
val textColor = MulticaColors.Text.toArgb()
|
||||||
val mutedColor = MulticaColors.Muted.toArgb()
|
val mutedColor = MulticaColors.Muted.toArgb()
|
||||||
val borderColor = MulticaColors.Border.toArgb()
|
val borderColor = MulticaColors.Border.toArgb()
|
||||||
val linkHandler = remember(members, agents, currentUser, onIssueIdClick, onIssueReferenceClick) {
|
val linkHandler = remember(members, agents, squads, currentUser, onIssueIdClick, onIssueReferenceClick) {
|
||||||
object : MarkdownRenderer.LinkHandler {
|
object : MarkdownRenderer.LinkHandler {
|
||||||
override fun resolveMentionLabel(
|
override fun resolveMentionLabel(
|
||||||
mentionType: String,
|
mentionType: String,
|
||||||
@@ -11604,6 +11780,7 @@ private fun MarkdownBlock(
|
|||||||
fallbackLabel,
|
fallbackLabel,
|
||||||
members,
|
members,
|
||||||
agents,
|
agents,
|
||||||
|
squads,
|
||||||
currentUser,
|
currentUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,44 +13,83 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
final class AgentMentionMarkdown {
|
final class AgentMentionMarkdown {
|
||||||
static String markdown(String name, String agentId) {
|
static String markdown(String name, String agentId) {
|
||||||
String cleanName = name == null ? "" : name.trim()
|
return mentionMarkdown("agent", name, agentId);
|
||||||
.replace("[", "\\[")
|
|
||||||
.replace("]", "\\]");
|
|
||||||
if (cleanName.isEmpty()) cleanName = Models.shortId(agentId);
|
|
||||||
return "[@" + cleanName + "](mention://agent/" + agentId + ")";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String displayText(String name, String agentId) {
|
static String memberMarkdown(String name, String memberId) {
|
||||||
|
return mentionMarkdown("member", name, memberId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String squadMarkdown(String name, String squadId) {
|
||||||
|
return mentionMarkdown("squad", name, squadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String displayText(String name, String id) {
|
||||||
String cleanName = cleanName(name);
|
String cleanName = cleanName(name);
|
||||||
if (cleanName.isEmpty()) cleanName = Models.shortId(agentId);
|
if (cleanName.isEmpty()) cleanName = Models.shortId(id);
|
||||||
return "@" + cleanName;
|
return "@" + cleanName;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String markdownFromDraft(String draft, List<Models.Agent> agents) {
|
static String markdownFromDraft(String draft, List<Models.Agent> agents) {
|
||||||
String content = draft == null ? "" : draft.trim();
|
return markdownFromDraft(draft, agents, Collections.emptyList(), Collections.emptyList());
|
||||||
if (content.isEmpty() || content.contains("mention://agent/") || agents == null || agents.isEmpty()) {
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
ArrayList<Models.Agent> sortedAgents = new ArrayList<>(agents);
|
|
||||||
sortedAgents.sort((left, right) -> Integer.compare(
|
static String markdownFromDraft(String draft, List<Models.Agent> agents,
|
||||||
displayText(right.name, right.id).length(),
|
List<Models.Member> members, List<Models.Squad> squads) {
|
||||||
displayText(left.name, left.id).length()
|
String content = draft == null ? "" : draft.trim();
|
||||||
));
|
if (content.isEmpty() || content.contains("mention://")) return content;
|
||||||
|
ArrayList<MentionReplacement> replacements = new ArrayList<>();
|
||||||
|
if (agents != null) {
|
||||||
|
for (Models.Agent agent : agents) {
|
||||||
|
replacements.add(new MentionReplacement(displayText(agent.name, agent.id), markdown(agent.name, agent.id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (members != null) {
|
||||||
|
for (Models.Member member : members) {
|
||||||
|
replacements.add(new MentionReplacement(displayText(member.displayName, member.id), memberMarkdown(member.displayName, member.id)));
|
||||||
|
if (member.userId != null && !member.userId.isEmpty() && !member.userId.equals(member.id)) {
|
||||||
|
replacements.add(new MentionReplacement(displayText(member.displayName, member.userId), memberMarkdown(member.displayName, member.userId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (squads != null) {
|
||||||
|
for (Models.Squad squad : squads) {
|
||||||
|
replacements.add(new MentionReplacement(displayText(squad.name, squad.id), squadMarkdown(squad.name, squad.id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replacements.sort((left, right) -> Integer.compare(right.display.length(), left.display.length()));
|
||||||
String normalized = content;
|
String normalized = content;
|
||||||
for (Models.Agent agent : sortedAgents) {
|
for (MentionReplacement replacement : replacements) {
|
||||||
String display = displayText(agent.name, agent.id);
|
if (replacement.display.length() <= 1) continue;
|
||||||
if (display.length() <= 1) continue;
|
normalized = normalized.replace(replacement.display, replacement.markdown);
|
||||||
normalized = normalized.replace(display, markdown(agent.name, agent.id));
|
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String mentionMarkdown(String type, String name, String id) {
|
||||||
|
String cleanName = cleanName(name)
|
||||||
|
.replace("[", "\\[")
|
||||||
|
.replace("]", "\\]");
|
||||||
|
if (cleanName.isEmpty()) cleanName = Models.shortId(id);
|
||||||
|
return "[@" + cleanName + "](mention://" + type + "/" + id + ")";
|
||||||
|
}
|
||||||
|
|
||||||
private static String cleanName(String name) {
|
private static String cleanName(String name) {
|
||||||
if (name == null) return "";
|
if (name == null) return "";
|
||||||
String trimmed = name.trim();
|
String trimmed = name.trim();
|
||||||
return "null".equalsIgnoreCase(trimmed) ? "" : trimmed;
|
return "null".equalsIgnoreCase(trimmed) ? "" : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class MentionReplacement {
|
||||||
|
final String display;
|
||||||
|
final String markdown;
|
||||||
|
|
||||||
|
MentionReplacement(String display, String markdown) {
|
||||||
|
this.display = display;
|
||||||
|
this.markdown = markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private AgentMentionMarkdown() {
|
private AgentMentionMarkdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,13 +99,19 @@ final class IssueCommentRichText {
|
|||||||
|
|
||||||
static String mentionDisplayLabel(String mentionType, String mentionId, String fallbackLabel,
|
static String mentionDisplayLabel(String mentionType, String mentionId, String fallbackLabel,
|
||||||
List<Models.Member> members, List<Models.Agent> agents,
|
List<Models.Member> members, List<Models.Agent> agents,
|
||||||
Models.User currentUser) {
|
List<Models.Squad> squads, Models.User currentUser) {
|
||||||
String resolved = resolveMentionName(mentionType, mentionId, members, agents, currentUser);
|
String resolved = resolveMentionName(mentionType, mentionId, members, agents, squads, currentUser);
|
||||||
String label = resolved.isEmpty() ? cleanMentionFallback(fallbackLabel, mentionId) : resolved;
|
String label = resolved.isEmpty() ? cleanMentionFallback(fallbackLabel, mentionId) : resolved;
|
||||||
if (label.startsWith("@")) return label;
|
if (label.startsWith("@")) return label;
|
||||||
return "@" + label;
|
return "@" + label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String mentionDisplayLabel(String mentionType, String mentionId, String fallbackLabel,
|
||||||
|
List<Models.Member> members, List<Models.Agent> agents,
|
||||||
|
Models.User currentUser) {
|
||||||
|
return mentionDisplayLabel(mentionType, mentionId, fallbackLabel, members, agents, Collections.emptyList(), currentUser);
|
||||||
|
}
|
||||||
|
|
||||||
static int issueIdentifierEndAt(String value, int start) {
|
static int issueIdentifierEndAt(String value, int start) {
|
||||||
if (value == null || start < 0 || start >= value.length()) return -1;
|
if (value == null || start < 0 || start >= value.length()) return -1;
|
||||||
Matcher matcher = ISSUE_IDENTIFIER.matcher(value);
|
Matcher matcher = ISSUE_IDENTIFIER.matcher(value);
|
||||||
@@ -105,7 +150,7 @@ final class IssueCommentRichText {
|
|||||||
|
|
||||||
private static String resolveMentionName(String mentionType, String mentionId,
|
private static String resolveMentionName(String mentionType, String mentionId,
|
||||||
List<Models.Member> members, List<Models.Agent> agents,
|
List<Models.Member> members, List<Models.Agent> agents,
|
||||||
Models.User currentUser) {
|
List<Models.Squad> squads, Models.User currentUser) {
|
||||||
String type = mentionType == null ? "" : mentionType.toLowerCase(Locale.US);
|
String type = mentionType == null ? "" : mentionType.toLowerCase(Locale.US);
|
||||||
String id = mentionId == null ? "" : mentionId;
|
String id = mentionId == null ? "" : mentionId;
|
||||||
if ("agent".equals(type)) {
|
if ("agent".equals(type)) {
|
||||||
@@ -119,6 +164,11 @@ final class IssueCommentRichText {
|
|||||||
if (id.equals(member.id) || id.equals(member.userId)) return clean(member.displayName);
|
if (id.equals(member.id) || id.equals(member.userId)) return clean(member.displayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ("squad".equals(type)) {
|
||||||
|
for (Models.Squad squad : squads) {
|
||||||
|
if (id.equals(squad.id)) return clean(squad.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (Models.Agent agent : agents) {
|
for (Models.Agent agent : agents) {
|
||||||
if (id.equals(agent.id)) return clean(agent.name);
|
if (id.equals(agent.id)) return clean(agent.name);
|
||||||
}
|
}
|
||||||
@@ -126,6 +176,9 @@ final class IssueCommentRichText {
|
|||||||
for (Models.Member member : members) {
|
for (Models.Member member : members) {
|
||||||
if (id.equals(member.id) || id.equals(member.userId)) return clean(member.displayName);
|
if (id.equals(member.id) || id.equals(member.userId)) return clean(member.displayName);
|
||||||
}
|
}
|
||||||
|
for (Models.Squad squad : squads) {
|
||||||
|
if (id.equals(squad.id)) return clean(squad.name);
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public final class MainActivity extends Activity {
|
|||||||
private List<Models.Project> projectCache = new ArrayList<>();
|
private List<Models.Project> projectCache = new ArrayList<>();
|
||||||
private List<Models.Member> memberCache = new ArrayList<>();
|
private List<Models.Member> memberCache = new ArrayList<>();
|
||||||
private List<Models.Agent> agentCache = new ArrayList<>();
|
private List<Models.Agent> agentCache = new ArrayList<>();
|
||||||
|
private List<Models.Squad> squadCache = new ArrayList<>();
|
||||||
private List<Models.Runtime> runtimeCache = new ArrayList<>();
|
private List<Models.Runtime> runtimeCache = new ArrayList<>();
|
||||||
private List<Models.Skill> skillCache = new ArrayList<>();
|
private List<Models.Skill> skillCache = new ArrayList<>();
|
||||||
private List<Models.Autopilot> autopilotCache = new ArrayList<>();
|
private List<Models.Autopilot> autopilotCache = new ArrayList<>();
|
||||||
@@ -266,6 +267,7 @@ public final class MainActivity extends Activity {
|
|||||||
projectCache = new ArrayList<>();
|
projectCache = new ArrayList<>();
|
||||||
memberCache = new ArrayList<>();
|
memberCache = new ArrayList<>();
|
||||||
agentCache = new ArrayList<>();
|
agentCache = new ArrayList<>();
|
||||||
|
squadCache = new ArrayList<>();
|
||||||
runtimeCache = new ArrayList<>();
|
runtimeCache = new ArrayList<>();
|
||||||
skillCache = new ArrayList<>();
|
skillCache = new ArrayList<>();
|
||||||
autopilotCache = new ArrayList<>();
|
autopilotCache = new ArrayList<>();
|
||||||
@@ -565,6 +567,7 @@ public final class MainActivity extends Activity {
|
|||||||
projectCache = data.projects;
|
projectCache = data.projects;
|
||||||
memberCache = data.members;
|
memberCache = data.members;
|
||||||
agentCache = data.agents;
|
agentCache = data.agents;
|
||||||
|
try { squadCache = safeSquads(); } catch (Exception ignored) { squadCache = new ArrayList<>(); }
|
||||||
LinearLayout screen = vertical();
|
LinearLayout screen = vertical();
|
||||||
LinearLayout scrollBody = vertical();
|
LinearLayout scrollBody = vertical();
|
||||||
scrollBody.setPadding(dp(14), dp(8), dp(14), dp(14));
|
scrollBody.setPadding(dp(14), dp(8), dp(14), dp(14));
|
||||||
@@ -1833,6 +1836,10 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Models.Squad> safeSquads() {
|
||||||
|
try { return api.squads(currentWorkspace.id); } catch (Exception e) { return new ArrayList<>(); }
|
||||||
|
}
|
||||||
|
|
||||||
private List<Models.Agent> safeAgents() {
|
private List<Models.Agent> safeAgents() {
|
||||||
if (currentWorkspace == null) return Models.emptyList();
|
if (currentWorkspace == null) return Models.emptyList();
|
||||||
try {
|
try {
|
||||||
@@ -1854,7 +1861,7 @@ public final class MainActivity extends Activity {
|
|||||||
private List<Models.Assignee> assignees(boolean includeEmpty) {
|
private List<Models.Assignee> assignees(boolean includeEmpty) {
|
||||||
if (memberCache.isEmpty()) memberCache = safeMembers();
|
if (memberCache.isEmpty()) memberCache = safeMembers();
|
||||||
if (agentCache.isEmpty()) agentCache = safeAgents();
|
if (agentCache.isEmpty()) agentCache = safeAgents();
|
||||||
return Models.issueAssignees(includeEmpty, t("unassigned"), currentUser, memberCache, agentCache);
|
return Models.issueAssignees(includeEmpty, t("unassigned"), currentUser, memberCache, agentCache, squadCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String assigneeName(String id, String type) {
|
private String assigneeName(String id, String type) {
|
||||||
@@ -1862,7 +1869,10 @@ public final class MainActivity extends Activity {
|
|||||||
if (currentUser != null && currentUser.id.equals(id)) return currentUser.name;
|
if (currentUser != null && currentUser.id.equals(id)) return currentUser.name;
|
||||||
for (Models.Member member : memberCache) if (member.id.equals(id)) return member.displayName;
|
for (Models.Member member : memberCache) if (member.id.equals(id)) return member.displayName;
|
||||||
for (Models.Agent agent : agentCache) if (agent.id.equals(id)) return agent.name;
|
for (Models.Agent agent : agentCache) if (agent.id.equals(id)) return agent.name;
|
||||||
String prefix = "agent".equals(type) ? t("agent") : t("member");
|
String prefix;
|
||||||
|
if ("agent".equals(type)) prefix = t("agent");
|
||||||
|
else if ("squad".equals(type)) prefix = t("squad");
|
||||||
|
else prefix = t("member");
|
||||||
return prefix + " " + Models.shortId(id);
|
return prefix + " " + Models.shortId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2252,6 +2262,7 @@ public final class MainActivity extends Activity {
|
|||||||
case "unassigned": return "未分配";
|
case "unassigned": return "未分配";
|
||||||
case "member": return "成员";
|
case "member": return "成员";
|
||||||
case "agent": return "Agent";
|
case "agent": return "Agent";
|
||||||
|
case "squad": return "小队";
|
||||||
case "archive": return "归档";
|
case "archive": return "归档";
|
||||||
case "archived": return "已归档";
|
case "archived": return "已归档";
|
||||||
case "archiveAgent": return "归档 Agent";
|
case "archiveAgent": return "归档 Agent";
|
||||||
@@ -2364,6 +2375,7 @@ public final class MainActivity extends Activity {
|
|||||||
case "unassigned": return "Unassigned";
|
case "unassigned": return "Unassigned";
|
||||||
case "member": return "Member";
|
case "member": return "Member";
|
||||||
case "agent": return "Agent";
|
case "agent": return "Agent";
|
||||||
|
case "squad": return "Squad";
|
||||||
case "archive": return "Archive";
|
case "archive": return "Archive";
|
||||||
case "archived": return "Archived";
|
case "archived": return "Archived";
|
||||||
case "archiveAgent": return "Archive Agent";
|
case "archiveAgent": return "Archive Agent";
|
||||||
|
|||||||
@@ -166,13 +166,17 @@ final class Models {
|
|||||||
return subscriber.userType == null ? "" : subscriber.userType;
|
return subscriber.userType == null ? "" : subscriber.userType;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String issueAssigneeDisplayName(Issue issue, List<Member> members, List<Agent> agents, User currentUser) {
|
static String issueAssigneeDisplayName(Issue issue, List<Member> members, List<Agent> agents, List<Squad> squads, User currentUser) {
|
||||||
if (issue == null || issue.assigneeId == null || issue.assigneeId.isEmpty()) return "";
|
if (issue == null || issue.assigneeId == null || issue.assigneeId.isEmpty()) return "";
|
||||||
String assigneeId = issue.assigneeId;
|
String assigneeId = issue.assigneeId;
|
||||||
if ("agent".equals(issue.assigneeType)) {
|
if ("agent".equals(issue.assigneeType)) {
|
||||||
for (Agent agent : agents) {
|
for (Agent agent : agents) {
|
||||||
if (assigneeId.equals(agent.id)) return agent.name;
|
if (assigneeId.equals(agent.id)) return agent.name;
|
||||||
}
|
}
|
||||||
|
} else if ("squad".equals(issue.assigneeType)) {
|
||||||
|
for (Squad squad : squads) {
|
||||||
|
if (assigneeId.equals(squad.id)) return squad.name;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentUser != null && assigneeId.equals(currentUser.id)) return currentUser.name;
|
if (currentUser != null && assigneeId.equals(currentUser.id)) return currentUser.name;
|
||||||
for (Member member : members) {
|
for (Member member : members) {
|
||||||
@@ -185,11 +189,16 @@ final class Models {
|
|||||||
for (Member member : members) {
|
for (Member member : members) {
|
||||||
if (assigneeId.equals(member.id) || assigneeId.equals(member.userId)) return member.displayName;
|
if (assigneeId.equals(member.id) || assigneeId.equals(member.userId)) return member.displayName;
|
||||||
}
|
}
|
||||||
|
for (Squad squad : squads) {
|
||||||
|
if (assigneeId.equals(squad.id)) return squad.name;
|
||||||
|
}
|
||||||
String prefix;
|
String prefix;
|
||||||
if ("agent".equals(issue.assigneeType)) {
|
if ("agent".equals(issue.assigneeType)) {
|
||||||
prefix = "Agent ";
|
prefix = "Agent ";
|
||||||
} else if ("member".equals(issue.assigneeType)) {
|
} else if ("member".equals(issue.assigneeType)) {
|
||||||
prefix = "Member ";
|
prefix = "Member ";
|
||||||
|
} else if ("squad".equals(issue.assigneeType)) {
|
||||||
|
prefix = "Squad ";
|
||||||
} else {
|
} else {
|
||||||
prefix = "";
|
prefix = "";
|
||||||
}
|
}
|
||||||
@@ -197,7 +206,7 @@ final class Models {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static List<Assignee> issueAssignees(boolean includeEmpty, String emptyLabel, User currentUser,
|
static List<Assignee> issueAssignees(boolean includeEmpty, String emptyLabel, User currentUser,
|
||||||
List<Member> members, List<Agent> agents) {
|
List<Member> members, List<Agent> agents, List<Squad> squads) {
|
||||||
List<Assignee> list = new ArrayList<>();
|
List<Assignee> list = new ArrayList<>();
|
||||||
List<String> seenMemberIds = new ArrayList<>();
|
List<String> seenMemberIds = new ArrayList<>();
|
||||||
if (includeEmpty) list.add(new Assignee(null, null, emptyLabel));
|
if (includeEmpty) list.add(new Assignee(null, null, emptyLabel));
|
||||||
@@ -214,6 +223,10 @@ final class Models {
|
|||||||
if (agent.id == null || agent.id.isEmpty()) continue;
|
if (agent.id == null || agent.id.isEmpty()) continue;
|
||||||
list.add(new Assignee(agent.id, "agent", agent.name));
|
list.add(new Assignee(agent.id, "agent", agent.name));
|
||||||
}
|
}
|
||||||
|
for (Squad squad : squads) {
|
||||||
|
if (squad.id == null || squad.id.isEmpty()) continue;
|
||||||
|
list.add(new Assignee(squad.id, "squad", squad.name));
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,6 +769,27 @@ final class Models {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final class Squad {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final String avatarUrl;
|
||||||
|
final String archivedAt;
|
||||||
|
final List<String> agentIds;
|
||||||
|
final List<String> memberIds;
|
||||||
|
|
||||||
|
Squad(JSONObject json) {
|
||||||
|
id = json.optString("id");
|
||||||
|
name = json.optString("name", shortId(id));
|
||||||
|
description = json.optString("description", "");
|
||||||
|
avatarUrl = cleanString(json, "avatar_url", cleanString(json, "avatarUrl", ""));
|
||||||
|
archivedAt = cleanString(json, "archived_at", "");
|
||||||
|
agentIds = stringList(json.optJSONArray("agent_ids"));
|
||||||
|
memberIds = stringList(json.optJSONArray("member_ids"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static final class Runtime {
|
static final class Runtime {
|
||||||
final String id;
|
final String id;
|
||||||
final String workspaceId;
|
final String workspaceId;
|
||||||
|
|||||||
Reference in New Issue
Block a user