@datagouv/components-next 1.1.1 → 1.1.2

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 (38) hide show
  1. package/dist/{Control-DuZJdKV_.js → Control-ZFh5ta_U.js} +1 -1
  2. package/dist/{Datafair.client-BzW-ctDf.js → Datafair.client-8bXp6UeQ.js} +1 -1
  3. package/dist/{Event--kp8kMdJ.js → Event-DSQcW7OF.js} +24 -24
  4. package/dist/{Image-34hvypZI.js → Image-BijNEG0p.js} +6 -6
  5. package/dist/{JsonPreview.client-BfMSzR07.js → JsonPreview.client-BxuoPK_w.js} +2 -2
  6. package/dist/{Map-BjUnLyj8.js → Map-BUtPf5GN.js} +756 -756
  7. package/dist/{MapContainer.client-CLs-im9i.js → MapContainer.client-CTz0wmJG.js} +37 -38
  8. package/dist/{OSM-s40W6sQ2.js → OSM-D4MTdBtk.js} +2 -2
  9. package/dist/{PdfPreview.client-C13PQCU_.js → PdfPreview.client-DpVreUSl.js} +2 -2
  10. package/dist/{Pmtiles.client-CL7PXXDl.js → Pmtiles.client-BwmHo3T8.js} +1 -1
  11. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-C6XnsZ-7.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-stmU5qEB.js} +1 -1
  12. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-hJQIqcZm.js} +2 -2
  13. package/dist/{Tile-DbNFNPfU.js → Tile-Dcl7oIVu.js} +35 -35
  14. package/dist/{TileImage-BsXBxMtq.js → TileImage-BJeHipMX.js} +4 -4
  15. package/dist/{View-BR92hTWP.js → View-xp_P_OHw.js} +412 -401
  16. package/dist/{XmlPreview.client-KaENrbbG.js → XmlPreview.client-DAOs89cR.js} +3 -3
  17. package/dist/{common-PJfpC179.js → common-BjQlan3k.js} +36 -36
  18. package/dist/components-next.css +4 -4
  19. package/dist/components-next.js +1 -1
  20. package/dist/{index-C7WVVGgD.js → index-JqjPja1u.js} +32886 -27183
  21. package/dist/{main-K-42Oe8-.js → main-D4WQMky0.js} +14994 -14977
  22. package/dist/{proj-DsetBcW7.js → proj-CsNo9yH1.js} +532 -512
  23. package/dist/{tilecoord-Db24Px13.js → tilecoord-A0fLnBZr.js} +28 -28
  24. package/dist/{vue3-xml-viewer.common-sHPSE-jD.js → vue3-xml-viewer.common-BGsoNyZe.js} +1 -1
  25. package/package.json +2 -2
  26. package/src/components/DataserviceCard.vue +3 -0
  27. package/src/components/DatasetCard.vue +3 -0
  28. package/src/components/ObjectCardHeader.vue +11 -4
  29. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
  30. package/src/components/ReuseCard.vue +12 -4
  31. package/src/components/Search/GlobalSearch.vue +19 -3
  32. package/src/components/Search/SearchInput.vue +2 -1
  33. package/src/composables/useMetrics.ts +1 -1
  34. package/src/config.ts +17 -3
  35. package/src/functions/api.ts +5 -34
  36. package/src/functions/metrics.ts +6 -4
  37. package/src/main.ts +27 -0
  38. package/src/types/ui.ts +2 -0
