Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Changed

- `project list` now orders projects the way Linear's own project list does: by the status's place in the project flow (backlog, planned, in progress, paused, completed, canceled), then by the status's configured position within that category, then by the manual order projects are dragged into, with name and id only breaking ties. The previous order was a hardcoded one that put in-progress work first, ignored both the workspace's own status positions and the manual order entirely, and fell back to sorting by name. Note that this ordering is reconstructed from what Linear's schema documents about `ProjectStatus.position` and `Project.sortOrder` rather than observed in the app, and that `project list --json` now carries those two fields
- `project list` now orders projects the way Linear's own project list showed them: by `sortOrder` ascending, the manual order projects are dragged into, with status playing no part and name and id only breaking ties. The previous order was a hardcoded one that grouped by status type with in-progress work first, ignored the manual order entirely, and fell back to sorting by name. The rule comes from comparing the app's projects list against a workspace with projects in five different statuses; whether that view had a customised grouping or ordering setting is not yet confirmed, and `sortOrder` and `prioritySortOrder` were identical there, so the observation could not tell those two apart. `project list --json` now carries `sortOrder`
- `issue archive` help, `docs/usage.md`, the README, and the linear-cli skill now explain that Linear archives closed issues automatically and offers no manual archive in its app or official MCP server, quoting and linking Linear's docs, so the command reads as an escape hatch rather than the normal way to retire an issue

