diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 7bea75fa0..4cdcc7b76 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -304,6 +304,7 @@ class IssueRelationSerializer(BaseSerializer): sequence_id = serializers.IntegerField( source="related_issue.sequence_id", read_only=True ) + name = serializers.CharField(source="related_issue.name", read_only=True) relation_type = serializers.CharField(read_only=True) class Meta: @@ -313,6 +314,7 @@ class IssueRelationSerializer(BaseSerializer): "project_id", "sequence_id", "relation_type", + "name", ] read_only_fields = [ "workspace", @@ -328,6 +330,7 @@ class RelatedIssueSerializer(BaseSerializer): sequence_id = serializers.IntegerField( source="issue.sequence_id", read_only=True ) + name = serializers.CharField(source="issue.name", read_only=True) relation_type = serializers.CharField(read_only=True) class Meta: @@ -337,6 +340,7 @@ class RelatedIssueSerializer(BaseSerializer): "project_id", "sequence_id", "relation_type", + "name", ] read_only_fields = [ "workspace", diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 7f1d49632..61cc7081b 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -64,8 +64,7 @@ export type TIssueParams = | "order_by" | "type" | "sub_issue" - | "show_empty_groups" - | "start_target_date"; + | "show_empty_groups"; export type TCalendarLayouts = "month" | "week"; @@ -93,7 +92,6 @@ export interface IIssueDisplayFilterOptions { layout?: TIssueLayouts; order_by?: TIssueOrderByOptions; show_empty_groups?: boolean; - start_target_date?: boolean; sub_issue?: boolean; type?: TIssueTypeFilters; } diff --git a/packages/ui/src/progress/linear-progress-indicator.tsx b/packages/ui/src/progress/linear-progress-indicator.tsx index 467285024..7cf9717a0 100644 --- a/packages/ui/src/progress/linear-progress-indicator.tsx +++ b/packages/ui/src/progress/linear-progress-indicator.tsx @@ -1,13 +1,20 @@ import React from "react"; import { Tooltip } from "../tooltip"; +import { cn } from "../../helpers"; type Props = { data: any; noTooltip?: boolean; inPercentage?: boolean; + size?: "sm" | "md" | "lg"; }; -export const LinearProgressIndicator: React.FC = ({ data, noTooltip = false, inPercentage = false }) => { +export const LinearProgressIndicator: React.FC = ({ + data, + noTooltip = false, + inPercentage = false, + size = "sm", +}) => { const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0); // eslint-disable-next-line @typescript-eslint/no-unused-vars let progress = 0; @@ -23,18 +30,24 @@ export const LinearProgressIndicator: React.FC = ({ data, noTooltip = fal if (noTooltip) return
; else return ( - -
+ +
); }); return ( -
+
{total === 0 ? ( -
{bars}
+
{bars}
) : ( -
{bars}
+
{bars}
)}
); diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index b5a666774..72a67883e 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -40,9 +40,9 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => { }`}`} target={activity.issue === null ? "_self" : "_blank"} rel={activity.issue === null ? "" : "noopener noreferrer"} - className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline whitespace-nowrap" + className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" > - {`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}{" "} + {`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}{" "} {activity.issue_detail?.name} ) : ( @@ -267,7 +267,7 @@ const activityDetails: { {activity.new_value} {showIssue && ( - + {" "} to diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index d42f9f543..12d836695 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -2,6 +2,7 @@ import { MouseEvent } from "react"; import Link from "next/link"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; +import { useTheme } from "next-themes"; // hooks import { useCycle, useIssues, useProject, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; @@ -43,6 +44,7 @@ interface IActiveCycleDetails { export const ActiveCycleDetails: React.FC = observer((props) => { // props const { workspaceSlug, projectId } = props; + const { resolvedTheme } = useTheme(); // store hooks const { currentUser } = useUser(); const { @@ -76,7 +78,9 @@ export const ActiveCycleDetails: React.FC = observer((props ); const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"]; - const emptyStateImage = getEmptyStateImagePath("cycle", "active", currentUser?.theme.theme === "light"); + + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode); if (!activeCycle && isLoading) return ( @@ -161,7 +165,7 @@ export const ActiveCycleDetails: React.FC = observer((props

{truncateText(activeCycle.name, 70)}

- + {`${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`} @@ -251,7 +255,7 @@ export const ActiveCycleDetails: React.FC = observer((props
Progress - +
{Object.keys(groupedIssues).map((group, index) => ( diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 1365e4aa4..19e7f2225 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks import { useUser } from "hooks/store"; // components @@ -18,11 +19,15 @@ export interface ICyclesBoard { export const CyclesBoard: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; + // theme + const { resolvedTheme } = useTheme(); // store hooks const { currentUser } = useUser(); const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light"); + + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 3dfa2130b..90fcdd8f9 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks import { useUser } from "hooks/store"; // components @@ -19,11 +20,15 @@ export interface ICyclesList { export const CyclesList: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId } = props; + // theme + const { resolvedTheme } = useTheme(); // store hooks const { currentUser } = useUser(); const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light"); + + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index e648a158e..865cc68a1 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components import { DateDropdown, ProjectDropdown } from "components/dropdowns"; @@ -11,19 +12,28 @@ import { ICycle } from "@plane/types"; type Props = { handleFormSubmit: (values: Partial) => Promise; handleClose: () => void; + status: boolean; projectId: string; setActiveProject: (projectId: string) => void; data?: ICycle | null; }; +const defaultValues: Partial = { + name: "", + description: "", + start_date: null, + end_date: null, +}; + export const CycleForm: React.FC = (props) => { - const { handleFormSubmit, handleClose, projectId, setActiveProject, data } = props; + const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props; // form data const { formState: { errors, isSubmitting }, handleSubmit, control, watch, + reset, } = useForm({ defaultValues: { project: projectId, @@ -34,6 +44,13 @@ export const CycleForm: React.FC = (props) => { }, }); + useEffect(() => { + reset({ + ...defaultValues, + ...data, + }); + }, [data, reset]); + const startDate = watch("start_date"); const endDate = watch("end_date"); diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index fed6eefc2..8144feef7 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; // services import { CycleService } from "services/cycle.service"; // hooks -import { useApplication, useCycle } from "hooks/store"; +import { useApplication, useCycle, useProject } from "hooks/store"; import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; // components @@ -25,11 +25,12 @@ const cycleService = new CycleService(); export const CycleCreateUpdateModal: React.FC = (props) => { const { isOpen, handleClose, data, workspaceSlug, projectId } = props; // states - const [activeProject, setActiveProject] = useState(projectId); + const [activeProject, setActiveProject] = useState(null); // store hooks const { eventTracker: { postHogEventTracker }, } = useApplication(); + const { workspaceProjectIds } = useProject(); const { createCycle, updateCycleDetails } = useCycle(); // toast alert const { setToastAlert } = useToast(); @@ -134,6 +135,27 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }; + useEffect(() => { + // if modal is closed, reset active project to null + // and return to avoid activeProject being set to some other project + if (!isOpen) { + setActiveProject(null); + return; + } + + // if data is present, set active project to the project of the + // issue. This has more priority than the project in the url. + if (data && data.project) { + setActiveProject(data.project); + return; + } + + // if data is not present, set active project to the project + // in the url. This has the least priority. + if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProject) + setActiveProject(projectId ?? workspaceProjectIds?.[0] ?? null); + }, [activeProject, data, projectId, workspaceProjectIds, isOpen]); + return ( @@ -164,7 +186,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { diff --git a/web/components/dashboard/home-dashboard-widgets.tsx b/web/components/dashboard/home-dashboard-widgets.tsx index fb6055a67..2e2f9ef88 100644 --- a/web/components/dashboard/home-dashboard-widgets.tsx +++ b/web/components/dashboard/home-dashboard-widgets.tsx @@ -50,11 +50,11 @@ export const DashboardWidgets = observer(() => { // if the widget is full width, return it in a 2 column grid if (widget.fullWidth) return ( -
+
); - else return ; + else return ; })}
); diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index 6f2913197..f5727f277 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -99,7 +99,7 @@ export const CreatedIssuesWidget: React.FC = observer((props) => {
{ISSUES_TABS_LIST.map((tab) => ( - + = observer((props) => { )} >
{stat.count}
-

{stat.title}

+

{stat.title}

); diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index c4318d889..aae8ff54b 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -57,7 +57,7 @@ const ProjectListItem: React.FC = observer((props) => {
{projectDetails.members?.map((member) => ( - + ))}
diff --git a/web/components/dnd/StrictModeDroppable.tsx b/web/components/dnd/StrictModeDroppable.tsx deleted file mode 100644 index 9feba79b2..000000000 --- a/web/components/dnd/StrictModeDroppable.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { useState, useEffect } from "react"; - -// react beautiful dnd -import { Droppable, DroppableProps } from "@hello-pangea/dnd"; - -const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { - const [enabled, setEnabled] = useState(false); - - useEffect(() => { - const animation = requestAnimationFrame(() => setEnabled(true)); - - return () => { - cancelAnimationFrame(animation); - setEnabled(false); - }; - }, []); - - if (!enabled) return null; - - return {children}; -}; - -export default StrictModeDroppable; diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx index 51c84098a..39b72fe08 100644 --- a/web/components/dropdowns/cycle.tsx +++ b/web/components/dropdowns/cycle.tsx @@ -30,6 +30,7 @@ type ButtonProps = { hideIcon: boolean; hideText?: boolean; dropdownArrow: boolean; + isActive?: boolean; dropdownArrowClassName: string; placeholder: string; tooltip: boolean; @@ -51,6 +52,7 @@ const BorderButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, placeholder, tooltip, } = props; @@ -60,6 +62,7 @@ const BorderButton = (props: ButtonProps) => {
@@ -111,6 +114,7 @@ const TransparentButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, placeholder, tooltip, } = props; @@ -120,6 +124,7 @@ const TransparentButton = (props: ButtonProps) => {
@@ -268,6 +273,7 @@ export const CycleDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "border-without-text" ? ( @@ -279,6 +285,7 @@ export const CycleDropdown: React.FC = observer((props) => { hideIcon={hideIcon} hideText placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "background-with-text" ? ( @@ -310,6 +317,7 @@ export const CycleDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "transparent-without-text" ? ( @@ -321,6 +329,7 @@ export const CycleDropdown: React.FC = observer((props) => { hideIcon={hideIcon} hideText placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : null} diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 3cbf3449a..5dac4ee49 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -2,7 +2,7 @@ import React, { useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; import DatePicker from "react-datepicker"; import { usePopper } from "react-popper"; -import { CalendarDays, X } from "lucide-react"; +import { Calendar, CalendarDays, X } from "lucide-react"; // hooks import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -23,6 +23,7 @@ type Props = TDropdownProps & { onChange: (val: Date | null) => void; value: Date | string | null; closeOnSelect?: boolean; + showPlaceholderIcon?: boolean; }; type ButtonProps = { @@ -33,9 +34,11 @@ type ButtonProps = { isClearable: boolean; hideIcon?: boolean; hideText?: boolean; + isActive?: boolean; onClear: () => void; placeholder: string; tooltip: boolean; + showPlaceholderIcon?: boolean; }; const BorderButton = (props: ButtonProps) => { @@ -47,6 +50,7 @@ const BorderButton = (props: ButtonProps) => { isClearable, hideIcon = false, hideText = false, + isActive = false, onClear, placeholder, tooltip, @@ -61,6 +65,7 @@ const BorderButton = (props: ButtonProps) => {
@@ -131,9 +136,11 @@ const TransparentButton = (props: ButtonProps) => { isClearable, hideIcon = false, hideText = false, + isActive = false, onClear, placeholder, tooltip, + showPlaceholderIcon = false, } = props; return ( @@ -145,11 +152,16 @@ const TransparentButton = (props: ButtonProps) => {
{!hideIcon && icon} {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {showPlaceholderIcon && !date && ( + + )} + {isClearable && ( = (props) => { placement, tabIndex, tooltip = false, + showPlaceholderIcon = false, value, } = props; const [isOpen, setIsOpen] = useState(false); @@ -246,6 +259,7 @@ export const DateDropdown: React.FC = (props) => { placeholder={placeholder} isClearable={isClearable && isDateSelected} onClear={() => onChange(null)} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "border-without-text" ? ( @@ -258,6 +272,7 @@ export const DateDropdown: React.FC = (props) => { placeholder={placeholder} isClearable={isClearable && isDateSelected} onClear={() => onChange(null)} + isActive={isOpen} tooltip={tooltip} hideText /> @@ -296,7 +311,9 @@ export const DateDropdown: React.FC = (props) => { placeholder={placeholder} isClearable={isClearable && isDateSelected} onClear={() => onChange(null)} + isActive={isOpen} tooltip={tooltip} + showPlaceholderIcon={showPlaceholderIcon} /> ) : buttonVariant === "transparent-without-text" ? ( = (props) => { placeholder={placeholder} isClearable={isClearable && isDateSelected} onClear={() => onChange(null)} + isActive={isOpen} tooltip={tooltip} hideText + showPlaceholderIcon={showPlaceholderIcon} /> ) : null} diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index 18144540d..31b2a840d 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -31,6 +31,7 @@ type ButtonProps = { dropdownArrowClassName: string; hideIcon?: boolean; hideText?: boolean; + isActive?: boolean; placeholder: string; tooltip: boolean; }; @@ -51,6 +52,7 @@ const BorderButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, placeholder, tooltip, } = props; @@ -64,6 +66,7 @@ const BorderButton = (props: ButtonProps) => {
@@ -123,6 +126,7 @@ const TransparentButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, placeholder, tooltip, } = props; @@ -136,6 +140,7 @@ const TransparentButton = (props: ButtonProps) => {
@@ -276,6 +281,7 @@ export const EstimateDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "border-without-text" ? ( @@ -287,6 +293,7 @@ export const EstimateDropdown: React.FC = observer((props) => { hideIcon={hideIcon} hideText placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "background-with-text" ? ( @@ -318,6 +325,7 @@ export const EstimateDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "transparent-without-text" ? ( @@ -329,6 +337,7 @@ export const EstimateDropdown: React.FC = observer((props) => { hideIcon={hideIcon} hideText placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : null} diff --git a/web/components/dropdowns/member/buttons.tsx b/web/components/dropdowns/member/buttons.tsx index c1ec93c53..e1664cdb4 100644 --- a/web/components/dropdowns/member/buttons.tsx +++ b/web/components/dropdowns/member/buttons.tsx @@ -14,6 +14,7 @@ type ButtonProps = { placeholder: string; hideIcon?: boolean; hideText?: boolean; + isActive?: boolean; tooltip: boolean; userIds: string | string[] | null; }; @@ -50,6 +51,7 @@ export const BorderButton = observer((props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, placeholder, userIds, tooltip, @@ -57,7 +59,7 @@ export const BorderButton = observer((props: ButtonProps) => { // store hooks const { getUserDetails } = useMember(); - const isMultiple = Array.isArray(userIds); + const isArray = Array.isArray(userIds); return ( {
{!hideIcon && } {!hideText && ( - - {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + {isArray && userIds.length > 0 + ? userIds.length === 1 + ? getUserDetails(userIds[0])?.display_name + : "" + : placeholder} )} {dropdownArrow && ( @@ -99,7 +106,7 @@ export const BackgroundButton = observer((props: ButtonProps) => { // store hooks const { getUserDetails } = useMember(); - const isMultiple = Array.isArray(userIds); + const isArray = Array.isArray(userIds); return ( { > {!hideIcon && } {!hideText && ( - - {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + {isArray && userIds.length > 0 + ? userIds.length === 1 + ? getUserDetails(userIds[0])?.display_name + : "" + : placeholder} )} {dropdownArrow && ( @@ -134,6 +145,7 @@ export const TransparentButton = observer((props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, placeholder, userIds, tooltip, @@ -141,7 +153,7 @@ export const TransparentButton = observer((props: ButtonProps) => { // store hooks const { getUserDetails } = useMember(); - const isMultiple = Array.isArray(userIds); + const isArray = Array.isArray(userIds); return ( {
{!hideIcon && } {!hideText && ( - - {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + {isArray && userIds.length > 0 + ? userIds.length === 1 + ? getUserDetails(userIds[0])?.display_name + : "" + : placeholder} )} {dropdownArrow && ( diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx index 7cd878841..cc1650527 100644 --- a/web/components/dropdowns/member/project-member.tsx +++ b/web/components/dropdowns/member/project-member.tsx @@ -147,6 +147,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "border-without-text" ? ( @@ -157,6 +158,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} hideText /> @@ -189,6 +191,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "transparent-without-text" ? ( @@ -199,6 +202,7 @@ export const ProjectMemberDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} hideText /> diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx index e41555802..4fc5e0e22 100644 --- a/web/components/dropdowns/module.tsx +++ b/web/components/dropdowns/module.tsx @@ -38,6 +38,7 @@ type ButtonProps = { dropdownArrowClassName: string; hideIcon?: boolean; hideText?: boolean; + isActive?: boolean; module: IModule | null; placeholder: string; tooltip: boolean; @@ -50,6 +51,7 @@ const BorderButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, module, placeholder, tooltip, @@ -60,6 +62,7 @@ const BorderButton = (props: ButtonProps) => {
@@ -110,6 +113,7 @@ const TransparentButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, module, placeholder, tooltip, @@ -120,6 +124,7 @@ const TransparentButton = (props: ButtonProps) => {
@@ -267,6 +272,7 @@ export const ModuleDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "border-without-text" ? ( @@ -278,6 +284,7 @@ export const ModuleDropdown: React.FC = observer((props) => { hideIcon={hideIcon} hideText placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "background-with-text" ? ( @@ -309,6 +316,7 @@ export const ModuleDropdown: React.FC = observer((props) => { dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "transparent-without-text" ? ( @@ -320,6 +328,7 @@ export const ModuleDropdown: React.FC = observer((props) => { hideIcon={hideIcon} hideText placeholder={placeholder} + isActive={isOpen} tooltip={tooltip} /> ) : null} diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index dff1f5aaa..7f96a3bd1 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -31,6 +31,7 @@ type ButtonProps = { dropdownArrowClassName: string; hideIcon?: boolean; hideText?: boolean; + isActive?: boolean; highlightUrgent: boolean; priority: TIssuePriorities; tooltip: boolean; @@ -181,6 +182,7 @@ const TransparentButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, highlightUrgent, priority, tooltip, @@ -207,6 +209,7 @@ const TransparentButton = (props: ButtonProps) => { "px-0.5": hideText, // highlight the whole button if text is hidden and priority is urgent "bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent, + "bg-custom-background-80": isActive, }, className )} @@ -312,7 +315,13 @@ export const PriorityDropdown: React.FC = (props) => { as="div" ref={dropdownRef} tabIndex={tabIndex} - className={cn("h-full", className)} + className={cn( + "h-full", + { + "bg-custom-background-80": isOpen, + }, + className + )} value={value} onChange={onChange} disabled={disabled} @@ -402,6 +411,7 @@ export const PriorityDropdown: React.FC = (props) => { dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "transparent-without-text" ? ( @@ -414,6 +424,7 @@ export const PriorityDropdown: React.FC = (props) => { dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} + isActive={isOpen} tooltip={tooltip} hideText /> diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx index fb8446d23..9ad41622b 100644 --- a/web/components/dropdowns/state.tsx +++ b/web/components/dropdowns/state.tsx @@ -30,6 +30,7 @@ type ButtonProps = { dropdownArrowClassName: string; hideIcon?: boolean; hideText?: boolean; + isActive?: boolean; state: IState | undefined; tooltip: boolean; }; @@ -41,6 +42,7 @@ const BorderButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, state, tooltip, } = props; @@ -50,6 +52,9 @@ const BorderButton = (props: ButtonProps) => {
@@ -111,6 +116,7 @@ const TransparentButton = (props: ButtonProps) => { dropdownArrowClassName, hideIcon = false, hideText = false, + isActive = false, state, tooltip, } = props; @@ -120,6 +126,9 @@ const TransparentButton = (props: ButtonProps) => {
@@ -251,6 +260,7 @@ export const StateDropdown: React.FC = observer((props) => { dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "border-without-text" ? ( @@ -260,6 +270,7 @@ export const StateDropdown: React.FC = observer((props) => { dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} + isActive={isOpen} tooltip={tooltip} hideText /> @@ -289,6 +300,7 @@ export const StateDropdown: React.FC = observer((props) => { dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} + isActive={isOpen} tooltip={tooltip} /> ) : buttonVariant === "transparent-without-text" ? ( @@ -298,6 +310,7 @@ export const StateDropdown: React.FC = observer((props) => { dropdownArrow={dropdownArrow && !disabled} dropdownArrowClassName={dropdownArrowClassName} hideIcon={hideIcon} + isActive={isOpen} tooltip={tooltip} hideText /> diff --git a/web/components/gantt-chart/blocks/blocks-display.tsx b/web/components/gantt-chart/blocks/blocks-display.tsx index 0c368090d..02d2eb865 100644 --- a/web/components/gantt-chart/blocks/blocks-display.tsx +++ b/web/components/gantt-chart/blocks/blocks-display.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // hooks import { useChart } from "../hooks"; // helpers -import { ChartDraggable } from "../helpers/draggable"; +import { ChartAddBlock, ChartDraggable } from "components/gantt-chart"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { IBlockUpdateData, IGanttBlock } from "../types"; @@ -15,6 +15,7 @@ export type GanttChartBlocksProps = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + showAllBlocks: boolean; }; export const GanttChartBlocks: FC = (props) => { @@ -26,6 +27,7 @@ export const GanttChartBlocks: FC = (props) => { enableBlockLeftResize, enableBlockRightResize, enableBlockMove, + showAllBlocks, } = props; const { activeBlock, dispatch } = useChart(); @@ -45,6 +47,8 @@ export const GanttChartBlocks: FC = (props) => { totalBlockShifts: number, dragDirection: "left" | "right" | "move" ) => { + if (!block.start_date || !block.target_date) return; + const originalStartDate = new Date(block.start_date); const updatedStartDate = new Date(originalStartDate); @@ -75,27 +79,31 @@ export const GanttChartBlocks: FC = (props) => { > {blocks && blocks.length > 0 && - blocks.map( - (block) => - block.start_date && - block.target_date && ( -
updateActiveBlock(block)} - onMouseLeave={() => updateActiveBlock(null)} - > - handleChartBlockPosition(block, ...args)} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - /> -
- ) - )} + blocks.map((block) => { + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!showAllBlocks && !(block.start_date && block.target_date)) return; + + const isBlockVisibleOnChart = block.start_date && block.target_date; + + return ( +
updateActiveBlock(block)} + onMouseLeave={() => updateActiveBlock(null)} + > + {!isBlockVisibleOnChart && } + handleChartBlockPosition(block, ...args)} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} + /> +
+ ); + })}
); }; diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx index 734d85efb..4592bfb5b 100644 --- a/web/components/gantt-chart/chart/index.tsx +++ b/web/components/gantt-chart/chart/index.tsx @@ -46,22 +46,25 @@ type ChartViewRootProps = { enableBlockMove: boolean; enableReorder: boolean; bottomSpacing: boolean; + showAllBlocks: boolean; }; -export const ChartViewRoot: FC = ({ - border, - title, - blocks = null, - loaderTitle, - blockUpdateHandler, - sidebarToRender, - blockToRender, - enableBlockLeftResize, - enableBlockRightResize, - enableBlockMove, - enableReorder, - bottomSpacing, -}) => { +export const ChartViewRoot: FC = (props) => { + const { + border, + title, + blocks = null, + loaderTitle, + blockUpdateHandler, + sidebarToRender, + blockToRender, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, + enableReorder, + bottomSpacing, + showAllBlocks, + } = props; // states const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [fullScreenMode, setFullScreenMode] = useState(false); @@ -311,6 +314,7 @@ export const ChartViewRoot: FC = ({ enableBlockLeftResize={enableBlockLeftResize} enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove} + showAllBlocks={showAllBlocks} /> )}
diff --git a/web/components/gantt-chart/chart/month.tsx b/web/components/gantt-chart/chart/month.tsx index 0bc6b7460..0b7a4c452 100644 --- a/web/components/gantt-chart/chart/month.tsx +++ b/web/components/gantt-chart/chart/month.tsx @@ -1,5 +1,4 @@ import { FC } from "react"; - // hooks import { useChart } from "../hooks"; // types diff --git a/web/components/gantt-chart/helpers/add-block.tsx b/web/components/gantt-chart/helpers/add-block.tsx new file mode 100644 index 000000000..bfeddffa2 --- /dev/null +++ b/web/components/gantt-chart/helpers/add-block.tsx @@ -0,0 +1,91 @@ +import { useEffect, useRef, useState } from "react"; +import { addDays } from "date-fns"; +import { Plus } from "lucide-react"; +// hooks +import { useChart } from "../hooks"; +// ui +import { Tooltip } from "@plane/ui"; +// helpers +import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import { IBlockUpdateData, IGanttBlock } from "../types"; + +type Props = { + block: IGanttBlock; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; +}; + +export const ChartAddBlock: React.FC = (props) => { + const { block, blockUpdateHandler } = props; + // states + const [isButtonVisible, setIsButtonVisible] = useState(false); + const [buttonXPosition, setButtonXPosition] = useState(0); + const [buttonStartDate, setButtonStartDate] = useState(null); + // refs + const containerRef = useRef(null); + // chart hook + const { currentViewData } = useChart(); + + const handleButtonClick = () => { + if (!currentViewData) return; + + const { startDate: chartStartDate, width } = currentViewData.data; + const columnNumber = buttonXPosition / width; + + const startDate = addDays(chartStartDate, columnNumber); + const endDate = addDays(startDate, 1); + + blockUpdateHandler(block.data, { + start_date: renderFormattedPayloadDate(startDate) ?? undefined, + target_date: renderFormattedPayloadDate(endDate) ?? undefined, + }); + }; + + useEffect(() => { + const container = containerRef.current; + + if (!container) return; + + const handleMouseMove = (e: MouseEvent) => { + if (!currentViewData) return; + + setButtonXPosition(e.offsetX); + + const { startDate: chartStartDate, width } = currentViewData.data; + const columnNumber = buttonXPosition / width; + + const startDate = addDays(chartStartDate, columnNumber); + setButtonStartDate(startDate); + }; + + container.addEventListener("mousemove", handleMouseMove); + + return () => { + container?.removeEventListener("mousemove", handleMouseMove); + }; + }, [buttonXPosition, currentViewData]); + + return ( +
setIsButtonVisible(true)} + onMouseLeave={() => setIsButtonVisible(false)} + > +
+ {isButtonVisible && ( + + + + )} +
+ ); +}; diff --git a/web/components/gantt-chart/helpers/block-structure.tsx b/web/components/gantt-chart/helpers/block-structure.tsx index bc59624a5..a7071ba28 100644 --- a/web/components/gantt-chart/helpers/block-structure.tsx +++ b/web/components/gantt-chart/helpers/block-structure.tsx @@ -3,14 +3,11 @@ import { TIssue } from "@plane/types"; import { IGanttBlock } from "components/gantt-chart"; export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => - blocks && blocks.length > 0 - ? blocks - .filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? "")) - .map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.target_date ?? ""), - })) - : []; + blocks && + blocks.map((block) => ({ + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: block.start_date ? new Date(block.start_date) : null, + target_date: block.target_date ? new Date(block.target_date) : null, + })); diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index 8f4f23566..d2c4448bb 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,6 +1,4 @@ import React, { useEffect, useRef, useState } from "react"; - -// icons import { ArrowLeft, ArrowRight } from "lucide-react"; // hooks import { useChart } from "../hooks"; @@ -16,23 +14,17 @@ type Props = { enableBlockMove: boolean; }; -export const ChartDraggable: React.FC = ({ - block, - blockToRender, - handleBlock, - enableBlockLeftResize, - enableBlockRightResize, - enableBlockMove, -}) => { +export const ChartDraggable: React.FC = (props) => { + const { block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove } = props; + // states const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); const [isMoving, setIsMoving] = useState(false); const [posFromLeft, setPosFromLeft] = useState(null); - + // refs const resizableRef = useRef(null); - + // chart hook const { currentViewData, scrollLeft } = useChart(); - // check if cursor reaches either end while resizing/dragging const checkScrollEnd = (e: MouseEvent): number => { const SCROLL_THRESHOLD = 70; @@ -68,7 +60,6 @@ export const ChartDraggable: React.FC = ({ return delWidth; }; - // handle block resize from the left end const handleBlockLeftResize = (e: React.MouseEvent) => { if (!currentViewData || !resizableRef.current || !block.position) return; @@ -120,7 +111,6 @@ export const ChartDraggable: React.FC = ({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; - // handle block resize from the right end const handleBlockRightResize = (e: React.MouseEvent) => { if (!currentViewData || !resizableRef.current || !block.position) return; @@ -163,7 +153,6 @@ export const ChartDraggable: React.FC = ({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; - // handle block x-axis move const handleBlockMove = (e: React.MouseEvent) => { if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return; @@ -210,7 +199,6 @@ export const ChartDraggable: React.FC = ({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; - // scroll to a hidden block const handleScrollToBlock = () => { const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; @@ -220,7 +208,6 @@ export const ChartDraggable: React.FC = ({ // update container's scroll position to the block's position scrollContainer.scrollLeft = block.position.marginLeft - 4; }; - // update block position from viewport's left end on scroll useEffect(() => { const block = resizableRef.current; @@ -229,7 +216,6 @@ export const ChartDraggable: React.FC = ({ setPosFromLeft(block.getBoundingClientRect().left); }, [scrollLeft]); - // check if block is hidden on either side const isBlockHiddenOnLeft = block.position?.marginLeft && diff --git a/web/components/gantt-chart/helpers/index.ts b/web/components/gantt-chart/helpers/index.ts index c4c919ec0..1b51dc374 100644 --- a/web/components/gantt-chart/helpers/index.ts +++ b/web/components/gantt-chart/helpers/index.ts @@ -1 +1,3 @@ +export * from "./add-block"; export * from "./block-structure"; +export * from "./draggable"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index 10c00a363..7673da88e 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -19,36 +19,43 @@ type GanttChartRootProps = { enableBlockMove?: boolean; enableReorder?: boolean; bottomSpacing?: boolean; + showAllBlocks?: boolean; }; -export const GanttChartRoot: FC = ({ - border = true, - title, - blocks, - loaderTitle = "blocks", - blockUpdateHandler, - sidebarToRender, - blockToRender, - enableBlockLeftResize = true, - enableBlockRightResize = true, - enableBlockMove = true, - enableReorder = true, - bottomSpacing = false, -}) => ( - - - -); +export const GanttChartRoot: FC = (props) => { + const { + border = true, + title, + blocks, + loaderTitle = "blocks", + blockUpdateHandler, + sidebarToRender, + blockToRender, + enableBlockLeftResize = true, + enableBlockRightResize = true, + enableBlockMove = true, + enableReorder = true, + bottomSpacing = false, + showAllBlocks = false, + } = props; + + return ( + + + + ); +}; diff --git a/web/components/gantt-chart/sidebar/cycle-sidebar.tsx b/web/components/gantt-chart/sidebar/cycle-sidebar.tsx index b7cb41837..1af1529c2 100644 --- a/web/components/gantt-chart/sidebar/cycle-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycle-sidebar.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; -import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks import { useChart } from "components/gantt-chart/hooks"; @@ -83,7 +82,7 @@ export const CycleGanttSidebar: React.FC = (props) => { return ( - + {(droppableProvided) => (
= (props) => {
)} -
+
); }; diff --git a/web/components/gantt-chart/sidebar/module-sidebar.tsx b/web/components/gantt-chart/sidebar/module-sidebar.tsx index 8dc437269..30f146dc5 100644 --- a/web/components/gantt-chart/sidebar/module-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/module-sidebar.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; -import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks import { useChart } from "components/gantt-chart/hooks"; @@ -83,7 +82,7 @@ export const ModuleGanttSidebar: React.FC = (props) => { return ( - + {(droppableProvided) => (
= (props) => {
)} -
+
); }; diff --git a/web/components/gantt-chart/sidebar/project-view-sidebar.tsx b/web/components/gantt-chart/sidebar/project-view-sidebar.tsx index b591f3b73..da7382859 100644 --- a/web/components/gantt-chart/sidebar/project-view-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/project-view-sidebar.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; -import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks import { useChart } from "components/gantt-chart/hooks"; @@ -84,7 +83,7 @@ export const ProjectViewGanttSidebar: React.FC = (props) => { return ( - + {(droppableProvided) => (
= (props) => {
)} -
+
); }; diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index 88a138a1b..062b76451 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -1,6 +1,5 @@ import { useRouter } from "next/router"; -import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks import { useChart } from "components/gantt-chart/hooks"; @@ -27,10 +26,10 @@ type Props = { ) => Promise; viewId?: string; disableIssueCreation?: boolean; + showAllBlocks?: boolean; }; export const IssueGanttSidebar: React.FC = (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { blockUpdateHandler, blocks, @@ -39,6 +38,7 @@ export const IssueGanttSidebar: React.FC = (props) => { quickAddCallback, viewId, disableIssueCreation, + showAllBlocks = false, } = props; const router = useRouter(); @@ -100,7 +100,7 @@ export const IssueGanttSidebar: React.FC = (props) => { return ( - + {(droppableProvided) => (
= (props) => { <> {blocks ? ( blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? ""); + const isBlockVisibleOnSidebar = block.start_date && block.target_date; + + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!showAllBlocks && !isBlockVisibleOnSidebar) return; + + const duration = + !block.start_date || !block.target_date + ? null + : findTotalDaysInRange(block.start_date, block.target_date); return ( = (props) => {
- {duration} day{duration > 1 ? "s" : ""} + {duration && ( + + {duration} day{duration > 1 ? "s" : ""} + + )}
@@ -173,7 +185,7 @@ export const IssueGanttSidebar: React.FC = (props) => { )}
)} - + ); }; diff --git a/web/components/gantt-chart/types/index.ts b/web/components/gantt-chart/types/index.ts index 9cab40f5c..1360f9f45 100644 --- a/web/components/gantt-chart/types/index.ts +++ b/web/components/gantt-chart/types/index.ts @@ -13,8 +13,8 @@ export interface IGanttBlock { width: number; }; sort_order: number; - start_date: Date; - target_date: Date; + start_date: Date | null; + target_date: Date | null; } export interface IBlockUpdateData { diff --git a/web/components/gantt-chart/views/month-view.ts b/web/components/gantt-chart/views/month-view.ts index fc145d69c..13d054da1 100644 --- a/web/components/gantt-chart/views/month-view.ts +++ b/web/components/gantt-chart/views/month-view.ts @@ -167,6 +167,8 @@ export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, const { startDate } = chartData.data; const { start_date: itemStartDate, target_date: itemTargetDate } = itemData; + if (!itemStartDate || !itemTargetDate) return null; + startDate.setHours(0, 0, 0, 0); itemStartDate.setHours(0, 0, 0, 0); itemTargetDate.setHours(0, 0, 0, 0); diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index b6aeda154..4cce45f44 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -12,12 +12,7 @@ import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plan // icons import { List, PlusIcon, Sheet } from "lucide-react"; // types -import { - IIssueDisplayFilterOptions, - IIssueDisplayProperties, - IIssueFilterOptions, - TStaticViewTypes, -} from "@plane/types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EUserWorkspaceRoles } from "constants/workspace"; diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 458fe443a..ca6d7e0e7 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FC, useCallback, useContext, useEffect, useState } from "react"; +import { ChangeEvent, FC, useCallback, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // hooks import useReloadConfirmations from "hooks/use-reload-confirmation"; diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx index 34923318a..7ad8c836f 100644 --- a/web/components/issues/issue-detail/parent-select.tsx +++ b/web/components/issues/issue-detail/parent-select.tsx @@ -64,6 +64,7 @@ export const IssueParentSelect: React.FC = observer((props) { "cursor-not-allowed": disabled, "hover:bg-custom-background-80": !disabled, + "bg-custom-background-80": isParentIssueModalOpen, }, className )} @@ -72,15 +73,20 @@ export const IssueParentSelect: React.FC = observer((props) > {issue.parent_id && parentIssue ? (
- - {parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id} - + + e.stopPropagation()} + > + {parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id} + + {!disabled && ( - + { e.preventDefault(); @@ -96,7 +102,15 @@ export const IssueParentSelect: React.FC = observer((props) ) : ( Add parent issue )} - {!disabled && } + {!disabled && ( + + + + )} ); diff --git a/web/components/issues/issue-detail/parent/siblings.tsx b/web/components/issues/issue-detail/parent/siblings.tsx index bc93ff138..45eca81d4 100644 --- a/web/components/issues/issue-detail/parent/siblings.tsx +++ b/web/components/issues/issue-detail/parent/siblings.tsx @@ -1,6 +1,5 @@ import { FC } from "react"; import useSWR from "swr"; -import { observer } from "mobx-react-lite"; // components import { IssueParentSiblingItem } from "./sibling-item"; // hooks diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 405dee3d7..1fdd353a6 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Link from "next/link"; import { observer } from "mobx-react-lite"; import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // hooks @@ -101,59 +102,72 @@ export const IssueRelationSelect: React.FC = observer((pro ); diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index f811078f8..0a38c3017 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -184,7 +184,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId?.toString() ?? ""} placeholder="Add assignees" multiple - buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"} + buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} className="w-3/5 flex-grow group" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm justify-between ${ @@ -233,6 +233,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" + showPlaceholderIcon />
@@ -257,6 +258,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" + showPlaceholderIcon />
@@ -332,8 +334,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Relates to
@@ -347,8 +349,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Blocking
@@ -362,8 +364,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Blocked by
@@ -377,8 +379,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
-
-
+
+
Duplicate of
diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 0f81d79a6..d486b2f48 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; // hooks -import { useProject, useWorkspace } from "hooks/store"; +import { useProject } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -57,14 +57,13 @@ const Inputs = (props: any) => { }; export const CalendarQuickAddIssueForm: React.FC = observer((props) => { - const { formKey, groupId, prePopulatedData, quickAddCallback, viewId, onOpen } = props; + const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props; // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; // store hooks const { getProjectById } = useProject(); - const { getWorkspaceBySlug } = useWorkspace(); // refs const ref = useRef(null); // states @@ -73,7 +72,6 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { const { setToastAlert } = useToast(); // derived values - const workspaceDetail = (workspaceSlug && getWorkspaceBySlug(workspaceSlug.toString())) || null; const projectDetail = projectId ? getProjectById(projectId.toString()) : null; const { diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 33f46ba24..89eb58110 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import size from "lodash/size"; +import { useTheme } from "next-themes"; // hooks import { useIssues, useUser } from "hooks/store"; // components @@ -26,6 +27,9 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // theme + const { resolvedTheme } = useTheme(); + // store hooks const { membership: { currentProjectRole }, currentUser, @@ -35,16 +39,13 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath( - "empty-filters", - activeLayout ?? "list", - currentUser?.theme.theme === "light" - ); - const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); + const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode); const issueFilterCount = size( Object.fromEntries( - Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0) + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) ) ); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 258d0d5d3..1d2695ff9 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import size from "lodash/size"; +import { useTheme } from "next-themes"; // hooks import { useIssues, useUser } from "hooks/store"; // components @@ -26,6 +27,9 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // theme + const { resolvedTheme } = useTheme(); + // store hooks const { membership: { currentProjectRole }, currentUser, @@ -35,16 +39,13 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath( - "empty-filters", - activeLayout ?? "list", - currentUser?.theme.theme === "light" - ); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); + const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", isLightMode); const issueFilterCount = size( Object.fromEntries( - Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0) + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) ) ); diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index 49de72aca..b72dfff18 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import size from "lodash/size"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useIssues, useUser } from "hooks/store"; // components @@ -26,6 +27,8 @@ export const ProjectEmptyState: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore, @@ -40,16 +43,13 @@ export const ProjectEmptyState: React.FC = observer(() => { const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath( - "empty-filters", - activeLayout ?? "list", - currentUser?.theme.theme === "light" - ); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode); const issueFilterCount = size( Object.fromEntries( - Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0) + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) ) ); diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 73802886e..601205b5c 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -13,11 +13,12 @@ import { } from "components/gantt-chart"; // types import { TIssue, TUnGroupedIssues } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle"; import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module"; import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views"; +// constants +import { EUserProjectRoles } from "constants/project"; import { EIssueActions } from "../types"; interface IBaseGanttRoot { @@ -76,12 +77,14 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan viewId={viewId} enableQuickIssueCreate disableIssueCreation={!enableIssueCreation || !isAllowed} + showAllBlocks /> )} enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed} + showAllBlocks />
diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index fefee880d..cf1d5d700 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -1,4 +1,3 @@ -import { useRouter } from "next/router"; // ui import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui"; // helpers diff --git a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx index 21aeb3d9d..b4610a2e0 100644 --- a/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/quick-add-issue-form.tsx @@ -54,7 +54,7 @@ const defaultValues: Partial = { }; export const KanBanQuickAddIssueForm: React.FC = observer((props) => { - const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props; + const { formKey, prePopulatedData, quickAddCallback, viewId } = props; // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index d808bded2..2ba023674 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; import isEmpty from "lodash/isEmpty"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; @@ -25,6 +26,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, globalViewId } = router.query; + // theme + const { resolvedTheme } = useTheme(); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); // store @@ -46,7 +49,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS]; - const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode); // filter init from the query params @@ -57,6 +61,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) ) { const routerQueryParams = { ...router.query }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { ["workspaceSlug"]: _workspaceSlug, ["globalViewId"]: _globalViewId, ...filters } = routerQueryParams; let issueFilters: any = {}; diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 3a022d447..a5667f99d 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -2,7 +2,7 @@ import { FC, useCallback } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues, useUser } from "hooks/store"; +import { useUser } from "hooks/store"; // views import { SpreadsheetView } from "./spreadsheet-view"; // types @@ -36,7 +36,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; // store hooks - const { issueMap } = useIssues(); const { membership: { currentProjectRole }, } = useUser(); @@ -55,7 +54,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { [canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed] ); - const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues; const handleIssues = useCallback( diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 3b72a31fe..6aee23a23 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -99,7 +99,7 @@ export const PeekOverviewProperties: FC = observer((pro projectId={projectId} placeholder="Add assignees" multiple - buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"} + buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} className="w-3/4 flex-grow group" buttonContainerClassName="w-full text-left" buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"}`} @@ -148,6 +148,7 @@ export const PeekOverviewProperties: FC = observer((pro buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" + showPlaceholderIcon />
@@ -173,6 +174,7 @@ export const PeekOverviewProperties: FC = observer((pro buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" + showPlaceholderIcon />
@@ -251,8 +253,8 @@ export const PeekOverviewProperties: FC = observer((pro
{/* relates to */} -
-
+
+
Relates to
@@ -267,8 +269,8 @@ export const PeekOverviewProperties: FC = observer((pro
{/* blocking */} -
-
+
+
Blocking
@@ -283,8 +285,8 @@ export const PeekOverviewProperties: FC = observer((pro
{/* blocked by */} -
-
+
+
Blocked by
@@ -299,8 +301,8 @@ export const PeekOverviewProperties: FC = observer((pro
{/* duplicate of */} -
-
+
+
Duplicate of
diff --git a/web/components/issues/select/label.tsx b/web/components/issues/select/label.tsx index 00f095d9e..54931f85d 100644 --- a/web/components/issues/select/label.tsx +++ b/web/components/issues/select/label.tsx @@ -1,6 +1,5 @@ import React, { Fragment, useRef, useState } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; import { Combobox } from "@headlessui/react"; import { usePopper } from "react-popper"; import { observer } from "mobx-react-lite"; diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 0708187c1..93b12d94c 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,5 +1,6 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useModule, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; @@ -15,6 +16,8 @@ export const ModulesListView: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { @@ -25,7 +28,8 @@ export const ModulesListView: React.FC = observer(() => { const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index fa5f7c2c1..3ae0b9e62 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { useTheme } from "next-themes"; import { observer } from "mobx-react-lite"; // hooks import { useApplication, useDashboard, useProject, useUser } from "hooks/store"; @@ -14,6 +15,8 @@ import { Spinner } from "@plane/ui"; import { EUserWorkspaceRoles } from "constants/workspace"; export const WorkspaceDashboardView = observer(() => { + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateProjectModal }, @@ -28,7 +31,8 @@ export const WorkspaceDashboardView = observer(() => { const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds } = useProject(); - const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode); const handleTourCompleted = () => { updateTourCompleted() diff --git a/web/components/pages/create-update-page-modal.tsx b/web/components/pages/create-update-page-modal.tsx index e6763acc6..b13f68fa4 100644 --- a/web/components/pages/create-update-page-modal.tsx +++ b/web/components/pages/create-update-page-modal.tsx @@ -1,8 +1,6 @@ import React, { FC } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// hooks -import { useApplication } from "hooks/store"; // components import { PageForm } from "./page-form"; // types @@ -25,10 +23,6 @@ export const CreateUpdatePageModal: FC = (props) => { const { workspaceSlug } = router.query; const { createPage } = useProjectPages(); - // store hooks - const { - eventTracker: { postHogEventTracker }, - } = useApplication(); const createProjectPage = async (payload: IPage) => { if (!workspaceSlug) return; diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index d1bde308d..4b5634736 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; @@ -18,7 +19,9 @@ type IPagesListView = { export const PagesListView: FC = (props) => { const { pageIds: projectPageIds } = props; - + // theme + const { resolvedTheme } = useTheme(); + // store hooks const { commandPalette: { toggleCreatePageModal }, } = useApplication(); @@ -36,11 +39,8 @@ export const PagesListView: FC = (props) => { ? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS] : PAGE_EMPTY_STATE_DETAILS["All"]; - const emptyStateImage = getEmptyStateImagePath( - "pages", - currentPageTabDetails.key, - currentUser?.theme.theme === "light" - ); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode); const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites"; diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 24916debc..960d5253b 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,5 +1,6 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useUser } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; @@ -14,6 +15,8 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { EUserProjectRoles } from "constants/project"; export const RecentPagesList: FC = observer(() => { + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { @@ -22,7 +25,8 @@ export const RecentPagesList: FC = observer(() => { } = useUser(); const { recentProjectPages } = useProjectPages(); - const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode); // FIXME: replace any with proper type const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 4b3721103..81c9b141c 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -2,6 +2,7 @@ import React from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // components import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; @@ -27,6 +28,9 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { workspaceSlug: string; userId: string; }; + // theme + const { resolvedTheme } = useTheme(); + // store hooks const { membership: { currentWorkspaceRole }, currentUser, @@ -46,7 +50,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { } ); - const emptyStateImage = getEmptyStateImagePath("profile", type, currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode); const activeLayout = issueFilters?.displayFilters?.layout || undefined; diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index ebb166f49..76e67e1b7 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,16 +1,17 @@ import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useProject, useUser } from "hooks/store"; // components import { ProjectCard } from "components/project"; import { Loader } from "@plane/ui"; import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -// icons -import { Plus } from "lucide-react"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; export const ProjectCardList = observer(() => { + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore, @@ -22,7 +23,8 @@ export const ProjectCardList = observer(() => { } = useUser(); const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); - const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode); const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 6b3b3e45a..13ad10441 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search } from "lucide-react"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useProjectView, useUser } from "hooks/store"; // components @@ -14,6 +15,8 @@ import { EUserProjectRoles } from "constants/project"; export const ProjectViewsList = observer(() => { // states const [query, setQuery] = useState(""); + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateViewModal }, @@ -43,7 +46,8 @@ export const ProjectViewsList = observer(() => { const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode); const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase())); diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index ec88c8c87..86386e968 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -13,7 +13,6 @@ const paramsToKey = (params: any) => { start_date, target_date, sub_issue, - start_target_date, project, layout, subscriber, @@ -28,7 +27,6 @@ const paramsToKey = (params: any) => { let createdByKey = created_by ? created_by.split(",") : []; let labelsKey = labels ? labels.split(",") : []; let subscriberKey = subscriber ? subscriber.split(",") : []; - const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE"; const startDateKey = start_date ?? ""; const targetDateKey = target_date ?? ""; const type = params.type ? params.type.toUpperCase() : "NULL"; @@ -47,7 +45,7 @@ const paramsToKey = (params: any) => { labelsKey = labelsKey.sort().join("_"); subscriberKey = subscriberKey.sort().join("_"); - return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${mentionsKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}_${subscriberKey}`; + return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${mentionsKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${subscriberKey}`; }; const myIssuesParamsToKey = (params: any) => { diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index cdaa85883..b0121320e 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -105,9 +105,6 @@ export const handleIssueQueryParamsByLayout = ( }); } - // add start_target_date query param for the gantt_chart layout - if (layout === "gantt_chart") queryParams.push("start_target_date"); - return queryParams; }; diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index fcaef9f70..3d6d501f2 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,6 +1,7 @@ import React, { Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useProject, useUser } from "hooks/store"; // layouts @@ -16,6 +17,8 @@ import { EUserWorkspaceRoles } from "constants/workspace"; import { NextPageWithLayout } from "lib/types"; const AnalyticsPage: NextPageWithLayout = observer(() => { + // theme + const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateProjectModal }, @@ -27,7 +30,8 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { } = useUser(); const { workspaceProjectIds } = useProject(); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode); const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index c00777319..d0dbb3514 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -2,6 +2,7 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; +import { useTheme } from "next-themes"; // hooks import { useCycle, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; @@ -22,6 +23,8 @@ import { EUserWorkspaceRoles } from "constants/workspace"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); + // theme + const { resolvedTheme } = useTheme(); // store hooks const { membership: { currentProjectRole }, @@ -49,7 +52,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { }, [handleCurrentLayout, setCycleTab] ); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light"); + + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); const totalCycles = currentProjectCycleIds?.length ?? 0; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 32299747f..10cda05e1 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -4,6 +4,7 @@ import dynamic from "next/dynamic"; import { Tab } from "@headlessui/react"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks import { useApplication, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; @@ -48,7 +49,9 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { const { workspaceSlug, projectId } = router.query; // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); - // store + // theme + const { resolvedTheme } = useTheme(); + // store hooks const { currentUser, currentUserLoader, @@ -94,7 +97,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { } }; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", currentUser?.theme.theme === "light"); + const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index da3a55f9d..e76473cf4 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -14,7 +14,7 @@ import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // icons import { History, MessageSquare } from "lucide-react"; // ui -import { ExternalLinkIcon, Loader } from "@plane/ui"; +import { Loader } from "@plane/ui"; // fetch-keys import { USER_ACTIVITY } from "constants/fetch-keys"; // helper diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index 9a9c91a37..d92453a30 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -89,7 +89,6 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index 27347536b..dd81cfc0e 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -90,7 +90,6 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index 7096040d5..8295c263d 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -89,7 +89,6 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index ac89c5018..6516b28fd 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -81,7 +81,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { // display filters type: displayFilters?.type || undefined, sub_issue: displayFilters?.sub_issue ?? true, - start_target_date: displayFilters?.start_target_date ?? true, }; const issueFiltersParams: Partial> = {}; @@ -170,7 +169,6 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { type: filters?.type || null, sub_issue: filters?.sub_issue || false, show_empty_groups: filters?.show_empty_groups || false, - start_target_date: filters?.start_target_date || false, }; }; diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index 3c309cecd..e92027235 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -90,7 +90,6 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index a0f8028f8..563af5b01 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -93,7 +93,6 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index e0dae761c..b3df3903b 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -90,7 +90,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index 392b7203f..69393a320 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -89,7 +89,6 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 82fb75ce2..92cc33a64 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -99,7 +99,6 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo filteredParams ); - if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams;