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
19 changes: 19 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,25 @@ $ lc profile delete manhattan
An explicit `--team`/`--project` on the command line always overrides the
active profile.

==== Favorite teams/projects

Not in Ruby's `linear-cli` - favorite the teams/projects you actually
care about, and once any exist, `team list`/`project list` default to
showing just favorites of that kind. `--all` bypasses the favorites
filter only - it doesn't change `team list`'s `--no-mine` or `project
list`'s `--mine`/`--team` scope, it just shows everything within
whatever scope you already asked for.

[source,sh]
----
$ lc team favorite CRY
$ lc team list
$ lc team list --all
$ lc team unfavorite CRY
$ lc project favorite Manhattan
$ lc project list
----

==== Post a project status update

Not in Ruby's `linear-cli` - a status post on a project (Linear's own "Project
Expand Down
51 changes: 49 additions & 2 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,21 @@ defmodule LinearCli.CLI do
defp dispatch([:whoami], result, halt), do: run(&Commands.whoami/1, result, halt)
defp dispatch([:version], result, halt), do: run(&Commands.version/1, result, halt)
defp dispatch([:team, :list], result, halt), do: run(&Commands.team_list/1, result, halt)

defp dispatch([:team, :favorite], result, halt),
do: run(&Commands.team_favorite/1, result, halt)

defp dispatch([:team, :unfavorite], result, halt),
do: run(&Commands.team_unfavorite/1, result, halt)

defp dispatch([:project, :list], result, halt), do: run(&Commands.project_list/1, result, halt)

defp dispatch([:project, :favorite], result, halt),
do: run(&Commands.project_favorite/1, result, halt)

defp dispatch([:project, :unfavorite], result, halt),
do: run(&Commands.project_unfavorite/1, result, halt)

defp dispatch([:project, :update], result, halt),
do: run(&Commands.project_update/1, result, halt)

