diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e80511..40100ecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,13 @@ ### 🐛 Bug Fixes +- *(i18n)* Format times and dates according to locale ([#572](https://github.com/f-eld-ch/sitrep/issues/572)) - ([204bbae](https://github.com/f-eld-ch/sitrep/commit/204bbaea4b402306a5e5f52c9a30e9509adbbb9a)) +- *(map)* Do not reload while editing text of feature ([#579](https://github.com/f-eld-ch/sitrep/issues/579)) - ([78f7f7f](https://github.com/f-eld-ch/sitrep/commit/78f7f7f0b4856ba64fdbd4a417fe0f82388e9748)) - *(map)* Fix searchbar icon - ([b7e1f98](https://github.com/f-eld-ch/sitrep/commit/b7e1f98dfaffae53f064d4630d19bd964c4d563d)) ### ⚙️ Other +- *(deps)* Bump graphql from 16.9.0 to 16.10.0 in /ui ([#574](https://github.com/f-eld-ch/sitrep/issues/574)) - ([a9b6fce](https://github.com/f-eld-ch/sitrep/commit/a9b6fce868736db4133956a8a31406abe603260c)) - *(deps)* Bump @apollo/client from 3.11.10 to 3.12.3 in /ui ([#569](https://github.com/f-eld-ch/sitrep/issues/569)) - ([50564e5](https://github.com/f-eld-ch/sitrep/commit/50564e52fe5043d2c218257aede67115d419ff0b)) - *(deps)* Bump react-i18next from 15.1.4 to 15.2.0 in /ui ([#570](https://github.com/f-eld-ch/sitrep/issues/570)) - ([dd2e0ad](https://github.com/f-eld-ch/sitrep/commit/dd2e0ad169b1edfd462d55f41ec841ac3ca71b09)) - *(deps)* Bump i18next from 24.0.5 to 24.1.0 in /ui ([#568](https://github.com/f-eld-ch/sitrep/issues/568)) - ([70613f6](https://github.com/f-eld-ch/sitrep/commit/70613f654fb6dcdbfe629893c74d02fd8030a0d3)) diff --git a/ui/package.json b/ui/package.json index d059bc56..1a30aeee 100644 --- a/ui/package.json +++ b/ui/package.json @@ -23,7 +23,7 @@ "bulma": "^1.0.2", "classnames": "^2.5.1", "dayjs": "^1.11.13", - "graphql": "^16.9.0", + "graphql": "^16.10.0", "hat": "^0.0.3", "i18next": "^24.1.0", "i18next-browser-languagedetector": "^8.0.2", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6039ea6c..10d49a89 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useEffect, useState } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { Navigate, Route, HashRouter as Router, Routes } from "react-router-dom"; import "./App.scss"; @@ -16,55 +16,49 @@ import { List as RequestList } from "views/measures/requests"; import { List as TaskList } from "views/measures/tasks"; import { List as ResourcesList } from "views/resource"; +import { useTranslation } from "react-i18next"; import { ApolloProvider } from "@apollo/client"; import { Spinner } from "components"; -import { useTranslation } from "react-i18next"; -import { UserState } from "types"; -import { UserContext } from "utils"; +import { UserProvider } from "utils"; import MessageSheet from "views/journal/MessageSheet"; import { Layout, LayoutMarginLess } from "views/Layout"; import { default as client } from "client"; import { Provider as FeatureFlagProvider } from "FeatureFlags"; - +import "./i18n"; +import dayjs from "dayjs"; +import LocalizedFormat from "dayjs/plugin/localizedFormat"; +import de from "dayjs/locale/de"; +import fr from "dayjs/locale/fr"; +import it from "dayjs/locale/it"; +import en from "dayjs/locale/en"; const Map = lazy(() => import("views/map")); function App() { - const [userState, setUserState] = useState({ isLoggedin: false, email: "", username: "" }); const { i18n } = useTranslation(); - - const setUserStateFromUserinfo = () => { - fetch("/oauth2/userinfo", { credentials: "include" }) - .then((response) => { - if (!response.ok) { - throw new Error("unauthenticated"); - } - return response.json(); - }) - .then((userInfo) => { - setUserState({ - isLoggedin: true, - email: userInfo.email, - username: userInfo.user || userInfo.preferredUsername, - }); - }) - .catch(() => { - setUserState({ isLoggedin: false, email: "", username: "" }); - }); - }; + dayjs.extend(LocalizedFormat); useEffect(() => { - setUserStateFromUserinfo(); i18n.changeLanguage(); - - const interval = setInterval(() => { - setUserStateFromUserinfo(); - }, 10000); - - return () => clearInterval(interval); - }, [i18n]); + const locale = (lang: string) => { + switch (lang) { + case "de": + return de; + case "en": + return en; + case "fr": + return fr; + case "it": + return it; + default: + return en; + } + }; + const lang = locale(i18n.language); + dayjs.locale(lang.toString()); + }, [i18n.language]); return ( - + @@ -189,7 +183,7 @@ function App() { - + ); } diff --git a/ui/src/FeatureFlags.tsx b/ui/src/FeatureFlags.tsx index b53169f9..971b54f6 100644 --- a/ui/src/FeatureFlags.tsx +++ b/ui/src/FeatureFlags.tsx @@ -23,7 +23,7 @@ const localFlagConfig = { const Provider = (props: PropsWithChildren) => { const { children } = props; - const userState = useContext(UserContext); + const { state: userState } = useContext(UserContext); useEffect(() => { const fliptProvider = new FliptWebProvider("sitrep-ui", { url: "https://flipt.sitrep.ch" }); @@ -38,11 +38,6 @@ const Provider = (props: PropsWithChildren) => { email: userState.email, }; OpenFeature.setContext(context); - - return () => { - console.log("closing openfeature provider"); - OpenFeature.close(); - }; }, [userState]); return {children}; diff --git a/ui/src/components/Navbar.tsx b/ui/src/components/Navbar.tsx index 16b061d6..49f24051 100644 --- a/ui/src/components/Navbar.tsx +++ b/ui/src/components/Navbar.tsx @@ -186,7 +186,7 @@ function VersionNavBar() { } function UserNavBar() { - const userState = useContext(UserContext); + const { state: userState } = useContext(UserContext); const { t } = useTranslation(); if (!userState.isLoggedin) return <>; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 878a4d61..10e257a3 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -2,7 +2,6 @@ import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; import { ReloadPrompt } from "utils"; -import "./i18n"; import reportWebVitals from "./reportWebVitals"; const container = document.getElementById("root"); diff --git a/ui/src/utils/UserContext.tsx b/ui/src/utils/UserContext.tsx index 8e2eca66..7f15a39b 100644 --- a/ui/src/utils/UserContext.tsx +++ b/ui/src/utils/UserContext.tsx @@ -1,5 +1,96 @@ -import { createContext } from "react"; +import { createContext, useReducer, useEffect, useCallback, useContext, ReactNode, Dispatch } from "react"; import { UserState } from "types"; -const UserContext = createContext({ isLoggedin: false, username: "", email: "" }); -export { UserContext }; +// Define the initial state +const initialState: UserState = { isLoggedin: false, username: "", email: "" }; + +// Define action types +type UserAction = { type: "LOGIN"; payload: { username: string; email: string } } | { type: "LOGOUT" }; + +// Define the reducer function +const userReducer = (state: UserState, action: UserAction): UserState => { + switch (action.type) { + case "LOGIN": + return { + isLoggedin: true, + username: action.payload.username, + email: action.payload.email, + }; + case "LOGOUT": + return { + isLoggedin: false, + username: "", + email: "", + }; + default: + return state; + } +}; + +// Create the UserContext with initial state and a dummy dispatch function +const UserContext = createContext<{ + state: UserState; + dispatch: Dispatch; +}>({ + state: initialState, + dispatch: () => null, +}); + +// Create the UserProvider component +const UserProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(userReducer, initialState); + + return ( + + + {children} + + ); +}; + +const UserInfoFetcher = () => { + const { state: userState, dispatch } = useContext(UserContext); + const setUserStateFromUserinfo = useCallback(() => { + fetch("/oauth2/userinfo", { credentials: "include" }) + .then((response) => { + if (!response.ok) { + throw new Error("unauthenticated"); + } + return response.json(); + }) + .then((userInfo) => { + const newUserState = { + isLoggedin: true, + email: userInfo.email, + username: userInfo.user || userInfo.preferredUsername, + }; + + // Only update state if it has changed + if ( + newUserState.isLoggedin !== userState.isLoggedin || + newUserState.email !== userState.email || + newUserState.username !== userState.username + ) { + dispatch({ type: "LOGIN", payload: newUserState }); + } + }) + .catch(() => { + if (userState.isLoggedin) { + dispatch({ type: "LOGOUT" }); + } + }); + }, [userState, dispatch]); + + useEffect(() => { + setUserStateFromUserinfo(); + const interval = setInterval(() => { + setUserStateFromUserinfo(); + }, 30000); + + return () => clearInterval(interval); + }, [userState]); + + return <>; +}; + +export { UserContext, UserProvider }; diff --git a/ui/src/utils/index.tsx b/ui/src/utils/index.tsx index 81c1c399..0cbc69f4 100644 --- a/ui/src/utils/index.tsx +++ b/ui/src/utils/index.tsx @@ -1,3 +1,3 @@ -export { UserContext } from "./UserContext"; +export { UserContext, UserProvider } from "./UserContext"; export { ReloadPrompt } from "./ReloadSWPrompt"; diff --git a/ui/src/utils/useDate.tsx b/ui/src/utils/useDate.tsx index 381e7171..a57d3fb6 100644 --- a/ui/src/utils/useDate.tsx +++ b/ui/src/utils/useDate.tsx @@ -1,7 +1,9 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; export const useDate = () => { + const { i18n } = useTranslation(); const [now, setNow] = useState(new Date()); useEffect(() => { const timer = setInterval(() => { @@ -12,8 +14,8 @@ export const useDate = () => { }; }, []); - const date = dayjs(now).format("LL"); - const time = dayjs(now).format("LT"); + const date = dayjs(now).locale(i18n.language).format("LL"); + const time = dayjs(now).locale(i18n.language).format("LT"); return { now, diff --git a/ui/src/views/Layout.tsx b/ui/src/views/Layout.tsx index e40b5196..a720ea80 100644 --- a/ui/src/views/Layout.tsx +++ b/ui/src/views/Layout.tsx @@ -13,7 +13,7 @@ export interface LayoutProps { export const Layout = (props: LayoutProps) => { const [searchParams] = useSearchParams(); const { i18n } = useTranslation(); - const userState = useContext(UserContext); + const { state: userState } = useContext(UserContext); const lang = searchParams.get("lang"); useEffect(() => { @@ -40,7 +40,7 @@ export const Layout = (props: LayoutProps) => { export const LayoutMarginLess = (props: LayoutProps) => { const [searchParams] = useSearchParams(); const { i18n } = useTranslation(); - const userState = useContext(UserContext); + const { state: userState } = useContext(UserContext); const lang = searchParams.get("lang"); useEffect(() => { diff --git a/ui/src/views/journal/Message.tsx b/ui/src/views/journal/Message.tsx index 5e00e582..b8f93c90 100644 --- a/ui/src/views/journal/Message.tsx +++ b/ui/src/views/journal/Message.tsx @@ -1,8 +1,6 @@ import classNames from "classnames"; import dayjs from "dayjs"; -import de from "dayjs/locale/de"; -import LocalizedFormat from "dayjs/plugin/localizedFormat"; -import relativeTime from "dayjs/plugin/relativeTime"; + import { useBooleanFlagValue } from "@openfeature/react-sdk"; import { faArrowsToEye, faEdit, faPrint, faSquareCheck } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -27,10 +25,6 @@ export interface MessageProps { setTriageMessage?: (message: MessageType | undefined) => void; } -dayjs.locale(de); -dayjs.extend(LocalizedFormat); -dayjs.extend(relativeTime); - function Message({ id, sender, @@ -45,7 +39,7 @@ function Message({ setTriageMessage, origMessage, }: MessageProps) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { incidentId, journalId } = useParams(); const showTasks = useBooleanFlagValue("show-tasks", false); @@ -102,7 +96,7 @@ function Message({

