diff --git a/src/components/GuildDashboard/NavigationDrawer.tsx b/src/components/GuildDashboard/NavigationDrawer.tsx index d088987..3bca71e 100644 --- a/src/components/GuildDashboard/NavigationDrawer.tsx +++ b/src/components/GuildDashboard/NavigationDrawer.tsx @@ -4,14 +4,15 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import MenuIcon from "@mui/icons-material/Menu"; import { Badge, Box, CSSObject, Divider, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, styled, Theme, Toolbar } from "@mui/material"; import MuiDrawer from "@mui/material/Drawer"; -import { Fragment, useState } from "react"; +import { Fragment, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { Link, useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { getGuildDashboardTranslations } from "../../i18n/i18n"; import { useGuildConfigEditionContext } from "../../repository/context/GuildConfigEditionContext"; import { GuildConfigOptionCategory, GuildConfigOptionCategoryNames } from "../../repository/types/guild-config-types"; import { useIsOnMobile } from "../../styles/useIsOnMobile"; +import UnsavedFeedsConfirmationDialog from "./SpecialCategoryComponents/RssFeeds/UnsavedFeedsConfirmationDialog"; const drawerWidth = 240; @@ -126,9 +127,7 @@ function getPageTitle(page: string) { return getGuildDashboardTranslations(`category_name.${page}`); } -function TabContent({ page, isOpen }: { page: GuildConfigOptionCategory; isOpen: boolean }) { - const { getCategoriesWithUnsavedChanges } = useGuildConfigEditionContext(); - const unsavedCategories = getCategoriesWithUnsavedChanges(); +function TabContent({ page, isOpen, isUnsaved }: { page: GuildConfigOptionCategory; isOpen: boolean; isUnsaved: boolean }) { const formatedTitle = getPageTitle(page); return ( @@ -141,7 +140,7 @@ function TabContent({ page, isOpen }: { page: GuildConfigOptionCategory; isOpen: justifyContent: "center", }} > - + @@ -157,7 +156,36 @@ interface NavigationDrawerContentProps { } function NavigationDrawerContent({ open, activePage, toggleOpen }: NavigationDrawerContentProps) { + const { getCategoriesWithUnsavedChanges } = useGuildConfigEditionContext(); + const unsavedCategories = getCategoriesWithUnsavedChanges(); const isOnMobile = useIsOnMobile(); + const navigate = useNavigate(); + + const [isUnsavedFeedsDialogOpen, setIsUnsavedFeedsDialogOpen] = useState(false); + const destinationPage = useRef(null); + + function handleClickOnTab(page: GuildConfigOptionCategory) { + return async () => { + if (activePage === "rss" && unsavedCategories.includes("rss")) { + destinationPage.current = page; + setIsUnsavedFeedsDialogOpen(true); + return; + } + navigate(page); + if (isOnMobile) { + toggleOpen(); + } + }; + } + + const closeUnsavedFeedsDialog = () => setIsUnsavedFeedsDialogOpen(false); + const closeUnsavedFeedsDialogAndNavigate = () => { + closeUnsavedFeedsDialog(); + if (destinationPage.current) { + navigate(destinationPage.current); + } + }; + return ( @@ -174,16 +202,19 @@ function NavigationDrawerContent({ open, activePage, toggleOpen }: NavigationDra - + ))} + ); } diff --git a/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/FeedComponent.tsx b/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/FeedComponent.tsx index 759dac4..0a7196d 100644 --- a/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/FeedComponent.tsx +++ b/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/FeedComponent.tsx @@ -5,7 +5,7 @@ import { Autocomplete, Button, Collapse, Dialog, DialogActions, DialogContent, D import { ChannelType } from "discord-api-types/v10"; import { CSSProperties, Fragment, PropsWithChildren, useState } from "react"; -import { useGuildConfigEditionContext } from "../../../../repository/context/GuildConfigEditionContext"; +import { useGuildConfigEditionContext, useRssFeedEditionContext } from "../../../../repository/context/GuildConfigEditionContext"; import { useFetchGuildChannelsQuery, useLazyTestRssFeedQuery, usePutGuildRssFeedMutation } from "../../../../repository/redux/api/api"; import { RssFeed } from "../../../../repository/types/api"; import { GuildChannel } from "../../../../repository/types/guild"; @@ -25,17 +25,16 @@ interface FeedComponentProps { feed: RssFeed; } -export default function FeedComponent({ feed }: FeedComponentProps) { - const { guildId } = useGuildConfigEditionContext(); +export default function FeedComponent({ feed: apiFeed }: FeedComponentProps) { const [editFeedMutation, { isLoading }] = usePutGuildRssFeedMutation(); const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); - const [editedFeed, setEditedFeed] = useState(null); + const { guildId, state: editedFeed, editFeed: setEditedFeed, unregisterFeed: unregisterEditedFeed } = useRssFeedEditionContext(apiFeed.id); const Icon = isDetailsOpen ? ExpandLess : ExpandMore; function editFeed(newFeedObject: RssFeed) { - if (compareFeeds(feed, newFeedObject)) { - setEditedFeed(null); + if (compareFeeds(apiFeed, newFeedObject)) { + unregisterEditedFeed(); } else { setEditedFeed(newFeedObject); } @@ -45,7 +44,7 @@ export default function FeedComponent({ feed }: FeedComponentProps) { if (editedFeed && !isLoading) { const result = await editFeedMutation({ guildId, feed: editedFeed }); if (!result.error) { - setEditedFeed(null); + unregisterEditedFeed(); } } } @@ -65,12 +64,12 @@ export default function FeedComponent({ feed }: FeedComponentProps) { function closeConfirmationDialogAndDetails() { closeConfirmationDialog(); setIsDetailsOpen(false); - setEditedFeed(null); + unregisterEditedFeed(); } - const isTwitter = feed.type === "tw"; - const displayRecentErrors = !isTwitter && feed.recentErrors >= RECENT_ERRORS_THRESHOLD; - const feedToShow = editedFeed ?? feed; + const isTwitter = apiFeed.type === "tw"; + const displayRecentErrors = !isTwitter && apiFeed.recentErrors >= RECENT_ERRORS_THRESHOLD; + const feedToShow = editedFeed ?? apiFeed; return ( @@ -78,7 +77,7 @@ export default function FeedComponent({ feed }: FeedComponentProps) { {displayRecentErrors && } - {!feed.enabled && } + {!apiFeed.enabled && } {editedFeed && } @@ -91,7 +90,7 @@ export default function FeedComponent({ feed }: FeedComponentProps) { - + diff --git a/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/UnsavedFeedsConfirmationDialog.tsx b/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/UnsavedFeedsConfirmationDialog.tsx new file mode 100644 index 0000000..d59b21b --- /dev/null +++ b/src/components/GuildDashboard/SpecialCategoryComponents/RssFeeds/UnsavedFeedsConfirmationDialog.tsx @@ -0,0 +1,40 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; + +import { useGuildConfigEditionContext } from "../../../../repository/context/GuildConfigEditionContext"; +import { usePutGuildRssFeedMutation } from "../../../../repository/redux/api/api"; + +interface UnsavedFeedsConfirmationDialogProps { + open: boolean; + onCancel: () => void; + onConfirm: () => void; +} +export default function UnsavedFeedsConfirmationDialog({ open, onCancel, onConfirm }: UnsavedFeedsConfirmationDialogProps) { + const { guildId, state, setRssFeedsValue } = useGuildConfigEditionContext(); + const [editFeedMutation, { isLoading }] = usePutGuildRssFeedMutation(); + + async function saveFeeds() { + await Promise.all(state.editedRssFeeds?.map( + (feed) => editFeedMutation({ guildId, feed }) + ) ?? []); + setRssFeedsValue(undefined); + onConfirm(); + } + + return ( + + You have unsaved changes! + + + You have unsaved changes in the RSS feeds configuration. Do you want to save them before leaving? + + + + + + + + + ); +} diff --git a/src/repository/context/GuildConfigEditionContext.tsx b/src/repository/context/GuildConfigEditionContext.tsx index dbd62d1..f4f1277 100644 --- a/src/repository/context/GuildConfigEditionContext.tsx +++ b/src/repository/context/GuildConfigEditionContext.tsx @@ -1,4 +1,4 @@ -import { createContext, PropsWithChildren, useCallback, useContext, useState } from "react"; +import { createContext, Dispatch, PropsWithChildren, SetStateAction, useCallback, useContext, useState } from "react"; import { useFetchDefaultGuildConfigQuery } from "../redux/api/api"; import { RssFeed } from "../types/api"; @@ -7,12 +7,6 @@ import { GuildConfigOptionCategory } from "../types/guild-config-types"; type EditionValueType = number | boolean | string | string[] | null; -export interface StateRssFeed { - add: RssFeed[]; - edit: RssFeed[]; - remove: string[]; -}; - interface GuildConfigEdition { baseOptions: Record; roleRewards?: { @@ -20,6 +14,7 @@ interface GuildConfigEdition { roleId: string; level: string; }[]; + editedRssFeeds?: RssFeed[]; } interface ContextType { @@ -31,6 +26,8 @@ interface ContextType { resetBaseOptionValue: (optionId: string) => void; setRoleRewardsValue: (roleRewards: GuildConfigEdition["roleRewards"]) => void; resetRoleRewardsValue: () => void; + setRssFeedsValue: Dispatch>; + resetRssFeedsValue: () => void; resetState: () => void; } @@ -53,6 +50,12 @@ const GuildConfigEditionContext = createContext({ resetRoleRewardsValue: () => { throw new Error("GuildConfigEditionContext is not provided"); }, + setRssFeedsValue: () => { + throw new Error("GuildConfigEditionContext is not provided"); + }, + resetRssFeedsValue: () => { + throw new Error("GuildConfigEditionContext is not provided"); + }, resetState: () => { throw new Error("GuildConfigEditionContext is not provided"); }, @@ -96,6 +99,29 @@ export function GuildConfigEditionProvider({ guildId, children }: PropsWithChild })); }, []); + const setRssFeedsValue: Dispatch> = useCallback( + (valueOrUpdater) => { + setState((prevState) => { + const newRssFeeds + = typeof valueOrUpdater === "function" + ? valueOrUpdater(prevState.editedRssFeeds) + : valueOrUpdater; + return { + ...prevState, + editedRssFeeds: newRssFeeds, + }; + }); + }, + [] + ); + + const resetRssFeedsValue = useCallback(() => { + setState((prevState) => ({ + ...prevState, + editedRssFeeds: undefined, + })); + }, []); + const resetState = useCallback(() => { setState(getDefaultState()); }, []); @@ -112,6 +138,9 @@ export function GuildConfigEditionProvider({ guildId, children }: PropsWithChild if (state.roleRewards !== undefined) { unsavedCategories.push("xp"); } + if (state.editedRssFeeds?.length) { + unsavedCategories.push("rss"); + } return unsavedCategories; }; @@ -130,6 +159,8 @@ export function GuildConfigEditionProvider({ guildId, children }: PropsWithChild resetBaseOptionValue, setRoleRewardsValue, resetRoleRewardsValue, + setRssFeedsValue, + resetRssFeedsValue, resetState, }} > @@ -161,3 +192,27 @@ export function useGuildConfigRoleRewardsEditionContext() { resetValue: resetRoleRewardsValue, }; } + +export function useRssFeedEditionContext(feedId: string) { + const { guildId, state, setRssFeedsValue } = useGuildConfigEditionContext(); + const existingFeed = state.editedRssFeeds?.find((f) => f.id === feedId); + + const editFeed = (feed: RssFeed) => { + if (existingFeed) { + setRssFeedsValue((prevState) => prevState?.map((f) => (f.id === feed.id ? feed : f))); + } else { + setRssFeedsValue((prevState) => [...(prevState ?? []), feed]); + } + }; + + const unregisterFeed = () => { + setRssFeedsValue((prevState) => prevState?.filter((f) => f.id !== feedId)); + }; + + return { + guildId, + state: existingFeed, + editFeed, + unregisterFeed, + }; +}