@datagouv/components-next 1.0.0 → 1.0.1

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 (53) hide show
  1. package/assets/main.css +0 -28
  2. package/dist/{Datafair.client-Dls5AHTE.js → Datafair.client-B5lBpOl8.js} +2 -2
  3. package/dist/{JsonPreview.client-DPDTs433.js → JsonPreview.client-Doz1Z0BS.js} +16 -16
  4. package/dist/{MapContainer.client-BdAzd7bj.js → MapContainer.client-oiieO8H-.js} +3 -3
  5. package/dist/PdfPreview.client-CdAhkDFJ.js +14513 -0
  6. package/dist/{Pmtiles.client-mF6xaOO_.js → Pmtiles.client-B0v8tGJQ.js} +2 -2
  7. package/dist/Swagger.client-CsK65JnG.js +4 -0
  8. package/dist/{XmlPreview.client-C0OgBkSq.js → XmlPreview.client-CrjHf74q.js} +15 -15
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +130 -125
  11. package/dist/components.css +1 -1
  12. package/dist/{index-BRGqW8aQ.js → index-Bbu9rOHt.js} +1 -1
  13. package/dist/{main-CNHxAJ8J.js → main-CiH8ZmBI.js} +22114 -21911
  14. package/dist/{vue3-xml-viewer.common-CmAdQfIy.js → vue3-xml-viewer.common-Bi_bsV6C.js} +1 -1
  15. package/package.json +2 -2
  16. package/src/components/DataserviceCard.vue +3 -3
  17. package/src/components/DatasetCard.vue +2 -2
  18. package/src/components/DatasetQuality.vue +23 -16
  19. package/src/components/DatasetQualityInline.vue +13 -17
  20. package/src/components/DatasetQualityScore.vue +12 -15
  21. package/src/components/DiscussionMessageCard.vue +1 -1
  22. package/src/components/ObjectCard.vue +2 -2
  23. package/src/components/ObjectCardHeader.vue +1 -1
  24. package/src/components/OrganizationHorizontalCard.vue +87 -0
  25. package/src/components/OrganizationNameWithCertificate.vue +1 -1
  26. package/src/components/ProgressBar.vue +31 -0
  27. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  28. package/src/components/ResourceAccordion/JsonPreview.client.vue +3 -3
  29. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -1
  30. package/src/components/ResourceAccordion/PdfPreview.client.vue +70 -74
  31. package/src/components/ResourceAccordion/Pmtiles.client.vue +1 -1
  32. package/src/components/ResourceAccordion/Preview.vue +1 -1
  33. package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -8
  34. package/src/components/ResourceAccordion/XmlPreview.client.vue +3 -3
  35. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +50 -1
  36. package/src/components/ReuseHorizontalCard.vue +1 -1
  37. package/src/components/Search/Filter/ProducerTypeFilter.vue +13 -3
  38. package/src/components/Search/GlobalSearch.vue +124 -28
  39. package/src/components/Toggletip.vue +5 -2
  40. package/src/components/TopicCard.vue +1 -1
  41. package/src/composables/useHasTabularData.ts +15 -0
  42. package/src/composables/useResourceCapabilities.ts +18 -5
  43. package/src/composables/useTranslation.ts +2 -1
  44. package/src/functions/api.ts +11 -3
  45. package/src/functions/api.types.ts +1 -1
  46. package/src/functions/resourceCapabilities.ts +55 -0
  47. package/src/main.ts +8 -1
  48. package/src/types/resources.ts +10 -0
  49. package/src/types/search.ts +29 -1
  50. package/dist/PdfPreview.client-CopqSDyt.js +0 -107
  51. package/dist/Swagger.client-eJ7gpfZA.js +0 -4
  52. package/dist/pdf-vue3-IkJO65RH.js +0 -273
  53. package/dist/pdf.min-f72cfa08-CdgJTooZ.js +0 -9501
@@ -113,6 +113,7 @@
113
113
  rel="ugc nofollow noopener"
114
114
  new-tab
115
115
  size="xs"
