Skip to content
Snippets Groups Projects
Commit f45af282 authored by Bastien DUMONT's avatar Bastien DUMONT :angel:
Browse files

Merge branch 'refactor/rq-for-customPopup' into 'dev'

refactor: use react-query for customPopup

See merge request !158
parents 075a28a2 33369fd9
Branches
No related tags found
2 merge requests!167chore(release): Ecolyo 3.1.0,!158refactor: use react-query for customPopup
Pipeline #104792 failed
# Ecolyo Agent Client
[check documentation here](https://doc-self-data.apps.grandlyon.com/ecolyo-agent/technical/getting_started/#local-usage)
[check documentation here](https://doc-self-data.apps.grandlyon.com/docs/ecolyo-agent/Technical/getting-started/#local-usage)
......@@ -16,7 +16,7 @@ const endpoints = {
animator: {
partnersInfo: '/api/animator/partnersInfo',
imageNames: '/api/animator/imageNames',
savePartnersInfo: '/api/animator/savePartnersInfo',
customPopup: '/api/animator/customPopup',
},
} as const
......@@ -38,7 +38,7 @@ export const fetchEcogestureImages = async (
axiosHeaders: AxiosRequestConfig
) => {
const { data: imageNames } = await axios.get<string[]>(
`/api/animator/imageNames`,
endpoints.animator.imageNames,
axiosHeaders
)
if (imageNames && imageNames !== null) {
......@@ -55,12 +55,12 @@ export const fetchPartnersIssue = async () => {
return data
} catch (error) {
console.error('error partnersInfo', error)
toast.error('Accès refusé, veuillez vous connecter')
toast.error('Aucune information des partenaires trouvée')
throw error
}
}
export const savePartnersInfo = async (
export const putPartnersInfo = async (
partnersInfo: IPartnersInfo,
axiosHeaders: AxiosRequestConfig
) => {
......@@ -88,7 +88,26 @@ export const fetchCustomPopup = async (): Promise<ICustomPopup> => {
return data
} catch (error) {
console.error('error customPopup', error)
toast.error('Accès refusé, veuillez vous connecter')
toast.error('Aucune pop-up personnalisée trouvée')
throw error
}
}
export const putCustomPopup = async (
customPopup: ICustomPopup,
axiosHeaders: AxiosRequestConfig
) => {
try {
await axios.put(
endpoints.animator.customPopup,
{
...customPopup,
},
axiosHeaders
)
toast.success('Pop-up personnalisée enregistrée !')
} catch (e) {
toast.error('Erreur lors de l’enregistrement de la pop-up personnalisée')
console.error(e)
}
}
......@@ -12,6 +12,7 @@ import React, { useEffect, useState } from 'react'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import { Link } from 'react-router-dom'
import { getAxiosXSRFHeader } from '../../axios.config'
import { useCustomPopup } from '../../hooks/useCustomPopup'
import { usePartnersIssue } from '../../hooks/usePartnersIssue'
import { useWhoAmI } from '../../hooks/useWhoAmI'
import { ICustomPopup, PopupDuration } from '../../models/customPopup.model'
......@@ -21,7 +22,6 @@ import {
durationType,
mapDuration,
} from '../../models/durationOptions.model'
import { CustomPopupService } from '../../services/customPopup.service'
import { convertStringToEditorState } from '../../utils/editorStateManagement'
import { getFilenameFromPath } from '../../utils/imagesUrlsGetter'
import ImagePicker from '../ImagePicker/ImagePicker'
......@@ -29,6 +29,7 @@ import Loader from '../Loader/Loader'
import CustomEditor from '../Newsletter/CustomEditor'
import { links } from '../Routes/Router'
import './popups.scss'
import { PopupWrapper } from './Wrapper'
const OPTIONS: Option[] = [
{
......@@ -46,37 +47,75 @@ const OPTIONS: Option[] = [
]
export const CustomPopup: React.FC = () => {
const { data: user } = useWhoAmI()
const { partnersIssue } = usePartnersIssue()
const defaultIcon = '/assets/ecogesture/bullhorn.png'
const { customPopup } = useCustomPopup()
const isPartnerNotificationOn = Boolean(
partnersIssue.data &&
(partnersIssue.data.enedis_failure ||
partnersIssue.data.egl_failure ||
partnersIssue.data.grdf_failure)
)
if (isPartnerNotificationOn) {
return (
<PopupWrapper>
<p className="singlePopupWarning">
La pop-up personnalisée ne peut pas être activée en même temps que la{' '}
<Link to={links.partnersIssue.path}>
pop-up maintenance des partenaires
</Link>
</p>
</PopupWrapper>
)
}
if (customPopup.isLoading) {
return <Loader />
}
if (customPopup.error) {
return <p>Une erreur est survenue</p>
}
if (customPopup.data) {
return (
<PopupWrapper>
<CustomPopupForm initialData={customPopup.data} />
</PopupWrapper>
)
}
}
export const CustomPopupForm = ({
initialData,
}: {
initialData: ICustomPopup
}) => {
const { data: user } = useWhoAmI()
const { saveCustomPopup } = useCustomPopup()
const isPopupOutdated = (date: string) =>
DateTime.local() >= DateTime.fromISO(date)
const isOutdated = isPopupOutdated(initialData.endDate)
const isEnabled = isOutdated ? false : initialData.popupEnabled
const [refreshData, setRefreshData] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [previousEndDate, setPreviousEndDate] = useState<string>()
const [popupDuration, setPopupDuration] = useState<PopupDuration>({
type: durationEnum.infinite,
duration: 5,
})
const [customPopup, setCustomPopup] = useState<ICustomPopup>({
popupEnabled: false,
title: '',
image: '',
description: '',
endDate: DateTime.local().plus({ days: 365 }).toISO(),
popupEnabled: isEnabled,
title: initialData.title,
image: initialData.image || 'bullhorn',
description: initialData.description,
endDate: isOutdated
? DateTime.local().plus({ days: 365 }).toISO()
: initialData.endDate,
})
const isPartnerNotificationOn = () =>
Boolean(
partnersIssue.data &&
(partnersIssue.data.enedis_failure ||
partnersIssue.data.egl_failure ||
partnersIssue.data.grdf_failure)
)
/** Only one type of popup can be enabled */
const isPageValid = () =>
!(isPartnerNotificationOn() && customPopup.popupEnabled)
const toggleCustomPopup = (value: boolean): void => {
setCustomPopup(prev => ({
...prev,
......@@ -94,57 +133,15 @@ export const CustomPopup: React.FC = () => {
}))
}
useEffect(() => {
let subscribed = true
setIsLoading(true)
async function loadCustomPopup() {
if (user) {
const customPopupService = new CustomPopupService()
const previousPopup = await customPopupService.getCustomPopupInfo()
if (previousPopup) {
const isOutdated = isPopupOutdated(previousPopup.endDate)
/** If outdated, set value to false, otherwise, set it to its value */
const isEnabled = isOutdated ? false : previousPopup.popupEnabled
const popupImage =
previousPopup.image === ''
? defaultIcon
: `/assets/ecogesture/${previousPopup.image}.png`
setCustomPopup({
...previousPopup,
image: popupImage,
endDate: customPopup.endDate,
popupEnabled: isEnabled,
})
setPreviousEndDate(
isOutdated ? customPopup.endDate : previousPopup.endDate
)
}
}
setIsLoading(false)
}
if (subscribed) {
loadCustomPopup()
}
return () => {
subscribed = false
setRefreshData(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, refreshData, setCustomPopup])
const handleSave = async (): Promise<void> => {
if (user) {
const customPopupService = new CustomPopupService()
await customPopupService.saveCustomPopup(
saveCustomPopup(
{
...customPopup,
image: getFilenameFromPath(customPopup.image),
},
getAxiosXSRFHeader(user.xsrftoken)
)
setPreviousEndDate(customPopup.endDate)
}
}
......@@ -158,30 +155,22 @@ export const CustomPopup: React.FC = () => {
/** Handles duration change */
useEffect(() => {
const now = DateTime.local()
let newDate: DateTime
if (popupDuration.type !== durationEnum.infinite) {
newDate = now.plus({
[popupDuration.type]: popupDuration.duration,
})
} else {
newDate = now.plus({
years: 1,
})
}
const newDate =
popupDuration.type !== durationEnum.infinite
? now.plus({ [popupDuration.type]: popupDuration.duration })
: now.plus({ years: 1 })
setCustomPopup(prev => ({
...prev,
endDate: newDate.toISO() ?? '',
}))
}, [popupDuration])
const isPopupOutdated = (date: string) =>
DateTime.local() >= DateTime.fromISO(date)
/**
* Returns "Popup expirée" OR "Temps restant : ..."
*/
const getRemainingDuration = (date: string) => {
if (isPopupOutdated(date)) {
if (isOutdated) {
return <p className="endDate">Popup expirée</p>
}
return (
......@@ -197,136 +186,105 @@ export const CustomPopup: React.FC = () => {
return (
<>
<div className="header">
<h1>Création de Pop-up</h1>
</div>
<div className="customPopup">
<h3>Affichage de pop-up personnalisée</h3>
<FormGroup style={{ flexDirection: 'row' }}>
<FormControlLabel
label="Pop-up active"
labelPlacement="top"
control={
<Switch
checked={customPopup.popupEnabled}
onChange={event => toggleCustomPopup(event.target.checked)}
/>
}
/>
{customPopup.popupEnabled &&
initialData.endDate &&
getRemainingDuration(initialData.endDate)}
</FormGroup>
<div className="content popups">
{isLoading && <Loader />}
{!isLoading && (
<>
<div className="customPopup">
<h3>Affichage de pop-up personnalisée</h3>
<FormGroup style={{ flexDirection: 'row' }}>
<FormControlLabel
label="Pop-up active"
labelPlacement="top"
control={
<Switch
disabled={isPartnerNotificationOn()}
checked={customPopup.popupEnabled}
onChange={event =>
toggleCustomPopup(event.target.checked)
}
/>
}
/>
{customPopup.popupEnabled &&
previousEndDate &&
getRemainingDuration(previousEndDate)}
{isPartnerNotificationOn() && (
<p className="singlePopupWarning">
La pop-up personnalisée ne peut pas être activée en même
temps que la{' '}
<Link to={links.partnersIssue.path}>
pop-up maintenance des partenaires
</Link>
</p>
)}
</FormGroup>
<div className="popupEndDate">
<h4>Durée</h4>
<div>
<FormControl style={{ flexDirection: 'row', gap: '1rem' }}>
<NativeSelect
inputProps={{
name: 'age',
id: 'uncontrolled-native',
}}
onChange={event => handleSelectChange(event)}
value={popupDuration.type}
>
{OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</NativeSelect>
{popupDuration.type !== 'infinite' && (
<TextField
style={{ width: '6rem' }}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
id="outlined-number"
type="number"
label={mapDuration[popupDuration.type]}
InputLabelProps={{
shrink: true,
}}
value={popupDuration.duration}
onChange={e =>
setPopupDuration(prev => ({
...prev,
duration: Number(e.target.value),
}))
}
/>
)}
</FormControl>
</div>
</div>
<h4>Image</h4>
<div>
<ImagePicker
imageURL={customPopup.image}
handleChange={handlePopupChange}
defaultIcon={defaultIcon}
/>
</div>
<div className="popupEndDate">
<h4>Durée</h4>
<div>
<FormControl style={{ flexDirection: 'row', gap: '1rem' }}>
<NativeSelect
inputProps={{
name: 'age',
id: 'uncontrolled-native',
}}
onChange={event => handleSelectChange(event)}
value={popupDuration.type}
>
{OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</NativeSelect>
<h4>Contenu</h4>
<div className="popupTitle">
{popupDuration.type !== 'infinite' && (
<TextField
type="text"
placeholder="Titre de la popup"
fullWidth
label="Titre"
value={customPopup.title}
onChange={event =>
handlePopupChange(event.target.value, 'title')
style={{ width: '6rem' }}
inputProps={{
inputMode: 'numeric',
pattern: '[0-9]*',
}}
id="outlined-number"
type="number"
label={mapDuration[popupDuration.type]}
InputLabelProps={{
shrink: true,
}}
value={popupDuration.duration}
onChange={e =>
setPopupDuration(prev => ({
...prev,
duration: Number(e.target.value),
}))
}
/>
</div>
)}
</FormControl>
</div>
</div>
<div className="popupDescription">
<CustomEditor
baseState={convertStringToEditorState(
customPopup.description
)}
handleChange={value =>
handlePopupChange(value, 'description')
}
type="custom_popup"
/>
</div>
</div>
<h4>Image</h4>
<div>
<ImagePicker
imageURL={`/assets/ecogesture/${customPopup.image}.png`}
handleChange={handlePopupChange}
/>
</div>
<h4>Contenu</h4>
<div className="popupTitle">
<TextField
type="text"
placeholder="Titre de la popup"
fullWidth
label="Titre"
value={customPopup.title}
onChange={event => handlePopupChange(event.target.value, 'title')}
/>
</div>
<div className="popupDescription">
<CustomEditor
baseState={convertStringToEditorState(customPopup.description)}
handleChange={value => handlePopupChange(value, 'description')}
type="custom_popup"
/>
</div>
</div>
<div className="buttons">
<Button variant="outlined" onClick={() => setRefreshData(true)}>
Annuler
</Button>
<Button disabled={!isPageValid()} onClick={handleSave}>
Sauvegarder
</Button>
</div>
</>
)}
<div className="buttons">
<Button variant="outlined" onClick={() => setCustomPopup(initialData)}>
Annuler
</Button>
<Button onClick={handleSave} disabled={!user}>
Sauvegarder
</Button>
</div>
</>
)
......
......@@ -7,6 +7,7 @@ import { useWhoAmI } from '../../hooks/useWhoAmI'
import Loader from '../Loader/Loader'
import { links } from '../Routes/Router'
import './popups.scss'
import { PopupWrapper } from './Wrapper'
export const PartnersIssue = () => {
const { data: user } = useWhoAmI()
......@@ -40,85 +41,70 @@ export const PartnersIssue = () => {
}
}
return (
<>
<div className="header">
<h1>Création de Pop-up</h1>
</div>
<div className="content popups">
<div className="partnersInfo">
<h3>Affichage des pop-up de panne</h3>
<div>
{partnersIssue.isPending && <Loader />}
{partnersIssue.isError && <p>Erreur</p>}
{partnersIssue.data && (
<div>
<p>Services concernés</p>
<FormGroup style={{ flexDirection: 'row' }}>
<FormControlLabel
label={'Panne Enedis'}
labelPlacement="top"
control={
<Switch
disabled={isCustomPopupEnabled}
checked={partnersIssue.data.enedis_failure}
onChange={event => {
handlePartnerIssue(
event.target.checked,
'enedis_failure'
)
}}
/>
}
/>
<FormControlLabel
label={'Panne EGL'}
labelPlacement="top"
control={
<Switch
disabled={isCustomPopupEnabled}
checked={partnersIssue.data.egl_failure}
onChange={event => {
handlePartnerIssue(
event.target.checked,
'egl_failure'
)
}}
/>
}
/>
<FormControlLabel
label={'Panne GRDF'}
labelPlacement="top"
control={
<Switch
disabled={isCustomPopupEnabled}
checked={partnersIssue.data.grdf_failure}
onChange={event => {
handlePartnerIssue(
event.target.checked,
'grdf_failure'
)
}}
/>
}
/>
</FormGroup>
if (partnersIssue.isLoading) {
return <Loader />
}
if (partnersIssue.error) {
return <p>Une erreur est survenue</p>
}
{isCustomPopupEnabled && (
<p className="singlePopupWarning">
La pop-up maintenance des partenaires ne peut pas être
activée en même temps que la{' '}
<Link to={links.customPopup.path}>
pop-up personnalisée
</Link>
</p>
)}
</div>
)}
</div>
</div>
</div>
</>
)
if (partnersIssue.data) {
return (
<PopupWrapper>
<h3>Affichage des pop-up de panne</h3>
<p>Services concernés</p>
<FormGroup style={{ flexDirection: 'row' }}>
<FormControlLabel
label={'Panne Enedis'}
labelPlacement="top"
control={
<Switch
disabled={isCustomPopupEnabled}
checked={partnersIssue.data.enedis_failure}
onChange={event => {
handlePartnerIssue(event.target.checked, 'enedis_failure')
}}
/>
}
/>
<FormControlLabel
label={'Panne EGL'}
labelPlacement="top"
control={
<Switch
disabled={isCustomPopupEnabled}
checked={partnersIssue.data.egl_failure}
onChange={event => {
handlePartnerIssue(event.target.checked, 'egl_failure')
}}
/>
}
/>
<FormControlLabel
label={'Panne GRDF'}
labelPlacement="top"
control={
<Switch
disabled={isCustomPopupEnabled}
checked={partnersIssue.data.grdf_failure}
onChange={event => {
handlePartnerIssue(event.target.checked, 'grdf_failure')
}}
/>
}
/>
</FormGroup>
{isCustomPopupEnabled && (
<p className="singlePopupWarning">
La pop-up maintenance des partenaires ne peut pas être activée en
même temps que la{' '}
<Link to={links.customPopup.path}>pop-up personnalisée</Link>
</p>
)}
</PopupWrapper>
)
}
}
import './popups.scss'
export const PopupWrapper = ({ children }: { children: React.ReactNode }) => {
return (
<>
<div className="header">
<h1>Création de Pop-up</h1>
</div>
<div className="content popups">{children}</div>
</>
)
}
@import '../../styles/config/colors';
.popups {
.partnersInfo {
margin-bottom: 2rem;
}
p.endDate {
color: $gold-dark;
font-weight: bold;
......
import { useQuery } from '@tanstack/react-query'
import { fetchCustomPopup } from '../API'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AxiosRequestConfig } from 'axios'
import { fetchCustomPopup, putCustomPopup } from '../API'
import { ICustomPopup } from '../models/customPopup.model'
import { queryKeys } from './query-keys'
export const useCustomPopup = () => {
const queryClient = useQueryClient()
const customPopup = useQuery({
queryKey: [queryKeys.customPopup],
queryFn: fetchCustomPopup,
})
return { customPopup }
const mutation = useMutation<
void,
unknown,
{ customPopup: ICustomPopup; axiosHeaders: AxiosRequestConfig }
>({
mutationFn: ({ customPopup, axiosHeaders }) =>
putCustomPopup(customPopup, axiosHeaders),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: [queryKeys.customPopup] }),
})
const saveCustomPopup = (
customPopup: ICustomPopup,
axiosHeaders: AxiosRequestConfig
) => {
mutation.mutate({ customPopup, axiosHeaders })
}
return { customPopup, saveCustomPopup }
}
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AxiosRequestConfig } from 'axios'
import { fetchPartnersIssue, savePartnersInfo } from '../API'
import { fetchPartnersIssue, putPartnersInfo } from '../API'
import { IPartnersInfo } from '../models/partnersInfo.model'
import { queryKeys } from './query-keys'
......@@ -18,10 +18,9 @@ export const usePartnersIssue = () => {
{ partnersInfo: IPartnersInfo; axiosHeaders: AxiosRequestConfig }
>({
mutationFn: ({ partnersInfo, axiosHeaders }) =>
savePartnersInfo(partnersInfo, axiosHeaders),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [queryKeys.partnersInfo] })
},
putPartnersInfo(partnersInfo, axiosHeaders),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: [queryKeys.partnersInfo] }),
})
const savePartnersIssue = (
......
......@@ -10,5 +10,6 @@ export interface ICustomPopup {
export interface PopupDuration {
type: durationType
/** number of days or hours */
duration: number
}
import axios, { AxiosRequestConfig } from 'axios'
import { toast } from 'react-toastify'
import { ICustomPopup } from '../models/customPopup.model'
export class CustomPopupService {
/**
* Save the customPopup info
* @param customPopup
* @param axiosHeaders
*/
public saveCustomPopup = async (
customPopup: ICustomPopup,
axiosHeaders: AxiosRequestConfig
): Promise<void> => {
try {
await axios.put(
`/api/animator/customPopup`,
{
...customPopup,
},
axiosHeaders
)
toast.success('Pop-up personnalisée enregistrée !')
} catch (e) {
toast.error('Erreur lors de l’enregistrement de la pop-up personnalisée')
console.error(e)
}
}
/**
* Gets the custom pop-up information
*/
public getCustomPopupInfo = async (): Promise<ICustomPopup | null> => {
try {
const { data } = await axios.get<ICustomPopup>(`/api/common/customPopup`)
return data
} catch (e) {
console.error('error', e)
return null
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment