diff --git a/packages/tailwind-config-custom/tailwind.config.js b/packages/tailwind-config-custom/tailwind.config.js index 8a03462c0..b877dc7c0 100644 --- a/packages/tailwind-config-custom/tailwind.config.js +++ b/packages/tailwind-config-custom/tailwind.config.js @@ -213,7 +213,9 @@ module.exports = { }, }, }), - + screens: { + "3xl": "1792px", + }, // scale down font sizes to 90% of default fontSize: { xs: "0.675rem", diff --git a/packages/ui/src/progress/circular-progress-indicator.tsx b/packages/ui/src/progress/circular-progress-indicator.tsx new file mode 100644 index 000000000..8ef74ea52 --- /dev/null +++ b/packages/ui/src/progress/circular-progress-indicator.tsx @@ -0,0 +1,102 @@ +import React, { Children } from "react"; + +interface ICircularProgressIndicator { + size: number; + percentage: number; + strokeWidth?: number; + strokeColor?: string; + children?: React.ReactNode; +} + +export const CircularProgressIndicator: React.FC = ( + props +) => { + const { size = 40, percentage = 25, strokeWidth = 6, children } = props; + + const sqSize = size; + const radius = (size - strokeWidth) / 2; + const viewBox = `0 0 ${sqSize} ${sqSize}`; + const dashArray = radius * Math.PI * 2; + const dashOffset = dashArray - (dashArray * percentage) / 100; + return ( +
+ + + + + + + + + + + + + + + + + + +
+ {children} +
+
+ ); +}; diff --git a/packages/ui/src/progress/index.tsx b/packages/ui/src/progress/index.tsx index ad5a371c1..56d28cee7 100644 --- a/packages/ui/src/progress/index.tsx +++ b/packages/ui/src/progress/index.tsx @@ -1,3 +1,4 @@ export * from "./radial-progress"; export * from "./progress-bar"; export * from "./linear-progress-indicator"; +export * from "./circular-progress-indicator"; diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 6de940296..ff8d30927 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -15,58 +15,63 @@ type Props = { export const LinksList: React.FC = ({ links, handleDeleteLink, handleEditLink, userAuth }) => { const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - return ( <> {links.map((link) => ( -
- {!isNotAllowed && ( -
- - - - - +
+
+
+ + + + {link.title && link.title !== "" ? link.title : link.url}
- )} - -
- -
-
-
{link.title ?? link.url}
-

- Added {timeAgo(link.created_at)} -
- by{" "} - {link.created_by_detail.is_bot - ? link.created_by_detail.first_name + " Bot" - : link.created_by_detail.display_name} -

-
-
+ + {!isNotAllowed && ( +
+ + + + + +
+ )} +
+
+

+ Added {timeAgo(link.created_at)} +
+ by{" "} + {link.created_by_detail.is_bot + ? link.created_by_detail.first_name + " Bot" + : link.created_by_detail.display_name} +

+
))} diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index fed61b59f..bb6690172 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -1,11 +1,16 @@ import React from "react"; +import Image from "next/image"; // headless ui import { Tab } from "@headlessui/react"; // hooks import useLocalStorage from "hooks/use-local-storage"; import useIssuesView from "hooks/use-issues-view"; +// images +import emptyLabel from "public/empty-state/empty_label.svg"; +import emptyMembers from "public/empty-state/empty_members.svg"; // components +import { StateGroupIcon } from "@plane/ui"; import { SingleProgressStats } from "components/core"; // ui import { Avatar } from "components/ui"; @@ -17,9 +22,7 @@ import { TLabelsDistribution, TStateGroups, } from "types"; -// constants -import { STATE_GROUP_COLORS } from "constants/state"; -// types + type Props = { distribution: { assignees: TAssigneesDistribution[]; @@ -33,6 +36,7 @@ type Props = { module?: IModule; roundedTab?: boolean; noBackground?: boolean; + isPeekModuleDetails?: boolean; }; export const SidebarProgressStats: React.FC = ({ @@ -42,6 +46,7 @@ export const SidebarProgressStats: React.FC = ({ module, roundedTab, noBackground, + isPeekModuleDetails = false, }) => { const { filters, setFilters } = useIssuesView(); @@ -55,7 +60,6 @@ export const SidebarProgressStats: React.FC = ({ return 1; case "States": return 2; - default: return 0; } @@ -72,7 +76,6 @@ export const SidebarProgressStats: React.FC = ({ return setTab("Labels"); case 2: return setTab("States"); - default: return setTab("Assignees"); } @@ -82,15 +85,17 @@ export const SidebarProgressStats: React.FC = ({ as="div" className={`flex w-full items-center gap-2 justify-between rounded-md ${ noBackground ? "" : "bg-custom-background-90" - } px-1 py-1.5 - ${module ? "text-xs" : "text-sm"} `} + } p-0.5 + ${module ? "text-xs" : "text-sm"}`} > `w-full ${ roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded" } px-3 py-1 text-custom-text-100 ${ - selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80" + selected + ? "bg-custom-background-100 text-custom-text-300 shadow-custom-shadow-2xs" + : "text-custom-text-400 hover:text-custom-text-300" }` } > @@ -101,7 +106,9 @@ export const SidebarProgressStats: React.FC = ({ `w-full ${ roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded" } px-3 py-1 text-custom-text-100 ${ - selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80" + selected + ? "bg-custom-background-100 text-custom-text-300 shadow-custom-shadow-2xs" + : "text-custom-text-400 hover:text-custom-text-300" }` } > @@ -112,113 +119,128 @@ export const SidebarProgressStats: React.FC = ({ `w-full ${ roundedTab ? "rounded-3xl border border-custom-border-200" : "rounded" } px-3 py-1 text-custom-text-100 ${ - selected ? " bg-custom-primary text-white" : " hover:bg-custom-background-80" + selected + ? "bg-custom-background-100 text-custom-text-300 shadow-custom-shadow-2xs" + : "text-custom-text-400 hover:text-custom-text-300" }` } > States - - - {distribution.assignees.map((assignee, index) => { - if (assignee.assignee_id) - return ( - - - {assignee.display_name} -
- } - completed={assignee.completed_issues} - total={assignee.total_issues} - onClick={() => { - if (filters?.assignees?.includes(assignee.assignee_id ?? "")) - setFilters({ - assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id), - }); - else - setFilters({ - assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""], - }); - }} - selected={filters?.assignees?.includes(assignee.assignee_id ?? "")} - /> - ); - else - return ( - -
- User + + {distribution.assignees.length > 0 ? ( + distribution.assignees.map((assignee, index) => { + if (assignee.assignee_id) + return ( + + + {assignee.display_name}
- No assignee -
- } - completed={assignee.completed_issues} - total={assignee.total_issues} - /> - ); - })} - - - {distribution.labels.map((label, index) => ( - - { + if (filters?.assignees?.includes(assignee.assignee_id ?? "")) + setFilters({ + assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id), + }); + else + setFilters({ + assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""], + }); + }, + selected: filters?.assignees?.includes(assignee.assignee_id ?? ""), + })} /> - {label.label_name ?? "No labels"} - - } - completed={label.completed_issues} - total={label.total_issues} - onClick={() => { - if (filters.labels?.includes(label.label_id ?? "")) - setFilters({ - labels: filters?.labels?.filter((l) => l !== label.label_id), - }); - else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] }); - }} - selected={filters?.labels?.includes(label.label_id ?? "")} - /> - ))} + ); + else + return ( + +
+ User +
+ No assignee + + } + completed={assignee.completed_issues} + total={assignee.total_issues} + /> + ); + }) + ) : ( +
+
+ empty members +
+
No assignees yet
+
+ )}
- + + {distribution.labels.length > 0 ? ( + distribution.labels.map((label, index) => ( + + + {label.label_name ?? "No labels"} + + } + completed={label.completed_issues} + total={label.total_issues} + {...(!isPeekModuleDetails && { + onClick: () => { + if (filters.labels?.includes(label.label_id ?? "")) + setFilters({ + labels: filters?.labels?.filter((l) => l !== label.label_id), + }); + else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] }); + }, + selected: filters?.labels?.includes(label.label_id ?? ""), + })} + /> + )) + ) : ( +
+
+ empty label +
+
No labels yet
+
+ )} +
+ {Object.keys(groupedIssues).map((group, index) => ( - + {group} } diff --git a/web/components/core/sidebar/single-progress-stats.tsx b/web/components/core/sidebar/single-progress-stats.tsx index 3ff214b57..ba6675c9a 100644 --- a/web/components/core/sidebar/single-progress-stats.tsx +++ b/web/components/core/sidebar/single-progress-stats.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { ProgressBar } from "@plane/ui"; +import { CircularProgressIndicator } from "@plane/ui"; type TSingleProgressStatsProps = { title: any; @@ -27,7 +27,7 @@ export const SingleProgressStats: React.FC = ({
- + {isNaN(Math.floor((completed / total) * 100)) ? "0" : Math.floor((completed / total) * 100)}% diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 8d5df1b32..01c6df5c8 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,25 +1,28 @@ import { useRouter } from "next/router"; import Link from "next/link"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; +import { GanttChart, LayoutGrid, List, Plus } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useLocalStorage from "hooks/use-local-storage"; // ui import { Breadcrumbs, BreadcrumbItem, Button, Tooltip } from "@plane/ui"; -import { Icon } from "components/ui"; // helper import { replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper"; -const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [ +const moduleViewOptions: { type: "list" | "grid" | "gantt_chart"; icon: any }[] = [ { - type: "gantt_chart", - icon: "view_timeline", + type: "list", + icon: List, }, { type: "grid", - icon: "table_rows", + icon: LayoutGrid, + }, + { + type: "gantt_chart", + icon: GanttChart, }, ]; @@ -67,7 +70,7 @@ export const ModulesListHeader: React.FC = observer(() => { }`} onClick={() => setModulesView(option.type)} > - + ))} diff --git a/web/components/modules/index.ts b/web/components/modules/index.ts index 750db2fd0..c87ea79d2 100644 --- a/web/components/modules/index.ts +++ b/web/components/modules/index.ts @@ -7,3 +7,5 @@ export * from "./modal"; export * from "./modules-list-view"; export * from "./sidebar"; export * from "./module-card-item"; +export * from "./module-list-item"; +export * from "./module-peek-overview"; diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 74dd20ad9..620333f8e 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -10,14 +10,16 @@ import useToast from "hooks/use-toast"; import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; // ui import { AssigneesList } from "components/ui"; -import { CustomMenu, Tooltip } from "@plane/ui"; +import { CustomMenu, LayersIcon, Tooltip } from "@plane/ui"; // icons -import { CalendarDays, LinkIcon, Pencil, Star, Target, Trash2 } from "lucide-react"; +import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers -import { copyUrlToClipboard, truncateText } from "helpers/string.helper"; -import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"; // types import { IModule } from "types"; +// constants +import { MODULE_STATUS } from "constants/module"; type Props = { module: IModule; @@ -72,9 +74,32 @@ export const ModuleCardItem: React.FC = observer((props) => { }); }; + const openModuleOverview = () => { + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekModule: module.id }, + }); + }; + const endDate = new Date(module.target_date ?? ""); const startDate = new Date(module.start_date ?? ""); - const lastUpdated = new Date(module.updated_at ?? ""); + + const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const moduleStatus = MODULE_STATUS.find((status) => status.value === module.status); + + const issueCount = + module.completed_issues && module.total_issues + ? module.total_issues === 0 + ? "0 Issue" + : module.total_issues === module.completed_issues + ? module.total_issues > 1 + ? `${module.total_issues} Issues` + : `${module.total_issues} Issue` + : `${module.completed_issues}/${module.total_issues} Issues` + : "0 Issue"; return ( <> @@ -88,96 +113,142 @@ export const ModuleCardItem: React.FC = observer((props) => { /> )} setModuleDeleteModal(false)} /> -
-
-
-
- - - -

- {truncateText(module.name, 75)} -

-
- + + +
+
+ + {module.name} +
+ {moduleStatus && ( + + {moduleStatus.label} + + )} + +
+
+
-
-
- {module?.status?.replace("-", " ")} +
+
+
+ + {issueCount} +
+ {module.members_detail.length > 0 && ( + +
+ +
+
+ )} +
+ + +
+
+
+
+ + +
+ + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + +
{module.is_favorite ? ( - ) : ( - )} - - - + + { + e.preventDefault(); + e.stopPropagation(); + setEditModuleModal(true); + }} + > - - Copy link - - - setEditModuleModal(true)}> - - + Edit module - setModuleDeleteModal(true)}> + { + e.preventDefault(); + e.stopPropagation(); + setModuleDeleteModal(true); + }} + > - + Delete module + { + e.preventDefault(); + e.stopPropagation(); + handleCopyText(); + }} + > + + + Copy module link + +
-
-
- - Start: - {renderShortDateWithYearFormat(startDate, "Not set")} -
-
- - End: - {renderShortDateWithYearFormat(endDate, "Not set")} -
-
-
-
-
- Progress -
-
-
- {isNaN(completionPercentage) ? 0 : completionPercentage.toFixed(0)}% -
-
-

