Notification Import dan Export
Import atau export memberi tahu pengguna tentang hasil proses melalui salah satu dari dua mekanisme, dan mekanisme yang digunakan sepenuhnya bergantung pada apakah pekerjaan selesai di dalam request. Toast bersifat sementara — jika pengguna tidak sedang melihat layar, pesan tersebut praktis tidak pernah terlihat. Itu tepat untuk pesan seperti "Saved.", tetapi salah untuk job yang baru selesai sepuluh menit setelah request awal. Karena itu, proses yang menghasilkan file juga menulis notification persistent dengan sebuah link.
Gunakan halaman ini ketika ingin mengubah wording, memahami mengapa sebuah notification muncul atau tidak muncul, atau menulis test untuknya.
Contoh minimal yang berfungsi
Wording setiap pesan berasal dari exporter atau importer, bukan dari action:
final class UserExporter extends Exporter
{
// …
public static function completedMessage(int $records): string
{
return sprintf('%s users exported.', number_format($records));
}
}2
3
4
5
6
7
8
9
final class UserImporter extends Importer
{
// …
public static function completedMessage(int $imported, int $failed): string
{
return $failed === 0
? sprintf('%d users imported.', $imported)
: sprintf('%d users imported, %d rows rejected.', $imported, $failed);
}
}2
3
4
5
6
7
8
9
10
11
Satu method tersebut menjadi title notification dan teks toast, baik pada jalur inline maupun queued.
Apa yang dikirim dan kapan
| Situasi | Toast | Notification persistent | Broadcast |
|---|---|---|---|
| export, inline | success, dengan link Download | export-ready | tidak |
| export, queued (dispatch) | success message milik action | — | — |
| export, queued (selesai) | — | export-ready, dengan Download | ya |
| export, queued (gagal) | — | export-failed | ya |
| import, inline, bersih | success | — | — |
| import, inline, ada failure | warning, dengan Download failed rows | import-finished | tidak |
| import, queued (dispatch) | info — Your import has started… | — | — |
| import, queued (selesai) | — | import-finished, dengan link report jika ada | ya |
| import, queued (gagal) | — | import-failed | ya |
Dua prinsip menjelaskan seluruh tabel:
- File layak memiliki notification persistent. Toast yang muncul ketika pengguna sedang berada di tab lain dapat membuat export sudah selesai tetapi tidak ada lagi jalan bagi pengguna untuk menemukannya.
- Inline import yang bersih tidak perlu pesan tambahan. Toast pada response sudah cukup, dan notification center yang terus diisi pesan "imported 40 rows" akan menjadi notification center yang tidak dibaca.
Inline export
use PandaPanel\Notifications\Notification;
use PandaPanel\Notifications\NotificationAction;
Notification::make('export-ready')
->title($exporter::completedMessage($result['records']))
->success()
->icon('download')
->persistent()
->broadcast(false)
->actions([
NotificationAction::make('download')->label('Download')->url($url),
])
->send($user);
Inertia::flash('toast', [
'type' => 'success',
'message' => $exporter::completedMessage($result['records']),
'url' => $url,
'urlLabel' => 'Download',
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
broadcast(false) adalah detail penting. Response yang membawa toast masih tersedia pada request ini; mengirim pesan yang sama melalui websocket akan menampilkan notifikasi dua kali. Database row tetap ditulis, sehingga file masih dapat ditemukan kemudian dari notification center.
Queued export
Notification::make('export-ready')
->title($exporter::completedMessage($result['records']))
->success()
->icon('download')
->persistent()
->actions([
NotificationAction::make('download')->label('Download')->url($url),
])
->send($user);2
3
4
5
6
7
8
9
Pada jalur ini broadcast diperlukan karena tidak ada lagi response HTTP yang dapat membawa hasil. Jika pengguna masih membuka panel, notification datang sebagai toast dan menaikkan unread count; jika panel sudah ditutup, notification tetap menunggu di notification center.
Jika seluruh retry akhirnya gagal, RunPanelExport::failed() mengirim:
Notification::make('export-failed')
->title('Export failed')
->body($exception?->getMessage() ?? 'The file could not be written.')
->danger()
->icon('triangle-alert')
->persistent()
->send($user);2
3
4
5
6
7
Import
Pada jalur inline, notification hanya dikirim jika ada row yang gagal:
Notification::make('import-finished')
->title($importer::completedMessage($result['imported'], $result['failed']))
->warning()
->persistent()
->broadcast(false)
->actions([
NotificationAction::make('failed-rows')->label('Download failed rows')->url($url),
])
->send($user);2
3
4
5
6
7
8
9
Pada queued import, notification selalu dikirim. Warna dan icon mengikuti hasil: success + check jika tidak ada failure, warning + triangle-alert jika ada. Action Download failed rows hanya ditambahkan jika report benar-benar tersedia.
RunPanelImport::failed() mengirim import-failed menggunakan pesan dari reader. Hal ini penting karena pesan seperti "That file is not a readable spreadsheet." memberi pengguna cara memperbaiki file, sedangkan "The import failed" tidak memberi informasi yang dapat ditindaklanjuti.
Download link
Kedua action menggunakan PandaPanel\Notifications\NotificationAction, yang hanya berisi label dan URL:
NotificationAction::make('download')
->label('Download')
->url(route($panel->routeName('export-file'), [
'file' => $result['file'],
'exporter' => $exporter,
], absolute: false));2
3
4
5
6
Notification adalah database row yang mungkin masih ada minggu depan, jauh setelah halaman yang membuatnya sudah tidak lagi relevan. Karena itu yang disimpan adalah link yang dibuat server. Target link melakukan authorization ulang saat dikunjungi — satu-satunya pemeriksaan yang masih bermakna seminggu kemudian — dan itulah alasan kedua download endpoint selalu membangun ulang owner directory dari pengguna yang sedang meminta.
Secara default, mengikuti notification action akan menandai notification sebagai sudah dibaca (markAsRead(true)), dan URL dibuka di tab yang sama (url($url, newTab: false)).
Payload toast
Bagian transient menggunakan Inertia::flash('toast', …), dengan shape yang dibaca frontend di resources/js/lib/flashToast.ts:
Inertia::flash('toast', [
'type' => 'success', // success | info | warning | error
'message' => 'Imported 998 rows.',
'url' => '/admin/imports/failed-rows-2026-08-15-114233.csv?importer=…',
'urlLabel' => 'Download failed rows',
]);2
3
4
5
6
url dan urlLabel dirender sebagai tombol action pada toast; tanpa url, tidak ada tombol. Toast tidak pernah melakukan navigasi otomatis karena hal tersebut dapat menginterupsi apa pun yang dilakukan pengguna setelah request selesai.
PandaPanel\Http\Middleware\ShareFlashToast memetakan conventional Laravel flash key (error, warning, success, info, sesuai urutan prioritas tersebut) ke channel yang sama, dan tidak pernah menimpa explicit toast. Hal ini menentukan pesan yang terlihat pada export:
- inline export membuat explicit toast sendiri, sehingga download link yang menang;
- queued export tidak membuat explicit toast, sehingga
getSuccessMessage()milik action — defaultYour export is ready.— yang tampil.
Override success message jika exporter dapat masuk queue:
ExportAction::make(OrderExporter::class, OrderResource::class)
->successMessage('Preparing your export. You will be notified when it is ready.');2
Warna dan icon
Notification menggunakan PandaPanel\Notifications\Enums\NotificationColor, dan notification tanpa explicit icon menggunakan icon default milik warnanya.
| Warna | Tipe toast | Icon default | Digunakan oleh |
|---|---|---|---|
success | success | check | export siap, queued import bersih |
info | info | info | — |
warning | warning | triangle-alert | import dengan failure |
danger | error | circle-alert | — |
Notification export mengoverride icon menjadi download; notification failure mengoverride icon menjadi triangle-alert.
Tempat notification disimpan dan dikirim
Notification persistent ditulis melalui tabel notifications milik Laravel menggunakan PandaPanel\Notifications\PanelDatabaseNotification, sehingga unreadNotifications dan markAsRead() bekerja sama seperti Laravel notification biasa. Notification broadcast menggunakan event PandaPanel\Notifications\PanelNotificationSent pada private channel pengguna dan dikirim sebagai .panel.notification.
User model harus notifiable agar bagian persistent dapat bekerja — send() hanya memanggil notify() jika method tersebut tersedia, sehingga model tanpa trait notifiable memang tidak memiliki tempat untuk menyimpan notification.
Testing
Package menyediakan helper untuk kedua mekanisme:
fakePanelNotifications();
// … run the export or import …
assertPanelNotificationSentTo($user, 'Export failed');
assertPanelNotificationStoredFor($user, 'Your export of 12 records is ready.');
assertNoPanelNotifications();
assertNoPanelNotificationsStoredFor($user);2
3
4
5
6
7
8
Argument title bersifat optional; tanpa title, assertion hanya memeriksa keberadaan notification apa pun.
Catatan
- Record count dalam pesan adalah jumlah yang benar-benar ditulis, diambil dari
ExportRun::write(), bukan count awal yang hanya digunakan untuk menentukan apakah proses masuk queue. completedMessage()dipanggil di worker untuk queued run, sehingga dependency seperti locale translation atau formatter menggunakan environment worker, bukan environment request awal.- Tidak ada email yang dikirim. Mekanisme ini hanya toast dan database notification; pengiriman email tetap menjadi concern aplikasi.
- Jika user dihapus setelah request tetapi sebelum job selesai, tidak ada notification yang dikirim. Kedua job berhenti diam-diam ketika
Auth::getProvider()->retrieveById()tidak menemukan owner. - Broadcasting bersifat optional. Tanpa broadcaster yang dikonfigurasi, database notification tetap tersedia; yang hilang hanya live toast.