From 1a6e1add323097182d4d5ac3335c611ac5012907 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Sat, 2 Aug 2025 00:16:10 -0400 Subject: [PATCH 1/2] Add initial support for filtering form submissions export --- .../js/components/forms/SubmissionListing.vue | 2 + src/Forms/Exporters/CsvExporter.php | 73 +++++++++++++------ src/Forms/Exporters/Exporter.php | 10 +-- src/Forms/Exporters/JsonExporter.php | 68 ++++++++++++++++- 4 files changed, 121 insertions(+), 32 deletions(-) diff --git a/resources/js/components/forms/SubmissionListing.vue b/resources/js/components/forms/SubmissionListing.vue index 7b91cf7db5a..105714c9b80 100644 --- a/resources/js/components/forms/SubmissionListing.vue +++ b/resources/js/components/forms/SubmissionListing.vue @@ -121,6 +121,8 @@ export default { listingKey: 'submissions', preferencesPrefix: `forms.${this.form}`, requestUrl: cp_url(`forms/${this.form}/submissions`), + pushQuery: true, + previousFilters: null, } }, diff --git a/src/Forms/Exporters/CsvExporter.php b/src/Forms/Exporters/CsvExporter.php index 202139874ce..088abcd7e8d 100644 --- a/src/Forms/Exporters/CsvExporter.php +++ b/src/Forms/Exporters/CsvExporter.php @@ -2,8 +2,10 @@ namespace Statamic\Forms\Exporters; +use Illuminate\Support\Uri; use League\Csv\Writer; -use SplTempFileObject; +use Statamic\Facades\Scope; +use Statamic\Fields\Field; use Statamic\Support\Arr; class CsvExporter extends Exporter @@ -11,16 +13,20 @@ class CsvExporter extends Exporter private Writer $writer; protected static string $title = 'CSV'; - public function export(): string + public function export(string $path): void { - $this->writer = Writer::createFromFileObject(new SplTempFileObject); + $this->writer = Writer::createFromPath($path); $this->writer->setDelimiter(Arr::get($this->config, 'delimiter', config('statamic.forms.csv_delimiter', ','))); $this->insertHeaders(); - - $this->insertData(); - - return (string) $this->writer; + $this->query() + ->lazy(10) + ->each(fn ($submission) => $this->writer->insertOne( + collect($submission->toArray()) + ->except('id') + ->map(fn ($value) => (is_array($value)) ? implode(', ', $value) : ((string) $value)) + ->all() + )); } private function insertHeaders() @@ -35,25 +41,46 @@ private function insertHeaders() $this->writer->insertOne($headers); } - private function insertData() + public function extension(): string { - $data = $this->form->submissions()->map(function ($submission) { - $submission = $submission->toArray(); - - $submission['date'] = (string) $submission['date']; - - unset($submission['id']); - - return collect($submission)->map(function ($value) { - return (is_array($value)) ? implode(', ', $value) : $value; - })->all(); - })->all(); - - $this->writer->insertAll($data); + return 'csv'; } - public function extension(): string + private function query() { - return 'csv'; + $form = $this->form; + $query = $form->querySubmissions(); + + if ($url = request()->headers->get('referer')) { + $url = Uri::of($url); + + if ($search = $url->query()->get('search')) { + $query->where('date', 'like', '%'.$search.'%'); + + $form->blueprint() + ->fields() + ->all() + ->filter(fn (Field $field) => in_array($field->type(), ['text', 'textarea', 'integer'])) + ->each(fn (Field $field) => $query->orWhere($field->handle(), 'like', '%'.$search.'%')); + } + + if ($filters = $url->query()->get('filters')) { + $filters = base64_decode($filters); + $filters = json_validate($filters) ? json_decode($filters, true) : []; + collect($filters) + ->map(fn ($values, $handle) => (object) [ + 'filterInstance' => Scope::find($handle, ['form' => $form->handle()]), + 'values' => $values, + ]) + ->filter(fn ($filter) => $filter->filterInstance !== null) + ->each(fn ($filter) => $filter->filterInstance->apply($query, $filter->values)); + } + + $sortField = $url->query()->get('sort', 'date'); + $sortDirection = $url->query()->get('order', $sortField === 'date' ? 'desc' : 'asc'); + $query->orderBy($sortField, $sortDirection); + } + + return $query; } } diff --git a/src/Forms/Exporters/Exporter.php b/src/Forms/Exporters/Exporter.php index 518cafca6d3..a70231a468d 100644 --- a/src/Forms/Exporters/Exporter.php +++ b/src/Forms/Exporters/Exporter.php @@ -3,8 +3,8 @@ namespace Statamic\Forms\Exporters; use Illuminate\Http\Response; +use Illuminate\Support\Facades\File; use Statamic\Contracts\Forms\Form; -use Statamic\Facades\File; use Symfony\Component\HttpFoundation\BinaryFileResponse; use function Statamic\trans as __; @@ -16,7 +16,7 @@ abstract class Exporter protected string $handle; protected Form $form; - abstract public function export(): string; + abstract public function export(string $path): void; public function setHandle(string $handle) { @@ -75,11 +75,9 @@ public function response(): Response public function download(): BinaryFileResponse { - $content = $this->export(); - $path = storage_path('statamic/tmp/forms/'.$this->form->handle().'-'.time().'.'.$this->extension()); - - File::put($path, $content); + File::put($path, ''); + $this->export($path); return response()->download($path)->deleteFileAfterSend(); } diff --git a/src/Forms/Exporters/JsonExporter.php b/src/Forms/Exporters/JsonExporter.php index b91ef490fc1..ee677861fad 100644 --- a/src/Forms/Exporters/JsonExporter.php +++ b/src/Forms/Exporters/JsonExporter.php @@ -2,15 +2,39 @@ namespace Statamic\Forms\Exporters; +use Illuminate\Support\Uri; +use Statamic\Facades\File; +use Statamic\Facades\Scope; +use Statamic\Fields\Field; + class JsonExporter extends Exporter { protected static string $title = 'JSON'; - public function export(): string + public function export(string $path): void { - $submissions = $this->form->submissions()->toArray(); + $file = fopen($path, 'w'); + if (!$file) { + return; + } + + fputs($file, '['); + + $first = true; + $this->query() + ->lazy(10) + ->each(function ($submission) use ($file, &$first) { + if (!$first) { + fputs($file, ','); + } + + fputs($file, json_encode($submission)); - return json_encode($submissions); + $first = false; + }); + + fputs($file, ']'); + fclose($file); } public function contentType(): string @@ -22,4 +46,42 @@ public function extension(): string { return 'json'; } + + private function query() + { + $form = $this->form; + $query = $form->querySubmissions(); + + if ($url = request()->headers->get('referer')) { + $url = Uri::of($url); + + if ($search = $url->query()->get('search')) { + $query->where('date', 'like', '%'.$search.'%'); + + $form->blueprint() + ->fields() + ->all() + ->filter(fn (Field $field) => in_array($field->type(), ['text', 'textarea', 'integer'])) + ->each(fn (Field $field) => $query->orWhere($field->handle(), 'like', '%'.$search.'%')); + } + + if ($filters = $url->query()->get('filters')) { + $filters = base64_decode($filters); + $filters = json_validate($filters) ? json_decode($filters, true) : []; + collect($filters) + ->map(fn ($values, $handle) => (object) [ + 'filterInstance' => Scope::find($handle, ['form' => $form->handle()]), + 'values' => $values, + ]) + ->filter(fn ($filter) => $filter->filterInstance !== null) + ->each(fn ($filter) => $filter->filterInstance->apply($query, $filter->values)); + } + + $sortField = $url->query()->get('sort', 'date'); + $sortDirection = $url->query()->get('order', $sortField === 'date' ? 'desc' : 'asc'); + $query->orderBy($sortField, $sortDirection); + } + + return $query; + } } From c479d99c7633941bd4d7bf2d544287660345ff35 Mon Sep 17 00:00:00 2001 From: Daniel Weaver Date: Sat, 2 Aug 2025 00:30:37 -0400 Subject: [PATCH 2/2] Code style --- src/Forms/Exporters/Exporter.php | 2 +- src/Forms/Exporters/JsonExporter.php | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Forms/Exporters/Exporter.php b/src/Forms/Exporters/Exporter.php index a70231a468d..51fcdf9507e 100644 --- a/src/Forms/Exporters/Exporter.php +++ b/src/Forms/Exporters/Exporter.php @@ -76,7 +76,7 @@ public function response(): Response public function download(): BinaryFileResponse { $path = storage_path('statamic/tmp/forms/'.$this->form->handle().'-'.time().'.'.$this->extension()); - File::put($path, ''); + File::put($path, ''); $this->export($path); return response()->download($path)->deleteFileAfterSend(); diff --git a/src/Forms/Exporters/JsonExporter.php b/src/Forms/Exporters/JsonExporter.php index ee677861fad..593d4f7c1ed 100644 --- a/src/Forms/Exporters/JsonExporter.php +++ b/src/Forms/Exporters/JsonExporter.php @@ -3,7 +3,6 @@ namespace Statamic\Forms\Exporters; use Illuminate\Support\Uri; -use Statamic\Facades\File; use Statamic\Facades\Scope; use Statamic\Fields\Field; @@ -14,26 +13,26 @@ class JsonExporter extends Exporter public function export(string $path): void { $file = fopen($path, 'w'); - if (!$file) { + if (! $file) { return; } - fputs($file, '['); + fwrite($file, '['); $first = true; $this->query() ->lazy(10) ->each(function ($submission) use ($file, &$first) { - if (!$first) { - fputs($file, ','); + if (! $first) { + fwrite($file, ','); } - fputs($file, json_encode($submission)); + fwrite($file, json_encode($submission)); $first = false; }); - fputs($file, ']'); + fwrite($file, ']'); fclose($file); }