@iqual/nuxt-icms 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.vue +3 -3
- package/components/DesignSystem/Atoms/LinkListItem.vue +1 -1
- package/components/Form/CheckboxInput.vue +3 -3
- package/components/Form/CheckboxInputs.vue +3 -3
- package/components/Form/FileUploadInput.vue +3 -3
- package/components/Form/RadioButtonsInput.vue +3 -3
- package/components/Form/SelectInput.vue +4 -4
- package/components/Form/SubmitButton.vue +1 -1
- package/components/Form/TextAreaInput.vue +2 -2
- package/components/Form/TextEmailInput.vue +2 -2
- package/components/Form/TextInput.vue +2 -2
- package/components/Form/TextPhoneInput.vue +2 -2
- package/components/Form/WebformMarkup.vue +1 -1
- package/components/Form/composables/useFormElement.ts +1 -1
- package/components/Media/Image/fragment.graphql +6 -0
- package/components/Node/Icms/Page/SearchResult/fragment.graphql +4 -2
- package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
- package/components/Node/Icms/Page/fragment.graphql +0 -18
- package/components/Paragraph/Downloads/fragment.graphql +3 -2
- package/components/Paragraph/Downloads/index.vue +2 -4
- package/components/Paragraph/Tabs/Element/index.vue +1 -17
- package/components/Paragraph/Tabs/Layout/index.vue +4 -13
- package/components/Search/Facets/FacetDropdownUI.vue +235 -92
- package/components/Search/Facets/FacetedSearchUI.vue +385 -69
- package/components/Search/Facets/fragment.graphql +22 -3
- package/components/Section/Footer/index.vue +2 -2
- package/components/Section/Header/GlobalMenu.vue +22 -24
- package/components/Section/Header/LanguageDropdown.vue +6 -8
- package/components/Section/Header/MetaMenu.vue +11 -6
- package/components/Section/Header/index.vue +20 -21
- package/composables/useImageSources.ts +2 -1
- package/composables/useIsMobile.ts +2 -6
- package/error.vue +3 -4
- package/layouts/default.vue +2 -2
- package/layouts/error.vue +2 -2
- package/package.json +1 -1
|
@@ -7,34 +7,76 @@
|
|
|
7
7
|
v-model="facetSelections"
|
|
8
8
|
@facets-change="onFacetsChange"
|
|
9
9
|
:total-count="totalCount"
|
|
10
|
-
:displayed-count="
|
|
10
|
+
:displayed-count="props.groupResultsByFacet ?
|
|
11
|
+
displayedResults?.reduce((total, group) => total + (group?.results?.length || 0), 0) :
|
|
12
|
+
displayedResults?.[0]?.results?.length"
|
|
11
13
|
:loading="isLoading"
|
|
14
|
+
:colorScheme="colorScheme"
|
|
12
15
|
class="mb-6"
|
|
16
|
+
:enable-text-search="enableTextSearch"
|
|
17
|
+
:search-term="searchTerm"
|
|
18
|
+
@update:search-term="onSearchTermChange"
|
|
13
19
|
/>
|
|
14
20
|
|
|
15
21
|
<template v-if="debug">
|
|
16
22
|
<!-- Debug info (only if enabled) -->
|
|
17
|
-
<pre
|
|
18
|
-
|
|
23
|
+
<pre
|
|
24
|
+
class="mt-4 whitespace-pre rounded bg-gray-50 p-3 text-xs"
|
|
25
|
+
v-if="Object.keys(facetSelections).length"
|
|
26
|
+
>{{ JSON.stringify(facetSelections, null, 2) }}</pre
|
|
27
|
+
>
|
|
28
|
+
<pre
|
|
29
|
+
class="bg-blue-50 mt-2 whitespace-pre rounded p-3 text-xs"
|
|
30
|
+
v-if="facetData.length"
|
|
31
|
+
>
|
|
32
|
+
Facets: {{ facetData.length }} items</pre
|
|
33
|
+
>
|
|
19
34
|
</template>
|
|
20
35
|
</div>
|
|
21
36
|
|
|
22
37
|
<template v-if="debug && listingType !== 'all'">
|
|
23
38
|
<!-- Debug info for manual/contextual filters -->
|
|
24
|
-
<pre class="mb-4
|
|
39
|
+
<pre class="bg-yellow-50 mb-4 whitespace-pre rounded p-3 text-xs">
|
|
40
|
+
Active Filters ({{ listingType }}): {{
|
|
41
|
+
JSON.stringify(activeFilters, null, 2)
|
|
42
|
+
}}</pre
|
|
43
|
+
>
|
|
25
44
|
</template>
|
|
26
|
-
|
|
27
45
|
<!-- Results Section -->
|
|
28
46
|
<div class="results-container">
|
|
29
|
-
<template v-if="displayedResults
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
<template v-if="displayedResults?.length">
|
|
48
|
+
<div
|
|
49
|
+
v-for="(group, groupIndex) in displayedResults"
|
|
50
|
+
:key="`group-${groupIndex}`"
|
|
51
|
+
>
|
|
52
|
+
<slot name="results" :results="group" :loading="loadingMore">
|
|
53
|
+
<p v-if="props.groupResultsByFacet && 'label' in group && group?.label">{{ group?.label }} <span v-if="'count' in group && group?.count">({{ group?.count }})</span></p>
|
|
54
|
+
<!-- Default slot content if no slot is provided -->
|
|
55
|
+
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
56
|
+
<div v-for="(result, index) in group?.results"
|
|
57
|
+
:key="`result-${index}`"
|
|
58
|
+
>
|
|
59
|
+
<pre class="rounded bg-gray-100 p-2 text-xs overflow-hidden">{{
|
|
60
|
+
JSON.stringify(result, null, 2)
|
|
61
|
+
}}</pre>
|
|
62
|
+
</div>
|
|
35
63
|
</div>
|
|
64
|
+
</slot>
|
|
65
|
+
|
|
66
|
+
<!-- Group-specific Load More Section (for grouped results) -->
|
|
67
|
+
<div v-if="props.groupResultsByFacet && 'label' in group && canGroupLoadMore(group)" class="mt-6 flex justify-center">
|
|
68
|
+
<slot name="group-load-more" :loadMore="() => loadMoreGroup(group)" :loading="isGroupLoading(group)" :group="group">
|
|
69
|
+
<button
|
|
70
|
+
@click="loadMoreGroup(group)"
|
|
71
|
+
:disabled="isGroupLoading(group)"
|
|
72
|
+
:class="props.loadMoreButtonClass || defaultButtonClass"
|
|
73
|
+
>
|
|
74
|
+
<span v-if="!isGroupLoading(group)">{{ defaultLoadMoreText }}</span>
|
|
75
|
+
<span v-else>{{ defaultLoadingText }}</span>
|
|
76
|
+
</button>
|
|
77
|
+
</slot>
|
|
36
78
|
</div>
|
|
37
|
-
</
|
|
79
|
+
</div>
|
|
38
80
|
</template>
|
|
39
81
|
|
|
40
82
|
<template v-else-if="!isLoading">
|
|
@@ -54,7 +96,7 @@
|
|
|
54
96
|
</template>
|
|
55
97
|
</div>
|
|
56
98
|
|
|
57
|
-
<!-- Load More Section -->
|
|
99
|
+
<!-- Global Load More Section (for non-grouped results) -->
|
|
58
100
|
<div v-if="showLoadMore" class="mt-10 flex justify-center lg:mt-20">
|
|
59
101
|
<slot name="load-more" :loadMore="loadMore" :loading="loadingMore">
|
|
60
102
|
<button
|
|
@@ -71,6 +113,7 @@
|
|
|
71
113
|
</template>
|
|
72
114
|
|
|
73
115
|
<script lang="ts" setup>
|
|
116
|
+
import { nextTick } from 'vue'
|
|
74
117
|
import type { SearchApiQueryFilterInput } from '#graphql-operations'
|
|
75
118
|
|
|
76
119
|
export interface FacetFilterItem {
|
|
@@ -91,7 +134,7 @@ export interface FacetedSearchUIProps {
|
|
|
91
134
|
|
|
92
135
|
/**
|
|
93
136
|
* Array of facet field names to request from the search API
|
|
94
|
-
* Example: ["topics"
|
|
137
|
+
* Example: ["topics"]
|
|
95
138
|
*/
|
|
96
139
|
facets: string[]
|
|
97
140
|
|
|
@@ -117,27 +160,39 @@ export interface FacetedSearchUIProps {
|
|
|
117
160
|
|
|
118
161
|
/** Custom CSS classes for the load more button */
|
|
119
162
|
loadMoreButtonClass?: string
|
|
163
|
+
|
|
164
|
+
/** Enable fulltext search */
|
|
165
|
+
enableTextSearch?: boolean
|
|
166
|
+
|
|
167
|
+
/** Machine name of facets, enables grouping */
|
|
168
|
+
groupResultsByFacet?: string
|
|
169
|
+
|
|
170
|
+
/** Conjunction operator for combining additional filter conditions (base conditions are always AND) */
|
|
171
|
+
conjunction?: 'AND' | 'OR'
|
|
120
172
|
}
|
|
121
173
|
|
|
122
174
|
const props = withDefaults(defineProps<FacetedSearchUIProps>(), {
|
|
123
175
|
itemsPerPage: 12,
|
|
124
176
|
debug: false,
|
|
125
177
|
colorScheme: 'none',
|
|
126
|
-
listingType: 'all'
|
|
178
|
+
listingType: 'all',
|
|
179
|
+
conjunction: 'AND',
|
|
127
180
|
})
|
|
128
181
|
|
|
129
182
|
// Get the $texts function for internationalization
|
|
130
183
|
const { $texts } = useNuxtApp()
|
|
131
184
|
|
|
132
185
|
// Default text values with fallbacks
|
|
133
|
-
const defaultNoResultsText = computed(
|
|
134
|
-
|
|
186
|
+
const defaultNoResultsText = computed(
|
|
187
|
+
() =>
|
|
188
|
+
props.noResultsText ||
|
|
189
|
+
$texts('listing.no_results', 'Currently there is no content available.'),
|
|
135
190
|
)
|
|
136
|
-
const defaultLoadingText = computed(
|
|
137
|
-
props.loadingText || $texts('listing.loading', 'Loading...')
|
|
191
|
+
const defaultLoadingText = computed(
|
|
192
|
+
() => props.loadingText || $texts('listing.loading', 'Loading...'),
|
|
138
193
|
)
|
|
139
|
-
const defaultLoadMoreText = computed(
|
|
140
|
-
props.loadMoreText || $texts('listing.load_more', 'Load more')
|
|
194
|
+
const defaultLoadMoreText = computed(
|
|
195
|
+
() => props.loadMoreText || $texts('listing.load_more', 'Load more'),
|
|
141
196
|
)
|
|
142
197
|
|
|
143
198
|
// Reactive state
|
|
@@ -147,11 +202,22 @@ const currentPage = ref(1)
|
|
|
147
202
|
const loadingMore = ref(false)
|
|
148
203
|
const isLoading = ref(false)
|
|
149
204
|
|
|
205
|
+
// Group-specific pagination and loading states
|
|
206
|
+
const groupPagination = ref<Record<string, number>>({})
|
|
207
|
+
const groupLoadingMore = ref<Record<string, boolean>>({})
|
|
208
|
+
|
|
209
|
+
// Helper function to get group key from group data
|
|
210
|
+
function getGroupKey(group: any): string {
|
|
211
|
+
return group?.label || 'default'
|
|
212
|
+
}
|
|
213
|
+
|
|
150
214
|
// Contextual filters (injected from parent)
|
|
151
215
|
const contextualFilters = {
|
|
152
|
-
topics: inject<FacetFilterItem[]>('topics', [])
|
|
216
|
+
topics: inject<FacetFilterItem[]>('topics', []),
|
|
153
217
|
}
|
|
154
218
|
|
|
219
|
+
const searchTerm = useQueryString('text')
|
|
220
|
+
|
|
155
221
|
// Get active filters based on listing type
|
|
156
222
|
const activeFilters = computed(() => {
|
|
157
223
|
if (props.listingType === 'manual' && props.manualFilters) {
|
|
@@ -163,18 +229,20 @@ const activeFilters = computed(() => {
|
|
|
163
229
|
})
|
|
164
230
|
|
|
165
231
|
// Build GraphQL filter from base filter and facet selections or manual/contextual filters
|
|
166
|
-
const buildSearchFilter = (
|
|
232
|
+
const buildSearchFilter = (
|
|
233
|
+
selections?: Record<string, string[]>,
|
|
234
|
+
): SearchApiQueryFilterInput => {
|
|
167
235
|
const baseConditions = props.baseFilter.conditions || []
|
|
168
|
-
const
|
|
236
|
+
const additionalConditions: any[] = []
|
|
169
237
|
|
|
170
238
|
if (props.listingType === 'all') {
|
|
171
239
|
// For 'all' listing type, use facet selections from dropdowns
|
|
172
240
|
const facetSelections = selections || {}
|
|
173
241
|
for (const [field, values] of Object.entries(facetSelections)) {
|
|
174
242
|
if (values.length > 0) {
|
|
175
|
-
|
|
243
|
+
additionalConditions.push({
|
|
176
244
|
field,
|
|
177
|
-
value: values
|
|
245
|
+
value: values,
|
|
178
246
|
})
|
|
179
247
|
}
|
|
180
248
|
}
|
|
@@ -182,54 +250,109 @@ const buildSearchFilter = (selections?: Record<string, string[]>): SearchApiQuer
|
|
|
182
250
|
// For 'manual' and 'contextual' listing types, use activeFilters
|
|
183
251
|
const filters = activeFilters.value
|
|
184
252
|
for (const [field, items] of Object.entries(filters)) {
|
|
185
|
-
if (items
|
|
186
|
-
|
|
253
|
+
if (items) {
|
|
254
|
+
let values: string[] = []
|
|
255
|
+
|
|
256
|
+
// Handle both arrays and single objects
|
|
257
|
+
if (Array.isArray(items)) {
|
|
258
|
+
// Handle array of items (multi-select fields)
|
|
259
|
+
values = items.map((item) => item?.id).filter(Boolean)
|
|
260
|
+
} else if (items && typeof items === 'object' && items.id) {
|
|
261
|
+
// Handle single object (single-select fields)
|
|
262
|
+
values = [items.id]
|
|
263
|
+
}
|
|
187
264
|
if (values.length > 0) {
|
|
188
265
|
// Convert camelCase field names to snake_case for GraphQL
|
|
189
266
|
const graphqlField = field.replace(/([A-Z])/g, '_$1').toLowerCase()
|
|
190
|
-
|
|
267
|
+
additionalConditions.push({
|
|
191
268
|
field: graphqlField,
|
|
192
|
-
value: values
|
|
269
|
+
value: values,
|
|
193
270
|
})
|
|
194
271
|
}
|
|
195
272
|
}
|
|
196
273
|
}
|
|
197
274
|
}
|
|
198
275
|
|
|
199
|
-
|
|
276
|
+
// Combine base conditions with additional conditions
|
|
277
|
+
// The conjunction only applies within additionalConditions, not between base and additional
|
|
278
|
+
if (additionalConditions.length === 0) {
|
|
279
|
+
// If no additional conditions, just return base conditions
|
|
280
|
+
return { conditions: baseConditions }
|
|
281
|
+
} else if (baseConditions.length === 0) {
|
|
282
|
+
// If no base conditions, return additional conditions with conjunction
|
|
283
|
+
return {
|
|
284
|
+
conditions: additionalConditions,
|
|
285
|
+
conjunction: props.conjunction
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
// Create the exact structure: base group AND additional group (with specified conjunction)
|
|
289
|
+
return {
|
|
290
|
+
conjunction: 'AND',
|
|
291
|
+
groups: [
|
|
292
|
+
{
|
|
293
|
+
conditions: baseConditions
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
conditions: additionalConditions,
|
|
297
|
+
conjunction: props.conjunction
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
200
302
|
}
|
|
201
303
|
|
|
202
304
|
// Initial query to get all available facets (only for 'all' listing type)
|
|
203
305
|
const { data: initialFacetsData } = await useAsyncData(
|
|
204
|
-
props.listingType === 'all'
|
|
306
|
+
props.listingType === 'all'
|
|
307
|
+
? `facetedSearch-facets-${JSON.stringify(props.baseFilter)}-${searchTerm.value}`
|
|
308
|
+
: 'skip-facets',
|
|
205
309
|
async () => {
|
|
206
310
|
if (props.listingType !== 'all') {
|
|
207
311
|
return null
|
|
208
312
|
}
|
|
209
313
|
return await useGraphqlQuery('layoutOverviewFacetsSearch', {
|
|
210
314
|
facets: props.facets,
|
|
211
|
-
filter: props.baseFilter
|
|
315
|
+
filter: props.baseFilter,
|
|
316
|
+
keys: searchTerm.value ?? '',
|
|
212
317
|
}).then((v) => {
|
|
213
318
|
return v.data?.searchApiQuery ?? null
|
|
214
319
|
})
|
|
215
|
-
}
|
|
320
|
+
},
|
|
216
321
|
)
|
|
217
322
|
|
|
218
323
|
// Initial query to get search results
|
|
219
|
-
const { data: initialResultsData, refresh: refreshResults } =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
324
|
+
const { data: initialResultsData, refresh: refreshResults } =
|
|
325
|
+
await useAsyncData(
|
|
326
|
+
`facetedSearch-results-${props.listingType}-${JSON.stringify(props.baseFilter)}-${JSON.stringify(activeFilters.value)}`,
|
|
327
|
+
async () => {
|
|
328
|
+
const filter = buildSearchFilter()
|
|
329
|
+
|
|
330
|
+
// Debug: Log the initial filter structure
|
|
331
|
+
if (props.debug) {
|
|
332
|
+
console.log('FacetedSearch - Initial filter being sent:', JSON.stringify(filter, null, 2))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
|
|
336
|
+
filter,
|
|
337
|
+
facets: props.facets,
|
|
338
|
+
limit: props?.groupResultsByFacet ? props.itemsPerPage * 10 : props.itemsPerPage,
|
|
339
|
+
offset: 0,
|
|
340
|
+
keys: searchTerm.value ?? '',
|
|
341
|
+
})
|
|
342
|
+
isLoading.value = false
|
|
343
|
+
if (props?.groupResultsByFacet && response?.data?.searchApiQuery?.facets) {
|
|
344
|
+
response.data.searchApiQuery.facets = response.data?.searchApiQuery?.facets?.map(f => {return {
|
|
345
|
+
count: f?.count,
|
|
346
|
+
field: f?.field,
|
|
347
|
+
label: f?.label,
|
|
348
|
+
filter: f?.filter,
|
|
349
|
+
results: f?.results?.slice(0, 4)
|
|
350
|
+
}})
|
|
351
|
+
}
|
|
352
|
+
return response.data?.searchApiQuery ?? null
|
|
353
|
+
},
|
|
354
|
+
)
|
|
228
355
|
|
|
229
|
-
isLoading.value = false
|
|
230
|
-
return response.data?.searchApiQuery ?? null
|
|
231
|
-
}
|
|
232
|
-
)
|
|
233
356
|
|
|
234
357
|
// Initialize facets from initial query (only for 'all' listing type)
|
|
235
358
|
if (initialFacetsData.value) {
|
|
@@ -245,22 +368,54 @@ async function updateSearchResults(selections: Record<string, string[]>) {
|
|
|
245
368
|
try {
|
|
246
369
|
isLoading.value = true
|
|
247
370
|
currentPage.value = 1 // Reset pagination when filters change
|
|
371
|
+
|
|
372
|
+
// Reset group pagination states when filters change
|
|
373
|
+
groupPagination.value = {}
|
|
374
|
+
groupLoadingMore.value = {}
|
|
375
|
+
|
|
248
376
|
const filter = buildSearchFilter(selections)
|
|
249
377
|
|
|
378
|
+
// Debug: Log the filter structure
|
|
379
|
+
if (props.debug) {
|
|
380
|
+
console.log('FacetedSearch - Filter being sent:', JSON.stringify(filter, null, 2))
|
|
381
|
+
}
|
|
382
|
+
|
|
250
383
|
const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
|
|
251
384
|
filter,
|
|
252
|
-
|
|
253
|
-
|
|
385
|
+
facets: props.facets,
|
|
386
|
+
limit: props?.groupResultsByFacet ? props.itemsPerPage * 10 : props.itemsPerPage,
|
|
387
|
+
offset: 0,
|
|
388
|
+
keys: searchTerm.value ?? '',
|
|
254
389
|
})
|
|
255
390
|
|
|
256
|
-
|
|
257
|
-
|
|
391
|
+
// Update the results data to reflect new search results
|
|
392
|
+
if (props?.groupResultsByFacet) {
|
|
393
|
+
// Apply the same transformation as in the initial query
|
|
394
|
+
let processedFacets = response?.data?.searchApiQuery?.facets
|
|
395
|
+
if (processedFacets) {
|
|
396
|
+
processedFacets = processedFacets.map(f => ({
|
|
397
|
+
count: f?.count,
|
|
398
|
+
field: f?.field,
|
|
399
|
+
label: f?.label,
|
|
400
|
+
filter: f?.filter,
|
|
401
|
+
results: f?.results?.slice(0, 4)
|
|
402
|
+
}))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
initialResultsData.value = {
|
|
406
|
+
...initialResultsData.value,
|
|
407
|
+
facets: processedFacets,
|
|
408
|
+
count: response?.data?.searchApiQuery?.count,
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
258
412
|
initialResultsData.value = {
|
|
259
413
|
...initialResultsData.value,
|
|
260
|
-
results: response
|
|
261
|
-
count: response
|
|
414
|
+
results: response?.data?.searchApiQuery?.results,
|
|
415
|
+
count: response?.data?.searchApiQuery?.count,
|
|
262
416
|
}
|
|
263
417
|
}
|
|
418
|
+
|
|
264
419
|
} catch (error) {
|
|
265
420
|
console.error('Error updating search results:', error)
|
|
266
421
|
} finally {
|
|
@@ -270,7 +425,20 @@ async function updateSearchResults(selections: Record<string, string[]>) {
|
|
|
270
425
|
|
|
271
426
|
// Computed properties
|
|
272
427
|
const displayedResults = computed(() => {
|
|
273
|
-
|
|
428
|
+
if (props?.groupResultsByFacet) {
|
|
429
|
+
return initialResultsData.value?.facets?.filter((facet) => {
|
|
430
|
+
return facet?.field == props?.groupResultsByFacet
|
|
431
|
+
}).map((facet) => {
|
|
432
|
+
return {
|
|
433
|
+
'label': facet?.filter?.label,
|
|
434
|
+
'count': facet?.count,
|
|
435
|
+
'results': facet?.results || [],
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
return [{
|
|
440
|
+
'results': initialResultsData.value?.results || []
|
|
441
|
+
}]
|
|
274
442
|
})
|
|
275
443
|
|
|
276
444
|
const totalCount = computed(() => {
|
|
@@ -278,17 +446,43 @@ const totalCount = computed(() => {
|
|
|
278
446
|
})
|
|
279
447
|
|
|
280
448
|
const showLoadMore = computed(() => {
|
|
281
|
-
|
|
282
|
-
|
|
449
|
+
if (props?.groupResultsByFacet) {
|
|
450
|
+
return false // Individual groups will have their own load more buttons
|
|
451
|
+
}
|
|
452
|
+
const currentlyDisplayed = displayedResults.value?.[0]?.results?.length || 0
|
|
453
|
+
return (
|
|
454
|
+
currentlyDisplayed > 0 &&
|
|
455
|
+
currentlyDisplayed < totalCount.value &&
|
|
456
|
+
!isLoading.value
|
|
457
|
+
)
|
|
283
458
|
})
|
|
284
459
|
|
|
460
|
+
// Check if a group can load more results
|
|
461
|
+
function canGroupLoadMore(group: any): boolean {
|
|
462
|
+
if (!props.groupResultsByFacet || !('label' in group)) return false
|
|
463
|
+
const currentResults = group?.results?.length || 0
|
|
464
|
+
const totalResults = group?.count || 0
|
|
465
|
+
return currentResults > 0 && currentResults < totalResults && !isLoading.value
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Get loading state for a specific group
|
|
469
|
+
function isGroupLoading(group: any): boolean {
|
|
470
|
+
const groupKey = getGroupKey(group)
|
|
471
|
+
return groupLoadingMore.value[groupKey] || false
|
|
472
|
+
}
|
|
473
|
+
|
|
285
474
|
// Transform allFacets data into the format expected by FacetsUI component
|
|
286
475
|
const facetData = computed(() => {
|
|
287
476
|
if (!allFacets.value || !Array.isArray(allFacets.value)) {
|
|
288
477
|
return []
|
|
289
478
|
}
|
|
290
479
|
|
|
291
|
-
const transformedFacets: Array<{
|
|
480
|
+
const transformedFacets: Array<{
|
|
481
|
+
field: string
|
|
482
|
+
label: string
|
|
483
|
+
filter: { id: string; label: string }
|
|
484
|
+
count: number
|
|
485
|
+
}> = []
|
|
292
486
|
|
|
293
487
|
allFacets.value.forEach((facet: any) => {
|
|
294
488
|
if (facet?.field && facet?.filter) {
|
|
@@ -297,7 +491,7 @@ const facetData = computed(() => {
|
|
|
297
491
|
label: facet.label,
|
|
298
492
|
filter: {
|
|
299
493
|
id: facet.filter.id,
|
|
300
|
-
label: facet.filter.label
|
|
494
|
+
label: facet.filter.label,
|
|
301
495
|
},
|
|
302
496
|
count: facet.count || 0,
|
|
303
497
|
})
|
|
@@ -313,15 +507,37 @@ function onFacetsChange(payload: Record<string, string[]>) {
|
|
|
313
507
|
updateSearchResults(payload)
|
|
314
508
|
}
|
|
315
509
|
|
|
510
|
+
// Handle search term changes
|
|
511
|
+
function onSearchTermChange(newSearchTerm: string) {
|
|
512
|
+
// Update the searchTerm ref directly
|
|
513
|
+
searchTerm.value = newSearchTerm
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Watch for search term changes and update results automatically
|
|
517
|
+
watch(
|
|
518
|
+
searchTerm,
|
|
519
|
+
() => {
|
|
520
|
+
// Trigger search results update for 'all' listing type
|
|
521
|
+
if (props.listingType === 'all') {
|
|
522
|
+
updateSearchResults(facetSelections.value)
|
|
523
|
+
} else {
|
|
524
|
+
// For manual/contextual types, refresh results
|
|
525
|
+
refreshResults()
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
{ immediate: false }
|
|
529
|
+
)
|
|
530
|
+
|
|
316
531
|
// Default button styling based on color scheme
|
|
317
532
|
const defaultButtonClass = computed(() => {
|
|
318
|
-
const baseClasses =
|
|
533
|
+
const baseClasses =
|
|
534
|
+
'shadow-none h-auto block text-h7 font-medium py-3 px-20 rounded-full duration-300 ease-in-out w-full md:w-fit border disabled:opacity-50 disabled:cursor-not-allowed'
|
|
319
535
|
|
|
320
536
|
const colorSchemeClasses = {
|
|
321
537
|
none: 'bg-white text-black hover:bg-black hover:text-white border-black',
|
|
322
538
|
dark: 'bg-white text-black hover:bg-black hover:text-white border-black',
|
|
323
539
|
light: 'bg-gray-50 text-black hover:bg-black hover:text-white border-black',
|
|
324
|
-
accent: 'bg-white text-black hover:bg-black hover:text-white border-black'
|
|
540
|
+
accent: 'bg-white text-black hover:bg-black hover:text-white border-black',
|
|
325
541
|
}
|
|
326
542
|
|
|
327
543
|
return `${baseClasses} ${colorSchemeClasses[props.colorScheme] || colorSchemeClasses.none}`
|
|
@@ -333,15 +549,17 @@ async function loadMore() {
|
|
|
333
549
|
loadingMore.value = true
|
|
334
550
|
|
|
335
551
|
try {
|
|
336
|
-
const filter =
|
|
337
|
-
|
|
338
|
-
|
|
552
|
+
const filter =
|
|
553
|
+
props.listingType === 'all'
|
|
554
|
+
? buildSearchFilter(facetSelections.value)
|
|
555
|
+
: buildSearchFilter()
|
|
339
556
|
const offset = currentPage.value * props.itemsPerPage
|
|
340
557
|
|
|
341
558
|
const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
|
|
342
559
|
filter,
|
|
343
560
|
limit: props.itemsPerPage,
|
|
344
|
-
offset
|
|
561
|
+
offset,
|
|
562
|
+
keys: searchTerm.value ?? '',
|
|
345
563
|
})
|
|
346
564
|
|
|
347
565
|
if (response.data?.searchApiQuery?.results) {
|
|
@@ -352,7 +570,7 @@ async function loadMore() {
|
|
|
352
570
|
initialResultsData.value = {
|
|
353
571
|
...initialResultsData.value,
|
|
354
572
|
results: [...currentResults, ...newResults],
|
|
355
|
-
count: response.data.searchApiQuery.count
|
|
573
|
+
count: response.data.searchApiQuery.count,
|
|
356
574
|
}
|
|
357
575
|
|
|
358
576
|
currentPage.value += 1
|
|
@@ -364,6 +582,100 @@ async function loadMore() {
|
|
|
364
582
|
}
|
|
365
583
|
}
|
|
366
584
|
|
|
585
|
+
// Load more handler for specific group
|
|
586
|
+
async function loadMoreGroup(group: any) {
|
|
587
|
+
if (!props.groupResultsByFacet || !group || !('label' in group)) return
|
|
588
|
+
|
|
589
|
+
const groupKey = getGroupKey(group)
|
|
590
|
+
if (groupLoadingMore.value[groupKey] || isLoading.value) return
|
|
591
|
+
|
|
592
|
+
groupLoadingMore.value[groupKey] = true
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Find the facet filter value that corresponds to this group
|
|
596
|
+
const groupFacetValue = initialResultsData.value?.facets?.find(f =>
|
|
597
|
+
f && f.field === props.groupResultsByFacet && f.filter?.label === group.label
|
|
598
|
+
)?.filter?.id
|
|
599
|
+
|
|
600
|
+
if (!groupFacetValue) {
|
|
601
|
+
console.warn('Could not find facet value for group:', group.label)
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Check if this facet is already selected
|
|
606
|
+
const currentSelection = facetSelections.value[props.groupResultsByFacet]
|
|
607
|
+
const isAlreadySelected = currentSelection?.includes(groupFacetValue)
|
|
608
|
+
|
|
609
|
+
if (!isAlreadySelected) {
|
|
610
|
+
// Step 1: First apply the facet filter and wait for it to complete
|
|
611
|
+
const updatedSelections = {
|
|
612
|
+
...facetSelections.value,
|
|
613
|
+
[props.groupResultsByFacet]: [groupFacetValue]
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Update facetSelections and trigger the normal facet change flow
|
|
617
|
+
facetSelections.value = updatedSelections
|
|
618
|
+
onFacetsChange(updatedSelections)
|
|
619
|
+
|
|
620
|
+
// Wait for the facet change to complete
|
|
621
|
+
await nextTick()
|
|
622
|
+
// Wait a bit more to ensure the search results have been updated
|
|
623
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Step 2: Now do the load more operation
|
|
627
|
+
// For grouped results, find the current results for this specific group
|
|
628
|
+
const currentGroupData = initialResultsData.value?.facets?.find(f =>
|
|
629
|
+
f && f.field === props.groupResultsByFacet && f.filter?.label === group.label
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
const currentGroupResults = currentGroupData?.results || []
|
|
633
|
+
const currentCount = currentGroupResults.length
|
|
634
|
+
|
|
635
|
+
// Use the current group result count as offset to load more
|
|
636
|
+
const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
|
|
637
|
+
filter: buildSearchFilter(facetSelections.value),
|
|
638
|
+
limit: props.itemsPerPage,
|
|
639
|
+
offset: currentCount,
|
|
640
|
+
keys: searchTerm.value ?? '',
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
if (response.data?.searchApiQuery?.results) {
|
|
644
|
+
// Append new results to the existing group results
|
|
645
|
+
const newResults = response.data.searchApiQuery.results
|
|
646
|
+
|
|
647
|
+
// Update the specific group's results in the facets array
|
|
648
|
+
if (initialResultsData.value?.facets && currentGroupData) {
|
|
649
|
+
const updatedFacets = initialResultsData.value.facets.map(facet => {
|
|
650
|
+
if (facet && facet.field === props.groupResultsByFacet && facet.filter?.label === group.label) {
|
|
651
|
+
return {
|
|
652
|
+
...facet,
|
|
653
|
+
results: [...currentGroupResults, ...newResults]
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return facet
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
initialResultsData.value = {
|
|
660
|
+
...initialResultsData.value,
|
|
661
|
+
facets: updatedFacets,
|
|
662
|
+
count: response.data.searchApiQuery.count,
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Update group pagination counter for future reference
|
|
667
|
+
if (!groupPagination.value[groupKey]) {
|
|
668
|
+
groupPagination.value[groupKey] = 1
|
|
669
|
+
}
|
|
670
|
+
groupPagination.value[groupKey] += 1
|
|
671
|
+
}
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error('Error loading more results for group:', error)
|
|
674
|
+
} finally {
|
|
675
|
+
groupLoadingMore.value[groupKey] = false
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
367
679
|
// Debug logging
|
|
368
680
|
watchEffect(() => {
|
|
369
681
|
if (props.debug) {
|
|
@@ -380,8 +692,12 @@ watchEffect(() => {
|
|
|
380
692
|
console.log('FacetedSearch - Active filters:', activeFilters.value)
|
|
381
693
|
}
|
|
382
694
|
|
|
383
|
-
if (displayedResults.value.length > 0) {
|
|
384
|
-
console.log(
|
|
695
|
+
if (displayedResults.value && displayedResults.value.length > 0) {
|
|
696
|
+
console.log(
|
|
697
|
+
'FacetedSearch - Results loaded:',
|
|
698
|
+
displayedResults.value.length,
|
|
699
|
+
'groups',
|
|
700
|
+
)
|
|
385
701
|
}
|
|
386
702
|
}
|
|
387
703
|
})
|
|
@@ -392,6 +708,6 @@ defineExpose({
|
|
|
392
708
|
loadMore,
|
|
393
709
|
facetSelections: readonly(facetSelections),
|
|
394
710
|
totalCount: readonly(totalCount),
|
|
395
|
-
displayedResults: readonly(displayedResults)
|
|
711
|
+
displayedResults: readonly(displayedResults),
|
|
396
712
|
})
|
|
397
|
-
</script>
|
|
713
|
+
</script>
|