@datagouv/components-next 1.0.2-dev.14 → 1.0.2-dev.140

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 (111) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-BtQqFBTQ.js → Datafair.client-DXPU1V24.js} +4 -4
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/JsonPreview.client-D1SnGfWG.js +40 -0
  8. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  9. package/dist/{MapContainer.client-DB6p6KF8.js → MapContainer.client-DlsaEVGx.js} +38 -39
  10. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  11. package/dist/{PdfPreview.client-Dr4VcIur.js → PdfPreview.client-C9OrLR0z.js} +823 -855
  12. package/dist/Pmtiles.client-CyylWfTy.js +26142 -0
  13. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-CtXZOHXI.js +61 -0
  14. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  15. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  16. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  17. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  18. package/dist/XmlPreview.client-DoX7RurH.js +34 -0
  19. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  20. package/dist/components-next.css +1 -52
  21. package/dist/components-next.js +174 -145
  22. package/dist/components.css +2 -2
  23. package/dist/{index-D3rieddI.js → index-Br1sa7wQ.js} +33955 -28030
  24. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-DWG7tmpQ.js} +185 -187
  25. package/dist/main-BU6kP4dE.js +73546 -0
  26. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  27. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  28. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  29. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  30. package/dist/{vue3-xml-viewer.common-CJXShyPB.js → vue3-xml-viewer.common-Cpui-Yi9.js} +16 -16
  31. package/package.json +50 -36
  32. package/src/chart.ts +5 -0
  33. package/src/components/ActivityList/ActivityList.vue +3 -2
  34. package/src/components/Chart/ChartViewer.vue +227 -0
  35. package/src/components/Chart/ChartViewerWrapper.vue +174 -0
  36. package/src/components/DataserviceCard.vue +3 -0
  37. package/src/components/DatasetCard.vue +9 -4
  38. package/src/components/Form/Listbox.vue +101 -0
  39. package/src/components/Form/SearchableSelect.vue +2 -1
  40. package/src/components/InfiniteLoader.vue +53 -0
  41. package/src/components/ObjectCardHeader.vue +11 -4
  42. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  43. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  44. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  45. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  46. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  47. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  48. package/src/components/OpenApiViewer/openapi.ts +150 -0
  49. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  50. package/src/components/Pagination.vue +8 -5
  51. package/src/components/RadioInput.vue +7 -2
  52. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  53. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  54. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  55. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
  56. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  57. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -87
  58. package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
  59. package/src/components/ResourceAccordion/Preview.vue +11 -11
  60. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  61. package/src/components/ResourceAccordion/ResourceAccordion.vue +12 -111
  62. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  63. package/src/components/ResourceExplorer/ResourceExplorer.vue +22 -19
  64. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  65. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +50 -148
  66. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  67. package/src/components/ReuseCard.vue +12 -4
  68. package/src/components/Search/GlobalSearch.vue +217 -115
  69. package/src/components/Search/SearchInput.vue +5 -4
  70. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  71. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  72. package/src/components/TabularExplorer/TabularExplorer.vue +1106 -0
  73. package/src/components/TabularExplorer/TabularFilterContent.vue +353 -0
  74. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  75. package/src/components/TabularExplorer/types.ts +86 -0
  76. package/src/composables/useHasTabularData.ts +13 -0
  77. package/src/composables/useMetrics.ts +1 -1
  78. package/src/composables/useResourceCapabilities.ts +1 -1
  79. package/src/composables/useSearchFilter.ts +120 -0
  80. package/src/composables/useStableQueryParams.ts +38 -6
  81. package/src/composables/useTabularProfile.ts +70 -0
  82. package/src/config.ts +20 -3
  83. package/src/functions/activities.ts +3 -3
  84. package/src/functions/api.ts +20 -42
  85. package/src/functions/api.types.ts +1 -0
  86. package/src/functions/charts.ts +79 -0
  87. package/src/functions/datasets.ts +0 -17
  88. package/src/functions/dates.ts +3 -2
  89. package/src/functions/metrics.ts +6 -4
  90. package/src/functions/resources.ts +61 -1
  91. package/src/functions/tabular.ts +171 -0
  92. package/src/functions/tabularApi.ts +96 -11
  93. package/src/main.ts +95 -9
  94. package/src/types/dataservices.ts +2 -0
  95. package/src/types/pages.ts +0 -5
  96. package/src/types/posts.ts +2 -2
  97. package/src/types/reports.ts +5 -1
  98. package/src/types/resources.ts +0 -1
  99. package/src/types/search.ts +63 -1
  100. package/src/types/site.ts +5 -3
  101. package/src/types/ui.ts +2 -0
  102. package/src/types/users.ts +2 -1
  103. package/src/types/visualizations.ts +93 -0
  104. package/assets/swagger-themes/newspaper.css +0 -1670
  105. package/dist/JsonPreview.client-DojrmGyU.js +0 -72
  106. package/dist/Pmtiles.client-DbMjfK00.js +0 -25185
  107. package/dist/Swagger.client-Db_xYKJv.js +0 -4
  108. package/dist/XmlPreview.client-CHxSFhd7.js +0 -64
  109. package/dist/main-D70Xt4a7.js +0 -105854
  110. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  111. package/src/functions/pagination.ts +0 -9
