Day 07:讓大量資料可用,篩選、排序與搜尋 結尾點出一個新的落差,就算篩選出一群目標商品,例如所有已下架的清潔用品,表格上目前沒有任何一顆按鈕可以讓使用者對這群資料做什麼。商品表格現在排得好看,也找得到想要的資料了,但找到之後能不能動手,是完全另一個問題。
今天要處理的正是這件事。
先把這個落差說得更具體一點。我們昨天學會用 TernaryFilter 篩出所有已下架的商品,再搭配分類篩選只留下清潔用品這一類,畫面上確實只剩下目標商品,但接下來呢。想把這批商品重新上架,得逐筆點進編輯頁面,把上架狀態的開關打開,再存檔,一筆一筆重複同樣的動作。資料筆數只有兩三筆的時候還能忍受,一旦累積到十幾筆、幾十筆,這種逐筆操作的成本會迅速超過篩選本身省下來的時間。
回頭看,這個問題其實從 Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面 就隱約存在。商品 Resource 剛生成的時候,列表頁每一列本來就帶著編輯這類基本連結,讀者當時大概沒有特別留意這顆連結背後是什麼機制,畢竟那時候要處理的問題是把整套 CRUD 骨架跑起來。這幾天的討論一路聚焦在欄位跟資料本身,Day 04:表單欄位全解析,從文字輸入到日期選擇器 處理表單欄位型別,Day 06:表格欄位怎麼呈現,格式化與縮圖顯示 處理表格呈現,Day 07:讓大量資料可用,篩選、排序與搜尋 處理找資料的效率,但從來沒有正式停下來講清楚,這些散落在列表頁各處的按鈕,背後到底是同一套機制,還是各自獨立拼出來的功能。
今天的任務是把這個一直存在、卻沒被講清楚的操作機制正式命名並拆解,並在商品 Resource 上示範它的三種不同掛載方式。
這個機制在 Filament 裡有一個正式的名字,Action。今天正式定案這個詞在本系列的定義:Action 掛載在資料列、頁面或表單上,代表一個可觸發的操作單元,含表格列操作、頁首操作、表單內操作三種掛載位置。
先拆解定義裡「可觸發的操作單元」這幾個字實際在說什麼。
一個 Action 通常包含三個部分,觸發它的按鈕外觀,點下去之後實際執行的邏輯,以及執行完成後給使用者的回饋提示,例如一句「已更新」的提示訊息。這三個部分不管掛載在畫面的哪個位置,都是同一套組成方式,差別只在於掛載的位置不同,以及因為位置不同而自然衍生出的使用情境不同。
三種掛載位置各自對應不同的操作情境。表格列操作掛在每一筆資料旁邊,針對單筆或勾選多筆資料操作,離開了具體的資料列,這種 Action 就沒有存在的意義。頁首操作掛在整個頁面的最上方,通常針對整頁層級、不特定於某一筆資料的操作,就算列表裡一筆資料都沒有,頁首操作依然可以被觸發。表單內操作掛在建立或編輯表單裡,通常針對這筆正在編輯中的資料本身,但又跟表單送出這個動作本身不是同一件事。
如果換成比喻來理解,這三種掛載位置更像是同一個按鈕的概念,裝在不同的門把上。門把裝在哪裡決定了這顆門會通往什麼房間,但轉動門把這個動作本身,原理是一樣的。過去讀者可能把表格列上的編輯按鈕、頁首的新增按鈕、表單裡的某個操作按鈕,當成三種互不相關、各自客製化出來的功能,但其實它們是同一個 Action 概念的不同掛載位置。學會其中一種的設定邏輯,另外兩種幾乎可以直接類推過去。
表格列操作的第一種型態,是針對單一商品的快捷操作,不需要進入完整的編輯表單就能完成。上架狀態這個欄位很適合示範這種情境,Day 04:表單欄位全解析,從文字輸入到日期選擇器 用 Toggle 定義這個欄位,現在可以在表格列上疊加一個對應的快捷開關:
use Filament\Actions\Action;
Action::make('toggleActive')
->label(fn ($record): string => $record->is_active ? '下架' : '上架')
->icon(fn ($record): string => $record->is_active ? 'heroicon-o-eye-slash' : 'heroicon-o-eye')
->action(function ($record): void {
$record->update(['is_active' => ! $record->is_active]);
}),
這顆按鈕的按鈕外觀會依目前狀態切換文字跟圖示,執行的邏輯是直接把 is_active 反轉存回資料庫,使用者不需要先點進編輯頁、找到那個開關、再存檔,一個按鈕就能完成單筆狀態切換。
第二種表格列操作的型態是批次操作,這正是 Day 7 結尾留下的懸念該解決的地方。使用者昨天已經學會用篩選功能,先選出所有已下架的清潔用品,接下來要做的是勾選其中多筆,觸發一個批次操作,一次把這批商品重新上架:
use Filament\Actions\BulkAction;
use Illuminate\Database\Eloquent\Collection;
BulkAction::make('bulkActivate')
->label('批次上架')
->icon('heroicon-o-eye')
->action(function (Collection $records): void {
$records->each(fn ($record) => $record->update(['is_active' => true]));
})
->deselectRecordsAfterCompletion(),
篩選出一批已下架的清潔用品之後,使用者勾選需要重新上架的那幾筆,畫面上方會出現「批次上架」這顆按鈕,點下去,action() 收到的是一整份被勾選的資料集合,逐筆把上架狀態更新回去。deselectRecordsAfterCompletion() 讓操作完成後自動清空勾選狀態,避免使用者誤以為勾選還留著。這正是 Day 7 結尾提出的問題,篩選出一群資料之後接下來能做什麼,答案就是掛在表格列位置的批次操作。
這兩種型態說到底有一個共同特徵,不管是針對單筆的快捷開關,還是針對勾選多筆的批次操作,這類 Action 都是圍繞著具體的資料列存在,脫離了資料列本身,這種掛載位置就沒有意義。
頁首操作跟表格列操作的性質完全不同。最直覺的例子,其實從 Day 3 建立 Resource 那一刻就已經存在,那顆放在列表頁最上方的新增商品按鈕。這幾天的討論一直沒有正式把它納入某個框架下理解,今天把它放回 Action 的脈絡裡重新看待,它正是頁首操作的典型代表:
use Filament\Actions\CreateAction;
CreateAction::make()
->label('新增商品'),
這顆按鈕不針對表格裡任何特定一筆或一批資料,就算目前商品列表一筆資料都沒有,這顆按鈕依然存在,依然可以被點擊,這正是頁首操作跟表格列操作最本質的差異,頁首操作針對的是整個頁面層級,而不是畫面裡的某一筆資料。
再舉一個頁首操作的例子,強化這種對比。假設要讓使用者一次匯出目前列表上所有商品的資料,這個操作同樣不特定於某一筆商品,而是針對整份列表:
use Filament\Actions\Action;
Action::make('exportProducts')
->label('匯出商品清單')
->icon('heroicon-o-arrow-down-tray')
->action(function () {
// 實際的檔案匯出邏輯留給後續天數展開
}),
匯出這個動作,不管勾選了哪幾筆、甚至沒有勾選任何一筆,都可以被觸發,因為它處理的對象是整份列表,不是特定的資料列。新增商品跟匯出清單,兩者都掛在頁首,都不依附於某一筆具體資料,這正是頁首操作這種掛載位置的共同性質。
最後一種掛載位置補齊三種情境的最後一塊,表單內操作。這種 Action 掛在商品的編輯表單裡,除了原本負責儲存整份表單的按鈕之外,額外掛載一個獨立的操作,針對這筆正在編輯中的商品做一次性動作。舉例來說,依目前庫存數量重新計算並提示是否需要進貨:
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Action::make('checkRestock')
->label('檢查是否需要進貨')
->icon('heroicon-o-calculator')
->action(function ($record): void {
$needsRestock = $record->stock < 10;
Notification::make()
->title($needsRestock ? '庫存偏低,建議安排進貨' : '目前庫存量正常')
->{$needsRestock ? 'warning' : 'success'}()
->send();
}),
這顆按鈕出現在商品編輯表單裡,跟其他表單欄位排在一起,但點下去執行的邏輯跟表單本身的送出流程完全無關,操作結果不透過表單儲存流程送出,而是獨立觸發、獨立完成、獨立給出通知提示。
這裡有一個很容易被誤解的地方,需要特別釐清。表單送出是把整份表單目前填寫的所有欄位資料一次性提交儲存,是這份表單原本就存在的核心動作。表單內操作則是掛載在表單裡的一個獨立按鈕,可以在完全不觸碰、不送出整份表單的情況下單獨被觸發,兩者互不干擾,使用者點了檢查是否需要進貨這顆按鈕,並不會連帶把表單裡其他還沒填完或還沒確認的欄位一併存進資料庫。
表單內操作跟表格列操作雖然都是針對「這一筆」資料,情境上仍有差異。表格列操作發生在列表頁,使用者還沒有進入這筆資料的細節,只是在一長串清單裡順手觸發一個快捷動作。表單內操作發生在使用者已經打開這筆資料、正在檢視或編輯的當下,這時候使用者對這筆資料的細節有更完整的掌握,操作往往也更貼近這筆資料當下的具體狀態。

今天定案了 Action 這個詞在本系列的意義,掛載在資料列、頁面或表單上,代表一個可觸發的操作單元,含表格列操作、頁首操作、表單內操作三種掛載位置。這三種位置今天都在商品 Resource 上分別示範過一次,表格列操作處理了單筆快捷切換與批次上架,頁首操作處理了新增商品與匯出清單,表單內操作處理了依庫存重新計算是否需要進貨。
第二階段到今天先收在這裡。從表單欄位型別、表單版面配置、表格欄位呈現、篩選排序搜尋,一路收斂到今天的操作機制,商品 Resource 現在已經是一個看得清楚、找得到資料、也能被操作的完整介面。這個階段完整的回顧留給後面的階段收尾篇處理,今天不展開。
商品表單裡目前七個既有欄位,加上 Day 6 新增的圖片欄位,說到底都是商品自己身上的屬性,價格是商品的價格,庫存是商品的庫存,沒有一個欄位牽涉到別的模組。但一套真正會上線的後台系統,遲早要處理「這筆資料跟另一個模組的資料有關聯」的情境,商品總有一天要能標示屬於哪張訂單、關聯到哪個供應商。表單裡選關聯資料,下拉選單背後在做什麼,是接下來要處理的內容。