@@ -1,6 +1,6 @@
1
- import { T as _, W as nt, n as xt, c as H, b as et } from "./Image-34hvypZI.js";
2
- import { z as Tt, g as pt } from "./common-PJfpC179.js";
3
- import { aC as It, E as ft, al as Xt, U as dt, g as at, X as Wt, w as Mt, F as b, n as _t, L, an as gt, aS as vt, x as Yt, r as Lt, aT as Ft, ak as wt, aI as yt, az as Dt, aJ as Ot, aU as st, ae as ot, i as Ct, W as z, a1 as $, d as V, l as Pt, u as rt, a7 as ht, I as At, a as Rt } from "./proj-DsetBcW7.js";
1
+ import { T as _, W as nt, n as xt, c as H, b as et } from "./Image-BijNEG0p.js";
2
+ import { z as Tt, g as pt } from "./common-BjQlan3k.js";
3
+ import { aD as It, E as ft, al as Xt, U as dt, g as at, X as Wt, w as Mt, F as b, n as _t, L, an as gt, aU as vt, x as Yt, r as Lt, aV as Ft, ak as wt, aJ as yt, aA as Dt, aK as Ot, aW as st, ae as ot, i as Ct, W as z, a1 as $, d as V, l as Pt, u as rt, a7 as ht, I as At, a as Rt } from "./proj-CsNo9yH1.js";
4
4
  class mt extends It {
5
5
  /**
6
6
  * @param {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
@@ -339,7 +339,7 @@ function ut(a, t, e, s) {
339
339
  }
340
340
  return i;
341
341
  }
342
- function Nt(a, t, e, s) {
342
+ function Kt(a, t, e, s) {
343
343
  const n = Yt(e);
344
344
  let i = ut(
345
345
  a,
@@ -356,7 +356,7 @@ function Nt(a, t, e, s) {
356
356
  ), isFinite(i) && i > 0;
357
357
  }), i;
358
358
  }
359
- function St(a, t, e, s, n, i, c, m, g, T, X, x, F, w) {
359
+ function Nt(a, t, e, s, n, i, c, m, g, T, X, x, F, w) {
360
360
  const r = H(
361
361
  Math.round(e * a),
362
362
  Math.round(e * t),
@@ -409,7 +409,7 @@ function St(a, t, e, s, n, i, c, m, g, T, X, x, F, w) {
409
409
  return m.getTriangles().forEach(function(o, y, C) {
410
410
  const p = o.source, I = o.target;
411
411
  let v = p[0][0], Y = p[0][1], W = p[1][0], M = p[1][1], D = p[2][0], A = p[2][1];
412
- const G = h((I[0][0] - u[0]) / i), N = h(
412
+ const G = h((I[0][0] - u[0]) / i), K = h(
413
413
  -(I[0][1] - u[1]) / i
414
414
  ), R = h((I[1][0] - u[0]) / i), k = h(
415
415
  -(I[1][1] - u[1]) / i
@@ -420,32 +420,32 @@ function St(a, t, e, s, n, i, c, m, g, T, X, x, F, w) {
420
420
  const Et = [
421
421
  [W, M, 0, 0, R - G],
422
422
  [D, A, 0, 0, U - G],
423
- [0, 0, W, M, k - N],
424
- [0, 0, D, A, Q - N]
425
- ], S = vt(Et);
426
- if (!S)
423
+ [0, 0, W, M, k - K],
424
+ [0, 0, D, A, Q - K]
425
+ ], N = vt(Et);
426
+ if (!N)
427
427
  return;
428
428
  if (r.save(), r.beginPath(), Gt() || !x) {
429
429
  r.moveTo(R, k);
430
- const O = 4, K = G - R, it = N - k;
430
+ const O = 4, S = G - R, it = K - k;
431
431
  for (let P = 0; P < O; P++)
432
432
  r.lineTo(
433
- R + h((P + 1) * K / O),
433
+ R + h((P + 1) * S / O),
434
434
  k + h(P * it / (O - 1))
435
435
  ), P != O - 1 && r.lineTo(
436
- R + h((P + 1) * K / O),
436
+ R + h((P + 1) * S / O),
437
437
  k + h((P + 1) * it / (O - 1))
438
438
  );
439
439
  r.lineTo(U, Q);
440
440
  } else
441
- r.moveTo(R, k), r.lineTo(G, N), r.lineTo(U, Q);
441
+ r.moveTo(R, k), r.lineTo(G, K), r.lineTo(U, Q);
442
442
  r.clip(), r.transform(
443
- S[0],
444
- S[2],
445
- S[1],
446
- S[3],
443
+ N[0],
444
+ N[2],
445
+ N[1],
446
+ N[3],
447
447
  G,
448
- N
448
+ K
449
449
  ), r.translate(
450
450
  E[0] - j,
451
451
  E[3] - q
@@ -454,10 +454,10 @@ function St(a, t, e, s, n, i, c, m, g, T, X, x, F, w) {
454
454
  if (l)
455
455
  Z = l.canvas, r.scale(d, -d);
456
456
  else {
457
- const O = g[0], K = O.extent;
457
+ const O = g[0], S = O.extent;
458
458
  Z = O.image, r.scale(
459
- L(K) / Z.width,
460
- -b(K) / Z.height
459
+ L(S) / Z.width,
460
+ -b(S) / Z.height
461
461
  );
462
462
  }
463
463
  r.drawImage(Z, 0, 0), r.restore();
@@ -467,7 +467,7 @@ function St(a, t, e, s, n, i, c, m, g, T, X, x, F, w) {
467
467
  }), r.restore()), r.canvas;
468
468
  }
469
469
  const Zt = 10, lt = 0.25;
470
- class Kt {
470
+ class St {
471
471
  /**
472
472
  * @param {import("../proj/Projection.js").default} sourceProj Source projection.
473
473
  * @param {import("../proj/Projection.js").default} targetProj Target projection.
@@ -715,7 +715,7 @@ class Vt extends mt {
715
715
  l && (h ? h = V(h, l) : h = l);
716
716
  const f = n.getResolution(
717
717
  this.wrappedTileCoord_[0]
718
- ), d = Nt(
718
+ ), d = Kt(
719
719
  t,
720
720
  s,
721
721
  E,
@@ -726,7 +726,7 @@ class Vt extends mt {
726
726
  return;
727
727
  }
728
728
  const u = X !== void 0 ? X : bt;
729
- if (this.triangulation_ = new Kt(
729
+ if (this.triangulation_ = new St(
730
730
  t,
731
731
  s,
732
732
  E,
@@ -806,7 +806,7 @@ class Vt extends mt {
806
806
  ), g = this.targetTileGrid_.getTileCoordExtent(
807
807
  this.wrappedTileCoord_
808
808
  );
809
- this.canvas_ = St(
809
+ this.canvas_ = Nt(
810
810
  n,
811
811
  i,
812
812
  this.pixelRatio_,
@@ -890,13 +890,13 @@ export {
890
890
  zt as I,
891
891
  Vt as R,
892
892
  mt as T,
893
- Kt as a,
893
+ St as a,
894
894
  Bt as b,
895
895
  ut as c,
896
896
  Jt as d,
897
897
  $t as e,
898
898
  ts as g,
899
899
  ss as h,
900
- St as r,
900
+ Nt as r,
901
901
  es as w
902
902
  };
@@ -1,4 +1,4 @@
1
- import { c as Ke } from "./main-K-42Oe8-.js";
1
+ import { c as Ke } from "./main-D4WQMky0.js";
2
2
  import We from "vue";
3
3
  function Fe(I, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,7 +27,7 @@
27
27
  "chart.js": "^4.4.8",
28
28
  "dompurify": "^3.2.5",
29
29
  "echarts": "^6.0.0",
30
- "geopf-extensions-openlayers": "^1.0.0-beta.5",
30
+ "geopf-extensions-openlayers": "^1.0.0-beta.10",
31
31
  "leaflet": "^1.9.4",
32
32
  "maplibre-gl": "^5.6.2",
33
33
  "ofetch": "^1.4.1",
@@ -53,6 +53,7 @@
53
53
  <ObjectCardHeader
54
54
  :icon="isTabularApi ? RiSparklingLine : RiTerminalLine"
55
55
  :url="dataserviceUrl || dataservice.self_web_url"
56
+ :title-tag="titleTag"
56
57
  >
57
58
  {{ dataservice.title }}
58
59
  </ObjectCardHeader>
@@ -115,6 +116,7 @@ import { useComponentsConfig } from '../config'
115
116
  import { useFormatDate } from '../functions/dates'
116
117
  import { summarize } from '../functions/helpers'
117
118
  import type { Dataservice } from '../types/dataservices'
119
+ import type { TitleTag } from '../types/ui'
118
120
  import { useTranslation } from '../composables/useTranslation'
119
121
  import OrganizationLogo from './OrganizationLogo.vue'
120
122
  import Avatar from './Avatar.vue'
@@ -130,6 +132,7 @@ type Props = {
130
132
  dataserviceUrl?: RouteLocationRaw
131
133
  organizationUrl?: RouteLocationRaw
132
134
  showDescription?: boolean
135
+ titleTag?: TitleTag
133
136
  }
134
137
 
135
138
  const props = withDefaults(defineProps<Props>(), {
@@ -36,6 +36,7 @@
36
36
  :icon="RiDatabase2Line"
37
37
  :url="datasetUrl || dataset.page"
38
38
  :target="datasetUrlInNewTab ? '_blank' : undefined"
39
+ :title-tag="titleTag"
39
40
  >
40
41
  {{ dataset.title }}
41
42
  <template
@@ -117,6 +118,7 @@
117
118
  import type { RouteLocationRaw } from 'vue-router'
118
119
  import { RiArchiveLine, RiDatabase2Line, RiDownloadLine, RiEyeLine, RiLineChartLine, RiLockLine, RiStarLine, RiSubtractLine } from '@remixicon/vue'
119
120
  import type { Dataset, DatasetV2 } from '../types/datasets'
121
+ import type { TitleTag } from '../types/ui'
120
122
  import { summarize } from '../functions/helpers'
121
123
  import { useFormatDate } from '../functions/dates'
122
124
  import { getDescriptionShort } from '../functions/description'
@@ -137,6 +139,7 @@ type Props = {
137
139
  datasetUrlInNewTab?: boolean
138
140
  organizationUrl?: RouteLocationRaw
139
141
  showDescriptionShort?: boolean
142
+ titleTag?: TitleTag
140
143
  }
141
144
 
142
145
  const props = withDefaults(defineProps<Props>(), {
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <h3 class="w-full text-base flex">
2
+ <component
3
+ :is="titleTag"
4
+ class="w-full text-base flex"
5
+ >
3
6
  <AppLink
4
7
  :to="url"
5
8
  class="text-gray-title text-base bg-none flex items-center w-full truncate gap-1"
@@ -19,17 +22,21 @@
19
22
  <slot name="extra" />
20
23
  <span class="absolute inset-0" />
21
24
  </AppLink>
22
- </h3>
25
+ </component>
23
26
  </template>
24
27
 
25
28
  <script setup lang="ts">
26
29
  import type { Component } from 'vue'
27
30
  import type { RouteLocationRaw } from 'vue-router'
31
+ import type { TitleTag } from '../types/ui'
28
32
  import AppLink from './AppLink.vue'
29
33
 
30
- defineProps<{
34
+ withDefaults(defineProps<{
31
35
  icon: Component
32
36
  url: RouteLocationRaw
33
37
  target?: string
34
- }>()
38
+ titleTag?: TitleTag
39
+ }>(), {
40
+ titleTag: 'h3',
41
+ })
35
42
  </script>
@@ -89,10 +89,8 @@ async function displayMap() {
89
89
 
90
90
  const attributions = new GeoportalAttribution({
91
91
  position: 'bottom-right',
92
- // collapsed option is ignored by the library, thus the override below
93
- // see https://github.com/IGNF/geopf-extensions-openlayers/issues/497
92
+ collapsed: false,
94
93
  })
95
- attributions.setCollapsed(false)
96
94
  map.addControl(attributions)
97
95
 
98
96
  const layerImport = new LayerImport({
@@ -2,14 +2,17 @@
2
2
  <article class="fr-enlarge-link group/reuse-card bg-white border border-gray-default hover:bg-gray-some flex flex-col relative">
3
3
  <div class="flex flex-col h-full flex-1 order-2 px-8">
4
4
  <div class="order-1 flex flex-col px-4 py-1 h-full -mx-8">
5
- <h3 class="font-bold text-base mt-1 mb-0 truncate">
5
+ <component
6
+ :is="titleTag"
7
+ class="font-bold text-base mt-1 mb-0 truncate"
8
+ >
6
9
  <AppLink
7
10
  class="text-gray-title overflow-hidden"
8
11
  :to="reuseUrl"
9
12
  >
10
13
  {{ reuse.title }}
11
14
  </AppLink>
12
- </h3>
15
+ </component>
13
16
  <div class="order-3 text-sm m-0 text-gray-medium">
14
17
  <div class="text-sm mb-0 flex items-center">
15
18
  <ObjectCardOwner
@@ -66,13 +69,14 @@ import { computed } from 'vue'
66
69
  import type { RouteLocationRaw } from 'vue-router'
67
70
  import { useFormatDate } from '../functions/dates'
68
71
  import type { Reuse } from '../types/reuses'
72
+ import type { TitleTag } from '../types/ui'
69
73
  import { useTranslation } from '../composables/useTranslation'
70
74
  import AppLink from './AppLink.vue'
71
75
  import ObjectCardOwner from './ObjectCardOwner.vue'
72
76
  import ReuseDetails from './ReuseDetails.vue'
73
77
  import Placeholder from './Placeholder.vue'
74
78
 
75
- const props = defineProps<{
79
+ const props = withDefaults(defineProps<{
76
80
  reuse: Reuse
77
81
 
78
82
  /**
@@ -86,7 +90,11 @@ const props = defineProps<{
86
90
  * It is used as a separate prop to allow other sites using the package to define their own organization pages.
87
91
  */
88
92
  organizationUrl?: RouteLocationRaw
89
- }>()
93
+
94
+ titleTag?: TitleTag
95
+ }>(), {
96
+ titleTag: 'h3',
97
+ })
90
98
 
