Skip to content

Commit 345f3d8

Browse files
Merge branch 'main' into feat/ccp-3031-customize-print
2 parents 7899e0f + 49ba53c commit 345f3d8

File tree

6 files changed

+742
-51
lines changed

6 files changed

+742
-51
lines changed

app/frontend/src/components/forms/SubmissionsTable.vue

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { computed, onMounted, ref, onBeforeMount } from 'vue';
66
import { useI18n } from 'vue-i18n';
77
88
import BaseDialog from '~/components/base/BaseDialog.vue';
9+
import AdvancedSubmissionSearch from './submission/AdvancedSubmissionSearch.vue';
910
import BaseFilter from '~/components/base/BaseFilter.vue';
1011
import { useAuthStore } from '~/store/auth';
1112
import { useFormStore } from '~/store/form';
@@ -59,6 +60,8 @@ const singleSubmissionDelete = ref(false);
5960
const singleSubmissionRestore = ref(false);
6061
const sort = ref({});
6162
const firstDataLoad = ref(true);
63+
const drawerOpen = ref(false);
64+
6265
// When filtering, this data will not be preselected when clicking reset
6366
const tableFilterIgnore = ref([
6467
{ key: 'updatedAt' },
@@ -205,6 +208,50 @@ const BASE_HEADERS = computed(() => {
205208
return headers;
206209
});
207210
211+
// All possible headers
212+
const ALL_HEADER_KEYS = computed(() => {
213+
return BASE_HEADERS.value ? BASE_HEADERS.value.map((h) => h.key) : [];
214+
});
215+
216+
const baseOrderIndex = computed(() => {
217+
const map = new Map();
218+
(BASE_HEADERS.value ?? []).forEach((h, i) => map.set(h.key, i));
219+
return map;
220+
});
221+
222+
const currentPrefKeys = computed(() => USER_PREFERENCES.value ?? []);
223+
224+
function normaliseToBaseOrder(keys) {
225+
const seen = new Set();
226+
return (
227+
keys
228+
// keep only real headers
229+
.filter((k) => ALL_HEADER_KEYS.value.includes(k))
230+
// remove duplicates (keep first occurrence)
231+
.filter((k) => (seen.has(k) ? false : (seen.add(k), true)))
232+
// sort according to canonical BASE_HEADERS order
233+
.sort(
234+
(a, b) =>
235+
(baseOrderIndex.value.get(a) ?? 9999) -
236+
(baseOrderIndex.value.get(b) ?? 9999)
237+
)
238+
);
239+
}
240+
async function addColumns(keys, options) {
241+
const { includeReset = false, toFront = false } = options;
242+
const validNew = normaliseToBaseOrder(keys);
243+
const base = includeReset
244+
? normaliseToBaseOrder([...currentPrefKeys.value, ...RESET_HEADERS.value])
245+
: normaliseToBaseOrder(currentPrefKeys.value);
246+
247+
// Append or prepend new keys while preserving canonical order
248+
const next = toFront
249+
? normaliseToBaseOrder([...validNew, ...base])
250+
: normaliseToBaseOrder([...base, ...validNew]);
251+
252+
await updateColumns(next);
253+
}
254+
208255
// The headers are based on the base headers but are modified
209256
// by the following order:
210257
// Add CRUD options to headers
@@ -382,7 +429,7 @@ async function getSubmissionData() {
382429
paginationEnabled: true,
383430
sortBy: sort.value,
384431
search: search.value,
385-
searchEnabled: search.value.length > 0,
432+
searchEnabled: search.value.length > 0 || search.value?.value?.length > 0,
386433
createdAt: Object.values({
387434
minDate: moment().subtract(50, 'years').utc().format('YYYY-MM-DD'), //Get User filter Criteria (Min Date)
388435
maxDate: moment().add(50, 'years').utc().format('YYYY-MM-DD'), //Get User filter Criteria (Max Date)
@@ -628,7 +675,12 @@ async function updateFormPreferences(id, columns, filters, sort) {
628675
}
629676
630677
async function handleSearch(value) {
631-
search.value = value;
678+
if (value?.fields?.length) {
679+
await addColumns(value.fields, { includeReset: true });
680+
search.value = value;
681+
} else {
682+
search.value = value;
683+
}
632684
if (value === '') {
633685
await refreshSubmissions();
634686
} else {
@@ -799,6 +851,14 @@ defineExpose({
799851
</template>
800852
</v-checkbox>
801853
</div>
854+
<div>
855+
<AdvancedSubmissionSearch
856+
v-model="drawerOpen"
857+
:form-fields="formFields"
858+
location="right"
859+
@search="handleSearch"
860+
/>
861+
</div>
802862
803863
<div>
804864
<!-- search input -->
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<script setup>
2+
import { ref, watch, computed, defineProps, defineEmits } from 'vue';
3+
4+
const props = defineProps({
5+
modelValue: { type: Boolean, default: false },
6+
formFields: { type: Array, required: true },
7+
location: { type: String, default: 'right' },
8+
9+
// Optional: customize the open button text
10+
buttonLabel: { type: String, default: 'Advanced Search' },
11+
12+
// NEW: show summary chips outside the drawer
13+
showSearchChips: { type: Boolean, default: true },
14+
});
15+
16+
const emit = defineEmits(['update:modelValue', 'search']);
17+
18+
const isOpen = ref(props.modelValue);
19+
20+
// Drawer state
21+
const searchQuery = ref('');
22+
const selectedFields = ref([]);
23+
24+
// NEW: “applied” search state for chips (so chips reflect what was applied)
25+
const appliedQuery = ref('');
26+
const appliedFields = ref([]);
27+
28+
// Sync v-model <-> internal drawer state
29+
watch(
30+
() => props.modelValue,
31+
(val) => (isOpen.value = val)
32+
);
33+
34+
watch(isOpen, (val) => emit('update:modelValue', val));
35+
36+
function openDrawer() {
37+
isOpen.value = true;
38+
}
39+
40+
function closeDrawer() {
41+
isOpen.value = false;
42+
}
43+
44+
function removeField(field) {
45+
selectedFields.value = selectedFields.value.filter((f) => f !== field);
46+
}
47+
48+
function getLabel(value) {
49+
const match = props.formFields.find((f) => f.value === value);
50+
return match ? match.label : value;
51+
}
52+
53+
/**
54+
* Build “summary chips” for the applied search:
55+
* - if fields exist: one chip per field: "Field Label: term"
56+
* - if no fields but term exists: one chip: "Any field: term"
57+
* - if term empty but fields exist: chips "Field Label" (field-only filter)
58+
*/
59+
const hasAppliedTerm = computed(() => {
60+
return Boolean((appliedQuery.value ?? '').trim());
61+
});
62+
63+
const appliedFieldChips = computed(() => {
64+
return (appliedFields.value || []).map((f) => ({
65+
key: `field:${f}`,
66+
field: f,
67+
label: getLabel(f),
68+
}));
69+
});
70+
71+
function emitSearchAndApply() {
72+
const payload = {
73+
value: searchQuery.value,
74+
fields: selectedFields.value,
75+
};
76+
77+
// Update applied state (chips reflect “applied” search, not “in-progress” edits)
78+
appliedQuery.value = payload.value;
79+
appliedFields.value = [...payload.fields];
80+
81+
emit('search', payload);
82+
closeDrawer();
83+
}
84+
85+
function removeAppliedTerm() {
86+
appliedQuery.value = '';
87+
searchQuery.value = '';
88+
89+
emit('search', {
90+
value: '',
91+
fields: appliedFields.value,
92+
});
93+
}
94+
95+
function removeAppliedField(field) {
96+
appliedFields.value = appliedFields.value.filter((f) => f !== field);
97+
selectedFields.value = [...appliedFields.value];
98+
99+
emit('search', {
100+
value: appliedQuery.value,
101+
fields: appliedFields.value,
102+
});
103+
}
104+
105+
function clearAllApplied() {
106+
appliedQuery.value = '';
107+
appliedFields.value = [];
108+
searchQuery.value = '';
109+
selectedFields.value = [];
110+
111+
emit('search', { value: '', fields: [] });
112+
}
113+
</script>
114+
115+
<template>
116+
<div class="d-flex align-center flex-wrap ga-2">
117+
<!-- Open button -->
118+
<v-btn
119+
class="md-ml-2 mt-1"
120+
prepend-icon="mdi:mdi-magnify"
121+
variant="text"
122+
no-caps
123+
@click="openDrawer"
124+
>
125+
{{ buttonLabel }}
126+
</v-btn>
127+
128+
<!-- NEW: Applied search chips -->
129+
<template v-if="showSearchChips">
130+
<!-- Search term chip -->
131+
<v-chip
132+
v-if="hasAppliedTerm"
133+
closable
134+
class="ma-1"
135+
color="primary"
136+
variant="outlined"
137+
@click:close="removeAppliedTerm"
138+
>
139+
Search: "{{ appliedQuery }}"
140+
</v-chip>
141+
142+
<!-- Field chips -->
143+
<v-chip
144+
v-for="chip in appliedFieldChips"
145+
:key="chip.key"
146+
closable
147+
class="ma-1"
148+
color="primary"
149+
variant="outlined"
150+
@click:close="removeAppliedField(chip.field)"
151+
>
152+
Field: {{ chip.label }}
153+
</v-chip>
154+
155+
<!-- Clear all -->
156+
<v-btn
157+
v-if="hasAppliedTerm || appliedFieldChips.length"
158+
variant="text"
159+
density="compact"
160+
class="ml-1"
161+
@click="clearAllApplied"
162+
>
163+
Clear
164+
</v-btn>
165+
</template>
166+
<!-- Drawer -->
167+
<v-navigation-drawer
168+
v-model="isOpen"
169+
:location="location"
170+
temporary
171+
width="400"
172+
>
173+
<v-card flat>
174+
<v-card-title class="text-h6 d-flex justify-space-between align-center">
175+
Advanced Search
176+
<v-btn icon="mdi:mdi-close" variant="text" @click="closeDrawer" />
177+
</v-card-title>
178+
179+
<v-divider />
180+
181+
<v-card-text>
182+
<!-- Search Input -->
183+
<v-text-field
184+
v-model="searchQuery"
185+
label="Search term"
186+
prepend-inner-icon="mdi:mdi-magnify"
187+
clearable
188+
class="mb-3"
189+
/>
190+
191+
<!-- Field Selection -->
192+
<v-select
193+
v-model="selectedFields"
194+
:items="formFields"
195+
label="Search in fields"
196+
multiple
197+
chips
198+
item-title="label"
199+
item-value="value"
200+
prepend-inner-icon="mdi:mdi-filter-variant"
201+
variant="outlined"
202+
class="mb-3"
203+
:menu-props="{ maxHeight: 300 }"
204+
/>
205+
206+
<!-- Active Field Chips (in-drawer selection) -->
207+
<div class="d-flex flex-wrap">
208+
<v-chip
209+
v-for="field in selectedFields"
210+
:key="field"
211+
closable
212+
class="ma-1"
213+
color="primary"
214+
variant="outlined"
215+
@click:close="removeField(field)"
216+
>
217+
{{ getLabel(field) }}
218+
</v-chip>
219+
</div>
220+
221+
<!-- Search Button -->
222+
<v-btn color="primary" class="mt-4" block @click="emitSearchAndApply">
223+
Apply Search
224+
</v-btn>
225+
</v-card-text>
226+
</v-card>
227+
</v-navigation-drawer>
228+
</div>
229+
</template>

app/frontend/tests/unit/components/forms/SubmissionsTable.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ describe('SubmissionsTable.vue', () => {
7070
},
7171
global: {
7272
plugins: [router, pinia],
73+
stubs: {
74+
...STUBS,
75+
AdvancedSubmissionSearch: {
76+
name: 'AdvancedSubmissionSearch',
77+
template: '<div/>',
78+
emits: ['search'],
79+
},
80+
},
7381
},
7482
});
7583

0 commit comments

Comments
 (0)