Skip to content

Commit 8ee4717

Browse files
jasonvargaclaude
andauthored
[6.x] Add ability to filter submission exports (#14432)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c7704bf commit 8ee4717

11 files changed

Lines changed: 173 additions & 40 deletions

File tree

lang/en/messages.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
'form_configure_mailer_instructions' => 'Choose the mailer for sending this email. Leave blank to fall back to the default mailer.',
136136
'form_configure_store_instructions' => 'Disable to stop storing submissions. Events and email notifications will still be sent.',
137137
'form_configure_title_instructions' => 'Use a call to action, such as \'Contact Us\'.',
138+
'form_export_filtered_description' => 'Exports submissions with current filters and visible columns.',
138139
'form_create_description' => 'Get started by creating your first form.',
139140
'getting_started_widget_collections' => 'Collections hold the different content types that make up your site, helping you stay organized.',
140141
'getting_started_widget_docs' => 'Discover everything Statamic can do, and learn how to use its powerful features the right way.',

resources/js/components/forms/SubmissionListing.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<Listing
3+
ref="listing"
34
:url="requestUrl"
45
:columns="columns"
56
:action-url="actionUrl"
@@ -42,5 +43,13 @@ export default {
4243
requestUrl: cp_url(`forms/${this.form}/submissions`),
4344
};
4445
},
46+
47+
computed: {
48+
parameters() {
49+
return this.$refs.listing?.parameters;
50+
},
51+
},
52+
53+
expose: ['parameters'],
4554
};
4655
</script>

resources/js/components/ui/Listing/Listing.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ provideListingContext({
670670
defineExpose({
671671
refresh,
672672
setFilter,
673+
parameters,
673674
});
674675
675676
watch(parameters, (newParams, oldParams) => {

resources/js/pages/forms/Show.vue

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup>
2-
import { ref } from 'vue';
2+
import { ref, computed } from 'vue';
33
import Head from '@/pages/layout/Head.vue';
4-
import { Header, Dropdown, DropdownMenu, DropdownItem, Button, CommandPaletteItem } from '@ui';
4+
import { Header, Dropdown, DropdownMenu, DropdownItem, Button, Modal, RadioGroup, Radio, CommandPaletteItem } from '@ui';
55
import ResourceDeleter from '@/components/ResourceDeleter.vue';
66
import FormSubmissionListing from '@/components/forms/SubmissionListing.vue';
77
@@ -15,6 +15,46 @@ const props = defineProps([
1515
]);
1616
1717
const deleter = ref(null);
18+
const submissionListing = ref(null);
19+
const exportModalOpen = ref(false);
20+
const exportFormat = ref(null);
21+
const exportScope = ref('all');
22+
const listingParameters = ref({});
23+
24+
const hasFilteredScope = computed(() => {
25+
const params = listingParameters.value;
26+
const hasSortOverride = (params.sort && params.sort !== 'datestamp') || (params.order && params.order !== 'desc');
27+
return !!(params.search || params.filters || hasSortOverride);
28+
});
29+
30+
function openExportModal() {
31+
listingParameters.value = submissionListing.value?.parameters ?? {};
32+
exportFormat.value = props.exporters[0]?.handle ?? null;
33+
exportScope.value = 'all';
34+
exportModalOpen.value = true;
35+
}
36+
37+
function exportSubmissions() {
38+
const exporter = props.exporters.find((e) => e.handle === exportFormat.value);
39+
if (!exporter) return;
40+
41+
let url = exporter.downloadUrl;
42+
43+
if (exportScope.value === 'filtered') {
44+
const params = listingParameters.value;
45+
const query = new URLSearchParams();
46+
if (params.search) query.set('search', params.search);
47+
if (params.sort) query.set('sort', params.sort);
48+
if (params.order) query.set('order', params.order);
49+
if (params.filters) query.set('filters', params.filters);
50+
51+
const separator = url.includes('?') ? '&' : '?';
52+
url += separator + query.toString();
53+
}
54+
55+
window.open(url, '_blank');
56+
exportModalOpen.value = false;
57+
}
1858
</script>
1959
2060
<template>
@@ -70,39 +110,51 @@ const deleter = ref(null);
70110
:redirect="redirectUrl"
71111
/>
72112
73-
<Dropdown v-if="exporters.length">
74-
<template #trigger>
75-
<Button :text="__('Export Submissions')" />
76-
</template>
77-
<DropdownMenu>
78-
<DropdownItem
79-
v-for="exporter in exporters"
80-
:key="exporter.downloadUrl"
81-
:text="exporter.title"
82-
:href="exporter.downloadUrl"
83-
target="_blank"
84-
/>
85-
</DropdownMenu>
86-
</Dropdown>
113+
<Button v-if="exporters.length" :text="__('Export Submissions')" @click="openExportModal" />
87114
88115
<CommandPaletteItem
89-
v-for="exporter in exporters"
90-
:key="exporter.downloadUrl"
116+
v-if="exporters.length"
91117
category="Actions"
92-
:text="[__('Export Submissions'), exporter.title]"
118+
:text="__('Export Submissions')"
93119
icon="save"
94-
:url="exporter.downloadUrl"
120+
:action="openExportModal"
95121
prioritize
96122
/>
97123
</Header>
98124
99125
<FormSubmissionListing
126+
ref="submissionListing"
100127
:form="form.handle"
101128
:action-url="actionUrl"
102129
sort-column="datestamp"
103130
sort-direction="desc"
104131
:columns="columns"
105-
:filters="filters"
132+
:filters="filters"
106133
/>
134+
135+
<Modal :open="exportModalOpen" @update:open="exportModalOpen = $event" :title="__('Export Submissions')">
136+
<div class="space-y-4">
137+
<div>
138+
<label class="text-sm font-medium mb-1.5 block">{{ __('Format') }}</label>
139+
<RadioGroup v-model="exportFormat" inline>
140+
<Radio v-for="format in exporters" :key="format.handle" :value="format.handle" :label="format.title" />
141+
</RadioGroup>
142+
</div>
143+
144+
<div>
145+
<label class="text-sm font-medium mb-1.5 block">{{ __('Submissions') }}</label>
146+
<RadioGroup v-model="exportScope">
147+
<Radio value="all" :label="__('All Submissions')" />
148+
<Radio value="filtered" :label="__('Filtered Submissions')" :description="__('statamic::messages.form_export_filtered_description')" :disabled="!hasFilteredScope" />
149+
</RadioGroup>
150+
</div>
151+
</div>
152+
153+
<template #footer>
154+
<div class="flex justify-end p-2">
155+
<Button variant="primary" :text="__('Export')" @click="exportSubmissions" />
156+
</div>
157+
</template>
158+
</Modal>
107159
</div>
108160
</template>

src/Forms/Exporters/CsvExporter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ private function insertHeaders()
3737

3838
private function insertData()
3939
{
40-
$data = $this->form->submissions()->map(function ($submission) {
40+
$data = $this->submissions()->map(function ($submission) {
4141
$submission = $submission->toArray();
4242

4343
$submission['date'] = (string) $submission['date'];

src/Forms/Exporters/Exporter.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Statamic\Forms\Exporters;
44

55
use Illuminate\Http\Response;
6+
use Illuminate\Support\Collection;
67
use Statamic\Contracts\Forms\Form;
78
use Statamic\Facades\File;
89
use Symfony\Component\HttpFoundation\BinaryFileResponse;
@@ -15,6 +16,7 @@ abstract class Exporter
1516
protected array $config;
1617
protected string $handle;
1718
protected Form $form;
19+
protected ?Collection $submissions = null;
1820

1921
abstract public function export(): string;
2022

@@ -25,6 +27,11 @@ public function setHandle(string $handle)
2527
return $this;
2628
}
2729

30+
public function handle(): string
31+
{
32+
return $this->handle;
33+
}
34+
2835
public function setConfig(array $config)
2936
{
3037
$this->config = $config;
@@ -39,6 +46,18 @@ public function setForm(Form $form)
3946
return $this;
4047
}
4148

49+
public function setSubmissions(Collection $submissions)
50+
{
51+
$this->submissions = $submissions;
52+
53+
return $this;
54+
}
55+
56+
protected function submissions(): Collection
57+
{
58+
return $this->submissions ?? $this->form->submissions();
59+
}
60+
4261
public function contentType(): string
4362
{
4463
return 'text/plain';

src/Forms/Exporters/JsonExporter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class JsonExporter extends Exporter
88

99
public function export(): string
1010
{
11-
$submissions = $this->form->submissions()->toArray();
11+
$submissions = $this->submissions()->toArray();
1212

1313
return json_encode($submissions);
1414
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Statamic\Http\Controllers\CP\Forms\Concerns;
4+
5+
use Statamic\Contracts\Forms\Form;
6+
use Statamic\Fields\Field;
7+
8+
trait QueriesFormSubmissionSearch
9+
{
10+
protected function applySubmissionSearch($query, Form $form, ?string $search)
11+
{
12+
if (! $search) {
13+
return $query;
14+
}
15+
16+
$query->where(function ($query) use ($form, $search) {
17+
$query->where('date', 'like', '%'.$search.'%');
18+
19+
$form->blueprint()->fields()->all()
20+
->filter(function (Field $field): bool {
21+
return in_array($field->type(), ['text', 'textarea', 'integer']);
22+
})
23+
->each(function (Field $field) use ($query, $search): void {
24+
$query->orWhere($field->handle(), 'like', '%'.$search.'%');
25+
});
26+
});
27+
28+
return $query;
29+
}
30+
}

src/Http/Controllers/CP/Forms/FormExportController.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,49 @@
44

55
use Statamic\Exceptions\NotFoundHttpException;
66
use Statamic\Http\Controllers\CP\CpController;
7+
use Statamic\Http\Controllers\CP\Forms\Concerns\QueriesFormSubmissionSearch;
8+
use Statamic\Http\Requests\FilteredRequest;
9+
use Statamic\Query\OrderBy;
10+
use Statamic\Query\Scopes\Filters\Concerns\QueriesFilters;
711

812
class FormExportController extends CpController
913
{
10-
public function export($form, $type)
14+
use QueriesFilters, QueriesFormSubmissionSearch;
15+
16+
public function export(FilteredRequest $request, $form, $type)
1117
{
1218
$this->authorize('view', $form);
1319

1420
if (! $exporter = $form->exporter($type)) {
1521
throw new NotFoundHttpException;
1622
}
1723

18-
return $this->request->has('download') ? $exporter->download() : $exporter->response();
24+
if ($this->shouldApplyFilteredScope($request)) {
25+
$exporter->setSubmissions($this->getScopedSubmissions($request, $form));
26+
}
27+
28+
return $request->has('download') ? $exporter->download() : $exporter->response();
29+
}
30+
31+
protected function shouldApplyFilteredScope(FilteredRequest $request)
32+
{
33+
return $request->has('filters') || $request->has('search') || $request->has('sort') || $request->has('order');
34+
}
35+
36+
protected function getScopedSubmissions(FilteredRequest $request, $form)
37+
{
38+
$query = $form->querySubmissions();
39+
40+
$this->queryFilters($query, $request->filters, [
41+
'form' => $form->handle(),
42+
]);
43+
44+
$this->applySubmissionSearch($query, $form, $request->input('search'));
45+
46+
if ($sort = OrderBy::column($request->input('sort'))) {
47+
$query->orderBy($sort, $request->input('order', $sort === 'date' ? 'desc' : 'asc'));
48+
}
49+
50+
return $query->get();
1951
}
2052
}

src/Http/Controllers/CP/Forms/FormSubmissionsController.php

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
namespace Statamic\Http\Controllers\CP\Forms;
44

55
use Inertia\Inertia;
6-
use Statamic\Fields\Field;
76
use Statamic\Http\Controllers\CP\CpController;
7+
use Statamic\Http\Controllers\CP\Forms\Concerns\QueriesFormSubmissionSearch;
88
use Statamic\Http\Requests\FilteredRequest;
99
use Statamic\Http\Resources\CP\Submissions\Submissions;
1010
use Statamic\Query\OrderBy;
1111
use Statamic\Query\Scopes\Filters\Concerns\QueriesFilters;
1212

1313
class FormSubmissionsController extends CpController
1414
{
15-
use QueriesFilters;
15+
use QueriesFilters, QueriesFormSubmissionSearch;
1616

1717
public function index(FilteredRequest $request, $form)
1818
{
@@ -49,19 +49,7 @@ protected function indexQuery($form)
4949
{
5050
$query = $form->querySubmissions();
5151

52-
if ($search = request('search')) {
53-
$query->where(function ($query) use ($form, $search) {
54-
$query->where('date', 'like', '%'.$search.'%');
55-
56-
$form->blueprint()->fields()->all()
57-
->filter(function (Field $field): bool {
58-
return in_array($field->type(), ['text', 'textarea', 'integer']);
59-
})
60-
->each(function (Field $field) use ($query, $search): void {
61-
$query->orWhere($field->handle(), 'like', '%'.$search.'%');
62-
});
63-
});
64-
}
52+
$this->applySubmissionSearch($query, $form, request('search'));
6553

6654
return $query;
6755
}

0 commit comments

Comments
 (0)