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' ? $this->fixFailureNotification($action) : $action, $table->getToolbarActions(), ), ); } 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( '%s%s', e($field['label']), $field['type'] === 'file' ? $this->fileCell($field) : e($field['value'] ?? ''), )) ->implode(''); return new HtmlString( '
' .'' .$rows .'
', ); } /** * 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( '', $previewUrl, $previewUrl, ) : ''; return sprintf( '
%s%s%s
', $thumbnail, e($file->original_name), $downloadUrl, Color::Blue[600], Blade::render(''), ); } private function fixFailureNotification(BulkAction $action): BulkAction { $originalAction = $action->getActionFunction(); if ($originalAction === null) { return $action; } return $action->action(function (array $arguments) use ($action, $originalAction) { try { return $action->evaluate($originalAction, $arguments); } catch (Halt $exception) { $action->sendFailureNotification(); throw $exception; } }); } }