91
99
  const { t } = useTranslation()
92
100
  const { formatRelativeIfRecentDate } = useFormatDate()
@@ -13,6 +13,7 @@
13
13
  <SearchInput
14
14
  v-model="q"
15
15
  :placeholder="resolvedPlaceholder"
16
+ :auto-focus
16
17
  />
17
18
  </div>
18
19
  <div class="grid grid-cols-12 mt-2 md:mt-5">
@@ -401,11 +402,17 @@ const props = withDefaults(defineProps<{
401
402
  config?: GlobalSearchConfig
402
403
  placeholder?: string | null
403
404
  hideSearchInput?: boolean
405
+ autoFocus?: boolean
404
406
  }>(), {
405
407
  config: getDefaultGlobalSearchConfig,
406
408
  hideSearchInput: false,
409
+ autoFocus: true,
407
410
  })
408
411
 
412
+ const emit = defineEmits<{
413
+ resultsCount: [total: number]
414
+ }>()
415
+
409
416
  // defineModel's default is static and can't depend on props, so we cast and initialize manually
410
417
  const currentType = defineModel<string>('type') as Ref<string>
411
418
  if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class: 'datasets' })
@@ -475,6 +482,11 @@ const showSidebar = computed(() => props.config.length > 1 || activeFilters.valu
475
482
  // URL query params
476
483
  const q = useRouteQuery<string>('q', '')
477
484
  const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsConfig.searchDebounce ?? 300)