@@ -1,13 +1,15 @@
1
1
  import { readonly, type Component } from 'vue'
2
2
 
3
3
  import { RiEarthLine, RiMap2Line } from '@remixicon/vue'
4
+ import { useComponentsConfig } from '../config'
4
5
  import Archive from '../components/Icons/Archive.vue'
5
6
  import Code from '../components/Icons/Code.vue'
7
+ import type { Dataset, DatasetV2 } from '../types/datasets'
6
8
  import Documentation from '../components/Icons/Documentation.vue'
7
9
  import Image from '../components/Icons/Image.vue'
8
10
  import Link from '../components/Icons/Link.vue'
9
11
  import Table from '../components/Icons/Table.vue'
10
- import type { Resource } from '../types/resources'
12
+ import type { CommunityResource, Resource } from '../types/resources'
11
13
  import { useTranslation } from '../composables/useTranslation'
12
14
 
13
15
  export function getResourceFormatIcon(format: string): Component | null {
@@ -89,6 +91,11 @@ export function getResourceTitleId(resource: Resource) {
89
91
  export const RESOURCE_TYPE = readonly(['main', 'documentation', 'update', 'api', 'code', 'other'] as const)
90
92
  export type ResourceType = typeof RESOURCE_TYPE[number]
91
93
 
94
+ // Page size used by ResourceExplorer for its per-type resource fetches.
95
+ // Exported so consumers (e.g. the dataset page's `main` prefetch) can build a
96
+ // byte-for-byte identical cache key and let Nuxt dedupe the request.
97
+ export const RESOURCE_EXPLORER_PAGE_SIZE = 10
98
+
92
99
  export const getResourceLabel = (type: ResourceType, count?: number) => {
93
100
  const { t } = useTranslation()
94
101
  switch (type) {
@@ -129,3 +136,56 @@ export const detectOgcService = (resource: Resource) => {
129
136
  }
130
137
  return false
131
138
  }
139
+
140
+ export function isCommunityResource(resource: Resource | CommunityResource): boolean {
141
+ return 'organization' in resource || 'owner' in resource
142
+ }
143
+
144
+ export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>, resource: Resource | CommunityResource): string {
145
+ return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
146
+ }
147
+
148
+ export function getResourceFilesize(resource: Resource): null | number {
149
+ if (resource.filesize) return resource.filesize
150
+ if ('analysis:content-length' in resource.extras) return resource.extras['analysis:content-length'] as number
151
+
152
+ return null
153
+ }
154
+
155
+ type CorsStatus = 'allowed' | 'blocked' | 'unknown'
156
+
157
+ export const getResourceCorsStatus = (resource: Resource): CorsStatus => {
158
+ const extras = resource.extras
159
+ if (!extras || !('check:cors:allow-origin' in extras)) {
160
+ return 'unknown'
161
+ }
162
+
163
+ const allowOrigin = extras['check:cors:allow-origin'] as string | undefined
164
+ const rawMethods = extras['check:cors:allow-methods'] as string | undefined
165
+
166
+ // Check if allow-origin is '*' or contains one of our trusted domains
167
+ const config = useComponentsConfig()
168
+ const trustedDomains = config.trustedDomains ?? []
169
+ const hasPublicCors = allowOrigin === '*'
170
+ const hasSpecificCors = allowOrigin
171
+ ? trustedDomains.some((domain) => {
172
+ try {
173
+ const hostname = new URL(allowOrigin).hostname
174
+ return hostname === domain || hostname.endsWith(`.${domain}`)
175
+ }
176
+ catch {
177
+ return false
178
+ }
179
+ })
180
+ : false
181
+
182
+ const isOriginAllowed = hasPublicCors || hasSpecificCors
183
+
184
+ // Ensure GET method is allowed
185
+ const allowedMethods = rawMethods
186
+ ? rawMethods.split(',').map(m => m.trim().toUpperCase())
187
+ : []
188
+ const supportsGet = allowedMethods.length === 0 || allowedMethods.includes('GET')
189
+
190
+ return isOriginAllowed && supportsGet ? 'allowed' : 'blocked'
191
+ }
@@ -0,0 +1,171 @@
1
+ import type { Component } from 'vue'
2
+ import {
3
+ RiHashtag,
4
+ RiPriceTag3Line,
5
+ RiText,
6
+ RiCalendarLine,
7
+ RiCalendarEventLine,
8
+ RiTimeLine,
9
+ RiCheckboxLine,
10
+ RiMapPin2Line,
11
+ RiGlobalLine,
12
+ RiFocus3Line,
13
+ RiFingerprintLine,
14
+ RiMailLine,
15
+ RiLinksLine,
16
+ RiPhoneLine,
17
+ RiUserLine,
18
+ RiMoneyEuroCircleLine,
19
+ RiPercentLine,
20
+ RiBracesLine,
21
+ RiCodeLine,
22
+ } from '@remixicon/vue'
23
+ import { useTranslation } from '../composables/useTranslation'
24
+ import type { TranslationFunction } from '../composables/useTranslation'
25
+ import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
26
+
27
+ export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
28
+ const f = filters[column]
29
+ if (!f) return false
30
+ return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
31
+ }
32
+
33
+ export type TypeDisplay = {
34
+ icon: Component
35
+ label: string
36
+ }
37
+
38
+ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, TypeDisplay> {
39
+ return {
40
+ number: { icon: RiHashtag, label: t('Nombre') },
41
+ categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
42
+ text: { icon: RiText, label: t('Texte') },
43
+ date: { icon: RiCalendarLine, label: t('Date') },
44
+ boolean: { icon: RiCheckboxLine, label: t('Booléen') },
45
+ }
46
+ }
47
+
48
+ export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
49
+ if (['int', 'float'].includes(colInfo.python_type)) return 'number'
50
+ if (colInfo.format === 'year') return 'date'
51
+ if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
52
+ if (colInfo.python_type === 'bool') return 'boolean'
53
+ if (isCategorical) return 'categorical'
54
+ return 'text'
55
+ }
56
+
57
+ // csv-detective formats that carry no semantic meaning beyond the generic
58
+ // display type already resolved by `ColumnType` — these fall back to
59
+ // `buildTypeConfig` (Texte / Nombre / Date / Booléen) in the header.
60
+ export const GENERIC_FORMATS = new Set(['string', 'int', 'float', 'date', 'bool', 'booleen'])
61
+
62
+ // Semantic types detected by csv-detective (see ../../../csv-detective/csv_detective/formats).
63
+ // The list is intentionally exhaustive, but unknown formats are tolerated:
64
+ // `getColumnDisplay` humanizes any format missing from this map.
65
+ export function buildFormatConfig(t: TranslationFunction): Record<string, TypeDisplay> {
66
+ return {
67
+ // Geo — territories & addresses
68
+ adresse: { icon: RiMapPin2Line, label: t('Adresse') },
69
+ commune: { icon: RiMapPin2Line, label: t('Commune') },
70
+ code_commune: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
71
+ code_commune_insee: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
72
+ departement: { icon: RiMapPin2Line, label: t('Département') },
73
+ code_departement: { icon: RiMapPin2Line, label: t('Code département') },
74
+ region: { icon: RiMapPin2Line, label: t('Région') },
75
+ code_region: { icon: RiMapPin2Line, label: t('Code région') },
76
+ code_epci: { icon: RiMapPin2Line, label: t('Code EPCI') },
77
+ code_postal: { icon: RiMapPin2Line, label: t('Code postal') },
78
+ insee_canton: { icon: RiMapPin2Line, label: t('Canton') },
79
+ code_fantoir: { icon: RiMapPin2Line, label: t('Code FANTOIR') },
80
+ id_rnb: { icon: RiMapPin2Line, label: t('Identifiant RNB') },
81
+ // Geo — countries
82
+ pays: { icon: RiGlobalLine, label: t('Pays') },
83
+ iso_country_code_alpha2: { icon: RiGlobalLine, label: t('Code pays (ISO α-2)') },
84
+ iso_country_code_alpha3: { icon: RiGlobalLine, label: t('Code pays (ISO α-3)') },
85
+ iso_country_code_numeric: { icon: RiGlobalLine, label: t('Code pays (ISO num.)') },
86
+ // Geo — coordinates
87
+ latitude_wgs: { icon: RiFocus3Line, label: t('Latitude') },
88
+ latitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Latitude') },
89
+ longitude_wgs: { icon: RiFocus3Line, label: t('Longitude') },
90
+ longitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Longitude') },
91
+ latitude_l93: { icon: RiFocus3Line, label: t('Latitude (Lambert 93)') },
92
+ longitude_l93: { icon: RiFocus3Line, label: t('Longitude (Lambert 93)') },
93
+ latlon_wgs: { icon: RiFocus3Line, label: t('Latitude / longitude') },
94
+ lonlat_wgs: { icon: RiFocus3Line, label: t('Longitude / latitude') },
95
+ // Identifiers
96
+ siren: { icon: RiFingerprintLine, label: t('SIREN') },
97
+ siret: { icon: RiFingerprintLine, label: t('SIRET') },
98
+ code_rna: { icon: RiFingerprintLine, label: t('Identifiant RNA') },
99
+ code_waldec: { icon: RiFingerprintLine, label: t('Identifiant WALDEC') },
100
+ code_import: { icon: RiFingerprintLine, label: t('Code Import') },
101
+ uai: { icon: RiFingerprintLine, label: t('UAI') },
102
+ insee_ape700: { icon: RiFingerprintLine, label: t('Code APE') },
103
+ code_csp_insee: { icon: RiFingerprintLine, label: t('Code CSP') },
104
+ uuid: { icon: RiFingerprintLine, label: t('UUID') },
105
+ mongo_object_id: { icon: RiFingerprintLine, label: t('Identifiant Mongo') },
106
+ // Contact
107
+ email: { icon: RiMailLine, label: t('Email') },
108
+ url: { icon: RiLinksLine, label: t('URL') },
109
+ tel_fr: { icon: RiPhoneLine, label: t('Téléphone') },
110
+ username: { icon: RiUserLine, label: t('Nom d\'utilisateur') },
111
+ // People
112
+ sexe: { icon: RiUserLine, label: t('Sexe') },
113
+ csp_insee: { icon: RiUserLine, label: t('Catégorie socio-pro.') },
114
+ // Quantities
115
+ money: { icon: RiMoneyEuroCircleLine, label: t('Montant') },
116
+ percent: { icon: RiPercentLine, label: t('Pourcentage') },
117
+ // Temporal
118
+ year: { icon: RiCalendarLine, label: t('Année') },
119
+ date_fr: { icon: RiCalendarLine, label: t('Date') },
120
+ jour_de_la_semaine: { icon: RiCalendarEventLine, label: t('Jour de la semaine') },
121
+ mois_de_lannee: { icon: RiCalendarEventLine, label: t('Mois de l\'année') },
122
+ datetime_naive: { icon: RiTimeLine, label: t('Date et heure') },
123
+ datetime_aware: { icon: RiTimeLine, label: t('Date et heure') },
124
+ datetime_rfc822: { icon: RiTimeLine, label: t('Date et heure') },
125
+ // Structured
126
+ json: { icon: RiBracesLine, label: t('JSON') },
127
+ geojson: { icon: RiBracesLine, label: t('GeoJSON') },
128
+ binary: { icon: RiCodeLine, label: t('Binaire') },
129
+ }
130
+ }
131
+
132
+ // Tolerant fallback for a semantic format we don't explicitly know about:
133
+ // "code_truc_machin" -> "Code truc machin".
134
+ export function humanizeFormat(format: string): string {
135
+ const spaced = format.replace(/_/g, ' ').trim()
136
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
137
+ }
138
+
139
+ export function useFormatTabular() {
140
+ const { locale } = useTranslation()
141
+
142
+ function formatNumber(value: unknown): string {
143
+ const num = Number(value)
144
+ if (Number.isNaN(num)) return String(value)
145
+ return num.toLocaleString(locale)
146
+ }
147
+
148
+ function formatCellDate(value: unknown): string {
149
+ if (value == null || value === '') return '–'
150
+ const d = new Date(String(value))
151
+ if (Number.isNaN(d.getTime())) return String(value)
152
+ return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
153
+ }
154
+
155
+ return { formatNumber, formatCellDate }
156
+ }
157
+
158
+ const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
159
+ const FALSY_VALUES = ['false', '0', 'non', 'no']
160
+
161
+ export function isTruthy(value: unknown): boolean {
162
+ if (typeof value === 'boolean') return value
163
+ if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
164
+ return Boolean(value)
165
+ }
166
+
167
+ export function isFalsy(value: unknown): boolean {
168
+ if (typeof value === 'boolean') return !value
169
+ if (typeof value === 'string') return FALSY_VALUES.includes(value.toLowerCase())
170
+ return !value
171
+ }
@@ -1,20 +1,105 @@
1
1
  import { ofetch } from 'ofetch'
