Feat: Updating FIle Services, Updating Order Views to list product extra options
This commit is contained in:
@@ -3,22 +3,60 @@
|
||||
namespace Modules\Core\Order\Filament\Extensions;
|
||||
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables\Columns\Layout\Panel;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Lunar\Admin\Support\Extending\BaseExtension;
|
||||
use Lunar\Models\OrderLine;
|
||||
use Modules\Core\File\Models\File;
|
||||
|
||||
/**
|
||||
* Same fix as OrderActionsExtension, applied to the order lines
|
||||
* table's "bulk_refund" toolbar action (Lunar\Admin\...\OrderItemsTable::
|
||||
* getBulkRefundAction()) — see that class's docblock for the underlying
|
||||
* Filament bug (failureNotification()+failure()+halt() never actually
|
||||
* sends the notification, because halt()'s Halt exception is caught before
|
||||
* Filament reaches the code that would send it).
|
||||
* extendTable() has two unrelated jobs: the "bulk_refund" toolbar-action
|
||||
* fix (see fixFailureNotification()'s own docblock — a genuine Filament
|
||||
* bug), and adding a "Custom Fields" entry to each order line's own
|
||||
* collapsible details dropdown (Lunar\Admin\...\OrderItemsTable::
|
||||
* getOrderLinesTableColumns()'s Panel — the same one already showing
|
||||
* stock level, notes, and the price_breakdowns table) — the shopper's
|
||||
* answers to Product::$custom_fields (a reference photo, personalization
|
||||
* text, ...), stored on OrderLine.meta by 3dealer's CartController::
|
||||
* customFieldsMeta() and, until now, never shown anywhere in the admin.
|
||||
*
|
||||
* Finds that Panel via $table->getCollapsibleColumnsLayout() — NOT
|
||||
* $table->getColumns(), which two earlier attempts at this both reached
|
||||
* for. HasColumns::pushColumns() flattens every Panel/Split into leaf
|
||||
* columns at table-build time and stores THAT flat list as
|
||||
* $this->columns (what getColumns() returns); the original nested
|
||||
* Panel/Stack objects actually used for rendering are kept separately —
|
||||
* in $this->columnsLayout for a non-collapsible layout component, or
|
||||
* $this->collapsibleColumnsLayout for one that IS collapsible (this
|
||||
* order-lines Panel is, via ->collapsible()). So `$column instanceof
|
||||
* Panel` over getColumns() can never match anything — Panel/Split
|
||||
* instances simply never appear in that array at all — and a fix built
|
||||
* on that check silently mutated nothing. A first attempt building a
|
||||
* brand new Panel and re-calling $table->columns() on top of the
|
||||
* existing setup fixed nothing either and instead rendered as a stray
|
||||
* empty extra column outside the dropdown (caught by actually opening
|
||||
* the order page). Mutates the found Panel's Stack in place via
|
||||
* Stack::schema(), the one part of both earlier attempts that actually
|
||||
* worked once the right object was found.
|
||||
*
|
||||
* Its own TextColumn rather than reusing the Panel's existing KeyValue:
|
||||
* KeyValue's own Blade view HTML-escapes every value ({{ $value }}),
|
||||
* which can't render a clickable link for a file answer.
|
||||
*/
|
||||
class OrderItemsTableExtension extends BaseExtension
|
||||
{
|
||||
public function extendTable(Table $table): Table
|
||||
{
|
||||
if ($table->getCollapsibleColumnsLayout() instanceof Panel) {
|
||||
$this->addCustomFieldsColumn($table->getCollapsibleColumnsLayout());
|
||||
}
|
||||
|
||||
return $table->toolbarActions(
|
||||
array_map(
|
||||
fn ($action) => $action instanceof BulkAction && $action->getName() === 'bulk_refund'
|
||||
@@ -29,6 +67,88 @@ class OrderItemsTableExtension extends BaseExtension
|
||||
);
|
||||
}
|
||||
|
||||
private function addCustomFieldsColumn(Panel $panel): void
|
||||
{
|
||||
$stack = $panel->getComponents()[0] ?? null;
|
||||
|
||||
if ($stack === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stack->schema([
|
||||
...$stack->getComponents(),
|
||||
TextColumn::make('custom_fields')
|
||||
->label('Custom Fields')
|
||||
->visible(fn (OrderLine $record) => filled($record->meta['custom_fields'] ?? null))
|
||||
->getStateUsing(fn (OrderLine $record) => $this->renderCustomFields($record))
|
||||
->html(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same table markup/classes as this Panel's own existing KeyValue
|
||||
* component (Lunar\Admin's price_breakdowns, right above this in the
|
||||
* dropdown — see lunarpanel::tables.components.key-value) for visual
|
||||
* consistency, rebuilt here rather than reused: KeyValue's Blade view
|
||||
* HTML-escapes every value ({{ $value }}), which can't render a
|
||||
* thumbnail/download link for a file answer.
|
||||
*/
|
||||
private function renderCustomFields(OrderLine $record): HtmlString
|
||||
{
|
||||
$rows = collect($record->meta['custom_fields'] ?? [])
|
||||
->map(fn (array $field) => sprintf(
|
||||
'<tr class="divide-x divide-gray-950/10 dark:divide-white/10"><td class="p-2 font-medium whitespace-nowrap">%s</td><td class="p-2">%s</td></tr>',
|
||||
e($field['label']),
|
||||
$field['type'] === 'file' ? $this->fileCell($field) : e($field['value'] ?? ''),
|
||||
))
|
||||
->implode('');
|
||||
|
||||
return new HtmlString(
|
||||
'<div class="w-full mt-2 overflow-hidden overflow-x-auto ring-1 ring-inset ring-gray-950/10 dark:ring-white/10 rounded bg-white/70 dark:bg-white/5">'
|
||||
.'<table class="min-w-full text-xs divide-y divide-gray-950/10 dark:divide-white/10"><tbody class="divide-y divide-gray-950/10 dark:divide-white/10">'
|
||||
.$rows
|
||||
.'</tbody></table></div>',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A thumbnail (previewable image types only — an inline-signed URL to
|
||||
* the same File; see FileService::retrieve()) alongside an icon-only
|
||||
* download link forcing Content-Disposition: attachment (FileService::
|
||||
* download()) — two separate signed URLs, not one reused with a query
|
||||
* string appended after signing, since a signature covers the exact
|
||||
* query parameters present when it was minted.
|
||||
*/
|
||||
private function fileCell(array $field): string
|
||||
{
|
||||
$file = File::find($field['file_id'] ?? null);
|
||||
|
||||
if ($file === null) {
|
||||
return __('lunarpanel::global.na');
|
||||
}
|
||||
|
||||
$previewUrl = URL::temporarySignedRoute('files.download', now()->addHours(2), ['file' => $file->id]);
|
||||
$downloadUrl = URL::temporarySignedRoute('files.download', now()->addHours(2), ['file' => $file->id, 'download' => 1]);
|
||||
$previewable = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||
|
||||
$thumbnail = in_array($file->mime, $previewable, true)
|
||||
? sprintf(
|
||||
'<a href="%s" target="_blank" rel="noopener"><img src="%s" alt="" style="width:2.5rem;height:2.5rem;object-fit:cover;border-radius:0.375rem;vertical-align:middle"></a>',
|
||||
$previewUrl,
|
||||
$previewUrl,
|
||||
)
|
||||
: '';
|
||||
|
||||
return sprintf(
|
||||
'<div style="display:flex;align-items:center;gap:0.5rem">%s<span>%s</span><a href="%s" title="Download" style="color:rgb(%s);display:inline-flex">%s</a></div>',
|
||||
$thumbnail,
|
||||
e($file->original_name),
|
||||
$downloadUrl,
|
||||
Color::Blue[600],
|
||||
Blade::render('<x-filament::icon icon="heroicon-o-arrow-down-tray" style="width:1rem;height:1rem"/>'),
|
||||
);
|
||||
}
|
||||
|
||||
private function fixFailureNotification(BulkAction $action): BulkAction
|
||||
{
|
||||
$originalAction = $action->getActionFunction();
|
||||
|
||||
Reference in New Issue
Block a user