-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(indicators): add list view with filters (#605)
* feat(indicators): add list * working sidebar filter * pagination * sort * fix lint * wip colors * indicator cards * add enjeu, factorize props * fix total count * extract useAccessibilityProperties * polishing up * a11y utils to custom * better list * fix spatialCoverage (for bouquets too) * dynamic props for child components * improve comment * review: SelectComponent * fix: select-enjeu id * add hero * fix label for * review: SelectComponent with label * use search component, scroll to list
- Loading branch information
Showing
16 changed files
with
745 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<!-- a select component with a selectable null value (default) --> | ||
<script setup lang="ts"> | ||
import { getRandomId } from '@gouvminint/vue-dsfr' | ||
|
||
type SelectOption = { | ||
id: string | ||
name: string | ||
} | ||
|
||
const selectedOption = defineModel({ | ||
type: String as () => string | null, | ||
default: null | ||
}) | ||
|
||
defineProps({ | ||
options: { | ||
type: Object as PropType<SelectOption[]>, | ||
required: true | ||
}, | ||
label: { | ||
type: String, | ||
required: true | ||
}, | ||
labelClass: { | ||
type: Array, | ||
default: () => ['fr-label'] | ||
}, | ||
defaultOption: { | ||
type: String, | ||
default: 'Tous' | ||
} | ||
}) | ||
|
||
const id = getRandomId('select') | ||
</script> | ||
|
||
<template> | ||
<label :class="labelClass" :for="id">{{ label }}</label> | ||
<select :id="id" v-model="selectedOption" class="fr-select fr-col"> | ||
<option :value="null"> | ||
{{ defaultOption }} | ||
</option> | ||
<option v-for="option in options" :key="option.id" :value="option.id"> | ||
{{ option.name }} | ||
</option> | ||
</select> | ||
</template> |
43 changes: 43 additions & 0 deletions
43
src/custom/ecospheres/components/indicators/IndicatorCard.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<script setup lang="ts"> | ||
import { stripFromMarkdown } from '@/utils' | ||
import type { Indicator } from '../../model/indicator' | ||
import { useTags } from '../../utils/indicator' | ||
const props = defineProps({ | ||
indicator: { | ||
type: Object as PropType<Indicator>, | ||
required: true | ||
} | ||
}) | ||
const tags = useTags(props.indicator) | ||
</script> | ||
|
||
<template> | ||
<div class="fr-card fr-enlarge-link"> | ||
<div class="fr-card__body"> | ||
<div class="fr-card__content"> | ||
<ul class="fr-badges-group fr-mb-1w"> | ||
<li v-for="tag in tags" :key="`${tag.type}-${tag.value}`"> | ||
<p :class="['fr-badge', `fr-badge--${tag.color}`]"> | ||
{{ tag.value }} | ||
</p> | ||
</li> | ||
</ul> | ||
<h3 class="fr-card__title fr-mb-1w"> | ||
<RouterLink | ||
:to="{ name: 'dataset_detail', params: { did: indicator.id } }" | ||
>{{ indicator.title }}</RouterLink | ||
> | ||
</h3> | ||
<p class="fr-card__desc"> | ||
<text-clamp | ||
:auto-resize="true" | ||
:text="stripFromMarkdown(indicator.description)" | ||
:max-lines="3" | ||
/> | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</template> |
154 changes: 154 additions & 0 deletions
154
src/custom/ecospheres/components/indicators/IndicatorList.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
<script setup lang="ts"> | ||
import { storeToRefs } from 'pinia' | ||
import { computed, watch, type ComputedRef } from 'vue' | ||
import { useLoading } from 'vue-loading-overlay' | ||
import { useRoute, useRouter, type LocationQueryRaw } from 'vue-router' | ||
import type { IndicatorFilters } from '../../model/indicator' | ||
import { useIndicatorStore } from '../../store/IndicatorStore' | ||
import SelectComponent from '../SelectComponent.vue' | ||
import IndicatorCard from './IndicatorCard.vue' | ||
const route = useRoute() | ||
const router = useRouter() | ||
const store = useIndicatorStore() | ||
type Props = IndicatorFilters & { | ||
query: string | ||
page: number | ||
sort: string | null | ||
geozone: string | null | ||
} | ||
const props = withDefaults(defineProps<Props>(), { query: '' }) | ||
const emits = defineEmits(['clearFilters']) | ||
const { indicators, pagination, total } = storeToRefs(store) | ||
const numberOfResultMsg: ComputedRef<string> = computed(() => { | ||
if (total.value === 1) { | ||
return '1 indicateur disponible' | ||
} else if (total.value > 1) { | ||
return `${total.value} indicateurs disponibles` | ||
} else { | ||
return 'Aucun résultat ne correspond à votre recherche' | ||
} | ||
}) | ||
const clearFilters = () => { | ||
const query: LocationQueryRaw = {} | ||
router.push({ name: 'indicators', query }).then(() => { | ||
emits('clearFilters') | ||
}) | ||
} | ||
const executeQuery = async (args: typeof props) => { | ||
const loader = useLoading().show({ enforceFocus: false }) | ||
return store.query(args).finally(() => loader.hide()) | ||
} | ||
const goToPage = (page: number) => { | ||
router.push({ | ||
name: 'indicators', | ||
query: { ...route.query, page: page + 1 }, | ||
hash: '#indicators-list' | ||
}) | ||
} | ||
const doSort = (value: string | null) => { | ||
router.push({ | ||
name: 'indicators', | ||
query: { ...route.query, sort: value }, | ||
hash: '#indicators-list' | ||
}) | ||
} | ||
// launch search on props (~route.query) changes | ||
watch(props, () => executeQuery(props), { immediate: true, deep: true }) | ||
defineExpose({ | ||
numberOfResultMsg | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div | ||
v-if="indicators.length > 0" | ||
class="fr-grid-row fr-grid-row--gutters fr-grid-row--middle justify-between fr-pb-2w" | ||
> | ||
<h2 class="fr-col-auto fr-my-0 h4">{{ numberOfResultMsg }}</h2> | ||
<div class="fr-col-auto fr-grid-row fr-grid-row--middle"> | ||
<SelectComponent | ||
label="Trier par :" | ||
default-option="Pertinence" | ||
:label-class="['fr-col-auto', 'fr-text--sm', 'fr-m-0', 'fr-mr-1w']" | ||
:options="[ | ||
{ id: '-created', name: 'Les plus récemment créés' }, | ||
{ id: '-last_update', name: 'Les plus récemment modifiés' } | ||
]" | ||
@update:model-value="doSort" | ||
/> | ||
</div> | ||
</div> | ||
<div | ||
v-if="indicators.length === 0" | ||
class="fr-mt-2w rounded-xxs fr-p-3w fr-grid-row flex-direction-column bg-contrast-blue-cumulus" | ||
> | ||
<div class="fr-col fr-grid-row fr-grid-row--gutters text-blue-400"> | ||
<div class="fr-col-auto"> | ||
<img | ||
src="/search/france_with_magnifying_glass.svg" | ||
alt="" | ||
loading="lazy" | ||
class="w-100" | ||
height="134" | ||
width="124" | ||
/> | ||
</div> | ||
<div | ||
class="fr-col-12 fr-col-sm fr-grid-row flex-direction-column justify-between" | ||
> | ||
<div class="fr-mb-1w"> | ||
<h2 class="fr-m-0 fr-mb-1w fr-text--bold fr-text--md"> | ||
Aucun résultat ne correspond à votre recherche | ||
</h2> | ||
<p class="fr-mt-1v fr-mb-3v"> | ||
Essayez de réinitialiser les filtres pour agrandir votre champ de | ||
recherche. | ||
</p> | ||
</div> | ||
<div class="fr-grid-row fr-grid-row--undefined"> | ||
<button class="fr-btn" @click.stop.prevent="clearFilters"> | ||
Réinitialiser les filtres | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="indicators-list-container fr-container fr-mb-4w border-top"> | ||
<ul class="fr-grid-row fr-grid-row--gutters fr-mb-1w fr-mt-2w" role="list"> | ||
<li | ||
v-for="indicator in indicators" | ||
:key="indicator.id" | ||
class="fr-col-md-6 fr-col-md" | ||
> | ||
<IndicatorCard :indicator="indicator" /> | ||
</li> | ||
</ul> | ||
</div> | ||
<DsfrPagination | ||
v-if="pagination.length" | ||
class="fr-container" | ||
:current-page="page - 1" | ||
:pages="pagination" | ||
@update:current-page="goToPage" | ||
/> | ||
</template> | ||
|
||
<style scoped> | ||
/* "revert" gutters — simpler than w/o gutters */ | ||
.indicators-list-container { | ||
padding-right: 0; | ||
padding-left: 0; | ||
} | ||
</style> |
90 changes: 90 additions & 0 deletions
90
src/custom/ecospheres/components/indicators/IndicatorSearch.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<script setup lang="ts"> | ||
import { ref, watchEffect, type Ref } from 'vue' | ||
import { useRoute, useRouter } from 'vue-router' | ||
import SelectSpatialCoverage from '@/components/forms/SelectSpatialCoverage.vue' | ||
import type { SpatialCoverage } from '@/model/spatial' | ||
import SpatialAPI from '@/services/api/SpatialAPI' | ||
import type { IndicatorFilters } from '../../model/indicator' | ||
import { useFiltersConf } from '../../utils/config' | ||
import SelectComponent from '../SelectComponent.vue' | ||
const spatialAPI = new SpatialAPI() | ||
type Props = IndicatorFilters & { | ||
geozone: string | null | ||
} | ||
const props = defineProps<Props>() | ||
const router = useRouter() | ||
const route = useRoute() | ||
const selectedGeozone: Ref<string | null> = ref(null) | ||
const selectedSpatialCoverage: Ref<SpatialCoverage | undefined> = ref(undefined) | ||
const filtersConf = useFiltersConf() | ||
const navigate = (data?: Record<string, string | null>) => { | ||
router.push({ | ||
name: 'indicators', | ||
query: { ...route.query, ...data }, | ||
hash: '#indicators-list' | ||
}) | ||
} | ||
const switchFilter = (filter: string, value: string | null) => { | ||
navigate({ [filter]: value }) | ||
} | ||
const switchSpatialCoverage = ( | ||
spatialCoverage: SpatialCoverage | null | undefined | ||
) => { | ||
selectedGeozone.value = spatialCoverage != null ? spatialCoverage.id : null | ||
navigate({ geozone: selectedGeozone.value }) | ||
} | ||
watchEffect(() => { | ||
if (props.geozone) { | ||
selectedGeozone.value = props.geozone | ||
spatialAPI | ||
.getZone(props.geozone) | ||
.then((zone) => (selectedSpatialCoverage.value = zone)) | ||
} else { | ||
selectedSpatialCoverage.value = undefined | ||
selectedGeozone.value = null | ||
} | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div className="filterForm"> | ||
<div class="fr-select-group"> | ||
<SelectComponent | ||
default-option="Toutes les thématiques" | ||
:label="filtersConf.theme.name" | ||
:options="filtersConf.theme.values" | ||
:model-value="props.theme" | ||
@update:model-value="(value) => switchFilter('theme', value)" | ||
/> | ||
</div> | ||
<div class="fr-select-group"> | ||
<SelectComponent | ||
default-option="Tous les enjeux" | ||
:label="filtersConf.enjeu.name" | ||
:options="filtersConf.enjeu.values" | ||
:model-value="props.enjeu" | ||
@update:model-value="(value) => switchFilter('enjeu', value)" | ||
/> | ||
</div> | ||
<div class="fr-select-group"> | ||
<label class="fr-label" for="select-spatial-coverage" | ||
>Couverture territoriale</label | ||
> | ||
<SelectSpatialCoverage | ||
v-model:spatial-coverage-model="selectedSpatialCoverage" | ||
:short="true" | ||
@update:spatial-coverage-model="switchSpatialCoverage" | ||
/> | ||
</div> | ||
</div> | ||
</template> |
Oops, something went wrong.