Skip to content

Commit

Permalink
Merge pull request #216 from darrenburns/polish-run-8mar25
Browse files Browse the repository at this point in the history
Polishing
  • Loading branch information
darrenburns authored Mar 8, 2025
2 parents 0f6931a + 248e9fe commit a212a88
Show file tree
Hide file tree
Showing 16 changed files with 1,177 additions and 233 deletions.
Binary file modified .coverage
Binary file not shown.
11 changes: 11 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## Unreleased

### Added

- Validation and corresponding UI feedback in New Request modal.

### Fixed

- Fixed crash when attempting to delete a request that doesn't exist on disk.
- Fixed being able to create requests with empty names.

## 2.5.1 [7th March 2025]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,5 @@ Fill out the form on the modal that appears, and press ++enter++ or ++ctrl+n++ t

!!! tip "Folders"

Requests can be saved to folders - simply include a `/` in the `Directory` field when you save the request,
Requests can be saved to folders - simply include a `/` in the `Path in collection` field when you save the request,
and Posting will create the required directory structure for you.
6 changes: 3 additions & 3 deletions docs/guide/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ You'll be prompted to supply a name for the request.
By default, this name is used to generate the filename, but you can also choose your own filename if you wish.
!!! tip
If you already have a collection loaded, the directory will be pre-selected based on the location of the cursor in the collection tree, so moving the cursor to the correct location *before* pressing ++ctrl+n++ will save you from needing to type out the path.
If you already have a collection loaded, the path in the "New Request" dialog will be pre-filled based on the position of the cursor in the collection tree, so moving the cursor to the correct location *before* pressing ++ctrl+n++ will save you from needing to type out the path.
Within the "Directory" field of this dialog, it's important to note that `.` refers to the currently loaded *collection* directory (that is, the directory that was loaded using the `--collection` option), and *not* necessarily the current working directory.
Within the "Path in collection" field of this dialog, it's important to note that `.` refers to the currently loaded *collection* directory (that is, the directory that was loaded using the `--collection` option), and *not* necessarily the current working directory.

### Duplicating a request

Expand All @@ -51,7 +51,7 @@ If you haven't saved the request yet, a dialog will appear, prompting you to giv

!!! tip "Folders"

Requests can be saved to folders - simply include a `/` in the `Directory` field when you save the request,
Requests can be saved to folders - simply include a `/` in the `Path in collection` field when you save the request,
and Posting will create the required directory structure for you.