{t("message.time")}

-

{dayjs(timeDate).format("LLL")}

+

{dayjs(timeDate).locale(i18n.language).format("LLL")}

diff --git a/ui/src/views/journal/MessageSheet.tsx b/ui/src/views/journal/MessageSheet.tsx index 8bab1461..cdf528f3 100644 --- a/ui/src/views/journal/MessageSheet.tsx +++ b/ui/src/views/journal/MessageSheet.tsx @@ -3,7 +3,6 @@ import { faSquare, faSquareCheck } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Spinner } from "components"; import dayjs from "dayjs"; -import de from "dayjs/locale/de"; import LocalizedFormat from "dayjs/plugin/localizedFormat"; import relativeTime from "dayjs/plugin/relativeTime"; @@ -13,13 +12,12 @@ import { useParams } from "react-router-dom"; import { Medium, PriorityStatus, TriageMessageData, TriageMessageVars, TriageStatus } from "types"; import { GetMessageForTriage } from "./graphql"; -dayjs.locale(de); dayjs.extend(LocalizedFormat); dayjs.extend(relativeTime); function MessageSheet() { const { messageId } = useParams(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { loading, error, data } = useQuery(GetMessageForTriage, { variables: { messageId: messageId }, @@ -60,9 +58,9 @@ function MessageSheet() { {t("message.time")} - {dayjs(data?.messagesByPk.createdAt).format("LLL")} + {dayjs(data?.messagesByPk.createdAt).locale(i18n.language).format("LLL")} {t("message.createdAt")} - {dayjs(data?.messagesByPk.createdAt).format("LLL")} + {dayjs(data?.messagesByPk.createdAt).locale(i18n.language).format("LLL")} {t("message.id")} diff --git a/ui/src/views/journal/Overview.tsx b/ui/src/views/journal/Overview.tsx index 2ad8eaba..b2ebbaac 100644 --- a/ui/src/views/journal/Overview.tsx +++ b/ui/src/views/journal/Overview.tsx @@ -1,6 +1,8 @@ import { useMutation, useQuery } from "@apollo/client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import LocalizedFormat from "dayjs/plugin/localizedFormat"; +import relativeTime from "dayjs/plugin/relativeTime"; import { faChartSimple, @@ -15,21 +17,21 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import classNames from "classnames"; import { Spinner } from "components"; import dayjs from "dayjs"; -import de from "dayjs/locale/de"; -import LocalizedFormat from "dayjs/plugin/localizedFormat"; import { t } from "i18next"; import { useTranslation } from "react-i18next"; import { Journal, JournalListData, JournalListVars } from "types"; import { CloseJournal, GetJournals } from "./graphql"; -dayjs.locale(de); -dayjs.extend(LocalizedFormat); - function Overview() { const { incidentId } = useParams(); const [filterClosed, setFilterClosed] = useState(true); const navigate = useNavigate(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); + + useEffect(() => { + dayjs.extend(LocalizedFormat); + dayjs.extend(relativeTime); + }, [i18n.language]); const { loading, error, data } = useQuery(GetJournals, { variables: { incidentId: incidentId || "" }, diff --git a/ui/src/views/map/Map.tsx b/ui/src/views/map/Map.tsx index dadda41c..e2c9d264 100644 --- a/ui/src/views/map/Map.tsx +++ b/ui/src/views/map/Map.tsx @@ -97,8 +97,7 @@ function Layers() { const { state } = useContext(LayerContext); return ( - - + <>
@@ -106,10 +105,11 @@ function Layers() { {/* Active Layer */} + {/* Inactive Layers */} l.id !== state.activeLayer) || []} /> - + ); } @@ -120,7 +120,7 @@ function LayerFetcher() { const { data, loading } = useQuery(GetLayers, { variables: { incidentId: incidentId || "" }, - pollInterval: 3000, + pollInterval: 2000, fetchPolicy: "cache-and-network", }); @@ -170,7 +170,6 @@ function ActiveLayer() { featureCollection={featureCollection} selectedFeature={state.selectedFeature} /> - ); } @@ -397,7 +396,10 @@ function InactiveLayer(props: { featureCollection: FeatureCollection; id: string function MapWithProvder() { return ( - + + + + ); } diff --git a/ui/src/views/map/controls/BabsIconController.tsx b/ui/src/views/map/controls/BabsIconController.tsx index 15a87150..2a55c09d 100644 --- a/ui/src/views/map/controls/BabsIconController.tsx +++ b/ui/src/views/map/controls/BabsIconController.tsx @@ -521,13 +521,12 @@ const FeatureDetailControlPanel = memo((props: BabsIconControllerProps) => { return (
Name
-
+
{ - e.preventDefault(); setEnteredText(e.target.value); }} value={enteredText} diff --git a/ui/yarn.lock b/ui/yarn.lock index 319ab7b0..d94d5034 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -8299,10 +8299,10 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^16.9.0": - version: 16.9.0 - resolution: "graphql@npm:16.9.0" - checksum: 10/5833f82bb6c31bec120bbf9cd400eda873e1bb7ef5c17974fa262cd82dc68728fda5d4cb859dc8aaa4c4fe4f6fe1103a9c47efc01a12c02ae5cb581d8e4029e2 +"graphql@npm:^16.10.0": + version: 16.10.0 + resolution: "graphql@npm:16.10.0" + checksum: 10/d42cf81ddcf3a61dfb213217576bf33c326f15b02c4cee369b373dc74100cbdcdc4479b3b797e79b654dabd8fddf50ef65ff75420e9ce5596c02e21f24c9126a languageName: node linkType: hard @@ -13158,7 +13158,7 @@ __metadata: eslint-plugin-prettier: "npm:^5.2.1" eslint-plugin-react: "npm:^7.37.2" globals: "npm:^15.13.0" - graphql: "npm:^16.9.0" + graphql: "npm:^16.10.0" hat: "npm:^0.0.3" husky: "npm:^9.1.7" i18next: "npm:^24.1.0"