feat(releases-widget): add Mark as read action to mark releases as seen (#3676)
This commit is contained in:
@@ -2340,6 +2340,7 @@
|
|||||||
"starsCount": "Stars",
|
"starsCount": "Stars",
|
||||||
"forksCount": "Forks",
|
"forksCount": "Forks",
|
||||||
"issuesCount": "Open Issues",
|
"issuesCount": "Open Issues",
|
||||||
|
"markViewed": "Mark as viewed",
|
||||||
"openProjectPage": "Open Project Page",
|
"openProjectPage": "Open Project Page",
|
||||||
"openReleasePage": "Open Release Page",
|
"openReleasePage": "Open Release Page",
|
||||||
"releaseDescription": "Release Description",
|
"releaseDescription": "Release Description",
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Button, Divider, Group, Stack, Text, Title, Tooltip } from "@mantine/core";
|
import { Button, Divider, Group, Stack, Text, Title, Tooltip } from "@mantine/core";
|
||||||
|
import { useLocalStorage } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
IconArchive,
|
IconArchive,
|
||||||
|
IconCheck,
|
||||||
IconCircleDot,
|
IconCircleDot,
|
||||||
IconCircleFilled,
|
IconCircleFilled,
|
||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
@@ -39,6 +41,11 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
const [expandedRepositoryId, setExpandedRepositoryId] = useState<string | null>(null);
|
const [expandedRepositoryId, setExpandedRepositoryId] = useState<string | null>(null);
|
||||||
const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]);
|
const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]);
|
||||||
|
const [releasesViewedList, setReleasesViewedList] = useLocalStorage<Record<string, string>>({
|
||||||
|
key: "releases-viewed-versions",
|
||||||
|
defaultValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
const relativeDateOptions = useMemo(
|
const relativeDateOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
newReleaseWithin: formatRelativeDate(options.newReleaseWithin),
|
newReleaseWithin: formatRelativeDate(options.newReleaseWithin),
|
||||||
@@ -125,6 +132,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt
|
relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt
|
||||||
? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
|
? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
|
||||||
: false,
|
: false,
|
||||||
|
viewed: releasesViewedList[repository.id] === response.latestRelease,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(
|
.filter(
|
||||||
@@ -152,14 +160,23 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
options.topReleases,
|
options.topReleases,
|
||||||
relativeDateOptions.newReleaseWithin,
|
relativeDateOptions.newReleaseWithin,
|
||||||
relativeDateOptions.staleReleaseWithin,
|
relativeDateOptions.staleReleaseWithin,
|
||||||
|
releasesViewedList,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toggleExpandedRepository = useCallback(
|
const toggleExpandedDisplay = useCallback(
|
||||||
(repository: ReleasesRepositoryResponse) =>
|
(repository: ReleasesRepositoryResponse) =>
|
||||||
setExpandedRepositoryId(expandedRepositoryId === repository.id ? "" : repository.id),
|
setExpandedRepositoryId(expandedRepositoryId === repository.id ? null : repository.id),
|
||||||
[expandedRepositoryId],
|
[expandedRepositoryId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const markReleaseViewed = useCallback(
|
||||||
|
(repository: ReleasesRepositoryResponse) => {
|
||||||
|
repository.viewed = true;
|
||||||
|
setReleasesViewedList((prev) => ({ ...prev, [repository.id]: repository.latestRelease ?? "" }));
|
||||||
|
},
|
||||||
|
[setReleasesViewedList],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} className="releases">
|
<Stack gap={0} className="releases">
|
||||||
{repositories.map((repository: ReleasesRepositoryResponse) => {
|
{repositories.map((repository: ReleasesRepositoryResponse) => {
|
||||||
@@ -182,7 +199,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
[classes.active ?? ""]: isActive,
|
[classes.active ?? ""]: isActive,
|
||||||
})}
|
})}
|
||||||
p="xs"
|
p="xs"
|
||||||
onClick={() => toggleExpandedRepository(repository)}
|
onClick={() => toggleExpandedDisplay(repository)}
|
||||||
>
|
>
|
||||||
<MaskedOrNormalImage
|
<MaskedOrNormalImage
|
||||||
className="releases-repository-header-icon"
|
className="releases-repository-header-icon"
|
||||||
@@ -232,7 +249,15 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
<Text
|
<Text
|
||||||
className="releases-repository-header-releaseDate"
|
className="releases-repository-header-releaseDate"
|
||||||
size="xs"
|
size="xs"
|
||||||
c={repository.isNewRelease ? "primaryColor" : repository.isStaleRelease ? "secondaryColor" : "dimmed"}
|
c={
|
||||||
|
repository.viewed
|
||||||
|
? "green"
|
||||||
|
: repository.isNewRelease
|
||||||
|
? "primaryColor"
|
||||||
|
: repository.isStaleRelease
|
||||||
|
? "secondaryColor"
|
||||||
|
: "dimmed"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{repository.latestReleaseAt &&
|
{repository.latestReleaseAt &&
|
||||||
!hasError &&
|
!hasError &&
|
||||||
@@ -241,10 +266,22 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
style: "narrow",
|
style: "narrow",
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
{!hasError ? (
|
{hasError ? (
|
||||||
|
<IconTriangleFilled
|
||||||
|
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-error"
|
||||||
|
size={10}
|
||||||
|
color="var(--mantine-color-red-filled)"
|
||||||
|
/>
|
||||||
|
) : repository.viewed ? (
|
||||||
|
<IconCheck
|
||||||
|
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-confirmed"
|
||||||
|
size={10}
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
(repository.isNewRelease || repository.isStaleRelease) && (
|
(repository.isNewRelease || repository.isStaleRelease) && (
|
||||||
<IconCircleFilled
|
<IconCircleFilled
|
||||||
className="releases-repository-header-releaseDate-marker"
|
className="releases-repository-header-releaseDate-icon releases-repository-header-releaseDate-marker"
|
||||||
size={10}
|
size={10}
|
||||||
color={
|
color={
|
||||||
repository.isNewRelease
|
repository.isNewRelease
|
||||||
@@ -253,19 +290,20 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
|
||||||
<IconTriangleFilled
|
|
||||||
className="releases-repository-header-releaseDate-error"
|
|
||||||
size={10}
|
|
||||||
color={"var(--mantine-color-red-filled)"}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
{options.showDetails && (
|
{options.showDetails && (
|
||||||
<DetailsDisplay repository={repository} toggleExpandedRepository={toggleExpandedRepository} />
|
<DetailsDisplay repository={repository} toggleExpandedDisplay={toggleExpandedDisplay} />
|
||||||
|
)}
|
||||||
|
{isActive && (
|
||||||
|
<ExpandedDisplay
|
||||||
|
repository={repository}
|
||||||
|
hasIconColor={hasIconColor}
|
||||||
|
markReleaseViewed={markReleaseViewed}
|
||||||
|
toggleExpandedDisplay={toggleExpandedDisplay}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{isActive && <ExpandedDisplay repository={repository} hasIconColor={hasIconColor} />}
|
|
||||||
<Divider className="releases-repository-divider" />
|
<Divider className="releases-repository-divider" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -276,21 +314,21 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
|
|
||||||
interface DetailsDisplayProps {
|
interface DetailsDisplayProps {
|
||||||
repository: ReleasesRepositoryResponse;
|
repository: ReleasesRepositoryResponse;
|
||||||
toggleExpandedRepository: (repository: ReleasesRepositoryResponse) => void;
|
toggleExpandedDisplay: (repository: ReleasesRepositoryResponse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplayProps) => {
|
const DetailsDisplay = ({ repository, toggleExpandedDisplay }: DetailsDisplayProps) => {
|
||||||
const t = useScopedI18n("widget.releases");
|
const t = useScopedI18n("widget.releases");
|
||||||
const formatter = useFormatter();
|
const formatter = useFormatter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider className="releases-repository-details-divider" onClick={() => toggleExpandedRepository(repository)} />
|
<Divider className="releases-repository-details-divider" onClick={() => toggleExpandedDisplay(repository)} />
|
||||||
<Group
|
<Group
|
||||||
className={combineClasses("releases-repository-details", classes.releasesRepositoryDetails)}
|
className={combineClasses("releases-repository-details", classes.releasesRepositoryDetails)}
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
p={5}
|
p={5}
|
||||||
onClick={() => toggleExpandedRepository(repository)}
|
onClick={() => toggleExpandedDisplay(repository)}
|
||||||
>
|
>
|
||||||
<Group className="releases-repository-details-icon-wrapper">
|
<Group className="releases-repository-details-icon-wrapper">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -484,9 +522,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
|
|||||||
interface ExtendedDisplayProps {
|
interface ExtendedDisplayProps {
|
||||||
repository: ReleasesRepositoryResponse;
|
repository: ReleasesRepositoryResponse;
|
||||||
hasIconColor: boolean;
|
hasIconColor: boolean;
|
||||||
|
markReleaseViewed: (repository: ReleasesRepositoryResponse) => void;
|
||||||
|
toggleExpandedDisplay: (repository: ReleasesRepositoryResponse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) => {
|
const ExpandedDisplay = ({
|
||||||
|
repository,
|
||||||
|
hasIconColor,
|
||||||
|
markReleaseViewed,
|
||||||
|
toggleExpandedDisplay,
|
||||||
|
}: ExtendedDisplayProps) => {
|
||||||
const t = useScopedI18n("widget.releases");
|
const t = useScopedI18n("widget.releases");
|
||||||
const now = useNow();
|
const now = useNow();
|
||||||
const formatter = useFormatter();
|
const formatter = useFormatter();
|
||||||
@@ -540,24 +585,48 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
|
|||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Divider className="releases-repository-expanded-actions-divider" mx="30%" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="releases-repository-expanded-markViewedButton"
|
||||||
|
disabled={repository.viewed}
|
||||||
|
color="green"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
markReleaseViewed(repository);
|
||||||
|
toggleExpandedDisplay(repository);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
className="releases-repository-expanded-markViewedButton-wrapper"
|
||||||
|
gap={5}
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<IconCheck className="releases-repository-expanded-markViewedButton-icon" size="1.5em" />
|
||||||
|
<Text className="releases-repository-expanded-markViewedButton-text">{t("markViewed")}</Text>
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
|
||||||
{(repository.releaseUrl ?? repository.projectUrl) && (
|
{(repository.releaseUrl ?? repository.projectUrl) && (
|
||||||
<>
|
<Button
|
||||||
<Divider className="releases-repository-expanded-openButton-divider" mx="30%" />
|
className="releases-repository-expanded-openButton"
|
||||||
<Button
|
variant="light"
|
||||||
className="releases-repository-expanded-openButton"
|
component="a"
|
||||||
variant="light"
|
href={repository.releaseUrl ?? repository.projectUrl}
|
||||||
component="a"
|
target="_blank"
|
||||||
href={repository.releaseUrl ?? repository.projectUrl}
|
rel="noreferrer"
|
||||||
target="_blank"
|
>
|
||||||
rel="noreferrer"
|
<Group className="releases-repository-expanded-openButton-wrapper" gap={5} justify="center" align="center">
|
||||||
>
|
<IconExternalLink className="releases-repository-expanded-openButton-icon" size="1.5em" />
|
||||||
<IconExternalLink className="releases-repository-expanded-openButton-icon" />
|
|
||||||
<Text className="releases-repository-expanded-openButton-text">
|
<Text className="releases-repository-expanded-openButton-text">
|
||||||
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")}
|
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Group>
|
||||||
</>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{repository.error && (
|
{repository.error && (
|
||||||
<>
|
<>
|
||||||
<Divider className="releases-repository-expanded-error-divider" mx="30%" />
|
<Divider className="releases-repository-expanded-error-divider" mx="30%" />
|
||||||
|
|||||||
@@ -37,5 +37,7 @@ export interface ReleasesRepositoryResponse extends ReleasesRepository {
|
|||||||
iconUrl?: string;
|
iconUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
viewed: boolean;
|
||||||
|
|
||||||
error?: { code?: string; message?: string };
|
error?: { code?: string; message?: string };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user