If the request is already saved on disk, ++ctrl+s++ will overwrite the previous version with your new changes.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "posting"
version = "2.5.1"
version = "2.5.2"
description = "The modern API client that lives in your terminal."
authors = [
{ name = "Darren Burns", email = "darrenb900@gmail.com" }
Expand Down
10 changes: 8 additions & 2 deletions src/posting/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
from pathlib import Path
from string import Template
from typing import Any, Generator, Literal, get_args
from typing import Any, Literal, get_args
import httpx
from pydantic import BaseModel, Field, HttpUrl
import rich
import yaml
import os
from textual import log
from posting.auth import HttpxBearerTokenAuth
from posting.tuple_to_multidict import tuples_to_dict
from posting.variables import SubstitutionError
Expand Down Expand Up @@ -294,7 +295,12 @@ def save_to_disk(self, path: Path) -> None:

def delete_from_disk(self) -> None:
if self.path:
self.path.unlink()
try:
self.path.unlink()
except FileNotFoundError:
log.warning(
f"Could not delete request {self.name!r} from disk: not found"
)

def to_curl(self, extra_args: str = "") -> str:
"""Convert the request model to a cURL command.
Expand Down
95 changes: 77 additions & 18 deletions src/posting/widgets/collection/new_request_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from textual.binding import Binding
from textual.containers import Horizontal, VerticalScroll
from textual.screen import ModalScreen
from textual.validation import ValidationResult, Validator
from textual.validation import Length, ValidationResult, Validator
from textual.widgets import Button, Footer, Input, Label
from textual.widgets.tree import TreeNode
from posting.files import is_valid_filename, request_file_exists
Expand Down Expand Up @@ -37,7 +37,7 @@ def validate(self, value: str) -> ValidationResult:
return (
self.success()
if is_valid_filename(value)
else self.failure("File name cannot be empty")
else self.failure("Invalid file name")
)


Expand Down Expand Up @@ -84,6 +84,21 @@ class NewRequestModal(ModalScreen[NewRequestData | None]):
background: $surface;
color: $text-muted;
}
& .error-label {
color: $text-error;
dock: right;
}
& .form-label-row {
height: 1;
}
& #title-error-label,
& #file-name-error-label,
& #directory-error-label {
display: none;
}
}
"""

Expand Down Expand Up @@ -115,14 +130,27 @@ def compose(self) -> ComposeResult:
vs.can_focus = False
vs.border_title = "New request"

yield Label("Title")
with Horizontal(classes="form-label-row"):
yield Label("Title")
yield Label(
"Must not be empty", id="title-error-label", classes="error-label"
)

yield PostingInput(
self._initial_title,
placeholder="Enter a title",
validators=[
Length(minimum=1, failure_description="Title cannot be empty")
],
id="title-input",
)

yield Label("File name [dim]optional[/dim]")
with Horizontal(classes="form-label-row"):
yield Label("File name [dim]optional[/]")
yield Label(
"Not valid", id="file-name-error-label", classes="error-label"
)

with Horizontal():
filename_input = PostingInput(
placeholder="Enter a file name",
Expand All @@ -135,17 +163,22 @@ def compose(self) -> ComposeResult:
yield filename_input
yield Label(".posting.yaml", id="file-suffix-label")

yield Label("Description [dim]optional[/dim]")
yield Label("Description [dim]optional[/]")
yield PostingTextArea(
self._initial_description,
id="description-textarea",
show_line_numbers=False,
)

yield Label("Directory")
with Horizontal(classes="form-label-row"):
yield Label("Path in collection")
yield Label(
"Not valid", id="directory-error-label", classes="error-label"
)

yield PostingInput(
self._initial_directory,
placeholder="Enter a directory",
placeholder="Enter a path to save the request to",
id="directory-input",
validators=[DirectoryValidator()],
)
Expand Down Expand Up @@ -188,19 +221,45 @@ def validate_and_create_request(
description_textarea = self.description_textarea
directory_input = self.directory_input

if not directory_input.is_valid:
self.notify(
"Directory must be relative to the collection root.",
severity="error",
)
return
directory_validation_result = directory_input.validate(directory_input.value)
file_name_validation_result = file_name_input.validate(file_name_input.value)
title_validation_result = title_input.validate(title_input.value)

if not file_name_input.is_valid:
self.notify(
"Invalid file name.",
severity="error",
)
title_error_label = self.query_one("#title-error-label", Label)
if title_validation_result is not None and not title_validation_result.is_valid:
description = title_validation_result.failures[0].description
if description is not None:
title_error_label.update(description)
title_error_label.display = "block"
return
else:
title_error_label.display = "none"

file_name_error_label = self.query_one("#file-name-error-label", Label)
if (
file_name_validation_result is not None
and not file_name_validation_result.is_valid
):
description = file_name_validation_result.failures[0].description
if description is not None:
file_name_error_label.update(description)
file_name_error_label.display = "block"
return
else:
file_name_error_label.display = "none"

directory_error_label = self.query_one("#directory-error-label", Label)
if (
directory_validation_result is not None
and not directory_validation_result.is_valid
):
description = directory_validation_result.failures[0].description
if description is not None:
directory_error_label.update(description)
directory_error_label.display = "block"
return
else:
directory_error_label.display = "none"

file_name = file_name_input.value
title = title_input.value
Expand Down
15 changes: 0 additions & 15 deletions src/posting/widgets/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,6 @@ def on_mount(self) -> None:
self.on_theme_change(self.app.current_theme)
self.app.theme_changed_signal.subscribe(self, self.on_theme_change)

def render(self) -> RenderResult:
self.view_position = self.view_position
if not self.value:
placeholder = Text(self.placeholder, justify="left")
placeholder.stylize(self.get_component_rich_style("input--placeholder"))
if self.has_focus:
if self._cursor_visible:
# If the placeholder is empty, there's no characters to stylise
# to make the cursor flash, so use a single space character
if len(placeholder) == 0:
placeholder = Text(" ")
placeholder.stylize(self.cursor_style, 0, 1)
return placeholder
return _InputRenderable(self, self._cursor_visible)

@property
def cursor_style(self) -> Style:
return (
Expand Down
Loading

0 comments on commit a212a88

Please sign in to comment.