Skip to content

Commit

Permalink
feat(rss): suggest saving before changing tab
Browse files Browse the repository at this point in the history
  • Loading branch information
ZRunner committed Jan 19, 2025
1 parent 21c48d8 commit 539e88d
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 30 deletions.
51 changes: 41 additions & 10 deletions src/components/GuildDashboard/NavigationDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 (
Expand All @@ -141,7 +140,7 @@ function TabContent({ page, isOpen }: { page: GuildConfigOptionCategory; isOpen:
justifyContent: "center",
}}
>
<Badge color="warning" variant="dot" invisible={!unsavedCategories.includes(page)}>
<Badge color="warning" variant="dot" invisible={!isUnsaved}>
<TabIcon page={page} />
</Badge>
</ListItemIcon>
Expand All @@ -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<GuildConfigOptionCategory | null>(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 (
<Fragment>
<Toolbar sx={{ display: { xs: "none", md: "initial" } }} />
Expand All @@ -174,16 +202,19 @@ function NavigationDrawerContent({ open, activePage, toggleOpen }: NavigationDra
<PageTab
isOpen={open}
isSelected={activePage === page}
component={Link}
to={page}
onClick={isOnMobile ? toggleOpen : undefined}
onClick={handleClickOnTab(page)}
>
<TabContent page={page} isOpen={open} />
<TabContent page={page} isOpen={open} isUnsaved={unsavedCategories.includes(page)} />
</PageTab>
</ListItem>
))}
</List>
</Box>
<UnsavedFeedsConfirmationDialog
open={isUnsavedFeedsDialogOpen}
onCancel={closeUnsavedFeedsDialog}
onConfirm={closeUnsavedFeedsDialogAndNavigate}
/>
</Fragment>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<RssFeed | null>(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);
}
Expand All @@ -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();
}
}
}
Expand All @@ -65,20 +64,20 @@ 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 (
<FeedRowContainer isOpen={isDetailsOpen}>
<FeedTitleStack onClick={toggleCollapsedZone}>
<Stack spacing={{ xs: 0, sm: 1 }} direction="row" overflow="hidden" alignItems="center">
<RssFeedMention feed={feedToShow} />
{displayRecentErrors && <RecentErrorsIcon />}
{!feed.enabled && <DisabledTag />}
{!apiFeed.enabled && <DisabledTag />}
{editedFeed && <UnsavedTag />}
</Stack>

Expand All @@ -91,7 +90,7 @@ export default function FeedComponent({ feed }: FeedComponentProps) {
</FeedTitleStack>

<Collapse in={isDetailsOpen}>
<InnerFeedComponent feed={feedToShow} isEdited={editedFeed !== null} editFeed={editFeed} saveFeed={saveFeed} displayRecentErrors={displayRecentErrors} isVisible={isDetailsOpen} />
<InnerFeedComponent feed={feedToShow} isEdited={!!editedFeed} editFeed={editFeed} saveFeed={saveFeed} displayRecentErrors={displayRecentErrors} isVisible={isDetailsOpen} />
</Collapse>

<UnsavedConfirmationDialog open={isConfirmModalOpen} onCancel={closeConfirmationDialog} onConfirm={closeConfirmationDialogAndDetails} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog maxWidth="xs" open={open} onClose={onCancel}>
<DialogTitle>You have unsaved changes!</DialogTitle>
<DialogContent>
<DialogContentText>
You have unsaved changes in the RSS feeds configuration. Do you want to save them before leaving?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button autoFocus color="gray" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
<Button color="primary" onClick={saveFeeds} disabled={isLoading}>Save</Button>
<Button color="error" onClick={onConfirm} disabled={isLoading}>Close without saving</Button>
</DialogActions>
</Dialog>
);
}
69 changes: 62 additions & 7 deletions src/repository/context/GuildConfigEditionContext.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,19 +7,14 @@ 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<string, EditionValueType>;
roleRewards?: {
id?: string;
roleId: string;
level: string;
}[];
editedRssFeeds?: RssFeed[];
}

interface ContextType {
Expand All @@ -31,6 +26,8 @@ interface ContextType {
resetBaseOptionValue: (optionId: string) => void;
setRoleRewardsValue: (roleRewards: GuildConfigEdition["roleRewards"]) => void;
resetRoleRewardsValue: () => void;
setRssFeedsValue: Dispatch<SetStateAction<GuildConfigEdition["editedRssFeeds"]>>;
resetRssFeedsValue: () => void;
resetState: () => void;
}

Expand All @@ -53,6 +50,12 @@ const GuildConfigEditionContext = createContext<ContextType>({
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");
},
Expand Down Expand Up @@ -96,6 +99,29 @@ export function GuildConfigEditionProvider({ guildId, children }: PropsWithChild
}));
}, []);

const setRssFeedsValue: Dispatch<SetStateAction<GuildConfigEdition["editedRssFeeds"]>> = 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());
}, []);
Expand All @@ -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;
};

Expand All @@ -130,6 +159,8 @@ export function GuildConfigEditionProvider({ guildId, children }: PropsWithChild
resetBaseOptionValue,
setRoleRewardsValue,
resetRoleRewardsValue,
setRssFeedsValue,
resetRssFeedsValue,
resetState,
}}
>
Expand Down Expand Up @@ -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,
};
}

0 comments on commit 539e88d

Please sign in to comment.