Conversation
This makes it possible to control how the action is rendered in the action dropdown, which opens up for actions that are not rendered as a form submission button, e.g. just a link.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved action scoping, recordless rendering, execution, and dependency-loading issues block approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Extracts built-in CRUD actions into reusable, context-aware action objects rendered through dropdowns.
Changes:
- Adds configurable New, Edit, and Delete actions.
- Updates repository views and dropdown rendering.
- Expands action, repository, component, and controller tests.
File summaries
| File | Summary |
|---|---|
test/uchi/repository/actions_test.rb |
Tests repository actions and scoping. |
test/uchi/action_test.rb |
Tests action configuration and rendering. |
test/dummy/app/uchi/repositories/book.rb |
Configures sample repository actions. |
test/controllers/uchi/repository_controller_test.rb |
Updates repository integration coverage. |
test/components/uchi/ui/actions/dropdown_test.rb |
Tests dropdown rendering. |
lib/uchi/repository.rb |
Adds default actions and context filtering; critical symbol-context mismatch remains. |
lib/uchi/action/new.rb |
Adds the New action; execution and translation issues remain. |
lib/uchi/action/edit.rb |
Adds the Edit action; recordless index rendering and translation issues remain. |
lib/uchi/action/delete.rb |
Adds the Delete action. |
lib/uchi/action/configuration.rb |
Adds action context configuration. |
lib/uchi/action.rb |
Adds shared rendering APIs; nil-record and direct-load issues remain. |
lib/uchi.rb |
Loads action classes. |
app/views/uchi/repository/show.html.erb |
Integrates scoped show actions. |
app/views/uchi/repository/index.html.erb |
Integrates index actions; empty header slot issue remains. |
app/views/uchi/repository/edit.html.erb |
Integrates scoped edit actions. |
app/components/uchi/ui/actions/dropdown/dropdown.html.erb |
Renders action controls; nil-record handling issue remains. |
app/components/uchi/ui/actions/dropdown.rb |
Configures dropdowns; unset time-zone handling issue remains. |
app/assets/stylesheets/uchi/application.css |
Adds dropdown positioning styles. |
Review details
Suppressed comments (8)
app/components/uchi/ui/actions/dropdown.rb:37
- For the recordless dropdown used by the index view (
record: nil), this callsTime.zone.now; Rails applications may not configureTime.zone(the dummy app leaves it unset), so rendering a multi-action index raisesNoMethodError. Use a zone-independent Rails clock here.
@record_id ||= record&.id || Time.zone.now.to_i
app/components/uchi/ui/actions/dropdown/dropdown.html.erb:2
- The index page passes
record: nil, so a single generic action reachesbutton_renderwith no record; the base implementation immediately callsrecord.idand raises. This also affects any record-dependent action configured for index, not just the built-in Edit action. Make the single-action path support nil/header actions or require and supply a record before callingbutton_render.
<%= actions.first.button_render(record: record, repository: repository, view: self.view_context) %>
app/views/uchi/repository/index.html.erb:14
- Unlike the show and edit templates, this unconditionally registers a page-header action slot even when
actions_for(:index)is empty.PageHeaderchecks whether slots exist, not whether their rendered content is nonempty, so repositories with no index actions get an empty action container in the header.
<% header.with_action do %>
<%= render(Uchi::Ui::Actions::Dropdown.new(
actions: @repository.actions_for(Uchi::View::INDEX),
record: nil,
repository: @repository
)) %>
<% end %>
lib/uchi/action.rb:123
- The index view passes
record: nil, and the base action's defaultonincludes:index. A single custom action that inherits this method therefore callsnil.idbefore it can render. Make the id field conditional and compact the fields as#renderalready does.
view.hidden_field_tag(:id, record.id),
lib/uchi/action.rb:3
Configuration#initializenow callsdefault_on, which referencesUchi::View, but this file does not load the view definitions. A directrequire "uchi/action"followed byUchi::Action.newtherefore raisesNameError; load the view dependency here as the field configuration does.
require_relative "action/configuration"
lib/uchi/action/edit.rb:30
- Like
New#perform, this method is reachable through the generic action execution endpoint, butrepositoryis not defined onAction. Invoking the registered defaultEditaction directly therefore raisesNoMethodErrorand returns a 500 response.
Uchi::ActionResponse.success.redirect_to(
path: repository.routes.path_for(:edit, id: record.id)
)
lib/uchi/action/edit.rb:37
- This standalone button also uses
Action#namerather thanrepository.translate.link_to_edit(record), so localized Edit labels are ignored when Edit is the only action. Use the repository translation here as well.
name,
lib/uchi/action/new.rb:32
- This standalone button also uses
Action#namerather than the repository-specificrepository.translate.link_to_newlookup, so localizedlink_to_newvalues are ignored when New is the only action. Use the repository translation here as well.
name,
- Files reviewed: 18/18 changed files
- Comments generated: 7
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved action rendering and built-in action execution defects block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (6)
lib/uchi/action.rb:123
- The index template passes
record: nil, and a single collection-scoped custom action uses this defaultbutton_render; unlikerender, this line unconditionally callsrecord.id, so an action that does not require a specific record still crashes when it is the only index action. Make the id field conditional (and compact the fields) as the regular renderer already does.
view.hidden_field_tag(:id, record.id),
lib/uchi/action/edit.rb:13
- Using the generic action
namehere drops the repository-specific label that the previous edit link used. For example,da.ymldefinesuchi.repository.author.button.link_to_edit, but this path now falls back to the generic "Edit"; userepository.translate.link_to_edit(record)instead.
name,
lib/uchi/action/edit.rb:37
- Using the generic action
namehere drops the repository-specific label that the previous edit link used. For example,da.ymldefinesuchi.repository.author.button.link_to_edit, but this path now falls back to the generic "Edit"; userepository.translate.link_to_edit(record)instead.
name,
lib/uchi/action/new.rb:15
performis part of the action contract and the executions controller will call it for any postedaction_name, butrepositoryis not a local or method onUchi::Action(only the rendering methods receive it). Invoking the built-in New action through the execution endpoint therefore raisesNameErrorinstead of redirecting; either make the action non-executable or provide it with a repository before callingperform.
Uchi::ActionResponse.success.redirect_to(
path: repository.routes.path_for(:new)
)
lib/uchi/action/new.rb:22
- Using the generic action
namehere drops the repository-specific label that the previous new link used. For example,da.ymldefinesuchi.repository.author.button.link_to_new, but this path now displays the generic "New"; userepository.translate.link_to_newinstead.
name,
lib/uchi/action/new.rb:32
- Using the generic action
namehere drops the repository-specific label that the previous new link used. For example,da.ymldefinesuchi.repository.author.button.link_to_new, but this path now displays the generic "New"; userepository.translate.link_to_newinstead.
name,
- Files reviewed: 18/18 changed files
- Comments generated: 3
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
New and Edit actions must preserve repository-specific localized labels.
Review details
Suppressed comments (4)
lib/uchi/action/edit.rb:13
- This replaces the previous repository-localized edit label with the generic action name.
repository.translate.link_to_edit(record)is the established API for this text and supports per-model translations (for example, “Rediger bog”); use it here instead ofname.
name,
lib/uchi/action/edit.rb:37
- The edit link in the dropdown likewise bypasses
repository.translate.link_to_edit(record), so localized and repository-specific labels are lost. Use that translation helper here rather than the generic action name.
name,
lib/uchi/action/new.rb:22
- This uses the generic action name instead of the repository-localized label. The existing translation contract is
repository.translate.link_to_new(seelib/uchi/repository/translate.rb:215), so locales such as the dummy Danish locale lose labels like “Ny forfatter” and fall back to “New”. Use the repository translation here.
name,
lib/uchi/action/new.rb:32
- The standalone New button also bypasses the repository-specific
button.link_to_newtranslation and displays the generic action name. Userepository.translate.link_to_newso this extracted action preserves the localization behavior of the previous index header link.
name,
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The unresolved critical Delete regression and moderate Edit label regression block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
test/components/uchi/ui/actions/dropdown_test.rb:32
- The test description has a subject/verb agreement error: “the number of actions don't exceed” should use “doesn't exceed.”
test "renders both actions directly, without a dropdown menu, when the number of actions don't exceed the configured number" do
- Files reviewed: 21/21 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate action-rendering, context, identifier, and API visibility issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
app/components/uchi/ui/actions/dropdown.rb:51
- Using
Time.current.to_ias the identifier for a collection-level dropdown makes every such component rendered in the same second share the same button/menu IDs. If a page renders more than one index action component, Stimulus andaria-labelledbycan target the wrong menu; use a per-component unique value (for example the component'sobject_id) for the nil-record case.
@record_id ||= record&.id || Time.current.to_i
lib/uchi/action/new.rb:50
- Because this method is defined after
protected,Uchi::Action::New.new.nameis now protected even thoughAction#nameis a public API and the actions documentation tells consumers to override/call#name. This also breaks callers that inspect or label the built-in New action directly. Keepnamepublic by moving it aboveprotectedor addingpublic :name.
def name
return super unless repository
repository.translate.link_to_new
- Files reviewed: 23/23 changed files
- Comments generated: 3
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
lib/uchi/action/configuration.rb can raise NameError because it does not load Uchi::View directly.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
One critical and two moderate unresolved issues remain in action context handling, action visibility, and repository title behavior.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
lib/uchi/action/new.rb:50
Action#nameis public, but theprotecteddeclaration above this method makesUchi::Action::New#nameprotected. This breaks the base action API for callers that inspect or customize the built-in action (Uchi::Action::New.new.nameraisesNoMethodError); expose this override publicly like the inherited method.
def name
return super unless repository
repository.translate.link_to_new
test/dummy/app/uchi/repositories/book.rb:23
Repository#titleexplicitly returnsnilfor a nil record, but this override now returns theBookrepository class instead. Any caller that asks for a title without a record will receive aClassobject, which is not a display title and can leak into translation/interpolation code. Preserve the base contract by returningnilwhenmodelis absent.
return self.class unless model
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Delete actions must preserve feedback messages, and the documented Delete-on-index example must be corrected.
Review details
Suppressed comments (2)
docs/actions.md:44
- This example adds
Deleteto:index, but the index header passesrecord: nilandDeleterendersrecord.id/record-specific translations, so copying this documented configuration raises while rendering the index. Since record-bound index actions are not supported, use a record-independent action here or scopeDeleteonly to a record view.
super + [Uchi::Action::Delete.new.on(:index, :edit)]
lib/uchi/action/delete.rb:15
- This drops the localized feedback that the existing
RepositoryController#destroysupplies: both successful deletes and callback failures now redirect without any flash message, so a failed delete appears to succeed silently. Returnrepository.translate.successful_destroy/repository.translate.failed_destroyin these responses (and preserve the failure redirect if that behavior is required).
if destroyed.all?
Uchi::ActionResponse.success
else
Uchi::ActionResponse.error
- Files reviewed: 23/23 changed files
- Comments generated: 0 new
- Review effort level: Lite
Using the same chaining as Fields, we can now configure which views an action should appear on
The Delete action could go pretty much everywhere; index, row, edit, show. However, placing it in the Edit view makes it more contextually relevant and reduces the risk of accidental deletions.
In order to preserve backwards compatibility with actions already defining `#render`, the call site (Dropdown's template) must keep calling `.render(...)`, not `.render_as_dropdown_item(...)`. - Uchi::Action#render (the old override point) was renamed to render_as_dropdown_item — this is now the officially documented method to override. - A new render was added that just delegates: render_as_dropdown_item(record:, view:). - Uchi::Ui::Actions::Dropdown's template still calls action.render(...) (unchanged) — this is what makes both old and new host-app subclasses work correctly: - A host app that overrides render_as_dropdown_item (new style) gets invoked via polymorphism when the base render delegates. - A host app that already overrides render directly (old style, pre-existing) still works, since Ruby dispatches to their override before ever reaching the base class's delegation logic — the base render_as_dropdown_item method is simply never called in that case. - Updated Edit, New, and Delete (Uchi's own built-in actions) to override render_as_dropdown_item instead of render, since they're not "host apps" needing the compat shim — they should model the new official pattern.
There was a problem hiding this comment.
🔵 Needs a closer look
Repository-level index actions currently receive an empty record set and require corrected execution behavior.
Review details
Suppressed comments (1)
lib/uchi/action.rb:125
- When an action is rendered on
:index,recordis nil and this intentionally omits theidfield, butExecutionsController#find_recordsturns a missingid/idsinto[]and callsrepository.find_many([]). Consequently, repository-level index actions receive an empty relation and are no-ops, contrary to the documentation that index actions apply to the repository as a whole. Define how index executions load their records (for example, pass the repository's full scope) before omitting the record ID.
(record ? view.hidden_field_tag(:id, record.id) : nil),
- Files reviewed: 23/23 changed files
- Comments generated: 0 new
- Review effort level: Lite
No description provided.