@datagouv/components-next 1.0.2-dev.92 → 1.0.2-dev.94

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.
@@ -1,4 +1,4 @@
1
- import { c as Ke } from "./main-Dk_66g-3.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.0.2-dev.92",
3
+ "version": "1.0.2-dev.94",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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>
@@ -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,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'