- Last updated: - {renderShortDateWithYearFormat(lastUpdated)} -

- {module.members_detail.length > 0 && ( -
- -
- )} -
-
-
+
+ ); }); diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx new file mode 100644 index 000000000..8b1271cc8 --- /dev/null +++ b/web/components/modules/module-list-item.tsx @@ -0,0 +1,242 @@ +import React, { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { CreateUpdateModuleModal, DeleteModuleModal } from "components/modules"; +// ui +import { AssigneesList } from "components/ui"; +import { CircularProgressIndicator, CustomMenu, Tooltip } from "@plane/ui"; +// icons +import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"; +// types +import { IModule } from "types"; +// constants +import { MODULE_STATUS } from "constants/module"; + +type Props = { + module: IModule; +}; + +export const ModuleListItem: React.FC = observer((props) => { + const { module } = props; + + const [editModuleModal, setEditModuleModal] = useState(false); + const [moduleDeleteModal, setModuleDeleteModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const { module: moduleStore } = useMobxStore(); + + const completionPercentage = ((module.completed_issues + module.cancelled_issues) / module.total_issues) * 100; + + const handleAddToFavorites = () => { + if (!workspaceSlug || !projectId) return; + + moduleStore.addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the module to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = () => { + if (!workspaceSlug || !projectId) return; + + moduleStore.removeModuleFromFavorites(workspaceSlug.toString(), projectId.toString(), module.id).catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the module from favorites. Please try again.", + }); + }); + }; + + const handleCopyText = () => { + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Module link copied to clipboard.", + }); + }); + }; + + const endDate = new Date(module.target_date ?? ""); + const startDate = new Date(module.start_date ?? ""); + + const renderDate = module.start_date || module.target_date; + + const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const moduleStatus = MODULE_STATUS.find((status) => status.value === module.status); + + const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage); + + const completedModuleCheck = module.status === "completed" && module.total_issues - module.completed_issues; + + const openModuleOverview = () => { + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekModule: module.id }, + }); + }; + + return ( + <> + {workspaceSlug && projectId && ( + setEditModuleModal(false)} + data={module} + projectId={projectId.toString()} + workspaceSlug={workspaceSlug.toString()} + /> + )} + setModuleDeleteModal(false)} /> + + +
+
+ + + {completedModuleCheck ? ( + {`!`} + ) : progress === 100 ? ( + + ) : ( + {`${progress}%`} + )} + + + + {module.name} + +
+ +
+ +
+
+ {moduleStatus && ( + + {moduleStatus.label} + + )} +
+ + {renderDate && ( + + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} + {" - "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + )} + + +
+ {module.members_detail.length > 0 ? ( + + ) : ( + + + + )} +
+
+ + {module.is_favorite ? ( + + ) : ( + + )} + + + { + e.preventDefault(); + e.stopPropagation(); + setEditModuleModal(true); + }} + > + + + Edit module + + + { + e.preventDefault(); + e.stopPropagation(); + setModuleDeleteModal(true); + }} + > + + + Delete module + + + { + e.preventDefault(); + e.stopPropagation(); + handleCopyText(); + }} + > + + + Copy module link + + + +
+
+ + + ); +}); diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx new file mode 100644 index 000000000..c7d2630cb --- /dev/null +++ b/web/components/modules/module-peek-overview.tsx @@ -0,0 +1,55 @@ +import React, { useEffect } from "react"; + +import { useRouter } from "next/router"; + +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { ModuleDetailsSidebar } from "./sidebar"; + +type Props = { + projectId: string; + workspaceSlug: string; +}; + +export const ModulePeekOverview: React.FC = observer(({ projectId, workspaceSlug }) => { + const router = useRouter(); + const { peekModule } = router.query; + + const ref = React.useRef(null); + + const { module: moduleStore } = useMobxStore(); + const { fetchModuleDetails } = moduleStore; + + const handleClose = () => { + delete router.query.peekModule; + router.push({ + pathname: router.pathname, + query: { ...router.query }, + }); + }; + + useEffect(() => { + if (!peekModule) return; + + fetchModuleDetails(workspaceSlug, projectId, peekModule.toString()); + }, [fetchModuleDetails, peekModule, projectId, workspaceSlug]); + + return ( + <> + {peekModule && ( +
+ +
+ )} + + ); +}); diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index d40b72a31..457f43524 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,3 +1,4 @@ +import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Plus } from "lucide-react"; // mobx store @@ -5,7 +6,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useLocalStorage from "hooks/use-local-storage"; // components -import { ModuleCardItem, ModulesListGanttChartView } from "components/modules"; +import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; import { EmptyState } from "components/common"; // ui import { Loader } from "@plane/ui"; @@ -13,6 +14,9 @@ import { Loader } from "@plane/ui"; import emptyModule from "public/empty-state/module.svg"; export const ModulesListView: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId, peekModule } = router.query; + const { module: moduleStore } = useMobxStore(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); @@ -22,12 +26,12 @@ export const ModulesListView: React.FC = observer(() => { if (!modulesList) return ( - - - - - - + + + + + + ); @@ -35,12 +39,39 @@ export const ModulesListView: React.FC = observer(() => { <> {modulesList.length > 0 ? ( <> + {modulesView === "list" && ( +
+
+
+ {modulesList.map((module) => ( + + ))} +
+ +
+
+ )} {modulesView === "grid" && ( -
-
- {modulesList.map((module) => ( - - ))} +
+
+
+ {modulesList.map((module) => ( + + ))} +
+
)} diff --git a/web/components/modules/sidebar-select/select-lead.tsx b/web/components/modules/sidebar-select/select-lead.tsx index 83ee93404..020aad037 100644 --- a/web/components/modules/sidebar-select/select-lead.tsx +++ b/web/components/modules/sidebar-select/select-lead.tsx @@ -7,7 +7,7 @@ import { ProjectService } from "services/project"; import { Avatar } from "components/ui"; import { CustomSearchSelect } from "@plane/ui"; // icons -import { UserCircle2 } from "lucide-react"; +import { ChevronDown, UserCircle2 } from "lucide-react"; // fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -36,7 +36,7 @@ export const SidebarLeadSelect: FC = (props) => { query: member.member.display_name, content: (
- + {member.member.display_name}
), @@ -46,18 +46,27 @@ export const SidebarLeadSelect: FC = (props) => { return (
-
- - Lead +
+ + Lead
-
+
- {selectedOption && } - {selectedOption ? selectedOption?.display_name : No lead} -
+ customButtonClassName="rounded-sm" + customButton={ + selectedOption ? ( +
+ + {selectedOption?.display_name} +
+ ) : ( +
+ No lead + +
+ ) } options={options} maxHeight="md" diff --git a/web/components/modules/sidebar-select/select-members.tsx b/web/components/modules/sidebar-select/select-members.tsx index c25959e3f..7b74ef794 100644 --- a/web/components/modules/sidebar-select/select-members.tsx +++ b/web/components/modules/sidebar-select/select-members.tsx @@ -10,6 +10,7 @@ import { ProjectService } from "services/project"; import { AssigneesList, Avatar } from "components/ui"; import { CustomSearchSelect, UserGroupIcon } from "@plane/ui"; // icons +import { ChevronDown } from "lucide-react"; // fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -37,7 +38,7 @@ export const SidebarMembersSelect: React.FC = ({ value, onChange }) => { query: member.member.display_name, content: (
- + {member.member.display_name}
), @@ -45,24 +46,26 @@ export const SidebarMembersSelect: React.FC = ({ value, onChange }) => { return (
-
- - Members +
+ + Members
-
+
- {value && value.length > 0 && Array.isArray(value) ? ( -
- - {value.length} Assignees -
- ) : ( - "No members" - )} -
+ customButtonClassName="rounded-sm" + customButton={ + value && value.length > 0 && Array.isArray(value) ? ( +
+ +
+ ) : ( +
+ No members + +
+ ) } options={options} onChange={onChange} diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index c6a2818ee..aa5462e08 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -3,8 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { mutate } from "swr"; import { Controller, useForm } from "react-hook-form"; -import { Disclosure, Popover, Transition } from "@headlessui/react"; -import DatePicker from "react-datepicker"; +import { Disclosure, Transition } from "@headlessui/react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services @@ -18,22 +17,12 @@ import { LinkModal, LinksList, SidebarProgressStats } from "components/core"; import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules"; import ProgressChart from "components/core/sidebar/progress-chart"; // ui -import { CustomSelect, CustomMenu, Loader, ProgressBar } from "@plane/ui"; +import { CustomMenu, Loader, LayersIcon } from "@plane/ui"; // icon -import { - AlertCircle, - CalendarDays, - ChevronDown, - File, - LinkIcon, - MoveRight, - PieChart, - Plus, - Trash2, -} from "lucide-react"; +import { AlertCircle, ChevronDown, ChevronRight, Info, LinkIcon, Plus, Trash2 } from "lucide-react"; // helpers -import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper"; -import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper"; +import { renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; // types import { linkDetails, IModule, ModuleLink } from "types"; // fetch-keys @@ -50,8 +39,8 @@ const defaultValues: Partial = { }; type Props = { - isOpen: boolean; moduleId: string; + handleClose: () => void; }; // services @@ -59,14 +48,14 @@ const moduleService = new ModuleService(); // TODO: refactor this component export const ModuleDetailsSidebar: React.FC = observer((props) => { - const { isOpen, moduleId } = props; + const { moduleId, handleClose } = props; const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, peekModule } = router.query; const { module: moduleStore, user: userStore } = useMobxStore(); @@ -77,7 +66,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const { setToastAlert } = useToast(); - const { reset, watch, control } = useForm({ + const { reset, control } = useForm({ defaultValues, }); @@ -209,12 +198,29 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { : null; const handleEditLink = (link: linkDetails) => { + console.log("link", link); setSelectedLinkToUpdate(link); setModuleLinkModal(true); }; if (!moduleDetails) return null; + const startDate = new Date(moduleDetails.start_date ?? ""); + const endDate = new Date(moduleDetails.target_date ?? ""); + + const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status); + + const issueCount = + moduleDetails.total_issues === 0 + ? "0 Issue" + : moduleDetails.total_issues === moduleDetails.completed_issues + ? moduleDetails.total_issues > 1 + ? `${moduleDetails.total_issues}` + : `${moduleDetails.total_issues}` + : `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`; + return ( <> = observer((props) => { updateIssueLink={handleUpdateLink} /> setModuleDeleteModal(false)} data={moduleDetails} /> -
- {module ? ( - <> -
-
-
- ( - - {capitalizeFirstLetter(`${watch("status")}`)} - - } - value={value} - onChange={(value: any) => { - submitChanges({ status: value }); - }} - > - {MODULE_STATUS.map((option) => ( - - {option.label} - - ))} - - )} - /> -
-
- - {({}) => ( - <> - - - - {renderShortDateWithYearFormat(new Date(`${moduleDetails.start_date}`), "Start date")} - - - - - - { - submitChanges({ - start_date: renderDateFormat(date), - }); - }} - selectsStart - startDate={new Date(`${watch("start_date")}`)} - endDate={new Date(`${watch("target_date")}`)} - maxDate={new Date(`${watch("target_date")}`)} - shouldCloseOnSelect - inline - /> - - - - )} - - - + {module ? ( + <> +
+
+ {peekModule && ( + + )} +
+
+ + + setModuleDeleteModal(true)}> + + + Delete - - {({}) => ( - <> - - + + +
+
- - {renderShortDateWithYearFormat(new Date(`${moduleDetails?.target_date}`), "End date")} - - +
+

{moduleDetails.name}

+
+ {moduleStatus && ( + + {moduleStatus.label} + + )} + + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + +
+
- - - { - submitChanges({ - target_date: renderDateFormat(date), - }); - }} - selectsEnd - startDate={new Date(`${watch("start_date")}`)} - endDate={new Date(`${watch("target_date")}`)} - minDate={new Date(`${watch("start_date")}`)} - shouldCloseOnSelect - inline - /> - - - - )} - -
+ {moduleDetails.description && ( + + {moduleDetails.description} + + )} + +
+ ( + { + submitChanges({ lead: val }); + }} + /> + )} + /> + ( + { + submitChanges({ members_list: val }); + }} + /> + )} + /> + +
+
+ + Issues
- -
-
-
-
-

