Skip to content

Commit f9c07de

Browse files
authored
✨ Allow to filter apps and sort both apps and teams (fastapilabs#246)
1 parent da00f1d commit f9c07de

4 files changed

Lines changed: 91 additions & 5 deletions

File tree

src/fastapi_cloud_cli/commands/apps/link.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ def _link_app_interactively(
125125
"Select the team:",
126126
options=[
127127
Option({"name": t["name"], "value": {"id": t["id"], "name": t["name"]}})
128-
for t in teams_data
128+
for t in sorted(teams_data, key=lambda t: t["name"].lower())
129129
],
130+
allow_filtering=True,
130131
bullet=False,
131132
)
132133

@@ -152,8 +153,9 @@ def _link_app_interactively(
152153
"Select the app to link:",
153154
options=[
154155
Option({"name": a["slug"], "value": {"id": a["id"], "slug": a["slug"]}})
155-
for a in apps_data
156+
for a in sorted(apps_data, key=lambda a: a["slug"].lower())
156157
],
158+
allow_filtering=True,
157159
bullet=False,
158160
)
159161

src/fastapi_cloud_cli/commands/apps/list.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,10 @@ def _prompt_for_team(toolkit: FastAPIRichToolkit, client: APIClient) -> Team:
170170

171171
return toolkit.ask(
172172
"Select the team:",
173-
options=[Option({"name": team.name, "value": team}) for team in teams],
173+
options=[
174+
Option({"name": team.name, "value": team})
175+
for team in sorted(teams, key=lambda team: team.name.lower())
176+
],
174177
allow_filtering=True,
175178
bullet=False,
176179
)

src/fastapi_cloud_cli/commands/deploy/configure.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ def _configure_app(
3535

3636
team = toolkit.ask(
3737
"Select the team you want to deploy to:",
38-
options=[Option({"name": team.name, "value": team}) for team in teams],
38+
options=[
39+
Option({"name": team.name, "value": team})
40+
for team in sorted(teams, key=lambda team: team.name.lower())
41+
],
3942
allow_filtering=True,
4043
emoji="🏢",
4144
)
@@ -66,7 +69,10 @@ def _configure_app(
6669

6770
selected_app = toolkit.ask(
6871
"Select the app you want to deploy to:",
69-
options=[Option({"name": app.slug, "value": app}) for app in apps],
72+
options=[
73+
Option({"name": app.slug, "value": app})
74+
for app in sorted(apps, key=lambda app: app.slug.lower())
75+
],
7076
allow_filtering=True,
7177
emoji="📦",
7278
)

tests/test_cli_link.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,81 @@ def test_shows_error_on_apps_api_failure(
324324
assert "Error fetching apps" in result.output
325325

326326

327+
@pytest.mark.respx
328+
def test_app_menu_is_ordered_by_slug(
329+
logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
330+
) -> None:
331+
steps = [Keys.ENTER, Keys.ENTER]
332+
333+
respx_mock.get("/teams/").mock(
334+
return_value=Response(
335+
200, json={"data": [{"id": "team-1", "name": "My Team", "slug": "my-team"}]}
336+
)
337+
)
338+
respx_mock.get("/apps/", params={"team_id": "team-1"}).mock(
339+
return_value=Response(
340+
200,
341+
json={
342+
"data": [
343+
{"id": "app-2", "slug": "zebra"},
344+
{"id": "app-1", "slug": "alpha"},
345+
]
346+
},
347+
)
348+
)
349+
350+
with (
351+
changing_dir(tmp_path),
352+
patch("rich_toolkit.container.getchar") as mock_getchar,
353+
):
354+
mock_getchar.side_effect = steps
355+
result = runner.invoke(app, ["link"])
356+
357+
assert result.exit_code == 0
358+
assert "Successfully linked to app" in result.output
359+
assert "alpha" in result.output
360+
361+
config_path = tmp_path / ".fastapicloud" / "cloud.json"
362+
config = AppConfig.model_validate_json(config_path.read_text())
363+
assert config.app_id == "app-1"
364+
365+
366+
@pytest.mark.respx
367+
def test_team_menu_is_ordered_by_name(
368+
logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
369+
) -> None:
370+
steps = [Keys.ENTER, Keys.ENTER]
371+
372+
respx_mock.get("/teams/").mock(
373+
return_value=Response(
374+
200,
375+
json={
376+
"data": [
377+
{"id": "team-2", "name": "Zebra Team", "slug": "zebra-team"},
378+
{"id": "team-1", "name": "Alpha Team", "slug": "alpha-team"},
379+
]
380+
},
381+
)
382+
)
383+
respx_mock.get("/apps/", params={"team_id": "team-1"}).mock(
384+
return_value=Response(200, json={"data": [{"id": "app-1", "slug": "my-app"}]})
385+
)
386+
387+
with (
388+
changing_dir(tmp_path),
389+
patch("rich_toolkit.container.getchar") as mock_getchar,
390+
):
391+
mock_getchar.side_effect = steps
392+
result = runner.invoke(app, ["link"])
393+
394+
assert result.exit_code == 0
395+
assert "Successfully linked to app" in result.output
396+
397+
config_path = tmp_path / ".fastapicloud" / "cloud.json"
398+
config = AppConfig.model_validate_json(config_path.read_text())
399+
assert config.team_id == "team-1"
400+
401+
327402
@pytest.mark.respx
328403
def test_links_with_multiple_teams_and_apps(
329404
logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path

0 commit comments

Comments
 (0)