@datagouv/components-next 0.0.1

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 (119) hide show
  1. package/README.md +150 -0
  2. package/assets/main.css +136 -0
  3. package/assets/placeholders/author.png +0 -0
  4. package/assets/placeholders/dataset.png +0 -0
  5. package/assets/placeholders/news.png +0 -0
  6. package/assets/placeholders/organization.png +0 -0
  7. package/assets/placeholders/reuse.png +0 -0
  8. package/assets/tailwind.config.js +24 -0
  9. package/dist/components.css +2 -0
  10. package/dist/locales/de.js +155 -0
  11. package/dist/locales/en.js +155 -0
  12. package/dist/locales/es.js +155 -0
  13. package/dist/locales/fr.js +155 -0
  14. package/dist/locales/it.js +155 -0
  15. package/dist/locales/pt.js +155 -0
  16. package/dist/locales/sr.js +155 -0
  17. package/package.json +72 -0
  18. package/src/components/AppLink.vue +51 -0
  19. package/src/components/Avatar.vue +27 -0
  20. package/src/components/AvatarWithName.vue +26 -0
  21. package/src/components/BannerAction.vue +39 -0
  22. package/src/components/BrandedButton.vue +170 -0
  23. package/src/components/CopyButton.vue +84 -0
  24. package/src/components/DataserviceCard.vue +184 -0
  25. package/src/components/DatasetCard.vue +198 -0
  26. package/src/components/DatasetInformationPanel.vue +210 -0
  27. package/src/components/DatasetQuality.vue +68 -0
  28. package/src/components/DatasetQualityInline.vue +32 -0
  29. package/src/components/DatasetQualityItem.vue +32 -0
  30. package/src/components/DatasetQualityItemWarning.vue +21 -0
  31. package/src/components/DatasetQualityScore.vue +35 -0
  32. package/src/components/DatasetQualityTooltipContent.vue +79 -0
  33. package/src/components/DescriptionDetails.vue +23 -0
  34. package/src/components/DescriptionList/DescriptionDetails.stories.ts +43 -0
  35. package/src/components/DescriptionList/DescriptionList.stories.ts +47 -0
  36. package/src/components/DescriptionList/DescriptionTerm.stories.ts +28 -0
  37. package/src/components/DescriptionList.vue +8 -0
  38. package/src/components/DescriptionTerm.vue +8 -0
  39. package/src/components/ExtraAccordion.vue +78 -0
  40. package/src/components/Icons/Archive.vue +21 -0
  41. package/src/components/Icons/Code.vue +21 -0
  42. package/src/components/Icons/Documentation.vue +21 -0
  43. package/src/components/Icons/File.vue +21 -0
  44. package/src/components/Icons/Image.vue +7 -0
  45. package/src/components/Icons/Link.vue +21 -0
  46. package/src/components/Icons/Table.vue +21 -0
  47. package/src/components/OrganizationCard.vue +68 -0
  48. package/src/components/OrganizationNameWithCertificate.vue +45 -0
  49. package/src/components/OwnerType.vue +43 -0
  50. package/src/components/OwnerTypeIcon.vue +18 -0
  51. package/src/components/Pagination.vue +205 -0
  52. package/src/components/Placeholder.vue +29 -0
  53. package/src/components/ReadMore.vue +107 -0
  54. package/src/components/ResourceAccordion/DataStructure.vue +87 -0
  55. package/src/components/ResourceAccordion/EditButton.vue +34 -0
  56. package/src/components/ResourceAccordion/Metadata.vue +171 -0
  57. package/src/components/ResourceAccordion/Preview.vue +229 -0
  58. package/src/components/ResourceAccordion/PreviewLoader.vue +148 -0
  59. package/src/components/ResourceAccordion/ResourceAccordion.vue +484 -0
  60. package/src/components/ResourceAccordion/ResourceIcon.vue +16 -0
  61. package/src/components/ResourceAccordion/SchemaBadge.vue +148 -0
  62. package/src/components/ResourceAccordion/SchemaLoader.vue +30 -0
  63. package/src/components/ResourceAccordion/Swagger.vue +46 -0
  64. package/src/components/ResourceAccordion/france.svg +1 -0
  65. package/src/components/ReuseCard.vue +106 -0
  66. package/src/components/ReuseDetails.vue +45 -0
  67. package/src/components/SimpleBanner.vue +24 -0
  68. package/src/components/SmallChart.vue +149 -0
  69. package/src/components/StatBox.vue +100 -0
  70. package/src/components/Tabs/Tab.vue +62 -0
  71. package/src/components/Tabs/TabGroup.vue +20 -0
  72. package/src/components/Tabs/TabList.vue +15 -0
  73. package/src/components/Tabs/TabPanel.vue +7 -0
  74. package/src/components/Tabs/TabPanels.vue +7 -0
  75. package/src/components/Toggletip.vue +62 -0
  76. package/src/components/ToggletipButton.vue +14 -0
  77. package/src/composables/useActiveDescendant.ts +103 -0
  78. package/src/composables/useReuseType.ts +14 -0
  79. package/src/config.ts +33 -0
  80. package/src/functions/api.ts +96 -0
  81. package/src/functions/api.types.ts +41 -0
  82. package/src/functions/config.ts +12 -0
  83. package/src/functions/datasets.ts +24 -0
  84. package/src/functions/dates.ts +85 -0
  85. package/src/functions/helpers.ts +38 -0
  86. package/src/functions/markdown.ts +47 -0
  87. package/src/functions/matomo.ts +3 -0
  88. package/src/functions/organizations.ts +85 -0
  89. package/src/functions/owned.ts +11 -0
  90. package/src/functions/resources.ts +99 -0
  91. package/src/functions/reuses.ts +28 -0
  92. package/src/functions/schemas.ts +96 -0
  93. package/src/functions/tabularApi.ts +27 -0
  94. package/src/functions/users.ts +7 -0
  95. package/src/locales/de.json +154 -0
  96. package/src/locales/en.json +154 -0
  97. package/src/locales/es.json +154 -0
  98. package/src/locales/fr.json +154 -0
  99. package/src/locales/it.json +154 -0
  100. package/src/locales/pt.json +154 -0
  101. package/src/locales/sr.json +154 -0
  102. package/src/main.ts +147 -0
  103. package/src/types/badges.ts +5 -0
  104. package/src/types/contact_point.ts +7 -0
  105. package/src/types/dataservices.ts +68 -0
  106. package/src/types/datasets.ts +80 -0
  107. package/src/types/frequency.ts +6 -0
  108. package/src/types/granularity.ts +6 -0
  109. package/src/types/harvest.ts +3 -0
  110. package/src/types/keyboard.ts +1 -0
  111. package/src/types/licenses.ts +9 -0
  112. package/src/types/organizations.ts +41 -0
  113. package/src/types/owned.ts +9 -0
  114. package/src/types/resources.ts +37 -0
  115. package/src/types/reuses.ts +49 -0
  116. package/src/types/site.ts +23 -0
  117. package/src/types/topics.ts +20 -0
  118. package/src/types/ui.ts +3 -0
  119. package/src/types/users.ts +10 -0