Expand Down Expand Up @@ -342,8 +355,19 @@ defmodule LinearCli.CLI do
name: "list",
about: "List teams",
flags: [
no_mine: [long: "--no-mine", help: "List all teams, not just your own"]
no_mine: [long: "--no-mine", help: "List all teams, not just your own"],
all: [long: "--all", help: "Ignore favorites (doesn't affect --no-mine)"]
]
],
favorite: [
name: "favorite",
about: "Favorite a team - list defaults to favorites once any exist",
args: [team: [value_name: "TEAM", help: "Team key or id", required: true]]
],
unfavorite: [
name: "unfavorite",
about: "Un-favorite a team",
args: [team: [value_name: "TEAM", help: "Team key or id", required: true]]
]
]
],
Expand All @@ -355,12 +379,35 @@ defmodule LinearCli.CLI do
name: "list",
about: "List projects",
flags: [
mine: [short: "-m", long: "--mine", help: "Only show my projects"]
mine: [short: "-m", long: "--mine", help: "Only show my projects"],
all: [long: "--all", help: "Ignore favorites (doesn't affect --mine/--team)"]
],
Comment thread
bougyman marked this conversation as resolved.
options: [
team: [short: "-t", long: "--team", help: "Show projects for only this team"]
]
],
favorite: [
name: "favorite",
about: "Favorite a project - list defaults to favorites once any exist",
args: [
project: [
value_name: "PROJECT",
help: "Project name, URL, ID, or search term",
required: true
]
]
],
unfavorite: [
name: "unfavorite",
about: "Un-favorite a project",
args: [
project: [
value_name: "PROJECT",
help: "Project name, URL, ID, or search term",
required: true
]
]
],
update: [
name: "update",
about: "Post a status update to a project",
Expand Down
80 changes: 77 additions & 3 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule LinearCli.CLI.Commands do
"""

alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt}
alias LinearCli.{Git, Linear, Profiles}
alias LinearCli.{Favorites, Git, Linear, Profiles}

@doc "Ported from commands/whoami.rb."
def whoami(%{flags: flags, options: options}) do
Expand Down Expand Up @@ -37,15 +37,21 @@ defmodule LinearCli.CLI.Commands do
result = if flags.no_mine, do: Linear.teams(), else: Linear.my_teams()

with {:ok, teams} <- result do
Display.show(teams, %{output: options.output})
Display.show(filter_favorites(teams, flags.all, "team", & &1.key), %{
output: options.output
})

:ok
end
end

@doc "Ported from commands/project/list.rb. Ruby's `--mine` defaults false."
def project_list(%{flags: flags, options: options}) do
with {:ok, projects} <- projects_for(flags, options) do
Display.show(projects, %{output: options.output})
Display.show(filter_favorites(projects, flags.all, "project", & &1.id), %{
output: options.output
})

:ok
end
end
Expand All @@ -59,6 +65,74 @@ defmodule LinearCli.CLI.Commands do
defp projects_for(%{mine: true}, _options), do: Linear.my_projects()
defp projects_for(_flags, _options), do: Linear.projects()

@doc """
New in this port - Ruby has no equivalent. Favorites a team
(`LinearCli.Favorites`) - once any team is favorited, `team list`
defaults to showing just favorites (`--all` overrides).
"""
def team_favorite(%{args: %{team: key}}) do
with {:ok, team} <- Linear.find_team(key) do
Favorites.add("team", team.key)
Prompt.ok("Favorited team #{team.key}")
:ok
end
end

@doc "New in this port - Ruby has no equivalent. Un-favorites a team."
def team_unfavorite(%{args: %{team: key}}) do
with {:ok, team} <- Linear.find_team(key) do
Favorites.remove("team", team.key)
Prompt.ok("Un-favorited team #{team.key}")
:ok
end
end

@doc """
New in this port - Ruby has no equivalent. Favorites a project
(`LinearCli.Favorites`), resolved the same way `project update`'s
`PROJECT` is - against every project in the workspace, prompting if
ambiguous. Once any project is favorited, `project list` defaults to
showing just favorites (`--all` overrides).
"""
def project_favorite(%{args: %{project: search}}) do
with {:ok, projects} <- Linear.projects(),
project when not is_nil(project) <- Projects.project_for(projects, search) do
Favorites.add("project", project.id)
Prompt.ok("Favorited project #{project.name}")
:ok
else
nil -> {:error, {:smells_bad, "No project found matching #{search}"}}
{:error, reason} -> {:error, reason}
end
end

@doc "New in this port - Ruby has no equivalent. Un-favorites a project."
def project_unfavorite(%{args: %{project: search}}) do
with {:ok, projects} <- Linear.projects(),
project when not is_nil(project) <- Projects.project_for(projects, search) do
Favorites.remove("project", project.id)
Prompt.ok("Un-favorited project #{project.name}")
:ok
else
nil -> {:error, {:smells_bad, "No project found matching #{search}"}}
{:error, reason} -> {:error, reason}
end
end

# Once any favorite of `kind` exists, narrows `records` down to just
# those (matched via `key_fun`) - invisible to anyone who's never
# favorited anything, since an empty favorites list leaves `records`
# untouched. `all?` (the new `--all` flag) always shows everything,
# bypassing the favorites lookup entirely.
defp filter_favorites(records, true, _kind, _key_fun), do: records

defp filter_favorites(records, _all?, kind, key_fun) do
case Favorites.list(kind) do
[] -> records
favorite_values -> Enum.filter(records, &(key_fun.(&1) in favorite_values))
end
end

@doc """
New in this port - Ruby has no equivalent. Posts a status update
(Linear's own "Project Update" feature - a journal-style status post,
Expand Down
94 changes: 94 additions & 0 deletions app/lib/linear_cli/favorites.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
defmodule LinearCli.Favorites do
@moduledoc """
Favorited teams/projects, persisted in the same local SQLite file
`LinearCli.Profiles` uses (`:profiles_db_path` - see
`config/runtime.exs`), in their own `favorites` table. Reuses
`LinearCli.Profiles`'s exact pattern (raw `Exqlite.Sqlite3`, one
connection open/ensure-schema/close per call, no `Ecto.Repo`) rather
than sharing code with it - see `documents/phase-10-plan.adoc` for why
this stays its own module against the same file instead of a change to
`LinearCli.Profiles` itself.

`list/1`, once non-empty for a given `kind`, is what
`LinearCli.CLI.Commands.team_list/1`/`project_list/1` filter their
results down to by default (a new `--all` flag opts back out).

New in this port - Ruby has no equivalent.
"""

alias Exqlite.Sqlite3

@doc "Favorites `value` under `kind` (`\"team\"` or `\"project\"`). A no-op if already favorited."
@spec add(String.t(), String.t()) :: :ok
def add(kind, value) do
with_db(fn conn ->
exec(conn, "INSERT OR IGNORE INTO favorites (kind, value) VALUES (?, ?)", [kind, value])
end)
end

@doc "Un-favorites `value` under `kind`. A no-op if it wasn't favorited."
@spec remove(String.t(), String.t()) :: :ok
def remove(kind, value) do
with_db(fn conn ->
exec(conn, "DELETE FROM favorites WHERE kind = ? AND value = ?", [kind, value])
end)
end

@doc "Every favorited value under `kind`, ordered by value."
@spec list(String.t()) :: [String.t()]
def list(kind) do
with_db(fn conn ->
conn
|> query("SELECT value FROM favorites WHERE kind = ? ORDER BY value", [kind])
|> Enum.map(fn [value] -> value end)
end)
end

defp with_db(fun) do
path = db_path()
File.mkdir_p!(Path.dirname(path))
{:ok, conn} = Sqlite3.open(path)

try do
ensure_schema!(conn)
fun.(conn)
after
Sqlite3.close(conn)
end
end

defp ensure_schema!(conn) do
:ok =
exec(
conn,
"""
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(kind, value)
)
""",
[]
)
end

defp exec(conn, sql, []), do: Sqlite3.execute(conn, sql)

defp exec(conn, sql, params) do
with {:ok, stmt} <- Sqlite3.prepare(conn, sql),
:ok <- Sqlite3.bind(stmt, params),
:done <- Sqlite3.step(conn, stmt) do
:ok
end
end

defp query(conn, sql, params) do
{:ok, stmt} = Sqlite3.prepare(conn, sql)
:ok = Sqlite3.bind(stmt, params)
{:ok, rows} = Sqlite3.fetch_all(conn, stmt)
rows
end

defp db_path, do: Application.fetch_env!(:linear_cli, :profiles_db_path)
end
Loading