@datagouv/components-next 0.0.7 → 0.0.9
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/README.md +34 -14
- package/assets/json/vector.json +2377 -0
- package/assets/main.css +3 -0
- package/assets/swagger-themes/newspaper.css +1669 -0
- package/assets/tailwind.config.js +1 -1
- package/dist/JsonPreview.client-BIz1_EiB.js +92 -0
- package/dist/MapContainer.client-ZDwr4Q_I.js +78276 -0
- package/dist/PdfPreview.client-BTTMM27i.js +112 -0
- package/dist/Pmtiles.client-4kOoUQcR.js +22377 -0
- package/dist/Swagger.client-Q7a5wb51.js +4 -0
- package/dist/XmlPreview.client-BYIIkDqf.js +84 -0
- package/dist/components-next.css +52 -1
- package/dist/components-next.js +42 -41
- package/dist/components.css +1 -1
- package/dist/main-CLUk9Jj7.js +105843 -0
- package/dist/pdf-vue3-BZh6kzke.js +273 -0
- package/dist/pdf.min-f72cfa08-DAetWL3M.js +9501 -0
- package/dist/{text-clamp.esm-DurZFOvT.js → text-clamp.esm-DP59tec5.js} +1 -1
- package/dist/vue3-json-viewer-DIQzFF6K.js +1089 -0
- package/dist/vue3-xml-viewer.common-BmKw6vER.js +5437 -0
- package/package.json +7 -5
- package/src/components/AvatarWithName.vue +6 -2
- package/src/components/BannerAction.vue +1 -1
- package/src/components/BrandedButton.vue +13 -8
- package/src/components/CopyButton.vue +7 -7
- package/src/components/DataserviceCard.vue +54 -23
- package/src/components/DatasetCard.vue +36 -24
- package/src/components/DatasetInformationPanel.vue +19 -18
- package/src/components/DatasetQuality.vue +21 -18
- package/src/components/DatasetQualityInline.vue +1 -1
- package/src/components/DatasetQualityItem.vue +3 -3
- package/src/components/DatasetQualityItemWarning.vue +2 -2
- package/src/components/DatasetQualityScore.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +29 -29
- package/src/components/DescriptionDetails.vue +2 -2
- package/src/components/ExtraAccordion.vue +10 -7
- package/src/components/OrganizationCard.vue +9 -4
- package/src/components/OrganizationNameWithCertificate.vue +25 -11
- package/src/components/Pagination.vue +26 -15
- package/src/components/ReadMore.vue +2 -2
- package/src/components/ResourceAccordion/DataStructure.vue +2 -2
- package/src/components/ResourceAccordion/EditButton.vue +10 -6
- package/src/components/ResourceAccordion/JsonPreview.client.vue +153 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +137 -0
- package/src/components/ResourceAccordion/Metadata.vue +33 -54
- package/src/components/ResourceAccordion/PdfPreview.client.vue +189 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +166 -0
- package/src/components/ResourceAccordion/Preview.vue +39 -37
- package/src/components/ResourceAccordion/ResourceAccordion.vue +141 -63
- package/src/components/ResourceAccordion/ResourceIcon.vue +7 -1
- package/src/components/ResourceAccordion/SchemaBadge.vue +26 -26
- package/src/components/ResourceAccordion/{Swagger.vue → Swagger.client.vue} +1 -1
- package/src/components/ResourceAccordion/XmlPreview.client.vue +143 -0
- package/src/components/ReuseCard.vue +10 -7
- package/src/components/ReuseDetails.vue +3 -3
- package/src/components/SimpleBanner.vue +7 -4
- package/src/components/SmallChart.vue +23 -9
- package/src/components/StatBox.vue +92 -10
- package/src/config.ts +6 -2
- package/src/functions/api.ts +18 -18
- package/src/functions/dates.ts +81 -74
- package/src/functions/helpers.ts +5 -4
- package/src/functions/organizations.ts +5 -5
- package/src/functions/resources.ts +34 -5
- package/src/functions/schemas.ts +4 -3
- package/src/functions/tabularApi.ts +1 -1
- package/src/main.ts +10 -11
- package/src/types/badges.ts +3 -3
- package/src/types/contact_point.ts +5 -5
- package/src/types/dataservices.ts +16 -2
- package/src/types/datasets.ts +20 -2
- package/src/types/frequency.ts +5 -5
- package/src/types/granularity.ts +12 -4
- package/src/types/harvest.ts +2 -2
- package/src/types/licenses.ts +8 -8
- package/src/types/organizations.ts +6 -0
- package/src/types/resources.ts +3 -3
- package/src/types/reuses.ts +3 -1
- package/src/types/site.ts +8 -0
- package/src/types/ui.ts +2 -2
- package/src/types/users.ts +24 -8
- package/src/types/vue3-xml-viewer.d.ts +10 -0
- package/dist/Swagger-DjysB-OI.js +0 -67851
- package/dist/en-DCRve7vN.js +0 -613
- package/dist/fr-DCOnbL-p.js +0 -613
- package/dist/locales/de.js +0 -155
- package/dist/locales/en.js +0 -155
- package/dist/locales/es.js +0 -155
- package/dist/locales/fr.js +0 -155
- package/dist/locales/it.js +0 -155
- package/dist/locales/pt.js +0 -155
- package/dist/locales/sr.js +0 -155
- package/dist/main-CPW2vNLE.js +0 -32008
- package/src/components/DescriptionList/DescriptionDetails.stories.ts +0 -43
- package/src/components/DescriptionList/DescriptionList.stories.ts +0 -47
- package/src/components/DescriptionList/DescriptionTerm.stories.ts +0 -28
- package/src/locales/de.json +0 -154
- package/src/locales/en.json +0 -154
- package/src/locales/es.json +0 -154
- package/src/locales/fr.json +0 -154
- package/src/locales/it.json +0 -154
- package/src/locales/pt.json +0 -154
- package/src/locales/sr.json +0 -154
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="fr-text--xs">
|
|
3
|
+
<div v-if="jsonData">
|
|
4
|
+
<JsonViewer
|
|
5
|
+
:value="jsonData"
|
|
6
|
+
boxed
|
|
7
|
+
sort
|
|
8
|
+
theme="light"
|
|
9
|
+
:max-depth="3"
|
|
10
|
+
:expand-depth="2"
|
|
11
|
+
:indent-width="2"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
<div
|
|
15
|
+
v-else-if="loading"
|
|
16
|
+
class="text-gray-medium"
|
|
17
|
+
>
|
|
18
|
+
{{ $t("Chargement de l'aperçu JSON...") }}
|
|
19
|
+
</div>
|
|
20
|
+
<SimpleBanner
|
|
21
|
+
v-else-if="fileTooLarge"
|
|
22
|
+
type="warning"
|
|
23
|
+
class="flex items-center space-x-2"
|
|
24
|
+
>
|
|
25
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
26
|
+
<span>{{ fileSizeBytes
|
|
27
|
+
? $t("Fichier JSON trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
|
|
28
|
+
: $t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
|
|
29
|
+
}}</span>
|
|
30
|
+
</SimpleBanner>
|
|
31
|
+
<SimpleBanner
|
|
32
|
+
v-else-if="error === 'network'"
|
|
33
|
+
type="warning"
|
|
34
|
+
class="flex items-center space-x-2"
|
|
35
|
+
>
|
|
36
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
37
|
+
<span>{{ $t("Ce fichier JSON ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
|
|
38
|
+
</SimpleBanner>
|
|
39
|
+
<SimpleBanner
|
|
40
|
+
v-else-if="error"
|
|
41
|
+
type="warning"
|
|
42
|
+
class="flex items-center space-x-2"
|
|
43
|
+
>
|
|
44
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
45
|
+
<span>{{ $t("Erreur lors du chargement de l'aperçu JSON.") }}</span>
|
|
46
|
+
</SimpleBanner>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
|
52
|
+
import { RiErrorWarningLine } from '@remixicon/vue'
|
|
53
|
+
|
|
54
|
+
import { useComponentsConfig } from '../../config'
|
|
55
|
+
import SimpleBanner from '../SimpleBanner.vue'
|
|
56
|
+
import type { Resource } from '../../types/resources'
|
|
57
|
+
|
|
58
|
+
const JsonViewer = defineAsyncComponent(() =>
|
|
59
|
+
import('vue3-json-viewer').then((module) => {
|
|
60
|
+
// Import CSS when component loads
|
|
61
|
+
import('vue3-json-viewer/dist/vue3-json-viewer.css')
|
|
62
|
+
return module.JsonViewer
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const props = defineProps<{
|
|
67
|
+
resource: Resource
|
|
68
|
+
}>()
|
|
69
|
+
|
|
70
|
+
const config = useComponentsConfig()
|
|
71
|
+
|
|
72
|
+
const jsonData = ref<unknown>(null)
|
|
73
|
+
const loading = ref(false)
|
|
74
|
+
const error = ref<string | null>(null)
|
|
75
|
+
const fileTooLarge = ref(false)
|
|
76
|
+
|
|
77
|
+
const fileSizeBytes = computed(() => {
|
|
78
|
+
// Check if resource has filesize
|
|
79
|
+
if (props.resource.filesize) {
|
|
80
|
+
return props.resource.filesize
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if resource has content-length in extras (from API metadata)
|
|
84
|
+
const contentLength = props.resource.extras?.['analysis:content-length']
|
|
85
|
+
if (contentLength && typeof contentLength === 'number') {
|
|
86
|
+
return contentLength
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const shouldLoadJson = computed(() => {
|
|
93
|
+
const size = fileSizeBytes.value
|
|
94
|
+
if (!size) {
|
|
95
|
+
// If we don't know the size, don't risk loading a potentially huge file
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if maxJsonPreviewSize is configured
|
|
100
|
+
if (!config.maxJsonPreviewSize) {
|
|
101
|
+
// If no limit is set, don't load unknown files
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convert maxJsonPreviewSize from characters to bytes (rough estimate)
|
|
106
|
+
// Assuming average 1 byte per character for JSON
|
|
107
|
+
const maxSizeBytes = config.maxJsonPreviewSize
|
|
108
|
+
|
|
109
|
+
return size <= maxSizeBytes
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const fetchJsonData = async () => {
|
|
113
|
+
// Check if file is too large or size is unknown before making the request
|
|
114
|
+
if (!shouldLoadJson.value) {
|
|
115
|
+
fileTooLarge.value = true
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
loading.value = true
|
|
120
|
+
error.value = null
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(props.resource.url)
|
|
124
|
+
// const response = await fetch('/test-data.json') // For testing locally without CORS issues
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
127
|
+
}
|
|
128
|
+
const data = await response.json()
|
|
129
|
+
|
|
130
|
+
// Use the original data directly - let the JSON viewer handle large files
|
|
131
|
+
jsonData.value = data
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error('Error loading JSON:', err)
|
|
135
|
+
|
|
136
|
+
if (err instanceof TypeError) {
|
|
137
|
+
error.value = 'network'
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
error.value = 'generic'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
jsonData.value = null
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
loading.value = false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onMounted(() => {
|
|
151
|
+
fetchJsonData()
|
|
152
|
+
})
|
|
153
|
+
</script>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<SimpleBanner
|
|
3
|
+
v-if="hasError"
|
|
4
|
+
type="warning"
|
|
5
|
+
class="flex items-center space-x-2"
|
|
6
|
+
>
|
|
7
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
8
|
+
<span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
|
|
9
|
+
</SimpleBanner>
|
|
10
|
+
<div
|
|
11
|
+
v-else
|
|
12
|
+
id="map"
|
|
13
|
+
ref="mapRef"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang = "ts">
|
|
18
|
+
import { onMounted, ref } from 'vue'
|
|
19
|
+
import { useI18n } from 'vue-i18n'
|
|
20
|
+
import { RiErrorWarningLine } from '@remixicon/vue'
|
|
21
|
+
|
|
22
|
+
import View from 'ol/View'
|
|
23
|
+
import Map from 'ol/Map'
|
|
24
|
+
import ScaleLine from 'ol/control/ScaleLine'
|
|
25
|
+
import TileLayer from 'ol/layer/Tile'
|
|
26
|
+
import OSM from 'ol/source/OSM'
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
CRS,
|
|
30
|
+
GeoportalAttribution,
|
|
31
|
+
GeoportalFullScreen,
|
|
32
|
+
GeoportalZoom,
|
|
33
|
+
LayerImport,
|
|
34
|
+
LayerSwitcher,
|
|
35
|
+
} from 'geopf-extensions-openlayers'
|
|
36
|
+
|
|
37
|
+
import SimpleBanner from '../SimpleBanner.vue'
|
|
38
|
+
import type { Resource } from '../../types/resources'
|
|
39
|
+
|
|
40
|
+
const props = defineProps<{ resource: Resource }>()
|
|
41
|
+
|
|
42
|
+
const { t } = useI18n()
|
|
43
|
+
|
|
44
|
+
let map = null
|
|
45
|
+
const mapRef = ref(0)
|
|
46
|
+
const hasError = ref(false)
|
|
47
|
+
|
|
48
|
+
async function displayMap() {
|
|
49
|
+
await import('ol/ol.css')
|
|
50
|
+
await import('@gouvfr/dsfr/dist/dsfr.css')
|
|
51
|
+
await import('@gouvfr/dsfr/dist/utility/icons/icons.css')
|
|
52
|
+
await import('geopf-extensions-openlayers/css/Dsfr.css')
|
|
53
|
+
|
|
54
|
+
CRS.load()
|
|
55
|
+
map = new Map({
|
|
56
|
+
target: mapRef.value,
|
|
57
|
+
layers: [
|
|
58
|
+
new TileLayer({
|
|
59
|
+
source: new OSM(),
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
view: new View({
|
|
63
|
+
center: [288074.8449901076, 6247982.515792289],
|
|
64
|
+
zoom: 8,
|
|
65
|
+
constrainResolution: true,
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const scaleControl = new ScaleLine({
|
|
70
|
+
units: 'metric',
|
|
71
|
+
bar: false,
|
|
72
|
+
})
|
|
73
|
+
map.addControl(scaleControl)
|
|
74
|
+
|
|
75
|
+
const fullscreen = new GeoportalFullScreen({
|
|
76
|
+
position: 'top-right',
|
|
77
|
+
})
|
|
78
|
+
map.addControl(fullscreen)
|
|
79
|
+
|
|
80
|
+
const zoom = new GeoportalZoom({
|
|
81
|
+
position: 'bottom-left',
|
|
82
|
+
})
|
|
83
|
+
map.addControl(zoom)
|
|
84
|
+
|
|
85
|
+
const layerSwitcher = new LayerSwitcher({
|
|
86
|
+
options: {
|
|
87
|
+
position: 'top-right',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
map.addControl(layerSwitcher)
|
|
91
|
+
|
|
92
|
+
const attributions = new GeoportalAttribution({
|
|
93
|
+
position: 'bottom-right',
|
|
94
|
+
})
|
|
95
|
+
map.addControl(attributions)
|
|
96
|
+
|
|
97
|
+
const layerImport = new LayerImport({
|
|
98
|
+
position: 'bottom-left',
|
|
99
|
+
listable: true,
|
|
100
|
+
layerTypes: ['WMS'],
|
|
101
|
+
})
|
|
102
|
+
layerImport._serviceUrlImportInput.value = props.resource.url
|
|
103
|
+
layerImport._formContainer.dispatchEvent(new CustomEvent('submit', { cancelable: true }))
|
|
104
|
+
|
|
105
|
+
map.addControl(layerImport)
|
|
106
|
+
|
|
107
|
+
// Wait for GetCapabilities to be called before trying to show layer
|
|
108
|
+
// TODO: use signal handling to know whether GetCapabilities failed or not
|
|
109
|
+
const waitTimeout = 500
|
|
110
|
+
let retry = 20
|
|
111
|
+
function showLayer() {
|
|
112
|
+
if (!layerImport._getCapResponseWMSLayers) {
|
|
113
|
+
retry--
|
|
114
|
+
if (retry > 0)
|
|
115
|
+
setTimeout(showLayer, waitTimeout)
|
|
116
|
+
else
|
|
117
|
+
hasError.value = true
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const layerInfo = layerImport._getCapResponseWMSLayers.filter(layer => layer.Name == props.resource.title)[0]
|
|
121
|
+
layerImport._addGetCapWMSLayer(layerInfo)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
setTimeout(showLayer, waitTimeout)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onMounted(() => {
|
|
128
|
+
displayMap()
|
|
129
|
+
})
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<style>
|
|
133
|
+
#map {
|
|
134
|
+
width: 100%;
|
|
135
|
+
height: 500px;
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
@@ -6,11 +6,10 @@ import CopyButton from '../CopyButton.vue'
|
|
|
6
6
|
import DescriptionDetails from '../DescriptionDetails.vue'
|
|
7
7
|
import DescriptionList from '../DescriptionList.vue'
|
|
8
8
|
import DescriptionTerm from '../DescriptionTerm.vue'
|
|
9
|
-
import {
|
|
9
|
+
import { useFormatDate } from '../../functions/dates'
|
|
10
10
|
import { filesize } from '../../functions/helpers'
|
|
11
11
|
import ExtraAccordion from '../ExtraAccordion.vue'
|
|
12
12
|
import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
|
|
13
|
-
import { useComponentsConfig } from '../../config'
|
|
14
13
|
|
|
15
14
|
const props = defineProps<{
|
|
16
15
|
resource: Resource
|
|
@@ -20,109 +19,89 @@ const hasExtras = computed(() => Object.keys(props.resource.extras).length)
|
|
|
20
19
|
const resourceTitleId = computed(() => getResourceTitleId(props.resource))
|
|
21
20
|
|
|
22
21
|
const { t } = useI18n()
|
|
23
|
-
const
|
|
22
|
+
const { formatDate } = useFormatDate()
|
|
24
23
|
</script>
|
|
25
24
|
|
|
26
25
|
<template>
|
|
27
26
|
<div>
|
|
28
|
-
<div class="flex gap-
|
|
29
|
-
<DescriptionList class="flex-1">
|
|
27
|
+
<div class="flex flex-wrap gap-12 flex-col md:flex-row overflow-hidden">
|
|
28
|
+
<DescriptionList class="flex-1 max-w-full">
|
|
30
29
|
<DescriptionTerm>
|
|
31
30
|
{{ t('URL') }}
|
|
32
31
|
<CopyButton
|
|
33
|
-
:label="$t('
|
|
34
|
-
:copied-label="$t('URL
|
|
32
|
+
:label="$t(`Copier l'URL`)"
|
|
33
|
+
:copied-label="$t('URL copiée !')"
|
|
35
34
|
:text="resource.url"
|
|
36
35
|
:aria-describedby="resourceTitleId"
|
|
37
36
|
/>
|
|
38
37
|
</DescriptionTerm>
|
|
39
38
|
<DescriptionDetails :with-ellipsis="false">
|
|
40
|
-
<code class="code">
|
|
41
|
-
<a :href="resource.url"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
:max-lines="1"
|
|
45
|
-
:autoresize="true"
|
|
46
|
-
:text="resource.url"
|
|
47
|
-
/></a>
|
|
39
|
+
<code class="code truncate p-1">
|
|
40
|
+
<a :href="resource.url">
|
|
41
|
+
{{ resource.url }}
|
|
42
|
+
</a>
|
|
48
43
|
</code>
|
|
49
44
|
</DescriptionDetails>
|
|
50
45
|
<DescriptionTerm>
|
|
51
|
-
{{ t('
|
|
46
|
+
{{ t('URL stable') }}
|
|
52
47
|
<CopyButton
|
|
53
|
-
:label="$t('
|
|
54
|
-
:copied-label="$t('
|
|
48
|
+
:label="$t(`Copier l'URL stable`)"
|
|
49
|
+
:copied-label="$t('URL stable copiée !')"
|
|
55
50
|
:text="resource.latest"
|
|
56
51
|
:aria-describedby="resourceTitleId"
|
|
57
52
|
/>
|
|
58
53
|
</DescriptionTerm>
|
|
59
54
|
<DescriptionDetails :with-ellipsis="false">
|
|
60
|
-
<code class="code">
|
|
61
|
-
<a :href="resource.latest"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
:max-lines="1"
|
|
65
|
-
:autoresize="true"
|
|
66
|
-
:text="resource.latest"
|
|
67
|
-
/></a>
|
|
55
|
+
<code class="code truncate p-1">
|
|
56
|
+
<a :href="resource.latest">
|
|
57
|
+
{{ resource.latest }}
|
|
58
|
+
</a>
|
|
68
59
|
</code>
|
|
69
60
|
</DescriptionDetails>
|
|
70
61
|
<DescriptionTerm>
|
|
71
|
-
{{ t('
|
|
62
|
+
{{ t('Identifiant') }}
|
|
72
63
|
<CopyButton
|
|
73
|
-
:label="$t(
|
|
74
|
-
:copied-label="$t('ID
|
|
64
|
+
:label="$t(`Copier l'identifiant`)"
|
|
65
|
+
:copied-label="$t('ID copié !')"
|
|
75
66
|
:text="resource.id"
|
|
76
67
|
:aria-describedby="resourceTitleId"
|
|
77
68
|
/>
|
|
78
69
|
</DescriptionTerm>
|
|
79
70
|
<DescriptionDetails :with-ellipsis="false">
|
|
80
|
-
<code class="code">
|
|
81
|
-
|
|
82
|
-
:is="config.textClamp"
|
|
83
|
-
v-if="config && config.textClamp"
|
|
84
|
-
:max-lines="1"
|
|
85
|
-
:autoresize="true"
|
|
86
|
-
:text="resource.id"
|
|
87
|
-
/>
|
|
71
|
+
<code class="code truncate p-1">
|
|
72
|
+
{{ resource.id }}
|
|
88
73
|
</code>
|
|
89
74
|
</DescriptionDetails>
|
|
90
75
|
<template v-if="resource.checksum">
|
|
91
76
|
<DescriptionTerm>
|
|
92
77
|
{{ resource.checksum.type }}
|
|
93
78
|
<CopyButton
|
|
94
|
-
:label="$t('
|
|
95
|
-
:copied-label="$t('
|
|
79
|
+
:label="$t('Copier la somme de contrôle')"
|
|
80
|
+
:copied-label="$t('Somme de contrôle copiée !')"
|
|
96
81
|
:text="resource.checksum.value"
|
|
97
82
|
:aria-describedby="resourceTitleId"
|
|
98
83
|
/>
|
|
99
84
|
</DescriptionTerm>
|
|
100
85
|
<DescriptionDetails :with-ellipsis="false">
|
|
101
|
-
<code class="code">
|
|
102
|
-
|
|
103
|
-
:is="config.textClamp"
|
|
104
|
-
v-if="config && config.textClamp"
|
|
105
|
-
:max-lines="1"
|
|
106
|
-
:autoresize="true"
|
|
107
|
-
:text="resource.checksum.value"
|
|
108
|
-
/>
|
|
86
|
+
<code class="code truncate p-1">
|
|
87
|
+
{{ resource.checksum.value }}
|
|
109
88
|
</code>
|
|
110
89
|
</DescriptionDetails>
|
|
111
90
|
</template>
|
|
112
91
|
</DescriptionList>
|
|
113
92
|
<DescriptionList style="flex-shrink: 0;">
|
|
114
|
-
<DescriptionTerm>{{ t('
|
|
93
|
+
<DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
|
|
115
94
|
<DescriptionDetails>
|
|
116
95
|
{{ formatDate(resource.created_at) }}
|
|
117
96
|
</DescriptionDetails>
|
|
118
|
-
<DescriptionTerm>{{ t('
|
|
97
|
+
<DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
|
|
119
98
|
<DescriptionDetails>
|
|
120
99
|
{{ formatDate(resource.last_modified) }}
|
|
121
100
|
</DescriptionDetails>
|
|
122
101
|
</DescriptionList>
|
|
123
102
|
<DescriptionList style="flex-shrink: 0;">
|
|
124
103
|
<template v-if="resource.filesize">
|
|
125
|
-
<DescriptionTerm>{{ t('
|
|
104
|
+
<DescriptionTerm>{{ t('Taille') }}</DescriptionTerm>
|
|
126
105
|
<DescriptionDetails>
|
|
127
106
|
{{ filesize(resource.filesize) }}
|
|
128
107
|
</DescriptionDetails>
|
|
@@ -134,9 +113,9 @@ const config = useComponentsConfig()
|
|
|
134
113
|
</DescriptionDetails>
|
|
135
114
|
</template>
|
|
136
115
|
<template v-if="resource.mime">
|
|
137
|
-
<DescriptionTerm>{{ t('MIME
|
|
116
|
+
<DescriptionTerm>{{ t('Type MIME') }}</DescriptionTerm>
|
|
138
117
|
<DescriptionDetails>
|
|
139
|
-
<code class="code
|
|
118
|
+
<code class="code truncate">{{ resource.mime }}</code>
|
|
140
119
|
</DescriptionDetails>
|
|
141
120
|
</template>
|
|
142
121
|
</DescriptionList>
|
|
@@ -145,8 +124,8 @@ const config = useComponentsConfig()
|
|
|
145
124
|
<ExtraAccordion
|
|
146
125
|
v-if="hasExtras"
|
|
147
126
|
class="pt-6 mt-6 border-top border-gray-default"
|
|
148
|
-
:button-text="t('
|
|
149
|
-
:title-text="t('
|
|
127
|
+
:button-text="t('Voir les extras')"
|
|
128
|
+
:title-text="t('Extras de la ressource')"
|
|
150
129
|
title-level="h5"
|
|
151
130
|
:extra="resource.extras"
|
|
152
131
|
/>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-xs">
|
|
3
|
+
<div v-if="pdfData">
|
|
4
|
+
<!--
|
|
5
|
+
We use props.resource.url instead of props.resource.latest
|
|
6
|
+
because the PDF component raises an error otherwise.
|
|
7
|
+
See https://github.com/datagouv/cdata/pull/611
|
|
8
|
+
-->
|
|
9
|
+
<PDF
|
|
10
|
+
:src="props.resource.url"
|
|
11
|
+
:show-progress="true"
|
|
12
|
+
progress-color="#0063cb"
|
|
13
|
+
:show-page-tooltip="true"
|
|
14
|
+
:show-back-to-top-btn="true"
|
|
15
|
+
:scroll-threshold="300"
|
|
16
|
+
pdf-width="100%"
|
|
17
|
+
:row-gap="12"
|
|
18
|
+
:use-system-fonts="true"
|
|
19
|
+
:disable-range="false"
|
|
20
|
+
:disable-stream="false"
|
|
21
|
+
:disable-auto-fetch="false"
|
|
22
|
+
class="w-full"
|
|
23
|
+
@on-progress="handleProgress"
|
|
24
|
+
@on-complete="handleComplete"
|
|
25
|
+
@on-page-change="handlePageChange"
|
|
26
|
+
@on-pdf-init="handlePdfInit"
|
|
27
|
+
@on-error="handleError"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div
|
|
31
|
+
v-else-if="loading"
|
|
32
|
+
class="text-gray-medium"
|
|
33
|
+
>
|
|
34
|
+
{{ $t("Chargement de l'aperçu PDF...") }}
|
|
35
|
+
</div>
|
|
36
|
+
<SimpleBanner
|
|
37
|
+
v-else-if="fileTooLarge"
|
|
38
|
+
type="warning"
|
|
39
|
+
class="flex items-center space-x-2"
|
|
40
|
+
>
|
|
41
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
42
|
+
<span>{{ fileSizeBytes
|
|
43
|
+
? $t("Fichier PDF trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
|
|
44
|
+
: $t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
|
|
45
|
+
}}</span>
|
|
46
|
+
</SimpleBanner>
|
|
47
|
+
<SimpleBanner
|
|
48
|
+
v-else-if="error === 'network'"
|
|
49
|
+
type="warning"
|
|
50
|
+
class="flex items-center space-x-2"
|
|
51
|
+
>
|
|
52
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
53
|
+
<span>{{ $t("Ce fichier PDF ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
|
|
54
|
+
</SimpleBanner>
|
|
55
|
+
<SimpleBanner
|
|
56
|
+
v-else-if="error"
|
|
57
|
+
type="warning"
|
|
58
|
+
class="flex items-center space-x-2"
|
|
59
|
+
>
|
|
60
|
+
<RiErrorWarningLine class="shink-0 size-6" />
|
|
61
|
+
<span>{{ $t("Erreur lors du chargement de l'aperçu PDF. Pour consulter le fichier, téléchargez-le depuis l'onglet Téléchargements.") }}</span>
|
|
62
|
+
</SimpleBanner>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
|
68
|
+
import { RiErrorWarningLine } from '@remixicon/vue'
|
|
69
|
+
import SimpleBanner from '../SimpleBanner.vue'
|
|
70
|
+
import { useComponentsConfig } from '../../config'
|
|
71
|
+
import type { Resource } from '../../types/resources'
|
|
72
|
+
|
|
73
|
+
const PDF = defineAsyncComponent(() =>
|
|
74
|
+
import('pdf-vue3').then((module) => {
|
|
75
|
+
return module.default
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const props = defineProps<{
|
|
80
|
+
resource: Resource
|
|
81
|
+
}>()
|
|
82
|
+
|
|
83
|
+
const config = useComponentsConfig()
|
|
84
|
+
|
|
85
|
+
const pdfData = ref<boolean>(false)
|
|
86
|
+
const loading = ref(false)
|
|
87
|
+
const error = ref<string | null>(null)
|
|
88
|
+
const fileTooLarge = ref(false)
|
|
89
|
+
|
|
90
|
+
const fileSizeBytes = computed(() => {
|
|
91
|
+
// Check if resource has filesize
|
|
92
|
+
if (props.resource.filesize) {
|
|
93
|
+
return props.resource.filesize
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if resource has content-length in extras (from API metadata)
|
|
97
|
+
const contentLength = props.resource.extras?.['analysis:content-length']
|
|
98
|
+
if (contentLength && typeof contentLength === 'number') {
|
|
99
|
+
return contentLength
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const shouldLoadPdf = computed(() => {
|
|
106
|
+
const size = fileSizeBytes.value
|
|
107
|
+
if (!size) {
|
|
108
|
+
// If we don't know the size, don't risk loading a potentially huge file
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Use maxPdfPreviewSize from config, fallback to 10 MB if not set
|
|
113
|
+
const maxSizeBytes = config.maxPdfPreviewSize ?? 10_000_000
|
|
114
|
+
return size <= maxSizeBytes
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const loadPdf = async () => {
|
|
118
|
+
// Check if file is too large or size is unknown before loading
|
|
119
|
+
if (!shouldLoadPdf.value) {
|
|
120
|
+
fileTooLarge.value = true
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
loading.value = true
|
|
125
|
+
error.value = null
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Test if the PDF URL is accessible
|
|
129
|
+
const response = await fetch(props.resource.url, { method: 'HEAD' })
|
|
130
|
+
// const response = await fetch('/test-data.pdf') // For testing locally without CORS issues
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If the URL is accessible, set pdfData to true
|
|
136
|
+
// The PDF component will handle the actual loading
|
|
137
|
+
pdfData.value = true
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error('Error testing PDF URL:', err)
|
|
141
|
+
|
|
142
|
+
if (err instanceof TypeError) {
|
|
143
|
+
error.value = 'network'
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
error.value = 'generic'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pdfData.value = false
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
loading.value = false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Event handlers for PDF component
|
|
157
|
+
const handleProgress = (loadRatio: number) => {
|
|
158
|
+
console.log(`PDF loading progress: ${loadRatio}%`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handleComplete = () => {
|
|
162
|
+
console.log('PDF download completed')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const handlePageChange = (page: number) => {
|
|
166
|
+
console.log(`PDF page changed to: ${page}`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handlePdfInit = (pdf: unknown) => {
|
|
170
|
+
console.log('PDF initialized:', pdf)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handleError = (err: unknown) => {
|
|
174
|
+
console.error('PDF loading error:', err)
|
|
175
|
+
|
|
176
|
+
if (err instanceof TypeError) {
|
|
177
|
+
error.value = 'network'
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
error.value = 'generic'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pdfData.value = false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onMounted(() => {
|
|
187
|
+
loadPdf()
|
|
188
|
+
})
|
|
189
|
+
</script>
|