Merge pull request #6985 from tchak/fix-eslint

Javascript : correction des avertissements ESLint
This commit is contained in:
Paul Chavard 2022-02-24 09:48:01 +01:00 committed by GitHub
commit ffcff87103
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 196 additions and 119 deletions

View file

@ -23,6 +23,7 @@ module.exports = {
rules: { rules: {
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error', 'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'react/prop-types': 'off' 'react/prop-types': 'off'
}, },
settings: { settings: {
@ -51,7 +52,13 @@ module.exports = {
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
'prettier' 'prettier'
] ],
rules: {
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'@typescript-eslint/no-explicit-any': 'error'
}
} }
] ]
}; };

View file

@ -26,6 +26,19 @@ import { useDeferredSubmit, useHiddenField } from './shared/hooks';
const Context = createContext(); const Context = createContext();
const optionValueByLabel = (values, options, label) => {
const maybeOption = values.includes(label)
? [label, label]
: options.find(([optionLabel]) => optionLabel == label);
return maybeOption ? maybeOption[1] : undefined;
};
const optionLabelByValue = (values, options, value) => {
const maybeOption = values.includes(value)
? [value, value]
: options.find(([, optionValue]) => optionValue == value);
return maybeOption ? maybeOption[0] : undefined;
};
function ComboMultiple({ function ComboMultiple({
options, options,
id, id,
@ -40,9 +53,6 @@ function ComboMultiple({
invariant(id || label, 'ComboMultiple: `id` or a `label` are required'); invariant(id || label, 'ComboMultiple: `id` or a `label` are required');
invariant(group, 'ComboMultiple: `group` is required'); invariant(group, 'ComboMultiple: `group` is required');
if (!Array.isArray(options[0])) {
options = options.filter((o) => o).map((o) => [o, o]);
}
const inputRef = useRef(); const inputRef = useRef();
const [term, setTerm] = useState(''); const [term, setTerm] = useState('');
const [selections, setSelections] = useState(selected); const [selections, setSelections] = useState(selected);
@ -51,25 +61,22 @@ function ComboMultiple({
const removedLabelledby = `${inputId}-remove`; const removedLabelledby = `${inputId}-remove`;
const selectedLabelledby = `${inputId}-selected`; const selectedLabelledby = `${inputId}-selected`;
const optionValueByLabel = (label) => { const optionsWithLabels = useMemo(
const maybeOption = newValues.includes(label) () =>
? [label, label] Array.isArray(options[0])
: options.find(([optionLabel]) => optionLabel == label); ? options
return maybeOption ? maybeOption[1] : undefined; : options.filter((o) => o).map((o) => [o, o]),
}; [options]
const optionLabelByValue = (value) => { );
const maybeOption = newValues.includes(value)
? [value, value]
: options.find(([, optionValue]) => optionValue == value);
return maybeOption ? maybeOption[0] : undefined;
};
const extraOptions = useMemo( const extraOptions = useMemo(
() => () =>
acceptNewValues && term && term.length > 2 && !optionLabelByValue(term) acceptNewValues &&
term &&
term.length > 2 &&
!optionLabelByValue(newValues, optionsWithLabels, term)
? [[term, term]] ? [[term, term]]
: [], : [],
[acceptNewValues, term, newValues.join(',')] [acceptNewValues, term, optionsWithLabels, newValues]
); );
const results = useMemo( const results = useMemo(
() => () =>
@ -77,12 +84,12 @@ function ComboMultiple({
...extraOptions, ...extraOptions,
...(term ...(term
? matchSorter( ? matchSorter(
options.filter(([label]) => !label.startsWith('--')), optionsWithLabels.filter(([label]) => !label.startsWith('--')),
term term
) )
: options) : optionsWithLabels)
].filter(([, value]) => !selections.includes(value)), ].filter(([, value]) => !selections.includes(value)),
[term, selections.join(','), newValues.join(',')] [term, selections, extraOptions, optionsWithLabels]
); );
const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name); const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name);
const awaitFormSubmit = useDeferredSubmit(hiddenField); const awaitFormSubmit = useDeferredSubmit(hiddenField);
@ -100,7 +107,7 @@ function ComboMultiple({
}; };
const onSelect = (value) => { const onSelect = (value) => {
const maybeValue = [...extraOptions, ...options].find( const maybeValue = [...extraOptions, ...optionsWithLabels].find(
([val]) => val == value ([val]) => val == value
); );
const selectedValue = maybeValue && maybeValue[1]; const selectedValue = maybeValue && maybeValue[1];
@ -128,7 +135,7 @@ function ComboMultiple({
}; };
const onRemove = (label) => { const onRemove = (label) => {
const optionValue = optionValueByLabel(label); const optionValue = optionValueByLabel(newValues, options, label);
if (optionValue) { if (optionValue) {
saveSelection((selections) => saveSelection((selections) =>
selections.filter((value) => value != optionValue) selections.filter((value) => value != optionValue)
@ -149,7 +156,9 @@ function ComboMultiple({
) { ) {
if ( if (
term && term &&
[...extraOptions, ...options].map(([label]) => label).includes(term) [...extraOptions, ...optionsWithLabels]
.map(([label]) => label)
.includes(term)
) { ) {
event.preventDefault(); event.preventDefault();
onSelect(term); onSelect(term);
@ -172,7 +181,9 @@ function ComboMultiple({
const onBlur = () => { const onBlur = () => {
const shouldSelect = const shouldSelect =
term && term &&
[...extraOptions, ...options].map(([label]) => label).includes(term); [...extraOptions, ...optionsWithLabels]
.map(([label]) => label)
.includes(term);
awaitFormSubmit(() => { awaitFormSubmit(() => {
if (shouldSelect) { if (shouldSelect) {
@ -199,7 +210,7 @@ function ComboMultiple({
<ComboboxToken <ComboboxToken
key={selection} key={selection}
describedby={removedLabelledby} describedby={removedLabelledby}
value={optionLabelByValue(selection)} value={optionLabelByValue(newValues, options, selection)}
/> />
))} ))}
</ul> </ul>

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useRef, ChangeEventHandler } from 'react';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { import {
@ -68,7 +68,7 @@ function ComboSearch<Result>({
const [, value, label] = transformResult(result); const [, value, label] = transformResult(result);
return label ?? value; return label ?? value;
}; };
const setExternalValueAndId = useCallback((label: string) => { const setExternalValueAndId = (label: string) => {
const { key, value, result } = resultsMap.current[label]; const { key, value, result } = resultsMap.current[label];
if (onChange) { if (onChange) {
onChange(value, result); onChange(value, result);
@ -76,36 +76,35 @@ function ComboSearch<Result>({
setExternalId(key); setExternalId(key);
setExternalValue(value); setExternalValue(value);
} }
}, []); };
const awaitFormSubmit = useDeferredSubmit(hiddenField); const awaitFormSubmit = useDeferredSubmit(hiddenField);
const handleOnChange = useCallback( const handleOnChange: ChangeEventHandler<HTMLInputElement> = ({
({ target: { value } }) => { target: { value }
setValue(value); }) => {
if (!value) { setValue(value);
if (onChange) { if (!value) {
onChange(null); if (onChange) {
} else { onChange(null);
setExternalId(''); } else {
setExternalValue(''); setExternalId('');
} setExternalValue('');
} else if (value.length >= minimumInputLength) {
setSearchTerm(value.trim());
if (allowInputValues) {
setExternalId('');
setExternalValue(value);
}
} }
}, } else if (value.length >= minimumInputLength) {
[minimumInputLength] setSearchTerm(value.trim());
); if (allowInputValues) {
setExternalId('');
setExternalValue(value);
}
}
};
const handleOnSelect = useCallback((value: string) => { const handleOnSelect = (value: string) => {
setExternalValueAndId(value); setExternalValueAndId(value);
setValue(value); setValue(value);
setSearchTerm(''); setSearchTerm('');
awaitFormSubmit.done(); awaitFormSubmit.done();
}, []); };
const { isSuccess, data } = useQuery<void, void, unknown, QueryKey>( const { isSuccess, data } = useQuery<void, void, unknown, QueryKey>(
[scope, debouncedSearchTerm, scopeExtra], [scope, debouncedSearchTerm, scopeExtra],
@ -117,14 +116,14 @@ function ComboSearch<Result>({
const results = const results =
isSuccess && data ? transformResults(debouncedSearchTerm, data) : []; isSuccess && data ? transformResults(debouncedSearchTerm, data) : [];
const onBlur = useCallback(() => { const onBlur = () => {
if (!allowInputValues && isSuccess && results[0]) { if (!allowInputValues && isSuccess && results[0]) {
const label = getLabel(results[0]); const label = getLabel(results[0]);
awaitFormSubmit(() => { awaitFormSubmit(() => {
handleOnSelect(label); handleOnSelect(label);
}); });
} }
}, [data]); };
return ( return (
<Combobox onSelect={handleOnSelect}> <Combobox onSelect={handleOnSelect}>

View file

@ -28,35 +28,41 @@ export function CadastreLayer({
const map = useMapLibre(); const map = useMapLibre();
const selectedCadastresRef = useRef(new Set<string>()); const selectedCadastresRef = useRef(new Set<string>());
const highlightFeature = useCallback((cid: string, highlight: boolean) => { const highlightFeature = useCallback(
if (highlight) { (cid: string, highlight: boolean) => {
selectedCadastresRef.current.add(cid); if (highlight) {
} else { selectedCadastresRef.current.add(cid);
selectedCadastresRef.current.delete(cid); } else {
} selectedCadastresRef.current.delete(cid);
if (selectedCadastresRef.current.size == 0) { }
map.setFilter('parcelle-highlighted', ['in', 'id', '']); if (selectedCadastresRef.current.size == 0) {
} else { map.setFilter('parcelle-highlighted', ['in', 'id', '']);
map.setFilter('parcelle-highlighted', [ } else {
'in', map.setFilter('parcelle-highlighted', [
'id', 'in',
...selectedCadastresRef.current 'id',
]); ...selectedCadastresRef.current
} ]);
}, []); }
},
[map]
);
const hoverFeature = useCallback((feature: Feature, hover: boolean) => { const hoverFeature = useCallback(
if (!selectedCadastresRef.current.has(feature.properties?.id)) { (feature: Feature, hover: boolean) => {
map.setFeatureState( if (!selectedCadastresRef.current.has(feature.properties?.id)) {
{ map.setFeatureState(
source: 'cadastre', {
sourceLayer: 'parcelles', source: 'cadastre',
id: String(feature.id) sourceLayer: 'parcelles',
}, id: String(feature.id)
{ hover } },
); { hover }
} );
}, []); }
},
[map]
);
useCadastres(featureCollection, { useCadastres(featureCollection, {
hoverFeature, hoverFeature,

View file

@ -48,6 +48,8 @@ export function DrawLayer({
trash: true trash: true
} }
}); });
// We use mapbox-draw plugin with maplibre. They are compatible but types are not.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.addControl(draw as any, 'top-left'); map.addControl(draw as any, 'top-left');
draw.set( draw.set(
filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR) filterFeatureCollection(featureCollection, SOURCE_SELECTION_UTILISATEUR)
@ -64,11 +66,15 @@ export function DrawLayer({
return () => { return () => {
if (drawRef.current) { if (drawRef.current) {
// We use mapbox-draw plugin with maplibre. They are compatible but types are not.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.removeControl(drawRef.current as any); map.removeControl(drawRef.current as any);
drawRef.current = null; drawRef.current = null;
} }
}; };
}, [enabled]); // We only want to rerender draw layer on component mount or when the layer is toggled.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, enabled]);
const onSetId = useCallback(({ detail }) => { const onSetId = useCallback(({ detail }) => {
drawRef.current?.setFeatureProperty(detail.lid, 'id', detail.id); drawRef.current?.setFeatureProperty(detail.lid, 'id', detail.id);
@ -167,7 +173,9 @@ function useExternalEvents(
useEffect(() => { useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike); fitBounds(featureCollection.bbox as LngLatBoundsLike);
}, []); // We only want to zoom on bbox on component mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus); useEvent('map:feature:focus', onFeatureFocus);
useEvent('map:feature:create', onFeatureCreate); useEvent('map:feature:create', onFeatureCreate);

View file

@ -44,13 +44,13 @@ export function GeoJSONLayer({
popup.remove(); popup.remove();
} }
}, },
[popup] [map, popup]
); );
const onMouseLeave = useCallback(() => { const onMouseLeave = useCallback(() => {
map.getCanvas().style.cursor = ''; map.getCanvas().style.cursor = '';
popup.remove(); popup.remove();
}, [popup]); }, [map, popup]);
useExternalEvents(featureCollection); useExternalEvents(featureCollection);
@ -99,17 +99,22 @@ export function GeoJSONLayer({
function useExternalEvents(featureCollection: FeatureCollection) { function useExternalEvents(featureCollection: FeatureCollection) {
const fitBounds = useFitBounds(); const fitBounds = useFitBounds();
const onFeatureFocus = useCallback(({ detail }) => { const onFeatureFocus = useCallback(
const { id } = detail; ({ detail }) => {
const feature = findFeature(featureCollection, id); const { id } = detail;
if (feature) { const feature = findFeature(featureCollection, id);
fitBounds(getBounds(feature.geometry)); if (feature) {
} fitBounds(getBounds(feature.geometry));
}, []); }
},
[featureCollection, fitBounds]
);
useEffect(() => { useEffect(() => {
fitBounds(featureCollection.bbox as LngLatBoundsLike); fitBounds(featureCollection.bbox as LngLatBoundsLike);
}, []); // We only want to zoom on bbox on component mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fitBounds]);
useEvent('map:feature:focus', onFeatureFocus); useEvent('map:feature:focus', onFeatureFocus);
} }
@ -139,7 +144,7 @@ function LineStringLayer({
type: 'line', type: 'line',
paint: lineStringSelectionLine paint: lineStringSelectionLine
}); });
}, []); }, [map, layerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId); useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId); useMapEvent('mouseleave', onMouseLeave, layerId);
@ -172,7 +177,7 @@ function PointLayer({
type: 'circle', type: 'circle',
paint: pointSelectionCircle paint: pointSelectionCircle
}); });
}, []); }, [map, layerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId); useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId); useMapEvent('mouseleave', onMouseLeave, layerId);
@ -212,7 +217,7 @@ function PolygonLayer({
type: 'fill', type: 'fill',
paint: polygonSelectionFill paint: polygonSelectionFill
}); });
}, []); }, [map, layerId, lineLayerId, sourceId, feature]);
useMapEvent('mouseenter', onMouseEnter, layerId); useMapEvent('mouseenter', onMouseEnter, layerId);
useMapEvent('mouseleave', onMouseLeave, layerId); useMapEvent('mouseleave', onMouseLeave, layerId);

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import invariant from 'tiny-invariant';
export function FlashMessage({ export function FlashMessage({
message, message,
@ -12,11 +13,13 @@ export function FlashMessage({
sticky?: boolean; sticky?: boolean;
fixed?: boolean; fixed?: boolean;
}) { }) {
const element = document.getElementById('flash_messages');
invariant(element, 'Flash messages root element not found');
return createPortal( return createPortal(
<div className="flash_message center"> <div className="flash_message center">
<div className={flashClassName(level, sticky, fixed)}>{message}</div> <div className={flashClassName(level, sticky, fixed)}>{message}</div>
</div>, </div>,
document.getElementById('flash_messages')! element
); );
} }

View file

@ -5,7 +5,8 @@ import React, {
useEffect, useEffect,
useMemo, useMemo,
ReactNode, ReactNode,
createContext createContext,
useCallback
} from 'react'; } from 'react';
import maplibre, { Map, Style, NavigationControl } from 'maplibre-gl'; import maplibre, { Map, Style, NavigationControl } from 'maplibre-gl';
@ -37,11 +38,14 @@ export function MapLibre({ children, header, footer, layers }: MapLibreProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<Map | null>(); const [map, setMap] = useState<Map | null>();
const onStyleChange = (style: Style) => { const onStyleChange = useCallback(
if (map) { (style: Style) => {
map.setStyle(style); if (map) {
} map.setStyle(style);
}; }
},
[map]
);
const { style, ...mapStyleProps } = useStyle(layers, onStyleChange); const { style, ...mapStyleProps } = useStyle(layers, onStyleChange);
useEffect(() => { useEffect(() => {
@ -56,7 +60,7 @@ export function MapLibre({ children, header, footer, layers }: MapLibreProps) {
setMap(map); setMap(map);
}); });
} }
}, []); }, [map, style, isSupported]);
if (!isSupported) { if (!isSupported) {
return ( return (

View file

@ -12,16 +12,22 @@ import { useMapLibre } from './MapLibre';
export function useFitBounds() { export function useFitBounds() {
const map = useMapLibre(); const map = useMapLibre();
return useCallback((bbox: LngLatBoundsLike) => { return useCallback(
map.fitBounds(bbox, { padding: 100 }); (bbox: LngLatBoundsLike) => {
}, []); map.fitBounds(bbox, { padding: 100 });
},
[map]
);
} }
export function useFlyTo() { export function useFlyTo() {
const map = useMapLibre(); const map = useMapLibre();
return useCallback((zoom: number, center: [number, number]) => { return useCallback(
map.flyTo({ zoom, center }); (zoom: number, center: [number, number]) => {
}, []); map.flyTo({ zoom, center });
},
[map]
);
} }
export function useEvent(eventName: string, callback: EventListener) { export function useEvent(eventName: string, callback: EventListener) {
@ -44,12 +50,16 @@ export function useMapEvent(
const map = useMapLibre(); const map = useMapLibre();
return useEffect(() => { return useEffect(() => {
if (target) { if (target) {
// event typing is hard
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.on(eventName as keyof MapLayerEventType, target, callback as any); map.on(eventName as keyof MapLayerEventType, target, callback as any);
} else { } else {
map.on(eventName, callback); map.on(eventName, callback);
} }
return () => { return () => {
if (target) { if (target) {
// event typing is hard
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.off(eventName as keyof MapLayerEventType, target, callback as any); map.off(eventName as keyof MapLayerEventType, target, callback as any);
} else { } else {
map.off(eventName, callback); map.off(eventName, callback);
@ -104,7 +114,7 @@ export function useStyle(
[styleId, enabledLayers] [styleId, enabledLayers]
); );
useEffect(() => onStyleChange(style), [style]); useEffect(() => onStyleChange(style), [onStyleChange, style]);
return { style, layers, setStyle, setLayerEnabled, setLayerOpacity }; return { style, layers, setStyle, setLayerEnabled, setLayerOpacity };
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { AnyLayer } from 'maplibre-gl'; import type { AnyLayer } from 'maplibre-gl';
const layers: AnyLayer[] = [ const layers: AnyLayer[] = [

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { AnyLayer } from 'maplibre-gl'; import type { AnyLayer } from 'maplibre-gl';
const layers: AnyLayer[] = [ const layers: AnyLayer[] = [

View file

@ -2,6 +2,17 @@ import { QueryClient, QueryFunction } from 'react-query';
import { getJSON, isNumeric } from '@utils'; import { getJSON, isNumeric } from '@utils';
import { matchSorter } from 'match-sorter'; import { matchSorter } from 'match-sorter';
type Gon = {
gon: {
autocomplete?: {
api_geo_url?: string;
api_adresse_url?: string;
api_education_url?: string;
};
};
};
declare const window: Window & typeof globalThis & Gon;
const API_EDUCATION_QUERY_LIMIT = 5; const API_EDUCATION_QUERY_LIMIT = 5;
const API_GEO_QUERY_LIMIT = 5; const API_GEO_QUERY_LIMIT = 5;
const API_ADRESSE_QUERY_LIMIT = 5; const API_ADRESSE_QUERY_LIMIT = 5;
@ -16,7 +27,7 @@ const API_ADRESSE_QUERY_LIMIT = 5;
const API_GEO_COMMUNES_QUERY_LIMIT = 60; const API_GEO_COMMUNES_QUERY_LIMIT = 60;
const { api_geo_url, api_adresse_url, api_education_url } = const { api_geo_url, api_adresse_url, api_education_url } =
(window as any).gon.autocomplete || {}; window.gon.autocomplete || {};
type QueryKey = readonly [ type QueryKey = readonly [
scope: string, scope: string,
@ -70,8 +81,9 @@ const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
} }
throw new Error(`Error fetching from "${scope}" API`); throw new Error(`Error fetching from "${scope}" API`);
}); });
(promise as any).cancel = () => controller && controller.abort(); return Object.assign(promise, {
return promise; cancel: () => controller && controller.abort()
});
}; };
let paysCache: { label: string }[]; let paysCache: { label: string }[];
@ -85,6 +97,8 @@ async function getPays(): Promise<{ label: string }[]> {
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
// we don't really care about global queryFn type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryFn: defaultQueryFn as any queryFn: defaultQueryFn as any
} }
} }

View file

@ -88,6 +88,15 @@ export function ajax(options: Rails.AjaxOptions) {
}); });
} }
class ResponseError extends Error {
response: Response;
constructor(response: Response) {
super(String(response.statusText || response.status));
this.response = response;
}
}
export function getJSON(url: string, data: unknown, method = 'GET') { export function getJSON(url: string, data: unknown, method = 'GET') {
const { query, ...options } = fetchOptions(data, method); const { query, ...options } = fetchOptions(data, method);
@ -98,9 +107,7 @@ export function getJSON(url: string, data: unknown, method = 'GET') {
} }
return response.json(); return response.json();
} }
const error = new Error(String(response.statusText || response.status)); throw new ResponseError(response);
(error as any).response = response;
throw error;
}); });
} }
@ -125,8 +132,9 @@ export function on(
); );
} }
export function isNumeric(n: string) { export function isNumeric(s: string) {
return !isNaN(parseFloat(n)) && isFinite(n as any as number); const n = parseFloat(s);
return !isNaN(n) && isFinite(n);
} }
function offset(element: HTMLElement) { function offset(element: HTMLElement) {