@datagouv/components-next 1.2.0 → 1.4.0
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/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
- package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
- package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
- package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
- package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
- package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
- package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +201 -170
- package/dist/components.css +2 -2
- package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
- package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
- package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +40 -39
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- 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 +17 -6
- 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/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +30 -19
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
- 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 +140 -184
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +28 -10
- package/src/components/Search/SearchInput.vue +1 -1
- 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 +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +21 -5
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +127 -780
- package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
- package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
- 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 +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +40 -28
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/api.ts +13 -7
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +88 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +222 -5
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js +0 -61
- /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
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
>
|
|
7
7
|
<div
|
|
8
8
|
v-if="!hideSearchInput"
|
|
9
|
-
ref="search"
|
|
10
9
|
class="flex flex-wrap items-center justify-between"
|
|
11
10
|
data-cy="search"
|
|
12
11
|
>
|
|
@@ -228,6 +227,7 @@
|
|
|
228
227
|
size="sm"
|
|
229
228
|
:icon="RiRssLine"
|
|
230
229
|
icon-only
|
|
230
|
+
external
|
|
231
231
|
target="_blank"
|
|
232
232
|
/>
|
|
233
233
|
</div>
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
name="reuse"
|
|
267
267
|
:reuse="result"
|
|
268
268
|
>
|
|
269
|
-
<ReuseHorizontalCard :reuse="(result as
|
|
269
|
+
<ReuseHorizontalCard :reuse="(result as ReuseV2)" />
|
|
270
270
|
</slot>
|
|
271
271
|
</template>
|
|
272
272
|
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
@@ -356,6 +356,7 @@
|
|
|
356
356
|
<script setup lang="ts">
|
|
357
357
|
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
358
358
|
import { useRouteQuery } from '@vueuse/router'
|
|
359
|
+
import { useRoute } from 'vue-router'
|
|
359
360
|
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
360
361
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
361
362
|
import { useTranslation } from '../../composables/useTranslation'
|
|
@@ -364,11 +365,12 @@ import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilte
|
|
|
364
365
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
365
366
|
import { useComponentsConfig } from '../../config'
|
|
366
367
|
import { useFetch } from '../../functions/api'
|
|
368
|
+
import { scrollToBlockTop } from '../../functions/scroll'
|
|
367
369
|
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
368
370
|
import type { Dataset } from '../../types/datasets'
|
|
369
371
|
import type { Dataservice } from '../../types/dataservices'
|
|
370
372
|
import type { Organization } from '../../types/organizations'
|
|
371
|
-
import type {
|
|
373
|
+
import type { ReuseV2 } from '../../types/reuses'
|
|
372
374
|
import type { TopicV2 } from '../../types/topics'
|
|
373
375
|
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
|
|
374
376
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
@@ -422,6 +424,7 @@ if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class
|
|
|
422
424
|
|
|
423
425
|
const { t } = useTranslation()
|
|
424
426
|
const componentsConfig = useComponentsConfig()
|
|
427
|
+
const route = useRoute()
|
|
425
428
|
|
|
426
429
|
// Custom filter registry for useSearchFilter composable
|
|
427
430
|
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
@@ -549,20 +552,34 @@ const allFilters: Record<string, Ref<unknown>> = {
|
|
|
549
552
|
type: reuseType,
|
|
550
553
|
}
|
|
551
554
|
|
|
552
|
-
// Reset sort and filters when changing type
|
|
555
|
+
// Reset page, sort and filters when changing type. Every reset below goes
|
|
556
|
+
// through useRouteQuery, so VueUse coalesces them into a single router.replace
|
|
557
|
+
// (its shared _queriesQueue flushes once on nextTick). Custom filters are cleared
|
|
558
|
+
// here too, rather than in useSearchFilter's onunmount, so their URL change joins
|
|
559
|
+
// that same batch instead of racing it with a separate router.replace.
|
|
553
560
|
watch(currentType, () => {
|
|
561
|
+
// page=1 is the default and is dropped from the URL, so only reset when needed.
|
|
562
|
+
if (page.value !== 1) page.value = 1
|
|
563
|
+
|
|
554
564
|
// Reset sort if not valid
|
|
555
565
|
const validSortValues = activeSortOptions.value.map(o => o.value as string)
|
|
556
566
|
if (sort.value && !validSortValues.includes(sort.value)) {
|
|
557
567
|
sort.value = undefined
|
|
558
568
|
}
|
|
559
569
|
|
|
560
|
-
// Reset filters that are not enabled for the new type
|
|
570
|
+
// Reset built-in filters that are not enabled for the new type
|
|
561
571
|
for (const [filterName, filterRef] of Object.entries(allFilters)) {
|
|
562
572
|
if (filterRef.value !== undefined && !activeFilters.value.includes(filterName)) {
|
|
563
573
|
filterRef.value = undefined
|
|
564
574
|
}
|
|
565
575
|
}
|
|
576
|
+
|
|
577
|
+
// Reset type-scoped custom filters that don't apply to the new type
|
|
578
|
+
for (const entry of customFilterRegistry.values()) {
|
|
579
|
+
if (entry.typeKeys && !entry.typeKeys.includes(currentType.value) && isCustomFilterActive(entry)) {
|
|
580
|
+
entry.ref.value = entry.defaultValue
|
|
581
|
+
}
|
|
582
|
+
}
|
|
566
583
|
})
|
|
567
584
|
|
|
568
585
|
// Create stable params for each type
|
|
@@ -782,14 +799,15 @@ function getFacets(key: string): FacetItem[] | undefined {
|
|
|
782
799
|
}
|
|
783
800
|
|
|
784
801
|
// Scroll handling
|
|
785
|
-
const
|
|
802
|
+
const resultsRef = useTemplateRef('results')
|
|
786
803
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
804
|
+
// Every criteria lives in the URL, custom filters included, so watching the
|
|
805
|
+
// query covers them all: whenever the result list is replaced, bring its top
|
|
806
|
+
// back into view rather than leaving the reader in the middle of a list they
|
|
807
|
+
// have not seen yet.
|
|
808
|
+
watch(() => route.query, () => scrollToBlockTop(resultsRef.value))
|
|
790
809
|
|
|
791
810
|
function changePage(newPage: number) {
|
|
792
811
|
page.value = newPage
|
|
793
|
-
scrollToTop()
|
|
794
812
|
}
|
|
795
813
|
</script>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
v-model="q"
|
|
13
13
|
type="search"
|
|
14
14
|
name="q"
|
|
15
|
-
class="input max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
15
|
+
class="input flex-1 max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
16
16
|
:aria-label="placeholder === null ? t('Rechercher...') : placeholder ?? t('Rechercher...')"
|
|
17
17
|
:placeholder="placeholder === null ? undefined : placeholder ?? t('Rechercher...')"
|
|
18
18
|
>
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
:class="{ 'rotate-180': open }"
|
|
12
12
|
/>
|
|
13
13
|
</button>
|
|
14
|
-
|
|
14
|
+
<!-- Collapsing with CSS rather than `v-if` on a JS media query: the server has no
|
|
15
|
+
viewport, so it always renders the desktop state and a narrow client would drop
|
|
16
|
+
the whole panel during hydration. -->
|
|
17
|
+
<div :class="{ 'hidden md:block': !open }">
|
|
15
18
|
<p
|
|
16
19
|
:id="titleId"
|
|
17
20
|
class="text-sm font-bold leading-tight mb-6 hidden md:block"
|
|
@@ -26,13 +29,11 @@
|
|
|
26
29
|
<script setup lang="ts">
|
|
27
30
|
import { ref, useId } from 'vue'
|
|
28
31
|
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
29
|
-
import { useMediaQuery } from '@vueuse/core'
|
|
30
32
|
|
|
31
33
|
defineProps<{
|
|
32
34
|
buttonText: string
|
|
33
35
|
}>()
|
|
34
36
|
|
|
35
37
|
const titleId = useId()
|
|
36
|
-
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
37
38
|
const open = ref(false)
|
|
38
39
|
</script>
|
|
@@ -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>
|
|
@@ -17,16 +17,15 @@
|
|
|
17
17
|
</span>
|
|
18
18
|
<span
|
|
19
19
|
v-else-if="columnType === 'categorical'"
|
|
20
|
-
class="inline-block rounded
|
|
21
|
-
:style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
|
|
20
|
+
class="inline-block max-w-full truncate rounded bg-gray-lower px-2 py-0.5 text-xs text-gray-plain"
|
|
22
21
|
>{{ value }}</span>
|
|
23
22
|
<span
|
|
24
|
-
v-else-if="columnType === 'number'"
|
|
25
|
-
:class="compact ? 'font-mono tabular-nums text-xs
|
|
26
|
-
>{{ formatNumber(value) }}</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>
|
|
27
26
|
<span
|
|
28
27
|
v-else-if="columnType === 'date'"
|
|
29
|
-
:class="compact ? 'font-mono tabular-nums text-xs
|
|
28
|
+
:class="compact ? 'font-mono tabular-nums text-xs' : ''"
|
|
30
29
|
>{{ formatCellDate(value) }}</span>
|
|
31
30
|
<span
|
|
32
31
|
v-else
|
|
@@ -37,13 +36,13 @@
|
|
|
37
36
|
<script setup lang="ts">
|
|
38
37
|
import { useTranslation } from '../../composables/useTranslation'
|
|
39
38
|
import { useFormatTabular, isTruthy } from '../../functions/tabular'
|
|
40
|
-
import type { ColumnType
|
|
39
|
+
import type { ColumnType } from './types'
|
|
41
40
|
|
|
42
41
|
defineProps<{
|
|
43
42
|
value: unknown
|
|
44
43
|
columnType: ColumnType
|
|
45
|
-
categoryBadgeStyle?: BadgeStyle
|
|
46
44
|
compact?: boolean
|
|
45
|
+
noNumberFormat?: boolean
|
|
47
46
|
}>()
|
|
48
47
|
|
|
49
48
|
const { t } = useTranslation()
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
<p class="text-[10px] text-gray-plain mb-0">
|
|
13
13
|
{{ t('Valeur brute') }}
|
|
14
14
|
</p>
|
|
15
|
-
<p
|
|
15
|
+
<p
|
|
16
|
+
class="text-xs text-gray-title mb-0"
|
|
17
|
+
data-testid="cell-raw-value"
|
|
18
|
+
>
|
|
16
19
|
{{ displayValue }}
|
|
17
20
|
</p>
|
|
18
21
|
</div>
|
|
@@ -38,7 +41,7 @@
|
|
|
38
41
|
class="flex items-center gap-2.5 w-full px-3 py-2 rounded-md text-xs font-medium hover:bg-gray-50"
|
|
39
42
|
@click="filterByValue"
|
|
40
43
|
>
|
|
41
|
-
<
|
|
44
|
+
<RiFilterLine
|
|
42
45
|
class="size-4"
|
|
43
46
|
aria-hidden="true"
|
|
44
47
|
/>
|
|
@@ -71,13 +74,13 @@ import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
|
71
74
|
import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
|
|
72
75
|
import { onClickOutside } from '@vueuse/core'
|
|
73
76
|
import {
|
|
74
|
-
|
|
77
|
+
RiFilterLine,
|
|
75
78
|
RiFileCopyLine,
|
|
76
79
|
RiCheckLine,
|
|
77
80
|
} from '@remixicon/vue'
|
|
78
81
|
import { toast } from 'vue-sonner'
|
|
79
82
|
import { useTranslation } from '../../composables/useTranslation'
|
|
80
|
-
import { buildTypeConfig } from '../../functions/tabular'
|
|
83
|
+
import { buildTypeConfig, toIsoDay } from '../../functions/tabular'
|
|
81
84
|
import ClientOnly from '../ClientOnly.vue'
|
|
82
85
|
import type { ColumnType, ColumnFilters } from './types'
|
|
83
86
|
|
|
@@ -128,7 +131,20 @@ function filterByValue() {
|
|
|
128
131
|
const val = String(cell.value.value ?? '')
|
|
129
132
|
const col = cell.value.column
|
|
130
133
|
const existing = filters.value[col] ?? {}
|
|
131
|
-
|
|
134
|
+
// A date goes through the same `date` filter the column panel writes, so the
|
|
135
|
+
// calendar opens on the day that was clicked instead of on an empty month.
|
|
136
|
+
if (cell.value.columnType === 'date') {
|
|
137
|
+
const day = toIsoDay(cell.value.value)
|
|
138
|
+
if (day) {
|
|
139
|
+
filters.value = { ...filters.value, [col]: { ...existing, date: { operator: 'is', start: day } } }
|
|
140
|
+
}
|
|
141
|
+
// An empty cell has no day to select: filtering on the missing values is
|
|
142
|
+
// what "this value" means there, and the panel shows it as such.
|
|
143
|
+
else if (cell.value.value == null || cell.value.value === '') {
|
|
144
|
+
filters.value = { ...filters.value, [col]: { ...existing, null: 'only' } }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'year') {
|
|
132
148
|
const current = existing.in ?? []
|
|
133
149
|
if (!current.includes(val)) {
|
|
134
150
|
filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
|
|
@@ -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>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Editable segments rather than a text input: the value never goes through a
|
|
3
|
+
locale-dependent parse, and the day/month/year order follows the locale. -->
|
|
4
|
+
<DateFieldRoot
|
|
5
|
+
v-slot="{ segments }"
|
|
6
|
+
v-model="model"
|
|
7
|
+
v-model:placeholder="placeholder"
|
|
8
|
+
:locale="locale"
|
|
9
|
+
granularity="day"
|
|
10
|
+
:min-value="minValue"
|
|
11
|
+
class="flex h-8 min-w-0 flex-1 items-center rounded-lg border border-transparent bg-[#f3f3f5] px-2 text-sm tabular-nums focus-within:border-new-primary"
|
|
12
|
+
:class="model ? 'text-gray-title' : 'text-gray-medium'"
|
|
13
|
+
:aria-label="label"
|
|
14
|
+
>
|
|
15
|
+
<DateFieldInput
|
|
16
|
+
v-for="(item, index) in segments"
|
|
17
|
+
:key="`${index}-${item.part}`"
|
|
18
|
+
:part="item.part"
|
|
19
|
+
class="rounded px-0.5 focus:bg-new-primary focus:text-white focus:outline-none"
|
|
20
|
+
>
|
|
21
|
+
{{ item.value }}
|
|
22
|
+
</DateFieldInput>
|
|
23
|
+
</DateFieldRoot>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { DateFieldInput, DateFieldRoot } from 'reka-ui'
|
|
28
|
+
import type { DateValue } from '@internationalized/date'
|
|
29
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
30
|
+
|
|
31
|
+
defineProps<{
|
|
32
|
+
label: string
|
|
33
|
+
minValue?: DateValue
|
|
34
|
+
}>()
|
|
35
|
+
|
|
36
|
+
const model = defineModel<DateValue | undefined>()
|
|
37
|
+
// Shared with the calendar and the other field: it is the month on display.
|
|
38
|
+
const placeholder = defineModel<DateValue>('placeholder', { required: true })
|
|
39
|
+
|
|
40
|
+
const { locale } = useTranslation()
|
|
41
|
+
</script>
|