@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200
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 +7 -6
- package/assets/main.css +4 -0
- package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
- package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
- package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
- package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
- package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
- package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
- package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
- package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
- package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +196 -150
- package/dist/components.css +2 -2
- package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
- package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
- package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
- package/package.json +47 -38
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +24 -14
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +320 -0
- package/src/components/Chart/ChartViewerWrapper.vue +182 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +24 -5
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +24 -9
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/Listbox.vue +101 -0
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +81 -0
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +25 -62
- package/src/components/PostCard.vue +13 -15
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +172 -0
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
- package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
- package/src/components/ResourceAccordion/Metadata.vue +4 -6
- package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
- package/src/components/ResourceAccordion/Preview.vue +19 -15
- package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +28 -9
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +2 -2
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +229 -123
- package/src/components/Search/SearchInput.vue +5 -4
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +50 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +481 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +84 -0
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +13 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +15 -1
- package/src/composables/useMetrics.ts +1 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +120 -0
- package/src/composables/useStableQueryParams.ts +38 -6
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +21 -3
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +20 -42
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +99 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/dates.ts +6 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +63 -60
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +136 -1
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +214 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +119 -14
- package/src/types/dataservices.ts +17 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +5 -1
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +63 -1
- package/src/types/site.ts +5 -3
- package/src/types/ui.ts +2 -0
- package/src/types/users.ts +2 -1
- package/src/types/visualizations.ts +93 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
- package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
- package/dist/Swagger.client-B4y0hXME.js +0 -4
- package/dist/XmlPreview.client-LETEfNBH.js +0 -64
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
class="flex flex-wrap justify-between"
|
|
12
12
|
>
|
|
13
13
|
<p class="text-[0.625rem] m-0 text-gray-medium">
|
|
14
|
-
|
|
14
|
+
<FormattedDate
|
|
15
|
+
:date="startDate"
|
|
16
|
+
:options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
|
|
17
|
+
/>
|
|
15
18
|
</p>
|
|
16
19
|
<p class="text-[0.625rem] m-0 text-gray-medium">
|
|
17
|
-
|
|
20
|
+
<FormattedDate
|
|
21
|
+
:date="endDate"
|
|
22
|
+
:options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
|
|
23
|
+
/>
|
|
18
24
|
</p>
|
|
19
25
|
</div>
|
|
20
26
|
</div>
|
|
@@ -24,6 +30,7 @@
|
|
|
24
30
|
import type { ChartOptions, ScriptableLineSegmentContext } from 'chart.js'
|
|
25
31
|
import Chart from 'chart.js/auto'
|
|
26
32
|
import { computed, onMounted, ref, useTemplateRef, watchEffect } from 'vue'
|
|
33
|
+
import FormattedDate from './FormattedDate.vue'
|
|
27
34
|
|
|
28
35
|
const LIGHT_COLOR = '#B6CFFB'
|
|
29
36
|
const COLOR = '#3558A2'
|
|
@@ -107,14 +114,8 @@ const additionalDatasetConfig = computed<{
|
|
|
107
114
|
return {}
|
|
108
115
|
})
|
|
109
116
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear().toString().slice(-2)}`
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const startDate = computed(() => months.value.length ? getMonthYear(months.value[0]!) : null)
|
|
117
|
-
const endDate = computed(() => months.value.length ? getMonthYear(months.value[months.value.length - 1]!) : null)
|
|
117
|
+
const startDate = computed(() => months.value[0] ?? null)
|
|
118
|
+
const endDate = computed(() => months.value[months.value.length - 1] ?? null)
|
|
118
119
|
|
|
119
120
|
const OPTIONS = {
|
|
120
121
|
// @ts-expect-error animation can be `true` but the typing is not expecting it
|
|
@@ -65,14 +65,20 @@
|
|
|
65
65
|
class="mt-1 mb-0 text-xs"
|
|
66
66
|
>
|
|
67
67
|
{{ t("depuis ") }}
|
|
68
|
-
|
|
68
|
+
<FormattedDate
|
|
69
|
+
:date="since"
|
|
70
|
+
:options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
|
|
71
|
+
/>
|
|
69
72
|
</p>
|
|
70
73
|
<p class="mt-1 mb-0 text-xs text-success-darkest">
|
|
71
74
|
<strong>
|
|
72
75
|
+ {{ summarize(lastValue, 2) }}
|
|
73
76
|
</strong>
|
|
74
77
|
{{ t(" en ") }}
|
|
75
|
-
|
|
78
|
+
<FormattedDate
|
|
79
|
+
:date="lastMonth"
|
|
80
|
+
:options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
|
|
81
|
+
/>
|
|
76
82
|
</p>
|
|
77
83
|
</template>
|
|
78
84
|
</div>
|
|
@@ -146,7 +152,10 @@
|
|
|
146
152
|
+ {{ summarize(lastValue, 2) }}
|
|
147
153
|
</strong>
|
|
148
154
|
{{ t(" en ") }}
|
|
149
|
-
|
|
155
|
+
<FormattedDate
|
|
156
|
+
:date="lastMonth"
|
|
157
|
+
:options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
|
|
158
|
+
/>
|
|
150
159
|
</p>
|
|
151
160
|
</div>
|
|
152
161
|
</template>
|
|
@@ -154,7 +163,7 @@
|
|
|
154
163
|
<script setup lang="ts">
|
|
155
164
|
import { computed } from 'vue'
|
|
156
165
|
import { ContentLoader } from 'vue-content-loader'
|
|
157
|
-
import
|
|
166
|
+
import FormattedDate from './FormattedDate.vue'
|
|
158
167
|
import { summarize } from '../functions/helpers'
|
|
159
168
|
import { useTranslation } from '../composables/useTranslation'
|
|
160
169
|
import SmallChart from './SmallChart.vue'
|
|
@@ -169,7 +178,6 @@ const props = defineProps<{
|
|
|
169
178
|
}>()
|
|
170
179
|
|
|
171
180
|
const { t } = useTranslation()
|
|
172
|
-
const { formatDate } = useFormatDate()
|
|
173
181
|
|
|
174
182
|
const months = computed(() => props.data ? Object.keys(props.data) : [])
|
|
175
183
|
const values = computed(() => props.data ? Object.values(props.data) : [])
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="activeFilters.length > 0 || sort"
|
|
4
|
+
class="flex flex-wrap items-center gap-1.5"
|
|
5
|
+
>
|
|
6
|
+
<!-- Sort chip -->
|
|
7
|
+
<span
|
|
8
|
+
v-if="sort"
|
|
9
|
+
class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
|
|
10
|
+
>
|
|
11
|
+
<component
|
|
12
|
+
:is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
|
|
13
|
+
class="size-3.5"
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
/>
|
|
16
|
+
<span class="font-medium">{{ sort.column }}</span>
|
|
17
|
+
<span>{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
|
|
18
|
+
<button
|
|
19
|
+
:aria-label="t('Supprimer le tri')"
|
|
20
|
+
class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
|
|
21
|
+
@click="sort = null"
|
|
22
|
+
>
|
|
23
|
+
<RiCloseLine
|
|
24
|
+
class="size-3.5"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
/>
|
|
27
|
+
</button>
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
<!-- Filter chips -->
|
|
31
|
+
<span
|
|
32
|
+
v-for="af in activeFilters"
|
|
33
|
+
:key="af.column"
|
|
34
|
+
:data-testid="`active-filter-${af.column}`"
|
|
35
|
+
class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
|
|
36
|
+
>
|
|
37
|
+
<component
|
|
38
|
+
:is="getColumnDisplay(af.column).icon"
|
|
39
|
+
class="size-3.5"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
<span class="font-medium">{{ af.column }}</span>
|
|
43
|
+
<span>{{ af.label }}</span>
|
|
44
|
+
<button
|
|
45
|
+
:aria-label="t('Supprimer ce filtre')"
|
|
46
|
+
class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
|
|
47
|
+
@click="removeFilter(af.column)"
|
|
48
|
+
>
|
|
49
|
+
<RiCloseLine
|
|
50
|
+
class="size-3.5"
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
/>
|
|
53
|
+
</button>
|
|
54
|
+
</span>
|
|
55
|
+
|
|
56
|
+
<!-- Clear all -->
|
|
57
|
+
<BrandedButton
|
|
58
|
+
v-if="withClear"
|
|
59
|
+
color="tertiary"
|
|
60
|
+
size="2xs"
|
|
61
|
+
:icon="RiCloseLine"
|
|
62
|
+
@click="clearAllFilters(); sort = null"
|
|
63
|
+
>
|
|
64
|
+
{{ t('Tout effacer') }}
|
|
65
|
+
</BrandedButton>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
import { RiArrowDownLine, RiArrowUpLine, RiCloseLine } from '@remixicon/vue'
|
|
71
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
72
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
73
|
+
import { useTabularContext } from './useTabularContext'
|
|
74
|
+
|
|
75
|
+
defineProps<{ withClear?: boolean }>()
|
|
76
|
+
|
|
77
|
+
const { t } = useTranslation()
|
|
78
|
+
const { activeFilters, sort, removeFilter, clearAllFilters, getColumnDisplay } = useTabularContext()
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-if="value == null || value === ''"
|
|
4
|
+
class="font-[Inconsolata,monospace] text-gray-low italic"
|
|
5
|
+
:class="compact ? 'text-xs' : 'text-sm'"
|
|
6
|
+
>null</span>
|
|
7
|
+
<span
|
|
8
|
+
v-else-if="columnType === 'boolean'"
|
|
9
|
+
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs"
|
|
10
|
+
:class="isTruthy(value) ? 'bg-new-success-light text-new-success' : 'bg-new-warning-light text-new-error'"
|
|
11
|
+
>
|
|
12
|
+
<span
|
|
13
|
+
class="size-2 rounded-full"
|
|
14
|
+
:class="isTruthy(value) ? 'bg-new-success' : 'bg-new-error'"
|
|
15
|
+
/>
|
|
16
|
+
{{ isTruthy(value) ? t('Vrai') : t('Faux') }}
|
|
17
|
+
</span>
|
|
18
|
+
<span
|
|
19
|
+
v-else-if="columnType === 'categorical'"
|
|
20
|
+
class="inline-block max-w-full truncate rounded bg-gray-lower px-2 py-0.5 text-xs text-gray-plain"
|
|
21
|
+
>{{ value }}</span>
|
|
22
|
+
<span
|
|
23
|
+
v-else-if="columnType === 'number' || columnType === 'year'"
|
|
24
|
+
:class="compact ? 'font-mono tabular-nums text-xs' : ''"
|
|
25
|
+
>{{ columnType === 'year' || noNumberFormat ? value : formatNumber(value) }}</span>
|
|
26
|
+
<span
|
|
27
|
+
v-else-if="columnType === 'date'"
|
|
28
|
+
:class="compact ? 'font-mono tabular-nums text-xs' : ''"
|
|
29
|
+
>{{ formatCellDate(value) }}</span>
|
|
30
|
+
<span
|
|
31
|
+
v-else
|
|
32
|
+
class="text-gray-title truncate block text-xs"
|
|
33
|
+
>{{ typeof value === 'object' ? JSON.stringify(value) : value }}</span>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
38
|
+
import { useFormatTabular, isTruthy } from '../../functions/tabular'
|
|
39
|
+
import type { ColumnType } from './types'
|
|
40
|
+
|
|
41
|
+
defineProps<{
|
|
42
|
+
value: unknown
|
|
43
|
+
columnType: ColumnType
|
|
44
|
+
compact?: boolean
|
|
45
|
+
noNumberFormat?: boolean
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
const { t } = useTranslation()
|
|
49
|
+
const { formatNumber, formatCellDate } = useFormatTabular()
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ClientOnly>
|
|
3
|
+
<Teleport to="#tooltips">
|
|
4
|
+
<div
|
|
5
|
+
v-if="cell"
|
|
6
|
+
ref="panel"
|
|
7
|
+
class="bg-white border border-black/10 rounded-lg shadow-md w-80 absolute z-[800]"
|
|
8
|
+
:style="floatingStyles"
|
|
9
|
+
>
|
|
10
|
+
<!-- Value -->
|
|
11
|
+
<div class="px-3 pt-3 pb-2 border-b border-gray-default">
|
|
12
|
+
<p class="text-[10px] text-gray-plain mb-0">
|
|
13
|
+
{{ t('Valeur brute') }}
|
|
14
|
+
</p>
|
|
15
|
+
<p class="text-xs text-gray-title mb-0">
|
|
16
|
+
{{ displayValue }}
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Type -->
|
|
21
|
+
<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-default">
|
|
22
|
+
<span class="text-[10px] text-gray-plain">{{ t('Type') }}</span>
|
|
23
|
+
<span class="inline-flex items-center gap-1 bg-gray-some rounded px-1.5 py-0.5 text-xs text-gray-plain">
|
|
24
|
+
<component
|
|
25
|
+
:is="typeIcon"
|
|
26
|
+
class="size-3"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
/>
|
|
29
|
+
{{ typeLabel }}
|
|
30
|
+
</span>
|
|
31
|
+
<span class="text-[10px] text-gray-plain shrink-0">·</span>
|
|
32
|
+
<span class="text-[10px] text-gray-plain truncate min-w-0">{{ cell.column }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Actions -->
|
|
36
|
+
<div class="p-1">
|
|
37
|
+
<button
|
|
38
|
+
class="flex items-center gap-2.5 w-full px-3 py-2 rounded-md text-xs font-medium hover:bg-gray-50"
|
|
39
|
+
@click="filterByValue"
|
|
40
|
+
>
|
|
41
|
+
<RiFilterLine
|
|
42
|
+
class="size-4"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
/>
|
|
45
|
+
{{ t('Filtrer par cette valeur') }}
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
class="flex items-center gap-2.5 w-full px-3 py-2 rounded-md text-xs font-medium hover:bg-gray-50"
|
|
49
|
+
@click="copyValue"
|
|
50
|
+
>
|
|
51
|
+
<RiCheckLine
|
|
52
|
+
v-if="copied"
|
|
53
|
+
class="size-4 text-green-500"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
/>
|
|
56
|
+
<RiFileCopyLine
|
|
57
|
+
v-else
|
|
58
|
+
class="size-4 text-gray-plain"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
/>
|
|
61
|
+
{{ copied ? t('Copié !') : t('Copier la valeur') }}
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</Teleport>
|
|
66
|
+
</ClientOnly>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
71
|
+
import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
|
|
72
|
+
import { onClickOutside } from '@vueuse/core'
|
|
73
|
+
import {
|
|
74
|
+
RiFilterLine,
|
|
75
|
+
RiFileCopyLine,
|
|
76
|
+
RiCheckLine,
|
|
77
|
+
} from '@remixicon/vue'
|
|
78
|
+
import { toast } from 'vue-sonner'
|
|
79
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
80
|
+
import { buildTypeConfig } from '../../functions/tabular'
|
|
81
|
+
import ClientOnly from '../ClientOnly.vue'
|
|
82
|
+
import type { ColumnType, ColumnFilters } from './types'
|
|
83
|
+
|
|
84
|
+
export interface CellInfo {
|
|
85
|
+
column: string
|
|
86
|
+
columnType: ColumnType
|
|
87
|
+
value: unknown
|
|
88
|
+
element: HTMLElement
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cell = defineModel<CellInfo | null>('cell', { default: null })
|
|
92
|
+
const filters = defineModel<Record<string, ColumnFilters>>('filters', { default: () => ({}) })
|
|
93
|
+
|
|
94
|
+
const { t } = useTranslation()
|
|
95
|
+
|
|
96
|
+
const panelRef = useTemplateRef<HTMLElement>('panel')
|
|
97
|
+
const anchorRef = ref<HTMLElement | null>(null)
|
|
98
|
+
|
|
99
|
+
watch(cell, (c) => {
|
|
100
|
+
anchorRef.value = c?.element ?? null
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const { floatingStyles } = useFloating(anchorRef, panelRef, {
|
|
104
|
+
placement: 'bottom-start',
|
|
105
|
+
middleware: [flip(), shift()],
|
|
106
|
+
whileElementsMounted: autoUpdate,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const displayValue = computed(() => {
|
|
110
|
+
if (!cell.value) return ''
|
|
111
|
+
const v = cell.value.value
|
|
112
|
+
if (v == null || v === '') return '–'
|
|
113
|
+
if (typeof v === 'object') return JSON.stringify(v)
|
|
114
|
+
return String(v)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const typeConfig = buildTypeConfig(t)
|
|
118
|
+
|
|
119
|
+
const typeIcon = computed(() => cell.value ? typeConfig[cell.value.columnType].icon : typeConfig.text.icon)
|
|
120
|
+
const typeLabel = computed(() => cell.value ? typeConfig[cell.value.columnType].label : '')
|
|
121
|
+
|
|
122
|
+
function close() {
|
|
123
|
+
cell.value = null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function filterByValue() {
|
|
127
|
+
if (!cell.value) return
|
|
128
|
+
const val = String(cell.value.value ?? '')
|
|
129
|
+
const col = cell.value.column
|
|
130
|
+
const existing = filters.value[col] ?? {}
|
|
131
|
+
if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date' || cell.value.columnType === 'year') {
|
|
132
|
+
const current = existing.in ?? []
|
|
133
|
+
if (!current.includes(val)) {
|
|
134
|
+
filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (cell.value.columnType === 'number') {
|
|
138
|
+
const num = Number(cell.value.value)
|
|
139
|
+
if (Number.isFinite(num)) {
|
|
140
|
+
filters.value = { ...filters.value, [col]: { ...existing, min: num, max: num } }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (cell.value.columnType === 'boolean') {
|
|
144
|
+
filters.value = { ...filters.value, [col]: { ...existing, exact: val } }
|
|
145
|
+
}
|
|
146
|
+
close()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const copied = ref(false)
|
|
150
|
+
|
|
151
|
+
async function copyValue() {
|
|
152
|
+
try {
|
|
153
|
+
await navigator.clipboard.writeText(displayValue.value)
|
|
154
|
+
copied.value = true
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
copied.value = false
|
|
157
|
+
}, 1500)
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
toast.error(t('Impossible de copier dans le presse-papier'))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onClickOutside(panelRef, (e) => {
|
|
165
|
+
if (!cell.value) return
|
|
166
|
+
const clickedCell = (e.target as HTMLElement).closest('[data-cell]')
|
|
167
|
+
if (clickedCell && clickedCell === cell.value.element) return
|
|
168
|
+
close()
|
|
169
|
+
})
|
|
170
|
+
</script>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Popover
|
|
3
|
+
ref="columnAnchor"
|
|
4
|
+
v-slot="{ open }"
|
|
5
|
+
class="relative"
|
|
6
|
+
>
|
|
7
|
+
<PopoverButton class="flex items-center gap-1.5 text-xs text-gray-plain rounded px-2 py-1 hover:bg-gray-50 focus:outline-none">
|
|
8
|
+
<RiLayoutColumnLine
|
|
9
|
+
class="size-3"
|
|
10
|
+
aria-hidden="true"
|
|
11
|
+
/>
|
|
12
|
+
<span class="font-bold hidden md:inline">{{ t('Colonnes') }}</span>
|
|
13
|
+
<span class="font-mono tabular-nums">{{ visibleColumns.size }}/{{ allColumns.length }}</span>
|
|
14
|
+
<RiArrowDownSLine
|
|
15
|
+
class="size-3 opacity-50"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
/>
|
|
18
|
+
</PopoverButton>
|
|
19
|
+
|
|
20
|
+
<ClientOnly>
|
|
21
|
+
<Teleport to="#tooltips">
|
|
22
|
+
<PopoverPanel
|
|
23
|
+
v-show="open"
|
|
24
|
+
ref="columnPanel"
|
|
25
|
+
static
|
|
26
|
+
class="bg-white border border-gray-default rounded shadow-lg w-72 absolute z-[800]"
|
|
27
|
+
:style="columnFloatingStyles"
|
|
28
|
+
>
|
|
29
|
+
<div class="px-3 py-2 border-b border-gray-default space-y-1.5">
|
|
30
|
+
<span class="block text-xs font-medium text-gray-title">
|
|
31
|
+
{{ visibleColumns.size }} {{ t('sur') }} {{ allColumns.length }} {{ t('colonnes visibles') }}
|
|
32
|
+
</span>
|
|
33
|
+
<div class="flex items-center gap-1">
|
|
34
|
+
<BrandedButton
|
|
35
|
+
v-if="hiddenCount"
|
|
36
|
+
color="tertiary"
|
|
37
|
+
size="2xs"
|
|
38
|
+
@click="showAllColumns"
|
|
39
|
+
>
|
|
40
|
+
{{ t('Tout afficher') }}
|
|
41
|
+
</BrandedButton>
|
|
42
|
+
<BrandedButton
|
|
43
|
+
v-if="visibleColumns.size > 0"
|
|
44
|
+
color="tertiary"
|
|
45
|
+
size="2xs"
|
|
46
|
+
@click="hideAllColumns"
|
|
47
|
+
>
|
|
48
|
+
{{ t('Tout masquer') }}
|
|
49
|
+
</BrandedButton>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="max-h-64 overflow-auto p-1">
|
|
53
|
+
<div
|
|
54
|
+
v-for="col in allColumns"
|
|
55
|
+
:key="col"
|
|
56
|
+
class="group/col flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-50 text-xs"
|
|
57
|
+
>
|
|
58
|
+
<label class="flex items-center gap-2 flex-1 min-w-0 cursor-pointer">
|
|
59
|
+
<input
|
|
60
|
+
type="checkbox"
|
|
61
|
+
:checked="visibleColumns.has(col)"
|
|
62
|
+
class="size-3.5 accent-new-primary"
|
|
63
|
+
@change="toggleColumn(col)"
|
|
64
|
+
>
|
|
65
|
+
<span class="truncate">{{ col }}</span>
|
|
66
|
+
</label>
|
|
67
|
+
<button
|
|
68
|
+
class="shrink-0 px-1 text-new-primary hover:underline hidden group-hover/col:block group-focus-within/col:block"
|
|
69
|
+
@click="selectOnlyColumn(col)"
|
|
70
|
+
>
|
|
71
|
+
{{ t('Uniquement') }}
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</PopoverPanel>
|
|
76
|
+
</Teleport>
|
|
77
|
+
</ClientOnly>
|
|
78
|
+
</Popover>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import { computed, useTemplateRef } from 'vue'
|
|
83
|
+
import { autoUpdate, flip, shift, useFloating } from '@floating-ui/vue'
|
|
84
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
85
|
+
import { RiArrowDownSLine, RiLayoutColumnLine } from '@remixicon/vue'
|
|
86
|
+
import ClientOnly from '../ClientOnly.vue'
|
|
87
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
88
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
89
|
+
import { useTabularContext } from './useTabularContext'
|
|
90
|
+
|
|
91
|
+
const { t } = useTranslation()
|
|
92
|
+
const {
|
|
93
|
+
allColumns,
|
|
94
|
+
visibleColumns,
|
|
95
|
+
hiddenCount,
|
|
96
|
+
toggleColumn,
|
|
97
|
+
showAllColumns,
|
|
98
|
+
hideAllColumns,
|
|
99
|
+
selectOnlyColumn,
|
|
100
|
+
} = useTabularContext()
|
|
101
|
+
|
|
102
|
+
// The panel is teleported, so it's positioned against its trigger with floating-ui.
|
|
103
|
+
const columnAnchor = useTemplateRef<InstanceType<typeof Popover>>('columnAnchor')
|
|
104
|
+
const columnPanel = useTemplateRef<InstanceType<typeof PopoverPanel>>('columnPanel')
|
|
105
|
+
const columnAnchorEl = computed(() => columnAnchor.value?.$el as HTMLElement | undefined)
|
|
106
|
+
const columnPanelEl = computed(() => columnPanel.value?.$el as HTMLElement | undefined)
|
|
107
|
+
const { floatingStyles: columnFloatingStyles } = useFloating(columnAnchorEl, columnPanelEl, {
|
|
108
|
+
placement: 'bottom-start',
|
|
109
|
+
middleware: [flip(), shift()],
|
|
110
|
+
whileElementsMounted: autoUpdate,
|
|
111
|
+
})
|
|
112
|
+
</script>
|