fix: minor bug fixes and quality of life improvements (#3444)

* add concurrency to dev command to avaoid erroring out

* add context to issue activity

* minor quality of life improvement for exporter modal

* show the option to save draft issue only when there is content in name and description

* maintain commonality while referencing the user in activity

* fix minor changes in draft save issue modal logical condition

* minor change is state component for filter selection

* change logic for create issue activity

* change use last draft issue button to state control over previous on hover as that was inconsistent
This commit is contained in:
rahulramesha 2024-01-23 20:45:44 +05:30 committed by GitHub
parent f27efb80e1
commit 47681fe9f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 88 additions and 51 deletions

View File

@ -15,7 +15,7 @@
], ],
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev", "dev": "turbo run dev --concurrency=13",
"start": "turbo run start", "start": "turbo run start",
"lint": "turbo run lint", "lint": "turbo run lint",
"clean": "turbo run clean", "clean": "turbo run clean",
@ -34,4 +34,4 @@
"@types/react": "18.2.42" "@types/react": "18.2.42"
}, },
"packageManager": "yarn@1.22.19" "packageManager": "yarn@1.22.19"
} }

View File

@ -27,6 +27,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
onChange, onChange,
options, options,
onOpen, onOpen,
onClose,
optionsClassName = "", optionsClassName = "",
value, value,
tabIndex, tabIndex,
@ -58,7 +59,10 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
setIsOpen(true); setIsOpen(true);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false); const closeDropdown = () => {
setIsOpen(false);
onClose && onClose();
};
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown); useOutsideClickDetector(dropdownRef, closeDropdown);

View File

@ -36,6 +36,7 @@ export interface ICustomSelectProps extends IDropdownProps {
interface CustomSearchSelectProps { interface CustomSearchSelectProps {
footerOption?: JSX.Element; footerOption?: JSX.Element;
onChange: any; onChange: any;
onClose?: () => void;
options: options:
| { | {
value: any; value: any;

View File

@ -26,7 +26,7 @@ import { capitalizeFirstLetter } from "helpers/string.helper";
// types // types
import { IIssueActivity } from "@plane/types"; import { IIssueActivity } from "@plane/types";
const IssueLink = ({ activity }: { activity: IIssueActivity }) => { export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -341,7 +341,9 @@ const activityDetails: {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
<span className="flex-shrink-0">added this issue to the cycle </span> <span className="flex-shrink-0">
added {showIssue ? <IssueLink activity={activity} /> : "this issue"} to the cycle{" "}
</span>
<a <a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank" target="_blank"
@ -388,7 +390,7 @@ const activityDetails: {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
added this issue to the module{" "} added {showIssue ? <IssueLink activity={activity} /> : "this issue"} to the module{" "}
<a <a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank" target="_blank"
@ -491,11 +493,11 @@ const activityDetails: {
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />, icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
}, },
relates_to: { relates_to: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
marked that this issue relates to{" "} marked that {showIssue ? <IssueLink activity={activity} /> : "this issue"} relates to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</> </>
); );
@ -509,11 +511,11 @@ const activityDetails: {
icon: <RelatedIcon height="12" width="12" color="#6b7280" />, icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
}, },
blocking: { blocking: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
marked this issue is blocking issue{" "} marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} is blocking issue{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</> </>
); );
@ -527,18 +529,18 @@ const activityDetails: {
icon: <BlockerIcon height="12" width="12" color="#6b7280" />, icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
}, },
blocked_by: { blocked_by: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
marked this issue is being blocked by{" "} marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} is being blocked by{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</> </>
); );
else else
return ( return (
<> <>
removed this issue being blocked by issue{" "} removed {showIssue ? <IssueLink activity={activity} /> : "this issue"} being blocked by issue{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>. <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</> </>
); );
@ -546,18 +548,18 @@ const activityDetails: {
icon: <BlockedIcon height="12" width="12" color="#6b7280" />, icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
}, },
duplicate: { duplicate: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
marked this issue as duplicate of{" "} marked {showIssue ? <IssueLink activity={activity} /> : "this issue"} as duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</> </>
); );
else else
return ( return (
<> <>
removed this issue as a duplicate of{" "} removed {showIssue ? <IssueLink activity={activity} /> : "this issue"} as a duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>. <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</> </>
); );

View File

@ -5,7 +5,7 @@ import { History } from "lucide-react";
// hooks // hooks
import { useDashboard, useUser } from "hooks/store"; import { useDashboard, useUser } from "hooks/store";
// components // components
import { ActivityIcon, ActivityMessage } from "components/core"; import { ActivityIcon, ActivityMessage, IssueLink } from "components/core";
import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets"; import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "components/dashboard/widgets";
// ui // ui
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
@ -75,15 +75,7 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
<ActivityMessage activity={activity} showIssue /> <ActivityMessage activity={activity} showIssue />
) : ( ) : (
<span> <span>
created this{" "} created <IssueLink activity={activity} />
<a
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-custom-text-200 hover:underline"
>
Issue.
</a>
</span> </span>
)} )}
</p> </p>