2
2
  import { useComponentsConfig, type PluginConfig } from '../config'
3
+ import type { GenericFilter } from '../types/visualizations'
4
+ import type { SortConfig, TabularProfileResponse } from '../components/TabularExplorer/types'
3
5
 
4
- export type SortConfig = {
5
- column: string
6
- type: string
7
- } | null
6
+ export type { SortConfig }
7
+
8
+ export type TabularDataResponse = {
9
+ data: Array<Record<string, unknown>>
10
+ links: {
11
+ profile: string
12
+ swagger: string
13
+ next: string
14
+ }
15
+ meta: { total: number }
16
+ }
17
+
18
+ export type TabularAggregateType = 'avg' | 'sum' | 'count' | 'min' | 'max'
19
+
20
+ export type FetchTabularDataOptions = {
21
+ resourceId: string
22
+ page?: number
23
+ pageSize?: number
24
+ columns?: Array<string> | undefined
25
+ sort?: SortConfig
26
+ groupBy?: string | undefined
27
+ aggregation?: {
28
+ column: string
29
+ type: TabularAggregateType
30
+ } | undefined
31
+ filters?: GenericFilter | undefined
32
+ }
8
33
 
9
34
  /**
10
- * Call Tabular-api to get table content
35
+ * Call Tabular-api to get table content with options object
11
36
  */
