fix: render cards as native links when the card action has a url - #166
Merged
Conversation
The card blade hardcoded wire:click="mountAction(...)" on both click targets, so a card action configured with ->url() never navigated. It mounted an action with no modal and no callback instead, leaving the click a no-op: no navigation, no modal, nothing logged. The same action rendered correctly as an anchor in the card's actions dropdown, because that path goes through Filament's own renderer. Resolve the configured card action from the already-processed record actions, so per-record closures such as ->url(fn ($record) => ...) evaluate against the right record, and render the card title and body as anchors when it has a url. Uses Filament\Support\generate_href_html(), matching how table rows build record urls, so SPA mode, prefetching, modifier keys and ->openUrlInNewTab() all behave the same as everywhere else in the panel. Actions with a modal, and actions posting to a url, keep the existing click handler. Fixes #164
getUrl() falls back to the livewire component's getDefaultActionUrl(), which Filament's resource pages implement for modal-less Create/Edit/View actions. BoardResourcePage extends that class, so honouring the fallback would have turned existing boards into links without the developer asking for it. Gate on hasUrl() so only an explicit ->url() opts a card into link rendering.
hasModal() only reports an explicit ->modal() call, so ->requiresConfirmation() and a custom modal heading, description or content left getUrl() returning a url. A card action carrying both then rendered as a link and navigated on click, skipping a confirmation that v4.0.14 showed. A whole card is an easy target to click by accident, so the modal now wins over the url. Filament's own dropdown renders such an action as a plain anchor. The card is deliberately more conservative than that, because a menu item is a considered click and a card is not.
The previous guard added hasUrl() and shouldOpenModal() checks the framework does not make. Filament's ListRecords::table() builds recordUrl() from the first view/edit action whose getUrl() is filled and has recordAction() skip those same actions, without consulting modal state, so a url action that also declares a confirmation already renders as a plain link in a table row. Verified against a real table in a Filament 5.4 app. Inventing a stricter rule for boards meant the same action behaved one way in a table and another on a card. Follow the framework instead: the url decides, and only postToUrl() actions stay on the click handler because a POST needs a form.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #164.
The bug
Clicking a card does nothing at all. Not a modal, not navigation.
->url()is ignored.Root cause
resources/views/livewire/card.blade.phphardcoded the Livewire handler on both click targets (the title<h4>and the body<div>):wire:click="mountAction('{{ $cardAction }}', [], @js(['recordKey' => $record['id']]))"There was no url branch anywhere in the card render path.
Board::getCardAction()returns only a name, so the resolvedActioninstance was never consulted and its url could never be read.The contrast is visible on a single card in a single response. Same action, two renderings:
cardAction)<h4 wire:click="mountAction('view', [], {recordKey: …})">cardActions)<a href="https://…/tasks/01kxm3…">The dropdown is correct because it goes through Filament's own action renderer. Mounting an action that has no modal and no callback is a no-op, so the click silently did nothing:
mountedActionscame back empty, the url never changed, and nothing was logged.The fix
Board::resolveCardAction()finds the configured card action among the record actions already processed for that card (flatteningActionGroups). Taking them fromgetBoardRecordActions()rather than rebuilding matters: those clones carry the record binding, which is what makes->url(fn ($record) => ...)resolvable.The blade then renders the title and body as anchors when the action has a url, built with
Filament\Support\generate_href_html()andhasNestedClickEventHandler: true, the same helper and flag Filament's table rows use forrecordUrl. SPA mode, prefetching, modifier-key handling and->openUrlInNewTab()therefore behave exactly as they do elsewhere in the panel.Following the framework's rule
The url on the action decides, with no check of its modal state. That is Filament's own rule, from
ListRecords::table():recordAction()iteratesview/editand skips any action whosegetUrl()is filledrecordUrl()iterates the same actions and skips any whosegetUrl()is falsyNeither closure consults
hasModal()orshouldOpenModal().This PR originally added
hasUrl()andshouldOpenModal()guards so that an action declaring both a url and a confirmation would keep showing the confirmation. That was wrong, and the last commit reverts it. Verified against a real Filament table in a Filament 5.4 app: with core's ownListRecordsdefaulting logic and an action carrying->url()->requiresConfirmation(), the table row renders<a href>and clicking it navigates with no confirmation. Keeping a stricter rule for boards would have meant one action behaving differently in a table row and on a card, which is worse than the config being self-contradictory in the first place.So: the url wins in a board card, a table row, and the actions dropdown alike. An action should declare a url or a modal, not both. Only
->postToUrl()actions stay on the Livewire click handler, because a POST needs a form rather than an anchor.Test plan
Nine tests in
tests/Feature/CardActionUrlTest.php, across sixBoardPagefixtures (url, new-tab url, modal, default-action-url page, url + confirmation, url + custom modal heading):hreffor a url card actionmountAction('view'handler is emitted anywhere for a url card action->openUrlInNewTab()producestarget="_blank"mountAction('edit'and nohrefresolveCardAction()finds the action, and returnsnullwhen none is configuredAssertions target the card title specifically, not the whole page: the actions dropdown renders its own anchor for the same action and would satisfy a page-wide assertion even with the bug present.
Also verified locally:
vendor/bin/pest: 156 passedvendor/bin/pint: passedvendor/bin/phpstan analyse: 7 errors, in the same seven places as unmodified4.x(pre-existing, none in the changed code)<a href>and navigates (previously the url never changed);openUrlInNewTabyieldstarget="_blank"; a card action without a url still opens its modal; a bareViewActionstill opens its modal; browser Back after navigating restores the board. A screenshot of the linked board is indistinguishable from the board before the change, so the anchors introduce no visual difference.Docs: added the url case to the
cardAction()section of the API reference, including the "a url decides" precedence and its two consequences.Worth squashing on merge: the four commits include one wrong turn and its revert.