@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.
- package/dist/{Control-DuZJdKV_.js → Control-ZFh5ta_U.js} +1 -1
- package/dist/{Datafair.client-BzW-ctDf.js → Datafair.client-8bXp6UeQ.js} +1 -1
- package/dist/{Event--kp8kMdJ.js → Event-DSQcW7OF.js} +24 -24
- package/dist/{Image-34hvypZI.js → Image-BijNEG0p.js} +6 -6
- package/dist/{JsonPreview.client-BfMSzR07.js → JsonPreview.client-BxuoPK_w.js} +2 -2
- package/dist/{Map-BjUnLyj8.js → Map-BUtPf5GN.js} +756 -756
- package/dist/{MapContainer.client-CLs-im9i.js → MapContainer.client-CTz0wmJG.js} +37 -38
- package/dist/{OSM-s40W6sQ2.js → OSM-D4MTdBtk.js} +2 -2
- package/dist/{PdfPreview.client-C13PQCU_.js → PdfPreview.client-DpVreUSl.js} +2 -2
- package/dist/{Pmtiles.client-CL7PXXDl.js → Pmtiles.client-BwmHo3T8.js} +1 -1
- 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
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-hJQIqcZm.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-Dcl7oIVu.js} +35 -35
- package/dist/{TileImage-BsXBxMtq.js → TileImage-BJeHipMX.js} +4 -4
- package/dist/{View-BR92hTWP.js → View-xp_P_OHw.js} +412 -401
- package/dist/{XmlPreview.client-KaENrbbG.js → XmlPreview.client-DAOs89cR.js} +3 -3
- package/dist/{common-PJfpC179.js → common-BjQlan3k.js} +36 -36
- package/dist/components-next.css +4 -4
- package/dist/components-next.js +1 -1
- package/dist/{index-C7WVVGgD.js → index-JqjPja1u.js} +32886 -27183
- package/dist/{main-K-42Oe8-.js → main-D4WQMky0.js} +14994 -14977
- package/dist/{proj-DsetBcW7.js → proj-CsNo9yH1.js} +532 -512
- package/dist/{tilecoord-Db24Px13.js → tilecoord-A0fLnBZr.js} +28 -28
- package/dist/{vue3-xml-viewer.common-sHPSE-jD.js → vue3-xml-viewer.common-BGsoNyZe.js} +1 -1
- package/package.json +2 -2
- package/src/components/DataserviceCard.vue +3 -0
- package/src/components/DatasetCard.vue +3 -0
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
- package/src/components/ReuseCard.vue +12 -4
- package/src/components/Search/GlobalSearch.vue +19 -3
- package/src/components/Search/SearchInput.vue +2 -1
- package/src/composables/useMetrics.ts +1 -1
- package/src/config.ts +17 -3
- package/src/functions/api.ts +5 -34
- package/src/functions/metrics.ts +6 -4
- package/src/main.ts +27 -0
- 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-
|
|
2
|
-
import { z as Tt, g as pt } from "./common-
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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),
|
|
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 -
|
|
424
|
-
[0, 0, D, A, Q -
|
|
425
|
-
],
|
|
426
|
-
if (!
|
|
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,
|
|
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) *
|
|
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) *
|
|
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,
|
|
441
|
+
r.moveTo(R, k), r.lineTo(G, K), r.lineTo(U, Q);
|
|
442
442
|
r.clip(), r.transform(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
443
|
+
N[0],
|
|
444
|
+
N[2],
|
|
445
|
+
N[1],
|
|
446
|
+
N[3],
|
|
447
447
|
G,
|
|
448
|
-
|
|
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],
|
|
457
|
+
const O = g[0], S = O.extent;
|
|
458
458
|
Z = O.image, r.scale(
|
|
459
|
-
L(
|
|
460
|
-
-b(
|
|
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
|
|
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 =
|
|
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
|
|
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_ =
|
|
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
|
-
|
|
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
|
-
|
|
900
|
+
Nt as r,
|
|
901
901
|
es as w
|
|
902
902
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datagouv/components-next",
|
|
3
|
-
"version": "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.
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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:
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
}
|
package/src/functions/api.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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) {
|
package/src/functions/metrics.ts
CHANGED
|
@@ -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
|
|
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')
|