forked from github/plane
Fix: bug fixes and UI / UX improvements (#2906)
* Fix: issue with project publish modal data not updating immediately. * fix: issue with workspace list not scrollable in profile settings. * fix: update redirect workspace slug logic to redirect to prev workspace instead of `/`. * style: update API tokens and webhooks empty state designs.
This commit is contained in:
parent
67de6d0729
commit
c22c6bb9b2
@ -14,7 +14,7 @@ export const ApiTokenEmptyState: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center mx-auto rounded-sm border border-custom-border-200 bg-custom-background-90 py-10 px-16 w-full`}
|
className={`flex items-center justify-center mx-auto rounded-sm border border-custom-border-200 bg-custom-background-90 py-10 px-16 w-full lg:w-3/4`}
|
||||||
>
|
>
|
||||||
<div className="text-center flex flex-col items-center w-full">
|
<div className="text-center flex flex-col items-center w-full">
|
||||||
<Image src={emptyApiTokens} className="w-52 sm:w-60" alt="empty" />
|
<Image src={emptyApiTokens} className="w-52 sm:w-60" alt="empty" />
|
||||||
|
@ -119,7 +119,7 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
reset({ ...updatedData });
|
reset({ ...updatedData });
|
||||||
}
|
}
|
||||||
}, [reset, projectPublishStore.projectPublishSettings]);
|
}, [reset, projectPublishStore.projectPublishSettings, isOpen]);
|
||||||
|
|
||||||
// fetch publish settings
|
// fetch publish settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -11,7 +11,7 @@ export const WebhooksEmptyState = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center mx-auto rounded-sm border border-custom-border-200 bg-custom-background-90 py-10 px-16 w-full`}
|
className={`flex items-center justify-center mx-auto rounded-sm border border-custom-border-200 bg-custom-background-90 py-10 px-16 w-full lg:w-3/4`}
|
||||||
>
|
>
|
||||||
<div className="text-center flex flex-col items-center w-full">
|
<div className="text-center flex flex-col items-center w-full">
|
||||||
<Image src={EmptyWebhook} className="w-52 sm:w-60" alt="empty" />
|
<Image src={EmptyWebhook} className="w-52 sm:w-60" alt="empty" />
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
|
import { Fragment, useEffect, useRef, useState } from "react";
|
||||||
|
import { mutate } from "swr";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
import { LogIn, LogOut, MoveLeft, Plus, User, UserPlus } from "lucide-react";
|
import { LogIn, LogOut, MoveLeft, Plus, User, UserPlus } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, Tooltip } from "@plane/ui";
|
import { Avatar, Tooltip } from "@plane/ui";
|
||||||
import { Fragment } from "react";
|
// hooks
|
||||||
import { mutate } from "swr";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
const SIDEBAR_LINKS = [
|
const SIDEBAR_LINKS = [
|
||||||
{
|
{
|
||||||
@ -28,6 +30,11 @@ const SIDEBAR_LINKS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ProfileLayoutSidebar = observer(() => {
|
export const ProfileLayoutSidebar = observer(() => {
|
||||||
|
// states
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
|
||||||
|
// refs
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
@ -62,6 +69,27 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementing scroll animation styles based on the scroll length of the container
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const scrollTop = containerRef.current.scrollTop;
|
||||||
|
setIsScrolled(scrollTop > 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const currentContainerRef = containerRef.current;
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
|
||||||
@ -166,11 +194,16 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{workspaces && workspaces.length > 0 && (
|
{workspaces && workspaces.length > 0 && (
|
||||||
<div className="flex flex-col px-4 flex-shrink-0">
|
<div className="flex flex-col h-full overflow-x-hidden px-4">
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<div className="rounded text-custom-sidebar-text-400 px-1.5 text-sm font-semibold">Your workspaces</div>
|
<div className="rounded text-custom-sidebar-text-400 px-1.5 text-sm font-semibold">Your workspaces</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2 mt-2">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`space-y-2 mt-2 pt-2 h-full overflow-y-auto ${
|
||||||
|
isScrolled ? "border-t border-custom-sidebar-border-300" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.map((workspace) => (
|
||||||
<Link
|
<Link
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
@ -208,7 +241,7 @@ export const ProfileLayoutSidebar = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-grow flex items-end px-4 py-2">
|
<div className="flex-grow flex items-end px-4 py-2 border-t border-custom-border-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
|
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
|
||||||
|
@ -48,25 +48,27 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||||
{tokens ? (
|
{tokens ? (
|
||||||
tokens.length > 0 ? (
|
<section className="pr-9 py-8 w-full overflow-y-auto">
|
||||||
<section className="pr-9 py-8 w-full overflow-y-auto">
|
{tokens.length > 0 ? (
|
||||||
<div className="flex items-center justify-between py-3.5 border-b border-custom-border-200 mb-2">
|
<>
|
||||||
<h3 className="text-xl font-medium">API tokens</h3>
|
<div className="flex items-center justify-between py-3.5 border-b border-custom-border-200 mb-2">
|
||||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
<h3 className="text-xl font-medium">API tokens</h3>
|
||||||
Add API token
|
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||||
</Button>
|
Add API token
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{tokens.map((token) => (
|
||||||
|
<ApiTokenListItem key={token.id} token={token} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="mx-auto">
|
||||||
|
<ApiTokenEmptyState onClick={() => setIsCreateTokenModalOpen(true)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
{tokens.map((token) => (
|
</section>
|
||||||
<ApiTokenListItem key={token.id} token={token} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : (
|
|
||||||
<div className="mx-auto py-8">
|
|
||||||
<ApiTokenEmptyState onClick={() => setIsCreateTokenModalOpen(true)} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full grid place-items-center p-4">
|
<div className="h-full w-full grid place-items-center p-4">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -4,6 +4,7 @@ import Image from "next/image";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// services
|
// services
|
||||||
import { WorkspaceService } from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
import { UserService } from "services/user.service";
|
import { UserService } from "services/user.service";
|
||||||
@ -30,15 +31,23 @@ import type { IWorkspaceMemberInvitation } from "types";
|
|||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
|
// mobx-store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
const UserInvitationsPage: NextPageWithLayout = () => {
|
const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
||||||
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
|
||||||
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
|
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
|
||||||
|
|
||||||
|
// store
|
||||||
|
const {
|
||||||
|
workspace: { workspaceSlug },
|
||||||
|
user: { currentUserSettings },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@ -51,6 +60,12 @@ const UserInvitationsPage: NextPageWithLayout = () => {
|
|||||||
workspaceService.userWorkspaceInvitations()
|
workspaceService.userWorkspaceInvitations()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const redirectWorkspaceSlug =
|
||||||
|
workspaceSlug ||
|
||||||
|
currentUserSettings?.workspace?.last_workspace_slug ||
|
||||||
|
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||||
|
"";
|
||||||
|
|
||||||
const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => {
|
const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => {
|
||||||
if (action === "accepted") {
|
if (action === "accepted") {
|
||||||
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
|
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
|
||||||
@ -180,7 +195,7 @@ const UserInvitationsPage: NextPageWithLayout = () => {
|
|||||||
>
|
>
|
||||||
Accept & Join
|
Accept & Join
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/">
|
<Link href={`/${redirectWorkspaceSlug}`}>
|
||||||
<a>
|
<a>
|
||||||
<Button variant="neutral-primary" size="md">
|
<Button variant="neutral-primary" size="md">
|
||||||
Go Home
|
Go Home
|
||||||
@ -197,8 +212,8 @@ const UserInvitationsPage: NextPageWithLayout = () => {
|
|||||||
description="You can see here if someone invites you to a workspace."
|
description="You can see here if someone invites you to a workspace."
|
||||||
image={emptyInvitation}
|
image={emptyInvitation}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: "Back to Dashboard",
|
text: "Back to dashboard",
|
||||||
onClick: () => router.push("/"),
|
onClick: () => router.push(`/${redirectWorkspaceSlug}`),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -206,7 +221,7 @@ const UserInvitationsPage: NextPageWithLayout = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
UserInvitationsPage.getLayout = function getLayout(page: ReactElement) {
|
UserInvitationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user