enhancement(web): add react suspense and improve DX (#1089)

* add react suspense, fix broken stuff, clean up code, improve DX

enhancement: added react suspense + spinner to show loading (still can be added in certain places)
chore: cleaned up Header/NavBar code
chore: cleaned up DeleteModal code
chore: cleaned up other relevant code
enhancement: changed remove button style to be much more pleasant (see e.g. filter tabs)
fix: made active tab on filters page to be blue (as it should've been) when active
fix: fixed ghost delimiter which was only visible when DeleteModal was active in FormButtonGroup
chore: removed most of linter warnings/errors
fix: fixed incorrect/double modal transition in FilterExternalItem
fix: fixed incorrect z-height on Options popover in Settings/IRC (would've been visible when Add new was clicked)
enhancement: improved robustness of all Context classes to support seamless new-feature expansion (#866)
enhancement: improved expand logic (see #994 comments)

* reverted irc expand view to previous design

* forgot to propagate previous z-height fix

* jinxed it

* add license header to new files

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com>
This commit is contained in:
stacksmash76 2023-09-10 12:35:43 +02:00 committed by GitHub
parent cbf668e87c
commit 2fed48e0dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 845 additions and 737 deletions

View file

@ -119,6 +119,7 @@ function APIListItem({ apikey }: ApiKeyItemProps) {
<li className="text-gray-500 dark:text-gray-400">
<DeleteModal
isOpen={deleteModalIsOpen}
isLoading={deleteMutation.isLoading}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={() => {

View file

@ -23,7 +23,7 @@ import { DeleteModal } from "@components/modals";
import { FeedUpdateForm } from "@forms/settings/FeedForms";
import { EmptySimple } from "@components/emptystates";
import { ImplementationBadges } from "./Indexer";
import {ArrowPathIcon} from "@heroicons/react/24/solid";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
export const feedKeys = {
all: ["feeds"] as const,
@ -64,7 +64,8 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
return sortableItems;
}, [items, sortConfig]);
const requestSort = (key: keyof ListItemProps["feed"] | "enabled") => { let direction: "ascending" | "descending" = "ascending";
const requestSort = (key: keyof ListItemProps["feed"] | "enabled") => {
let direction: "ascending" | "descending" = "ascending";
if (
sortConfig &&
sortConfig.key === key &&
@ -133,13 +134,13 @@ function FeedSettings() {
Last run <span className="sort-indicator">{sortedFeeds.getSortIndicator("last_run")}</span>
</div>
<div
className="hidden md:flex col-span-2 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
onClick={() => sortedFeeds.requestSort("next_run")}>
className="hidden md:flex col-span-2 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
onClick={() => sortedFeeds.requestSort("next_run")}>
Next run <span className="sort-indicator">{sortedFeeds.getSortIndicator("next_run")}</span>
</div>
</li>
{sortedFeeds.items.map((feed) => (
<ListItem key={feed.id} feed={feed}/>
<ListItem key={feed.id} feed={feed} />
))}
</ol>
</section>
@ -150,7 +151,7 @@ function FeedSettings() {
}
interface ListItemProps {
feed: Feed;
feed: Feed;
}
function ListItem({ feed }: ListItemProps) {
@ -165,7 +166,7 @@ function ListItem({ feed }: ListItemProps) {
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) });
toast.custom((t) => <Toast type="success" body={`${feed.name} was ${!enabled ? "disabled" : "enabled"} successfully.`} t={t}/>);
toast.custom((t) => <Toast type="success" body={`${feed.name} was ${!enabled ? "disabled" : "enabled"} successfully.`} t={t} />);
}
});
@ -176,7 +177,7 @@ function ListItem({ feed }: ListItemProps) {
return (
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed} />
<div className="grid grid-cols-12 items-center">
<div className="col-span-2 sm:col-span-1 px-6 flex items-center">
@ -230,9 +231,9 @@ function ListItem({ feed }: ListItemProps) {
}
interface FeedItemDropdownProps {
feed: Feed;
onToggle: (newState: boolean) => void;
toggleUpdate: () => void;
feed: Feed;
onToggle: (newState: boolean) => void;
toggleUpdate: () => void;
}
const FeedItemDropdown = ({
@ -254,14 +255,14 @@ const FeedItemDropdown = ({
queryClient.invalidateQueries({ queryKey: feedKeys.lists() });
queryClient.invalidateQueries({ queryKey: feedKeys.detail(feed.id) });
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t} />);
}
});
const deleteCacheMutation = useMutation({
mutationFn: (id: number) => APIClient.feeds.deleteCache(id),
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} cache was cleared!`} t={t}/>);
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} cache was cleared!`} t={t} />);
}
});
@ -269,6 +270,7 @@ const FeedItemDropdown = ({
<Menu as="div">
<DeleteModal
isOpen={deleteModalIsOpen}
isLoading={deleteMutation.isLoading}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={() => {
@ -279,14 +281,15 @@ const FeedItemDropdown = ({
text="Are you sure you want to remove this feed? This action cannot be undone."
/>
<DeleteModal
isOpen={deleteCacheModalIsOpen}
toggle={toggleDeleteCacheModal}
buttonRef={cancelCacheModalButtonRef}
deleteAction={() => {
deleteCacheMutation.mutate(feed.id);
}}
title={`Remove feed cache: ${feed.name}`}
text="Are you sure you want to remove the feed cache? This action cannot be undone."
isOpen={deleteCacheModalIsOpen}
isLoading={deleteMutation.isLoading}
toggle={toggleDeleteCacheModal}
buttonRef={cancelCacheModalButtonRef}
deleteAction={() => {
deleteCacheMutation.mutate(feed.id);
}}
title={`Remove feed cache: ${feed.name}`}
text="Are you sure you want to remove the feed cache? This action cannot be undone."
/>
<Menu.Button className="px-4 py-2">
<EllipsisHorizontalIcon
@ -349,49 +352,49 @@ const FeedItemDropdown = ({
</Menu.Item>
</div>
<div>
<Menu.Item>
{({ active }) => (
<a
href={`${baseUrl()}api/feeds/${feed.id}/latest`}
target="_blank"
rel="noopener noreferrer"
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
>
<DocumentTextIcon
<Menu.Item>
{({ active }) => (
<a
href={`${baseUrl()}api/feeds/${feed.id}/latest`}
target="_blank"
rel="noopener noreferrer"
className={classNames(
active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2"
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
aria-hidden="true"
/>
View latest run
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
>
<DocumentTextIcon
className={classNames(
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2"
)}
onClick={() => toggleDeleteCacheModal()}
title="Manually clear all feed cache"
aria-hidden="true"
/>
View latest run
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={classNames(
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
onClick={() => toggleDeleteCacheModal()}
title="Manually clear all feed cache"
>
<ArrowPathIcon
className={classNames(
active ? "text-white" : "text-red-500",
"w-5 h-5 mr-2"
)}
aria-hidden="true"
className={classNames(
active ? "text-white" : "text-red-500",
"w-5 h-5 mr-2"
)}
aria-hidden="true"
/>
Clear feed cache
</button>
)}
</Menu.Item>
)}
</Menu.Item>
</div>
<div className="px-1 py-1">
<Menu.Item>

View file

@ -171,7 +171,7 @@ const IrcSettings = () => {
? <span className="flex items-center">Collapse <ArrowsPointingInIcon className="ml-1 w-4 h-4"/></span>
: <span className="flex items-center">Expand <ArrowsPointingOutIcon className="ml-1 w-4 h-4"/></span>
}</button>
<div className="relative z-10"><IRCLogsDropdown/></div>
<IRCLogsDropdown/>
</div>
</div>
@ -478,6 +478,7 @@ const ListItemDropdown = ({
>
<DeleteModal
isOpen={deleteModalIsOpen}
isLoading={deleteMutation.isLoading}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={() => {
@ -665,33 +666,11 @@ export const Events = ({ network, channel }: EventsProps) => {
setLogs([]);
}, [settings.scrollOnNewLog]);
// const { handleSubmit, register , resetField } = useForm<IrcMsg>({
// defaultValues: { msg: "" },
// mode: "onBlur"
// });
// const cmdMutation = useMutation({
// mutationFn: (data: SendIrcCmdRequest) => APIClient.irc.sendCmd(data),
// onSuccess: (_, _variables) => {
// resetField("msg");
// },
// onError: () => {
// toast.custom((t) => (
// <Toast type="error" body="Error sending IRC cmd" t={t} />
// ));
// }
// });
// const onSubmit = (msg: IrcMsg) => {
// const payload = { network_id: network.id, nick: network.nick, server: network.server, channel: channel, msg: msg.msg };
// cmdMutation.mutate(payload);
// };
return (
<div
className={classNames(
"dark:bg-gray-800 rounded-lg shadow-lg p-2",
isFullscreen ? "fixed top-0 left-0 w-screen h-screen z-50" : ""
isFullscreen ? "fixed top-0 left-0 right-0 bottom-0 w-screen h-screen z-50" : ""
)}
>
<div className="flex relative">
@ -725,21 +704,6 @@ export const Events = ({ network, channel }: EventsProps) => {
</div>
))}
</div>
{/*<div>*/}
{/* <form onSubmit={handleSubmit(onSubmit)}>*/}
{/* <input*/}
{/* id="msg"*/}
{/* {...(register && register("msg"))}*/}
{/* type="text"*/}
{/* minLength={2}*/}
{/* className={classNames(*/}
{/* "focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",*/}
{/* "block w-full dark:bg-gray-900 shadow-sm dark:text-gray-100 sm:text-sm rounded-md"*/}
{/* )}*/}
{/* />*/}
{/* </form>*/}
{/*</div>*/}
</div>
);
};
@ -758,7 +722,7 @@ const IRCLogsDropdown = () => {
}));
return (
<Menu as="div">
<Menu as="div" className="relative">
<Menu.Button>
<button className="flex items-center text-gray-800 dark:text-gray-400 p-1 px-2 rounded shadow bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600">
<span className="flex items-center">Options <Cog6ToothIcon className="ml-1 w-4 h-4"/></span>
@ -774,7 +738,7 @@ const IRCLogsDropdown = () => {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute right-0 mt-2 bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
className="absolute z-10 right-0 mt-2 bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
>
<div className="p-3">
<Menu.Item>
@ -786,7 +750,6 @@ const IRCLogsDropdown = () => {
/>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>

View file

@ -101,6 +101,7 @@ function DeleteReleases() {
<div className="flex justify-between items-center rounded-md shadow-sm">
<DeleteModal
isOpen={deleteModalIsOpen}
isLoading={deleteOlderMutation.isLoading}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteOlderReleases}