*/ protected string $exporter; protected ?string $job = null; protected int | Closure $chunkSize = 100; protected int | Closure | null $maxRows = null; protected string | Closure | null $csvDelimiter = null; /** * @var array | Closure */ protected array | Closure $options = []; protected string | Closure | null $fileDisk = null; protected string | Closure | null $fileName = null; /** * @var array | Closure | null */ protected array | Closure | null $formats = null; protected ?Closure $modifyQueryUsing = null; protected bool | Closure $hasColumnMapping = true; protected function setUp(): void { parent::setUp(); $this->label(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): string => __('filament-actions::export.label', ['label' => $action->getPluralModelLabel()])); $this->modalHeading(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): string => __('filament-actions::export.modal.heading', ['label' => $action->getPluralModelLabel()])); $this->modalSubmitActionLabel(__('filament-actions::export.modal.actions.export.label')); $this->groupedIcon(FilamentIcon::resolve('actions::export-action.grouped') ?? 'heroicon-m-arrow-down-tray'); $this->form(fn (ExportAction | ExportTableAction | ExportTableBulkAction $action): array => [ ...($action->hasColumnMapping() ? [Fieldset::make(__('filament-actions::export.modal.form.columns.label')) ->columns(1) ->inlineLabel() ->schema(function () use ($action): array { return array_map( fn (ExportColumn $column): Split => Split::make([ Forms\Components\Checkbox::make('isEnabled') ->label(__('filament-actions::export.modal.form.columns.form.is_enabled.label', ['column' => $column->getName()])) ->hiddenLabel() ->default($column->isEnabledByDefault()) ->live() ->grow(false), Forms\Components\TextInput::make('label') ->label(__('filament-actions::export.modal.form.columns.form.label.label', ['column' => $column->getName()])) ->hiddenLabel() ->default($column->getLabel()) ->placeholder($column->getLabel()) ->disabled(fn (Forms\Get $get): bool => ! $get('isEnabled')) ->required(fn (Forms\Get $get): bool => (bool) $get('isEnabled')), ]) ->verticallyAlignCenter() ->statePath($column->getName()), $action->getExporter()::getColumns(), ); }) ->statePath('columnMap')] : []), ...$action->getExporter()::getOptionsFormComponents(), ]); $this->action(function (ExportAction | ExportTableAction | ExportTableBulkAction $action, array $data, Component $livewire) { $exporter = $action->getExporter(); if ($livewire instanceof HasTable) { $query = $livewire->getTableQueryForExport(); } else { $query = $exporter::getModel()::query(); } $query = $exporter::modifyQuery($query); $options = array_merge( $action->getOptions(), Arr::except($data, ['columnMap']), ); if ($this->modifyQueryUsing) { $query = $this->evaluate($this->modifyQueryUsing, [ 'query' => $query, 'options' => $options, ]) ?? $query; } $records = $action instanceof ExportTableBulkAction ? $action->getRecords() : null; $totalRows = $records ? $records->count() : $query->toBase()->getCountForPagination(); if ((! $records) && $query->getQuery()->limit) { $totalRows = min($totalRows, $query->getQuery()->limit); } $maxRows = $action->getMaxRows() ?? $totalRows; if ($maxRows < $totalRows) { Notification::make() ->title(__('filament-actions::export.notifications.max_rows.title')) ->body(trans_choice('filament-actions::export.notifications.max_rows.body', $maxRows, [ 'count' => Number::format($maxRows), ])) ->danger() ->send(); return; } $user = auth()->user(); if ($action->hasColumnMapping()) { $columnMap = collect($data['columnMap']) ->dot() ->reduce(fn (Collection $carry, mixed $value, string $key): Collection => $carry->mergeRecursive([ Str::beforeLast($key, '.') => [Str::afterLast($key, '.') => $value], ]), collect()) ->filter(fn (array $column): bool => $column['isEnabled'] ?? false) ->mapWithKeys(fn (array $column, string $columnName): array => [$columnName => $column['label']]) ->all(); } else { $columnMap = collect($exporter::getColumns()) ->mapWithKeys(fn (ExportColumn $column): array => [$column->getName() => $column->getLabel()]) ->all(); } $export = app(Export::class); $export->user()->associate($user); $export->exporter = $exporter; $export->total_rows = $totalRows; $exporter = $export->getExporter( columnMap: $columnMap, options: $options, ); $export->file_disk = $action->getFileDisk() ?? $exporter->getFileDisk(); // Temporary save to obtain the sequence number of the export file. $export->save(); // Delete the export directory to prevent data contamination from previous exports with the same ID. $export->deleteFileDirectory(); $export->file_name = $action->getFileName($export) ?? $exporter->getFileName($export); $export->save(); $formats = $action->getFormats() ?? $exporter->getFormats(); $hasCsv = in_array(ExportFormat::Csv, $formats); $hasXlsx = in_array(ExportFormat::Xlsx, $formats); $serializedQuery = EloquentSerializeFacade::serialize($query); $job = $action->getJob(); $jobQueue = $exporter->getJobQueue(); $jobConnection = $exporter->getJobConnection(); $jobBatchName = $exporter->getJobBatchName(); // We do not want to send the loaded user relationship to the queue in job payloads, // in case it contains attributes that are not serializable, such as binary columns. $export->unsetRelation('user'); $makeCreateXlsxFileJob = fn (): CreateXlsxFile => app(CreateXlsxFile::class, [ 'export' => $export, 'columnMap' => $columnMap, 'options' => $options, ]); Bus::chain([ Bus::batch([app($job, [ 'export' => $export, 'query' => $serializedQuery, 'columnMap' => $columnMap, 'options' => $options, 'chunkSize' => $action->getChunkSize(), 'records' => $action instanceof ExportTableBulkAction ? $action->getRecords()->all() : null, ])]) ->when( filled($jobQueue), fn (PendingBatch $batch) => $batch->onQueue($jobQueue), ) ->when( filled($jobConnection), fn (PendingBatch $batch) => $batch->onConnection($jobConnection), ) ->when( filled($jobBatchName), fn (PendingBatch $batch) => $batch->name($jobBatchName), ) ->allowFailures(), ...(($hasXlsx && (! $hasCsv)) ? [$makeCreateXlsxFileJob()] : []), app(ExportCompletion::class, [ 'export' => $export, 'columnMap' => $columnMap, 'formats' => $formats, 'options' => $options, ]), ...(($hasXlsx && $hasCsv) ? [$makeCreateXlsxFileJob()] : []), ]) ->when( filled($jobQueue), fn (PendingChain $chain) => $chain->onQueue($jobQueue), ) ->when( filled($jobConnection), fn (PendingChain $chain) => $chain->onConnection($jobConnection), ) ->dispatch(); if ( (filled($jobConnection) && ($jobConnection !== 'sync')) || (blank($jobConnection) && (config('queue.default') !== 'sync')) ) { Notification::make() ->title($action->getSuccessNotificationTitle()) ->body(trans_choice('filament-actions::export.notifications.started.body', $export->total_rows, [ 'count' => Number::format($export->total_rows), ])) ->success() ->send(); } }); $this->defaultColor('gray'); $this->modalWidth('xl'); $this->successNotificationTitle(__('filament-actions::export.notifications.started.title')); if (! $this instanceof ExportTableBulkAction) { $this->model(fn (ExportAction | ExportTableAction $action): string => $action->getExporter()::getModel()); } } public static function getDefaultName(): ?string { return 'export'; } /** * @param class-string $exporter */ public function exporter(string $exporter): static { $this->exporter = $exporter; return $this; } /** * @param class-string | null $job */ public function job(?string $job): static { $this->job = $job; return $this; } public function chunkSize(int | Closure $size): static { $this->chunkSize = $size; return $this; } public function maxRows(int | Closure | null $rows): static { $this->maxRows = $rows; return $this; } public function csvDelimiter(string | Closure | null $delimiter): static { $this->csvDelimiter = $delimiter; return $this; } /** * @return class-string */ public function getExporter(): string { return $this->exporter; } /** * @return class-string */ public function getJob(): string { return $this->job ?? PrepareCsvExport::class; } public function getChunkSize(): int { return $this->evaluate($this->chunkSize); } public function getMaxRows(): ?int { return $this->evaluate($this->maxRows); } /** * @param array | Closure $options */ public function options(array | Closure $options): static { $this->options = $options; return $this; } /** * @return array */ public function getOptions(): array { return $this->evaluate($this->options); } public function fileDisk(string | Closure | null $disk): static { $this->fileDisk = $disk; return $this; } public function getFileDisk(): ?string { return $this->evaluate($this->fileDisk); } public function fileName(string | Closure | null $name): static { $this->fileName = $name; return $this; } public function getFileName(Export $export): ?string { return $this->evaluate($this->fileName, [ 'export' => $export, ]); } /** * @param array | Closure | null $formats */ public function formats(array | Closure | null $formats): static { $this->formats = $formats; return $this; } /** * @return array | null */ public function getFormats(): ?array { return $this->evaluate($this->formats); } public function modifyQueryUsing(?Closure $callback): static { $this->modifyQueryUsing = $callback; return $this; } public function columnMapping(bool | Closure $condition = true): static { $this->hasColumnMapping = $condition; return $this; } public function hasColumnMapping(): bool { return (bool) $this->evaluate($this->hasColumnMapping); } }