View File

@ -28,6 +28,7 @@ export const Exporter: React.FC<Props> = observer((props) => {
const { isOpen, handleClose, user, provider, mutateServices } = props; const { isOpen, handleClose, user, provider, mutateServices } = props;
// states // states
const [exportLoading, setExportLoading] = useState(false); const [exportLoading, setExportLoading] = useState(false);
const [isSelectOpen, setIsSelectOpen] = useState(false);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -91,7 +92,13 @@ export const Exporter: React.FC<Props> = observer((props) => {
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <Dialog
as="div"
className="relative z-20"
onClose={() => {
if (!isSelectOpen) handleClose();
}}
>
<Transition.Child <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -142,6 +149,8 @@ export const Exporter: React.FC<Props> = observer((props) => {
.join(", ") .join(", ")
: "All projects" : "All projects"
} }
onOpen={() => setIsSelectOpen(true)}
onClose={() => setIsSelectOpen(false)}
optionsClassName="min-w-full" optionsClassName="min-w-full"
multiple multiple
/> />

View File

@ -1,4 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react";
// components // components
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// ui // ui
@ -13,7 +14,7 @@ type Props = {
states: IState[] | undefined; states: IState[] | undefined;
}; };
export const FilterState: React.FC<Props> = (props) => { export const FilterState: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery, states } = props; const { appliedFilters, handleUpdate, searchQuery, states } = props;
const [itemsToRender, setItemsToRender] = useState(5); const [itemsToRender, setItemsToRender] = useState(5);
@ -75,4 +76,4 @@ export const FilterState: React.FC<Props> = (props) => {
)} )}
</> </>
); );
}; });

View File

@ -194,7 +194,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const handleFormChange = () => { const handleFormChange = () => {
if (!onChange) return; if (!onChange) return;
if (isDirty) onChange(watch()); if (isDirty && (watch("name") || watch("description_html"))) onChange(watch());
else onChange(null); else onChange(null);
}; };

View File