- {moduleDetails.name} -

-
- - setModuleDeleteModal(true)}> - - - Delete - - - - - - Copy link - - - -
- - - {moduleDetails.description} - -
- -
- ( - { - submitChanges({ lead: val }); - }} - /> - )} - /> - ( - { - submitChanges({ members_list: val }); - }} - /> - )} - /> - -
-
- - Progress -
- -
- - - - {moduleDetails.completed_issues}/{moduleDetails.total_issues} -
-
-
+
+ {issueCount}
+
-
- +
+
+ {({ open }) => (
-
+
Progress - {!open && progressPercentage ? ( - +
+ +
+ {progressPercentage ? ( + {progressPercentage ? `${progressPercentage}%` : ""} ) : ( "" )} -
- - {isStartValid && isEndValid ? ( - - - ) : ( -
- - - Invalid date. Please enter valid date. - -
- )} -
- - {isStartValid && isEndValid ? ( -
-
-
- - - - - Pending Issues -{" "} - {moduleDetails.total_issues - - (moduleDetails.completed_issues + moduleDetails.cancelled_issues)}{" "} - -
- -
-
- - Ideal -
-
- - Current -
-
-
-
- -
-
+ + ) : ( - "" +
+ + + Invalid date. Please enter valid date. + +
)} -
-
-
- )} - -
- -
- - {({ open }) => ( -
-
-
- Other Information
- - {moduleDetails.total_issues > 0 ? ( - - - ) : ( -
- - - No issues found. Please add issue. - -
- )}
- {moduleDetails.total_issues > 0 ? ( - <> -
+
+ {isStartValid && isEndValid ? ( +
+
+
+
+ + Ideal +
+
+ + Current +
+
+
+
+ +
+
+ ) : ( + "" + )} + {moduleDetails.total_issues > 0 && ( +
= observer((props) => { }} totalIssues={moduleDetails.total_issues} module={moduleDetails} + isPeekModuleDetails={Boolean(peekModule)} />
- - ) : ( - "" - )} + )} +
@@ -555,42 +412,83 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
-
-
-

Links

- -
-
- {memberRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( - - ) : null} -
+
+ + {({ open }) => ( +
+
+
+ Links +
+ +
+ + +
+
+ + +
+ {memberRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? ( + <> +
+ +
+ + + + ) : ( +
+
+ + No links added yet +
+ +
+ )} +
+
+
+
+ )} +
- - ) : ( - -
- - -
-
- - - -
-
- )} -
+
+ + ) : ( + +
+ + +
+
+ + + +
+
+ )} ); }); diff --git a/web/components/ui/avatar.tsx b/web/components/ui/avatar.tsx index 0eb76fe93..44997f807 100644 --- a/web/components/ui/avatar.tsx +++ b/web/components/ui/avatar.tsx @@ -76,11 +76,20 @@ export const Avatar: React.FC = ({ type AsigneesListProps = { users?: Partial | (Partial | undefined)[] | Partial[]; userIds?: string[]; + height?: string; + width?: string; length?: number; showLength?: boolean; }; -export const AssigneesList: React.FC = ({ users, userIds, length = 3, showLength = true }) => { +export const AssigneesList: React.FC = ({ + users, + userIds, + height = "24px", + width = "24px", + length = 3, + showLength = true, +}) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -101,7 +110,7 @@ export const AssigneesList: React.FC = ({ users, userIds, len {users && ( <> {users.slice(0, length).map((user, index) => ( - + ))} {users.length > length ? (
@@ -118,7 +127,7 @@ export const AssigneesList: React.FC = ({ users, userIds, len {userIds.slice(0, length).map((userId, index) => { const user = people?.find((p) => p.member.id === userId)?.member; - return ; + return ; })} {showLength ? ( userIds.length > length ? ( diff --git a/web/constants/module.ts b/web/constants/module.ts index 058171328..a49df336c 100644 --- a/web/constants/module.ts +++ b/web/constants/module.ts @@ -5,11 +5,49 @@ export const MODULE_STATUS: { label: string; value: TModuleStatus; color: string; + textColor: string; + bgColor: string; }[] = [ - { label: "Backlog", value: "backlog", color: "#a3a3a2" }, - { label: "Planned", value: "planned", color: "#3f76ff" }, - { label: "In Progress", value: "in-progress", color: "#f39e1f" }, - { label: "Paused", value: "paused", color: "#525252" }, - { label: "Completed", value: "completed", color: "#16a34a" }, - { label: "Cancelled", value: "cancelled", color: "#ef4444" }, + { + label: "Backlog", + value: "backlog", + color: "#a3a3a2", + textColor: "text-custom-text-400", + bgColor: "bg-custom-background-80", + }, + { + label: "Planned", + value: "planned", + color: "#3f76ff", + textColor: "text-blue-500", + bgColor: "bg-indigo-50", + }, + { + label: "In Progress", + value: "in-progress", + color: "#f39e1f", + textColor: "text-amber-500", + bgColor: "bg-amber-50", + }, + { + label: "Paused", + value: "paused", + color: "#525252", + textColor: "text-custom-text-300", + bgColor: "bg-custom-background-90", + }, + { + label: "Completed", + value: "completed", + color: "#16a34a", + textColor: "text-green-600", + bgColor: "bg-green-100", + }, + { + label: "Cancelled", + value: "cancelled", + color: "#ef4444", + textColor: "text-red-500", + bgColor: "bg-red-50", + }, ]; diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index 08dff4a18..dced747f9 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -172,6 +172,18 @@ export const renderShortDate = (date: string | Date, placeholder?: string) => { return isNaN(date.getTime()) ? placeholder ?? "N/A" : `${day} ${month}`; }; +export const renderShortMonthDate = (date: string | Date, placeholder?: string) => { + if (!date || date === "") return null; + + date = new Date(date); + + const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const month = months[date.getMonth()]; + const year = date.getFullYear(); + + return isNaN(date.getTime()) ? placeholder ?? "N/A" : `${month} ${year}`; +}; + export const render12HourFormatTime = (date: string | Date): string => { if (!date || date === "") return ""; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index fd6d05f8e..27e6880d7 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -27,7 +27,7 @@ const ModuleIssuesPage: NextPage = () => { const { module: moduleStore } = useMobxStore(); - const { storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); + const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const { error } = useSWR( @@ -60,6 +60,10 @@ const ModuleIssuesPage: NextPage = () => { // setModuleIssuesListModal(true); // }; + const toggleSidebar = () => { + setValue(`${!isSidebarCollapsed}`); + }; + return ( <> } withProjectWrapper> @@ -82,10 +86,20 @@ const ModuleIssuesPage: NextPage = () => { /> ) : (
-
+
- {moduleId && } + {moduleId && !isSidebarCollapsed && ( +
+ +
+ )}
)} diff --git a/web/public/empty-state/empty_label.svg b/web/public/empty-state/empty_label.svg new file mode 100644 index 000000000..c664da6f4 --- /dev/null +++ b/web/public/empty-state/empty_label.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/public/empty-state/empty_members.svg b/web/public/empty-state/empty_members.svg new file mode 100644 index 000000000..6672c587b --- /dev/null +++ b/web/public/empty-state/empty_members.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + +