Skip to content

Commit

Permalink
refact(rss): use individual save system
Browse files Browse the repository at this point in the history
  • Loading branch information
ZRunner committed Jan 19, 2025
1 parent a589596 commit fefc96f
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 297 deletions.
5 changes: 1 addition & 4 deletions src/components/GuildDashboard/SaveConfigBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface SaveConfigBannerProps {
}

export default function SaveConfigBanner({ guildId }: SaveConfigBannerProps) {
const { patchGuildConfigCommand, putRoleRewardsCommand, patchRssFeedsCommand, loading, success } = usePatchGuildConfig();
const { patchGuildConfigCommand, putRoleRewardsCommand, loading, success } = usePatchGuildConfig();
const { state, hasAnyUnsavedChange, resetState } = useGuildConfigEditionContext();
const isOnMobile = useIsOnMobile();

Expand All @@ -23,9 +23,6 @@ export default function SaveConfigBanner({ guildId }: SaveConfigBannerProps) {
if (state.roleRewards !== undefined) {
putRoleRewardsCommand(guildId, state.roleRewards);
}
if (state.rssFeeds !== undefined) {
patchRssFeedsCommand(guildId, state.rssFeeds);
}
}

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { ExpandLess, ExpandMore, WarningAmberRounded } from "@mui/icons-material";
import { Autocomplete, Box, Collapse, IconButton, Stack, styled, Switch, TextField, Tooltip, Typography } from "@mui/material";
import SaveIcon from "@mui/icons-material/Save";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { Autocomplete, Button, Collapse, IconButton, Stack, styled, Switch, TextField, Tooltip, Typography } from "@mui/material";
import { ChannelType } from "discord-api-types/v10";
import { CSSProperties, Fragment, PropsWithChildren, useState } from "react";

import { useGuildConfigEditionContext, useGuildConfigRssFeedsEditionContext } from "../../../../repository/context/GuildConfigEditionContext";
import { useFetchGuildChannelsQuery } from "../../../../repository/redux/api/api";
import { useGuildConfigEditionContext } 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";
import ChannelMention from "../../../common/ChannelMention";
import { ReadonlyChannelPicker } from "../../ConfigComponents/shared/TextChannelPicker";
import FeedDeleteButton from "./FeedDeleteButton";
import FeedEmbedSettings from "./FeedEmbedSettings";
import FeedPreviewButton from "./FeedPreviewButton";
import FeedPreview from "./FeedPreview";
import FeedTextEditor from "./FeedTextEditor";
import FeedToggle from "./FeedToggle";
import RssFeedMention from "./RssFeedMention";
Expand All @@ -21,43 +23,60 @@ const RECENT_ERRORS_THRESHOLD = 3;

interface FeedComponentProps {
feed: RssFeed;
editFeed: (feed: RssFeed) => void;
}

export default function FeedComponent({ feed, editFeed }: FeedComponentProps) {
export default function FeedComponent({ feed }: FeedComponentProps) {
const { guildId } = useGuildConfigEditionContext();
const [editFeedMutation, { isLoading }] = usePutGuildRssFeedMutation();
const [isOpen, setIsOpen] = useState(false);
const [editedFeed, setEditedFeed] = useState<RssFeed | null>(null);
const Icon = isOpen ? ExpandLess : ExpandMore;
const { isFeedMarkedForDeletion } = useGuildConfigRssFeedsEditionContext();

const isMarkedForDeletion = isFeedMarkedForDeletion(feed.id);
function editFeed(newFeedObject: RssFeed) {
if (compareFeeds(feed, newFeedObject)) {
setEditedFeed(null);
} else {
setEditedFeed(newFeedObject);
}
}

async function saveFeed() {
if (editedFeed && !isLoading) {
const result = await editFeedMutation({ guildId, feed: editedFeed });
if (!result.error) {
setEditedFeed(null);
}
}
}

function toggleCollapsedZone() {
setIsOpen(!isOpen);
}

const isTwitter = feed.type === "tw";
const displayRecentErrors = !isTwitter && feed.recentErrors >= RECENT_ERRORS_THRESHOLD;
const feedToShow = editedFeed ?? feed;

return (
<FeedRowContainer isOpen={isOpen} disabled={!feed.enabled}>
<FeedTitleStack onClick={toggleCollapsedZone}>
<Stack direction="row" overflow="hidden" alignItems="center">
<RssFeedMention feed={feed} strikethrough={isMarkedForDeletion} />
<Stack spacing={{ xs: 0, sm: 1 }} direction="row" overflow="hidden" alignItems="center">
<RssFeedMention feed={feedToShow} />
{displayRecentErrors && <RecentErrorsIcon />}
{!feed.enabled && !isMarkedForDeletion && <DisabledTag />}
{isMarkedForDeletion && <MarkedForDeletionTag />}
{!feed.enabled && <DisabledTag />}
{editedFeed && <UnsavedTag />}
</Stack>

<Stack direction="row">
<FeedToggle feed={feed} disabled={isTwitter || isMarkedForDeletion} />
<FeedToggle feed={feedToShow} disabled={isTwitter} />
<IconButton onClick={toggleCollapsedZone}>
<Icon />
</IconButton>
</Stack>
</FeedTitleStack>

<Collapse in={isOpen}>
<InnerFeedComponent feed={feed} editFeed={editFeed} displayRecentErrors={displayRecentErrors} isVisible={isOpen} />
<InnerFeedComponent feed={feedToShow} isEdited={editedFeed !== null} editFeed={editFeed} saveFeed={saveFeed} displayRecentErrors={displayRecentErrors} isVisible={isOpen} />
</Collapse>
</FeedRowContainer>
);
Expand All @@ -66,15 +85,18 @@ export default function FeedComponent({ feed, editFeed }: FeedComponentProps) {

interface InnerComponentsProps {
feed: RssFeed;
isEdited: boolean;
displayRecentErrors: boolean;
isVisible: boolean;
editFeed: (feed: RssFeed) => void;
saveFeed: () => void;
}

function InnerFeedComponent({ feed, editFeed, displayRecentErrors, isVisible }: InnerComponentsProps & { displayRecentErrors: boolean; isVisible: boolean }) {
function InnerFeedComponent({ feed, isEdited, editFeed, saveFeed, displayRecentErrors, isVisible }: InnerComponentsProps) {
const isMinecraft = feed.type === "mc";
const canPreview = ["yt", "web"].includes(feed.type);

return (
<Stack gap={1} px={2}>
<Stack spacing={1} px={2}>
{displayRecentErrors && <RecentErrorsDescription recentErrors={feed.recentErrors} />}
<SimpleParameterRow label="Channel">
<ChannelSelection feed={feed} editFeed={editFeed} />
Expand All @@ -95,15 +117,7 @@ function InnerFeedComponent({ feed, editFeed, displayRecentErrors, isVisible }:
</SimpleParameterColumn>
</Fragment>
)}
{canPreview && (
<Box my={2}>
<FeedPreviewButton feed={feed} />
</Box>
)}

<Box mb={2}>
<FeedDeleteButton feedId={feed.id} />
</Box>
<FeedActionsAndPreview feed={feed} isEdited={isEdited} saveFeed={saveFeed} />
</Stack>
);
}
Expand Down Expand Up @@ -161,8 +175,8 @@ function DisabledTag() {
return <Typography variant="caption" color="text.secondary" ml={0.5}>Disabled</Typography>;
}

function MarkedForDeletionTag() {
return <Typography variant="caption" color="text.secondary" ml={0.5}>Marked for deletion</Typography>;
function UnsavedTag() {
return <Typography variant="caption" color="primary" ml={0.5}>Unsaved</Typography>;
}

function RecentErrorsIcon() {
Expand All @@ -189,7 +203,7 @@ function RecentErrorsDescription({ recentErrors }: { recentErrors: number }) {
);
}

function ChannelSelection({ feed, editFeed }: InnerComponentsProps) {
function ChannelSelection({ feed, editFeed }: Pick<InnerComponentsProps, "feed" | "editFeed">) {
const { guildId } = useGuildConfigEditionContext();
const { data, isLoading, error } = useFetchGuildChannelsQuery({ guildId });
const [editing, setEditing] = useState(false);
Expand Down Expand Up @@ -253,7 +267,7 @@ function ChannelSelection({ feed, editFeed }: InnerComponentsProps) {
return <ReadonlyChannelPicker currentChannel={currentChannel} onClick={() => setEditing(true)} />;
}

function SilentMentionToggle({ feed, editFeed }: InnerComponentsProps) {
function SilentMentionToggle({ feed, editFeed }: Pick<InnerComponentsProps, "feed" | "editFeed">) {
function onChange() {
editFeed({
...feed,
Expand All @@ -269,7 +283,7 @@ function SilentMentionToggle({ feed, editFeed }: InnerComponentsProps) {
);
}

function UseEmbedToggle({ feed, editFeed }: InnerComponentsProps) {
function UseEmbedToggle({ feed, editFeed }: Pick<InnerComponentsProps, "feed" | "editFeed">) {
function onChange() {
editFeed({
...feed,
Expand All @@ -284,3 +298,46 @@ function UseEmbedToggle({ feed, editFeed }: InnerComponentsProps) {
/>
);
}

interface FeedActionsAndPreviewProps {
feed: RssFeed;
isEdited: boolean;
saveFeed: () => void;
}
function FeedActionsAndPreview({ feed, isEdited, saveFeed }: FeedActionsAndPreviewProps) {
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const canPreview = ["yt", "web"].includes(feed.type);
const [fetchFeed, { data: previewData, isLoading }] = useLazyTestRssFeedQuery();

const togglePreview = async () => {
setIsPreviewOpen(!isPreviewOpen);
if (!isPreviewOpen && !previewData && !isLoading) {
await fetchFeed({ type: feed.type, url: feed.link });
}
};

return (
<Stack direction="column" spacing={2} my={2}>
<Stack direction={{ xs: "column", md: "row" }} spacing={2}>
{canPreview && (
<Button color="secondary" variant="outlined" onClick={togglePreview} startIcon={<VisibilityIcon />}>
{isPreviewOpen ? "Hide Preview" : "Preview"}
</Button>
)}
{isEdited && (
<Button color="primary" variant="outlined" onClick={saveFeed} startIcon={<SaveIcon />}>
Save changes
</Button>
)}
<FeedDeleteButton feed={feed} />
</Stack>
<FeedPreview isOpen={isPreviewOpen} isLoading={isLoading} feed={feed} data={previewData} />
</Stack>
);
}


function compareFeeds(a: RssFeed, b: RssFeed) {
return Object.keys(a).every((key) => key in b && a[key as keyof RssFeed] === b[key as keyof RssFeed]);
}

Original file line number Diff line number Diff line change
@@ -1,31 +1,64 @@
import DeleteIcon from "@mui/icons-material/Delete";
import RestoreIcon from "@mui/icons-material/Restore";
import { Button } from "@mui/material";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Fragment, useState } from "react";

import { useGuildConfigRssFeedsEditionContext } from "../../../../repository/context/GuildConfigEditionContext";
import { getGuildDashboardTranslations } from "../../../../i18n/i18n";
import { useGuildConfigEditionContext } from "../../../../repository/context/GuildConfigEditionContext";
import { useDeleteGuildRssFeedMutation } from "../../../../repository/redux/api/api";
import { RssFeed } from "../../../../repository/types/api";

interface FeedDeleteButtonProps {
feedId: string;
feed: Pick<RssFeed, "id" | "type" | "link" | "displayName">;
}
export default function FeedDeleteButton({ feedId }: FeedDeleteButtonProps) {
const { deleteFeed, unDeleteFeed, isFeedMarkedForDeletion } = useGuildConfigRssFeedsEditionContext();
export default function FeedDeleteButton({ feed }: FeedDeleteButtonProps) {
const { guildId } = useGuildConfigEditionContext();
const [deleteFeedMutation, { isLoading }] = useDeleteGuildRssFeedMutation();
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);

const isMarkedForDeletion = isFeedMarkedForDeletion(feedId);
const deleteCurrentFeed = () => deleteFeed(feedId);
const restoreCurrentFeed = () => unDeleteFeed(feedId);
async function deleteCurrentFeed() {
if (isLoading) return;
await deleteFeedMutation({ guildId, feedId: feed.id });
closeConfirmModal();
}

const openConfirmModal = () => setIsConfirmModalOpen(true);
const closeConfirmModal = () => setIsConfirmModalOpen(false);

if (isMarkedForDeletion) {
return (
<Button color="error" variant="outlined" onClick={restoreCurrentFeed} startIcon={<RestoreIcon />}>
Restore this feed
</Button>
);
} else {
return (
<Button color="error" variant="outlined" onClick={deleteCurrentFeed} startIcon={<DeleteIcon />}>
return (
<Fragment>
<Button color="error" variant="outlined" onClick={openConfirmModal} startIcon={<DeleteIcon />}>
Delete this feed
</Button>
);
}
<ConfirmationDialog open={isConfirmModalOpen} feed={feed} onCancel={closeConfirmModal} onConfirm={deleteCurrentFeed} />
</Fragment>
);
}


interface ConfirmationDialogProps {
open: boolean;
feed: FeedDeleteButtonProps["feed"];
onCancel: () => void;
onConfirm: () => void;
}
function ConfirmationDialog({ open, feed, onCancel, onConfirm }: ConfirmationDialogProps) {
const feedType = getGuildDashboardTranslations("rss_type." + feed.type, feed.type);
const description = `This action is irreversible. The ${feedType} feed '${feed.displayName ?? feed.link}' will be deleted forever.`;

return (
<Dialog maxWidth="xs" open={open} onClose={onCancel}>
<DialogTitle>Are you sure you want to delete this feed?</DialogTitle>
<DialogContent>
<DialogContentText>
{description}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button autoFocus color="gray" onClick={onCancel}>
Cancel
</Button>
<Button color="error" onClick={onConfirm}>Delete</Button>
</DialogActions>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,31 @@
import VisibilityIcon from "@mui/icons-material/Visibility";
import { Box, Button, Collapse, Paper, Typography } from "@mui/material";
import { ComponentProps, useState } from "react";
import { Fragment } from "react/jsx-runtime";
import { Box, Collapse, Paper, Typography } from "@mui/material";
import { ComponentProps } from "react";

import { useLazyTestRssFeedQuery } from "../../../../repository/redux/api/api";
import { RssFeed, RssFeedParsedEntry } from "../../../../repository/types/api";
import DiscordMessagePreview from "../../../common/DiscordMessagePreview";

interface FeedPreviewButtonProps {
isOpen: boolean;
isLoading: boolean;
feed: RssFeed;
data: RssFeedParsedEntry | undefined;
}
export default function FeedPreviewButton({ feed }: FeedPreviewButtonProps) {
const [isOpen, setIsOpen] = useState(false);
const [fetchFeed, { data, isLoading }] = useLazyTestRssFeedQuery();

const toggleOpen = async () => {
setIsOpen(!isOpen);
if (!isOpen && !data && !isLoading) {
await fetchFeed({ type: feed.type, url: feed.link });
}
};

export default function FeedPreview({ isOpen, feed, data, isLoading }: FeedPreviewButtonProps) {
return (
<Fragment>
<Button color="secondary" variant="outlined" onClick={toggleOpen} startIcon={<VisibilityIcon />}>
{isOpen ? "Hide Preview" : "Preview this feed"}
</Button>
<Collapse in={isOpen}>
<Box>
{isLoading && (<Typography color="textSecondary">Fetching the latest data...</Typography>)}
{(!isLoading && !data) && (<Typography color="error">Oops, something went wrong while fetching your feed.</Typography>)}
{(!isLoading && data) && <FeedPreview feed={feed} data={data} />}
</Box>
</Collapse>
</Fragment>
<Collapse in={isOpen}>
<Box>
{isLoading && (<Typography color="textSecondary">Fetching the latest data...</Typography>)}
{(!isLoading && !data) && (<Typography color="error">Oops, something went wrong while fetching your feed.</Typography>)}
{(!isLoading && data) && <InnerFeedPreview feed={feed} data={data} />}
</Box>
</Collapse>
);
}

function FeedPreview({ feed, data }: { feed: RssFeed; data: RssFeedParsedEntry }) {
function InnerFeedPreview({ feed, data }: { feed: RssFeed; data: RssFeedParsedEntry }) {
const discordMessage = useBuildDiscordMessageFromFeed({ feed, feedData: data });
return (
<Paper elevation={3} sx={{ mt: 2, px: 2, py: 1 }}>
<Paper elevation={3} sx={{ px: 2, py: 1 }}>
<DiscordMessagePreview {...discordMessage} />
</Paper>
);
Expand Down
Loading

0 comments on commit fefc96f

Please sign in to comment.