116
+ color="secondary"
116
117
  external
117
118
  @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
118
119
  >
@@ -127,7 +128,7 @@
127
128
  :id="resource.id + '-copy'"
128
129
  :data-clipboard-text="resource.url"
129
130
  :aria-describedby="resourceTitleId"
130
- color="primary"
131
+ color="secondary"
131
132
  size="xs"
132
133
  :icon="RiFileCopyLine"
133
134
  >
@@ -390,6 +391,7 @@ import { getResourceFormatIcon, getResourceTitleId, detectOgcService } from '../
390
391
  import BrandedButton from '../BrandedButton.vue'
391
392
  import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
392
393
  import { useTranslation } from '../../composables/useTranslation'
394
+ import { useHasTabularData } from '../../composables/useHasTabularData'
393
395
  import Metadata from './Metadata.vue'
394
396
  import SchemaBadge from './SchemaBadge.vue'
395
397
  import ResourceIcon from './ResourceIcon.vue'
@@ -426,6 +428,7 @@ const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue
426
428
 
427
429
  const { t } = useTranslation()
428
430
  const { formatRelativeIfRecentDate } = useFormatDate()
431
+ const checkTabularData = useHasTabularData()
429
432
 
430
433
  const hasPreview = computed(() => {
431
434
  // For JSON, PDF, and XML files, show preview.
@@ -435,13 +438,7 @@ const hasPreview = computed(() => {
435
438
  return format === 'json' || format === 'pdf' || format === 'xml'
436
439
  })
437
440
 
438
- const hasTabularData = computed(() => {
439
- // Determines if we should show the "Données" tab for tabular files AND the "Structure des données" tab (for tabular data structure)
440
- return config.tabularApiUrl
441
- && props.resource.extras['analysis:parsing:parsing_table']
442
- && !props.resource.extras['analysis:parsing:error']
443
- && (config.tabularAllowRemote || props.resource.filetype === 'file')
444
- })
441
+ const hasTabularData = computed(() => checkTabularData(props.resource))
445
442
 
446
443
  const hasPmtiles = computed(() => {
447
444
  return props.resource.extras['analysis:parsing:pmtiles_url'] || props.resource.format === 'pmtiles'
@@ -14,7 +14,7 @@
14
14
  type="warning"
15
15
  class="flex items-center space-x-2"
16
16
  >
17
- <RiErrorWarningLine class="shink-0 size-6" />
17
+ <RiErrorWarningLine class="shrink-0 size-6" />
18
18
  <span>{{ fileSizeBytes
19
19
  ? t("Fichier XML trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
20
20
  : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
@@ -25,7 +25,7 @@
25
25
  type="warning"
26
26
  class="flex items-center space-x-2"
27
27
  >
28
- <RiErrorWarningLine class="shink-0 size-6" />
28
+ <RiErrorWarningLine class="shrink-0 size-6" />
29
29
  <span>{{ t("Ce fichier XML ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
30
30
  </SimpleBanner>
31
31
  <SimpleBanner
@@ -33,7 +33,7 @@
33
33
  type="warning"
34
34
  class="flex items-center space-x-2"
35
35
  >
36
- <RiErrorWarningLine class="shink-0 size-6" />
36
+ <RiErrorWarningLine class="shrink-0 size-6" />
37
37
  <span>{{ t("Erreur lors du chargement de l'aperçu XML.") }}</span>
38
38
  </SimpleBanner>
39
39
  </div>
@@ -228,6 +228,52 @@
228
228
  />
229
229
  </dd>
230
230
  </template>
231
+ <template v-if="wfsFormats.length">
232
+ <dt class="font-bold fr-text--sm fr-mb-0">
233
+ <div class="flex gap-1 items-center">
234
+ {{ t('Formats exportés depuis le service WFS') }}
235
+ <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
236
+ <Tooltip>
237
+ <RiInformationLine
238
+ class="flex-none size-4"
239
+ :aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
240
+ aria-hidden="true"
241
+ />
242
+ <template #tooltip>
243
+ <p class="text-sm font-normal mb-0">
244
+ {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
245
+ </p>
246
+ <p class="text-sm font-normal mb-0">
247
+ {{ t(`Le nombre de features téléchargées peut être limité.`) }}
248
+ </p>
249
+ </template>
250
+ </Tooltip>
251
+ </div>
252
+ </dt>
253
+ <dd
254
+ v-for="wfsFormat in wfsFormats"
255
+ :key="wfsFormat.format"
256
+ class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
257
+ >
258
+ <span>
259
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
260
+ <a
261
+ :href="wfsFormat.url"
262
+ class="fr-link"
263
+ rel="ugc nofollow noopener"
264
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
265
+ >
266
+ <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
267
+ </a>
268
+ </span>
269
+ <CopyButton
270
+ :label="t('Copier le lien')"
271
+ :copied-label="t('Lien copié !')"
272
+ :text="wfsFormat.url"
273
+ class="relative"
274
+ />
275
+ </dd>
276
+ </template>
231
277
  </dl>
232
278
  </div>
233
279
  <div v-if="tab.key === 'swagger'">
@@ -251,7 +297,7 @@
251
297
 
252
298
  <script setup lang="ts">
253
299
  import { computed, defineAsyncComponent } from 'vue'
254
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
300
+ import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
255
301
  import { toast } from 'vue-sonner'
256
302
  import BrandedButton from '../BrandedButton.vue'
257
303
  import CopyButton from '../CopyButton.vue'
@@ -263,6 +309,7 @@ import TabList from '../Tabs/TabList.vue'
263
309
  import Tab from '../Tabs/Tab.vue'
264
310
  import TabPanels from '../Tabs/TabPanels.vue'
265
311
  import TabPanel from '../Tabs/TabPanel.vue'
312
+ import Tooltip from '../Tooltip.vue'
266
313
  import Preview from '../ResourceAccordion/Preview.vue'
267
314
  import DataStructure from '../ResourceAccordion/DataStructure.vue'
268
315
  import Metadata from '../ResourceAccordion/Metadata.vue'
@@ -316,6 +363,8 @@ const {
316
363
  ogcService,
317
364
  ogcWms,
318
365
  generatedFormats,
366
+ wfsFormats,
367
+ defaultWfsProjection,
319
368
  isResourceUrl,
320
369
  tabsOptions,
321
370
  } = useResourceCapabilities(() => props.resource, () => props.dataset)
@@ -38,7 +38,7 @@
38
38
 
39
39
  <div
40
40
  v-if="reuse.organization || reuse.owner"
41
- class="text-sm m-0 flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
41
+ class="text-sm flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
42
42
  >
43
43
  <ObjectCardOwner
44
44
  :organization="reuse.organization"
@@ -13,15 +13,19 @@
13
13
  </template>
14
14
 
15
15
  <script setup lang="ts">
16
+ import { computed } from 'vue'
16
17
  import type { FacetItem } from '../../../types/search'
17
18
  import { useTranslation } from '../../../composables/useTranslation'
18
19
  import FilterButtonGroup from './FilterButtonGroup.vue'
19
20
 
20
- defineProps<{
21
+ const props = withDefaults(defineProps<{
21
22
  modelValue: string | undefined
22
23
  facets?: FacetItem[]
23
24
  loading?: boolean
24
- }>()
25
+ exclude?: string[]
26
+ }>(), {
27
+ exclude: () => [],
28
+ })
25
29
 
26
30
  const emit = defineEmits<{
27
31
  'update:modelValue': [value: string | undefined]
@@ -29,11 +33,17 @@ const emit = defineEmits<{
29
33
 
30
34
  const { t } = useTranslation()
31
35
 
32
- const options = [
36
+ const allOptions = [
33
37
  { value: 'public-service', label: t('Service public') },
34
38
  { value: 'local-authority', label: t('Collectivité territoriale') },
35
39
  { value: 'company', label: t('Entreprise') },
36
40
  { value: 'association', label: t('Association') },
37
41
  { value: 'user', label: t('Utilisateur') },
38
42
  ]
43
+
44
+ const options = computed(() =>
45
+ props.exclude.length > 0
46
+ ? allOptions.filter(o => !props.exclude.includes(o.value))
47
+ : allOptions,
48
+ )
39
49
  </script>
@@ -11,7 +11,7 @@
11
11
  >
12
12
  <SearchInput
13
13
  v-model="q"
14
- :placeholder="placeholder || t('Ex : élection présidentielle 2022')"
14
+ :placeholder="placeholder || typesMeta[currentType].placeholder"
15
15
  />
16
16
  </div>
17
17
  <div class="grid grid-cols-12 mt-2 md:mt-5">
@@ -123,6 +123,7 @@
123
123
  v-model="producerType"
124
124
  :facets="getFacets('producer_type')"
125
125
  :loading="searchResultsStatus === 'pending'"
126
+ :exclude="currentType === 'organizations' ? ['user'] : []"
126
127
  :style="{ order: getOrder('producer_type') }"
127
128
  />
128
129
  <DatasetBadgeFilter
@@ -177,31 +178,44 @@
177
178
  >
178
179
  {{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
179
180
  </p>
180
- <div class="fr-col-auto fr-grid-row fr-grid-row--middle">
181
- <label
182
- for="sort-search"
183
- class="fr-col-auto text-sm m-0 mr-2"
184
- >
185
- {{ t('Trier par :') }}
186
- </label>
187
- <div class="fr-col">
188
- <select
189
- id="sort-search"
190
- v-model="sort"
191
- class="fr-select text-sm shadow-input-blue!"
181
+ <div class="fr-col-auto fr-grid-row fr-grid-row--middle gap-4">
182
+ <div class="flex items-center">
183
+ <label
184
+ for="sort-search"
185
+ class="fr-col-auto text-sm m-0 mr-2"
192
186
  >
193
- <option :value="undefined">
194
- {{ t('Pertinence') }}
195
- </option>
196
- <option
197
- v-for="option in activeSortOptions"
198
- :key="option.value"
199
- :value="option.value"
187
+ {{ t('Trier par :') }}
188
+ </label>
189
+ <div class="fr-col">
190
+ <select
191
+ id="sort-search"
192
+ v-model="sort"
193
+ class="fr-select text-sm shadow-input-blue!"
200
194
  >
201
- {{ option.label }}
202
- </option>
203
- </select>
195
+ <option :value="undefined">
196
+ {{ t('Pertinence') }}
197
+ </option>
198
+ <option
199
+ v-for="option in allSortOptions"
200
+ :key="option.value"
201
+ :value="option.value"
202
+ :hidden="!activeSortValues.has(option.value)"
203
+ >
204
+ {{ option.label }}
205
+ </option>
206
+ </select>
207
+ </div>
204
208
  </div>
209
+ <BrandedButton
210
+ v-if="rssUrl"
211
+ :href="rssUrl"
212
+ :title="t('Flux RSS')"
213
+ color="secondary"
214
+ size="sm"
215
+ :icon="RiRssLine"
216
+ icon-only
217
+ target="_blank"
218
+ />
205
219
  </div>
206
220
  </div>
207
221
  <transition mode="out-in">
@@ -241,6 +255,14 @@
241
255
  <ReuseHorizontalCard :reuse="(result as Reuse)" />
242
256
  </slot>
243
257
  </template>
258
+ <template v-else-if="currentType === 'organizations'">
259
+ <slot
260
+ name="organization"
261
+ :organization="result"
262
+ >
263
+ <OrganizationHorizontalCard :organization="(result as Organization)" />
264
+ </slot>
265
+ </template>
244
266
  </li>
245
267
  </ul>
246
268
  <Pagination
@@ -294,6 +316,7 @@
294
316
  color="tertiary"
295
317
  :href="componentsConfig.forumUrl"
296
318
  :icon="RiLightbulbLine"
319
+ keep-margins-even-without-borders
297
320
  >
298
321
  {{ t("Voir le forum") }}
299
322
  </BrandedButton>
@@ -312,7 +335,7 @@
312
335
  <script setup lang="ts">
313
336
  import { computed, watch, useTemplateRef, type Ref } from 'vue'
314
337
  import { useRouteQuery } from '@vueuse/router'
315
- import { RiCloseCircleLine, RiDatabase2Line, RiRobot2Line, RiLineChartLine, RiLightbulbLine } from '@remixicon/vue'
338
+ import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
316
339
  import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
317
340
  import { useTranslation } from '../../composables/useTranslation'
318
341
  import { useDebouncedRef } from '../../composables/useDebouncedRef'
@@ -322,8 +345,9 @@ import { useFetch } from '../../functions/api'
322
345
  import { getLink } from '../../functions/pagination'
323
346
  import type { Dataset } from '../../types/datasets'
324
347
  import type { Dataservice } from '../../types/dataservices'
348
+ import type { Organization } from '../../types/organizations'
325
349
  import type { Reuse } from '../../types/reuses'
326
- import type { GlobalSearchConfig, SearchType, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, FacetItem } from '../../types/search'
350
+ import type { GlobalSearchConfig, SearchType, SortOption, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, OrganizationSearchResponse, FacetItem } from '../../types/search'
327
351
  import { getDefaultGlobalSearchConfig } from '../../types/search'
328
352
  import BrandedButton from '../BrandedButton.vue'
329
353
  import LoadingBlock from '../LoadingBlock.vue'
@@ -332,6 +356,7 @@ import RadioGroup from '../RadioGroup.vue'
332
356
  import RadioInput from '../RadioInput.vue'
333
357
  import DatasetCard from '../DatasetCard.vue'
334
358
  import DataserviceCard from '../DataserviceCard.vue'
359
+ import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
335
360
  import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
336
361
  import SearchInput from './SearchInput.vue'
337
362
  import Sidemenu from './Sidemenu.vue'
@@ -385,6 +410,22 @@ const activeSortOptions = computed(() =>
385
410
  currentTypeConfig.value?.sortOptions ?? [],
386
411
  )
387
412
 
413
+ const activeSortValues = computed(() =>
414
+ new Set(activeSortOptions.value.map(o => o.value as string)),
415
+ )
416
+
417
+ // Deduplicated union of all sort options across all search types.
418
+ // Rendered as hidden <option> elements so the <select> always has a stable
419
+ // intrinsic width regardless of which type is currently active.
420
+ const allSortOptions = computed(() => {
421
+ const seen = new Set<string>()
422
+ return props.config.flatMap(c => (c.sortOptions ?? []) as SortOption<string>[]).filter((o) => {
423
+ if (seen.has(o.value)) return false
424
+ seen.add(o.value)
425
+ return true
426
+ })
427
+ })
428
+
388
429
  const activeFilters = computed(() => [
389
430
  ...(currentTypeConfig.value?.basicFilters ?? []),
390
431
  ...(currentTypeConfig.value?.advancedFilters ?? []),
@@ -458,6 +499,7 @@ watch(currentType, () => {
458
499
  const datasetsEnabled = computed(() => props.config.some(c => c.class === 'datasets'))
459
500
  const dataservicesEnabled = computed(() => props.config.some(c => c.class === 'dataservices'))
460
501
  const reusesEnabled = computed(() => props.config.some(c => c.class === 'reuses'))
502
+ const organizationsEnabled = computed(() => props.config.some(c => c.class === 'organizations'))
461
503
 
462
504
  // Create stable params for each type
463
505
  const stableParamsOptions = {
@@ -480,11 +522,16 @@ const reusesParams = useStableQueryParams({
480
522
  ...stableParamsOptions,
481
523
  typeConfig: props.config.find(c => c.class === 'reuses'),
482
524
  })
525
+ const organizationsParams = useStableQueryParams({
526
+ ...stableParamsOptions,
527
+ typeConfig: props.config.find(c => c.class === 'organizations'),
528
+ })
483
529
 
484
530
  // URLs that return null when type is not enabled
485
531
  const datasetsUrl = computed(() => datasetsEnabled.value ? '/api/2/datasets/search/' : null)
486
532
  const dataservicesUrl = computed(() => dataservicesEnabled.value ? '/api/2/dataservices/search/' : null)
487
533
  const reusesUrl = computed(() => reusesEnabled.value ? '/api/2/reuses/search/' : null)
534
+ const organizationsUrl = computed(() => organizationsEnabled.value ? '/api/2/organizations/search/' : null)
488
535
 
489
536
  // Reset page on filter/sort change
490
537
  const filtersForReset = computed(() => ({
@@ -528,7 +575,7 @@ const hasFilters = computed(() => {
528
575
  || reuseType.value
529
576
  })
530
577
 
531
- const showForumLink = computed(() => currentType.value === 'datasets' && !!componentsConfig.forumUrl)
578
+ const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
532
579
 
533
580
  function resetFilters() {
534
581
  organizationId.value = undefined
@@ -564,31 +611,80 @@ const { data: reusesResults, status: reusesStatus } = await useFetch<ReuseSearch
564
611
  reusesUrl,
565
612
  { params: reusesParams, lazy: true, server: initialType === 'reuses' },
566
613
  )
614
+ const { data: organizationsResults, status: organizationsStatus } = await useFetch<OrganizationSearchResponse<Organization>>(
615
+ organizationsUrl,
616
+ { params: organizationsParams, lazy: true, server: initialType === 'organizations' },
617
+ )
567
618
 
568
619
  const typesMeta = {
569
620
  datasets: {
570
621
  icon: RiDatabase2Line,
571
622
  name: t('Jeux de données'),
623
+ placeholder: t('ex. élections présidentielles'),
572
624
  results: datasetsResults,
573
625
  status: datasetsStatus,
574
626
  },
575
627
  dataservices: {
576
- icon: RiRobot2Line,
577
- name: t('APIs'),
628
+ icon: RiTerminalLine,
629
+ name: t('API'),
630
+ placeholder: t('ex: SIRENE'),
578
631
  results: dataservicesResults,
579
632
  status: dataservicesStatus,
580
633
  },
581
634
  reuses: {
582
635
  icon: RiLineChartLine,
583
636
  name: t('Réutilisations'),
637
+ placeholder: t('Rechercher une réutilisation de données'),
584
638
  results: reusesResults,
585
639
  status: reusesStatus,
586
640
  },
641
+ organizations: {
642
+ icon: RiBuilding2Line,
643
+ name: t('Organisations'),
644
+ placeholder: t('Rechercher une organisation'),
645
+ results: organizationsResults,
646
+ status: organizationsStatus,
647
+ },
587
648
  } as const
588
649
 
589
650
  const searchResults = computed(() => typesMeta[currentType.value].results.value)
590
651
  const searchResultsStatus = computed(() => typesMeta[currentType.value].status.value)
591
652
 
653
+ // RSS feed URL for datasets
654
+ const rssUrl = computed(() => {
655
+ if (currentType.value !== 'datasets') return null
656
+
657
+ const params = new URLSearchParams()
658
+ const datasetsConfig = props.config.find(c => c.class === 'datasets')
659
+
660
+ // Add hidden filters first
661
+ if (datasetsConfig?.hiddenFilters) {
662
+ for (const hf of datasetsConfig.hiddenFilters) {
663
+ if (hf?.value) params.set(hf.key as string, String(hf.value))
664
+ }
665
+ }
666
+
667
+ // Add active filters
668
+ if (qDebounced.value) params.set('q', qDebounced.value)
669
+ if (organizationId.value) params.set('organization', organizationId.value)
670
+ if (organizationType.value) params.set('organization_badge', organizationType.value)
671
+ if (tag.value) params.set('tag', tag.value)
672
+ if (format.value) params.set('format', format.value)
673
+ if (license.value) params.set('license', license.value)
674
+ if (schema.value) params.set('schema', schema.value)
675
+ if (geozone.value) params.set('geozone', geozone.value)
676
+ if (granularity.value) params.set('granularity', granularity.value)
677
+ if (badge.value) params.set('badge', badge.value)
678
+ if (topic.value) params.set('topic', topic.value)
679
+
680
+ // Add sort if set
681
+ if (sort.value) params.set('sort', sort.value)
682
+
683
+ const queryString = params.toString()
684
+ const basePath = '/api/1/datasets/recent.atom'
685
+ return `${componentsConfig.apiBase}${basePath}${queryString ? '?' + queryString : ''}`
686
+ })
687
+
592
688
  // Facets for filters
593
689
  const currentFacets = computed(() => searchResults.value?.facets)
594
690
 
@@ -13,8 +13,10 @@
13
13
  />
14
14
  <PopoverButton
15
15
  v-bind="buttonProps"
16
- class="border-transparent -outline-offset-2 inline-flex items-center justify-center hover:bg-gray-some"
17
- :class="{ 'w-8 h-8 rounded-full bg-transparent': styledButton }"
16
+ :class="[
17
+ buttonClass ?? 'border-transparent -outline-offset-2 inline-flex items-center justify-center hover:bg-gray-some',
18
+ { 'w-8 h-8 rounded-full bg-transparent': styledButton && !buttonClass },
19
+ ]"
18
20
  >
19
21
  <slot>
20
22
  <RiInformationLine
@@ -57,6 +59,7 @@ import ValueWatcher from './ValueWatcher.vue'
57
59
 
58
60
  withDefaults(defineProps<{
59
61
  buttonProps?: object
62
+ buttonClass?: string
60
63
  noMargin?: boolean
61
64
  styledButton?: boolean
62
65
  }>(), {
@@ -30,7 +30,7 @@
30
30
 
31
31
  <div
32
32
  v-if="topic.organization || topic.owner"
33
- class="text-sm m-0 flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
33
+ class="text-sm flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
34
34
  >
35
35
  <ObjectCardOwner
36
36
  :organization="topic.organization"
@@ -0,0 +1,15 @@
1
+ import { useComponentsConfig } from '../config'
2
+ import type { Resource } from '../types/resources'
3
+
4
+ export const useHasTabularData = () => {
5
+ const config = useComponentsConfig()
6
+
7
+ return (resource: Resource) => {
8
+ return (
9
+ config.tabularApiUrl
10
+ && resource.extras['analysis:parsing:parsing_table']
11
+ && !resource.extras['analysis:parsing:error']
12
+ && (config.tabularAllowRemote || resource.filetype === 'file')
13
+ )
14
+ }
15
+ }
@@ -1,10 +1,12 @@
1
1
  import { computed, toValue, type MaybeRefOrGetter } from 'vue'
2
2
  import { useComponentsConfig } from '../config'
3
3
  import { useTranslation } from './useTranslation'
4
+ import { useHasTabularData } from './useHasTabularData'
4
5
  import { detectOgcService } from '../functions/resources'
5
6
  import { isOrganizationCertified } from '../functions/organizations'
6
- import type { Resource } from '../types/resources'
7
+ import type { Resource, WfsMetadata } from '../types/resources'
7
8
  import type { Dataset, DatasetV2 } from '../types/datasets'
9
+ import { getWfsExportFormats } from '../functions/resourceCapabilities'
8
10
 
9
11
  const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
10
12
  const URL_FORMATS = ['url', 'doi', 'www:link', 'www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
@@ -15,6 +17,7 @@ export function useResourceCapabilities(
15
17
  ) {
16
18
  const config = useComponentsConfig()
17
19
  const { t } = useTranslation()
20
+ const checkTabularData = useHasTabularData()
18
21
 
19
22
  const hasPreview = computed(() => {
20
23
  const format = toValue(resource).format?.toLowerCase()
@@ -23,10 +26,7 @@ export function useResourceCapabilities(
23
26
 
24
27
  const hasTabularData = computed(() => {
25
28
  const r = toValue(resource)
26
- return config.tabularApiUrl
27
- && r.extras['analysis:parsing:parsing_table']
28
- && !r.extras['analysis:parsing:error']
29
- && (config.tabularAllowRemote || r.filetype === 'file')
29
+ return checkTabularData(r)
30
30
  })
31
31
 
32
32
  const hasPmtiles = computed(() => {
@@ -68,6 +68,17 @@ export function useResourceCapabilities(
68
68
  return formats
69
69
  })
70
70
 
71
+ const wfsFormats = computed(() => {
72
+ return getWfsExportFormats(toValue(resource))
73
+ })
74
+
75
+ const defaultWfsProjection = computed<string | null>(() => {
76
+ const r = toValue(resource)
77
+ const wfsMetadata = r.extras['analysis:parsing:ogc_metadata'] as WfsMetadata | null
78
+ if (!wfsMetadata || wfsMetadata.format !== `wfs`) return null
79
+ return wfsMetadata?.detected_layer?.default_crs ?? null
80
+ })
81
+
71
82
  const isResourceUrl = computed(() => URL_FORMATS.includes(toValue(resource).format))
72
83
 
73
84
  const tabsOptions = computed(() => {
@@ -112,6 +123,8 @@ export function useResourceCapabilities(
112
123
  ogcService,
113
124
  ogcWms,
114
125
  generatedFormats,
126
+ wfsFormats,
127
+ defaultWfsProjection,
115
128
  isResourceUrl,
116
129
  tabsOptions,
117
130
  }
@@ -21,7 +21,8 @@ function detectLanguage(): string {
21
21
  const acceptLanguage = header
22
22
  if (acceptLanguage) {
23
23
  const primaryLang = acceptLanguage.split(';')[0]!.split(',')[0]!.split('-')[0]!.toLowerCase()
24
- return primaryLang
24
+ // Ignore wildcard * language, that should fallback to client side detection or default language
25
+ if (primaryLang !== '*') return primaryLang
25
26
  }
26
27
  }
27
28
  catch {
@@ -1,9 +1,17 @@
1
- import { ref, toValue, watchEffect, onMounted, type ComputedRef, type Ref } from 'vue'
1
+ import { ref, toValue, watchEffect, onMounted, type ComputedRef, type MaybeRefOrGetter, type Ref } from 'vue'
2
2
  import { ofetch } from 'ofetch'
3
3
  import { useComponentsConfig } from '../config'
4
4
  import { useTranslation } from '../composables/useTranslation'
5
5
  import type { AsyncData, AsyncDataRequestStatus, UseFetchOptions } from './api.types'
6
6
 
7
+ function deepToValue(obj: MaybeRefOrGetter<Record<string, unknown> | undefined>): Record<string, unknown> | undefined {
8
+ const val = toValue(obj)
9
+ if (!val || typeof val !== 'object') return val
10
+ return Object.fromEntries(
11
+ Object.entries(val).map(([k, v]) => [k, toValue(v as MaybeRefOrGetter<unknown>)]),
12
+ )
13
+ }
14
+
7
15
  export async function useFetch<DataT, ErrorT = never>(
8
16
  url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
9
17
  options?: UseFetchOptions<DataT>,
@@ -23,8 +31,8 @@ export async function useFetch<DataT, ErrorT = never>(
23
31
  const execute = async () => {
24
32
  const urlValue = toValue(url)
25
33
  if (!urlValue) return
26
- const params = toValue(options?.params)
27
- const query = toValue(options?.query)
34
+ const params = deepToValue(options?.params)
35
+ const query = deepToValue(options?.query)
28
36
  status.value = 'pending'
29
37
  try {
30
38
  data.value = await ofetch<DataT | null>(urlValue, {
@@ -35,5 +35,5 @@ export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
35
35
 
36
36
  export type UseFetchFunction = (<DataT, ErrorT>(
37
37
  url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
38
- options?: UseFetchOptions<DataT>
38
+ options?: UseFetchOptions<DataT>,
39
39
  ) => Promise<AsyncData<DataT, ErrorT>>)