@@ -105,8 +105,17 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
description,
|
||||
"aria-describedby": ariaDescribedBy,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item> & {
|
||||
description?: React.ReactNode;
|
||||
}) {
|
||||
const descriptionId = React.useId();
|
||||
const describedBy = description
|
||||
? [ariaDescribedBy, descriptionId].filter(Boolean).join(" ")
|
||||
: ariaDescribedBy;
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
@@ -114,6 +123,7 @@ function SelectItem({
|
||||
"focus:bg-muted focus:text-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-pointer items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
aria-describedby={describedBy}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
@@ -121,7 +131,16 @@ function SelectItem({
|
||||
<IconCheck className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
{description ? (
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start gap-0.5 pr-5 leading-tight">
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
<div id={descriptionId} className="text-muted-foreground text-[11px] leading-tight">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
)}
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,37 @@ import { useTranslation } from "react-i18next";
|
||||
// `labelKey` holds a catalog key, not copy: this table is module scope, so a
|
||||
// resolved `t()` here would freeze at the boot locale. `key` is a persisted
|
||||
// sort value and stays in English.
|
||||
const SORT_OPTIONS: Array<{ key: SortKey; labelKey: string }> = [
|
||||
{ key: "state", labelKey: "task:sortStatus" },
|
||||
{ key: "updatedAt", labelKey: "task:sortUpdated" },
|
||||
{ key: "lastActivityAt", labelKey: "task:sortLastActivity" },
|
||||
{ key: "createdAt", labelKey: "task:sortCreated" },
|
||||
{ key: "title", labelKey: "task:sortTitle" },
|
||||
{ key: "custom", labelKey: "task:sortCustom" },
|
||||
const SORT_OPTIONS: Array<{ key: SortKey; labelKey: string; descriptionKey: string }> = [
|
||||
{
|
||||
key: "state",
|
||||
labelKey: "task:sortStatus",
|
||||
descriptionKey: "task:sortStatusDescription",
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
labelKey: "task:sortUpdated",
|
||||
descriptionKey: "task:sortUpdatedDescription",
|
||||
},
|
||||
{
|
||||
key: "lastActivityAt",
|
||||
labelKey: "task:sortLastActivity",
|
||||
descriptionKey: "task:sortLastActivityDescription",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
labelKey: "task:sortCreated",
|
||||
descriptionKey: "task:sortCreatedDescription",
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
labelKey: "task:sortTitle",
|
||||
descriptionKey: "task:sortTitleDescription",
|
||||
},
|
||||
{
|
||||
key: "custom",
|
||||
labelKey: "task:sortCustom",
|
||||
descriptionKey: "task:sortCustomDescription",
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
@@ -35,11 +59,20 @@ export function SortPicker({ value, onChange }: Props) {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.key} value={opt.key} className="text-xs">
|
||||
{t(opt.labelKey)}
|
||||
</SelectItem>
|
||||
))}
|
||||
{SORT_OPTIONS.map((opt) => {
|
||||
const label = t(opt.labelKey);
|
||||
return (
|
||||
<SelectItem
|
||||
key={opt.key}
|
||||
value={opt.key}
|
||||
className="text-xs"
|
||||
aria-label={label}
|
||||
description={t(opt.descriptionKey)}
|
||||
>
|
||||
{label}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{!isCustom && (
|
||||
|
||||
@@ -275,6 +275,43 @@ test.describe("Mobile sidebar — view system", () => {
|
||||
const popover = testPage.getByTestId("sidebar-filter-popover");
|
||||
await expect(popover).toBeVisible();
|
||||
await popover.getByTestId("sort-key-select").tap();
|
||||
await expect(testPage.getByRole("option", { name: "Updated", exact: true })).toContainText(
|
||||
"Last task summary refresh. Background events can change it.",
|
||||
);
|
||||
await expect(
|
||||
testPage.getByRole("option", { name: "Last activity", exact: true }),
|
||||
).toContainText("Last user or agent action. Viewing a task does not change it.");
|
||||
await expect(testPage.getByRole("option", { name: "Status", exact: true })).toContainText(
|
||||
"Task state, from review to backlog.",
|
||||
);
|
||||
await expect(testPage.getByRole("option", { name: "Created", exact: true })).toContainText(
|
||||
"When the task was created.",
|
||||
);
|
||||
await expect(testPage.getByRole("option", { name: "Title", exact: true })).toContainText(
|
||||
"Task title in alphabetical order.",
|
||||
);
|
||||
await expect(testPage.getByRole("option", { name: "Custom", exact: true })).toContainText(
|
||||
"The manual order you set for tasks.",
|
||||
);
|
||||
for (const { label, description } of [
|
||||
{
|
||||
label: "Updated",
|
||||
description: "Last task summary refresh. Background events can change it.",
|
||||
},
|
||||
{
|
||||
label: "Last activity",
|
||||
description: "Last user or agent action. Viewing a task does not change it.",
|
||||
},
|
||||
{ label: "Status", description: "Task state, from review to backlog." },
|
||||
{ label: "Created", description: "When the task was created." },
|
||||
{ label: "Title", description: "Task title in alphabetical order." },
|
||||
{ label: "Custom", description: "The manual order you set for tasks." },
|
||||
]) {
|
||||
const option = testPage.getByRole("option", { name: label, exact: true });
|
||||
const descriptionId = await option.getAttribute("aria-describedby");
|
||||
expect(descriptionId).toBeTruthy();
|
||||
await expect(testPage.locator(`[id="${descriptionId}"]`)).toHaveText(description);
|
||||
}
|
||||
await testPage.getByRole("option", { name: "Last activity", exact: true }).tap();
|
||||
const direction = popover.getByTestId("sort-direction-toggle");
|
||||
if ((await direction.getAttribute("data-direction")) !== "desc") await direction.tap();
|
||||
|
||||
@@ -350,6 +350,45 @@ test.describe("Sidebar filter — group + sort", () => {
|
||||
await session.waitForLoad();
|
||||
const filters = new SidebarFilterPopoverPage(testPage);
|
||||
await filters.open();
|
||||
await filters.popover.getByTestId("sort-key-select").click();
|
||||
await expect(testPage.getByRole("option", { name: "Updated", exact: true })).toContainText(
|
||||
"Last task summary refresh. Background events can change it.",
|
||||
);
|
||||
await expect(
|
||||
testPage.getByRole("option", { name: "Last activity", exact: true }),
|
||||
).toContainText("Last user or agent action. Viewing a task does not change it.");
|
||||
await expect(testPage.getByRole("option", { name: "Status", exact: true })).toContainText(
|
||||
"Task state, from review to backlog.",
|
||||
);
|
||||
await expect(testPage.getByRole("option", { name: "Created", exact: true })).toContainText(
|
||||
"When the task was created.",
|
||||
);
|
||||
await expect(testPage.getByRole("option", { name: "Title", exact: true })).toContainText(
|
||||
"Task title in alphabetical order.",
|
||||
);
|
||||
await expect(testPage.getByRole("option", { name: "Custom", exact: true })).toContainText(
|
||||
"The manual order you set for tasks.",
|
||||
);
|
||||
for (const { label, description } of [
|
||||
{
|
||||
label: "Updated",
|
||||
description: "Last task summary refresh. Background events can change it.",
|
||||
},
|
||||
{
|
||||
label: "Last activity",
|
||||
description: "Last user or agent action. Viewing a task does not change it.",
|
||||
},
|
||||
{ label: "Status", description: "Task state, from review to backlog." },
|
||||
{ label: "Created", description: "When the task was created." },
|
||||
{ label: "Title", description: "Task title in alphabetical order." },
|
||||
{ label: "Custom", description: "The manual order you set for tasks." },
|
||||
]) {
|
||||
const option = testPage.getByRole("option", { name: label, exact: true });
|
||||
const descriptionId = await option.getAttribute("aria-describedby");
|
||||
expect(descriptionId).toBeTruthy();
|
||||
await expect(testPage.locator(`[id="${descriptionId}"]`)).toHaveText(description);
|
||||
}
|
||||
await testPage.keyboard.press("Escape");
|
||||
await filters.setGroup("None");
|
||||
await filters.setSort("Last activity", "desc");
|
||||
await filters.saveAs("Last activity view");
|
||||
|
||||
@@ -1351,12 +1351,18 @@
|
||||
"someone": "Someone",
|
||||
"sort": "Sort",
|
||||
"sortCreated": "Created",
|
||||
"sortCreatedDescription": "When the task was created.",
|
||||
"sortCustom": "Custom",
|
||||
"sortCustomDescription": "The manual order you set for tasks.",
|
||||
"sortDirection": "Sort direction {{direction}}",
|
||||
"sortLastActivity": "Last activity",
|
||||
"sortLastActivityDescription": "Last user or agent action. Viewing a task does not change it.",
|
||||
"sortStatus": "Status",
|
||||
"sortStatusDescription": "Task state, from review to backlog.",
|
||||
"sortTitle": "Title",
|
||||
"sortTitleDescription": "Task title in alphabetical order.",
|
||||
"sortUpdated": "Updated",
|
||||
"sortUpdatedDescription": "Last task summary refresh. Background events can change it.",
|
||||
"source": "Source",
|
||||
"sourceChangesTitle": "{{source}} Changes",
|
||||
"specifyALocalPortOrLeave": "Specify a local port or leave blank for a random one. For Docker/K8s, use a port you've pre-exposed.",
|
||||
|
||||
@@ -1351,12 +1351,18 @@
|
||||
"someone": "Śōḿēōńē",
|
||||
"sort": "Śōŕţ",
|
||||
"sortCreated": "Ćŕēàţēď",
|
||||
"sortCreatedDescription": "Ŵĥēń ţĥē ţàśķ ŵàś ćŕēàţēď.",
|
||||
"sortCustom": "Ćũśţōḿ",
|
||||
"sortCustomDescription": "Ŧĥē ḿàńũàĺ ōŕďēŕ ẏōũ śēţ ƒōŕ ţàśķś.",
|
||||
"sortDirection": "Śōŕţ ďĩŕēćţĩōń {{direction}}",
|
||||
"sortLastActivity": "Ĺàśţ àćţĩvĩţŷ",
|
||||
"sortLastActivityDescription": "Ĺàśţ ũśēŕ ōŕ àĝēńţ àćţĩōń. Vĩēŵĩńĝ à ţàśķ ďōēś ńōţ ćĥàńĝē ĩţ.",
|
||||
"sortStatus": "Śţàţũś",
|
||||
"sortStatusDescription": "Ŧàśķ śţàţē, ƒŕōḿ ŕēvĩēŵ ţō ƀàćķĺōĝ.",
|
||||
"sortTitle": "Ţĩţĺē",
|
||||
"sortTitleDescription": "Ŧàśķ ţĩţĺē ĩń àĺƥĥàƀēţĩćàĺ ōŕďēŕ.",
|
||||
"sortUpdated": "Ũƥďàţēď",
|
||||
"sortUpdatedDescription": "Ĺàśţ ţàśķ śũḿḿàŕŷ ŕēƒŕēśĥ. Ɓàćķĝŕōũńď ēvēńţś ćàń ćĥàńĝē ĩţ.",
|
||||
"source": "Śōũŕćē",
|
||||
"sourceChangesTitle": "{{source}} Ćĥàńĝēś",
|
||||
"specifyALocalPortOrLeave": "Śƥēćĩƒŷ à ĺōćàĺ ƥōŕţ ōŕ ĺēàvē ƀĺàńķ ƒōŕ à ŕàńďōḿ ōńē. Ƒōŕ Ďōćķēŕ/Ķ8ś, ũśē à ƥōŕţ ŷōũ'vē ƥŕē-ēxƥōśēď.",
|
||||
|
||||
@@ -1306,12 +1306,18 @@
|
||||
"someone": "Alguém",
|
||||
"sort": "Ordenar",
|
||||
"sortCreated": "Criação",
|
||||
"sortCreatedDescription": "Quando a tarefa foi criada.",
|
||||
"sortCustom": "Personalizada",
|
||||
"sortCustomDescription": "A ordem manual que definiu para as tarefas.",
|
||||
"sortDirection": "Sentido da ordenação {{direction}}",
|
||||
"sortLastActivity": "Última atividade",
|
||||
"sortLastActivityDescription": "Última ação do utilizador ou turno do agente. Ver uma tarefa não conta como atividade.",
|
||||
"sortStatus": "Estado",
|
||||
"sortStatusDescription": "Estado da tarefa, de revisão a pendente.",
|
||||
"sortTitle": "Título",
|
||||
"sortTitleDescription": "Título da tarefa por ordem alfabética.",
|
||||
"sortUpdated": "Atualização",
|
||||
"sortUpdatedDescription": "Última atualização do resumo da tarefa. Eventos em segundo plano podem alterá-la.",
|
||||
"source": "Fonte",
|
||||
"sourceChangesTitle": "Alterações de {{source}}",
|
||||
"specifyALocalPortOrLeave": "Indique uma porta local ou deixe em branco para uma aleatória. No Docker/K8s, use uma porta que já tenha exposto.",
|
||||
|
||||
@@ -1305,12 +1305,18 @@
|
||||
"someone": "某人",
|
||||
"sort": "排序",
|
||||
"sortCreated": "创建时间",
|
||||
"sortCreatedDescription": "任务创建的时间。",
|
||||
"sortCustom": "自定义",
|
||||
"sortCustomDescription": "你为任务设置的手动顺序。",
|
||||
"sortDirection": "排序方向 {{direction}}",
|
||||
"sortLastActivity": "最后活动",
|
||||
"sortLastActivityDescription": "最近一次用户操作或智能体回合。查看任务不会改变此时间。",
|
||||
"sortStatus": "状态",
|
||||
"sortStatusDescription": "任务状态,按审核、进行中和待处理排序。",
|
||||
"sortTitle": "标题",
|
||||
"sortTitleDescription": "按任务标题的字母顺序。",
|
||||
"sortUpdated": "更新时间",
|
||||
"sortUpdatedDescription": "任务摘要的最近刷新时间。后台事件可能会改变此时间。",
|
||||
"source": "来源",
|
||||
"sourceChangesTitle": "{{source}} 变更",
|
||||
"specifyALocalPortOrLeave": "指定本地端口,或留空以随机分配。对于 Docker/K8s,请使用已预先暴露的端口。",
|
||||
|
||||
@@ -1305,12 +1305,18 @@
|
||||
"someone": "某人",
|
||||
"sort": "排序",
|
||||
"sortCreated": "建立時間",
|
||||
"sortCreatedDescription": "任務建立的時間。",
|
||||
"sortCustom": "自訂",
|
||||
"sortCustomDescription": "你為任務設定的手動順序。",
|
||||
"sortDirection": "排序方向 {{direction}}",
|
||||
"sortLastActivity": "最後活動",
|
||||
"sortLastActivityDescription": "最近一次使用者操作或代理程式回合。查看任務不會改變此時間。",
|
||||
"sortStatus": "狀態",
|
||||
"sortStatusDescription": "任務狀態,按審核、進行中和待處理排序。",
|
||||
"sortTitle": "標題",
|
||||
"sortTitleDescription": "按任務標題的字母順序。",
|
||||
"sortUpdated": "更新時間",
|
||||
"sortUpdatedDescription": "任務摘要的最近重新整理時間。背景事件可能會改變此時間。",
|
||||
"source": "來源",
|
||||
"sourceChangesTitle": "{{source}} 變更",
|
||||
"specifyALocalPortOrLeave": "指定本地端口,或留空以隨機分配。對於 Docker/K8s,請使用已預先暴露的端口。",
|
||||
|
||||
@@ -1305,12 +1305,18 @@
|
||||
"someone": "某人",
|
||||
"sort": "排序",
|
||||
"sortCreated": "建立時間",
|
||||
"sortCreatedDescription": "任務建立的時間。",
|
||||
"sortCustom": "自訂",
|
||||
"sortCustomDescription": "你為任務設定的手動順序。",
|
||||
"sortDirection": "排序方向 {{direction}}",
|
||||
"sortLastActivity": "最後活動",
|
||||
"sortLastActivityDescription": "最近一次使用者操作或代理程式回合。檢視任務不會改變此時間。",
|
||||
"sortStatus": "狀態",
|
||||
"sortStatusDescription": "任務狀態,依審核、進行中和待處理排序。",
|
||||
"sortTitle": "標題",
|
||||
"sortTitleDescription": "依任務標題的字母順序。",
|
||||
"sortUpdated": "更新時間",
|
||||
"sortUpdatedDescription": "任務摘要的最近重新整理時間。背景事件可能會改變此時間。",
|
||||
"source": "來源",
|
||||
"sourceChangesTitle": "{{source}} 變更",
|
||||
"specifyALocalPortOrLeave": "指定本地埠,或留空以隨機分配。對於 Docker/K8s,請使用已預先暴露的埠。",
|
||||
|
||||
@@ -367,6 +367,15 @@ On desktop and tablet, the header switches between **Kanban**, **Pipeline**, and
|
||||
|
||||
Under **Settings → General → Appearance → Startup Page**, choose **Task overview** (the default) or **Last visited task**. The latter resumes the most recently opened task in the current workspace on that device when Kandev starts or you open bare Home. It does not change an explicit task or workflow link. Home navigation and a task's Back action always return to the task overview; when there is no matching local recent task, Kandev opens the overview instead.
|
||||
|
||||
The **TASKS** list in the left sidebar has two time-based sort choices. These choices are separate from the sort choices in the task **List** view.
|
||||
|
||||
| Sort choice | Meaning |
|
||||
| --- | --- |
|
||||
| **Updated** | The last task summary refresh. Background events, such as pull-request status changes, can change this time. |
|
||||
| **Last activity** | The last real user or agent action. Opening or focusing a task and background provider polling do not change this time. |
|
||||
|
||||
Choose **Last activity** when you want to review tasks by the least recent user or agent interaction.
|
||||
|
||||
- Search matches tasks without changing their state.
|
||||
- The display menu filters by **Workflow** and **Repository** and can enable **Open preview on click**. In Kanban/Pipeline, each workflow lane has a **Columns** menu to hide individual steps. Unticking a step hides its column and tasks on that board, scoped to its own workflow, until you re-tick it. The optional **Auto-hide empty columns** setting collapses unoccupied steps without changing those manual choices; auto-hidden empty steps return as move destinations while a task is being moved, while manually hidden steps remain unavailable for pointer and bulk moves. On phones, open the menu drawer to change columns for the focused workflow.
|
||||
- In **List**, the display menu can enable **Show task details** to include available repository, description, pull-request, session, parent, review, and archive context in each row. This option is off by default and follows the user across devices.
|
||||
|
||||
Reference in New Issue
Block a user