### Added
Expand Down
82 changes: 24 additions & 58 deletions src/commands/project/project-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { Command } from "@cliffy/command"
import { unicodeWidth } from "@std/cli"
import { open } from "@opensrc/deno-open"
import { gql } from "../../__codegen__/gql.ts"
import type {
GetProjectsQuery,
ProjectStatusType,
} from "../../__codegen__/graphql.ts"
import type { GetProjectsQuery } from "../../__codegen__/graphql.ts"
import { getGraphQLClient } from "../../utils/graphql.ts"
import {
getProjectPriorityLabel,
Expand Down Expand Up @@ -34,7 +31,6 @@ const GetProjects = gql(`
name
color
type
position
}
lead {
name
Expand Down Expand Up @@ -74,42 +70,6 @@ export interface ProjectDisplayOrderKey {
id: string
name: string
sortOrder: number
status: { type: ProjectStatusType; position: number }
}

/**
* Rank a project status by where its category sits in Linear's project flow.
*
* `ProjectStatusType`'s order in the SDL is alphabetical and so says nothing
* about the lifecycle; the flow order below is the one Linear lays its project
* statuses out in. The `switch` is exhaustive on purpose: a status type added
* to the schema should fail the type check here, where someone has to decide
* where it belongs, rather than silently sort to the end.
*/
function statusTypeRank(type: ProjectStatusType): number {
switch (type) {
case "backlog":
return 0
case "planned":
return 1
case "started":
return 2
case "paused":
return 3
case "completed":
return 4
case "canceled":
return 5
default: {
const unreachable: never = type
throw new CliError(
`Linear returned an unknown project status type: ${
String(unreachable)
}`,
{ suggestion: "Update the CLI, or report this if it persists." },
)
}
}
}

/**
Expand All @@ -128,28 +88,24 @@ function compareNumericKey(a: number, b: number, field: string): number {
}

/**
* Order projects the way Linear's own project list does.
* Order projects the way Linear's own project list does: by `sortOrder`
* ascending, the manual order projects are dragged into, with status ignored.
*
* Reconstructed from the schema rather than observed in the app: `position` is
* documented as ordering statuses "within its type group", so the type's place
* in the flow comes first and the configured position refines it, and
* `sortOrder` is documented as the manual order used in list views. Name and id
* only break ties, so the result is stable across runs.
* Observed in the app rather than read off the schema. In a workspace holding
* backlog, planned, in progress, completed and canceled projects, Linear's
* projects list showed every project strictly by `sortOrder`: backlog projects
* with a higher `sortOrder` sat after the canceled one instead of being grouped
* with the backlog project that led the list. Whether that view had customised
* grouping or ordering settings is not yet confirmed. `prioritySortOrder`
* matched `sortOrder` for every project there, so the observation cannot tell
* the two apart; `sortOrder` is the one the schema documents as the manual
* order used in list views. Name and id only break ties, so the result is
* stable across runs.
*/
export function compareProjectsForDisplay(
a: ProjectDisplayOrderKey,
b: ProjectDisplayOrderKey,
): number {
const byType = statusTypeRank(a.status.type) - statusTypeRank(b.status.type)
if (byType !== 0) return byType

const byPosition = compareNumericKey(
a.status.position,
b.status.position,
"status position",
)
if (byPosition !== 0) return byPosition

const byManualOrder = compareNumericKey(a.sortOrder, b.sortOrder, "sortOrder")
if (byManualOrder !== 0) return byManualOrder

Expand Down Expand Up @@ -324,8 +280,18 @@ export const listCommand = new Command()
: `Created ${getTimeAgo(new Date(project.createdAt))}`
case "backlog":
case "paused":
default:
return `Updated ${getTimeAgo(new Date(project.updatedAt))}`
default: {
// Exhaustive so a status type added to the schema fails the type
// check here instead of silently getting the backlog date.
const unreachable: never = project.status.type
throw new CliError(
`Linear returned an unknown project status type: ${
String(unreachable)
}`,
{ suggestion: "Update the CLI, or report this if it persists." },
)
}
}
}

Expand Down
73 changes: 35 additions & 38 deletions test/commands/project/__snapshots__/project-list.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ stdout:
"id": "status-1",
"name": "In Progress",
"color": "#f59e0b",
"type": "started",
"position": 2
"type": "started"
},
"lead": {
"name": "test.user",
Expand Down Expand Up @@ -103,6 +102,39 @@ snapshot[`Project List Command - JSON Output With Pagination 1`] = `
stdout:
'{
"nodes": [
{
"id": "project-page1-1",
"name": "Alpha Project",
"description": "First page project",
"slugId": "alpha-proj",
"sortOrder": 900,
"icon": null,
"color": "#3b82f6",
"status": {
"id": "status-1",
"name": "In Progress",
"color": "#f59e0b",
"type": "started"
},
"lead": null,
"priority": 2,
"health": "onTrack",
"startDate": null,
"targetDate": null,
"startedAt": null,
"completedAt": null,
"canceledAt": null,
"createdAt": "2024-01-10T10:00:00Z",
"updatedAt": "2024-01-20T15:30:00Z",
"url": "https://linear.app/test/project/alpha-proj",
"teams": {
"nodes": [
{
"key": "ENG"
}
]
}
},
{
"id": "project-page2-1",
"name": "Beta Project",
Expand All @@ -115,8 +147,7 @@ stdout:
"id": "status-2",
"name": "Planned",
"color": "#6366f1",
"type": "planned",
"position": 1
"type": "planned"
},
"lead": {
"name": "pat.planner",
Expand All @@ -140,40 +171,6 @@ stdout:
}
]
}
},
{
"id": "project-page1-1",
"name": "Alpha Project",
"description": "First page project",
"slugId": "alpha-proj",
"sortOrder": 900,
"icon": null,
"color": "#3b82f6",
"status": {
"id": "status-1",
"name": "In Progress",
"color": "#f59e0b",
"type": "started",
"position": 2
},
"lead": null,
"priority": 2,
"health": "onTrack",
"startDate": null,
"targetDate": null,
"startedAt": null,
"completedAt": null,
"canceledAt": null,
"createdAt": "2024-01-10T10:00:00Z",
"updatedAt": "2024-01-20T15:30:00Z",
"url": "https://linear.app/test/project/alpha-proj",
"teams": {
"nodes": [
{
"key": "ENG"
}
]
}
}
],
"pageInfo": {
Expand Down
Loading
Loading