485
+ // When the search input is hidden, the parent owns the input and is expected
486
+ // to debounce user typing itself (otherwise typing would land in the URL
487
+ // instantly via v-model and stack two debounces). Bypass the internal debounce
488
+ // so URL-driven q changes hit the fetch params immediately.
489
+ const qForParams = computed(() => props.hideSearchInput ? q.value : qDebounced.value)
478
490
  const page = useRouteQuery('page', 1, { transform: Number })
479
491
  const sort = useRouteQuery<string | undefined>('sort')
480
492
 
@@ -550,7 +562,7 @@ watch(currentType, () => {
550
562
  const stableParamsOptions = {
551
563
  allFilters,
552
564
  customFilterRegistry,
553
- q: qDebounced,
565
+ q: qForParams,
554
566
  sort,
555
567
  page,
556
568
  pageSize,
@@ -643,7 +655,7 @@ for (const c of props.config) {
643
655
  // intentionally excluded here to avoid resetting the page when a filter
644
656
  // registers with its URL-derived value.
645
657
  const filtersForReset = computed(() => ({
646
- q: qDebounced.value,
658
+ q: qForParams.value,
647
659
  organization: organizationId.value,
648
660
  organization_badge: organizationType.value,
649
661
  tag: tag.value,
@@ -712,6 +724,10 @@ function resetFilters() {
712
724
  const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
713
725
  const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
714
726
 
727
+ watch(searchResults, (results) => {
728
+ if (results) emit('resultsCount', results.total)
729
+ }, { immediate: true })
730
+
715
731
  // RSS feed URL for datasets
716
732
  const rssUrl = computed(() => {
717
733
  if (currentTypeConfig.value?.class !== 'datasets') return null
@@ -726,7 +742,7 @@ const rssUrl = computed(() => {
726
742
  }
727
743
 
728
744
  // Add active filters
729
- if (qDebounced.value) params.set('q', qDebounced.value)
745
+ if (qForParams.value) params.set('q', qForParams.value)
730
746
  if (organizationId.value) params.set('organization', organizationId.value)
731
747
  if (organizationType.value) params.set('organization_badge', organizationType.value)
732
748
  if (tag.value) params.set('tag', tag.value)
@@ -37,7 +37,7 @@ import BrandedButton from '../BrandedButton.vue'
37
37
 
38
38
  const q = defineModel<string>({ required: true })
39
39
 
40
- withDefaults(defineProps<{
40
+ const props = withDefaults(defineProps<{
41
41
  placeholder?: string | null
42
42
  autoFocus?: boolean
43
43
  }>(), {
@@ -57,6 +57,7 @@ const focus = () => {
57
57
  }
58
58
 
59
59
  onMounted(async () => {
60
+ if (!props.autoFocus) return
60
61
  await nextTick()
61
62
  focus()
62
63
  })
@@ -13,6 +13,6 @@ export function useMetrics() {
13
13
  getDatasetMetrics: (datasetId: string) => getDatasetMetrics(datasetId, config.metricsApiUrl!),
14
14
  getDataserviceMetrics: (dataserviceId: string) => getDataserviceMetrics(dataserviceId, config.metricsApiUrl!),
15
15
  getReuseMetrics: (reuseId: string) => getReuseMetrics(reuseId, config.metricsApiUrl!),
16
- createDatasetsForOrganizationMetricsUrl: (organizationId: string) => createDatasetsForOrganizationMetricsUrl(organizationId, config.metricsApiUrl!, config.apiBase),
16
+ createDatasetsForOrganizationMetricsUrl: (organizationId: string) => createDatasetsForOrganizationMetricsUrl(organizationId, config.metricsApiUrl!, config.apiBase, config.$fetch),
17
17
  }
18
18
  }
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { inject, type Component, type InjectionKey } from 'vue'
2
2
  import type { UseFetchFunction } from './functions/api.types'
3
- import type { FetchOptions } from 'ofetch'
3
+ import type { $Fetch, FetchOptions } from 'ofetch'
4
4
 
5
5
  export type PluginConfig = {
6
6
  name: string // Name of the application (ex: data.gouv.fr)
@@ -25,6 +25,16 @@ export type PluginConfig = {
25
25
  tabularAllowRemote?: boolean
26
26
  tabularApiDataserviceId?: string
27
27
  customUseFetch?: UseFetchFunction | null
28
+ /**
29
+ * Imperative configured fetch (auth, headers, error handling): the single source of truth for
30
+ * requests. The default `useFetch` uses it as its transport, and imperative helpers (metrics CSV
31
+ * export, …) call it directly — so they never need a `?? ofetch` fallback.
32
+ * Optional for consumers: when omitted, the plugin defaults it to an `ofetch` instance built from
33
+ * the `onRequest`/`onResponse` hooks below (see the `datagouv` plugin install). A consumer can
34
+ * instead provide its own (e.g. a Bearer-authenticated `$fetch`) and skip the hooks entirely.
35
+ */
36
+ $fetch?: $Fetch | null
37
+ /** Auth/headers/error hooks. Folded into the default `$fetch` when no `$fetch` is provided. */
28
38
  onRequest?: FetchOptions['onRequest']
29
39
  onRequestError?: FetchOptions['onRequestError']
30
40
  onResponse?: FetchOptions['onResponse']
@@ -38,8 +48,12 @@ export type PluginConfig = {
38
48
 
39
49
  export const configKey = Symbol() as InjectionKey<PluginConfig>
40
50
 
41
- export function useComponentsConfig(): PluginConfig {
51
+ // After the `datagouv` plugin install, `$fetch` is always set (defaulted to an ofetch instance), so
52
+ // consumers of the config can rely on it without a fallback.
53
+ export type ResolvedPluginConfig = PluginConfig & { $fetch: $Fetch }
54
+
55
+ export function useComponentsConfig(): ResolvedPluginConfig {
42
56
  const config = inject(configKey)
43
57
  if (!config) throw new Error('Calling `useComponentsConfig` outside @datagouv/components')
44
- return config
58
+ return config as ResolvedPluginConfig
45
59
  }
@@ -1,7 +1,6 @@
1
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
- import { useTranslation } from '../composables/useTranslation'
5
4
  import type { AsyncData, AsyncDataRequestStatus, UseFetchOptions } from './api.types'
6
5
 
7
6
  function deepToValue(obj: MaybeRefOrGetter<Record<string, unknown> | undefined>): Record<string, unknown> | undefined {
@@ -18,8 +17,6 @@ export async function useFetch<DataT, ErrorT = never>(
18
17
  ): Promise<AsyncData<DataT, ErrorT>> {
19
18
  const config = useComponentsConfig()
20
19
 
21
- const { locale } = useTranslation()
22
-
23
20
  if (config.customUseFetch) {
24
21
  return await config.customUseFetch(url, options)
25
22
  }
@@ -37,38 +34,12 @@ export async function useFetch<DataT, ErrorT = never>(
37
34
  status.value = 'pending'
38
35
  try {
39
36
  data.value = isRaw
37
+ // Raw targets another data.gouv service (the Tabular API in TabularExplorer) via its own
38
+ // absolute URL, so it must stay bare ofetch: no datagouv apiBase, no datagouv auth attached.
40
39
  ? await ofetch<DataT | null>(urlValue, { params: params ?? query })
41
- : await ofetch<DataT | null>(urlValue, {
42
- baseURL: config.apiBase,
43
- params: params ?? query,
44
- onRequest(param) {
45
- if (config.onRequest) {
46
- if (Array.isArray(config.onRequest)) {
47
- config.onRequest.forEach(r => r(param))
48
- }
49
- else {
50
- config.onRequest(param)
51
- }
52
- }
53
- const { options } = param
54
- options.headers.set('Content-Type', 'application/json')
55
- options.headers.set('Accept', 'application/json')
56
- options.credentials = 'include'
57
- if (config.devApiKey) {
58
- options.headers.set('X-API-KEY', config.devApiKey)
59
- }
60
-
61
- if (locale) {
62
- if (!options.params) {
63
- options.params = {}
64
- }
65
- options.params['lang'] = locale
66
- }
67
- },
68
- onRequestError: config.onRequestError,
69
- onResponse: config.onResponse,
70
- onResponseError: config.onResponseError,
71
- })
40
+ // The configured `$fetch` carries baseURL + auth + headers (see the `datagouv` plugin install
41
+ // for the default one). We only forward the URL and params here.
42
+ : await config.$fetch<DataT | null>(urlValue, { baseURL: config.apiBase, params: params ?? query })
72
43
  status.value = 'success'
73
44
  }
74
45
  catch (e) {
@@ -1,5 +1,5 @@
1
1
  import { escapeCsvValue } from './helpers'
2
- import { ofetch } from 'ofetch'
2
+ import { ofetch, type $Fetch } from 'ofetch'
3
3
  import type { DatasetV2 } from '../types/datasets'
4
4
  import type { PaginatedArray } from '../types/api'
5
5
 
@@ -119,14 +119,16 @@ export async function getDatasetMetrics(datasetId: string, metricsApi: string):
119
119
  }
120
120
  }
121
121
 
122
- export async function createDatasetsForOrganizationMetricsUrl(organizationId: string, metricsApi: string, apiBase: string) {
122
+ export async function createDatasetsForOrganizationMetricsUrl(organizationId: string, metricsApi: string, apiBase: string, apiFetch: $Fetch) {
123
123
  let data = 'dataset_title,dataset_id,month,monthly_visit,monthly_download_resource\n'
124
124
 
125
- // fetch datasets info from organization datasets
125
+ // fetch datasets info from organization datasets through the configured fetch, so it carries the
126
+ // consumer's auth (cookie for cdata via `$api`, Bearer for verticals) instead of a hardcoded
127
+ // `credentials: 'include'`, which breaks CORS cross-origin on the verticals.
126
128
  const datasets: Record<string, Record<string, string>> = {}
127
129
  let datasetsUrl: string | null = `/api/2/datasets/?organization=${organizationId}&page_size=200`
128
130
  while (datasetsUrl) {
129
- const body: PaginatedArray<DatasetV2> = await ofetch(datasetsUrl, { baseURL: apiBase, credentials: 'include' })
131
+ const body: PaginatedArray<DatasetV2> = await apiFetch(datasetsUrl, { baseURL: apiBase })
130
132
  datasetsUrl = body.next_page
131
133
  for (const row of body.data) {
132
134
  datasets[row.id] = { title: row.title }
package/src/main.ts CHANGED
@@ -104,6 +104,8 @@ import InfiniteLoader from './components/InfiniteLoader.vue'
104
104
  import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
105
105
  import type { UseFetchFunction } from './functions/api.types'
106
106
  import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
107
+ import { ofetch } from 'ofetch'
108
+ import { useTranslation } from './composables/useTranslation'
107
109
 
108
110
  export { Toaster, toast } from 'vue-sonner'
109
111
 
@@ -278,6 +280,31 @@ export {
278
280
  // Vue Plugin
279
281
  const datagouv: Plugin<PluginConfig> = {
280
282
  async install(app: App, options) {
283
+ // Default `$fetch` to an ofetch instance carrying the datagouv API specifics + the consumer's
284
+ // auth hooks, so everything downstream (the default `useFetch`, imperative helpers) can rely on
285
+ // a single configured fetch. A consumer that provides its own `$fetch` keeps full control.
286
+ if (!options.$fetch) {
287
+ options.$fetch = ofetch.create({
288
+ baseURL: options.apiBase,
289
+ onRequest(context) {
290
+ if (options.onRequest) {
291
+ if (Array.isArray(options.onRequest)) options.onRequest.forEach(hook => hook(context))
292
+ else options.onRequest(context)
293
+ }
294
+ context.options.headers.set('Content-Type', 'application/json')
295
+ context.options.headers.set('Accept', 'application/json')
296
+ if (options.devApiKey) context.options.headers.set('X-API-KEY', options.devApiKey)
297
+ const { locale } = useTranslation()
298
+ if (locale) {
299
+ context.options.params ??= {}
300
+ context.options.params['lang'] = locale
301
+ }
302
+ },
303
+ onRequestError: options.onRequestError,
304
+ onResponse: options.onResponse,
305
+ onResponseError: options.onResponseError,
306
+ })
307
+ }
281
308
  app.provide(configKey, options)
282
309
  if (!options.textClamp) {
283
310
  const textClamp = await import('vue3-text-clamp')
package/src/types/ui.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export type WellType = 'primary' | 'secondary'
2
2
 
3
3
  export type Weight = 'light' | 'regular' | 'semi-bold' | 'bold' | 'heavy'
4
+
5
+ export type TitleTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'