@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.
Files changed (36) hide show
  1. package/app.vue +3 -3
  2. package/components/DesignSystem/Atoms/LinkListItem.vue +1 -1
  3. package/components/Form/CheckboxInput.vue +3 -3
  4. package/components/Form/CheckboxInputs.vue +3 -3
  5. package/components/Form/FileUploadInput.vue +3 -3
  6. package/components/Form/RadioButtonsInput.vue +3 -3
  7. package/components/Form/SelectInput.vue +4 -4
  8. package/components/Form/SubmitButton.vue +1 -1
  9. package/components/Form/TextAreaInput.vue +2 -2
  10. package/components/Form/TextEmailInput.vue +2 -2
  11. package/components/Form/TextInput.vue +2 -2
  12. package/components/Form/TextPhoneInput.vue +2 -2
  13. package/components/Form/WebformMarkup.vue +1 -1
  14. package/components/Form/composables/useFormElement.ts +1 -1
  15. package/components/Media/Image/fragment.graphql +6 -0
  16. package/components/Node/Icms/Page/SearchResult/fragment.graphql +4 -2
  17. package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
  18. package/components/Node/Icms/Page/fragment.graphql +0 -18
  19. package/components/Paragraph/Downloads/fragment.graphql +3 -2
  20. package/components/Paragraph/Downloads/index.vue +2 -4
  21. package/components/Paragraph/Tabs/Element/index.vue +1 -17
  22. package/components/Paragraph/Tabs/Layout/index.vue +4 -13
  23. package/components/Search/Facets/FacetDropdownUI.vue +235 -92
  24. package/components/Search/Facets/FacetedSearchUI.vue +385 -69
  25. package/components/Search/Facets/fragment.graphql +22 -3
  26. package/components/Section/Footer/index.vue +2 -2
  27. package/components/Section/Header/GlobalMenu.vue +22 -24
  28. package/components/Section/Header/LanguageDropdown.vue +6 -8
  29. package/components/Section/Header/MetaMenu.vue +11 -6
  30. package/components/Section/Header/index.vue +20 -21
  31. package/composables/useImageSources.ts +2 -1
  32. package/composables/useIsMobile.ts +2 -6
  33. package/error.vue +3 -4
  34. package/layouts/default.vue +2 -2
  35. package/layouts/error.vue +2 -2
  36. 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="displayedResults.length"
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 class="mt-4 text-xs whitespace-pre bg-gray-50 p-3 rounded" v-if="Object.keys(facetSelections).length">{{ JSON.stringify(facetSelections, null, 2) }}</pre>
18
- <pre class="mt-2 text-xs whitespace-pre bg-blue-50 p-3 rounded" v-if="facetData.length">Facets: {{ facetData.length }} items</pre>
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 text-xs whitespace-pre bg-yellow-50 p-3 rounded">Active Filters ({{ listingType }}): {{ JSON.stringify(activeFilters, null, 2) }}</pre>
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.length > 0">
30
- <slot name="results" :results="displayedResults" :loading="loadingMore">
31
- <!-- Default slot content if no slot is provided -->
32
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
33
- <div v-for="result in displayedResults" :key="result?.id || Math.random()" class="result-item">
34
- <pre class="text-xs bg-gray-100 p-2 rounded">{{ JSON.stringify(result, null, 2) }}</pre>
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
- </slot>
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", "project_phase", "locations"]
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
- props.noResultsText || $texts('listing.no_results', 'Currently there is no content available.')
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 = (selections?: Record<string, string[]>): SearchApiQueryFilterInput => {
232
+ const buildSearchFilter = (
233
+ selections?: Record<string, string[]>,
234
+ ): SearchApiQueryFilterInput => {
167
235
  const baseConditions = props.baseFilter.conditions || []
168
- const conditions = [...baseConditions]
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
- conditions.push({
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 && Array.isArray(items) && items.length > 0) {
186
- const values = items.map(item => item.id).filter(Boolean)
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
- conditions.push({
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
- return { conditions }
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' ? `facetedSearch-facets-${JSON.stringify(props.baseFilter)}` : 'skip-facets',
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 } = await useAsyncData(
220
- `facetedSearch-results-${props.listingType}-${JSON.stringify(props.baseFilter)}-${JSON.stringify(activeFilters.value)}`,
221
- async () => {
222
- const filter = buildSearchFilter()
223
- const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
224
- filter,
225
- limit: props.itemsPerPage,
226
- offset: 0
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
- limit: props.itemsPerPage,
253
- offset: 0
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
- if (response.data?.searchApiQuery?.results) {
257
- // Update the results data to reflect new search results
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.data.searchApiQuery.results,
261
- count: response.data.searchApiQuery.count
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
- return initialResultsData.value?.results || []
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
- const currentlyDisplayed = displayedResults.value.length
282
- return currentlyDisplayed > 0 && currentlyDisplayed < totalCount.value && !isLoading.value
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<{ field: string; label: string, filter: {id: string, label: string}; count: number; }> = []
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 = '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'
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 = props.listingType === 'all'
337
- ? buildSearchFilter(facetSelections.value)
338
- : buildSearchFilter()
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('FacetedSearch - Results loaded:', displayedResults.value.length, 'items')
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>