diff --git a/backend b/backend index df2de8d8..4e7dc6d4 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit df2de8d85510a9364c405bc17b1b35fb0946b26d +Subproject commit 4e7dc6d410948c9a49e2196796b55b8d111b5279 diff --git a/src/views/Home/AlertsView/AlertContext.tsx b/src/views/Home/AlertContext.tsx similarity index 65% rename from src/views/Home/AlertsView/AlertContext.tsx rename to src/views/Home/AlertContext.tsx index e2ac6912..ae0edbfe 100644 --- a/src/views/Home/AlertsView/AlertContext.tsx +++ b/src/views/Home/AlertContext.tsx @@ -1,5 +1,11 @@ import { createContext } from 'react'; +import { + AlertInfoCertaintyEnum, + AlertInfoSeverityEnum, + AlertInfoUrgencyEnum, +} from '#generated/types/graphql'; + type Id = string; // type SetStateFn = React.Dispatch>; type SetStateFn = (newValue: T | undefined) => void; @@ -24,6 +30,14 @@ export interface AlertContextProps { setActiveAlertId: SetStateFn; setActiveCountryName: SetStateFn; + + selectedUrgencyTypes: AlertInfoUrgencyEnum[] | undefined; + selectedSeverityTypes: AlertInfoSeverityEnum[] | undefined; + selectedCertaintyTypes: AlertInfoCertaintyEnum[] | undefined; + + setSelectedUrgencyTypes: SetStateFn; + setSelectedSeverityTypes: SetStateFn; + setSelectedCertaintyTypes: SetStateFn; } const AlertContext = createContext({ @@ -34,6 +48,9 @@ const AlertContext = createContext({ activeAdmin1Id: undefined, activeGoAdmin1Id: undefined, activeAlertId: undefined, + selectedUrgencyTypes: undefined, + selectedSeverityTypes: undefined, + selectedCertaintyTypes: undefined, setBbox: () => { // eslint-disable-next-line no-console console.warn('AlertContext::setBbox called without provider'); @@ -62,6 +79,18 @@ const AlertContext = createContext({ // eslint-disable-next-line no-console console.warn('AlertContext::setActiveCountryName called without provider'); }, + setSelectedUrgencyTypes: () => { + // eslint-disable-next-line no-console + console.warn('AlertContext::setSelectedUrgencyTypes called without provider'); + }, + setSelectedSeverityTypes: () => { + // eslint-disable-next-line no-console + console.warn('AlertContext::setSelectedSeverityTypes called without provider'); + }, + setSelectedCertaintyTypes: () => { + // eslint-disable-next-line no-console + console.warn('AlertContext::setSelectedCertaintyTypes called without provider'); + }, }); export default AlertContext; diff --git a/src/views/Home/AlertsTable/index.tsx b/src/views/Home/AlertsTable/index.tsx index b4e07cfe..cb02be9d 100644 --- a/src/views/Home/AlertsTable/index.tsx +++ b/src/views/Home/AlertsTable/index.tsx @@ -1,6 +1,7 @@ import { ComponentType, HTMLProps, + useContext, useMemo, } from 'react'; import { @@ -19,22 +20,30 @@ import { createListDisplayColumn, createStringColumn, } from '@ifrc-go/ui/utils'; -import { isNotDefined } from '@togglecorp/fujs'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; import { + AlertFilter, AlertInformationsQuery, AlertInformationsQueryVariables, + OffsetPaginationInput, } from '#generated/types/graphql'; import useFilterState from '#hooks/useFilterState'; import { createLinkColumn } from '#utils/domain/tableHelpers'; +import AlertContext from '../AlertContext'; +import useAlertFilters from '../useAlertFilters'; + import i18n from './i18n.json'; import styles from './styles.module.css'; const ALERT_INFORMATIONS = gql` - query AlertInformations($pagination: OffsetPaginationInput) { + query AlertInformations($pagination: OffsetPaginationInput, $filters: AlertFilter) { public { - alerts(pagination: $pagination) { + alerts(pagination: $pagination, filters: $filters) { limit offset count @@ -74,6 +83,8 @@ const PAGE_SIZE = 20; function AlertsTable() { const strings = useTranslation(i18n); + const alertFilters = useAlertFilters(); + const { activeCountryId, activeAdmin1Id } = useContext(AlertContext); const { sortState, @@ -89,14 +100,24 @@ function AlertsTable() { filter: {}, }); - const variables = useMemo(() => ({ + const variables = useMemo<{ filters: AlertFilter, pagination: OffsetPaginationInput }>(() => ({ pagination: { offset: page, limit, }, + filters: { + ...alertFilters, + country: isDefined(activeCountryId) + ? { pk: activeCountryId } + : undefined, + admin1: activeAdmin1Id, + }, }), [ page, limit, + alertFilters, + activeCountryId, + activeAdmin1Id, ]); const { diff --git a/src/views/Home/AlertsTable/styles.module.css b/src/views/Home/AlertsTable/styles.module.css index 8220554a..a10f696c 100644 --- a/src/views/Home/AlertsTable/styles.module.css +++ b/src/views/Home/AlertsTable/styles.module.css @@ -11,12 +11,12 @@ .region { width: 0%; - min-width: 5rem; + min-width: 7rem; } .country { width: 0%; - min-width: 6rem; + min-width: 8rem; } .admins { diff --git a/src/views/Home/AlertsView/AlertsAside/Admin1Alerts/index.tsx b/src/views/Home/AlertsView/AlertsAside/Admin1Alerts/index.tsx index a4cdb259..8fd6bbdf 100644 --- a/src/views/Home/AlertsView/AlertsAside/Admin1Alerts/index.tsx +++ b/src/views/Home/AlertsView/AlertsAside/Admin1Alerts/index.tsx @@ -26,7 +26,7 @@ import { } from '#generated/types/graphql'; import { stringIdSelector } from '#utils/selectors'; -import AlertContext from '../../AlertContext'; +import AlertContext from '../../../AlertContext'; import AlertListItem from '../AlertListItem'; const ADMIN1_DETAIL = gql` diff --git a/src/views/Home/AlertsView/AlertsAside/CountryAdmin1List/index.tsx b/src/views/Home/AlertsView/AlertsAside/CountryAdmin1List/index.tsx index 2a7108ae..d32cf8d3 100644 --- a/src/views/Home/AlertsView/AlertsAside/CountryAdmin1List/index.tsx +++ b/src/views/Home/AlertsView/AlertsAside/CountryAdmin1List/index.tsx @@ -18,22 +18,22 @@ import { CountryAdmin1QueryVariables, } from '#generated/types/graphql'; import { stringIdSelector } from '#utils/selectors'; +import useAlertFilters from '#views/Home/useAlertFilters'; -import AlertContext from '../../AlertContext'; +import AlertContext from '../../../AlertContext'; import Admin1ListItem from '../Admin1ListItem'; type CountryAdmin1 = NonNullable['country']>['admin1s'][number]; const COUNTRY_ADMIN1 = gql` -query CountryAdmin1($countryId: ID!) { +query CountryAdmin1($countryId: ID!, $alertFilters: AlertFilter) { public { - id country(pk: $countryId) { id name alertCount ifrcGoId - admin1s(alertFilters: {}) { + admin1s(alertFilters: $alertFilters) { id name ifrcGoId @@ -51,10 +51,14 @@ interface Props { function CountryAdmin1List(props: Props) { const { countryId } = props; const { setActiveAdmin1Id } = useContext(AlertContext); + const alertFilters = useAlertFilters(); const variables = useMemo( - () => ({ countryId }), - [countryId], + () => ({ + countryId, + alertFilters, + }), + [countryId, alertFilters], ); const { diff --git a/src/views/Home/AlertsView/AlertsAside/CountryAlerts/index.tsx b/src/views/Home/AlertsView/AlertsAside/CountryAlerts/index.tsx index 3453a817..6b42410d 100644 --- a/src/views/Home/AlertsView/AlertsAside/CountryAlerts/index.tsx +++ b/src/views/Home/AlertsView/AlertsAside/CountryAlerts/index.tsx @@ -24,7 +24,7 @@ import { } from '#generated/types/graphql'; import { stringIdSelector } from '#utils/selectors'; -import AlertContext from '../../AlertContext'; +import AlertContext from '../../../AlertContext'; import AlertListItem from '../AlertListItem'; const COUNTRY_ALERTS = gql` diff --git a/src/views/Home/AlertsView/AlertsAside/CountryDetail/index.tsx b/src/views/Home/AlertsView/AlertsAside/CountryDetail/index.tsx index 4df782d6..6593198d 100644 --- a/src/views/Home/AlertsView/AlertsAside/CountryDetail/index.tsx +++ b/src/views/Home/AlertsView/AlertsAside/CountryDetail/index.tsx @@ -25,7 +25,7 @@ import { CountryDetailQueryVariables, } from '#generated/types/graphql'; -import AlertContext from '../../AlertContext'; +import AlertContext from '../../../AlertContext'; import Admin1Alerts from '../Admin1Alerts'; import AlertDetail from '../AlertDetail'; import CountryAdmin1List from '../CountryAdmin1List'; diff --git a/src/views/Home/AlertsView/AlertsAside/index.tsx b/src/views/Home/AlertsView/AlertsAside/index.tsx index a4d60f10..35790a0e 100644 --- a/src/views/Home/AlertsView/AlertsAside/index.tsx +++ b/src/views/Home/AlertsView/AlertsAside/index.tsx @@ -18,7 +18,7 @@ import { import { CountryListQuery } from '#generated/types/graphql'; import { stringIdSelector } from '#utils/selectors'; -import AlertContext from '../AlertContext'; +import AlertContext from '../../AlertContext'; import CountryDetail from './CountryDetail'; import CountryListItem from './CountryListItem'; diff --git a/src/views/Home/AlertsView/AlertsMap/index.tsx b/src/views/Home/AlertsView/AlertsMap/index.tsx index f8df1589..e87ccdfa 100644 --- a/src/views/Home/AlertsView/AlertsMap/index.tsx +++ b/src/views/Home/AlertsView/AlertsMap/index.tsx @@ -26,7 +26,7 @@ import { COLOR_PRIMARY_RED, } from '#utils/constants'; -import AlertContext from '../AlertContext'; +import AlertContext from '../../AlertContext'; import styles from './styles.module.css'; diff --git a/src/views/Home/AlertsView/AlertsMap/styles.module.css b/src/views/Home/AlertsView/AlertsMap/styles.module.css index 7124a2c4..d768c966 100644 --- a/src/views/Home/AlertsView/AlertsMap/styles.module.css +++ b/src/views/Home/AlertsView/AlertsMap/styles.module.css @@ -3,7 +3,7 @@ .map-container { flex-grow: 1; - height: 40rem; + height: 45rem; } } diff --git a/src/views/Home/AlertsView/index.tsx b/src/views/Home/AlertsView/index.tsx index a0d12ad9..8d5e8dc9 100644 --- a/src/views/Home/AlertsView/index.tsx +++ b/src/views/Home/AlertsView/index.tsx @@ -1,8 +1,4 @@ -import { - useCallback, - useMemo, - useState, -} from 'react'; +import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { gql, @@ -13,7 +9,6 @@ import { useTranslation } from '@ifrc-go/ui/hooks'; import { _cs, isDefined, - isNotDefined, } from '@togglecorp/fujs'; import { @@ -21,7 +16,7 @@ import { CountryListQueryVariables, } from '#generated/types/graphql'; -import AlertContext, { AlertContextProps } from './AlertContext'; +import useAlertFilters from '../useAlertFilters'; import AlertsAside from './AlertsAside'; import AlertsMap from './AlertsMap'; @@ -29,10 +24,10 @@ import i18n from './i18n.json'; import styles from './styles.module.css'; // NOTE: alertFilters is related with filteredAlertCount -const COUNTRIES_LIST = gql` -query CountryList { +const COUNTRY_LIST = gql` +query CountryList($alertFilters: AlertFilter) { public { - allCountries(alertFilters: {}) { + allCountries(alertFilters: $alertFilters) { name id iso3 @@ -58,72 +53,21 @@ function AlertsView(props: Props) { const { className } = props; const strings = useTranslation(i18n); - - const [activeCountryId, setActiveCountryId] = useState(undefined); - const [activeGoCountryId, setActiveGoCountryId] = useState(undefined); - const [activeAlertId, setActiveAlertId] = useState(undefined); - const [activeAdmin1Id, setActiveAdmin1Id] = useState(undefined); - const [activeGoAdmin1Id, setActiveGoAdmin1Id] = useState(undefined); + const alertFilters = useAlertFilters(); const { data: countryListResponse, loading: countryListLoading, error: countryListError, } = useQuery( - COUNTRIES_LIST, + COUNTRY_LIST, + { variables: { alertFilters } }, ); const countriesWithAlert = useMemo(() => countryListResponse?.public.allCountries.filter( (country) => (country?.filteredAlertCount ?? 0) > 0, ), [countryListResponse?.public.allCountries]); - const [bbox, setBbox] = useState(); - const [activeCountryName, setActiveCountryName] = useState(); - - const setActiveCountryIdSafe = useCallback( - (countryId: string | undefined) => { - setActiveCountryId(countryId); - setActiveCountryName(undefined); - setActiveAlertId(undefined); - setActiveAdmin1Id(undefined); - setActiveGoAdmin1Id(undefined); - setActiveGoCountryId(undefined); - if (isNotDefined(countryId)) { - setBbox(undefined); - } - }, - [], - ); - - const alertContextValue = useMemo( - () => ({ - bbox, - setBbox, - activeAlertId, - activeCountryId, - activeCountryName, - activeAdmin1Id, - activeGoAdmin1Id, - activeGoCountryId, - setActiveAlertId, - setActiveGoCountryId, - setActiveGoAdmin1Id, - setActiveCountryId: setActiveCountryIdSafe, - setActiveAdmin1Id, - setActiveCountryName, - }), - [ - bbox, - activeCountryName, - activeAlertId, - activeGoCountryId, - activeGoAdmin1Id, - activeAdmin1Id, - activeCountryId, - setActiveCountryIdSafe, - ], - ); - return ( )} + overlayPending pending={countryListLoading} errored={isDefined(countryListError)} errorMessage={countryListError?.message} contentViewType="grid" numPreferredGridContentColumns={3} > - - - - + + ); } diff --git a/src/views/Home/AlertsView/styles.module.css b/src/views/Home/AlertsView/styles.module.css index e291b1cd..58a492c4 100644 --- a/src/views/Home/AlertsView/styles.module.css +++ b/src/views/Home/AlertsView/styles.module.css @@ -8,7 +8,7 @@ } .alerts-aside { - height: 40rem; + height: 45rem; overflow: auto; } diff --git a/src/views/Home/Filters/i18n.json b/src/views/Home/Filters/i18n.json new file mode 100644 index 00000000..2e5cdff8 --- /dev/null +++ b/src/views/Home/Filters/i18n.json @@ -0,0 +1,10 @@ +{ + "namespace": "alertFilters", + "strings": { + "alertCountries": "All Countries", + "alertAdmin1": "All Admin1", + "alertUrgency": "Select Urgency type", + "alertSeverity": "Select Severity type", + "alertCertainty": "Select Certainty type" + } +} diff --git a/src/views/Home/Filters/index.tsx b/src/views/Home/Filters/index.tsx new file mode 100644 index 00000000..f6f8219c --- /dev/null +++ b/src/views/Home/Filters/index.tsx @@ -0,0 +1,144 @@ +import { + useContext, + useMemo, +} from 'react'; +import { + MultiSelectInput, + SelectInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { stringNameSelector } from '@ifrc-go/ui/utils'; +import { isNotDefined } from '@togglecorp/fujs'; + +import { + AlertEnumsQuery, + CountryListQuery, + FilteredAdminListQuery, +} from '#generated/types/graphql'; +import { stringIdSelector } from '#utils/selectors'; + +import AlertContext from '../AlertContext'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type AdminOption = NonNullable['admin1s']>['items']>[number]; + +type Admin1 = NonNullable['admin1s']>['items']>; +type Countries = NonNullable; +type Urgency = NonNullable[number]; +type Severity = NonNullable[number]; +type Certainty = NonNullable[number]; + +interface AlertFilters { + key: string; + label: string; +} + +const adminKeySelector = (admin1: AdminOption) => admin1.id; +const urgencyKeySelector = (urgency: Urgency) => urgency.key; +const severityKeySelector = (severity: Severity) => severity.key; +const certaintyKeySelector = (certainty: Certainty) => certainty.key; + +const labelSelector = (alert: AlertFilters) => alert.label; + +interface Props { + admin1List?: Admin1; + countryList?: Countries; + urgencyList?: Urgency[]; + severityList?: Severity[]; + certaintyList?: Certainty[]; +} + +function Filters(props: Props) { + const { + countryList, + admin1List, + urgencyList, + severityList, + certaintyList, + } = props; + + const { + activeCountryId, + activeAdmin1Id, + selectedSeverityTypes, + selectedUrgencyTypes, + selectedCertaintyTypes, + setActiveCountryId, + setActiveAdmin1Id, + setSelectedSeverityTypes, + setSelectedUrgencyTypes, + setSelectedCertaintyTypes, + } = useContext(AlertContext); + + const strings = useTranslation(i18n); + + // TODO: this should be done in server + const admin1ListForSelectedCountry = useMemo( + () => ( + admin1List?.filter( + ({ countryId }) => countryId === activeCountryId, + ) + ), + [activeCountryId, admin1List], + ); + + return ( +
+ + + + + +
+ ); +} + +export default Filters; diff --git a/src/views/Home/Filters/styles.module.css b/src/views/Home/Filters/styles.module.css new file mode 100644 index 00000000..07a3bfcd --- /dev/null +++ b/src/views/Home/Filters/styles.module.css @@ -0,0 +1,5 @@ +.filters { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + grid-gap: var(--go-ui-spacing-md); +} diff --git a/src/views/Home/index.tsx b/src/views/Home/index.tsx index 0885e278..a1794596 100644 --- a/src/views/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -1,4 +1,12 @@ -import { useState } from 'react'; +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + gql, + useQuery, +} from '@apollo/client'; import { Tab, TabList, @@ -6,52 +14,222 @@ import { Tabs, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; +import { isNotDefined } from '@togglecorp/fujs'; import Page from '#components/Page'; +import { + AlertEnumsQuery, + AlertEnumsQueryVariables, + AlertInfoCertaintyEnum, + AlertInfoSeverityEnum, + AlertInfoUrgencyEnum, + CountryListQuery, + CountryListQueryVariables, + FilteredAdminListQuery, + FilteredAdminListQueryVariables, +} from '#generated/types/graphql'; +import AlertContext, { AlertContextProps } from './AlertContext'; import AlertsTable from './AlertsTable'; import AlertsView from './AlertsView'; +import Filters from './Filters'; import i18n from './i18n.json'; import styles from './styles.module.css'; +const ALERT_ENUMS = gql` +query AlertEnums { + enums { + AlertInfoCertainty { + key + label + } + AlertInfoUrgency { + label + key + } + AlertInfoSeverity { + key + label + } + } +}`; + +// NOTE: alertFilters is related with filteredAlertCount +const COUNTRIES_LIST = gql` +query CountryList($alertFilters: AlertFilter) { + public { + allCountries(alertFilters: $alertFilters) { + name + id + iso3 + filteredAlertCount + ifrcGoId + } + } +} +`; + +// TODO: filter this by selected country +const ADMIN_LIST = gql` +query FilteredAdminList { + public { + admin1s(filters: {}) { + items { + id + name + countryId + } + } + } + } +`; + +type CountryOption = NonNullable['allCountries']>[number]; + export type TabKeys = 'map' | 'table'; + // eslint-disable-next-line import/prefer-default-export export function Component() { const strings = useTranslation(i18n); const [activeTab, setActiveTab] = useState('map'); + const [activeCountryId, setActiveCountryId] = useState(undefined); + const [activeGoCountryId, setActiveGoCountryId] = useState(undefined); + const [activeAlertId, setActiveAlertId] = useState(undefined); + const [activeAdmin1Id, setActiveAdmin1Id] = useState(undefined); + const [activeGoAdmin1Id, setActiveGoAdmin1Id] = useState(undefined); + const [bbox, setBbox] = useState(); + const [activeCountryName, setActiveCountryName] = useState(); + const [ + selectedUrgencyTypes, + setSelectedUrgencyTypes, + ] = useState(); + const [ + selectedSeverityTypes, + setSelectedSeverityTypes, + ] = useState(); + const [ + selectedCertaintyTypes, + setSelectedCertaintyTypes, + ] = useState(); + + const { + data: alertEnumsResponse, + } = useQuery( + ALERT_ENUMS, + ); + + const { + data: countryResponse, + } = useQuery( + COUNTRIES_LIST, + { variables: { alertFilters: {} } }, + ); + + const { + data: adminResponse, + } = useQuery( + ADMIN_LIST, + ); + + const countriesWithAlert = useMemo(() => countryResponse?.public.allCountries.filter( + (country: CountryOption) => (country?.filteredAlertCount ?? 0) > 0, + ), [countryResponse]); + + const setActiveCountryIdSafe = useCallback( + (countryId: string | undefined) => { + setActiveCountryId(countryId); + setActiveCountryName(undefined); + setActiveAlertId(undefined); + setActiveAdmin1Id(undefined); + setActiveGoAdmin1Id(undefined); + setActiveGoCountryId(undefined); + if (isNotDefined(countryId)) { + setBbox(undefined); + } + }, + [], + ); + + const alertContextValue = useMemo( + () => ({ + bbox, + setBbox, + activeAlertId, + activeCountryId, + activeCountryName, + activeAdmin1Id, + activeGoAdmin1Id, + activeGoCountryId, + selectedUrgencyTypes, + selectedSeverityTypes, + selectedCertaintyTypes, + setActiveAlertId, + setActiveGoCountryId, + setActiveGoAdmin1Id, + setActiveCountryId: setActiveCountryIdSafe, + setActiveAdmin1Id, + setActiveCountryName, + setSelectedCertaintyTypes, + setSelectedUrgencyTypes, + setSelectedSeverityTypes, + }), + [ + bbox, + activeCountryName, + activeAlertId, + activeGoCountryId, + activeGoAdmin1Id, + activeAdmin1Id, + activeCountryId, + selectedCertaintyTypes, + selectedUrgencyTypes, + selectedSeverityTypes, + setActiveCountryIdSafe, + ], + ); + return ( - - - { strings.mapTabTitle } - - - { strings.tableTabTitle } - - - )} - > - - - - - - - + + + + {strings.mapTabTitle} + + + {strings.tableTabTitle} + + + )} + > + + + + + + + + + ); } diff --git a/src/views/Home/useAlertFilters.ts b/src/views/Home/useAlertFilters.ts new file mode 100644 index 00000000..5e7119c5 --- /dev/null +++ b/src/views/Home/useAlertFilters.ts @@ -0,0 +1,36 @@ +import { + useContext, + useMemo, +} from 'react'; +import { isDefined } from '@togglecorp/fujs'; + +import { AlertFilter } from '#generated/types/graphql'; + +import AlertContext from './AlertContext'; + +function useAlertFilters() { + const { + selectedCertaintyTypes, + selectedSeverityTypes, + selectedUrgencyTypes, + } = useContext(AlertContext); + + const alertFilters = useMemo( + () => ({ + certainty: isDefined(selectedCertaintyTypes) && selectedCertaintyTypes.length > 0 + ? selectedCertaintyTypes + : undefined, + severity: isDefined(selectedSeverityTypes) && selectedSeverityTypes.length > 0 + ? selectedSeverityTypes + : undefined, + urgency: isDefined(selectedUrgencyTypes) && selectedUrgencyTypes.length > 0 + ? selectedUrgencyTypes + : undefined, + }), + [selectedUrgencyTypes, selectedSeverityTypes, selectedCertaintyTypes], + ); + + return alertFilters; +} + +export default useAlertFilters;