@ -1,9 +1,12 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react";
//hooks
import { useUser } from "hooks/store";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// components // components
import { ActivityMessage } from "components/core"; import { ActivityMessage, IssueLink } from "components/core";
// ui // ui
import { ProfileEmptyState } from "components/ui"; import { ProfileEmptyState } from "components/ui";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
@ -17,9 +20,11 @@ import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys";
// services // services
const userService = new UserService(); const userService = new UserService();
export const ProfileActivity = () => { export const ProfileActivity = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
// store hooks
const { currentUser } = useUser();
const { data: userProfileActivity } = useSWR( const { data: userProfileActivity } = useSWR(
workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString()) : null, workspaceSlug && userId ? USER_PROFILE_ACTIVITY(workspaceSlug.toString(), userId.toString()) : null,
@ -54,20 +59,14 @@ export const ProfileActivity = () => {
</div> </div>
<div className="-mt-1 w-4/5 break-words"> <div className="-mt-1 w-4/5 break-words">
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">
<span className="font-medium text-custom-text-100">{activity.actor_detail.display_name} </span> <span className="font-medium text-custom-text-100">
{currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail.display_name}{" "}
</span>
{activity.field ? ( {activity.field ? (
<ActivityMessage activity={activity} showIssue /> <ActivityMessage activity={activity} showIssue />
) : ( ) : (
<span> <span>
created this{" "} created <IssueLink activity={activity} />
<a
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-custom-text-200 hover:underline"
>
Issue.
</a>
</span> </span>
)} )}
</p> </p>
@ -95,4 +94,4 @@ export const ProfileActivity = () => {
</div> </div>
</div> </div>
); );
}; });

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useRef, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ChevronUp, PenSquare, Search } from "lucide-react"; import { ChevronUp, PenSquare, Search } from "lucide-react";
// hooks // hooks
@ -25,10 +25,26 @@ export const WorkspaceSidebarQuickAction = observer(() => {
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({})); const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({}));
//useState control for displaying draft issue button instead of group hover
const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false);
const timeoutRef = useRef<any>();
const isSidebarCollapsed = themeStore.sidebarCollapsed; const isSidebarCollapsed = themeStore.sidebarCollapsed;
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const onMouseEnter = () => {
//if renet before timout clear the timeout
timeoutRef?.current && clearTimeout(timeoutRef.current);
setIsDraftButtonOpen(true);
};
const onMouseLeave = () => {
timeoutRef.current = setTimeout(() => {
setIsDraftButtonOpen(false);
}, 300);
};
return ( return (
<> <>
<CreateUpdateDraftIssueModal <CreateUpdateDraftIssueModal
@ -45,10 +61,12 @@ export const WorkspaceSidebarQuickAction = observer(() => {
className={`mt-4 flex w-full cursor-pointer items-center justify-between px-4 ${ className={`mt-4 flex w-full cursor-pointer items-center justify-between px-4 ${
isSidebarCollapsed ? "flex-col gap-1" : "gap-2" isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`} }`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
> >
{isAuthorizedUser && ( {isAuthorizedUser && (
<div <div
className={`group relative flex w-full cursor-pointer items-center justify-between gap-1 rounded px-2 ${ className={`relative flex w-full cursor-pointer items-center justify-between gap-1 rounded px-2 ${
isSidebarCollapsed isSidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80" ? "px-2 hover:bg-custom-sidebar-background-80"
: "border-[0.5px] border-custom-border-200 px-3 shadow-custom-sidebar-shadow-2xs" : "border-[0.5px] border-custom-border-200 px-3 shadow-custom-sidebar-shadow-2xs"
@ -80,12 +98,16 @@ export const WorkspaceSidebarQuickAction = observer(() => {
isSidebarCollapsed ? "hidden" : "block" isSidebarCollapsed ? "hidden" : "block"
}`} }`}
> >
<ChevronUp className="h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-0" /> <ChevronUp
className={`h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 ${
isDraftButtonOpen ? "rotate-0" : ""
}`}
/>
</button> </button>
<div <div
className={`pointer-events-none fixed left-4 mt-0 h-10 w-[203px] pt-2 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ${ className={`fixed left-4 mt-0 h-10 w-[203px] pt-2 ${isSidebarCollapsed ? "top-[5.5rem]" : "top-24"} ${
isSidebarCollapsed ? "top-[5.5rem]" : "top-24" isDraftButtonOpen ? "block" : "hidden"
}`} }`}
> >
<div className="h-full w-full"> <div className="h-full w-full">

View File

@ -1,6 +1,9 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import useSWR from "swr"; import useSWR from "swr";
import Link from "next/link"; import Link from "next/link";
import { observer } from "mobx-react";
//hooks
import { useUser } from "hooks/store";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// layouts // layouts
@ -21,8 +24,10 @@ import { NextPageWithLayout } from "lib/types";
const userService = new UserService(); const userService = new UserService();
const ProfileActivityPage: NextPageWithLayout = () => { const ProfileActivityPage: NextPageWithLayout = observer(() => {
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
// store hooks
const { currentUser } = useUser();
return ( return (
<section className="mx-auto mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5"> <section className="mx-auto mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
@ -158,7 +163,9 @@ const ProfileActivityPage: NextPageWithLayout = () => {
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`} href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
> >
<span className="text-gray font-medium"> <span className="text-gray font-medium">
{activityItem.actor_detail.display_name} {currentUser?.id === activityItem.actor_detail.id
? "You"
: activityItem.actor_detail.display_name}
</span> </span>
</Link> </Link>
)}{" "} )}{" "}
@ -189,7 +196,7 @@ const ProfileActivityPage: NextPageWithLayout = () => {
)} )}
</section> </section>
); );
}; });
ProfileActivityPage.getLayout = function getLayout(page: ReactElement) { ProfileActivityPage.getLayout = function getLayout(page: ReactElement) {
return <ProfileSettingsLayout>{page}</ProfileSettingsLayout>; return <ProfileSettingsLayout>{page}</ProfileSettingsLayout>;