12
- export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig) {
13
- let url = `${config.tabularApiUrl}/api/resources/${id}/data/?page=${page}&page_size=${config.tabularApiPageSize || 15}`
14
- if (sortConfig) {
15
- url = url + `&${sortConfig.column}__sort=${sortConfig.type}`
37
+ export async function fetchTabularData(config: PluginConfig, options: FetchTabularDataOptions): Promise<TabularDataResponse> {
38
+ const page = options.page ?? 1
39
+ const pageSize = options.pageSize ?? config.tabularApiPageSize ?? 15
40
+ let url = `${config.tabularApiUrl}/api/resources/${options.resourceId}/data/?page=${page}&page_size=${pageSize}`
41
+ if (options.columns) {
42
+ url += `&columns=${options.columns.map(col => encodeURIComponent(col)).join(',')}`
43
+ }
44
+ if (options.sort) {
45
+ url += `&${encodeURIComponent(options.sort.column)}__sort=${encodeURIComponent(options.sort.direction)}`
46
+ }
47
+ if (options.groupBy && options.aggregation?.type) {
48
+ url += `&${encodeURIComponent(options.groupBy)}__groupby&${encodeURIComponent(options.aggregation.column)}__${encodeURIComponent(options.aggregation.type)}`
49
+ }
50
+ if (options.filters) {
51
+ const filterQuery = buildFilterQuery(options.filters)
52
+ if (filterQuery) {
53
+ url += `&${filterQuery}`
54
+ }
16
55
  }
17
- return await ofetch(url)
56
+ return await ofetch<TabularDataResponse>(url)
57
+ }
58
+
59
+ function buildFilterQuery(filters: GenericFilter): string {
60
+ const params: Array<string> = []
61
+ if ('filters' in filters) {
62
+ for (const filter of filters.filters) {
63
+ if ('filters' in filter) {
64
+ params.push(buildFilterQuery(filter))
65
+ }
66
+ else {
67
+ if (filter.condition === 'is_null') {
68
+ params.push(`${encodeURIComponent(filter.column)}__isnull`)
69
+ }
70
+ else if (filter.condition === 'is_not_null') {
71
+ params.push(`${encodeURIComponent(filter.column)}__isnotnull`)
72
+ }
73
+ else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
74
+ params.push(`${encodeURIComponent(filter.column)}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`)
75
+ }
76
+ }
77
+ }
78
+ }
79
+ else {
80
+ const filter = filters
81
+ if (filter.condition === 'is_null') {
82
+ params.push(`${encodeURIComponent(filter.column)}__isnull`)
83
+ }
84
+ else if (filter.condition === 'is_not_null') {
85
+ params.push(`${encodeURIComponent(filter.column)}__isnotnull`)
86
+ }
87
+ else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
88
+ params.push(`${encodeURIComponent(filter.column)}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`)
89
+ }
90
+ }
91
+ return params.join('&')
92
+ }
93
+
94
+ /**
95
+ * Call Tabular-api to get table content
96
+ */
97
+ export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig | null): Promise<TabularDataResponse> {
98
+ return fetchTabularData(config, {
99
+ resourceId: id,
100
+ page,
101
+ sort: sortConfig ?? undefined,
102
+ })
18
103
  }
