Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add company data filter to program catalog #1400

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0c4a6fe
feat: Add company data filter to program listing
dolemoine Dec 5, 2024
706ba35
refactor: Refactor filter accordion display logic
dolemoine Dec 6, 2024
a32811c
Add missing newline in ProgramFiltersAccordion.vue
dolemoine Dec 6, 2024
2433c72
refactor: Rename CSS selector for company data accordion
dolemoine Dec 6, 2024
50fb845
Merge branch 'main' into feat/siret-catalog-impact
dolemoine Dec 11, 2024
7f30aea
refactor: Refactor program filters to use `FilterItemKeys` enum
dolemoine Dec 11, 2024
9c2b49c
refactor: Refactor filter badge logic and improve styling.
dolemoine Dec 11, 2024
60d8c7d
Merge branch 'main' into feat/siret-catalog-impact
dolemoine Dec 11, 2024
19e2bb8
refactor: Refactor timeout handling and improve configuration setup
dolemoine Dec 12, 2024
c1f78a6
merge main
dolemoine Dec 16, 2024
f94172e
refactor: Refactor ProgramFiltersAccordion to separate company filter…
dolemoine Dec 16, 2024
d035dc3
refactor: Refactor companyDataAccordion to a primitive value
dolemoine Dec 16, 2024
8b87f2a
fix: Set `onlyEligible` to false for catalog navigation
dolemoine Dec 16, 2024
78bad72
refactor: Refactor company filter to improve UI clarity and styling.
dolemoine Dec 16, 2024
18a676e
refactor: Increase timeout duration in configuration for playwright web
dolemoine Dec 16, 2024
2b7d7da
feat: Add `resetFilter` method and integrate it in ProgramFilter
dolemoine Dec 16, 2024
118be69
feat: Add size-to-text mapping for company sizes in filters
dolemoine Dec 16, 2024
51defc7
feat: Add responsive text size for labels in program filters
dolemoine Dec 18, 2024
eeae82c
Merge branch 'main' into feat/siret-catalog-impact
dolemoine Dec 18, 2024
24b96b5
Refactor: Replace CompanyDataStorage with CompanyData usage
dolemoine Dec 18, 2024
b4939f3
Refactor: Update company data handling to use new utility
dolemoine Dec 18, 2024
f285754
refactor: Refine icon styling in ProgramFilterByCompanyData.vue
dolemoine Dec 18, 2024
f239eec
refactor: Enable "NotEligible" state link in ProgramEligibilityBar
dolemoine Dec 18, 2024
d56d4b2
refactor: Add padding-right to program filter company detail section
dolemoine Dec 18, 2024
afcf381
Refactor: Rename programFiltersType to ProgramFiltersType
dolemoine Dec 18, 2024
21b2b34
refactor: Adjust text and icon sizes for Company Data Filter
dolemoine Dec 20, 2024
250be64
refactor: Refine styles and layout for company data filter.
dolemoine Dec 23, 2024
4742b63
fix: Fix unintended style issue in ProgramFilterByCompanyData
dolemoine Dec 23, 2024
a7ad6a2
Merge branch 'main' into feat/siret-catalog-impact
dolemoine Dec 23, 2024
534ed70
Merge branch 'main' into feat/siret-catalog-impact
dolemoine Dec 26, 2024
66187fe
refactor: Enhance vertical alignment for company data filter labels
dolemoine Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: [['html', { open: 'never' }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL: 'http://localhost:4242',
Expand Down
1 change: 1 addition & 0 deletions apps/web-e2e/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const timeOut = 5000
39 changes: 20 additions & 19 deletions apps/web-e2e/src/formResults.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test'
import { tests } from './formResultsData'
import { timeOut } from './config'


tests.forEach((singleTest) => {
Expand All @@ -8,12 +9,12 @@ tests.forEach((singleTest) => {
page.on('response', async (response) => {
if (response.url().includes('/api/opportunities')) {
try {
await page.locator('[teste2e-selector="callback-contact-form"]').waitFor({ timeout: 3000, state: 'visible' })
await page.locator('[teste2e-selector="callback-contact-form"]').waitFor({ timeout: timeOut, state: 'visible' })
if (response.status() === 200) {
await expect(page.locator('[teste2e-selector="success-callback-contact-form"]')).toBeVisible({ timeout: 3000 })
await expect(page.locator('[teste2e-selector="success-callback-contact-form"]')).toBeVisible({ timeout: timeOut })
} else {
console.log('error during opportunityApiCall');
await expect(page.locator('[teste2e-selector="error-callback-contact-form"]')).toBeVisible({ timeout: 3000 })
await expect(page.locator('[teste2e-selector="error-callback-contact-form"]')).toBeVisible({ timeout: timeOut })
}
} catch (e) {
throw new Error(`Error handling API response: ${e.message}`)
Expand Down Expand Up @@ -52,25 +53,25 @@ tests.forEach((singleTest) => {
}
localStorage.setItem('structure_size', JSON.stringify('TPE'))
}, singleTest)

await page.reload({ waitUntil: 'load' })
await page.waitForLoadState('networkidle')

if (singleTest.type === 'customProject') {
await page.waitForSelector('[teste2e-selector="open-custom-project-form"]', {
timeout: 3000,
timeout: timeOut,
state: 'visible',
})
await page.click('[teste2e-selector="open-custom-project-form"]', { timeout: 3000 })
await page.click('[teste2e-selector="open-custom-project-form"]', { timeout: timeOut })
}
await expect(page.locator(`form[name="${singleTest.type}"]`)).toHaveCount(1, { timeout: 3000 })
await expect(page.locator(`form[name="${singleTest.type}"]`)).toHaveCount(1, { timeout: timeOut })
for (const [fieldKey, value] of Object.entries(singleTest.values)) {
const selector = `[teste2e-selector="${fieldKey}-${value.type}"]`

try {
if (fieldKey === 'siret') {
if (singleTest.manual) {
await page.locator(selector).fill(value.value as string, { timeout: 3000 })
await page.locator(selector).fill(value.value as string, { timeout: timeOut })
} else {
const actualSiretValue = await page.inputValue(selector)
expect(actualSiretValue).toBe(value.value)
Expand All @@ -79,29 +80,29 @@ tests.forEach((singleTest) => {
const actualNeedsValue = await page.inputValue(selector)
expect(actualNeedsValue).toContain(singleTest.manual ? 'tertiaire' : 'Programmation informatique')
} else if (['text', 'email', 'tel'].includes(value.type)) {
await page.locator(selector).fill(value.value as string, { timeout: 3000 })
await page.locator(selector).fill(value.value as string, { timeout: timeOut })
} else if (value.type === 'select' && singleTest.type === 'customProject') {
await page.locator(selector).selectOption({ label: value.value as string })
} else if (value.type === 'checkbox' && value.value) {
await page.locator(selector).click({ force: true, timeout: 3000 })
await page.locator(selector).click({ force: true, timeout: timeOut })
}
} catch (e) {
throw new Error(`Error processing field ${fieldKey}: ${e.message}`)
}
}

const submitButton = page.locator('button[type="submit"]')
await expect(submitButton).toHaveCount(1, { timeout: 3000 })
await expect(submitButton).toHaveCount(1, { timeout: timeOut })

if (singleTest.valid) {
await expect(submitButton).toBeEnabled({ timeout: 3000 })
await submitButton.click({ force: true, timeout: 3000 })
await expect(submitButton).toBeEnabled({ timeout: timeOut })
await submitButton.click({ force: true, timeout: timeOut })
} else {
await expect(submitButton).toBeDisabled({ timeout: 3000 })
await expect(submitButton).toBeDisabled({ timeout: timeOut })
}
} catch (e) {
console.error(`Test failed for Test ID: ${singleTest.id} - ${e.message}`)
throw e
}
});
})
})
3 changes: 2 additions & 1 deletion apps/web-e2e/src/programResults.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test'
import { tests } from './programResultsData'
import { timeOut } from './config'

/**
* Test the number of programs proposed as a result of a list of queries and their order.
Expand All @@ -8,7 +9,7 @@ tests.forEach((singleTest) => {
test(`Test id ${singleTest.id} - Verify programs number and order for query ${singleTest.url}`, async ({ page }) => {
await page.goto(singleTest.url)
try {
await page.waitForSelector('.teste2e-program-target', { timeout: 3000 })
await page.waitForSelector('.teste2e-program-target', { timeout: timeOut })
} catch (error) {
// this is an expected error what can happen
// - if the number of results is 0
Expand Down
7 changes: 4 additions & 3 deletions apps/web-e2e/src/projectResults.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { test, expect } from '@playwright/test'
import { tests } from './projectResultsData'
import { timeOut } from './config'

tests.forEach((singleTest) => {
test(`Test id ${singleTest.id} - Verify content and elements for query ${singleTest.url}`, async ({ page }) => {
await page.goto(singleTest.url)
try {
await page.waitForSelector('.teste2e-project-target', { timeout: 3000 })
await page.waitForSelector('.teste2e-project-target', { timeout: timeOut })
} catch (error) {
// this is an expected error that can happen
// - if the number of results is 0
Expand All @@ -29,7 +30,7 @@ test(`Check projects found while initially selecting different tags`, async ({ p

await page.goto(urlTag1)
try {
await page.waitForSelector('.teste2e-project-target', { timeout: 3000 })
await page.waitForSelector('.teste2e-project-target', { timeout: timeOut })
} catch (error) {
// this is an expected error that can happen
// - if the number of results is 0
Expand All @@ -38,7 +39,7 @@ test(`Check projects found while initially selecting different tags`, async ({ p
const elementsurlTag1 = await page.$$eval('.teste2e-project-target h3 a', (els) => els.map((el) => el.innerHTML.trim()))
await page.goto(urlTag2)
try {
await page.waitForSelector('.teste2e-project-target', { timeout: 3000 })
await page.waitForSelector('.teste2e-project-target', { timeout: timeOut })
} catch (error) {
// this is an expected error that can happen
// - if the number of results is 0
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ declare module 'vue' {
DsfrCheckbox: typeof import('@gouvminint/vue-dsfr')['DsfrCheckbox']
DsfrCheckboxSet: typeof import('@gouvminint/vue-dsfr')['DsfrCheckboxSet']
DsfrHighlight: typeof import('@gouvminint/vue-dsfr')['DsfrHighlight']
DsfrIcon: typeof import('@gouvminint/vue-dsfr')['DsfrIcon']
DsfrInput: typeof import('@gouvminint/vue-dsfr')['DsfrInput']
DsfrInputGroup: typeof import('@gouvminint/vue-dsfr')['DsfrInputGroup']
DsfrRadioButtonSet: typeof import('@gouvminint/vue-dsfr')['DsfrRadioButtonSet']
Expand All @@ -38,6 +39,7 @@ declare module 'vue' {
ProgramEligibility: typeof import('./components/program/detail/ProgramEligibility.vue')['default']
ProgramEligibilityBar: typeof import('./components/program/detail/ProgramEligibilityBar.vue')['default']
ProgramFilterByAidType: typeof import('./components/program/list/filters/ProgramFilterByAidType.vue')['default']
ProgramFilterByCompanyData: typeof import('./components/program/list/filters/ProgramFilterByCompanyData.vue')['default']
ProgramFilterByOperator: typeof import('./components/program/list/filters/ProgramFilterByOperator.vue')['default']
ProgramFilterByRegion: typeof import('./components/program/list/filters/ProgramFilterByRegion.vue')['default']
ProgramFiltersAccordion: typeof import('./components/program/list/filters/ProgramFiltersAccordion.vue')['default']
Expand Down
23 changes: 17 additions & 6 deletions apps/web/src/components/catalog/CatalogPrograms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,19 @@

<script setup lang="ts">
import { useProgramStore } from '@/stores/program'
import { type ProgramData, ThemeId } from '@/types'
import { FilterItemKeys, type ProgramData, ThemeId } from '@/types'
import { MetaSeo } from '@/utils/metaSeo'
import UsedTrack from '@/utils/track/usedTrack'
import { computed, onBeforeMount } from 'vue'
import CompanyDataStorage from '@/utils/storage/companyDataStorage'

const programStore = useProgramStore()

const programs = ref<ProgramData[]>()
const hasError = ref<boolean>(false)

const registeredData = CompanyDataStorage.getData()

const title = 'Le catalogue des aides publiques à la transition écologique'
const description =
'Réalisez une recherche parmi les aides à la transition écologique des entreprises, proposées par l’ensemble des partenaires publics :' +
Expand All @@ -111,7 +114,7 @@ const hasThemeCard = computed(() => {

const theme = computed(() => {
if (programStore.hasThemeTypeSelected()) {
return programStore.programFilters.themeTypeSelected
return programStore.programFilters[FilterItemKeys.themeType]
}

if (UsedTrack.isSpecificGoal() && UsedTrack.hasPriorityTheme()) {
Expand All @@ -129,15 +132,23 @@ const showThemeCard = computed(() => {
return hasThemeCard.value && !hasSpinner.value
})

onBeforeMount(async () => {
useSeoMeta(MetaSeo.get(title, description))

const result = await programStore.programs
const getPrograms = async () => {
const result = await programStore.programsByUsedTracks
if (result.isOk) {
programs.value = result.value
} else {
hasError.value = true
}
}

onBeforeMount(async () => {
useSeoMeta(MetaSeo.get(title, description))

await getPrograms()
})

watch(registeredData.value, async () => {
await getPrograms()
})

onBeforeRouteLeave(() => {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/identification/TeeRegisterModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@
</template>
<script setup lang="ts">
import Translation from '@/utils/translation'
import { EstablishmentFront, CompanyDataStorageKey, CompanyDataType } from '@/types'
import { EstablishmentFront, CompanyDataStorageKey, CompanyDataType, FilterItemKeys } from '@/types'
import Breakpoint from '@/utils/breakpoints'
import CompanyDataStorage from '@/utils/storage/companyDataStorage'
import { onClickOutside } from '@vueuse/core'
import Navigation from '@/utils/navigation'
import { useNavigationStore } from '@/stores/navigation'
import { CompanyDataStorageHandler } from '@/utils/storage/companyDataStorageHandler'
import { useProgramStore } from '@/stores/program'

const registerModal = ref(null)
const registeredData = CompanyDataStorage.getData()
Expand Down Expand Up @@ -93,6 +94,7 @@ const resetSiret = () => {
manualRegistration.value = false
CompanyDataStorage.removeItem(CompanyDataStorageKey.Company)
CompanyDataStorage.removeItem(CompanyDataStorageKey.Size)
useProgramStore().programFilters[FilterItemKeys.companyData] = false
CompanyDataStorageHandler.updateRouteFromStorage()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<DsfrCheckboxSet
v-model="programFilters.programAidTypesSelected"
v-model="programFilters[FilterItemKeys.typeAid]"
small
:options="programAidTypeOptions"
/>
</template>

<script setup lang="ts">
import { useProgramStore } from '@/stores/program'
import { ProgramAidType, type programFiltersType } from '@/types'
import { FilterItemKeys, ProgramAidType, type programFiltersType } from '@/types'
import { DsfrCheckboxSetProps } from '@gouvminint/vue-dsfr'

const programFilters: programFiltersType = useProgramStore().programFilters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<div
class="fr-py-2v"
:class="{
'fr-bg--green--lightness': programFilters[FilterItemKeys.companyData],
'fr-bg--grey--lightness': !programFilters[FilterItemKeys.companyData]
}"
>
<DsfrCheckbox
v-model="programFilters[FilterItemKeys.companyData]"
:value="`selected-company-${companyName}`"
small
name="companyFilter"
>
<template #label>
<span
class="fr-text--bold fr-pl-0-5v"
:class="{ 'fr-text--grey': !programFilters[FilterItemKeys.companyData] }"
>{{ filterData.title }}</span
>
</template>
</DsfrCheckbox>
<div class="fr-pl-1v fr-pb-2v fr-text-left">
<div
v-for="(detail, key) in filterData.details"
:key="key"
class="fr-mb-4v"
:class="{ 'fr-text--grey': !programFilters[FilterItemKeys.companyData] }"
>
<div class="fr-grid-row">
<div class="fr-col-1">
<span :class="detail.icon" />
</div>
<div class="fr-col-11 fr-pl-md-4v">
<span class="fr-text--md">{{ detail.label }}</span>
</div>
</div>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { useProgramStore } from '@/stores/program'
import { CompanyDataStorageKey, FilterItemKeys, type programFiltersType, SizeToText, StructureSize } from '@/types'
import CompanyDataStorage from '@/utils/storage/companyDataStorage'

type CompanyFilterProps = {
title: ComputedRef<string | undefined>
details: {
sector: CompanyFilterDetailProps
region: CompanyFilterDetailProps
size: CompanyFilterDetailProps
}
}

type CompanyFilterDetailProps = {
label: ComputedRef<string | undefined | null>
icon?: string
}

const programFilters: programFiltersType = useProgramStore().programFilters

const registeredData = CompanyDataStorage.getData()
const hasRegisteredData = CompanyDataStorage.isDataFull()
const companyName = computed(() => registeredData.value[CompanyDataStorageKey.Company]?.denomination)
const companySector = computed(() => registeredData.value[CompanyDataStorageKey.Company]?.secteur)
const companyRegion = computed(() => registeredData.value[CompanyDataStorageKey.Company]?.region)
const companySize = computed(() => SizeToText[registeredData.value[CompanyDataStorageKey.Size] as StructureSize])

const filterData: CompanyFilterProps = {
title: companyName,
details: {
sector: { label: companySector, icon: 'fr-icon-briefcase-line' },
region: { label: companyRegion, icon: 'fr-icon-map-pin-2-line' },
size: { label: companySize, icon: 'fr-icon-team-line' }
}
}
watch(
hasRegisteredData,
(value) => {
programFilters[FilterItemKeys.companyData] = value
},
{ immediate: true }
)
</script>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<DsfrCheckboxSet
v-model="programFilters.operatorAidSelected"
v-model="programFilters[FilterItemKeys.operatorAid]"
small
:options="programOperatorOptions"
/>
Expand All @@ -9,7 +9,7 @@
<script setup lang="ts">
import { useProgramStore } from '@/stores/program'
import { DsfrCheckboxSetProps } from '@gouvminint/vue-dsfr'
import { OperatorFilter, type programFiltersType } from '@/types'
import { FilterItemKeys, OperatorFilter, type programFiltersType } from '@/types'

import { enrichedOperators } from '@tee/data/static'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<DsfrCheckboxSet
v-model="programFilters.regionAidSelected"
v-model="programFilters[FilterItemKeys.regionAid]"
small
:options="programRegionsOptions"
/>
</template>

<script setup lang="ts">
import { useProgramStore } from '@/stores/program'
import { Region, type programFiltersType } from '@/types'
import { Region, type programFiltersType, FilterItemKeys } from '@/types'
import { DsfrCheckboxSetProps } from '@gouvminint/vue-dsfr'

const programFilters: programFiltersType = useProgramStore().programFilters
Expand Down
Loading
Loading