@@ -0,0 +1,484 @@
1
+ <template>
2
+ <div
3
+ class="border border-gray-default"
4
+ :class="{ 'fr-pb-4v': open }"
5
+ >
6
+ <header
7
+ :id="resourceHeaderId"
8
+ class="fr-p-4v flex items-center justify-between relative"
9
+ >
10
+ <div>
11
+ <div class="flex items-center fr-mb-1v">
12
+ <h4
13
+ :id="resourceTitleId"
14
+ class="fr-m-0"
15
+ >
16
+ <button
17
+ type="button"
18
+ class="fr-p-0 flex items-baseline text-base leading-none font-normal"
19
+ data-testid="expand-button"
20
+ :aria-expanded="open"
21
+ @click="toggle"
22
+ >
23
+ <ResourceIcon
24
+ :resource
25
+ class="size-3.5 mr-1"
26
+ />
27
+ <span
28
+ :class="{
29
+ 'font-bold': open,
30
+ }"
31
+ ><component
32
+ :is="config.textClamp"
33
+ v-if="config && config.textClamp"
34
+ :max-lines="1"
35
+ :text="resource.title || t('Nameless file')"
36
+ /></span>
37
+
38
+ <span class="absolute inset-0 z-1" />
39
+ </button>
40
+ </h4>
41
+ <CopyButton
42
+ :label="$t('Copy link')"
43
+ :copied-label="$t('Link copied!')"
44
+ :text="resourceExternalUrl"
45
+ class="z-2"
46
+ />
47
+ </div>
48
+ <div class="text-gray-medium subheaders-infos">
49
+ <SchemaBadge
50
+ :resource
51
+ class="dash-after"
52
+ />
53
+ <span class="fr-text--xs fr-mb-0 dash-after">{{ t('Updated {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
54
+ <span
55
+ v-if="resource.format"
56
+ class="fr-text--xs fr-mb-0 dash-after"
57
+ >
58
+ <span class="hidden show-on-small">{{ t("Format") }}</span>
59
+ {{ resource.format.trim().toLowerCase() }}
60
+ <span v-if="resource.filesize">({{ filesize(resource.filesize) }})</span>
61
+ </span>
62
+ <span
63
+ class="inline-flex items-center fr-text--xs fr-mb-0"
64
+ :aria-label="t('{n} downloads', resource.metrics.views)"
65
+ >
66
+ <span class="fr-icon-download-line fr-icon--xs fr-mr-1v" />
67
+ <span>{{ summarize(resource.metrics.views) }} <span class="hidden show-on-small">{{ t("downloads") }}</span></span>
68
+ </span>
69
+ </div>
70
+ <p
71
+ v-if="communityResource"
72
+ class="fr-mb-0 fr-mt-1v fr-text--xs text-gray-medium"
73
+ >
74
+ {{ t('From') }}
75
+ <a
76
+ v-if="communityResource.organization"
77
+ class="fr-link fr-text--xs"
78
+ :href="communityResource.organization.page"
79
+ >
80
+ <OrganizationNameWithCertificate :organization="communityResource.organization" />
81
+ </a>
82
+ <template v-else-if="owner">
83
+ {{ owner }}
84
+ </template>
85
+ </p>
86
+ </div>
87
+ <div class="flex items-center fr-ml-4v buttons">
88
+ <p
89
+ v-if="unavailable"
90
+ class="text-default-warning fr-m-0 fr-mr-2v"
91
+ >
92
+ {{ t('Unavailable') }}
93
+ </p>
94
+ <p
95
+ v-if="resource.format === 'url'"
96
+ class="fr-col-auto fr-ml-3v fr-m-0 z-2"
97
+ >
98
+ <BrandedButton
99
+ :href="resource.latest"
100
+ :title="t('File link - opens a new window')"
101
+ :aria-describedby="resourceTitleId"
102
+ rel="ugc nofollow noopener"
103
+ new-tab
104
+ size="xs"
105
+ >
106
+ {{ $t('Visit') }}
107
+ </BrandedButton>
108
+ </p>
109
+ <p
110
+ v-else-if="ogcService"
111
+ class="fr-col-auto fr-ml-3v fr-m-0 z-2"
112
+ >
113
+ <BrandedButton
114
+ :id="resource.id + '-copy'"
115
+ :data-clipboard-text="resource.url"
116
+ :aria-describedby="resourceTitleId"
117
+ color="primary"
118
+ size="xs"
119
+ :icon="RiFileCopyLine"
120
+ >
121
+ {{ t('Copy link') }}
122
+ </BrandedButton>
123
+ </p>
124
+ <p
125
+ v-else
126
+ class="fr-col-auto fr-ml-3v fr-m-0"
127
+ >
128
+ <BrandedButton
129
+ :href="resource.latest"
130
+ rel="ugc nofollow noopener"
131
+ :title="t('Download file')"
132
+ download
133
+ class="relative text-transform-uppercase matomo_download z-2"
134
+ :icon="RiDownloadLine"
135
+ size="xs"
136
+ :aria-describedby="resourceTitleId"
137
+ >
138
+ <span class="sr-only">{{ t('Download file as ') }}</span>{{ format }}
139
+ </BrandedButton>
140
+ </p>
141
+ <p
142
+ v-if="canEdit"
143
+ class="fr-col-auto fr-ml-3v fr-m-0 z-2"
144
+ >
145
+ <EditButton
146
+ :dataset-id="dataset.id"
147
+ :resource-id="resource.id"
148
+ :is-community-resource="isCommunityResource"
149
+ />
150
+ </p>
151
+ <div
152
+ class="fr-icon--sm fr-ml-4v"
153
+ :class="{ 'fr-icon-arrow-up-s-line': open, 'fr-icon-arrow-down-s-line': !open }"
154
+ />
155
+ </div>
156
+ </header>
157
+ <section
158
+ v-if="open"
159
+ :id="resourceContentId"
160
+ :aria-labelledby="resourceTitleId"
161
+ >
162
+ <TabGroup
163
+ size="sm"
164
+ @change="switchTab"
165
+ >
166
+ <div class="fr-pl-4v fr-pr-4v fr-pb-4v">
167
+ <TabList style="max-width: 100%; overflow-y: auto;">
168
+ <Tab
169
+ v-for="tab in tabsOptions"
170
+ :key="tab.key"
171
+ >
172
+ {{ tab.label }}
173
+ </Tab>
174
+ </TabList>
175
+ </div>
176
+ <TabPanels>
177
+ <TabPanel
178
+ v-for="tab in tabsOptions"
179
+ :key="tab.key"
180
+ >
181
+ <div v-if="tab.key === 'data'">
182
+ <Preview :resource="resource" />
183
+ </div>
184
+ <div
185
+ v-if="tab.key === 'description'"
186
+ class="fr-pl-4v fr-pr-4v"
187
+ >
188
+ <div
189
+ class="fr-mt-0 markdown fr-text--sm text-mention-grey"
190
+ v-html="markdown(resource.description || '')"
191
+ />
192
+ </div>
193
+ <div
194
+ v-if="tab.key === 'data-structure'"
195
+ class="fr-pl-4v fr-pr-4v"
196
+ >
197
+ <DataStructure
198
+ v-if="hasPreview"
199
+ :resource="resource"
200
+ />
201
+ </div>
202
+ <div
203
+ v-if="tab.key === 'metadata'"
204
+ class="fr-pl-4v fr-pr-4v"
205
+ >
206
+ <Metadata :resource />
207
+ </div>
208
+ <div
209
+ v-if="tab.key === 'downloads'"
210
+ class="fr-pl-4v fr-pr-4v"
211
+ >
212
+ <dl class="fr-pl-0">
213
+ <dt
214
+ v-if="resource.format === 'url'"
215
+ class="font-bold fr-text--sm fr-mb-0"
216
+ >
217
+ {{ $t('Original URL') }}
218
+ </dt>
219
+ <dt
220
+ v-else
221
+ class="font-bold fr-text--sm fr-mb-0"
222
+ >
223
+ {{ $t('Original format') }}
224
+ </dt>
225
+ <dd class="text-sm ml-0 mt-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
226
+ <span v-if="resource.format === 'url'">
227
+ <a
228
+ :href="resource.latest"
229
+ class="fr-link no-icon-after"
230
+ rel="ugc nofollow noopener"
231
+ target="_blank"
232
+ >
233
+ <component
234
+ :is="config.textClamp"
235
+ v-if="config && config.textClamp"
236
+ :auto-resize="true"
237
+ :max-lines="1"
238
+ :text="resource.url"
239
+ >
240
+ <template #after>
241
+ <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm" />
242
+ </template>
243
+ </component>
244
+ </a>
245
+ </span>
246
+ <span v-else>
247
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
248
+ <a
249
+ :href="resource.latest"
250
+ class="fr-link"
251
+ rel="ugc nofollow noopener"
252
+ >
253
+ <span>{{ $t('Format {format}', { format: resource.format }) }}<span v-if="resource.filesize"> - {{ filesize(resource.filesize) }}</span></span>
254
+ </a>
255
+ </span>
256
+ <CopyButton
257
+ :label="$t('Copy link')"
258
+ :copied-label="$t('Link copied!')"
259
+ :text="resource.latest"
260
+ class="relative"
261
+ />
262
+ </dd>
263
+ <template v-if="resource.extras['analysis:parsing:parquet_url']">
264
+ <dt class="font-bold fr-text--sm fr-mb-0">
265
+ {{ $t('Auto-generated formats from {platform}', { platform: config.name }) }}
266
+ </dt>
267
+ <dd class="text-sm ml-0 mt-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
268
+ <span>
269
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
270
+ <a
271
+ :href="resource.extras['analysis:parsing:parquet_url']"
272
+ class="fr-link"
273
+ rel="ugc nofollow noopener"
274
+ >
275
+ <span>{{ $t('Format {format}', { format: 'parquet' }) }}<span v-if="resource.extras['analysis:parsing:parquet_size']"> - {{ filesize(resource.extras['analysis:parsing:parquet_size']) }}</span></span>
276
+ </a>
277
+ </span>
278
+ <CopyButton
279
+ :label="$t('Copy link')"
280
+ :copied-label="$t('Link copied!')"
281
+ :text="resource.extras['analysis:parsing:parquet_url']"
282
+ class="relative"
283
+ />
284
+ </dd>
285
+ </template>
286
+ </dl>
287
+ </div>
288
+ <div
289
+ v-if="tab.key === 'swagger'"
290
+ class="fr-pl-4v fr-pr-4v"
291
+ >
292
+ <div>{{ t('Swagger automatically generated by data.gouv.fr. This swagger allows you to query data by API by filtering it by column value.') }}</div>
293
+ <Swagger
294
+ v-if="hasPreview"
295
+ :url="`${config.tabularApiUrl}/api/resources/${props.resource.id}/swagger/`"
296
+ />
297
+ </div>
298
+ </TabPanel>
299
+ </TabPanels>
300
+ </TabGroup>
301
+ </section>
302
+ </div>
303
+ </template>
304
+
305
+ <script setup lang="ts">
306
+ import { ref, computed, defineAsyncComponent } from 'vue'
307
+ import { useI18n } from 'vue-i18n'
308
+ import { RiDownloadLine, RiFileCopyLine } from '@remixicon/vue'
309
+ import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
310
+ import { filesize, summarize } from '../../functions/helpers'
311
+ import { markdown } from '../../functions/markdown'
312
+ import { formatRelativeIfRecentDate } from '../../functions/dates'
313
+ import type { CommunityResource, Resource } from '../../types/resources'
314
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
315
+ import TabGroup from '../Tabs/TabGroup.vue'
316
+ import TabList from '../Tabs/TabList.vue'
317
+ import Tab from '../Tabs/Tab.vue'
318
+ import TabPanels from '../Tabs/TabPanels.vue'
319
+ import TabPanel from '../Tabs/TabPanel.vue'
320
+ import { trackEvent } from '../../functions/matomo'
321
+ import CopyButton from '../CopyButton.vue'
322
+ import { useComponentsConfig } from '../../config'
323
+ import { getOwnerName } from '../../functions/owned'
324
+ import { getResourceFormatIcon, getResourceTitleId } from '../../functions/resources'
325
+ import BrandedButton from '../BrandedButton.vue'
326
+ import { getResourceExternalUrl } from '../../functions/datasets'
327
+ import Metadata from './Metadata.vue'
328
+ import SchemaBadge from './SchemaBadge.vue'
329
+ import ResourceIcon from './ResourceIcon.vue'
330
+ import EditButton from './EditButton.vue'
331
+ import DataStructure from './DataStructure.vue'
332
+ import Preview from './Preview.vue'
333
+
334
+ const OGC_SERVICES_FORMATS = ['ogc:wfs', 'ogc:wms', 'wfs', 'wms']
335
+
336
+ const props = withDefaults(defineProps<{
337
+ dataset: Dataset | DatasetV2
338
+ expandedOnMount?: boolean
339
+ isCommunityResource?: boolean
340
+ resource: Resource | CommunityResource
341
+ canEdit?: boolean
342
+ }>(), {
343
+ expandedOnMount: false,
344
+ isCommunityResource: false,
345
+ canEdit: false,
346
+ })
347
+
348
+ const config = useComponentsConfig()
349
+
350
+ const Swagger = defineAsyncComponent(() => import('./Swagger.vue'))
351
+
352
+ const { t } = useI18n()
353
+
354
+ const hasPreview = computed(() => {
355
+ return config.tabularApiUrl
356
+ && props.resource.extras['analysis:parsing:finished_at']
357
+ && !props.resource.extras['analysis:parsing:error']
358
+ && (config.tabularAllowRemote || props.resource.filetype === 'file')
359
+ })
360
+
361
+ const format = computed(() => getResourceFormatIcon(props.resource.format) ? props.resource.format : t('File'))
362
+
363
+ const ogcService = computed(() => OGC_SERVICES_FORMATS.includes(props.resource.format))
364
+
365
+ const open = ref(props.expandedOnMount)
366
+ const toggle = () => {
367
+ open.value = !open.value
368
+
369
+ if (open.value) {
370
+ trackEvent(['Open resource', props.resource.id])
371
+ }
372
+ else {
373
+ trackEvent(['Close resource', props.resource.id])
374
+ }
375
+ }
376
+
377
+ const tabsOptions = computed(() => {
378
+ const options = []
379
+
380
+ if (hasPreview.value) {
381
+ options.push({ key: 'data', label: t('Data') })
382
+ }
383
+
384
+ if (props.resource.description) {
385
+ options.push({ key: 'description', label: t('Description') })
386
+ }
387
+
388
+ if (hasPreview.value) {
389
+ options.push({ key: 'data-structure', label: t('Data structure') })
390
+ }
391
+
392
+ options.push({ key: 'metadata', label: t('Metadata') })
393
+ options.push({ key: 'downloads', label: t('Downloads') })
394
+
395
+ if (hasPreview.value) {
396
+ options.push({ key: 'swagger', label: t('Swagger') })
397
+ }
398
+
399
+ return options
400
+ })
401
+ const switchTab = (index: number) => {
402
+ const option = tabsOptions.value[index]
403
+ trackEvent(['View resource tab', props.resource.id, option.label])
404
+
405
+ if (option.key === 'data') {
406
+ trackEvent(['Show preview', props.resource.id])
407
+ }
408
+ if (option.key === 'data-structure') {
409
+ trackEvent(['Show data structure', props.resource.id])
410
+ }
411
+ }
412
+
413
+ const communityResource = computed<CommunityResource | null>(() => {
414
+ if (!props.isCommunityResource) return null
415
+ return props.resource as CommunityResource
416
+ })
417
+ const owner = computed(() => communityResource.value ? getOwnerName(communityResource.value) : null)
418
+
419
+ const lastUpdate = props.resource.last_modified
420
+ const availabilityChecked = props.resource.extras && 'check:available' in props.resource.extras
421
+ const unavailable = availabilityChecked && props.resource.extras['check:available'] === false
422
+
423
+ const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
424
+
425
+ const resourceContentId = 'resource-' + props.resource.id
426
+ const resourceHeaderId = 'resource-' + props.resource.id + '-header'
427
+ const resourceTitleId = getResourceTitleId(props.resource)
428
+ </script>
429
+
430
+ <style scoped>
431
+ .fr-link--no-after::after {
432
+ display: none !important;
433
+ }
434
+
435
+ header:hover {
436
+ background-color: #f6f6f6;
437
+ }
438
+
439
+ /**
440
+ If we do not put z-index, the header is fully clickable except for the DSFR icons (bad because one of the icons is the chevron up/down). It may be due to the usage of ::before to add the icon in the markup or the `mask-image`. We need to put a `z-2` on all elements that we want to be clickable over the header.
441
+ */
442
+ .z-1 {
443
+ z-index: 1;
444
+ }
445
+ .z-2 {
446
+ z-index: 2;
447
+ }
448
+
449
+ .z-3 {
450
+ z-index: 3;
451
+ }
452
+
453
+ article {
454
+ container-type: inline-size;
455
+ }
456
+
457
+ @container (max-width: 600px) {
458
+ article header.flex {
459
+ flex-direction: column;
460
+ align-items: start;
461
+ justify-content: start;
462
+ }
463
+ article header .buttons {
464
+ margin-top: 1.25rem;
465
+ margin-left: auto !important;
466
+ }
467
+ /*
468
+ If we want to put subheaders info in column on mobile…
469
+ article header .subheaders-infos {
470
+ display: flex;
471
+ flex-direction: column
472
+ }
473
+ article header .subheaders-infos .hidden.show-on-small {
474
+ display: inline !important;
475
+ }
476
+ article header .dash-after::after {
477
+ content: ''
478
+ } */
479
+
480
+ /* article .fr-pl-4v fr-pr-4v {
481
+ padding: 0.75rem !important;
482
+ } */
483
+ }
484
+ </style>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <component
3
+ :is="(resource.filetype === 'remote' && resource.format ? getResourceFormatIcon(resource.format) : null) || File"
4
+ class="text-gray-800 shrink-0"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import type { ResourceFileType } from '../../types/resources'
10
+ import File from '../Icons/File.vue'
11
+ import { getResourceFormatIcon } from '../../functions/resources'
12
+
13
+ defineProps<{
14
+ resource: { format?: string | null, filetype: ResourceFileType | null }
15
+ }>()
16
+ </script>
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <span
3
+ v-if="title"
4
+ class="inline-flex fr-mb-0 align-items-baseline fr-text--xs"
5
+ >
6
+ <Toggletip
7
+ position="right"
8
+ no-margin
9
+ class="relative z-2"
10
+ >
11
+ <template #toggletip="{ close }">
12
+ <div class="flex justify-between border-bottom">
13
+ <h5 class="fr-text--sm fr-my-0 fr-p-2v">{{ $t("Data schema") }}</h5>
14
+ <button
15
+ type="button"
16
+ :title="t('Close')"
17
+ class="border-left close-button flex items-center justify-center"
18
+ @click="close"
19
+ >&times;</button>
20
+ </div>
21
+ <div class="fr-p-3v">
22
+ <div v-if="validataStatus === 'ok'">
23
+ {{ t("This file is valid for the shema:") }} <component
24
+ :is="documentationUrl ? 'a' : 'span'"
25
+ :href="documentationUrl"
26
+ class="fr-link fr-text--sm"
27
+ >{{ title }}</component>.
28
+ </div>
29
+ <div v-if="validataStatus === 'warnings'">
30
+ {{ t("This file is valid for the shema:") }} <component
31
+ :is="documentationUrl ? 'a' : 'span'"
32
+ :href="documentationUrl"
33
+ class="fr-link fr-text--sm"
34
+ >{{ title }}</component>. {{ t("But its compliance could be improved.") }}
35
+ </div>
36
+ <div v-if="validataStatus === 'ko'">
37
+ {{ t("This file indicates to follow the schema:") }} <component
38
+ :is="documentationUrl ? 'a' : 'span'"
39
+ :href="documentationUrl"
40
+ class="fr-link fr-text--sm"
41
+ >{{ title }}</component>. {{ t("But is not compliant.") }}
42
+ </div>
43
+
44
+ <div
45
+ v-if="validataWarnings.length"
46
+ class="text-default-warning flex items-center fr-mt-4v"
47
+ >
48
+ <span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
49
+ <span>{{ validataWarnings.length }} {{ t('advices') }}</span>
50
+ </div>
51
+ <div
52
+ v-if="validataStructureErrors.length"
53
+ class="text-default-warning flex items-center fr-mt-4v"
54
+ >
55
+ <span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
56
+ <span>{{ validataStructureErrors.length }} {{ t('structure errors') }}</span>
57
+ </div>
58
+ <div
59
+ v-if="validataBodyErrors.length"
60
+ class="text-default-warning flex items-center fr-mt-4v"
61
+ >
62
+ <span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
63
+ <span>{{ validataBodyErrors.length }} {{ t('body errors') }}</span>
64
+ </div>
65
+
66
+ <div
67
+ v-if="validationUrl"
68
+ class="w-100 text-align-right fr-mt-5v"
69
+ target="_blank"
70
+ >
71
+ <a :href="validationUrl">{{ t('See validation report') }}</a>
72
+ </div>
73
+ </div>
74
+ </template>
75
+ </Toggletip>
76
+ <span class="fr-mr-1v text-gray-medium">{{ t("Schema:") }}</span>
77
+ <span class="flex items-center bg-danger-lightest rounded-sm">
78
+ <span class="fr-tag fr-tag--sm">{{ title }}</span>
79
+ <span
80
+ v-if="validataStatus === 'warnings'"
81
+ class="flex items-center padding-sm"
82
+ >
83
+ <span class="fr-icon-alert-line fr-icon--sm fr-mr-1v" />
84
+ <span>{{ t("Invalid") }}</span>
85
+ </span>
86
+ <span
87
+ v-if="validataStatus === 'ko'"
88
+ class="flex items-center text-warning-dark padding-sm"
89
+ >
90
+ <span class="fr-icon-error-line fr-icon--sm fr-mr-1v" />
91
+ <span>{{ t("Invalid") }}</span>
92
+ </span>
93
+ </span>
94
+ </span>
95
+ </template>
96
+
97
+ <script setup lang="ts">
98
+ import { computed, onMounted, ref } from 'vue'
99
+ import { useI18n } from 'vue-i18n'
100
+ import type { Resource } from '../../types/resources'
101
+ import Toggletip from '../Toggletip.vue'
102
+ import type { RegisteredSchema, ValidataError } from '../../functions/schemas'
103
+ import { findSchemaInCatalog, getCatalog, getSchemaDocumentation, getSchemaValidationUrl } from '../../functions/schemas'
104
+
105
+ const props = defineProps<{
106
+ resource: Resource
107
+ }>()
108
+
109
+ const { t } = useI18n()
110
+
111
+ const catalog = ref<Array<RegisteredSchema> | null>(null)
112
+ onMounted(async () => {
113
+ catalog.value = await getCatalog()
114
+ })
115
+ const catalogSchema = computed(() => catalog.value ? findSchemaInCatalog(catalog.value, props.resource.schema) : null)
116
+ const validationUrl = computed(() => catalogSchema.value ? getSchemaValidationUrl(props.resource, catalogSchema.value) : null)
117
+ const documentationUrl = computed(() => catalogSchema.value ? getSchemaDocumentation(catalogSchema.value.name) : null)
118
+
119
+ const title = computed(() => {
120
+ if (!props.resource.schema) return null
121
+ return props.resource.schema.name || props.resource.schema.url
122
+ })
123
+
124
+ const validataErrors = computed<Array<ValidataError>>(() => props.resource.extras['validation-report:errors'] || [])
125
+ const validataWarnings = computed(() => validataErrors.value.filter(error => [''].includes(error.code)))
126
+ const validataBodyErrors = computed(() => validataErrors.value.filter(error => ['#body', '#cell', '#content', '#row', '#table'].some(tag => error.tags.includes(tag))))
127
+ const validataStructureErrors = computed(() => validataErrors.value.filter(error => ['#head', '#structure', '#header'].some(tag => error.tags.includes(tag))))
128
+
129
+ const validataStatus = computed<'ok' | 'warnings' | 'ko'>(() => {
130
+ if (validataErrors.value.length === 0) return 'ok'
131
+ if (validataErrors.value.length === validataWarnings.value.length) return 'warnings'
132
+ return 'ko'
133
+ })
134
+ </script>
135
+
136
+ <style scoped>
137
+ .close-button {
138
+ width: 40px;
139
+ font-size: 1.2rem;
140
+ line-height: 0;
141
+ }
142
+ .rounded-sm {
143
+ border-radius: 0.75rem;
144
+ }
145
+ .padding-sm {
146
+ padding: .125rem .5rem .125rem .25rem;
147
+ }
148
+ </style>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <ContentLoader
3
+ :width="454"
4
+ :height="40"
5
+ :speed="2"
6
+ primary-color="#f3f3f3"
7
+ secondary-color="#ecebeb"
8
+ >
9
+ <rect
10
+ x="0"
11
+ y="0"
12
+ rx="20"
13
+ ry="20"
14
+ width="196"
15
+ height="40"
16
+ />
17
+ <rect
18
+ x="212"
19
+ y="0"
20
+ rx="20"
21
+ ry="20"
22
+ width="242"
23
+ height="40"
24
+ />
25
+ </ContentLoader>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { ContentLoader } from 'vue-content-loader'
30
+ </script>