19
104
 
20
105
  /**
@@ -22,5 +107,5 @@ export async function getData(config: PluginConfig, id: string, page: number, so
22
107
  */
23
108
  export function useGetProfile() {
24
109
  const config = useComponentsConfig()
25
- return (id: string) => ofetch(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
110
+ return (id: string) => ofetch<TabularProfileResponse>(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
26
111
  }
package/src/main.ts CHANGED
@@ -13,7 +13,7 @@ import type { License } from './types/licenses'
13
13
  import type { Member, MemberRole, NewOrganization, Organization, OrganizationOrSuggest, OrganizationReference, OrganizationSuggest } from './types/organizations'
14
14
  import type { Owned, OwnedWithFullObject, OwnedWithId } from './types/owned'
15
15
  import type { Comment, Thread } from './types/discussions'
16
- import type { Page, PageBloc, ContentBloc, BlocWithTitle, DatasetsListBloc, DataservicesListBloc, ReusesListBloc, LinkInBloc, LinksListBloc, MarkdownBloc, AccordionItemBloc, AccordionListBloc, HeroBloc } from './types/pages'
16
+ import type { PageBloc, ContentBloc, BlocWithTitle, DatasetsListBloc, DataservicesListBloc, ReusesListBloc, LinkInBloc, LinksListBloc, MarkdownBloc, AccordionItemBloc, AccordionListBloc, HeroBloc } from './types/pages'
17
17
  import type { Post } from './types/posts'
18
18
  import type { ReuseReference, NewReuse, Reuse, ReuseTopic, ReuseType } from './types/reuses'
19
19
  import type { RegisteredSchema, Schema, SchemaDetails, SchemaField, SchemaPath, SchemaPublicationMode, SchemaResponseData, SchemaVersion, ValidataError } from './types/schemas'
@@ -23,8 +23,12 @@ import type { Site } from './types/site'
23
23
  import type { Weight, WellType } from './types/ui'
24
24
  import type { User, UserReference } from './types/users'
25
25
  import type { Report, ReportSubject, ReportReason } from './types/reports'
26
- import type { GlobalSearchConfig, SearchType, SortOption } from './types/search'
27
- import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
26
+ import type { Chart, ChartForm, ChartForApi, FilterCondition, Filter, AndFilters, GenericFilter, XAxisType, XAxisSortBy, SortDirection, XAxis, XAxisForm, UnitPosition, YAxis, DataSeriesType, DataSeries, DataSeriesForm, CombinedSort, ColumnDefinition, ColumnsDefinition } from './types/visualizations'
27
+ import type { ColumnType } from './components/TabularExplorer/types'
28
+ import type { GlobalSearchConfig, SearchType, SearchTypeConfig, SortOption, HiddenFilter, BuiltInFilterKey, DatasetSearchConfig, DatasetSearchFilters, DataserviceSearchConfig, DataserviceSearchFilters, ReuseSearchConfig, ReuseSearchFilters, OrganizationSearchConfig, OrganizationSearchFilters, TopicSearchConfig, TopicSearchFilters } from './types/search'
29
+ import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultTopicConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
30
+ import { useSearchFilter } from './composables/useSearchFilter'
31
+ import type { UseSearchFilterOptions } from './composables/useSearchFilter'
28
32
 
29
33
  import ActivityList from './components/ActivityList/ActivityList.vue'
30
34
  import UserActivityList from './components/ActivityList/UserActivityList.vue'
@@ -37,6 +41,9 @@ import BrandedButton from './components/BrandedButton.vue'
37
41
  import CopyButton from './components/CopyButton.vue'
38
42
  import DataserviceCard from './components/DataserviceCard.vue'
39
43
  import DatasetCard from './components/DatasetCard.vue'
44
+ import DataStructure from './components/ResourceAccordion/DataStructure.vue'
45
+ import Downloads from './components/ResourceAccordion/Downloads.vue'
46
+ import Metadata from './components/ResourceAccordion/Metadata.vue'
40
47
  import DescriptionListTerm from './components/DescriptionListTerm.vue'
41
48
  import DescriptionListDetails from './components/DescriptionListDetails.vue'
42
49
  import DiscussionMessageCard from './components/DiscussionMessageCard.vue'
@@ -57,6 +64,7 @@ import LoadingBlock from './components/LoadingBlock.vue'
57
64
  import MarkdownViewer from './components/MarkdownViewer.vue'
58
65
  import OrganizationCard from './components/OrganizationCard.vue'
59
66
  import OrganizationHorizontalCard from './components/OrganizationHorizontalCard.vue'
67
+ import ObjectCardOwner from './components/ObjectCardOwner.vue'
60
68
  import OrganizationLogo from './components/OrganizationLogo.vue'
61
69
  import OrganizationNameWithCertificate from './components/OrganizationNameWithCertificate.vue'
62
70
  import OwnerType from './components/OwnerType.vue'
@@ -70,16 +78,16 @@ import PostCard from './components/PostCard.vue'
70
78
  import ReadMore from './components/ReadMore.vue'
71
79
  import ResourceAccordion from './components/ResourceAccordion/ResourceAccordion.vue'
72
80
  import ResourceIcon from './components/ResourceAccordion/ResourceIcon.vue'
81
+ import ResourceSelector from './components/ResourceExplorer/ResourceSelector.vue'
73
82
  import ResourceExplorer from './components/ResourceExplorer/ResourceExplorer.vue'
74
83
  import ResourceExplorerSidebar from './components/ResourceExplorer/ResourceExplorerSidebar.vue'
75
84
  import ResourceExplorerViewer from './components/ResourceExplorer/ResourceExplorerViewer.vue'
76
- import Swagger from './components/ResourceAccordion/Swagger.client.vue'
85
+ import OpenApiViewer from './components/OpenApiViewer/OpenApiViewer.vue'
77
86
  import ReuseCard from './components/ReuseCard.vue'
78
87
  import ReuseHorizontalCard from './components/ReuseHorizontalCard.vue'
79
88
  import ReuseDetails from './components/ReuseDetails.vue'
80
89
  import SchemaCard from './components/SchemaCard.vue'
81
90
  import SimpleBanner from './components/SimpleBanner.vue'
82
- import SmallChart from './components/SmallChart.vue'
83
91
  import StatBox from './components/StatBox.vue'
84
92
  import Tab from './components/Tabs/Tab.vue'
85
93
  import TabGroup from './components/Tabs/TabGroup.vue'
@@ -94,16 +102,24 @@ import GlobalSearch from './components/Search/GlobalSearch.vue'
94
102
  import SearchInput from './components/Search/SearchInput.vue'
95
103
  import SearchableSelect from './components/Form/SearchableSelect.vue'
96
104
  import SelectGroup from './components/Form/SelectGroup.vue'
105
+ import Listbox from './components/Form/Listbox.vue'
106
+ import InfiniteLoader from './components/InfiniteLoader.vue'
107
+ import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
97
108
  import type { UseFetchFunction } from './functions/api.types'
98
109
  import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
110
+ import { ofetch } from 'ofetch'
111
+ import { useTranslation } from './composables/useTranslation'
99
112
 
100
113
  export { Toaster, toast } from 'vue-sonner'
101
114
 
102
115
  export * from './composables/useActiveDescendant'
116
+ export * from './composables/useDebouncedRef'
103
117
  export * from './composables/useMetrics'
104
118
  export * from './composables/useReuseType'
105
119
  export * from './composables/useTranslation'
106
120
  export * from './composables/useHasTabularData'
121
+ export * from './composables/useResourceCapabilities'
122
+ export * from './composables/useTabularProfile'
107
123
 
108
124
  export * from './functions/activities'
109
125
  export * from './functions/datasets'
@@ -116,17 +132,33 @@ export * from './functions/metrics'
116
132
  export * from './functions/never'
117
133
  export * from './functions/organizations'
118
134
  export * from './functions/owned'
119
- export * from './functions/pagination'
120
135
  export * from './functions/resources'
121
136
  export * from './functions/reuses'
122
137
  export * from './functions/schemas'
138
+ export * from './functions/tabular'
123
139
  export * from './functions/users'
140
+ export * from './functions/tabularApi'
141
+ export * from './functions/charts'
124
142
  export * from './types/access_types'
125
143
 
126
144
  export type {
127
145
  GlobalSearchConfig,
128
146
  SearchType,
147
+ SearchTypeConfig,
129
148
  SortOption,
149
+ HiddenFilter,
150
+ BuiltInFilterKey,
151
+ DatasetSearchConfig,
152
+ DatasetSearchFilters,
153
+ DataserviceSearchConfig,
154
+ DataserviceSearchFilters,
155
+ ReuseSearchConfig,
156
+ ReuseSearchFilters,
157
+ OrganizationSearchConfig,
158
+ OrganizationSearchFilters,
159
+ TopicSearchConfig,
160
+ TopicSearchFilters,
161
+ UseSearchFilterOptions,
130
162
  UseFetchFunction,
131
163
  AccessType,
132
164
  AccessAudience,
@@ -168,7 +200,6 @@ export type {
168
200
  Owned,
169
201
  OwnedWithFullObject,
170
202
  OwnedWithId,
171
- Page,
172
203
  PageBloc,
173
204
  ContentBloc,
174
205
  BlocWithTitle,
@@ -218,6 +249,27 @@ export type {
218
249
  ValidataError,
219
250
  Weight,
220
251
  WellType,
252
+ Chart,
253
+ ChartForm,
254
+ ChartForApi,
255
+ CombinedSort,
256
+ ColumnDefinition,
257
+ ColumnType,
258
+ ColumnsDefinition,
259
+ FilterCondition,
260
+ Filter,
261
+ AndFilters,
262
+ GenericFilter,
263
+ XAxisType,
264
+ XAxisSortBy,
265
+ SortDirection,
266
+ XAxis,
267
+ XAxisForm,
268
+ UnitPosition,
269
+ YAxis,
270
+ DataSeriesType,
271
+ DataSeries,
272
+ DataSeriesForm,
221
273
  }
222
274
 
223
275
  export {
@@ -225,16 +277,43 @@ export {
225
277
  getDefaultDataserviceConfig,
226
278
  getDefaultReuseConfig,
227
279
  getDefaultOrganizationConfig,
280
+ getDefaultTopicConfig,
228
281
  getDefaultGlobalSearchConfig,
229
282
  defaultDatasetSortOptions,
230
283
  defaultDataserviceSortOptions,
231
284
  defaultReuseSortOptions,
232
285
  defaultOrganizationSortOptions,
286
+ useSearchFilter,
233
287
  }
234
288
 
235
289
  // Vue Plugin
236
290
  const datagouv: Plugin<PluginConfig> = {
237
291
  async install(app: App, options) {
292
+ // Default `$fetch` to an ofetch instance carrying the datagouv API specifics + the consumer's
293
+ // auth hooks, so everything downstream (the default `useFetch`, imperative helpers) can rely on
294
+ // a single configured fetch. A consumer that provides its own `$fetch` keeps full control.
295
+ if (!options.$fetch) {
296
+ options.$fetch = ofetch.create({
297
+ baseURL: options.apiBase,
298
+ onRequest(context) {
299
+ if (options.onRequest) {
300
+ if (Array.isArray(options.onRequest)) options.onRequest.forEach(hook => hook(context))
301
+ else options.onRequest(context)
302
+ }
303
+ context.options.headers.set('Content-Type', 'application/json')
304
+ context.options.headers.set('Accept', 'application/json')
305
+ if (options.devApiKey) context.options.headers.set('X-API-KEY', options.devApiKey)
306
+ const { locale } = useTranslation()
307
+ if (locale) {
308
+ context.options.params ??= {}
309
+ context.options.params['lang'] = locale
310
+ }
311
+ },
312
+ onRequestError: options.onRequestError,
313
+ onResponse: options.onResponse,
314
+ onResponseError: options.onResponseError,
315
+ })
316
+ }
238
317
  app.provide(configKey, options)
239
318
  if (!options.textClamp) {
240
319
  const textClamp = await import('vue3-text-clamp')
@@ -256,6 +335,9 @@ export {
256
335
  CopyButton,
257
336
  DataserviceCard,
258
337
  DatasetCard,
338
+ DataStructure,
339
+ Downloads,
340
+ Metadata,
259
341
  DatasetInformationSection,
260
342
  DatasetTemporalitySection,
261
343
  DatasetSpatialSection,
@@ -295,15 +377,16 @@ export {
295
377
  ResourceExplorer,
296
378
  ResourceExplorerSidebar,
297
379
  ResourceExplorerViewer,
380
+ ObjectCardOwner,
298
381
  ResourceIcon,
382
+ ResourceSelector,
299
383
  ReuseCard,
300
384
  ReuseDetails,
301
385
  ReuseHorizontalCard,
302
386
  SchemaCard,
303
387
  SimpleBanner,
304
- SmallChart,
305
388
  StatBox,
306
- Swagger,
389
+ OpenApiViewer,
307
390
  Tab,
308
391
  TabGroup,
309
392
  TabList,
@@ -318,4 +401,7 @@ export {
318
401
  SearchInput,
319
402
  SearchableSelect,
320
403
  SelectGroup,
404
+ Listbox,
405
+ InfiniteLoader,
406
+ TabularExplorer,
321
407
  }
@@ -24,6 +24,7 @@ export type BaseDataservice = Owned & WithAccessType & {
24
24
  license: string | null
25
25
  private: boolean
26
26
  rate_limiting: string
27
+ rate_limiting_url: string | null
27
28
  title: DataserviceReference['title']
28
29
  contact_points: Array<ContactPoint>
29
30
  }
@@ -65,6 +66,7 @@ export type Dataservice = Owned & WithAccessType & {
65
66
  permissions: { edit: boolean, delete: boolean }
66
67
  private: boolean
67
68
  rate_limiting: string
69
+ rate_limiting_url: string | null
68
70
  self_api_url: DataserviceReference['self_api_url']
69
71
  self_web_url: DataserviceReference['self_web_url']
70
72
  slug: string
@@ -2,11 +2,6 @@ import type { DatasetV2 } from './datasets'
2
2
  import type { Dataservice } from './dataservices'
3
3
  import type { Reuse } from './reuses'
4
4
 
5
- export type Page = {
6
- id: string
7
- blocs: Array<PageBloc>
8
- }
9
-
10
5
  export type BlocWithTitle = {
11
6
  title: string
12
7
  subtitle: string | null
@@ -1,12 +1,12 @@
1
1
  import type { Dataset } from './datasets'
2
- import type { Page } from './pages'
2
+ import type { PageBloc } from './pages'
3
3
  import type { Reuse } from './reuses'
4
4
  import type { User } from './users'
5
5
 
6
6
  export type Post = {
7
7
  body_type: 'markdown' | 'html' | 'blocs'
8
+ blocs: Array<PageBloc>
8
9
  content: string
9
- content_as_page: Page | null
10
10
  created_at: string
11
11
  credit_to: string
12
12
  credit_url: string