From 8eaac60aa5c2913e0cdddf046cb65cbea762868c Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:22:27 +0530 Subject: [PATCH 01/86] style: cycle ui revamp, and chore: code refactor (#2558) * chore: cycle custom svg icon added and code refactor * chore: module code refactor * style: cycle ui revamp and code refactor * chore: cycle card view layout fix * chore: layout fix * style: module and cycle title tooltip position --- .../src/icons/cycle/circle-dot-full-icon.tsx | 20 + .../src/icons/{ => cycle}/contrast-icon.tsx | 2 +- .../ui/src/icons/cycle/cycle-group-icon.tsx | 33 ++ .../icons/{ => cycle}/double-circle-icon.tsx | 2 +- packages/ui/src/icons/cycle/helper.tsx | 18 + packages/ui/src/icons/cycle/index.ts | 5 + packages/ui/src/icons/index.tsx | 3 +- .../core/sidebar/sidebar-progress-stats.tsx | 8 +- web/components/cycles/cycle-peek-overview.tsx | 55 ++ web/components/cycles/cycles-board-card.tsx | 483 ++++++--------- web/components/cycles/cycles-board.tsx | 33 +- web/components/cycles/cycles-list-item.tsx | 431 ++++++-------- web/components/cycles/cycles-list.tsx | 23 +- web/components/cycles/cycles-view.tsx | 11 +- web/components/cycles/index.ts | 2 + web/components/cycles/sidebar.tsx | 556 ++++++++---------- web/components/modules/module-card-item.tsx | 170 +++--- web/components/modules/module-list-item.tsx | 155 +++-- web/components/modules/sidebar.tsx | 2 +- web/constants/cycle.tsx | 37 ++ .../projects/[projectId]/cycles/[cycleId].tsx | 22 +- .../projects/[projectId]/cycles/index.tsx | 18 +- yarn.lock | 4 +- 23 files changed, 986 insertions(+), 1107 deletions(-) create mode 100644 packages/ui/src/icons/cycle/circle-dot-full-icon.tsx rename packages/ui/src/icons/{ => cycle}/contrast-icon.tsx (95%) create mode 100644 packages/ui/src/icons/cycle/cycle-group-icon.tsx rename packages/ui/src/icons/{ => cycle}/double-circle-icon.tsx (91%) create mode 100644 packages/ui/src/icons/cycle/helper.tsx create mode 100644 packages/ui/src/icons/cycle/index.ts create mode 100644 web/components/cycles/cycle-peek-overview.tsx diff --git a/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx b/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx new file mode 100644 index 000000000..0a2c46e99 --- /dev/null +++ b/packages/ui/src/icons/cycle/circle-dot-full-icon.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; + +import { ISvgIcons } from "../type"; + +export const CircleDotFullIcon: React.FC = ({ + className = "text-current", + ...rest +}) => ( + + + + +); diff --git a/packages/ui/src/icons/contrast-icon.tsx b/packages/ui/src/icons/cycle/contrast-icon.tsx similarity index 95% rename from packages/ui/src/icons/contrast-icon.tsx rename to packages/ui/src/icons/cycle/contrast-icon.tsx index 99316dbe0..7b51fd1e7 100644 --- a/packages/ui/src/icons/contrast-icon.tsx +++ b/packages/ui/src/icons/cycle/contrast-icon.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { ISvgIcons } from "../type"; export const ContrastIcon: React.FC = ({ className = "text-current", diff --git a/packages/ui/src/icons/cycle/cycle-group-icon.tsx b/packages/ui/src/icons/cycle/cycle-group-icon.tsx new file mode 100644 index 000000000..731d90702 --- /dev/null +++ b/packages/ui/src/icons/cycle/cycle-group-icon.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; + +import { ContrastIcon } from "./contrast-icon"; +import { CircleDotFullIcon } from "./circle-dot-full-icon"; +import { CircleDotDashed, Circle } from "lucide-react"; + +import { CYCLE_GROUP_COLORS, ICycleGroupIcon } from "./helper"; + +const iconComponents = { + current: ContrastIcon, + upcoming: CircleDotDashed, + completed: CircleDotFullIcon, + draft: Circle, +}; + +export const CycleGroupIcon: React.FC = ({ + className = "", + color, + cycleGroup, + height = "12px", + width = "12px", +}) => { + const CycleIconComponent = iconComponents[cycleGroup] || ContrastIcon; + + return ( + + ); +}; diff --git a/packages/ui/src/icons/double-circle-icon.tsx b/packages/ui/src/icons/cycle/double-circle-icon.tsx similarity index 91% rename from packages/ui/src/icons/double-circle-icon.tsx rename to packages/ui/src/icons/cycle/double-circle-icon.tsx index b5ced3f8a..a191b71a6 100644 --- a/packages/ui/src/icons/double-circle-icon.tsx +++ b/packages/ui/src/icons/cycle/double-circle-icon.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { ISvgIcons } from "./type"; +import { ISvgIcons } from "../type"; export const DoubleCircleIcon: React.FC = ({ className = "text-current", diff --git a/packages/ui/src/icons/cycle/helper.tsx b/packages/ui/src/icons/cycle/helper.tsx new file mode 100644 index 000000000..ec91cc2c2 --- /dev/null +++ b/packages/ui/src/icons/cycle/helper.tsx @@ -0,0 +1,18 @@ +export interface ICycleGroupIcon { + className?: string; + color?: string; + cycleGroup: TCycleGroups; + height?: string; + width?: string; +} + +export type TCycleGroups = "current" | "upcoming" | "completed" | "draft"; + +export const CYCLE_GROUP_COLORS: { + [key in TCycleGroups]: string; +} = { + current: "#F59E0B", + upcoming: "#3F76FF", + completed: "#16A34A", + draft: "#525252", +}; diff --git a/packages/ui/src/icons/cycle/index.ts b/packages/ui/src/icons/cycle/index.ts new file mode 100644 index 000000000..e74c8ff8c --- /dev/null +++ b/packages/ui/src/icons/cycle/index.ts @@ -0,0 +1,5 @@ +export * from "./double-circle-icon"; +export * from "./circle-dot-full-icon"; +export * from "./contrast-icon"; +export * from "./circle-dot-full-icon"; +export * from "./cycle-group-icon"; diff --git a/packages/ui/src/icons/index.tsx b/packages/ui/src/icons/index.tsx index 2e98e7138..518a4bfad 100644 --- a/packages/ui/src/icons/index.tsx +++ b/packages/ui/src/icons/index.tsx @@ -1,5 +1,4 @@ export * from "./user-group-icon"; -export * from "./contrast-icon"; export * from "./dice-icon"; export * from "./layers-icon"; export * from "./photo-filter-icon"; @@ -7,7 +6,6 @@ export * from "./archive-icon"; export * from "./admin-profile-icon"; export * from "./create-icon"; export * from "./subscribe-icon"; -export * from "./double-circle-icon"; export * from "./external-link-icon"; export * from "./copy-icon"; export * from "./layer-stack"; @@ -20,6 +18,7 @@ export * from "./blocked-icon"; export * from "./blocker-icon"; export * from "./related-icon"; export * from "./module"; +export * from "./cycle"; export * from "./github-icon"; export * from "./discord-icon"; export * from "./transfer-icon"; diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index bb6690172..706448492 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -36,7 +36,7 @@ type Props = { module?: IModule; roundedTab?: boolean; noBackground?: boolean; - isPeekModuleDetails?: boolean; + isPeekView?: boolean; }; export const SidebarProgressStats: React.FC = ({ @@ -46,7 +46,7 @@ export const SidebarProgressStats: React.FC = ({ module, roundedTab, noBackground, - isPeekModuleDetails = false, + isPeekView = false, }) => { const { filters, setFilters } = useIssuesView(); @@ -154,7 +154,7 @@ export const SidebarProgressStats: React.FC = ({ } completed={assignee.completed_issues} total={assignee.total_issues} - {...(!isPeekModuleDetails && { + {...(!isPeekView && { onClick: () => { if (filters?.assignees?.includes(assignee.assignee_id ?? "")) setFilters({ @@ -213,7 +213,7 @@ export const SidebarProgressStats: React.FC = ({ } completed={label.completed_issues} total={label.total_issues} - {...(!isPeekModuleDetails && { + {...(!isPeekView && { onClick: () => { if (filters.labels?.includes(label.label_id ?? "")) setFilters({ diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx new file mode 100644 index 000000000..fb30150ca --- /dev/null +++ b/web/components/cycles/cycle-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 { CycleDetailsSidebar } from "./sidebar"; + +type Props = { + projectId: string; + workspaceSlug: string; +}; + +export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug }) => { + const router = useRouter(); + const { peekCycle } = router.query; + + const ref = React.useRef(null); + + const { cycle: cycleStore } = useMobxStore(); + + const { fetchCycleWithId } = cycleStore; + + const handleClose = () => { + delete router.query.peekCycle; + router.push({ + pathname: router.pathname, + query: { ...router.query }, + }); + }; + + useEffect(() => { + if (!peekCycle) return; + fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString()); + }, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]); + + return ( + <> + {peekCycle && ( +
+ +
+ )} + + ); +}); diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index f2f921365..cbcfe2b55 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -1,64 +1,32 @@ import { FC, MouseEvent, useState } from "react"; + +import { useRouter } from "next/router"; + // next imports import Link from "next/link"; -// headless ui -import { Disclosure, Transition } from "@headlessui/react"; // hooks import useToast from "hooks/use-toast"; // components -import { SingleProgressStats } from "components/core"; import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; // ui import { AssigneesList } from "components/ui/avatar"; -import { CustomMenu, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui"; +import { CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui"; // icons -import { - AlarmClock, - AlertTriangle, - ArrowRight, - CalendarDays, - ChevronDown, - LinkIcon, - Pencil, - Star, - Target, - Trash2, -} from "lucide-react"; +import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers -import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { + getDateRangeStatus, + findHowManyDaysLeft, + renderShortDate, + renderShortMonthDate, +} from "helpers/date-time.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; // types import { ICycle } from "types"; // store import { useMobxStore } from "lib/mobx/store-provider"; - -const stateGroups = [ - { - key: "backlog_issues", - title: "Backlog", - color: "#dee2e6", - }, - { - key: "unstarted_issues", - title: "Unstarted", - color: "#26b5ce", - }, - { - key: "started_issues", - title: "Started", - color: "#f7ae59", - }, - { - key: "cancelled_issues", - title: "Cancelled", - color: "#d687ff", - }, - { - key: "completed_issues", - title: "Completed", - color: "#09a953", - }, -]; +// constants +import { CYCLE_STATUS } from "constants/cycle"; export interface ICyclesBoardCard { workspaceSlug: string; @@ -81,7 +49,34 @@ export const CyclesBoardCard: FC = (props) => { const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); - const handleCopyText = () => { + const router = useRouter(); + + const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + + const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const cycleTotalIssues = + cycle.backlog_issues + + cycle.unstarted_issues + + cycle.started_issues + + cycle.completed_issues + + cycle.cancelled_issues; + + const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100; + + const issueCount = cycle + ? cycleTotalIssues === 0 + ? "0 Issue" + : cycleTotalIssues === cycle.completed_issues + ? cycleTotalIssues > 1 + ? `${cycleTotalIssues} Issues` + : `${cycleTotalIssues} Issue` + : `${cycle.completed_issues}/${cycleTotalIssues} Issues` + : "0 Issue"; + + const handleCopyText = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { @@ -93,21 +88,6 @@ export const CyclesBoardCard: FC = (props) => { }); }; - const progressIndicatorData = stateGroups.map((group, index) => ({ - id: index, - name: group.title, - value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, - color: group.color, - })); - - const groupedIssues: any = { - backlog: cycle.backlog_issues, - unstarted: cycle.unstarted_issues, - started: cycle.started_issues, - completed: cycle.completed_issues, - cancelled: cycle.cancelled_issues, - }; - const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -134,6 +114,29 @@ export const CyclesBoardCard: FC = (props) => { }); }; + const handleEditCycle = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setUpdateModal(true); + }; + + const handleDeleteCycle = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDeleteModal(true); + }; + + const openCycleOverview = (e: MouseEvent) => { + const { query } = router; + e.preventDefault(); + e.stopPropagation(); + + router.push({ + pathname: router.pathname, + query: { ...query, peekCycle: cycle.id }, + }); + }; + return (
= (props) => { projectId={projectId} /> -
- - -
-
- - - - - -

{truncateText(cycle.name, 15)}

-
+ +
+
+
+
+ + - + + {cycle.name} + +
+
+ {currentCycle && ( - {cycleStatus === "current" ? ( - - - {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left - - ) : cycleStatus === "upcoming" ? ( - - - {findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left - - ) : cycleStatus === "completed" ? ( - - {cycle.total_issues - cycle.completed_issues > 0 && ( - - - - - - )}{" "} - Completed - - ) : ( - cycleStatus - )} + {currentCycle.value === "current" + ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + : `${currentCycle.label}`} - {cycle.is_favorite ? ( - - ) : ( - - )} - -
-
- {cycleStatus !== "draft" && ( - <> -
- - {renderShortDateWithYearFormat(startDate)} -
- -
- - {renderShortDateWithYearFormat(endDate)} -
- )} -
- -
-
-
-
Creator:
-
- {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - {cycle.owned_by.display_name} - ) : ( - - {cycle.owned_by.display_name.charAt(0)} - - )} - {cycle.owned_by.display_name} -
-
-
-
Members:
- {cycle.assignees.length > 0 ? ( -
- -
- ) : ( - "No members" - )} -
-
- -
- {!isCompleted && ( - - )} - - - {!isCompleted && ( - { - e.preventDefault(); - setDeleteModal(true); - }} - > - - - Delete cycle - - - )} - { - e.preventDefault(); - handleCopyText(); - }} - > - - - Copy cycle link - - - -
+
-
- +
-
- - {({ open }) => ( -
-
- Progress - - {Object.keys(groupedIssues).map((group, index) => ( - - - {group} -
- } - completed={groupedIssues[group]} - total={cycle.total_issues} - /> - ))} -
- } - position="bottom" - > -
- -
- - - - - -
- - -
-
-
- {stateGroups.map((group) => ( -
-
- -
{group.title}
-
-
- - {cycle[group.key as keyof ICycle] as number}{" "} - - -{" "} - {cycle.total_issues > 0 - ? `${Math.round( - ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 - )}%` - : "0%"} - - -
-
- ))} -
-
-
-
-
+
+
+
+ + {issueCount}
- )} - -
-
+ {cycle.assignees.length > 0 && ( + +
+ +
+
+ )} +
+ + +
+
+
+
+
+ + +
+ + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + +
+ {cycle.is_favorite ? ( + + ) : ( + + )} + + {!isCompleted && ( + <> + + + + Edit cycle + + + + + + Delete module + + + + )} + + + + Copy cycle link + + + +
+
+
+ +
); }; diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 3eca2e9b9..105d16128 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -2,26 +2,41 @@ import { FC } from "react"; // types import { ICycle } from "types"; // components -import { CyclesBoardCard } from "components/cycles"; +import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; export interface ICyclesBoard { cycles: ICycle[]; filter: string; workspaceSlug: string; projectId: string; + peekCycle: string; } export const CyclesBoard: FC = (props) => { - const { cycles, filter, workspaceSlug, projectId } = props; + const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; return ( -
+ <> {cycles.length > 0 ? ( - <> - {cycles.map((cycle) => ( - - ))} - +
+
+
+ {cycles.map((cycle) => ( + + ))} +
+ +
+
) : (
@@ -50,6 +65,6 @@ export const CyclesBoard: FC = (props) => {
)} -
+ ); }; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 9b381a03d..96efceddb 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -1,29 +1,30 @@ import { FC, MouseEvent, useState } from "react"; import Link from "next/link"; +import { useRouter } from "next/router"; + +// stores +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +import { AssigneesList } from "components/ui"; // ui -import { CustomMenu, RadialProgressBar, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui"; +import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon } from "@plane/ui"; // icons -import { - AlarmClock, - AlertTriangle, - ArrowRight, - CalendarDays, - LinkIcon, - Pencil, - Star, - Target, - Trash2, -} from "lucide-react"; +import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // helpers -import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; +import { + getDateRangeStatus, + findHowManyDaysLeft, + renderShortDate, + renderShortMonthDate, +} from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // types import { ICycle } from "types"; -import { useMobxStore } from "lib/mobx/store-provider"; +// constants +import { CYCLE_STATUS } from "constants/cycle"; type TCyclesListItem = { cycle: ICycle; @@ -35,34 +36,6 @@ type TCyclesListItem = { projectId: string; }; -const stateGroups = [ - { - key: "backlog_issues", - title: "Backlog", - color: "#dee2e6", - }, - { - key: "unstarted_issues", - title: "Unstarted", - color: "#26b5ce", - }, - { - key: "started_issues", - title: "Started", - color: "#f7ae59", - }, - { - key: "cancelled_issues", - title: "Cancelled", - color: "#d687ff", - }, - { - key: "completed_issues", - title: "Completed", - color: "#09a953", - }, -]; - export const CyclesListItem: FC = (props) => { const { cycle, workspaceSlug, projectId } = props; // store @@ -78,7 +51,28 @@ export const CyclesListItem: FC = (props) => { const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); - const handleCopyText = () => { + const router = useRouter(); + + const cycleTotalIssues = + cycle.backlog_issues + + cycle.unstarted_issues + + cycle.started_issues + + cycle.completed_issues + + cycle.cancelled_issues; + + const renderDate = cycle.start_date || cycle.end_date; + + const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100; + + const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage); + + const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + + const handleCopyText = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { @@ -90,13 +84,6 @@ export const CyclesListItem: FC = (props) => { }); }; - const progressIndicatorData = stateGroups.map((group, index) => ({ - id: index, - name: group.title, - value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, - color: group.color, - })); - const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -123,224 +110,31 @@ export const CyclesListItem: FC = (props) => { }); }; + const handleEditCycle = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setUpdateModal(true); + }; + + const handleDeleteCycle = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDeleteModal(true); + }; + + const openCycleOverview = (e: MouseEvent) => { + const { query } = router; + e.preventDefault(); + e.stopPropagation(); + + router.push({ + pathname: router.pathname, + query: { ...query, peekCycle: cycle.id }, + }); + }; + return ( <> -
-
- - - {/* left content */} -
- {/* cycle state */} -
- -
- - {/* cycle title and description */} -
- -
- {cycle.name} -
-
- {cycle.description && ( -
{cycle.description}
- )} -
-
- - {/* right content */} -
- {/* cycle status */} -
- {cycleStatus === "current" ? ( - - - {findHowManyDaysLeft(cycle.end_date ?? new Date())} days left - - ) : cycleStatus === "upcoming" ? ( - - - {findHowManyDaysLeft(cycle.start_date ?? new Date())} days left - - ) : cycleStatus === "completed" ? ( - - {cycle.total_issues - cycle.completed_issues > 0 && ( - - - - - - )}{" "} - Completed - - ) : ( - cycleStatus - )} -
- - {/* cycle start_date and target_date */} - {cycleStatus !== "draft" && ( -
-
- - {renderShortDateWithYearFormat(startDate)} -
- - - -
- - {renderShortDateWithYearFormat(endDate)} -
-
- )} - - {/* cycle created by */} -
- {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( - {cycle.owned_by.display_name} - ) : ( - - {cycle.owned_by.display_name.charAt(0)} - - )} -
- - {/* cycle progress */} - - Progress - -
- } - > - - {cycleStatus === "current" ? ( - - {cycle.total_issues > 0 ? ( - <> - - {Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} % - - ) : ( - No issues present - )} - - ) : cycleStatus === "upcoming" ? ( - - Yet to start - - ) : cycleStatus === "completed" ? ( - - - {100} % - - ) : ( - - - {cycleStatus} - - )} - - - - {/* cycle favorite */} - {cycle.is_favorite ? ( - - ) : ( - - )} -
- - -
- -
- - {!isCompleted && ( - setUpdateModal(true)}> - - - Edit Cycle - - - )} - - {!isCompleted && ( - setDeleteModal(true)}> - - - Delete cycle - - - )} - - - - - Copy cycle link - - - -
-
- = (props) => { workspaceSlug={workspaceSlug} projectId={projectId} /> - = (props) => { workspaceSlug={workspaceSlug} projectId={projectId} /> + + +
+
+ + + {isCompleted ? ( + {`!`} + ) : progress === 100 ? ( + + ) : ( + {`${progress}%`} + )} + + + +
+ + + + + {cycle.name} + +
+
+ +
+ +
+
+ {currentCycle && ( + + {currentCycle.value === "current" + ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + : `${currentCycle.label}`} + + )} +
+ + {renderDate && ( + + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} + {" - "} + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + )} + + +
+ {cycle.assignees.length > 0 ? ( + + ) : ( + + + + )} +
+
+ + {cycle.is_favorite ? ( + + ) : ( + + )} + + + {!isCompleted && ( + <> + + + + Edit cycle + + + + + + Delete module + + + + )} + + + + Copy cycle link + + + +
+
+ ); }; diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 947bd1fea..03698f1d8 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; // components -import { CyclesListItem } from "./cycles-list-item"; +import { CyclePeekOverview, CyclesListItem } from "components/cycles"; + // ui import { Loader } from "@plane/ui"; // types @@ -17,18 +18,22 @@ export const CyclesList: FC = (props) => { const { cycles, filter, workspaceSlug, projectId } = props; return ( -
+ <> {cycles ? ( <> {cycles.length > 0 ? ( -
- {cycles.map((cycle) => ( -
-
+
+
+
+ {cycles.map((cycle) => ( -
+ ))}
- ))} + +
) : (
@@ -68,6 +73,6 @@ export const CyclesList: FC = (props) => { )} -
+ ); }; diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index 36955398e..f0640dec6 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -15,10 +15,11 @@ export interface ICyclesView { layout: TCycleLayout; workspaceSlug: string; projectId: string; + peekCycle: string; } export const CyclesView: FC = observer((props) => { - const { filter, layout, workspaceSlug, projectId } = props; + const { filter, layout, workspaceSlug, projectId, peekCycle } = props; // store const { cycle: cycleStore } = useMobxStore(); @@ -50,7 +51,13 @@ export const CyclesView: FC = observer((props) => { {layout === "board" && ( <> {!isLoading ? ( - + ) : ( diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index 20bbfb627..6bf801bbe 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -17,3 +17,5 @@ export * from "./cycles-board"; export * from "./cycles-board-card"; export * from "./cycles-gantt"; export * from "./delete-modal"; +export * from "./cycle-peek-overview"; +export * from "./cycles-list-item"; diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index d55a261eb..f56ddcf18 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -15,36 +15,29 @@ import { SidebarProgressStats } from "components/core"; import ProgressChart from "components/core/sidebar/progress-chart"; import { CycleDeleteModal } from "components/cycles/delete-modal"; // ui -import { CustomRangeDatePicker } from "components/ui"; -import { CustomMenu, Loader, ProgressBar } from "@plane/ui"; +import { Avatar, CustomRangeDatePicker } from "components/ui"; +import { CustomMenu, Loader, LayersIcon } from "@plane/ui"; // icons -import { - CalendarDays, - ChevronDown, - File, - MoveRight, - LinkIcon, - PieChart, - Trash2, - UserCircle2, - AlertCircle, -} from "lucide-react"; +import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, MoveRight } from "lucide-react"; // helpers -import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; import { + findHowManyDaysLeft, getDateRangeStatus, isDateGreaterThanToday, renderDateFormat, - renderShortDateWithYearFormat, + renderShortDate, + renderShortMonthDate, } from "helpers/date-time.helper"; // types import { ICycle } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; +import { CYCLE_STATUS } from "constants/cycle"; type Props = { - isOpen: boolean; cycleId: string; + handleClose: () => void; }; // services @@ -52,12 +45,12 @@ const cycleService = new CycleService(); // TODO: refactor the whole component export const CycleDetailsSidebar: React.FC = observer((props) => { - const { isOpen, cycleId } = props; + const { cycleId, handleClose } = props; const [cycleDeleteModal, setCycleDeleteModal] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, peekCycle } = router.query; const { user: userStore, cycle: cycleDetailsStore } = useMobxStore(); @@ -280,6 +273,22 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { if (!cycleDetails) return null; + const endDate = new Date(cycleDetails.end_date ?? ""); + const startDate = new Date(cycleDetails.start_date ?? ""); + + const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + + const issueCount = + cycleDetails.total_issues === 0 + ? "0 Issue" + : cycleDetails.total_issues === cycleDetails.completed_issues + ? cycleDetails.total_issues > 1 + ? `${cycleDetails.total_issues}` + : `${cycleDetails.total_issues}` + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + return ( <> {cycleDetails && workspaceSlug && projectId && ( @@ -291,327 +300,266 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { projectId={projectId.toString()} /> )} -
- {cycleDetails ? ( - <> -
-
-
- - {capitalizeFirstLetter(cycleStatus)} - -
-
- - {({}) => ( - <> - - - - {renderShortDateWithYearFormat( - new Date(`${watch("start_date") ? watch("start_date") : cycleDetails?.start_date}`), - "Start date" - )} - - - - - { - if (val) { - handleStartDateChange(val); - } - }} - startDate={watch("start_date") ? `${watch("start_date")}` : null} - endDate={watch("end_date") ? `${watch("end_date")}` : null} - maxDate={new Date(`${watch("end_date")}`)} - selectsStart - /> - - - - )} - - - - - - {({}) => ( - <> - - + {cycleDetails ? ( + <> +
+
+ {peekCycle && ( + + )} +
+
+ + {!isCompleted && ( + + setCycleDeleteModal(true)}> + + + Delete + + + + )} +
+
- - {renderShortDateWithYearFormat( - new Date(`${watch("end_date") ? watch("end_date") : cycleDetails?.end_date}`), - "End date" - )} - -
+
+

{cycleDetails.name}

+
+ {currentCycle && ( + + {currentCycle.value === "current" + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` + : `${currentCycle.label}`} + + )} +
+ + {({}) => ( + <> + + {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} + - - - { - if (val) { - handleEndDateChange(val); - } - }} - startDate={watch("start_date") ? `${watch("start_date")}` : null} - endDate={watch("end_date") ? `${watch("end_date")}` : null} - minDate={new Date(`${watch("start_date")}`)} - selectsEnd - /> - - - - )} - -
+ + + { + if (val) { + handleStartDateChange(val); + } + }} + startDate={watch("start_date") ? `${watch("start_date")}` : null} + endDate={watch("end_date") ? `${watch("end_date")}` : null} + maxDate={new Date(`${watch("end_date")}`)} + selectsStart + /> + + + + )} + + + + {({}) => ( + <> + + {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + + + + { + if (val) { + handleEndDateChange(val); + } + }} + startDate={watch("start_date") ? `${watch("start_date")}` : null} + endDate={watch("end_date") ? `${watch("end_date")}` : null} + minDate={new Date(`${watch("start_date")}`)} + selectsEnd + /> + + + + )} +
+
+
-
-
-
-
-

- {cycleDetails.name} -

-
- - {!isCompleted && ( - setCycleDeleteModal(true)}> - - - Delete - - - )} - - - - Copy link - - - -
+ {cycleDetails.description && ( + + {cycleDetails.description} + + )} - - {cycleDetails.description} - -
- -
-
-
- - Lead -
- -
- {cycleDetails.owned_by.avatar && cycleDetails.owned_by.avatar !== "" ? ( - {cycleDetails.owned_by.display_name} - ) : ( - - {cycleDetails.owned_by.display_name.charAt(0)} - - )} - {cycleDetails.owned_by.display_name} -
-
- -
-
- - Progress -
- -
- - - - {cycleDetails.completed_issues}/{cycleDetails.total_issues} -
-
+
+
+
+ + Lead +
+
+
+ + {cycleDetails.owned_by.display_name}
-
- + +
+
+ + Issues +
+
+ {issueCount} +
+
+
+ +
+
+ {({ open }) => (
-
+
Progress - {!open && progressPercentage ? ( - +
+ +
+ {progressPercentage ? ( + {progressPercentage ? `${progressPercentage}%` : ""} ) : ( "" )} + {isStartValid && isEndValid ? ( + + + ) : ( +
+ + + Invalid date. Please enter valid date. + +
+ )}
- {isStartValid && isEndValid ? ( - - - ) : ( -
- - - {cycleStatus === "upcoming" - ? "Cycle is yet to start." - : "Invalid date. Please enter valid date."} - -
- )}
- {isStartValid && isEndValid ? ( -
-
-
- - - - - Pending Issues -{" "} - {cycleDetails.total_issues - - (cycleDetails.completed_issues + cycleDetails.cancelled_issues)} - +
+ {isStartValid && isEndValid ? ( +
+
+
+
+ + Ideal +
+
+ + Current +
+
- -
-
- - Ideal -
-
- - Current -
+
+
-
- 0 && ( +
+
-
- ) : ( - "" - )} - - -
- )} - -
-
- - {({ open }) => ( -
-
-
- Other Information -
- - {cycleDetails.total_issues > 0 ? ( - - - ) : ( -
- - - No issues found. Please add issue. - + )}
- )} -
- - - {cycleDetails.total_issues > 0 ? ( -
- -
- ) : ( - "" - )}
)}
- - ) : ( - -
- - -
-
- - - -
-
- )} -
+
+ + ) : ( + +
+ + +
+
+ + + +
+
+ )} ); }); diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 620333f8e..b3625df6c 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -28,8 +28,8 @@ type Props = { export const ModuleCardItem: React.FC = observer((props) => { const { module } = props; - const [editModuleModal, setEditModuleModal] = useState(false); - const [moduleDeleteModal, setModuleDeleteModal] = useState(false); + const [editModal, setEditModal] = useState(false); + const [deleteModal, setDeleteModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -38,50 +38,7 @@ export const ModuleCardItem: React.FC = observer((props) => { 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 openModuleOverview = () => { - const { query } = router; - - router.push({ - pathname: router.pathname, - query: { ...query, peekModule: module.id }, - }); - }; + const completionPercentage = (module.completed_issues / module.total_issues) * 100; const endDate = new Date(module.target_date ?? ""); const startDate = new Date(module.start_date ?? ""); @@ -101,23 +58,86 @@ export const ModuleCardItem: React.FC = observer((props) => { : `${module.completed_issues}/${module.total_issues} Issues` : "0 Issue"; + const handleAddToFavorites = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + 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 = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + 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 = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Module link copied to clipboard.", + }); + }); + }; + + const handleEditModule = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setEditModal(true); + }; + + const handleDeleteModule = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDeleteModal(true); + }; + + const openModuleOverview = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekModule: module.id }, + }); + }; + return ( <> {workspaceSlug && projectId && ( setEditModuleModal(false)} + isOpen={editModal} + onClose={() => setEditModal(false)} data={module} projectId={projectId.toString()} workspaceSlug={workspaceSlug.toString()} /> )} - setModuleDeleteModal(false)} /> + setDeleteModal(false)} />
- + {module.name}
@@ -128,13 +148,7 @@ export const ModuleCardItem: React.FC = observer((props) => { {moduleStatus.label} )} -
@@ -184,60 +198,28 @@ export const ModuleCardItem: React.FC = observer((props) => {
{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-list-item.tsx b/web/components/modules/module-list-item.tsx index 8b1271cc8..745d15c9d 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -28,8 +28,8 @@ type Props = { export const ModuleListItem: React.FC = observer((props) => { const { module } = props; - const [editModuleModal, setEditModuleModal] = useState(false); - const [moduleDeleteModal, setModuleDeleteModal] = useState(false); + const [editModal, setEditModal] = useState(false); + const [deleteModal, setDeleteModal] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -40,40 +40,6 @@ export const ModuleListItem: React.FC = observer((props) => { 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 ?? ""); @@ -87,7 +53,61 @@ export const ModuleListItem: React.FC = observer((props) => { const completedModuleCheck = module.status === "completed" && module.total_issues - module.completed_issues; - const openModuleOverview = () => { + const handleAddToFavorites = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + 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 = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + 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 = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module.id}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Module link copied to clipboard.", + }); + }); + }; + + const handleEditModule = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setEditModal(true); + }; + + const handleDeleteModule = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDeleteModal(true); + }; + + const openModuleOverview = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); const { query } = router; router.push({ @@ -100,14 +120,14 @@ export const ModuleListItem: React.FC = observer((props) => { <> {workspaceSlug && projectId && ( setEditModuleModal(false)} + isOpen={editModal} + onClose={() => setEditModal(false)} data={module} projectId={projectId.toString()} workspaceSlug={workspaceSlug.toString()} /> )} - setModuleDeleteModal(false)} /> + setDeleteModal(false)} />
@@ -123,18 +143,11 @@ export const ModuleListItem: React.FC = observer((props) => { )} - + {module.name}
-
@@ -171,63 +184,29 @@ export const ModuleListItem: React.FC = observer((props) => { {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/sidebar.tsx b/web/components/modules/sidebar.tsx index aa5462e08..f931f3fc1 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -400,7 +400,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { }} totalIssues={moduleDetails.total_issues} module={moduleDetails} - isPeekModuleDetails={Boolean(peekModule)} + isPeekView={Boolean(peekModule)} />
)} diff --git a/web/constants/cycle.tsx b/web/constants/cycle.tsx index a2433d70c..78d1e0a54 100644 --- a/web/constants/cycle.tsx +++ b/web/constants/cycle.tsx @@ -37,3 +37,40 @@ export const CYCLE_VIEWS = [ icon: , }, ]; + +export const CYCLE_STATUS: { + label: string; + value: "current" | "upcoming" | "completed" | "draft"; + color: string; + textColor: string; + bgColor: string; +}[] = [ + { + label: "day left", + value: "current", + color: "#F59E0B", + textColor: "text-amber-500", + bgColor: "bg-amber-50", + }, + { + label: "Yet to start", + value: "upcoming", + color: "#3F76FF", + textColor: "text-blue-500", + bgColor: "bg-indigo-50", + }, + { + label: "Completed", + value: "completed", + color: "#16A34A", + textColor: "text-green-600", + bgColor: "bg-green-50", + }, + { + label: "Draft", + value: "draft", + color: "#525252", + textColor: "text-custom-text-300", + bgColor: "bg-custom-background-90", + }, +]; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 6f8384297..e96038b7f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -25,7 +25,7 @@ const SingleCycle: React.FC = () => { const { cycle: cycleStore } = useMobxStore(); - const { storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); + const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false; const { error } = useSWR( @@ -35,6 +35,10 @@ const SingleCycle: React.FC = () => { : null ); + const toggleSidebar = () => { + setValue(`${!isSidebarCollapsed}`); + }; + // TODO: add this function to bulk add issues to cycle // const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { // if (!workspaceSlug || !projectId) return; @@ -75,11 +79,21 @@ const SingleCycle: React.FC = () => { /> ) : ( <> -
-
+
+
- {cycleId && } + {cycleId && !isSidebarCollapsed && ( +
+ +
+ )}
)} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 5fb79fb36..5f9988e52 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -29,7 +29,11 @@ const ProjectCyclesPage: NextPage = observer(() => { const { project: projectStore, cycle: cycleStore } = useMobxStore(); // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + const { workspaceSlug, projectId, peekCycle } = router.query as { + workspaceSlug: string; + projectId: string; + peekCycle: string; + }; // fetching project details useSWR( workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null, @@ -150,13 +154,14 @@ const ProjectCyclesPage: NextPage = observer(() => {
- + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} @@ -165,35 +170,38 @@ const ProjectCyclesPage: NextPage = observer(() => { - + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} - + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} - + {cycleView && cycleLayout && workspaceSlug && projectId && ( )} diff --git a/yarn.lock b/yarn.lock index e95d031bf..7f729677e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2755,7 +2755,7 @@ dependencies: "@types/react" "*" -"@types/react-color@^3.0.6": +"@types/react-color@^3.0.6", "@types/react-color@^3.0.9": version "3.0.9" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.9.tgz#8dbb0d798f2979c3d7e2e26dd46321e80da950b4" integrity sha512-Ojyc6jySSKvM6UYQrZxaYe0JZXtgHHXwR2q9H4MhcNCswFdeZH1owYZCvPtdHtMOfh7t8h1fY0Gd0nvU1JGDkQ== @@ -7393,7 +7393,7 @@ react-markdown@^8.0.7: unist-util-visit "^4.0.0" vfile "^5.0.0" -react-moveable@^0.54.1: +react-moveable@^0.54.1, react-moveable@^0.54.2: version "0.54.2" resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.2.tgz#87ce9af3499dc1c8218bce7e174b10264c1bbecf" integrity sha512-NGaVLbn0i9pb3+BWSKGWFqI/Mgm4+WMeWHxXXQ4Qi1tHxWCXrUrbGvpxEpt69G/hR7dez+/m68ex+fabjnvcUg== From 050406b8a402576c24715137c85cac3dbb995209 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 30 Oct 2023 20:09:04 +0530 Subject: [PATCH 02/86] chore: add empty state for list and spreadsheet layouts (#2531) * chore: add empty state for list and spreadsheet layouts * fix: build errors --- .../issue-layouts/empty-states/cycle.tsx | 25 +++++++++ .../empty-states/global-view.tsx | 25 +++++++++ .../issue-layouts/empty-states/index.ts | 5 ++ .../issue-layouts/empty-states/module.tsx | 25 +++++++++ .../empty-states/project-view.tsx | 25 +++++++++ .../issue-layouts/empty-states/project.tsx | 25 +++++++++ web/components/issues/issue-layouts/index.ts | 1 + .../issues/issue-layouts/list/index.ts | 1 + .../issue-layouts/list/roots/module-root.tsx | 2 +- .../issue-layouts/roots/cycle-layout-root.tsx | 33 ++++++----- .../roots/global-view-layout-root.tsx | 32 ++++++----- .../roots/module-layout-root.tsx | 33 ++++++----- .../roots/project-layout-root.tsx | 33 ++++++----- .../roots/project-view-layout-root.tsx | 33 ++++++----- web/store/archived-issues/issue.store.ts | 15 ++--- web/store/cycle/cycle_issue.store.ts | 55 ++++++++++++++++--- .../cycle/cycle_issue_calendar_view.store.ts | 2 +- web/store/draft-issues/issue.store.ts | 15 ++--- web/store/issue/issue.store.ts | 40 ++++++++++++++ web/store/module/module_issue.store.ts | 55 ++++++++++++++++--- .../module_issue_calendar_view.store.ts | 2 +- .../project-view/project_view_issues.store.ts | 40 ++++++++++++++ 22 files changed, 417 insertions(+), 105 deletions(-) create mode 100644 web/components/issues/issue-layouts/empty-states/cycle.tsx create mode 100644 web/components/issues/issue-layouts/empty-states/global-view.tsx create mode 100644 web/components/issues/issue-layouts/empty-states/index.ts create mode 100644 web/components/issues/issue-layouts/empty-states/module.tsx create mode 100644 web/components/issues/issue-layouts/empty-states/project-view.tsx create mode 100644 web/components/issues/issue-layouts/empty-states/project.tsx diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx new file mode 100644 index 000000000..e7fb0833f --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from "lucide-react"; +// components +import { EmptyState } from "components/common"; +// assets +import emptyIssue from "public/empty-state/issue.svg"; + +export const CycleEmptyState: React.FC = () => ( +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + /> +
+); diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx new file mode 100644 index 000000000..dc7829127 --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from "lucide-react"; +// components +import { EmptyState } from "components/common"; +// assets +import emptyIssue from "public/empty-state/issue.svg"; + +export const GlobalViewEmptyState: React.FC = () => ( +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + /> +
+); diff --git a/web/components/issues/issue-layouts/empty-states/index.ts b/web/components/issues/issue-layouts/empty-states/index.ts new file mode 100644 index 000000000..0373709d2 --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/index.ts @@ -0,0 +1,5 @@ +export * from "./cycle"; +export * from "./global-view"; +export * from "./module"; +export * from "./project-view"; +export * from "./project"; diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx new file mode 100644 index 000000000..830fde1ff --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from "lucide-react"; +// components +import { EmptyState } from "components/common"; +// assets +import emptyIssue from "public/empty-state/issue.svg"; + +export const ModuleEmptyState: React.FC = () => ( +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + /> +
+); diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx new file mode 100644 index 000000000..2b046a14f --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from "lucide-react"; +// components +import { EmptyState } from "components/common"; +// assets +import emptyIssue from "public/empty-state/issue.svg"; + +export const ProjectViewEmptyState: React.FC = () => ( +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + /> +
+); diff --git a/web/components/issues/issue-layouts/empty-states/project.tsx b/web/components/issues/issue-layouts/empty-states/project.tsx new file mode 100644 index 000000000..03c4522c0 --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/project.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from "lucide-react"; +// components +import { EmptyState } from "components/common"; +// assets +import emptyIssue from "public/empty-state/issue.svg"; + +export const ProjectEmptyState: React.FC = () => ( +
+ , + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }, + }} + /> +
+); diff --git a/web/components/issues/issue-layouts/index.ts b/web/components/issues/issue-layouts/index.ts index 5e6b51931..066febb94 100644 --- a/web/components/issues/issue-layouts/index.ts +++ b/web/components/issues/issue-layouts/index.ts @@ -1,5 +1,6 @@ // filters export * from "./filters"; +export * from "./empty-states"; export * from "./quick-action-dropdowns"; // layouts diff --git a/web/components/issues/issue-layouts/list/index.ts b/web/components/issues/issue-layouts/list/index.ts index e557fe022..be3968fdd 100644 --- a/web/components/issues/issue-layouts/list/index.ts +++ b/web/components/issues/issue-layouts/list/index.ts @@ -1,4 +1,5 @@ export * from "./roots"; export * from "./block"; +export * from "./roots"; export * from "./blocks-list"; export * from "./inline-create-issue-form"; diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index daa12e64a..b32d303e1 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -68,7 +68,7 @@ export const ModuleListLayout: React.FC = observer(() => { : null; return ( -
+
{ ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) : "draft"; + const issueCount = cycleIssueStore.getIssuesCount; + return ( <> setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
{cycleStatus === "completed" && setTransferIssuesModal(true)} />} -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( + + ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); diff --git a/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx index c88c821f4..e7adcf0e9 100644 --- a/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx @@ -5,7 +5,7 @@ import useSWR from "swr"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { GlobalViewsAppliedFiltersRoot, SpreadsheetView } from "components/issues"; +import { GlobalViewEmptyState, GlobalViewsAppliedFiltersRoot, SpreadsheetView } from "components/issues"; // types import { IIssue, IIssueDisplayFilterOptions, TStaticViewTypes } from "types"; @@ -81,19 +81,23 @@ export const GlobalViewLayoutRoot: React.FC = observer((props) => { return (
-
- m.member) : undefined} - labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined} - handleIssueAction={() => {}} - handleUpdateIssue={handleUpdateIssue} - disableUserActions={false} - /> -
+ {issues?.length === 0 ? ( + + ) : ( +
+ m.member) : undefined} + labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined} + handleIssueAction={() => {}} + handleUpdateIssue={handleUpdateIssue} + disableUserActions={false} + /> +
+ )}
); }); diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index f01fd1fc2..ff7867c3d 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -9,6 +9,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { ModuleAppliedFiltersRoot, ModuleCalendarLayout, + ModuleEmptyState, ModuleGanttLayout, ModuleKanBanLayout, ModuleListLayout, @@ -46,22 +47,28 @@ export const ModuleLayoutRoot: React.FC = observer(() => { const activeLayout = issueFilterStore.userDisplayFilters.layout; + const issueCount = moduleIssueStore.getIssuesCount; + return (
-
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( + + ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); }); diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index f76d2a3e3..0cd5911a5 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -12,6 +12,7 @@ import { KanBanLayout, ProjectAppliedFiltersRoot, ProjectSpreadsheetLayout, + ProjectEmptyState, } from "components/issues"; export const ProjectLayoutRoot: React.FC = observer(() => { @@ -30,22 +31,28 @@ export const ProjectLayoutRoot: React.FC = observer(() => { const activeLayout = issueFilterStore.userDisplayFilters.layout; + const issueCount = issueStore.getIssuesCount; + return (
-
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( + + ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); }); diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index 7d5dad9e2..dfb69e63a 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -11,6 +11,7 @@ import { ModuleListLayout, ProjectViewAppliedFiltersRoot, ProjectViewCalendarLayout, + ProjectViewEmptyState, ProjectViewGanttLayout, ProjectViewSpreadsheetLayout, } from "components/issues"; @@ -48,22 +49,28 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => { const activeLayout = issueFilterStore.userDisplayFilters.layout; + const issueCount = projectViewIssuesStore.getIssuesCount; + return (
-
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} -
+ {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( + + ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); }); diff --git a/web/store/archived-issues/issue.store.ts b/web/store/archived-issues/issue.store.ts index 141805e23..11c29f25c 100644 --- a/web/store/archived-issues/issue.store.ts +++ b/web/store/archived-issues/issue.store.ts @@ -6,15 +6,12 @@ import { IIssue } from "types"; // services import { IssueService } from "services/issue"; import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; - -export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; -export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; -export type IIssueGroupWithSubGroupsStructure = { - [group_id: string]: { - [sub_group_id: string]: IIssue[]; - }; -}; -export type IIssueUnGroupedStructure = IIssue[]; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; export interface IArchivedIssueStore { loader: boolean; diff --git a/web/store/cycle/cycle_issue.store.ts b/web/store/cycle/cycle_issue.store.ts index e343a733d..333218060 100644 --- a/web/store/cycle/cycle_issue.store.ts +++ b/web/store/cycle/cycle_issue.store.ts @@ -9,15 +9,12 @@ import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; // types import { IIssue } from "types"; import { IBlockUpdateData } from "components/gantt-chart"; - -export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; -export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; -export type IIssueGroupWithSubGroupsStructure = { - [group_id: string]: { - [sub_group_id: string]: IIssue[]; - }; -}; -export type IIssueUnGroupedStructure = IIssue[]; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; export interface ICycleIssueStore { loader: boolean; @@ -33,6 +30,7 @@ export interface ICycleIssueStore { // computed getIssueType: IIssueType | null; getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; // action fetchIssues: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; @@ -73,6 +71,7 @@ export class CycleIssueStore implements ICycleIssueStore { // computed getIssueType: computed, getIssues: computed, + getIssuesCount: computed, // actions fetchIssues: action, updateIssueStructure: action, @@ -130,6 +129,44 @@ export class CycleIssueStore implements ICycleIssueStore { return this.issues?.[cycleId]?.[issueType] || null; } + get getIssuesCount() { + const issueType = this.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { const cycleId: string | null = this.rootStore?.cycle?.cycleId || null; const issueType = this.getIssueType; diff --git a/web/store/cycle/cycle_issue_calendar_view.store.ts b/web/store/cycle/cycle_issue_calendar_view.store.ts index 0ebb38c37..fa80f39ac 100644 --- a/web/store/cycle/cycle_issue_calendar_view.store.ts +++ b/web/store/cycle/cycle_issue_calendar_view.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, runInAction } from "mobx"; // types import { RootStore } from "../root"; -import { IIssueType } from "./cycle_issue.store"; +import { IIssueType } from "store/issue"; export interface ICycleIssueCalendarViewStore { // actions diff --git a/web/store/draft-issues/issue.store.ts b/web/store/draft-issues/issue.store.ts index c8afece07..f31a0bbb4 100644 --- a/web/store/draft-issues/issue.store.ts +++ b/web/store/draft-issues/issue.store.ts @@ -6,15 +6,12 @@ import { IIssue } from "types"; // services import { IssueService } from "services/issue"; import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; - -export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; -export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; -export type IIssueGroupWithSubGroupsStructure = { - [group_id: string]: { - [sub_group_id: string]: IIssue[]; - }; -}; -export type IIssueUnGroupedStructure = IIssue[]; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; export interface IDraftIssueStore { loader: boolean; diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 85d726f78..924c91eb1 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -31,6 +31,7 @@ export interface IIssueStore { // computed getIssueType: IIssueType | null; getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; // action fetchIssues: (workspaceSlug: string, projectId: string) => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; @@ -68,6 +69,7 @@ export class IssueStore implements IIssueStore { // computed getIssueType: computed, getIssues: computed, + getIssuesCount: computed, // actions fetchIssues: action, updateIssueStructure: action, @@ -120,6 +122,44 @@ export class IssueStore implements IIssueStore { return this.issues?.[projectId]?.[issueType] || null; } + get getIssuesCount() { + const issueType = this.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { const projectId: string | null = issue?.project; const issueType = this.getIssueType; diff --git a/web/store/module/module_issue.store.ts b/web/store/module/module_issue.store.ts index 9751ef708..5b5893b0d 100644 --- a/web/store/module/module_issue.store.ts +++ b/web/store/module/module_issue.store.ts @@ -8,15 +8,12 @@ import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; // types import { IIssue } from "types"; import { IBlockUpdateData } from "components/gantt-chart"; - -export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; -export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; -export type IIssueGroupWithSubGroupsStructure = { - [group_id: string]: { - [sub_group_id: string]: IIssue[]; - }; -}; -export type IIssueUnGroupedStructure = IIssue[]; +import { + IIssueGroupWithSubGroupsStructure, + IIssueGroupedStructure, + IIssueType, + IIssueUnGroupedStructure, +} from "store/issue"; export interface IModuleIssueStore { loader: boolean; @@ -32,6 +29,7 @@ export interface IModuleIssueStore { // computed getIssueType: IIssueType | null; getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; // action fetchIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; @@ -76,6 +74,7 @@ export class ModuleIssueStore implements IModuleIssueStore { // computed getIssueType: computed, getIssues: computed, + getIssuesCount: computed, // actions fetchIssues: action, updateIssueStructure: action, @@ -132,6 +131,44 @@ export class ModuleIssueStore implements IModuleIssueStore { return this.issues?.[moduleId]?.[issueType] || null; } + get getIssuesCount() { + const issueType = this.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { const moduleId: string | null = this.rootStore?.module?.moduleId; const issueType = this.getIssueType; diff --git a/web/store/module/module_issue_calendar_view.store.ts b/web/store/module/module_issue_calendar_view.store.ts index 95a866040..313745a18 100644 --- a/web/store/module/module_issue_calendar_view.store.ts +++ b/web/store/module/module_issue_calendar_view.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, runInAction } from "mobx"; // types import { RootStore } from "../root"; -import { IIssueType } from "./module_issue.store"; +import { IIssueType } from "store/issue"; export interface IModuleIssueCalendarViewStore { // actions diff --git a/web/store/project-view/project_view_issues.store.ts b/web/store/project-view/project_view_issues.store.ts index 1c6374942..1e699c270 100644 --- a/web/store/project-view/project_view_issues.store.ts +++ b/web/store/project-view/project_view_issues.store.ts @@ -45,6 +45,7 @@ export interface IProjectViewIssuesStore { // computed getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + getIssuesCount: number; getIssueType: IIssueType | null; } @@ -86,6 +87,7 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore { // computed getIssueType: computed, getIssues: computed, + getIssuesCount: computed, }); this.rootStore = _rootStore; @@ -147,6 +149,44 @@ export class ProjectViewIssuesStore implements IProjectViewIssuesStore { return this.viewIssues?.[viewId]?.[issueType] || null; } + get getIssuesCount() { + const issueType = this.rootStore.issue.getIssueType; + + let issuesCount = 0; + + if (issueType === "grouped") { + const issues = this.getIssues as IIssueGroupedStructure; + + if (!issues) return 0; + + Object.keys(issues).map((group_id) => { + issuesCount += issues[group_id].length; + }); + } + + if (issueType === "groupWithSubGroups") { + const issues = this.getIssues as IIssueGroupWithSubGroupsStructure; + + if (!issues) return 0; + + Object.keys(issues).map((sub_group_id) => { + Object.keys(issues[sub_group_id]).map((group_id) => { + issuesCount += issues[sub_group_id][group_id].length; + }); + }); + } + + if (issueType === "ungrouped") { + const issues = this.getIssues as IIssueUnGroupedStructure; + + if (!issues) return 0; + + issuesCount = issues.length; + } + + return issuesCount; + } + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { const viewId: string | null = this.rootStore.projectViews.viewId; const issueType = this.rootStore.issue.getIssueType; From dcf81e28e4479a9caf32362802ae0feaa5b0b912 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 30 Oct 2023 20:38:50 +0530 Subject: [PATCH 03/86] dev: implemented MobX in workspace settings and create workspace form (#2561) * dev: implement mobx store for workspace settings * chore: workspace general settings mobx integration * chore: workspace members settings mobx integration --- .../integration/jira/import-users.tsx | 2 +- web/components/issues/index.ts | 1 - web/components/issues/my-issues/index.ts | 3 - .../my-issues/my-issues-select-filters.tsx | 213 ----------- .../my-issues/my-issues-view-options.tsx | 105 ----- .../issues/my-issues/my-issues-view.tsx | 296 -------------- web/components/onboarding/workspace.tsx | 1 - web/components/profile/index.ts | 1 - .../profile/profile-issues-view-options.tsx | 282 -------------- .../confirm-workspace-member-remove.tsx | 51 ++- .../workspace/create-workspace-form.tsx | 47 ++- .../workspace/delete-workspace-modal.tsx | 34 +- web/components/workspace/index.ts | 6 +- .../send-workspace-invitation-modal.tsx | 48 +-- web/components/workspace/settings/index.ts | 3 + .../workspace/settings/members-list-item.tsx | 202 ++++++++++ .../workspace/settings/members-list.tsx | 75 ++++ .../workspace/settings/workspace-details.tsx | 306 +++++++++++++++ web/contexts/workspace-member.context.tsx | 3 +- web/hooks/my-issues/use-my-issues-filter.tsx | 4 +- web/layouts/auth-layout/workspace-wrapper.tsx | 12 +- web/pages/[workspaceSlug]/settings/index.tsx | 360 +----------------- .../[workspaceSlug]/settings/members.tsx | 288 +------------- web/pages/create-workspace/index.tsx | 1 - web/services/workspace.service.ts | 19 +- web/store/user.store.ts | 8 +- web/store/workspace/workspace.store.ts | 189 ++++++++- .../workspace/workspace_filters.store.ts | 4 +- web/types/users.d.ts | 1 + web/types/workspace.d.ts | 27 +- 30 files changed, 934 insertions(+), 1658 deletions(-) delete mode 100644 web/components/issues/my-issues/index.ts delete mode 100644 web/components/issues/my-issues/my-issues-select-filters.tsx delete mode 100644 web/components/issues/my-issues/my-issues-view-options.tsx delete mode 100644 web/components/issues/my-issues/my-issues-view.tsx delete mode 100644 web/components/profile/profile-issues-view-options.tsx create mode 100644 web/components/workspace/settings/index.ts create mode 100644 web/components/workspace/settings/members-list-item.tsx create mode 100644 web/components/workspace/settings/members-list.tsx create mode 100644 web/components/workspace/settings/workspace-details.tsx diff --git a/web/components/integration/jira/import-users.tsx b/web/components/integration/jira/import-users.tsx index 440584e1c..c49a3483a 100644 --- a/web/components/integration/jira/import-users.tsx +++ b/web/components/integration/jira/import-users.tsx @@ -31,7 +31,7 @@ export const JiraImportUsers: FC = () => { const { data: members } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null, - workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug?.toString() ?? "") : null + workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug?.toString() ?? "") : null ); const options = members?.map((member) => ({ diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index aee53a882..f48bb3894 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -1,6 +1,5 @@ export * from "./attachment"; export * from "./comment"; -export * from "./my-issues"; export * from "./sidebar-select"; export * from "./view-select"; export * from "./activity"; diff --git a/web/components/issues/my-issues/index.ts b/web/components/issues/my-issues/index.ts deleted file mode 100644 index 65a063f4c..000000000 --- a/web/components/issues/my-issues/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./my-issues-select-filters"; -export * from "./my-issues-view-options"; -export * from "./my-issues-view"; diff --git a/web/components/issues/my-issues/my-issues-select-filters.tsx b/web/components/issues/my-issues/my-issues-select-filters.tsx deleted file mode 100644 index 355496888..000000000 --- a/web/components/issues/my-issues/my-issues-select-filters.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -// services -import { IssueLabelService } from "services/issue"; -// components -import { DateFilterModal } from "components/core"; -// ui -import { MultiLevelDropdown } from "components/ui"; -// icons -import { PriorityIcon, StateGroupIcon } from "@plane/ui"; -// helpers -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; -// types -import { IIssueFilterOptions, TStateGroups } from "types"; -// fetch-keys -import { WORKSPACE_LABELS } from "constants/fetch-keys"; -// constants -import { GROUP_CHOICES, PRIORITIES } from "constants/project"; -import { DATE_FILTER_OPTIONS } from "constants/filters"; - -type Props = { - filters: Partial | any; - onSelect: (option: any) => void; - direction?: "left" | "right"; - height?: "sm" | "md" | "rg" | "lg"; -}; - -const issueLabelService = new IssueLabelService(); - -export const MyIssuesSelectFilters: React.FC = ({ filters, onSelect, direction = "right", height = "md" }) => { - const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); - const [dateFilterType, setDateFilterType] = useState<{ - title: string; - type: "start_date" | "target_date"; - }>({ - title: "", - type: "start_date", - }); - const [fetchLabels, setFetchLabels] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { data: labels } = useSWR( - workspaceSlug && fetchLabels ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug && fetchLabels ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null - ); - - return ( - <> - {/* {isDateFilterModalOpen && ( - setIsDateFilterModalOpen(false)} - isOpen={isDateFilterModalOpen} - onSelect={onSelect} - /> - )} */} - ({ - id: priority === null ? "null" : priority, - label: ( -
- {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - ], - }, - { - id: "state_group", - label: "State groups", - value: GROUP_CHOICES, - hasChildren: true, - children: [ - ...Object.keys(GROUP_CHOICES).map((key) => ({ - id: key, - label: ( -
- - {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} -
- ), - value: { - key: "state_group", - value: key, - }, - selected: filters?.state?.includes(key), - })), - ], - }, - { - id: "labels", - label: "Labels", - onClick: () => setFetchLabels(true), - value: labels, - hasChildren: true, - children: labels?.map((label) => ({ - id: label.id, - label: ( -
-
- {label.name} -
- ), - value: { - key: "labels", - value: label.id, - }, - selected: filters?.labels?.includes(label.id), - })), - }, - { - id: "start_date", - label: "Start date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...(DATE_FILTER_OPTIONS?.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "start_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], [option.value]), - })) ?? []), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - { - id: "target_date", - label: "Due date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...(DATE_FILTER_OPTIONS?.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "target_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], [option.value]), - })) ?? []), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - ]} - /> - - ); -}; diff --git a/web/components/issues/my-issues/my-issues-view-options.tsx b/web/components/issues/my-issues/my-issues-view-options.tsx deleted file mode 100644 index 7ca6dfce7..000000000 --- a/web/components/issues/my-issues/my-issues-view-options.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -// hooks -import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; -// components -import { MyIssuesSelectFilters } from "components/issues"; -// ui -import { Tooltip } from "@plane/ui"; -// icons -import { List, Sheet } from "lucide-react"; -// helpers -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; -// types -import { TIssueLayouts } from "types"; - -const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [ - { - type: "list", - Icon: List, - }, - { - type: "spreadsheet", - Icon: Sheet, - }, -]; - -export const MyIssuesViewOptions: React.FC = () => { - const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; - - const { displayFilters, setDisplayFilters, filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString()); - - const workspaceViewPathName = ["workspace-views/all-issues"]; - - const isWorkspaceViewPath = workspaceViewPathName.some((pathname) => router.pathname.includes(pathname)); - - const showFilters = isWorkspaceViewPath || globalViewId; - - return ( -
-
- {issueViewOptions.map((option) => ( - {replaceUnderscoreIfSnakeCase(option.type)} View} - position="bottom" - > - - - ))} -
- {showFilters && ( - { - const key = option.key as keyof typeof filters; - - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value); - - setFilters({ - [key]: valueExists ? null : option.value, - }); - } else { - const valueExists = filters[key]?.includes(option.value); - - if (valueExists) - setFilters({ - [option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value), - }); - else - setFilters({ - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }); - } - }} - direction="left" - height="rg" - /> - )} -
- ); -}; diff --git a/web/components/issues/my-issues/my-issues-view.tsx b/web/components/issues/my-issues/my-issues-view.tsx deleted file mode 100644 index 9ec77cbbf..000000000 --- a/web/components/issues/my-issues/my-issues-view.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -// services -import { IssueLabelService } from "services/issue"; -// hooks -import useMyIssues from "hooks/my-issues/use-my-issues"; -import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter"; -// components -import { FiltersList } from "components/core"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -// types -import { IIssue, IIssueFilterOptions } from "types"; -// fetch-keys -import { WORKSPACE_LABELS } from "constants/fetch-keys"; - -type Props = { - openIssuesListModal?: () => void; - disableUserActions?: false; -}; - -const issueLabelService = new IssueLabelService(); - -export const MyIssuesView: React.FC = () => { - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData] = useState<(Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined>( - undefined - ); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit] = useState<(IIssue & { actionType: "edit" | "delete" }) | undefined>(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete] = useState(null); - - // trash box - // const [trashBox, setTrashBox] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); - const { filters, setFilters } = useMyIssuesFilters(workspaceSlug?.toString()); - - const { data: labels } = useSWR( - workspaceSlug && (filters?.labels ?? []).length > 0 ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug && (filters?.labels ?? []).length > 0 - ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) - : null - ); - - // const handleDeleteIssue = useCallback( - // (issue: IIssue) => { - // setDeleteIssueModal(true); - // setIssueToDelete(issue); - // }, - // [setDeleteIssueModal, setIssueToDelete] - // ); - - // const handleOnDragEnd = useCallback( - // async (result: DropResult) => { - // setTrashBox(false); - - // if (!result.destination || !workspaceSlug || !groupedIssues || displayFilters?.group_by !== "priority") return; - - // const { source, destination } = result; - - // if (source.droppableId === destination.droppableId) return; - - // const draggedItem = groupedIssues[source.droppableId][source.index]; - - // if (!draggedItem) return; - - // if (destination.droppableId === "trashBox") handleDeleteIssue(draggedItem); - // else { - // const sourceGroup = source.droppableId; - // const destinationGroup = destination.droppableId; - - // draggedItem[displayFilters.group_by] = destinationGroup as TIssuePriorities; - - // mutate<{ - // [key: string]: IIssue[]; - // }>( - // USER_ISSUES(workspaceSlug.toString(), params), - // (prevData) => { - // if (!prevData) return prevData; - - // const sourceGroupArray = [...groupedIssues[sourceGroup]]; - // const destinationGroupArray = [...groupedIssues[destinationGroup]]; - - // sourceGroupArray.splice(source.index, 1); - // destinationGroupArray.splice(destination.index, 0, draggedItem); - - // return { - // ...prevData, - // [sourceGroup]: orderArrayBy(sourceGroupArray, displayFilters.order_by ?? "-created_at"), - // [destinationGroup]: orderArrayBy(destinationGroupArray, displayFilters.order_by ?? "-created_at"), - // }; - // }, - // false - // ); - - // // patch request - // issuesService - // .patchIssue( - // workspaceSlug as string, - // draggedItem.project, - // draggedItem.id, - // { - // priority: draggedItem.priority, - // }, - // user - // ) - // .catch(() => mutate(USER_ISSUES(workspaceSlug.toString(), params))); - // } - // }, - // [displayFilters, groupedIssues, handleDeleteIssue, params, user, workspaceSlug] - // ); - - // const addIssueToGroup = useCallback( - // (groupTitle: string) => { - // setCreateIssueModal(true); - - // let preloadedValue: string | string[] = groupTitle; - - // if (displayFilters?.group_by === "labels") { - // if (groupTitle === "None") preloadedValue = []; - // else preloadedValue = [groupTitle]; - // } - - // if (displayFilters?.group_by) - // setPreloadedData({ - // [displayFilters?.group_by]: preloadedValue, - // actionType: "createIssue", - // }); - // else setPreloadedData({ actionType: "createIssue" }); - // }, - // [setCreateIssueModal, setPreloadedData, displayFilters?.group_by] - // ); - - // const addIssueToDate = useCallback( - // (date: string) => { - // setCreateIssueModal(true); - // setPreloadedData({ - // target_date: date, - // actionType: "createIssue", - // }); - // }, - // [setCreateIssueModal, setPreloadedData] - // ); - - // const makeIssueCopy = useCallback( - // (issue: IIssue) => { - // setCreateIssueModal(true); - - // setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - // }, - // [setCreateIssueModal, setPreloadedData] - // ); - - // const handleEditIssue = useCallback( - // (issue: IIssue) => { - // setEditIssueModal(true); - // setIssueToEdit({ - // ...issue, - // actionType: "edit", - // cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - // module: issue.issue_module ? issue.issue_module.module : null, - // }); - // }, - // [setEditIssueModal, setIssueToEdit] - // ); - - // const handleIssueAction = useCallback( - // (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - // if (action === "copy") makeIssueCopy(issue); - // else if (action === "edit") handleEditIssue(issue); - // else if (action === "delete") handleDeleteIssue(issue); - // }, - // [makeIssueCopy, handleEditIssue, handleDeleteIssue] - // ); - - const filtersToDisplay = { ...filters, assignees: null, created_by: null, subscriber: null }; - - const nullFilters = Object.keys(filtersToDisplay).filter( - (key) => filtersToDisplay[key as keyof IIssueFilterOptions] === null - ); - const areFiltersApplied = - Object.keys(filtersToDisplay).length > 0 && nullFilters.length !== Object.keys(filtersToDisplay).length; - - // const isSubscribedIssuesRoute = router.pathname.includes("subscribed"); - // const isMySubscribedIssues = - // (filters.subscriber && filters.subscriber.length > 0 && router.pathname.includes("my-issues")) ?? false; - - // const disableAddIssueOption = isSubscribedIssuesRoute || isMySubscribedIssues; - - return ( - <> - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateMyIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateMyIssues(); - }} - /> - {issueToDelete && ( - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - onSubmit={async () => { - mutateMyIssues(); - }} - /> - )} - {areFiltersApplied && ( - <> -
- - setFilters({ - labels: null, - priority: null, - state_group: null, - start_date: null, - target_date: null, - }) - } - /> -
- {
} - - )} - {/* , - text: "New Issue", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "c", - }); - document.dispatchEvent(e); - }, - }, - }} - handleOnDragEnd={handleOnDragEnd} - handleIssueAction={handleIssueAction} - openIssuesListModal={openIssuesListModal ? openIssuesListModal : null} - removeIssue={null} - disableAddIssueOption={disableAddIssueOption} - trashBox={trashBox} - setTrashBox={setTrashBox} - viewProps={{ - displayFilters, - groupedIssues, - isEmpty, - mutateIssues: mutateMyIssues, - params, - properties, - }} - /> */} - - ); -}; diff --git a/web/components/onboarding/workspace.tsx b/web/components/onboarding/workspace.tsx index b0de35d78..8cafdde43 100644 --- a/web/components/onboarding/workspace.tsx +++ b/web/components/onboarding/workspace.tsx @@ -48,7 +48,6 @@ export const Workspace: React.FC = ({ finishOnboarding, stepChange, updat onSubmit={completeStep} defaultValues={defaultValues} setDefaultValues={setDefaultValues} - user={user} primaryButtonText={{ loading: "Creating...", default: "Continue", diff --git a/web/components/profile/index.ts b/web/components/profile/index.ts index 247a10dc6..2573da1b2 100644 --- a/web/components/profile/index.ts +++ b/web/components/profile/index.ts @@ -1,6 +1,5 @@ export * from "./overview"; export * from "./navbar"; -export * from "./profile-issues-view-options"; export * from "./profile-issues-view"; export * from "./sidebar"; diff --git a/web/components/profile/profile-issues-view-options.tsx b/web/components/profile/profile-issues-view-options.tsx deleted file mode 100644 index 95caa002d..000000000 --- a/web/components/profile/profile-issues-view-options.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -// headless ui -import { Popover, Transition } from "@headlessui/react"; -// hooks -import useProfileIssues from "hooks/use-profile-issues"; -import useEstimateOption from "hooks/use-estimate-option"; -// components -import { MyIssuesSelectFilters } from "components/issues"; -// ui -import { CustomMenu, ToggleSwitch, Tooltip } from "@plane/ui"; -// icons -import { ChevronDown, Kanban, List } from "lucide-react"; -// helpers -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; -// types -import { Properties, TIssueLayouts } from "types"; -// constants -import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue"; - -const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [ - { - type: "list", - Icon: List, - }, - { - type: "kanban", - Icon: Kanban, - }, -]; - -export const ProfileIssuesViewOptions: React.FC = () => { - const router = useRouter(); - const { workspaceSlug, userId } = router.query; - - const { displayFilters, setDisplayFilters, filters, displayProperties, setProperties, setFilters } = useProfileIssues( - workspaceSlug?.toString(), - userId?.toString() - ); - - const { isEstimateActive } = useEstimateOption(); - - if ( - !router.pathname.includes("assigned") && - !router.pathname.includes("created") && - !router.pathname.includes("subscribed") - ) - return null; - - return ( -
-
- {issueViewOptions.map((option) => ( - {replaceUnderscoreIfSnakeCase(option.type)} Layout} - position="bottom" - > - - - ))} -
- { - const key = option.key as keyof typeof filters; - - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value); - - setFilters({ - [key]: valueExists ? null : option.value, - }); - } else { - const valueExists = filters[key]?.includes(option.value); - - if (valueExists) - setFilters({ - [option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value), - }); - else - setFilters({ - [option.key]: [...((filters[key] ?? []) as any[]), option.value], - }); - } - }} - direction="left" - height="rg" - /> - - {({ open }) => ( - <> - - Display - - - - -
-
- {displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && ( - <> -
-

Group by

-
- option.key === displayFilters?.group_by) - ?.title ?? "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {ISSUE_GROUP_BY_OPTIONS.map((option) => { - if (displayFilters?.layout === "kanban" && option.key === null) return null; - if (option.key === "state" || option.key === "created_by" || option.key === "assignees") - return null; - - return ( - setDisplayFilters({ group_by: option.key })} - > - {option.title} - - ); - })} - -
-
-
-

Order by

-
- option.key === displayFilters?.order_by) - ?.title ?? "Select" - } - className="!w-full" - buttonClassName="w-full" - > - {ISSUE_ORDER_BY_OPTIONS.map((option) => { - if (displayFilters?.group_by === "priority" && option.key === "priority") return null; - if (option.key === "sort_order") return null; - - return ( - { - setDisplayFilters({ order_by: option.key }); - }} - > - {option.title} - - ); - })} - -
-
- - )} -
-

Issue type

-
- - {ISSUE_FILTER_OPTIONS.find((option) => option.key === displayFilters?.type)?.title ?? - "Select"} - - } - className="!w-full" - buttonClassName="w-full" - > - {ISSUE_FILTER_OPTIONS.map((option) => ( - - setDisplayFilters({ - type: option.key, - }) - } - > - {option.title} - - ))} - -
-
- - {displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && ( - <> -
-

Show empty states

-
- - setDisplayFilters({ - show_empty_groups: !displayFilters?.show_empty_groups, - }) - } - /> -
-
- - )} -
- -
-

Display Properties

-
- {displayProperties && - Object.keys(displayProperties).map((key) => { - if (key === "estimate" && !isEstimateActive) return null; - - if ( - displayFilters?.layout === "spreadsheet" && - (key === "attachment_count" || key === "link" || key === "sub_issue_count") - ) - return null; - - if ( - displayFilters?.layout !== "spreadsheet" && - (key === "created_on" || key === "updated_on") - ) - return null; - - return ( - - ); - })} -
-
-
-
-
- - )} -
-
- ); -}; diff --git a/web/components/workspace/confirm-workspace-member-remove.tsx b/web/components/workspace/confirm-workspace-member-remove.tsx index 2cf3c27af..4b057f1de 100644 --- a/web/components/workspace/confirm-workspace-member-remove.tsx +++ b/web/components/workspace/confirm-workspace-member-remove.tsx @@ -1,29 +1,37 @@ import React, { useState } from "react"; -// headless ui +import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// icons import { AlertTriangle } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // ui import { Button } from "@plane/ui"; type Props = { isOpen: boolean; onClose: () => void; - handleDelete: () => void; + onSubmit: () => Promise; data?: any; }; -const ConfirmWorkspaceMemberRemove: React.FC = ({ isOpen, onClose, data, handleDelete }) => { - const [isDeleteLoading, setIsDeleteLoading] = useState(false); +export const ConfirmWorkspaceMemberRemove: React.FC = observer((props) => { + const { isOpen, onClose, data, onSubmit } = props; + + const [isRemoving, setIsRemoving] = useState(false); + + const { user: userStore } = useMobxStore(); + const user = userStore.currentUser; const handleClose = () => { onClose(); - setIsDeleteLoading(false); + setIsRemoving(false); }; const handleDeletion = async () => { - setIsDeleteLoading(true); - handleDelete(); + setIsRemoving(true); + + await onSubmit(); + handleClose(); }; @@ -61,14 +69,21 @@ const ConfirmWorkspaceMemberRemove: React.FC = ({ isOpen, onClose, data,
- Remove {data?.display_name}? + {user?.id === data?.memberId ? "Leave workspace?" : `Remove ${data?.display_name}?`}
-

- Are you sure you want to remove member-{" "} - {data?.display_name}? They will no longer have access to - this workspace. This action cannot be undone. -

+ {user?.id === data?.memberId ? ( +

+ Are you sure you want to leave the workspace? You will no longer have access to this + workspace. This action cannot be undone. +

+ ) : ( +

+ Are you sure you want to remove member-{" "} + {data?.display_name}? They will no longer have access to + this workspace. This action cannot be undone. +

+ )}
@@ -77,8 +92,8 @@ const ConfirmWorkspaceMemberRemove: React.FC = ({ isOpen, onClose, data, -
@@ -88,6 +103,4 @@ const ConfirmWorkspaceMemberRemove: React.FC = ({ isOpen, onClose, data, ); -}; - -export default ConfirmWorkspaceMemberRemove; +}); diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index 658e0cedb..584292422 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -1,7 +1,10 @@ import { Dispatch, SetStateAction, useEffect, useState, FC } from "react"; -import { mutate } from "swr"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; +import { mutate } from "swr"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // services import { WorkspaceService } from "services/workspace.service"; // hooks @@ -9,7 +12,7 @@ import useToast from "hooks/use-toast"; // ui import { Button, CustomSelect, Input } from "@plane/ui"; // types -import { IUser, IWorkspace } from "types"; +import { IWorkspace } from "types"; // fetch-keys import { USER_WORKSPACES } from "constants/fetch-keys"; // constants @@ -23,7 +26,6 @@ type Props = { organization_size: string; }; setDefaultValues: Dispatch>; - user: IUser | undefined; secondaryButton?: React.ReactNode; primaryButtonText?: { loading: string; @@ -49,23 +51,27 @@ const restrictedUrls = [ const workspaceService = new WorkspaceService(); -export const CreateWorkspaceForm: FC = ({ - onSubmit, - defaultValues, - setDefaultValues, - user, - secondaryButton, - primaryButtonText = { - loading: "Creating...", - default: "Create Workspace", - }, -}) => { +export const CreateWorkspaceForm: FC = observer((props) => { + const { + onSubmit, + defaultValues, + setDefaultValues, + secondaryButton, + primaryButtonText = { + loading: "Creating...", + default: "Create Workspace", + }, + } = props; + const [slugError, setSlugError] = useState(false); const [invalidSlug, setInvalidSlug] = useState(false); - const { setToastAlert } = useToast(); const router = useRouter(); + const { workspace: workspaceStore } = useMobxStore(); + + const { setToastAlert } = useToast(); + const { handleSubmit, control, @@ -81,8 +87,8 @@ export const CreateWorkspaceForm: FC = ({ if (res.status === true && !restrictedUrls.includes(formData.slug)) { setSlugError(false); - await workspaceService - .createWorkspace(formData, user) + await workspaceStore + .createWorkspace(formData) .then(async (res) => { setToastAlert({ type: "success", @@ -157,7 +163,7 @@ export const CreateWorkspaceForm: FC = ({
-
+
{window && window.location.host}/ = ({ onChange={onChange} label={ ORGANIZATION_SIZE.find((c) => c === value) ?? ( - Select organization size + Select organization size ) } + buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none" input width="w-full" > @@ -232,4 +239,4 @@ export const CreateWorkspaceForm: FC = ({
); -}; +}); diff --git a/web/components/workspace/delete-workspace-modal.tsx b/web/components/workspace/delete-workspace-modal.tsx index 14ab211ce..4dcbebafb 100644 --- a/web/components/workspace/delete-workspace-modal.tsx +++ b/web/components/workspace/delete-workspace-modal.tsx @@ -1,31 +1,22 @@ import React from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// react-hook-form +import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; -// headless ui import { Dialog, Transition } from "@headlessui/react"; -// services -import { WorkspaceService } from "services/workspace.service"; +import { AlertTriangle } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; -// icons -import { AlertTriangle } from "lucide-react"; // ui import { Button, Input } from "@plane/ui"; // types -import type { IUser, IWorkspace } from "types"; -// fetch-keys -import { USER_WORKSPACES } from "constants/fetch-keys"; +import type { IWorkspace } from "types"; type Props = { isOpen: boolean; data: IWorkspace | null; onClose: () => void; - user: IUser | undefined; }; const defaultValues = { @@ -33,12 +24,13 @@ const defaultValues = { confirmDelete: "", }; -// services -const workspaceService = new WorkspaceService(); +export const DeleteWorkspaceModal: React.FC = observer((props) => { + const { isOpen, data, onClose } = props; -export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, user }) => { const router = useRouter(); + const { workspace: workspaceStore } = useMobxStore(); + const { setToastAlert } = useToast(); const { @@ -63,15 +55,13 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u const onSubmit = async () => { if (!data || !canDelete) return; - await workspaceService - .deleteWorkspace(data.slug, user) + await workspaceStore + .deleteWorkspace(data.slug) .then(() => { handleClose(); router.push("/"); - mutate(USER_WORKSPACES, (prevData) => prevData?.filter((workspace) => workspace.id !== data.id)); - setToastAlert({ type: "success", title: "Success!", @@ -196,4 +186,4 @@ export const DeleteWorkspaceModal: React.FC = ({ isOpen, data, onClose, u ); -}; +}); diff --git a/web/components/workspace/index.ts b/web/components/workspace/index.ts index 9fe2934b0..332ea6c60 100644 --- a/web/components/workspace/index.ts +++ b/web/components/workspace/index.ts @@ -1,13 +1,17 @@ +export * from "./settings"; export * from "./views"; export * from "./activity-graph"; export * from "./completed-issues-graph"; +export * from "./confirm-workspace-member-remove"; export * from "./create-workspace-form"; export * from "./delete-workspace-modal"; export * from "./help-section"; export * from "./issues-list"; export * from "./issues-pie-chart"; export * from "./issues-stats"; +export * from "./member-select"; +export * from "./send-workspace-invitation-modal"; export * from "./sidebar-dropdown"; export * from "./sidebar-menu"; export * from "./sidebar-quick-action"; -export * from "./member-select"; +export * from "./single-invitation"; diff --git a/web/components/workspace/send-workspace-invitation-modal.tsx b/web/components/workspace/send-workspace-invitation-modal.tsx index 1fa4f002a..abeacd43d 100644 --- a/web/components/workspace/send-workspace-invitation-modal.tsx +++ b/web/components/workspace/send-workspace-invitation-modal.tsx @@ -14,14 +14,15 @@ import { Plus, X } from "lucide-react"; import { IUser } from "types"; // constants import { ROLE } from "constants/workspace"; +// fetch-keys import { WORKSPACE_INVITATIONS } from "constants/fetch-keys"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; - workspace_slug: string; + onClose: () => void; + workspaceSlug: string; user: IUser | undefined; - onSuccess: () => void; + onSuccess?: () => Promise; }; type EmailRole = { @@ -44,8 +45,9 @@ const defaultValues: FormValues = { const workspaceService = new WorkspaceService(); -const SendWorkspaceInvitationModal: React.FC = (props) => { - const { isOpen, setIsOpen, workspace_slug, user, onSuccess } = props; +export const SendWorkspaceInvitationModal: React.FC = (props) => { + const { isOpen, onClose, workspaceSlug, user, onSuccess } = props; + const { control, reset, @@ -61,42 +63,38 @@ const SendWorkspaceInvitationModal: React.FC = (props) => { const { setToastAlert } = useToast(); const handleClose = () => { - setIsOpen(false); + onClose(); + const timeout = setTimeout(() => { reset(defaultValues); clearTimeout(timeout); - }, 500); + }, 350); }; const onSubmit = async (formData: FormValues) => { - if (!workspace_slug) return; - - const payload = { ...formData }; + if (!workspaceSlug) return; await workspaceService - .inviteWorkspace(workspace_slug, payload, user) + .inviteWorkspace(workspaceSlug, formData, user) .then(async () => { - setIsOpen(false); + if (onSuccess) await onSuccess(); + handleClose(); + setToastAlert({ type: "success", title: "Success!", message: "Invitations sent successfully.", }); - onSuccess(); }) - .catch((err) => { + .catch((err) => setToastAlert({ type: "error", title: "Error!", - message: `${err.error}`, - }); - console.log(err); - }) - .finally(() => { - reset(defaultValues); - mutate(WORKSPACE_INVITATIONS); - }); + message: `${err.error ?? "Something went wrong. Please try again."}`, + }) + ) + .finally(() => mutate(WORKSPACE_INVITATIONS)); }; const appendField = () => { @@ -104,9 +102,7 @@ const SendWorkspaceInvitationModal: React.FC = (props) => { }; useEffect(() => { - if (fields.length === 0) { - append([{ email: "", role: 15 }]); - } + if (fields.length === 0) append([{ email: "", role: 15 }]); }, [fields, append]); return ( @@ -249,5 +245,3 @@ const SendWorkspaceInvitationModal: React.FC = (props) => { ); }; - -export default SendWorkspaceInvitationModal; diff --git a/web/components/workspace/settings/index.ts b/web/components/workspace/settings/index.ts new file mode 100644 index 000000000..fb7aa7526 --- /dev/null +++ b/web/components/workspace/settings/index.ts @@ -0,0 +1,3 @@ +export * from "./members-list-item"; +export * from "./members-list"; +export * from "./workspace-details"; diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx new file mode 100644 index 000000000..df2defd5b --- /dev/null +++ b/web/components/workspace/settings/members-list-item.tsx @@ -0,0 +1,202 @@ +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { WorkspaceService } from "services/workspace.service"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { ConfirmWorkspaceMemberRemove } from "components/workspace"; +// ui +import { CustomSelect, Tooltip } from "@plane/ui"; +// icons +import { ChevronDown, XCircle } from "lucide-react"; +// constants +import { ROLE } from "constants/workspace"; + +type Props = { + member: { + id: string; + memberId: string; + avatar: string; + first_name: string; + last_name: string; + email: string | undefined; + display_name: string; + role: 5 | 10 | 15 | 20; + status: boolean; + member: boolean; + accountCreated: boolean; + }; +}; + +// services +const workspaceService = new WorkspaceService(); + +export const WorkspaceMembersListItem: React.FC = (props) => { + const { member } = props; + + const [removeMemberModal, setRemoveMemberModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { setToastAlert } = useToast(); + + const { workspace: workspaceStore, user: userStore } = useMobxStore(); + + const user = userStore.workspaceMemberInfo; + const isAdmin = userStore.workspaceMemberInfo?.role === 20; + + const handleRemoveMember = async () => { + if (!workspaceSlug) return; + + if (member.member) + await workspaceStore.removeMember(workspaceSlug.toString(), member.id).catch((err) => { + const error = err?.error; + setToastAlert({ + type: "error", + title: "Error", + message: error || "Something went wrong", + }); + }); + else + await workspaceService + .deleteWorkspaceInvitations(workspaceSlug.toString(), member.id) + .then(() => { + setToastAlert({ + type: "success", + title: "Success", + message: "Member removed successfully", + }); + }) + .catch((err) => { + const error = err?.error; + + setToastAlert({ + type: "error", + title: "Error", + message: error || "Something went wrong", + }); + }); + }; + + if (!user) return null; + + return ( + <> + setRemoveMemberModal(false)} + data={member} + onSubmit={handleRemoveMember} + /> +
+
+ {member.avatar && member.avatar !== "" ? ( + + + {member.display_name + + + ) : ( + + + {(member.email ?? member.display_name ?? "?")[0]} + + + )} +
+ {member.member ? ( + + + {member.first_name} {member.last_name} + + + ) : ( +

{member.display_name || member.email}

+ )} +

{member.email ?? member.display_name}

+
+
+
+ {!member?.status && ( +
+

Pending

+
+ )} + {member?.status && !member?.accountCreated && ( +
+

Account not created

+
+ )} + + + {ROLE[member.role as keyof typeof ROLE]} + + {member.memberId !== user.member && ( + + + + )} +
+ } + value={member.role} + onChange={(value: 5 | 10 | 15 | 20 | undefined) => { + if (!workspaceSlug) return; + + workspaceStore + .updateMember(workspaceSlug.toString(), member.id, { + role: value, + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "An error occurred while updating member role. Please try again.", + }); + }); + }} + disabled={ + member.memberId === user.member || !member.status || (user.role !== 20 && user.role < member.role) + } + placement="bottom-end" + > + {Object.keys(ROLE).map((key) => { + if (user.role !== 20 && user.role < parseInt(key)) return null; + + return ( + + <>{ROLE[parseInt(key) as keyof typeof ROLE]} + + ); + })} + + {isAdmin && ( + + + + )} +
+
+ + ); +}; diff --git a/web/components/workspace/settings/members-list.tsx b/web/components/workspace/settings/members-list.tsx new file mode 100644 index 000000000..02c9bd6e0 --- /dev/null +++ b/web/components/workspace/settings/members-list.tsx @@ -0,0 +1,75 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { WorkspaceService } from "services/workspace.service"; +// components +import { WorkspaceMembersListItem } from "components/workspace"; +// ui +import { Loader } from "@plane/ui"; + +const workspaceService = new WorkspaceService(); + +export const WorkspaceMembersList: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { workspace: workspaceStore, user: userStore } = useMobxStore(); + + const workspaceMembers = workspaceStore.workspaceMembers; + const user = userStore.workspaceMemberInfo; + + const { data: workspaceInvitations } = useSWR( + workspaceSlug ? `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}` : null, + workspaceSlug ? () => workspaceService.workspaceInvitations(workspaceSlug.toString()) : null + ); + + const members = [ + ...(workspaceInvitations?.map((item) => ({ + id: item.id, + memberId: item.id, + avatar: "", + first_name: item.email, + last_name: "", + email: item.email, + display_name: item.email, + role: item.role, + status: item.accepted, + member: false, + accountCreated: item.accepted, + })) || []), + ...(workspaceMembers?.map((item) => ({ + id: item.id, + memberId: item.member?.id, + avatar: item.member?.avatar, + first_name: item.member?.first_name, + last_name: item.member?.last_name, + email: item.member?.email, + display_name: item.member?.display_name, + role: item.role, + status: true, + member: true, + accountCreated: true, + })) || []), + ]; + + if (!workspaceMembers || !workspaceInvitations || !user) + return ( + + + + + + + ); + + return ( +
+ {members.length > 0 + ? members.map((member) => ) + : null} +
+ ); +}); diff --git a/web/components/workspace/settings/workspace-details.tsx b/web/components/workspace/settings/workspace-details.tsx new file mode 100644 index 000000000..517a0f058 --- /dev/null +++ b/web/components/workspace/settings/workspace-details.tsx @@ -0,0 +1,306 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; +import { Disclosure, Transition } from "@headlessui/react"; +import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// services +import { FileService } from "services/file.service"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { DeleteWorkspaceModal } from "components/workspace"; +import { ImageUploadModal } from "components/core"; +// ui +import { Button, CustomSelect, Input, Spinner } from "@plane/ui"; +// types +import { IWorkspace } from "types"; +// constants +import { ORGANIZATION_SIZE } from "constants/workspace"; + +const defaultValues: Partial = { + name: "", + url: "", + organization_size: "2-10", + logo: null, +}; + +// services +const fileService = new FileService(); + +export const WorkspaceDetails: React.FC = observer(() => { + const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false); + const [isImageUploading, setIsImageUploading] = useState(false); + const [isImageRemoving, setIsImageRemoving] = useState(false); + const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); + + const { workspace: workspaceStore, user: userStore } = useMobxStore(); + const activeWorkspace = workspaceStore.currentWorkspace; + + const { setToastAlert } = useToast(); + + const { + handleSubmit, + control, + reset, + watch, + setValue, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { ...defaultValues, ...activeWorkspace }, + }); + + const onSubmit = async (formData: IWorkspace) => { + if (!activeWorkspace) return; + + const payload: Partial = { + logo: formData.logo, + name: formData.name, + organization_size: formData.organization_size, + }; + + await workspaceStore + .updateWorkspace(activeWorkspace.slug, payload) + .then(() => + setToastAlert({ + title: "Success", + type: "success", + message: "Workspace updated successfully", + }) + ) + .catch((err) => console.error(err)); + }; + + const handleDelete = (url: string | null | undefined) => { + if (!activeWorkspace || !url) return; + + setIsImageRemoving(true); + + fileService.deleteFile(activeWorkspace.id, url).then(() => { + workspaceStore + .updateWorkspace(activeWorkspace.slug, { logo: "" }) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Workspace picture removed successfully.", + }); + setIsImageUploadModalOpen(false); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "There was some error in deleting your profile picture. Please try again.", + }); + }) + .finally(() => setIsImageRemoving(false)); + }); + }; + + useEffect(() => { + if (activeWorkspace) reset({ ...activeWorkspace }); + }, [activeWorkspace, reset]); + + const isAdmin = userStore.workspaceMemberInfo?.role === 20; + + if (!activeWorkspace) + return ( +
+ +
+ ); + + return ( + <> + setDeleteWorkspaceModal(false)} + data={activeWorkspace} + /> + setIsImageUploadModalOpen(false)} + isRemoving={isImageRemoving} + handleDelete={() => handleDelete(activeWorkspace?.logo)} + onSuccess={(imageUrl) => { + setIsImageUploading(true); + setValue("logo", imageUrl); + setIsImageUploadModalOpen(false); + handleSubmit(onSubmit)().then(() => setIsImageUploading(false)); + }} + value={watch("logo")} + /> +
+
+
+ +
+
+

{watch("name")}

+ {`${ + typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "") + }/${activeWorkspace.slug}`} +
+ +
+
+
+ +
+
+
+

Workspace Name

+ ( + + )} + /> +
+ +
+

Company Size

+ ( + c === value) ?? "Select organization size"} + width="w-full" + buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none" + input + disabled={!isAdmin} + > + {ORGANIZATION_SIZE.map((item) => ( + + {item} + + ))} + + )} + /> +
+ +
+

Workspace URL

+ ( + + )} + /> +
+
+ +
+ +
+
+ {isAdmin && ( + + {({ open }) => ( +
+ + Delete Workspace + {/* */} + {open ? : } + + + + +
+ + The danger zone of the workspace delete page is a critical area that requires careful + consideration and attention. When deleting a workspace, all of the data and resources within + that workspace will be permanently removed and cannot be recovered. + +
+ +
+
+
+
+
+ )} +
+ )} +
+ + ); +}); diff --git a/web/contexts/workspace-member.context.tsx b/web/contexts/workspace-member.context.tsx index d312751fe..e08e09399 100644 --- a/web/contexts/workspace-member.context.tsx +++ b/web/contexts/workspace-member.context.tsx @@ -26,6 +26,7 @@ type Props = { // services const workspaceService = new WorkspaceService(); +// TODO: remove this context export const WorkspaceMemberProvider: React.FC = (props) => { const { children } = props; @@ -40,7 +41,7 @@ export const WorkspaceMemberProvider: React.FC = (props) => { const loading = !memberDetails && !error; return ( - + {children} ); diff --git a/web/hooks/my-issues/use-my-issues-filter.tsx b/web/hooks/my-issues/use-my-issues-filter.tsx index f9b54b8ce..06fe5ef5e 100644 --- a/web/hooks/my-issues/use-my-issues-filter.tsx +++ b/web/hooks/my-issues/use-my-issues-filter.tsx @@ -6,7 +6,7 @@ import { WorkspaceService } from "services/workspace.service"; import { IIssueDisplayFilterOptions, IIssueFilterOptions, - IWorkspaceMember, + IWorkspaceMemberMe, IWorkspaceViewProps, Properties, } from "types"; @@ -66,7 +66,7 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => { const oldData = { ...myWorkspace }; - mutate( + mutate( WORKSPACE_MEMBERS_ME(workspaceSlug.toString()), (prevData) => { if (!prevData) return; diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index e1ae8e4db..e46515bf7 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -12,6 +12,8 @@ export interface IWorkspaceAuthWrapper { children: ReactNode; } +const HIGHER_ROLES = [20, 15]; + export const WorkspaceAuthWrapper: FC = observer((props) => { const { children } = props; // store @@ -22,7 +24,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) // fetching all workspaces useSWR(`USER_WORKSPACES_LIST`, () => workspaceStore.fetchWorkspaces()); // fetching user workspace information - useSWR( + const { data: workspaceMemberInfo } = useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, workspaceSlug ? () => userStore.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null ); @@ -33,8 +35,12 @@ export const WorkspaceAuthWrapper: FC = observer((props) ); // fetch workspace members useSWR( - workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, - workspaceSlug ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) : null + workspaceSlug && workspaceMemberInfo && HIGHER_ROLES.includes(workspaceMemberInfo.role) + ? `WORKSPACE_MEMBERS_${workspaceSlug}` + : null, + workspaceSlug && workspaceMemberInfo && HIGHER_ROLES.includes(workspaceMemberInfo.role) + ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) + : null ); // fetch workspace labels useSWR( diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index 5ccd4ed9e..580c45942 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -1,362 +1,18 @@ -import React, { useEffect, useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; - -// react-hook-form -import { Controller, useForm } from "react-hook-form"; -// services -import { WorkspaceService } from "services/workspace.service"; -import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; -import useUserAuth from "hooks/use-user-auth"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/setting-layout"; // components -import { ImageUploadModal } from "components/core"; -import { DeleteWorkspaceModal } from "components/workspace"; import { WorkspaceSettingHeader } from "components/headers"; -// ui -import { Disclosure, Transition } from "@headlessui/react"; -import { Button, CustomSelect, Input, Spinner } from "@plane/ui"; -// icons -import { ChevronDown, ChevronUp, Pencil } from "lucide-react"; +import { WorkspaceDetails } from "components/workspace"; // types -import type { IWorkspace } from "types"; import type { NextPage } from "next"; -// fetch-keys -import { WORKSPACE_DETAILS, USER_WORKSPACES, WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; -// constants -import { ORGANIZATION_SIZE } from "constants/workspace"; -const defaultValues: Partial = { - name: "", - url: "", - organization_size: "2-10", - logo: null, -}; - -// services -const workspaceService = new WorkspaceService(); -const fileService = new FileService(); - -const WorkspaceSettings: NextPage = () => { - const [isOpen, setIsOpen] = useState(false); - const [isImageUploading, setIsImageUploading] = useState(false); - const [isImageRemoving, setIsImageRemoving] = useState(false); - const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { user } = useUserAuth(); - - const { data: memberDetails } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null, - workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null - ); - - const { setToastAlert } = useToast(); - - const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () => - workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null - ); - - const { - handleSubmit, - control, - reset, - watch, - setValue, - formState: { errors, isSubmitting }, - } = useForm({ - defaultValues: { ...defaultValues, ...activeWorkspace }, - }); - - useEffect(() => { - if (activeWorkspace) reset({ ...activeWorkspace }); - }, [activeWorkspace, reset]); - - const onSubmit = async (formData: IWorkspace) => { - if (!activeWorkspace) return; - - const payload: Partial = { - logo: formData.logo, - name: formData.name, - organization_size: formData.organization_size, - }; - - await workspaceService - .updateWorkspace(activeWorkspace.slug, payload, user) - .then((res) => { - mutate(USER_WORKSPACES, (prevData) => - prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) - ); - mutate(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - logo: formData.logo, - }; - }); - setToastAlert({ - title: "Success", - type: "success", - message: "Workspace updated successfully", - }); - }) - .catch((err) => console.error(err)); - }; - - const handleDelete = (url: string | null | undefined) => { - if (!activeWorkspace || !url) return; - - setIsImageRemoving(true); - - fileService.deleteFile(activeWorkspace.id, url).then(() => { - workspaceService - .updateWorkspace(activeWorkspace.slug, { logo: "" }, user) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Workspace picture removed successfully.", - }); - mutate(USER_WORKSPACES, (prevData) => - prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) - ); - mutate(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - logo: "", - }; - }); - setIsImageUploadModalOpen(false); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in deleting your profile picture. Please try again.", - }); - }) - .finally(() => setIsImageRemoving(false)); - }); - }; - - const isAdmin = memberDetails?.role === 20; - - return ( - }> - - setIsImageUploadModalOpen(false)} - isRemoving={isImageRemoving} - handleDelete={() => handleDelete(activeWorkspace?.logo)} - onSuccess={(imageUrl) => { - setIsImageUploading(true); - setValue("logo", imageUrl); - setIsImageUploadModalOpen(false); - handleSubmit(onSubmit)().then(() => setIsImageUploading(false)); - }} - value={watch("logo")} - /> - { - setIsOpen(false); - }} - data={activeWorkspace ?? null} - user={user} - /> - {activeWorkspace ? ( -
-
-
- -
-
-

{watch("name")}

- {`${ - typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "") - }/${activeWorkspace.slug}`} -
- -
-
-
- -
-
-
-

Workspace Name

- ( - - )} - /> -
- -
-

Company Size

- ( - c === value) ?? "Select organization size"} - width="w-full" - input - disabled={!isAdmin} - > - {ORGANIZATION_SIZE?.map((item) => ( - - {item} - - ))} - - )} - /> -
- -
-

Workspace URL

- ( - - )} - /> -
-
- -
- -
-
- {isAdmin && ( - - {({ open }) => ( -
- - Delete Workspace - {/* */} - {open ? : } - - - - -
- - The danger zone of the workspace delete page is a critical area that requires careful - consideration and attention. When deleting a workspace, all of the data and resources within - that workspace will be permanently removed and cannot be recovered. - -
- -
-
-
-
-
- )} -
- )} -
- ) : ( -
- -
- )} -
-
- ); -}; +const WorkspaceSettings: NextPage = () => ( + }> + + + + +); export default WorkspaceSettings; diff --git a/web/pages/[workspaceSlug]/settings/members.tsx b/web/pages/[workspaceSlug]/settings/members.tsx index 4c055b687..7855f01c8 100644 --- a/web/pages/[workspaceSlug]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/settings/members.tsx @@ -1,309 +1,45 @@ import { useState } from "react"; - -import Link from "next/link"; import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import { WorkspaceService } from "services/workspace.service"; // hooks -import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; -import useWorkspaceMembers from "hooks/use-workspace-members"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/setting-layout"; // components -import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove"; -import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal"; import { WorkspaceSettingHeader } from "components/headers"; +import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "components/workspace"; // ui -import { Button, CustomMenu, CustomSelect, Loader } from "@plane/ui"; -// icons -import { ChevronDown, X } from "lucide-react"; +import { Button } from "@plane/ui"; // types import type { NextPage } from "next"; -// fetch-keys -import { WORKSPACE_INVITATION_WITH_EMAIL, WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys"; -// constants -import { ROLE } from "constants/workspace"; -// helper - -// services -const workspaceService = new WorkspaceService(); const MembersSettings: NextPage = () => { - const [selectedRemoveMember, setSelectedRemoveMember] = useState(null); - const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState(null); const [inviteModal, setInviteModal] = useState(false); const router = useRouter(); const { workspaceSlug } = router.query; - const { setToastAlert } = useToast(); - const { user } = useUser(); - const { isOwner } = useWorkspaceMembers(workspaceSlug?.toString(), Boolean(workspaceSlug)); - - const { data: workspaceMembers, mutate: mutateMembers } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug.toString()) : null, - workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug.toString()) : null - ); - - const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR( - workspaceSlug ? WORKSPACE_INVITATION_WITH_EMAIL(workspaceSlug.toString()) : null, - workspaceSlug ? () => workspaceService.workspaceInvitationsWithEmail(workspaceSlug.toString()) : null - ); - - const members = [ - ...(workspaceInvitations?.map((item) => ({ - id: item.id, - memberId: item.id, - avatar: "", - first_name: item.email, - last_name: "", - email: item.email, - display_name: item.email, - role: item.role, - status: item.accepted, - member: false, - accountCreated: item?.accepted ? false : true, - })) || []), - ...(workspaceMembers?.map((item) => ({ - id: item.id, - memberId: item.member?.id, - avatar: item.member?.avatar, - first_name: item.member?.first_name, - last_name: item.member?.last_name, - email: item.member?.email, - display_name: item.member?.display_name, - role: item.role, - status: true, - member: true, - accountCreated: true, - })) || []), - ]; - - const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id); - - const handleInviteModalSuccess = () => { - mutateInvitations(); - }; - return ( }> - { - setSelectedRemoveMember(null); - setSelectedInviteRemoveMember(null); - }} - data={ - selectedRemoveMember - ? members.find((item) => item.id === selectedRemoveMember) - : selectedInviteRemoveMember - ? members.find((item) => item.id === selectedInviteRemoveMember) - : null - } - handleDelete={async () => { - if (!workspaceSlug) return; - if (selectedRemoveMember) { - workspaceService - .deleteWorkspaceMember(workspaceSlug as string, selectedRemoveMember) - .catch((err) => { - const error = err?.error; - setToastAlert({ - type: "error", - title: "Error", - message: error || "Something went wrong", - }); - }) - .finally(() => { - mutateMembers((prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember)); - }); - } - if (selectedInviteRemoveMember) { - mutateInvitations( - (prevData: any) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember), - false - ); - workspaceService - .deleteWorkspaceInvitations(workspaceSlug as string, selectedInviteRemoveMember) - .then(() => { - setToastAlert({ - type: "success", - title: "Success", - message: "Member removed successfully", - }); - }) - .catch((err) => { - const error = err?.error; - setToastAlert({ - type: "error", - title: "Error", - message: error || "Something went wrong", - }); - }) - .finally(() => { - mutateInvitations(); - }); - } - setSelectedRemoveMember(null); - setSelectedInviteRemoveMember(null); - }} - /> - + {workspaceSlug && ( + setInviteModal(false)} + workspaceSlug={workspaceSlug.toString()} + user={user} + /> + )}
-
+

Members

- {!workspaceMembers || !workspaceInvitations ? ( - - - - - - - ) : ( -
- {members.length > 0 - ? members.map((member) => ( -
-
- {member.avatar && member.avatar !== "" ? ( - - - {member.display_name - - - ) : member.display_name || member.email ? ( - - - {(member.display_name || member.email)?.charAt(0)} - - - ) : ( -
- ? -
- )} -
- {member.member ? ( - - - - {member.first_name} {member.last_name} - - ({member.display_name}) - - - ) : ( -

{member.display_name || member.email}

- )} - {isOwner &&

{member.email}

} -
-
-
- {!member?.status && ( -
-

Pending

-
- )} - {member?.status && !member?.accountCreated && ( -
-

Account not created

-
- )} - - - {ROLE[member.role as keyof typeof ROLE]} - - {member.memberId !== user?.id && } -
- } - value={member.role} - onChange={(value: 5 | 10 | 15 | 20 | undefined) => { - if (!workspaceSlug) return; - - mutateMembers( - (prevData: any) => - prevData?.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)), - false - ); - - workspaceService - .updateWorkspaceMember(workspaceSlug?.toString(), member.id, { - role: value, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "An error occurred while updating member role. Please try again.", - }); - }); - }} - disabled={ - member.memberId === currentUser?.member.id || - !member.status || - (currentUser && currentUser.role !== 20 && currentUser.role < member.role) - } - > - {Object.keys(ROLE).map((key) => { - if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null; - - return ( - - <>{ROLE[parseInt(key) as keyof typeof ROLE]} - - ); - })} - - - { - if (member.member) { - setSelectedRemoveMember(member.id); - } else { - setSelectedInviteRemoveMember(member.id); - } - }} - > - - - - {user?.id === member.memberId ? "Leave" : "Remove member"} - - - -
-
- )) - : null} -
- )} +
diff --git a/web/pages/create-workspace/index.tsx b/web/pages/create-workspace/index.tsx index 81048940b..370ee7352 100644 --- a/web/pages/create-workspace/index.tsx +++ b/web/pages/create-workspace/index.tsx @@ -68,7 +68,6 @@ const CreateWorkspace: NextPage = () => { onSubmit={onSubmit} defaultValues={defaultValues} setDefaultValues={setDefaultValues} - user={user} />
diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 3d97eca9c..6d2f9cdf7 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -6,6 +6,7 @@ import { API_BASE_URL } from "helpers/common.helper"; // types import { IWorkspace, + IWorkspaceMemberMe, IWorkspaceMember, IWorkspaceMemberInvitation, ILastActiveWorkspaceDetails, @@ -139,15 +140,7 @@ export class WorkspaceService extends APIService { }); } - async workspaceMembersWithEmail(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/members/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async workspaceMemberMe(workspaceSlug: string): Promise { + async workspaceMemberMe(workspaceSlug: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`) .then((response) => response?.data) .catch((error) => { @@ -191,14 +184,6 @@ export class WorkspaceService extends APIService { }); } - async workspaceInvitationsWithEmail(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/invitations/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async getWorkspaceInvitation(invitationId: string): Promise { return this.get(`/api/users/me/invitations/${invitationId}/`, { headers: {} }) .then((response) => response?.data) diff --git a/web/store/user.store.ts b/web/store/user.store.ts index 42024a775..cbb861288 100644 --- a/web/store/user.store.ts +++ b/web/store/user.store.ts @@ -6,7 +6,7 @@ import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; // interfaces import { IUser, IUserSettings } from "types/users"; -import { IWorkspaceMember, IProjectMember } from "types"; +import { IWorkspaceMemberMe, IProjectMember } from "types"; export interface IUserStore { loader: boolean; @@ -17,7 +17,7 @@ export interface IUserStore { dashboardInfo: any; - workspaceMemberInfo: any; + workspaceMemberInfo: IWorkspaceMemberMe | null; hasPermissionToWorkspace: boolean | null; projectMemberInfo: IProjectMember | null; @@ -27,7 +27,7 @@ export interface IUserStore { fetchCurrentUser: () => Promise; fetchCurrentUserSettings: () => Promise; - fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise; + fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise; fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise; fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise; @@ -45,7 +45,7 @@ class UserStore implements IUserStore { dashboardInfo: any = null; - workspaceMemberInfo: any = null; + workspaceMemberInfo: IWorkspaceMemberMe | null = null; hasPermissionToWorkspace: boolean | null = null; projectMemberInfo: IProjectMember | null = null; diff --git a/web/store/workspace/workspace.store.ts b/web/store/workspace/workspace.store.ts index 9b27d255c..28e901928 100644 --- a/web/store/workspace/workspace.store.ts +++ b/web/store/workspace/workspace.store.ts @@ -26,6 +26,15 @@ export interface IWorkspaceStore { fetchWorkspaceLabels: (workspaceSlug: string) => Promise; fetchWorkspaceMembers: (workspaceSlug: string) => Promise; + // workspace write operations + createWorkspace: (data: Partial) => Promise; + updateWorkspace: (workspaceSlug: string, data: Partial) => Promise; + deleteWorkspace: (workspaceSlug: string) => Promise; + + // members write operations + updateMember: (workspaceSlug: string, memberId: string, data: Partial) => Promise; + removeMember: (workspaceSlug: string, memberId: string) => Promise; + // computed currentWorkspace: IWorkspace | null; workspaceLabels: IIssueLabels[] | null; @@ -72,6 +81,15 @@ export class WorkspaceStore implements IWorkspaceStore { fetchWorkspaceLabels: action, fetchWorkspaceMembers: action, + // workspace write operations + createWorkspace: action, + updateWorkspace: action, + deleteWorkspace: action, + + // members write operations + updateMember: action, + removeMember: action, + // computed currentWorkspace: computed, workspaceLabels: computed, @@ -189,7 +207,6 @@ export class WorkspaceStore implements IWorkspaceStore { * fetch workspace members using workspace slug * @param workspaceSlug */ - fetchWorkspaceMembers = async (workspaceSlug: string) => { try { runInAction(() => { @@ -214,4 +231,174 @@ export class WorkspaceStore implements IWorkspaceStore { }); } }; + + /** + * create workspace using the workspace data + * @param data + */ + createWorkspace = async (data: Partial) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.workspaceService.createWorkspace(data, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.workspaces = [...this.workspaces, response]; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * update workspace using the workspace slug and new workspace data + * @param workspaceSlug + * @param data + */ + updateWorkspace = async (workspaceSlug: string, data: Partial) => { + const newWorkspaces = this.workspaces?.map((w) => (w.slug === workspaceSlug ? { ...w, ...data } : w)); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + const response = await this.workspaceService.updateWorkspace(workspaceSlug, data, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.workspaces = newWorkspaces; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * delete workspace using the workspace slug + * @param workspaceSlug + */ + deleteWorkspace = async (workspaceSlug: string) => { + const newWorkspaces = this.workspaces?.filter((w) => w.slug !== workspaceSlug); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const user = this.rootStore.user.currentUser ?? undefined; + + await this.workspaceService.deleteWorkspace(workspaceSlug, user); + + runInAction(() => { + this.loader = false; + this.error = null; + this.workspaces = newWorkspaces; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * update workspace member using workspace slug and member id and data + * @param workspaceSlug + * @param memberId + * @param data + */ + updateMember = async (workspaceSlug: string, memberId: string, data: Partial) => { + const members = this.members?.[workspaceSlug]; + members?.map((m) => (m.id === memberId ? { ...m, ...data } : m)); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data); + + runInAction(() => { + this.loader = false; + this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + /** + * remove workspace member using workspace slug and member id + * @param workspaceSlug + * @param memberId + */ + removeMember = async (workspaceSlug: string, memberId: string) => { + const members = this.members?.[workspaceSlug]; + members?.filter((m) => m.id !== memberId); + + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberId); + + runInAction(() => { + this.loader = false; + this.error = null; + this.members = { + ...this.members, + [workspaceSlug]: members, + }; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; } diff --git a/web/store/workspace/workspace_filters.store.ts b/web/store/workspace/workspace_filters.store.ts index d33a56057..048ee6b07 100644 --- a/web/store/workspace/workspace_filters.store.ts +++ b/web/store/workspace/workspace_filters.store.ts @@ -9,7 +9,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, - IWorkspaceMember, + IWorkspaceMemberMe, IWorkspaceViewProps, TIssueParams, } from "types"; @@ -25,7 +25,7 @@ export interface IWorkspaceFilterStore { workspaceDisplayProperties: IIssueDisplayProperties; // actions - fetchUserWorkspaceFilters: (workspaceSlug: string) => Promise; + fetchUserWorkspaceFilters: (workspaceSlug: string) => Promise; updateWorkspaceFilters: (workspaceSlug: string, filterToUpdate: Partial) => Promise; // computed diff --git a/web/types/users.d.ts b/web/types/users.d.ts index 6ffeedf8b..252f4fe80 100644 --- a/web/types/users.d.ts +++ b/web/types/users.d.ts @@ -56,6 +56,7 @@ export interface IUserLite { avatar: string; created_at: Date; display_name: string; + email?: string; first_name: string; readonly id: string; is_bot: boolean; diff --git a/web/types/workspace.d.ts b/web/types/workspace.d.ts index 66e1e1273..004c1ad58 100644 --- a/web/types/workspace.d.ts +++ b/web/types/workspace.d.ts @@ -1,4 +1,4 @@ -import type { IProjectMember, IUser, IUserMemberLite, IWorkspaceViewProps } from "types"; +import type { IProjectMember, IUser, IUserLite, IUserMemberLite, IWorkspaceViewProps } from "types"; export interface IWorkspace { readonly id: string; @@ -56,16 +56,29 @@ export type Properties = { }; export interface IWorkspaceMember { - readonly id: string; - workspace: IWorkspace; - member: IUserMemberLite; - role: 5 | 10 | 15 | 20; company_role: string | null; - view_props: IWorkspaceViewProps; created_at: Date; - updated_at: Date; created_by: string; + id: string; + member: IUserLite; + role: 5 | 10 | 15 | 20; + updated_at: Date; updated_by: string; + workspace: IWorkspaceLite; +} + +export interface IWorkspaceMemberMe { + company_role: string | null; + created_at: Date; + created_by: string; + default_props: IWorkspaceViewProps; + id: string; + member: string; + role: 5 | 10 | 15 | 20; + updated_at: Date; + updated_by: string; + view_props: IWorkspaceViewProps; + workspace: string; } export interface ILastActiveWorkspaceDetails { From 52474715de2b5edaa4d74f8279aff9106568d06d Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 31 Oct 2023 12:04:36 +0530 Subject: [PATCH 04/86] chore: handled next_url redirection issue (#2562) --- web/components/page-views/signin.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index 23a1ea634..e0be15e31 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -32,6 +32,8 @@ export const SignInView = observer(() => { const { fetchCurrentUserSettings } = userStore; // router const router = useRouter(); + const { next: next_url } = router.query as { next: string }; + // states const [isLoading, setLoading] = useState(false); // toast @@ -46,15 +48,17 @@ export const SignInView = observer(() => { useEffect(() => { fetchCurrentUserSettings().then((settings) => { setLoading(true); - router.push( - `/${ - settings.workspace.last_workspace_slug - ? settings.workspace.last_workspace_slug - : settings.workspace.fallback_workspace_slug - }` - ); + if (next_url) router.push(next_url); + else + router.push( + `/${ + settings.workspace.last_workspace_slug + ? settings.workspace.last_workspace_slug + : settings.workspace.fallback_workspace_slug + }` + ); }); - }, [fetchCurrentUserSettings, router]); + }, [fetchCurrentUserSettings, router, next_url]); const handleLoginRedirection = () => { userStore.fetchCurrentUser().then((user) => { @@ -65,7 +69,8 @@ export const SignInView = observer(() => { .then((userSettings: IUserSettings) => { const workspaceSlug = userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug; - if (workspaceSlug) router.push(`/${workspaceSlug}`); + if (next_url) router.push(next_url); + else if (workspaceSlug) router.push(`/${workspaceSlug}`); else if (userSettings.workspace.invites > 0) router.push("/invitations"); else router.push("/create-workspace"); }) From 8b7b5c54b9e795c7987a6561e80bca41ccf32cdf Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:06:11 +0530 Subject: [PATCH 05/86] fix: global views bugs (#2563) --- web/components/headers/global-issues.tsx | 1 + .../issues/issue-layouts/kanban/block.tsx | 6 ++-- .../issue-layouts/kanban/blocks-list.tsx | 8 ++--- .../issues/issue-layouts/kanban/default.tsx | 24 ++++++------- .../issue-layouts/kanban/properties.tsx | 34 ++++++++++++------- .../issue-layouts/kanban/roots/cycle-root.tsx | 6 ++-- .../kanban/roots/module-root.tsx | 6 ++-- .../kanban/roots/profile-issues-root.tsx | 6 ++-- .../kanban/roots/project-root.tsx | 6 ++-- .../issues/issue-layouts/kanban/swimlanes.tsx | 24 ++++++------- .../roots/global-view-layout-root.tsx | 14 +++++--- .../roots/project-layout-root.tsx | 2 ++ .../spreadsheet/columns/created-on-column.tsx | 2 +- .../spreadsheet/columns/updated-on-column.tsx | 2 +- web/components/workspace/views/form.tsx | 1 + .../global-view/global_view_issues.store.ts | 27 +++++++++++++++ 16 files changed, 106 insertions(+), 63 deletions(-) diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index b80dd45a1..5f0f1c054 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -134,6 +134,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} labels={workspaceStore.workspaceLabels ?? undefined} + members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} /> diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 3cbb4e7d5..51197c45d 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd"; // components import { KanBanProperties } from "./properties"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; +import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; interface IssueBlockProps { sub_group_id: string; @@ -17,7 +17,7 @@ interface IssueBlockProps { action: "update" | "delete" ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: any; + displayProperties: IIssueDisplayProperties; states: IState[] | null; labels: IIssueLabels[] | null; members: IUserLite[] | null; @@ -81,7 +81,7 @@ export const KanbanIssueBlock: React.FC = (props) => { columnId={columnId} issue={issue} handleIssues={updateIssue} - display_properties={displayProperties} + displayProperties={displayProperties} states={states} labels={labels} members={members} diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 0e921638a..7c6e55f81 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -1,6 +1,6 @@ // components import { KanbanIssueBlock } from "components/issues"; -import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; +import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; interface IssueBlocksListProps { sub_group_id: string; @@ -14,7 +14,7 @@ interface IssueBlocksListProps { action: "update" | "delete" ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; states: IState[] | null; labels: IIssueLabels[] | null; members: IUserLite[] | null; @@ -29,7 +29,7 @@ export const KanbanIssueBlocksList: React.FC = (props) => isDragDisabled, handleIssues, quickActions, - display_properties, + displayProperties, states, labels, members, @@ -47,7 +47,7 @@ export const KanbanIssueBlocksList: React.FC = (props) => issue={issue} handleIssues={handleIssues} quickActions={quickActions} - displayProperties={display_properties} + displayProperties={displayProperties} columnId={columnId} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 354cf011f..f441d3e22 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; +import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; @@ -26,7 +26,7 @@ export interface IGroupByKanBan { action: "update" | "delete" ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; enableQuickIssueCreate?: boolean; @@ -48,7 +48,7 @@ const GroupByKanBan: React.FC = observer((props) => { isDragDisabled, handleIssues, quickActions, - display_properties, + displayProperties, kanBanToggle, handleKanBanToggle, states, @@ -104,7 +104,7 @@ const GroupByKanBan: React.FC = observer((props) => { isDragDisabled={isDragDisabled} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} states={states} labels={labels} members={members} @@ -151,7 +151,7 @@ export interface IKanBan { action: "update" | "delete" ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; states: IState[] | null; @@ -172,7 +172,7 @@ export const KanBan: React.FC = observer((props) => { sub_group_id = "null", handleIssues, quickActions, - display_properties, + displayProperties, kanBanToggle, handleKanBanToggle, states, @@ -200,7 +200,7 @@ export const KanBan: React.FC = observer((props) => { isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} @@ -223,7 +223,7 @@ export const KanBan: React.FC = observer((props) => { isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} @@ -246,7 +246,7 @@ export const KanBan: React.FC = observer((props) => { isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} @@ -269,7 +269,7 @@ export const KanBan: React.FC = observer((props) => { isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} @@ -292,7 +292,7 @@ export const KanBan: React.FC = observer((props) => { isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} @@ -315,7 +315,7 @@ export const KanBan: React.FC = observer((props) => { isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index e321094d4..4ab4613aa 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -10,14 +10,22 @@ import { IssuePropertyAssignee } from "../properties/assignee"; import { IssuePropertyEstimates } from "../properties/estimates"; import { IssuePropertyDate } from "../properties/date"; import { Tooltip } from "@plane/ui"; -import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types"; +import { + IEstimatePoint, + IIssue, + IIssueDisplayProperties, + IIssueLabels, + IState, + IUserLite, + TIssuePriorities, +} from "types"; export interface IKanBanProperties { sub_group_id: string; columnId: string; issue: IIssue; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void; - display_properties: any; + displayProperties: IIssueDisplayProperties; states: IState[] | null; labels: IIssueLabels[] | null; members: IUserLite[] | null; @@ -30,7 +38,7 @@ export const KanBanProperties: React.FC = observer((props) => columnId: group_id, issue, handleIssues, - display_properties, + displayProperties, states, labels, members, @@ -97,7 +105,7 @@ export const KanBanProperties: React.FC = observer((props) =>
{/* basic properties */} {/* state */} - {display_properties && display_properties?.state && ( + {displayProperties && displayProperties?.state && ( = observer((props) => )} {/* priority */} - {display_properties && display_properties?.priority && ( + {displayProperties && displayProperties?.priority && ( = observer((props) => )} {/* label */} - {display_properties && display_properties?.labels && ( + {displayProperties && displayProperties?.labels && ( = observer((props) => )} {/* assignee */} - {display_properties && display_properties?.assignee && ( + {displayProperties && displayProperties?.assignee && ( = observer((props) => )} {/* start date */} - {display_properties && display_properties?.start_date && ( + {displayProperties && displayProperties?.start_date && ( handleStartDate(date)} @@ -150,7 +158,7 @@ export const KanBanProperties: React.FC = observer((props) => )} {/* target/due date */} - {display_properties && display_properties?.due_date && ( + {displayProperties && displayProperties?.due_date && ( handleTargetDate(date)} @@ -160,7 +168,7 @@ export const KanBanProperties: React.FC = observer((props) => )} {/* estimates */} - {display_properties && display_properties?.estimate && ( + {displayProperties && displayProperties?.estimate && ( = observer((props) => {/* extra render properties */} {/* sub-issues */} - {display_properties && display_properties?.sub_issue_count && ( + {displayProperties && displayProperties?.sub_issue_count && (
@@ -182,7 +190,7 @@ export const KanBanProperties: React.FC = observer((props) => )} {/* attachments */} - {display_properties && display_properties?.attachment_count && ( + {displayProperties && displayProperties?.attachment_count && (
@@ -192,7 +200,7 @@ export const KanBanProperties: React.FC = observer((props) => )} {/* link */} - {display_properties && display_properties?.link && ( + {displayProperties && displayProperties?.link && (
diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 188e27a68..ed54b0213 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -35,7 +35,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by ? "swimlanes" @@ -113,7 +113,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -138,7 +138,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 754693d11..cebfc8f81 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -35,7 +35,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by ? "swimlanes" @@ -113,7 +113,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -138,7 +138,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index d2346120a..eaa37bb91 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -34,7 +34,7 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; - const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; + const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null; const currentKanBanView: "swimlanes" | "default" = profileIssueFiltersStore?.userDisplayFilters?.sub_group_by ? "swimlanes" @@ -96,7 +96,7 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -120,7 +120,7 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index ab087e5c8..7a003842c 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -35,7 +35,7 @@ export const KanBanLayout: React.FC = observer(() => { const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - const display_properties = issueFilterStore?.userDisplayProperties || null; + const displayProperties = issueFilterStore?.userDisplayProperties || null; const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by ? "swimlanes" @@ -103,7 +103,7 @@ export const KanBanLayout: React.FC = observer(() => { handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -128,7 +128,7 @@ export const KanBanLayout: React.FC = observer(() => { handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} /> )} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 7a3126cb2..104f89dc3 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -7,7 +7,7 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBan } from "./default"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; +import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; @@ -73,7 +73,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { action: "update" | "delete" ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; states: IState[] | null; @@ -93,7 +93,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { listKey, handleIssues, quickActions, - display_properties, + displayProperties, kanBanToggle, handleKanBanToggle, states, @@ -143,7 +143,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { sub_group_id={getValueFromObject(_list, listKey) as string} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -174,7 +174,7 @@ export interface IKanBanSwimLanes { action: "update" | "delete" ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - display_properties: any; + displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; states: IState[] | null; @@ -193,7 +193,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { group_by, handleIssues, quickActions, - display_properties, + displayProperties, kanBanToggle, handleKanBanToggle, states, @@ -322,7 +322,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -344,7 +344,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`key`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -366,7 +366,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`key`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -388,7 +388,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -410,7 +410,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`member.id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} @@ -432,7 +432,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`member.id`} handleIssues={handleIssues} quickActions={quickActions} - display_properties={display_properties} + displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} states={states} diff --git a/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx index e7adcf0e9..77b3b6ee5 100644 --- a/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/global-view-layout-root.tsx @@ -25,6 +25,7 @@ export const GlobalViewLayoutRoot: React.FC = observer((props) => { globalViewFilters: globalViewFiltersStore, workspaceFilter: workspaceFilterStore, workspace: workspaceStore, + issueDetail: issueDetailStore, } = useMobxStore(); const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined; @@ -62,14 +63,17 @@ export const GlobalViewLayoutRoot: React.FC = observer((props) => { const handleUpdateIssue = useCallback( (issue: IIssue, data: Partial) => { - if (!workspaceSlug) return; + if (!workspaceSlug || !globalViewId) return; - console.log("issue", issue); - console.log("data", data); + const payload = { + ...issue, + ...data, + }; - // TODO: add update issue logic here + globalViewIssuesStore.updateIssueStructure(globalViewId.toString(), payload); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, data); }, - [workspaceSlug] + [globalViewId, globalViewIssuesStore, workspaceSlug, issueDetailStore] ); const issues = type diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index 0cd5911a5..1826eb344 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -33,6 +33,8 @@ export const ProjectLayoutRoot: React.FC = observer(() => { const issueCount = issueStore.getIssuesCount; + console.log("issueCount", issueCount); + return (
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx index 54a8ffee5..b1a7c3fef 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx @@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC = ({ issue, expandedIss return ( <> - {renderLongDetailDateFormat(issue.created_at)} +
{renderLongDetailDateFormat(issue.created_at)}
{isExpanded && !isLoading && diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx index b2892f2a9..225657d34 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx @@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC = (props) => { return ( <> - {renderLongDetailDateFormat(issue.updated_at)} +
{renderLongDetailDateFormat(issue.updated_at)}
{isExpanded && !isLoading && diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx index 5524555e0..8b5112eb9 100644 --- a/web/components/workspace/views/form.tsx +++ b/web/components/workspace/views/form.tsx @@ -143,6 +143,7 @@ export const WorkspaceViewForm: React.FC = observer((props) => { }} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} labels={workspaceStore.workspaceLabels ?? undefined} + members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} /> diff --git a/web/store/global-view/global_view_issues.store.ts b/web/store/global-view/global_view_issues.store.ts index dc8e9717b..006c9b380 100644 --- a/web/store/global-view/global_view_issues.store.ts +++ b/web/store/global-view/global_view_issues.store.ts @@ -7,6 +7,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; // types import { RootStore } from "../root"; import { IIssue, IIssueFilterOptions, TStaticViewTypes } from "types"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; export interface IGlobalViewIssuesStore { // states @@ -21,6 +22,7 @@ export interface IGlobalViewIssuesStore { // actions fetchViewIssues: (workspaceSlug: string, viewId: string, filters: IIssueFilterOptions) => Promise; fetchStaticIssues: (workspaceSlug: string, type: TStaticViewTypes) => Promise; + updateIssueStructure: (viewId: string, issue: IIssue) => Promise; } export class GlobalViewIssuesStore implements IGlobalViewIssuesStore { @@ -52,6 +54,7 @@ export class GlobalViewIssuesStore implements IGlobalViewIssuesStore { // actions fetchViewIssues: action, fetchStaticIssues: action, + updateIssueStructure: action, }); this.rootStore = _rootStore; @@ -174,4 +177,28 @@ export class GlobalViewIssuesStore implements IGlobalViewIssuesStore { throw error; } }; + + updateIssueStructure = async (viewId: string, issue: IIssue) => { + let issues = this.viewIssues[viewId]; + + if (!issues) return null; + + const _currentIssueId = issues?.find((_i) => _i?.id === issue.id); + issues = _currentIssueId + ? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) + : [...(issues ?? []), issue]; + + const orderBy = this.rootStore?.workspaceFilter?.workspaceDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") issues = sortArrayByDate(issues as any, "created_at"); + + if (orderBy === "-updated_at") issues = sortArrayByDate(issues as any, "updated_at"); + + if (orderBy === "start_date") issues = sortArrayByDate(issues as any, "updated_at"); + + if (orderBy === "priority") issues = sortArrayByPriority(issues as any, "priority"); + + runInAction(() => { + this.viewIssues = { ...this.viewIssues, [viewId]: issues }; + }); + }; } From 1c2ea6da5ee0085caa359b549e1e6131ffd0919e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:06:55 +0530 Subject: [PATCH 06/86] fix: edit project button redirection (#2564) * fix: redirect to project settings * fix: 404 page button alignment --- web/components/project/card.tsx | 7 ++++++- web/components/project/index.ts | 1 - .../project/{ => settings}/delete-project-section.tsx | 1 + web/components/project/settings/index.ts | 1 + web/pages/404.tsx | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) rename web/components/project/{ => settings}/delete-project-section.tsx (99%) diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index caab8571a..ca746d02e 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -186,7 +186,12 @@ export const ProjectCard: React.FC = observer((props) => { {(isOwner || isMember) && ( diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 040a0f3df..8fd14b987 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -4,7 +4,6 @@ export * from "./card-list"; export * from "./card"; export * from "./create-project-modal"; export * from "./delete-project-modal"; -export * from "./delete-project-section"; export * from "./form-loader"; export * from "./form"; export * from "./join-project-modal"; diff --git a/web/components/project/delete-project-section.tsx b/web/components/project/settings/delete-project-section.tsx similarity index 99% rename from web/components/project/delete-project-section.tsx rename to web/components/project/settings/delete-project-section.tsx index ccd0bf571..44d4d7f8f 100644 --- a/web/components/project/delete-project-section.tsx +++ b/web/components/project/settings/delete-project-section.tsx @@ -15,6 +15,7 @@ export interface IDeleteProjectSection { export const DeleteProjectSection: React.FC = (props) => { const { projectDetails, handleDelete } = props; + return ( {({ open }) => ( diff --git a/web/components/project/settings/index.ts b/web/components/project/settings/index.ts index 65333a0e2..0bf79ec17 100644 --- a/web/components/project/settings/index.ts +++ b/web/components/project/settings/index.ts @@ -1 +1,2 @@ +export * from "./delete-project-section"; export * from "./features-list"; diff --git a/web/pages/404.tsx b/web/pages/404.tsx index e5f5ee7fd..a1f462f2c 100644 --- a/web/pages/404.tsx +++ b/web/pages/404.tsx @@ -27,7 +27,7 @@ const PageNotFound: NextPage = () => (

- + From 08ca016f657a0b15e0d8259190260e2badda0a78 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:12:24 +0530 Subject: [PATCH 07/86] fix: custom theme form validations (#2565) --- .../ui/src/form-fields/input-color-picker.tsx | 58 +++-- .../core/theme/custom-theme-selector.tsx | 222 +++++++++++------- 2 files changed, 175 insertions(+), 105 deletions(-) diff --git a/packages/ui/src/form-fields/input-color-picker.tsx b/packages/ui/src/form-fields/input-color-picker.tsx index 738d0e0c8..0a91a8838 100644 --- a/packages/ui/src/form-fields/input-color-picker.tsx +++ b/packages/ui/src/form-fields/input-color-picker.tsx @@ -11,12 +11,14 @@ export interface InputColorPickerProps { value: string | undefined; onChange: (value: string) => void; name: string; - className: string; + className?: string; + style?: React.CSSProperties; placeholder: string; } export const InputColorPicker: React.FC = (props) => { - const { value, hasError, onChange, name, className, placeholder } = props; + const { value, hasError, onChange, name, className, style, placeholder } = + props; const [referenceElement, setReferenceElement] = React.useState(null); @@ -32,12 +34,12 @@ export const InputColorPicker: React.FC = (props) => { onChange(hex); }; - const handleInputChange = (value: any) => { - onChange(value); + const handleInputChange = (e: React.ChangeEvent) => { + onChange(e.target.value); }; return ( -
+
= (props) => { onChange={handleInputChange} hasError={hasError} placeholder={placeholder} - className={`border-none ${className}`} + className={`border-[0.5px] border-custom-border-200 ${className}`} + style={style} /> - + {({ open }) => { if (open) { } @@ -60,26 +66,26 @@ export const InputColorPicker: React.FC = (props) => { ref={setReferenceElement} variant="neutral-primary" size="sm" - className="border-none !p-1.5" + className="border-none !bg-transparent" > - {value && value !== "" ? ( - - ) : ( - - - - )} + + + + + + + = observer(() => { +export const CustomThemeSelector: React.FC = observer(() => { const { user: userStore } = useMobxStore(); const userTheme = userStore?.currentUser?.theme; // hooks const { setTheme } = useTheme(); const { + control, formState: { errors, isSubmitting }, handleSubmit, - control, + watch, } = useForm({ defaultValues: { background: userTheme?.background !== "" ? userTheme?.background : "#0d101b", @@ -51,100 +64,151 @@ export const CustomThemeSelector: FC = observer(() => { return userStore.updateCurrentUser({ theme: payload }); }; + const handleValueChange = (val: string | undefined, onChange: any) => { + let hex = val; + + // prepend a hashtag if it doesn't exist + if (val && val[0] !== "#") hex = `#${val}`; + + onChange(hex); + }; + return (

Customize your theme

-
+

Background color

- ( - - )} - /> +
+ ( + handleValueChange(val, onChange)} + placeholder="#0d101b" + className="w-full" + style={{ + backgroundColor: value, + color: watch("text"), + }} + hasError={Boolean(errors?.background)} + /> + )} + /> + {errors.background &&

{errors.background.message}

} +

Text color

- ( - - )} - /> +
+ ( + handleValueChange(val, onChange)} + placeholder="#c5c5c5" + className="w-full" + style={{ + backgroundColor: watch("background"), + color: value, + }} + hasError={Boolean(errors?.text)} + /> + )} + /> + {errors.text &&

{errors.text.message}

} +

Primary(Theme) color

- ( - - )} - /> +
+ ( + handleValueChange(val, onChange)} + placeholder="#3f76ff" + className="w-full" + style={{ + backgroundColor: value, + color: watch("text"), + }} + hasError={Boolean(errors?.primary)} + /> + )} + /> + {errors.primary &&

{errors.primary.message}

} +

Sidebar background color

- ( - +
+ ( + handleValueChange(val, onChange)} + placeholder="#0d101b" + className="w-full" + style={{ + backgroundColor: value, + color: watch("sidebarText"), + }} + hasError={Boolean(errors?.sidebarBackground)} + /> + )} + /> + {errors.sidebarBackground && ( +

{errors.sidebarBackground.message}

)} - /> +

Sidebar text color

- ( - - )} - /> +
+ ( + handleValueChange(val, onChange)} + placeholder="#c5c5c5" + className="w-full" + style={{ + backgroundColor: watch("sidebarBackground"), + color: value, + }} + hasError={Boolean(errors?.sidebarText)} + /> + )} + /> + {errors.sidebarText &&

{errors.sidebarText.message}

} +
From 59c52023fbeb90f9c7615d645707f4decefdbf27 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:14:06 +0530 Subject: [PATCH 08/86] style: list layout (#2566) --- packages/ui/src/dropdowns/helper.tsx | 1 + web/components/headers/cycle-issues.tsx | 4 +-- web/components/headers/global-issues.tsx | 4 +-- web/components/headers/module-issues.tsx | 4 +-- web/components/headers/project-issues.tsx | 4 +-- .../headers/project-view-issues.tsx | 4 +-- .../filters/header/helpers/dropdown.tsx | 7 +++-- .../issues/issue-layouts/list/block.tsx | 2 +- .../issues/issue-layouts/list/blocks-list.tsx | 12 ++++---- .../issues/issue-layouts/list/default.tsx | 30 +++++++++---------- .../list/inline-create-issue-form.tsx | 6 ++-- .../quick-action-dropdowns/cycle-issue.tsx | 2 +- .../quick-action-dropdowns/module-issue.tsx | 2 +- .../quick-action-dropdowns/project-issue.tsx | 2 +- .../profile/profile-issues-filter.tsx | 4 +-- 15 files changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 4c9a92d4d..eac53b6e6 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -1,3 +1,4 @@ +// FIXME: fix this!!! import { Placement } from "@blueprintjs/popover2"; export interface IDropdownProps { diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index ac6ed2e6b..cfadc80aa 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -147,7 +147,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + { states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> - + = observer((props) => { {activeLayout === "spreadsheet" && ( <> {!STATIC_VIEW_TYPES.some((word) => router.pathname.includes(word)) && ( - + = observer((props) => { )} - + { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + { states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> - + { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + { states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> - + { onChange={(layout) => handleLayoutChange(layout)} selectedLayout={activeLayout} /> - + { states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> - + = (props) => { - const { children, title = "Dropdown" } = props; + const { children, title = "Dropdown", placement } = props; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: "auto", + placement: placement ?? "auto", }); return ( diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 5a84c5f9e..c697da78c 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -27,7 +27,7 @@ export const IssueBlock: React.FC = (props) => { return ( <> -
+
{display_properties && display_properties?.key && (
{issue?.project_detail?.identifier}-{issue.sequence_id} diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 3267e221c..2350fe12c 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -21,9 +21,8 @@ export const IssueBlocksList: FC = (props) => { props; return ( - <> - {issues && - issues?.length > 0 && +
+ {issues && issues.length > 0 ? ( issues.map((issue) => ( = (props) => { members={members} estimates={estimates} /> - ))} - + )) + ) : ( +
No issues
+ )} +
); }; diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 114b308eb..e6fce190a 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -53,7 +53,7 @@ const GroupByList: React.FC = observer((props) => { list.length > 0 && list.map((_list: any) => (
-
+
= observer((props) => { } />
-
- {issues && ( - - )} -
+ {issues && ( + + )} {enableQuickIssueCreate && ( = observer((props) => { }; return ( -
+
= observer((props) => { {!isOpen && ( )} diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 8dc826300..9736c575e 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -62,7 +62,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); }} /> - + { e.preventDefault(); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 066a16d35..93594de9e 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -62,7 +62,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); }} /> - + { e.preventDefault(); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 0cad8c46f..7999be31d 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -61,7 +61,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => { if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); }} /> - + { e.preventDefault(); diff --git a/web/components/profile/profile-issues-filter.tsx b/web/components/profile/profile-issues-filter.tsx index 5dd6b1b3a..7acc73c99 100644 --- a/web/components/profile/profile-issues-filter.tsx +++ b/web/components/profile/profile-issues-filter.tsx @@ -41,7 +41,7 @@ export const ProfileIssuesFilter = observer(() => { selectedLayout={activeLayout} /> - + { /> - + Date: Tue, 31 Oct 2023 12:16:40 +0530 Subject: [PATCH 09/86] chore: update members endpoint (#2569) --- .../integration/github/single-user-select.tsx | 2 +- .../integration/jira/import-users.tsx | 6 +- .../issues/attachment/attachments.tsx | 2 +- web/components/issues/select/assignee.tsx | 2 +- .../issues/sidebar-select/assignee.tsx | 2 +- web/components/modules/select/lead.tsx | 2 +- web/components/modules/select/members.tsx | 2 +- .../modules/sidebar-select/select-lead.tsx | 2 +- .../modules/sidebar-select/select-members.tsx | 2 +- web/components/pages/pages-view.tsx | 2 +- web/components/project/member-select.tsx | 2 +- .../project/send-project-invitation-modal.tsx | 6 +- web/components/ui/avatar.tsx | 2 +- web/components/views/select-filters.tsx | 2 +- web/components/web-view/select-assignee.tsx | 2 +- web/constants/fetch-keys.ts | 27 +-------- web/hooks/use-project-members.tsx | 4 +- web/hooks/use-workspace-members.tsx | 2 +- web/layouts/auth-layout/project-wrapper.tsx | 8 ++- web/layouts/auth-layout/workspace-wrapper.tsx | 6 +- .../projects/[projectId]/settings/members.tsx | 28 +++++----- web/pages/workspace-invitations/index.tsx | 5 +- web/services/project/project.service.ts | 56 ++++++++----------- .../project/project_invitation.service.ts | 10 +--- web/services/workspace.service.ts | 16 +++--- web/store/project/project.store.ts | 14 ++--- web/store/workspace/workspace.store.ts | 4 +- web/types/projects.d.ts | 2 +- 28 files changed, 94 insertions(+), 126 deletions(-) diff --git a/web/components/integration/github/single-user-select.tsx b/web/components/integration/github/single-user-select.tsx index 2bc16d87d..40e743aa8 100644 --- a/web/components/integration/github/single-user-select.tsx +++ b/web/components/integration/github/single-user-select.tsx @@ -44,7 +44,7 @@ export const SingleUserSelect: React.FC = ({ collaborator, index, users, const { data: members } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null, - workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug.toString()) : null + workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null ); const options = members?.map((member) => ({ diff --git a/web/components/integration/jira/import-users.tsx b/web/components/integration/jira/import-users.tsx index c49a3483a..48f9231c2 100644 --- a/web/components/integration/jira/import-users.tsx +++ b/web/components/integration/jira/import-users.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import useSWR from "swr"; import { useFormContext, useFieldArray, Controller } from "react-hook-form"; // fetch keys -import { WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys"; +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; // services import { WorkspaceService } from "services/workspace.service"; // components @@ -30,8 +30,8 @@ export const JiraImportUsers: FC = () => { }); const { data: members } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null, - workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug?.toString() ?? "") : null + workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug?.toString() ?? "") : null, + workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug?.toString() ?? "") : null ); const options = members?.map((member) => ({ diff --git a/web/components/issues/attachment/attachments.tsx b/web/components/issues/attachment/attachments.tsx index 176ff5f7c..7d8872f9c 100644 --- a/web/components/issues/attachment/attachments.tsx +++ b/web/components/issues/attachment/attachments.tsx @@ -44,7 +44,7 @@ export const IssueAttachments = () => { const { data: people } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/issues/select/assignee.tsx b/web/components/issues/select/assignee.tsx index bb2ffcbd4..7e4c5bedb 100644 --- a/web/components/issues/select/assignee.tsx +++ b/web/components/issues/select/assignee.tsx @@ -26,7 +26,7 @@ export const IssueAssigneeSelect: React.FC = ({ projectId, value = [], on const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/issues/sidebar-select/assignee.tsx b/web/components/issues/sidebar-select/assignee.tsx index a56c6176b..09b1af7dd 100644 --- a/web/components/issues/sidebar-select/assignee.tsx +++ b/web/components/issues/sidebar-select/assignee.tsx @@ -28,7 +28,7 @@ export const SidebarAssigneeSelect: React.FC = ({ value, onChange, disabl const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/modules/select/lead.tsx b/web/components/modules/select/lead.tsx index 982acdeb6..86ced41a9 100644 --- a/web/components/modules/select/lead.tsx +++ b/web/components/modules/select/lead.tsx @@ -25,7 +25,7 @@ export const ModuleLeadSelect: React.FC = ({ value, onChange }) => { const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/modules/select/members.tsx b/web/components/modules/select/members.tsx index 49f8ab7db..36aaa869b 100644 --- a/web/components/modules/select/members.tsx +++ b/web/components/modules/select/members.tsx @@ -24,7 +24,7 @@ export const ModuleMembersSelect: React.FC = ({ value, onChange }) => { const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); const options = members?.map((member) => ({ diff --git a/web/components/modules/sidebar-select/select-lead.tsx b/web/components/modules/sidebar-select/select-lead.tsx index 020aad037..271b1dfc9 100644 --- a/web/components/modules/sidebar-select/select-lead.tsx +++ b/web/components/modules/sidebar-select/select-lead.tsx @@ -27,7 +27,7 @@ export const SidebarLeadSelect: FC = (props) => { const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/modules/sidebar-select/select-members.tsx b/web/components/modules/sidebar-select/select-members.tsx index 7b74ef794..35fbcd7a1 100644 --- a/web/components/modules/sidebar-select/select-members.tsx +++ b/web/components/modules/sidebar-select/select-members.tsx @@ -29,7 +29,7 @@ export const SidebarMembersSelect: React.FC = ({ value, onChange }) => { const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/pages/pages-view.tsx b/web/components/pages/pages-view.tsx index f441c4e00..699cbb582 100644 --- a/web/components/pages/pages-view.tsx +++ b/web/components/pages/pages-view.tsx @@ -54,7 +54,7 @@ export const PagesView: React.FC = ({ pages, viewType }) => { const { data: people } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString()) + ? () => projectService.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null ); diff --git a/web/components/project/member-select.tsx b/web/components/project/member-select.tsx index 90b4cb6b3..686568513 100644 --- a/web/components/project/member-select.tsx +++ b/web/components/project/member-select.tsx @@ -30,7 +30,7 @@ export const MemberSelect: React.FC = ({ value, onChange, isDisabled = fa const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index 961e28802..be27088fd 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -67,7 +67,7 @@ const SendProjectInvitationModal: React.FC = (props) => { const { data: people } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null, - workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null + workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug as string) : null ); const { @@ -90,9 +90,11 @@ const SendProjectInvitationModal: React.FC = (props) => { const onSubmit = async (formData: FormValues) => { if (!workspaceSlug || !projectId || isSubmitting) return; + const payload = { ...formData }; + await projectService - .inviteProject(workspaceSlug as string, projectId as string, payload, user) + .bulkAddMembersToProject(workspaceSlug.toString(), projectId.toString(), payload, user) .then(() => { setIsOpen(false); setToastAlert({ diff --git a/web/components/ui/avatar.tsx b/web/components/ui/avatar.tsx index 44997f807..a28b449ba 100644 --- a/web/components/ui/avatar.tsx +++ b/web/components/ui/avatar.tsx @@ -95,7 +95,7 @@ export const AssigneesList: React.FC = ({ const { data: people } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null, - workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null + workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug as string) : null ); if ((users && users.length === 0) || (userIds && userIds.length === 0)) diff --git a/web/components/views/select-filters.tsx b/web/components/views/select-filters.tsx index b566bb96e..b032b1057 100644 --- a/web/components/views/select-filters.tsx +++ b/web/components/views/select-filters.tsx @@ -54,7 +54,7 @@ export const SelectFilters: React.FC = ({ filters, onSelect, direction = const { data: members } = useSWR( projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/components/web-view/select-assignee.tsx b/web/components/web-view/select-assignee.tsx index 877452579..1848b944f 100644 --- a/web/components/web-view/select-assignee.tsx +++ b/web/components/web-view/select-assignee.tsx @@ -33,7 +33,7 @@ export const AssigneeSelect: React.FC = (props) => { const { data: members } = useSWR( workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string) : null ); diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 61c899ee2..fc1b5544d 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -47,19 +47,6 @@ const paramsToKey = (params: any) => { return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}_${subscriberKey}`; }; -const inboxParamsToKey = (params: any) => { - const { priority, inbox_status } = params; - - let priorityKey = priority ? priority.split(",") : []; - let inboxStatusKey = inbox_status ? inbox_status.split(",") : []; - - // sorting each keys in ascending order - priorityKey = priorityKey.sort().join("_"); - inboxStatusKey = inboxStatusKey.sort().join("_"); - - return `${priorityKey}_${inboxStatusKey}`; -}; - const myIssuesParamsToKey = (params: any) => { const { assignees, created_by, labels, priority, state_group, subscriber, start_date, target_date } = params; @@ -93,13 +80,9 @@ export const USER_WORKSPACES = "USER_WORKSPACES"; export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`; export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug.toUpperCase()}`; -export const WORKSPACE_MEMBERS_WITH_EMAIL = (workspaceSlug: string) => - `WORKSPACE_MEMBERS_WITH_EMAIL_${workspaceSlug.toUpperCase()}`; export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) => `WORKSPACE_MEMBERS_ME${workspaceSlug.toUpperCase()}`; -export const WORKSPACE_INVITATIONS = "WORKSPACE_INVITATIONS"; -export const WORKSPACE_INVITATION_WITH_EMAIL = (workspaceSlug: string) => - `WORKSPACE_INVITATION_WITH_EMAIL_${workspaceSlug.toUpperCase()}`; -export const WORKSPACE_INVITATION = "WORKSPACE_INVITATION"; +export const WORKSPACE_INVITATIONS = (workspaceSlug: string) => `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`; +export const WORKSPACE_INVITATION = (invitationId: string) => `WORKSPACE_INVITATION_${invitationId}`; export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS"; export const PROJECTS_LIST = ( @@ -115,11 +98,7 @@ export const PROJECTS_LIST = ( export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`; export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`; -export const PROJECT_MEMBERS_WITH_EMAIL = (workspaceSlug: string, projectId: string) => - `PROJECT_MEMBERS_WITH_EMAIL_${workspaceSlug}_${projectId.toUpperCase()}`; -export const PROJECT_INVITATIONS = "PROJECT_INVITATIONS"; -export const PROJECT_INVITATIONS_WITH_EMAIL = (workspaceSlug: string, projectId: string) => - `PROJECT_INVITATIONS_WITH_EMAIL_${workspaceSlug}_${projectId.toUpperCase()}`; +export const PROJECT_INVITATIONS = (projectId: string) => `PROJECT_INVITATIONS_${projectId.toString()}`; export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) => `PROJECT_ISSUES_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}`; diff --git a/web/hooks/use-project-members.tsx b/web/hooks/use-project-members.tsx index 4b7abe641..298d0b3de 100644 --- a/web/hooks/use-project-members.tsx +++ b/web/hooks/use-project-members.tsx @@ -19,7 +19,9 @@ const useProjectMembers = ( // fetching project members const { data: members } = useSWR( workspaceSlug && projectId && fetchCondition ? PROJECT_MEMBERS(projectId) : null, - workspaceSlug && projectId && fetchCondition ? () => projectService.projectMembers(workspaceSlug, projectId) : null + workspaceSlug && projectId && fetchCondition + ? () => projectService.fetchProjectMembers(workspaceSlug, projectId) + : null ); const hasJoined = members?.some((item: any) => item.member.id === (user as any)?.id); diff --git a/web/hooks/use-workspace-members.tsx b/web/hooks/use-workspace-members.tsx index e6fa6d2e3..b486ac13e 100644 --- a/web/hooks/use-workspace-members.tsx +++ b/web/hooks/use-workspace-members.tsx @@ -15,7 +15,7 @@ const useWorkspaceMembers = (workspaceSlug: string | undefined, fetchCondition?: const { data: workspaceMembers, error: workspaceMemberErrors } = useSWR( workspaceSlug && fetchCondition ? WORKSPACE_MEMBERS(workspaceSlug) : null, - workspaceSlug && fetchCondition ? () => workspaceService.workspaceMembers(workspaceSlug) : null + workspaceSlug && fetchCondition ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug) : null ); const hasJoined = workspaceMembers?.some((item: any) => item.member.id === (user as any)?.id); diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index 91645222e..688d97129 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -38,7 +38,7 @@ export const ProjectAuthWrapper: FC = observer((props) => { : null ); // fetching user project member information - useSWR( + const { data: projectMemberInfo } = useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) @@ -53,8 +53,10 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // fetching project members useSWR( - workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId + workspaceSlug && projectId && projectMemberInfo && [20, 15].includes(projectMemberInfo.role) + ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` + : null, + workspaceSlug && projectId && projectMemberInfo && [20, 15].includes(projectMemberInfo.role) ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null ); diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index e46515bf7..d218bf68c 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -12,8 +12,6 @@ export interface IWorkspaceAuthWrapper { children: ReactNode; } -const HIGHER_ROLES = [20, 15]; - export const WorkspaceAuthWrapper: FC = observer((props) => { const { children } = props; // store @@ -35,10 +33,10 @@ export const WorkspaceAuthWrapper: FC = observer((props) ); // fetch workspace members useSWR( - workspaceSlug && workspaceMemberInfo && HIGHER_ROLES.includes(workspaceMemberInfo.role) + workspaceSlug && workspaceMemberInfo && [20, 15].includes(workspaceMemberInfo.role) ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, - workspaceSlug && workspaceMemberInfo && HIGHER_ROLES.includes(workspaceMemberInfo.role) + workspaceSlug && workspaceMemberInfo && [20, 15].includes(workspaceMemberInfo.role) ? () => workspaceStore.fetchWorkspaceMembers(workspaceSlug.toString()) : null ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index 7a0b34197..c55b33710 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -30,8 +30,8 @@ import { IProject, IUserLite, IWorkspace } from "types"; import { PROJECTS_LIST, PROJECT_DETAILS, - PROJECT_INVITATIONS_WITH_EMAIL, - PROJECT_MEMBERS_WITH_EMAIL, + PROJECT_INVITATIONS, + PROJECT_MEMBERS, USER_PROJECT_VIEW, WORKSPACE_DETAILS, } from "constants/fetch-keys"; @@ -68,21 +68,21 @@ const MembersSettings: NextPage = () => { const { reset, control } = useForm({ defaultValues }); - const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () => - workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null + const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug.toString()) : null, () => + workspaceSlug ? workspaceService.getWorkspace(workspaceSlug.toString()) : null ); const { data: projectMembers, mutate: mutateMembers } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString()) : null, + workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null, workspaceSlug && projectId - ? () => projectService.projectMembersWithEmail(workspaceSlug as string, projectId as string) + ? () => projectService.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null ); const { data: projectInvitations, mutate: mutateInvitations } = useSWR( - workspaceSlug && projectId ? PROJECT_INVITATIONS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString()) : null, + workspaceSlug && projectId ? PROJECT_INVITATIONS(projectId.toString()) : null, workspaceSlug && projectId - ? () => projectInvitationService.projectInvitationsWithEmail(workspaceSlug as string, projectId as string) + ? () => projectInvitationService.fetchProjectInvitations(workspaceSlug.toString(), projectId.toString()) : null ); @@ -133,12 +133,12 @@ const MembersSettings: NextPage = () => { // }; // await projectService - // .updateProject(workspaceSlug as string, projectId as string, payload, user) + // .updateProject(workspaceSlug.toString(), projectId.toString(), payload, user) // .then((res) => { - // mutate(PROJECT_DETAILS(projectId as string)); + // mutate(PROJECT_DETAILS(projectId.toString())); // mutate( - // PROJECTS_LIST(workspaceSlug as string, { + // PROJECTS_LIST(workspaceSlug.toString(), { // is_favorite: "all", // }) // ); @@ -173,12 +173,12 @@ const MembersSettings: NextPage = () => { }; await projectService - .updateProject(workspaceSlug as string, projectId as string, payload, user) + .updateProject(workspaceSlug.toString(), projectId.toString(), payload, user) .then(() => { - mutate(PROJECT_DETAILS(projectId as string)); + mutate(PROJECT_DETAILS(projectId.toString())); mutate( - PROJECTS_LIST(workspaceSlug as string, { + PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all", }) ); diff --git a/web/pages/workspace-invitations/index.tsx b/web/pages/workspace-invitations/index.tsx index 7288a669c..ea671a4a9 100644 --- a/web/pages/workspace-invitations/index.tsx +++ b/web/pages/workspace-invitations/index.tsx @@ -30,8 +30,9 @@ const WorkspaceInvitation: NextPage = () => { const { user } = useUser(); - const { data: invitationDetail, error } = useSWR(invitation_id && WORKSPACE_INVITATION, () => - invitation_id ? workspaceService.getWorkspaceInvitation(invitation_id as string) : null + const { data: invitationDetail, error } = useSWR( + invitation_id && WORKSPACE_INVITATION(invitation_id.toString()), + () => (invitation_id ? workspaceService.getWorkspaceInvitation(invitation_id as string) : null) ); const handleAccept = () => { diff --git a/web/services/project/project.service.ts b/web/services/project/project.service.ts index e1309fafa..1c824a40c 100644 --- a/web/services/project/project.service.ts +++ b/web/services/project/project.service.ts @@ -7,7 +7,7 @@ import type { GithubRepositoriesResponse, IUser, IProject, - IProjectBulkInviteFormData, + IProjectBulkAddFormData, IProjectMember, ISearchIssueResponse, ProjectPreferences, @@ -83,32 +83,6 @@ export class ProjectService extends APIService { }); } - async inviteProject( - workspaceSlug: string, - projectId: string, - data: IProjectBulkInviteFormData, - user: IUser | undefined - ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data) - .then((response) => { - trackEventService.trackProjectEvent( - { - workspaceId: response?.data?.workspace?.id, - workspaceSlug, - projectId, - projectName: response?.data?.project?.name, - memberEmail: response?.data?.member?.email, - }, - "PROJECT_MEMBER_INVITE", - user as IUser - ); - return response?.data; - }) - .catch((error) => { - throw error?.response?.data; - }); - } - async joinProject(workspaceSlug: string, project_ids: string[]): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/join/`, { project_ids }) .then((response) => response?.data) @@ -136,17 +110,35 @@ export class ProjectService extends APIService { }); } - async projectMembers(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/`) + async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async projectMembersWithEmail(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`) - .then((response) => response?.data) + async bulkAddMembersToProject( + workspaceSlug: string, + projectId: string, + data: IProjectBulkAddFormData, + user: IUser | undefined + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`, data) + .then((response) => { + trackEventService.trackProjectEvent( + { + workspaceId: response?.data?.workspace?.id, + workspaceSlug, + projectId, + projectName: response?.data?.project?.name, + memberEmail: response?.data?.member?.email, + }, + "PROJECT_MEMBER_INVITE", + user as IUser + ); + return response?.data; + }) .catch((error) => { throw error?.response?.data; }); diff --git a/web/services/project/project_invitation.service.ts b/web/services/project/project_invitation.service.ts index 54686dd6a..1fbf6e24c 100644 --- a/web/services/project/project_invitation.service.ts +++ b/web/services/project/project_invitation.service.ts @@ -9,15 +9,7 @@ export class ProjectInvitationService extends APIService { super(API_BASE_URL); } - async projectInvitations(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async projectInvitationsWithEmail(workspaceSlug: string, projectId: string): Promise { + async fetchProjectInvitations(workspaceSlug: string, projectId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/invitations/`) .then((response) => response?.data) .catch((error) => { diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 6d2f9cdf7..30126c2ee 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -132,14 +132,6 @@ export class WorkspaceService extends APIService { }); } - async workspaceMembers(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async workspaceMemberMe(workspaceSlug: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`) .then((response) => response?.data) @@ -156,6 +148,14 @@ export class WorkspaceService extends APIService { }); } + async fetchWorkspaceMembers(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/members/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async updateWorkspaceMember( workspaceSlug: string, memberId: string, diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index b4361ec2f..225ca76bc 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -78,21 +78,21 @@ export class ProjectStore implements IProjectStore { searchQuery: string = ""; projectId: string | null = null; - projects: { [workspaceSlug: string]: IProject[] } = {}; // workspace_id: project[] + projects: { [workspaceSlug: string]: IProject[] } = {}; // workspaceSlug: project[] project_details: { - [key: string]: IProject; // project_id: project + [projectId: string]: IProject; // projectId: project } = {}; states: { - [key: string]: IStateResponse; // project_id: states + [projectId: string]: IStateResponse; // projectId: states } | null = {}; labels: { - [key: string]: IIssueLabels[]; // project_id: labels + [projectId: string]: IIssueLabels[]; // projectId: labels } | null = {}; members: { - [key: string]: IProjectMember[]; // project_id: members + [projectId: string]: IProjectMember[]; // projectId: members } | null = {}; estimates: { - [key: string]: IEstimate[]; // project_id: estimates + [projectId: string]: IEstimate[]; // projectId: estimates } | null = {}; // root store @@ -370,7 +370,7 @@ export class ProjectStore implements IProjectStore { this.loader = true; this.error = null; - const membersResponse = await this.projectService.projectMembers(workspaceSlug, projectId); + const membersResponse = await this.projectService.fetchProjectMembers(workspaceSlug, projectId); const _members = { ...this.members, [projectId]: membersResponse, diff --git a/web/store/workspace/workspace.store.ts b/web/store/workspace/workspace.store.ts index 28e901928..7c6551ec0 100644 --- a/web/store/workspace/workspace.store.ts +++ b/web/store/workspace/workspace.store.ts @@ -49,7 +49,7 @@ export class WorkspaceStore implements IWorkspaceStore { // observables workspaceSlug: string | null = null; workspaces: IWorkspace[] = []; - projects: { [workspaceSlug: string]: IProject[] } = {}; // workspace_id: project[] + projects: { [workspaceSlug: string]: IProject[] } = {}; // workspaceSlug: project[] labels: { [workspaceSlug: string]: IIssueLabels[] } = {}; members: { [workspaceSlug: string]: IWorkspaceMember[] } = {}; @@ -214,7 +214,7 @@ export class WorkspaceStore implements IWorkspaceStore { this.error = null; }); - const membersResponse = await this.workspaceService.workspaceMembers(workspaceSlug); + const membersResponse = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug); runInAction(() => { this.members = { diff --git a/web/types/projects.d.ts b/web/types/projects.d.ts index 5ddfd64eb..f1f69af2a 100644 --- a/web/types/projects.d.ts +++ b/web/types/projects.d.ts @@ -108,7 +108,7 @@ export interface IProjectMemberInvitation { updated_by: string; } -export interface IProjectBulkInviteFormData { +export interface IProjectBulkAddFormData { members: { role: 5 | 10 | 15 | 20; member_id: string }[]; } From 442c83eea2f4c57f82ebb915a3c79392c3188c54 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:18:04 +0530 Subject: [PATCH 10/86] style: spreadsheet columns (#2554) * style: spreadsheet columns * fix: build errors --- .../command-palette/command-modal.tsx | 2 +- .../issue/change-issue-assignee.tsx | 4 +- .../views/inline-issue-create-wrapper.tsx | 3 +- web/components/inbox/main-content.tsx | 8 +- web/components/issues/draft-issue-form.tsx | 7 +- web/components/issues/draft-issue-modal.tsx | 8 +- web/components/issues/form.tsx | 7 +- .../calendar/inline-create-issue-form.tsx | 12 -- .../gantt/inline-create-issue-form.tsx | 6 - .../issues/issue-layouts/kanban/block.tsx | 6 +- .../kanban/inline-create-issue-form.tsx | 12 -- .../issue-layouts/kanban/properties.tsx | 4 +- .../list/inline-create-issue-form.tsx | 12 -- .../issues/issue-layouts/list/properties.tsx | 4 +- .../spreadsheet/columns/assignee-column.tsx | 8 +- .../spreadsheet/columns/estimate-column.tsx | 12 +- .../columns/issue/issue-column.tsx | 7 +- .../issue/spreadsheet-issue-column.tsx | 4 - .../spreadsheet/columns/label-column.tsx | 4 +- .../spreadsheet/columns/priority-column.tsx | 9 +- .../spreadsheet/columns/state-column.tsx | 3 +- .../spreadsheet/inline-create-issue-form.tsx | 12 -- .../spreadsheet/spreadsheet-column.tsx | 5 +- .../spreadsheet/spreadsheet-view.tsx | 3 +- web/components/issues/modal.tsx | 5 +- .../issues/peek-overview/issue-properties.tsx | 2 +- .../issues/sidebar-select/label.tsx | 12 +- web/components/issues/sidebar.tsx | 4 +- .../issues/sub-issues/properties.tsx | 2 +- .../issues/view-select/assignee.tsx | 2 +- web/components/issues/view-select/label.tsx | 2 +- web/components/modules/form.tsx | 4 +- web/components/modules/modal.tsx | 3 +- web/components/modules/sidebar.tsx | 7 +- web/components/project/label-select.tsx | 130 +++++++++--------- web/components/project/priority-select.tsx | 43 +++--- .../web-view/issue-properties-detail.tsx | 12 +- web/constants/issue.ts | 2 - web/helpers/issue.helper.ts | 4 - .../archived-issues/[archivedIssueId].tsx | 5 - .../projects/[projectId]/issues/[issueId].tsx | 5 - .../projects/[projectId]/pages/[pageId].tsx | 7 +- .../projects/[projectId]/issues/[issueId].tsx | 4 +- web/store/issue/issue_draft.store.ts | 1 - web/types/issues.d.ts | 2 - web/types/modules.d.ts | 13 +- web/types/pages.d.ts | 1 - 47 files changed, 171 insertions(+), 263 deletions(-) diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index b3a3325eb..2d65bd58a 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -144,7 +144,7 @@ export const CommandModal: React.FC = (props) => { } else { updatedAssignees.push(assignee); } - updateIssue({ assignees_list: updatedAssignees }); + updateIssue({ assignees: updatedAssignees }); }; const redirect = (path: string) => { diff --git a/web/components/command-palette/issue/change-issue-assignee.tsx b/web/components/command-palette/issue/change-issue-assignee.tsx index d7ed90bd6..606acb536 100644 --- a/web/components/command-palette/issue/change-issue-assignee.tsx +++ b/web/components/command-palette/issue/change-issue-assignee.tsx @@ -79,7 +79,7 @@ export const ChangeIssueAssignee: FC = ({ setIsPaletteOpen, issue, user } ); const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees_list ?? []; + const updatedAssignees = issue.assignees ?? []; if (updatedAssignees.includes(assignee)) { updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); @@ -87,7 +87,7 @@ export const ChangeIssueAssignee: FC = ({ setIsPaletteOpen, issue, user } updatedAssignees.push(assignee); } - updateIssue({ assignees_list: updatedAssignees }); + updateIssue({ assignees: updatedAssignees }); setIsPaletteOpen(false); }; diff --git a/web/components/core/views/inline-issue-create-wrapper.tsx b/web/components/core/views/inline-issue-create-wrapper.tsx index b474cb74c..b17a6b34b 100644 --- a/web/components/core/views/inline-issue-create-wrapper.tsx +++ b/web/components/core/views/inline-issue-create-wrapper.tsx @@ -198,8 +198,7 @@ export const InlineCreateIssueFormWrapper: React.FC = (props) => { if (onSuccess) await onSuccess(res); - if (formData.assignees_list?.some((assignee) => assignee === user?.id)) - mutate(USER_ISSUE(workspaceSlug as string)); + if (formData.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent)); }) diff --git a/web/components/inbox/main-content.tsx b/web/components/inbox/main-content.tsx index 6cc17abe6..a978dc7ce 100644 --- a/web/components/inbox/main-content.tsx +++ b/web/components/inbox/main-content.tsx @@ -22,10 +22,10 @@ import { IInboxIssue, IIssue } from "types"; const defaultValues: Partial = { name: "", description_html: "", - assignees_list: [], + assignees: [], priority: "low", target_date: new Date().toString(), - labels_list: [], + labels: [], }; export const InboxMainContent: React.FC = observer(() => { @@ -122,8 +122,8 @@ export const InboxMainContent: React.FC = observer(() => { reset({ ...issueDetails, - assignees_list: issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id), - labels_list: issueDetails.labels_list ?? issueDetails.labels, + assignees: issueDetails.assignees ?? (issueDetails.assignee_details ?? []).map((user) => user.id), + labels: issueDetails.labels ?? issueDetails.labels, }); }, [issueDetails, reset, inboxIssueId]); diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index 9220b7cf4..9deafb51a 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -51,9 +51,7 @@ const defaultValues: Partial = { parent: null, priority: "none", assignees: [], - assignees_list: [], labels: [], - labels_list: [], start_date: null, target_date: null, }; @@ -310,10 +308,7 @@ export const DraftIssueForm: FC = (props) => { handleClose={() => setLabelModal(false)} projectId={projectId} user={user} - onSuccess={(response) => { - setValue("labels", [...watch("labels"), response.id]); - setValue("labels_list", [...watch("labels_list"), response.id]); - }} + onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} /> )} diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 535b77abc..80aebcbbd 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -208,8 +208,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( message: "Issue created successfully.", }); - if (payload.assignees_list?.some((assignee) => assignee === user?.id)) - mutate(USER_ISSUE(workspaceSlug as string)); + if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); }) .catch(() => { setToastAlert({ @@ -325,8 +324,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( if (!createMore) onClose(); - if (payload.assignees_list?.some((assignee) => assignee === user?.id)) - mutate(USER_ISSUE(workspaceSlug as string)); + if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); }) @@ -347,8 +345,6 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( const payload: Partial = { ...formData, - assignees_list: formData.assignees ?? [], - labels_list: formData.labels ?? [], description: formData.description ?? "", description_html: formData.description_html ?? "

", }; diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 13e622385..071a7661b 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -41,9 +41,7 @@ const defaultValues: Partial = { parent: null, priority: "none", assignees: [], - assignees_list: [], labels: [], - labels_list: [], start_date: null, target_date: null, }; @@ -262,10 +260,7 @@ export const IssueForm: FC = observer((props) => { handleClose={() => setLabelModal(false)} projectId={projectId} user={user ?? undefined} - onSuccess={(response) => { - setValue("labels", [...watch("labels"), response.id]); - setValue("labels_list", [...watch("labels_list"), response.id]); - }} + onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} /> )} diff --git a/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx index 3ab74c368..18b76c449 100644 --- a/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx @@ -147,18 +147,6 @@ export const CalendarInlineCreateIssueForm: React.FC = observer((props) = const payload = createIssuePayload(workspaceDetail!, projectDetails!, { ...(prePopulatedData ?? {}), ...formData, - labels_list: - formData.labels_list?.length !== 0 - ? formData.labels_list - : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" - ? [prePopulatedData.labels as any] - : [], - assignees_list: - formData.assignees_list?.length !== 0 - ? formData.assignees_list - : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" - ? [prePopulatedData.assignees as any] - : [], }); try { diff --git a/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx index b4edddbd1..bcdd1ba13 100644 --- a/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx @@ -113,12 +113,6 @@ export const GanttInlineCreateIssueForm: React.FC = observer((props) => { const payload = createIssuePayload(workspaceDetail!, projectDetails!, { ...(prePopulatedData ?? {}), ...formData, - labels_list: - formData.labels_list?.length !== 0 - ? formData.labels_list - : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" - ? [prePopulatedData.labels as any] - : [], start_date: renderDateFormat(new Date()), target_date: renderDateFormat(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)), }); diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 51197c45d..f30453a44 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -65,9 +65,9 @@ export const KanbanIssueBlock: React.FC = (props) => { )}
{displayProperties && displayProperties?.key && (
diff --git a/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx index cad0814b8..764c8a7c6 100644 --- a/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx @@ -117,18 +117,6 @@ export const BoardInlineCreateIssueForm: React.FC = observer((props) => { const payload = createIssuePayload(workspaceDetail!, projectDetails!, { ...(prePopulatedData ?? {}), ...formData, - labels_list: - formData.labels_list && formData.labels_list.length !== 0 - ? formData.labels_list - : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" - ? [prePopulatedData.labels as any] - : [], - assignees_list: - formData.assignees_list && formData.assignees_list.length !== 0 - ? formData.assignees_list - : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" - ? [prePopulatedData.assignees as any] - : [], }); try { diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index 4ab4613aa..f40434686 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -65,7 +65,7 @@ export const KanBanProperties: React.FC = observer((props) => handleIssues( !sub_group_id && sub_group_id === "null" ? null : sub_group_id, !group_id && group_id === "null" ? null : group_id, - { ...issue, labels_list: ids } + { ...issue, labels: ids } ); }; @@ -73,7 +73,7 @@ export const KanBanProperties: React.FC = observer((props) => handleIssues( !sub_group_id && sub_group_id === "null" ? null : sub_group_id, !group_id && group_id === "null" ? null : group_id, - { ...issue, assignees_list: ids } + { ...issue, assignees: ids } ); }; diff --git a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx index a0624a74f..761fd7f9e 100644 --- a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx @@ -116,18 +116,6 @@ export const ListInlineCreateIssueForm: React.FC = observer((props) => { const payload = createIssuePayload(workspaceDetail!, projectDetails!, { ...(prePopulatedData ?? {}), ...formData, - labels_list: - formData.labels_list?.length !== 0 - ? formData.labels_list - : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" - ? [prePopulatedData.labels as any] - : [], - assignees_list: - formData.assignees_list?.length !== 0 - ? formData.assignees_list - : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" - ? [prePopulatedData.assignees as any] - : [], }); try { diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 9c70f9fdd..e751a2fe9 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -36,11 +36,11 @@ export const KanBanProperties: FC = observer((props) => { }; const handleLabel = (ids: string[]) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels_list: ids }); + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids }); }; const handleAssignee = (ids: string[]) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees_list: ids }); + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); }; const handleStartDate = (date: string) => { diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx index f4878d137..db06d81b8 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx @@ -21,12 +21,12 @@ export const SpreadsheetAssigneeColumn: React.FC = ({ issue, members, onC const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); return ( - <> +
onChange({ assignees_list: data })} + onChange={(data) => onChange({ assignees: data })} members={members ?? []} - buttonClassName="!p-0 !rounded-none !shadow-none !border-0" + buttonClassName="!p-0 !rounded-none !border-0" hideDropdownArrow disabled={disabled} multiple @@ -46,6 +46,6 @@ export const SpreadsheetAssigneeColumn: React.FC = ({ issue, members, onC disabled={disabled} /> ))} - +
); }; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx index 156fb707f..fa1cd4c0e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx @@ -1,5 +1,5 @@ // components -import { ViewEstimateSelect } from "components/issues"; +import { EstimateSelect } from "components/estimates"; // hooks import useSubIssue from "hooks/use-sub-issue"; // types @@ -21,7 +21,15 @@ export const SpreadsheetEstimateColumn: React.FC = (props) => { return ( <> - onChange({ estimate_point: data })} disabled={disabled} /> + onChange({ estimate_point: data })} + className="h-full" + buttonClassName="!border-0 !h-full !w-full !rounded-none px-4" + estimatePoints={undefined} + disabled={disabled} + hideDropdownArrow + /> {isExpanded && !isLoading && diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx index 8bb5235c9..e26cdb789 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/issue/issue-column.tsx @@ -5,13 +5,12 @@ import { MoreHorizontal, Pencil, Trash2, ChevronRight, Link } from "lucide-react // hooks import useToast from "hooks/use-toast"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IIssue, IIssueDisplayProperties } from "types"; type Props = { issue: IIssue; - projectId: string; expanded: boolean; handleToggleExpand: (issueId: string) => void; properties: IIssueDisplayProperties; @@ -23,7 +22,6 @@ type Props = { export const IssueColumn: React.FC = ({ issue, - projectId, expanded, handleToggleExpand, properties, @@ -50,8 +48,7 @@ export const IssueColumn: React.FC = ({ }; const handleCopyText = () => { - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx index 887b7bae4..22692a396 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/issue/spreadsheet-issue-column.tsx @@ -9,7 +9,6 @@ import { IIssue, IIssueDisplayProperties } from "types"; type Props = { issue: IIssue; - projectId: string; expandedIssues: string[]; setExpandedIssues: React.Dispatch>; properties: IIssueDisplayProperties; @@ -20,7 +19,6 @@ type Props = { export const SpreadsheetIssuesColumn: React.FC = ({ issue, - projectId, expandedIssues, setExpandedIssues, properties, @@ -48,7 +46,6 @@ export const SpreadsheetIssuesColumn: React.FC = ({ <> = ({ = (props) => { <> onChange({ labels_list: data })} + onChange={(data) => onChange({ labels: data })} labels={labels ?? []} + className="h-full" + buttonClassName="!border-0 !h-full !w-full !rounded-none" hideDropdownArrow maxRender={1} disabled={disabled} diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx index 22ef2a0bd..1f1301d5e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx @@ -20,11 +20,14 @@ export const SpreadsheetPriorityColumn: React.FC = ({ issue, onChange, ex const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); return ( -
+ <> onChange({ priority: data })} - buttonClassName="!p-0 !rounded-none !shadow-none !border-0" + className="h-full" + buttonClassName="!border-0 !h-full !w-full !rounded-none px-4" + showTitle + highlightUrgentPriority={false} hideDropdownArrow disabled={disabled} /> @@ -42,6 +45,6 @@ export const SpreadsheetPriorityColumn: React.FC = ({ issue, onChange, ex disabled={disabled} /> ))} -
+ ); }; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx index 81f45d04f..1a9a32e2e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx @@ -32,7 +32,8 @@ export const SpreadsheetStateColumn: React.FC = (props) => { value={issue.state_detail} onChange={(data) => onChange({ state: data.id, state_detail: data })} states={statesList} - buttonClassName="!shadow-none !border-0" + className="h-full" + buttonClassName="!border-0 !h-full !w-full !rounded-none" hideDropdownArrow disabled={disabled} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx index 20105a67e..5284d64e4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx @@ -120,18 +120,6 @@ export const SpreadsheetInlineCreateIssueForm: React.FC = observer((props const payload = createIssuePayload(workspaceDetail!, projectDetails!, { ...(prePopulatedData ?? {}), ...formData, - labels_list: - formData.labels_list && formData.labels_list?.length !== 0 - ? formData.labels_list - : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" - ? [prePopulatedData.labels as any] - : [], - assignees_list: - formData.assignees_list && formData.assignees_list?.length !== 0 - ? formData.assignees_list - : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" - ? [prePopulatedData.assignees as any] - : [], }); try { diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx index d6e674e28..4013c7737 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx @@ -100,6 +100,7 @@ export const SpreadsheetColumn: React.FC = (props) => {
} width="xl" + placement="bottom-end" > handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
= (props) => { {issues?.map((issue) => (
{property === "state" ? ( = observer((props) => { Issue
- {issues.map((issue: IIssue, index) => ( + {issues.map((issue, index) => ( = observer((prop setFormDirtyState(null); setShowConfirmDiscard(false); - if (payload.assignees_list?.some((assignee) => assignee === user?.id)) - mutate(USER_ISSUE(workspaceSlug as string)); + if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); }) @@ -271,8 +270,6 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const payload: Partial = { ...formData, - assignees_list: formData.assignees ?? [], - labels_list: formData.labels ?? [], description: formData.description ?? "", description_html: formData.description_html ?? "

", }; diff --git a/web/components/issues/peek-overview/issue-properties.tsx b/web/components/issues/peek-overview/issue-properties.tsx index 9a871dfa2..6bb267a6c 100644 --- a/web/components/issues/peek-overview/issue-properties.tsx +++ b/web/components/issues/peek-overview/issue-properties.tsx @@ -93,7 +93,7 @@ export const PeekOverviewIssueProperties: FC = (props) => {
handleUpdateIssue({ assignees_list: val })} + onChange={(val: string[]) => handleUpdateIssue({ assignees: val })} disabled={readOnly} />
diff --git a/web/components/issues/sidebar-select/label.tsx b/web/components/issues/sidebar-select/label.tsx index dffb97f56..a442a1d42 100644 --- a/web/components/issues/sidebar-select/label.tsx +++ b/web/components/issues/sidebar-select/label.tsx @@ -77,7 +77,7 @@ export const SidebarLabelSelect: React.FC = ({ issueLabelMutate((prevData: any) => [...(prevData ?? []), res], false); - submitChanges({ labels_list: [...(issueDetails?.labels ?? []), res.id] }); + submitChanges({ labels: [...(issueDetails?.labels ?? []), res.id] }); setCreateLabelForm(false); }); @@ -99,7 +99,7 @@ export const SidebarLabelSelect: React.FC = ({
- {watchIssue("labels_list")?.map((labelId) => { + {watchIssue("labels")?.map((labelId) => { const label = issueLabels?.find((l) => l.id === labelId); if (label) @@ -108,9 +108,9 @@ export const SidebarLabelSelect: React.FC = ({ key={label.id} className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-100 px-1 py-0.5 text-xs hover:border-red-500/20 hover:bg-red-500/20" onClick={() => { - const updatedLabels = watchIssue("labels_list")?.filter((l) => l !== labelId); + const updatedLabels = watchIssue("labels")?.filter((l) => l !== labelId); submitChanges({ - labels_list: updatedLabels, + labels: updatedLabels, }); }} > @@ -127,12 +127,12 @@ export const SidebarLabelSelect: React.FC = ({ })} ( submitChanges({ labels_list: val })} + onChange={(val: any) => submitChanges({ labels: val })} className="flex-shrink-0" multiple disabled={isNotAllowed || uneditable} diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index 075c43cc3..d87033f5b 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -357,11 +357,11 @@ export const IssueDetailsSidebar: React.FC = ({
( submitChanges({ assignees_list: val })} + onChange={(val: string[]) => submitChanges({ assignees: val })} disabled={memberRole.isGuest || memberRole.isViewer || uneditable} /> )} diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index ce84d92c5..665ff8514 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -72,7 +72,7 @@ export const IssueProperty: React.FC = observer((props) => { }; const handleAssigneeChange = (data: string[]) => { - partialUpdateIssue({ assignees_list: data, assignees: data }); + partialUpdateIssue({ assignees: data }); trackEventService.trackIssuePartialPropertyUpdateEvent( { diff --git a/web/components/issues/view-select/assignee.tsx b/web/components/issues/view-select/assignee.tsx index f096c03fc..3be22b25d 100644 --- a/web/components/issues/view-select/assignee.tsx +++ b/web/components/issues/view-select/assignee.tsx @@ -90,7 +90,7 @@ export const ViewAssigneeSelect: React.FC = ({ if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); else newData.push(data); - partialUpdateIssue({ assignees_list: data }, issue); + partialUpdateIssue({ assignees: data }, issue); trackEventService.trackIssuePartialPropertyUpdateEvent( { diff --git a/web/components/issues/view-select/label.tsx b/web/components/issues/view-select/label.tsx index 6aa85fb1a..c7f71e4f1 100644 --- a/web/components/issues/view-select/label.tsx +++ b/web/components/issues/view-select/label.tsx @@ -139,7 +139,7 @@ export const ViewLabelSelect: FC = ({ { - partialUpdateIssue({ labels_list: data }, issue); + partialUpdateIssue({ labels: data }, issue); }} options={options} {...(customButton ? { customButton: labelsLabel } : { label: labelsLabel })} diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 15fe24cca..c26926ff8 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -23,7 +23,7 @@ const defaultValues: Partial = { description: "", status: "backlog", lead: null, - members_list: [], + members: [], }; export const ModuleForm: React.FC = ({ @@ -47,7 +47,7 @@ export const ModuleForm: React.FC = ({ description: data?.description || "", status: data?.status || "backlog", lead: data?.lead || null, - members_list: data?.members_list || [], + members: data?.members || [], }, }); diff --git a/web/components/modules/modal.tsx b/web/components/modules/modal.tsx index cfb422cbf..887884ba2 100644 --- a/web/components/modules/modal.tsx +++ b/web/components/modules/modal.tsx @@ -24,7 +24,7 @@ const defaultValues: Partial = { description: "", status: "backlog", lead: null, - members_list: [], + members: [], }; export const CreateUpdateModuleModal: React.FC = observer((props) => { @@ -98,7 +98,6 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { const payload: Partial = { ...formData, - members_list: formData.members, }; if (!data) await createModule(payload); diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index f931f3fc1..58ea811a6 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -32,7 +32,7 @@ import { MODULE_STATUS } from "constants/module"; const defaultValues: Partial = { lead: "", - members_list: [], + members: [], start_date: null, target_date: null, status: "backlog", @@ -186,7 +186,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { if (moduleDetails) reset({ ...moduleDetails, - members_list: moduleDetails.members_list ?? moduleDetails.members_detail?.map((m) => m.id), }); }, [moduleDetails, reset]); @@ -301,12 +300,12 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { /> ( { - submitChanges({ members_list: val }); + submitChanges({ members: val }); }} /> )} diff --git a/web/components/project/label-select.tsx b/web/components/project/label-select.tsx index ebf7f4776..c70aa8d29 100644 --- a/web/components/project/label-select.tsx +++ b/web/components/project/label-select.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { usePopper } from "react-popper"; import { Placement } from "@popperjs/core"; import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, PlusIcon, Search } from "lucide-react"; +import { Check, ChevronDown, Search } from "lucide-react"; // ui import { Tooltip } from "components/ui"; // types @@ -21,18 +21,20 @@ type Props = { disabled?: boolean; }; -export const LabelSelect: React.FC = ({ - value, - onChange, - labels, - className = "", - buttonClassName = "", - optionsClassName = "", - maxRender = 2, - placement, - hideDropdownArrow = false, - disabled = false, -}) => { +export const LabelSelect: React.FC = (props) => { + const { + value, + onChange, + labels, + className = "", + buttonClassName = "", + optionsClassName = "", + maxRender = 2, + placement, + hideDropdownArrow = false, + disabled = false, + } = props; + const [query, setQuery] = useState(""); const [referenceElement, setReferenceElement] = useState(null); @@ -79,63 +81,57 @@ export const LabelSelect: React.FC = ({ multiple > - diff --git a/web/components/project/priority-select.tsx b/web/components/project/priority-select.tsx index 5c9ff83e7..38a51ff6e 100644 --- a/web/components/project/priority-select.tsx +++ b/web/components/project/priority-select.tsx @@ -20,6 +20,8 @@ type Props = { buttonClassName?: string; optionsClassName?: string; placement?: Placement; + showTitle?: boolean; + highlightUrgentPriority?: boolean; hideDropdownArrow?: boolean; disabled?: boolean; }; @@ -31,6 +33,8 @@ export const PrioritySelect: React.FC = ({ buttonClassName = "", optionsClassName = "", placement, + showTitle = false, + highlightUrgentPriority = true, hideDropdownArrow = false, disabled = false, }) => { @@ -69,20 +73,21 @@ export const PrioritySelect: React.FC = ({ const label = ( - +
+ + {showTitle && {value}} +
); @@ -99,9 +104,13 @@ export const PrioritySelect: React.FC = ({ ref={setReferenceElement} type="button" className={`flex items-center justify-between gap-1 h-full w-full text-xs rounded border-[0.5px] ${ - value === "urgent" ? "border-red-500/20 bg-red-500" : "border-custom-border-300" - } ${ - disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" + value === "urgent" + ? highlightUrgentPriority + ? "border-red-500/20 bg-red-500" + : "border-custom-border-300" + : "border-custom-border-300" + } ${!disabled ? "hover:bg-custom-background-80" : ""} ${ + disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer" } ${buttonClassName}`} > {label} diff --git a/web/components/web-view/issue-properties-detail.tsx b/web/components/web-view/issue-properties-detail.tsx index 40b84ff82..67e067dac 100644 --- a/web/components/web-view/issue-properties-detail.tsx +++ b/web/components/web-view/issue-properties-detail.tsx @@ -144,7 +144,7 @@ export const IssuePropertiesDetail: React.FC = (props) => {
( = (props) => { onChange={(val: string) => { const assignees = value?.includes(val) ? value?.filter((i) => i !== val) : [...(value ?? []), val]; - submitChanges({ assignees_list: assignees }); + submitChanges({ assignees: assignees }); }} /> )} @@ -232,7 +232,7 @@ export const IssuePropertiesDetail: React.FC = (props) => { JSON.stringify({ issue_id: relation.issue_detail?.id, project_id: relation.issue_detail?.project_detail.id, - issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}` + issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`, }) ) } @@ -295,7 +295,7 @@ export const IssuePropertiesDetail: React.FC = (props) => { JSON.stringify({ issue_id: relation.issue_detail?.id, project_id: relation.issue_detail?.project_detail.id, - issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}` + issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`, }) ) } @@ -358,7 +358,7 @@ export const IssuePropertiesDetail: React.FC = (props) => { JSON.stringify({ issue_id: relation.issue_detail?.id, project_id: relation.issue_detail?.project_detail.id, - issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}` + issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`, }) ) } @@ -421,7 +421,7 @@ export const IssuePropertiesDetail: React.FC = (props) => { JSON.stringify({ issue_id: relation.issue_detail?.id, project_id: relation.issue_detail?.project_detail.id, - issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}` + issue_identifier: `${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`, }) ) } diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 2e7983c11..376c99aa0 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -437,7 +437,6 @@ export const createIssuePayload: ( archived_at: null, assignees: [], assignee_details: [], - assignees_list: [], attachment_count: 0, attachments: [], issue_relations: [], @@ -459,7 +458,6 @@ export const createIssuePayload: ( labels: [], label_details: [], is_draft: false, - labels_list: [], links_list: [], link_count: 0, module: null, diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 919699d69..6f4381dec 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -37,8 +37,6 @@ export const handleIssuesMutation: THandleIssuesMutation = ( const updatedIssue = { ...prevData[issueIndex], ...formData, - assignees: formData?.assignees_list ?? prevData[issueIndex]?.assignees, - labels: formData?.labels_list ?? prevData[issueIndex]?.labels, }; prevData.splice(issueIndex, 1, updatedIssue); @@ -55,8 +53,6 @@ export const handleIssuesMutation: THandleIssuesMutation = ( const updatedIssue = { ...oldGroup[issueIndex], ...formData, - assignees: formData?.assignees_list ?? oldGroup[issueIndex]?.assignees, - labels: formData?.labels_list ?? oldGroup[issueIndex]?.labels, }; if (selectedGroupBy !== Object.keys(formData)[0]) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index df20f8a1f..d6d0c6859 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -32,12 +32,10 @@ const defaultValues: Partial = { description_html: "", estimate_point: null, state: "", - assignees_list: [], priority: "low", target_date: new Date().toString(), issue_cycle: null, issue_module: null, - labels_list: [], }; // services @@ -109,9 +107,6 @@ const ArchivedIssueDetailsPage: NextPage = () => { mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string)); reset({ ...issueDetails, - assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), - labels_list: issueDetails.labels_list ?? issueDetails.labels, - labels: issueDetails.labels_list ?? issueDetails.labels, }); }, [issueDetails, reset, archivedIssueId]); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index af942ef47..c8e70bdb7 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -27,13 +27,11 @@ import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys"; // helper const defaultValues: Partial = { - assignees_list: [], description: "", description_html: "", estimate_point: null, issue_cycle: null, issue_module: null, - labels_list: [], name: "", priority: "low", start_date: null, @@ -109,9 +107,6 @@ const IssueDetailsPage: NextPage = () => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); reset({ ...issueDetails, - assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), - labels_list: issueDetails.labels_list ?? issueDetails.labels, - labels: issueDetails.labels_list ?? issueDetails.labels, }); }, [issueDetails, reset, issueId]); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 7c4cae55d..24a25fa9d 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -133,7 +133,6 @@ const SinglePage: NextPage = () => { (prevData) => ({ ...(prevData as IPage), ...formData, - labels: formData.labels_list ? formData.labels_list : (prevData as IPage).labels, }), false ); @@ -361,7 +360,7 @@ const SinglePage: NextPage = () => { className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs hover:border-red-500 hover:bg-red-50" onClick={() => { const updatedLabels = pageDetails.labels.filter((l) => l !== labelId); - partialUpdatePage({ labels_list: updatedLabels }); + partialUpdatePage({ labels: updatedLabels }); }} style={{ backgroundColor: `${label?.color && label.color !== "" ? label.color : "#000000"}20`, @@ -402,7 +401,7 @@ const SinglePage: NextPage = () => { } - onChange={(val: string[]) => partialUpdatePage({ labels_list: val })} + onChange={(val: string[]) => partialUpdatePage({ labels: val })} options={options} multiple noChevron @@ -606,7 +605,7 @@ const SinglePage: NextPage = () => { user={user} onSuccess={(response) => { partialUpdatePage({ - labels_list: [...(pageDetails.labels ?? []), response.id], + labels: [...(pageDetails.labels ?? []), response.id], }); }} /> diff --git a/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 167b89de7..37ce2d90e 100644 --- a/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -69,15 +69,13 @@ const MobileWebViewIssueDetail_ = () => { useEffect(() => { if (!issueDetails) return; + reset({ ...issueDetails, name: issueDetails.name, description: issueDetails.description, description_html: issueDetails.description_html, state: issueDetails.state, - assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), - labels_list: issueDetails.labels_list ?? issueDetails.labels, - labels: issueDetails.labels_list ?? issueDetails.labels, }); }, [issueDetails, reset]); diff --git a/web/store/issue/issue_draft.store.ts b/web/store/issue/issue_draft.store.ts index 815e58278..76faacb7e 100644 --- a/web/store/issue/issue_draft.store.ts +++ b/web/store/issue/issue_draft.store.ts @@ -122,7 +122,6 @@ export class DraftIssuesStore { // immediately update the issue in the store const updatedIssue = { ...this.issues[issueId], ...issueForm }; - if (updatedIssue.assignees_list) updatedIssue.assignees = updatedIssue.assignees_list; try { runInAction(() => { diff --git a/web/types/issues.d.ts b/web/types/issues.d.ts index 164718237..553a12ced 100644 --- a/web/types/issues.d.ts +++ b/web/types/issues.d.ts @@ -80,7 +80,6 @@ export interface IIssue { archived_at: string; assignees: string[]; assignee_details: IUser[]; - assignees_list: string[]; attachment_count: number; attachments: any[]; issue_relations: IssueRelation[]; @@ -105,7 +104,6 @@ export interface IIssue { labels: string[]; label_details: any[]; is_draft: boolean; - labels_list: string[]; links_list: IIssueLink[]; link_count: number; module: string | null; diff --git a/web/types/modules.d.ts b/web/types/modules.d.ts index 709d1d300..6ec86c4f5 100644 --- a/web/types/modules.d.ts +++ b/web/types/modules.d.ts @@ -10,13 +10,7 @@ import type { linkDetails, } from "types"; -export type TModuleStatus = - | "backlog" - | "planned" - | "in-progress" - | "paused" - | "completed" - | "cancelled"; +export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; export interface IModule { backlog_issues: number; @@ -38,7 +32,6 @@ export interface IModule { link_module: linkDetails[]; links_list: ModuleLink[]; members: string[]; - members_list: string[]; members_detail: IUserLite[]; is_favorite: boolean; name: string; @@ -80,8 +73,6 @@ export type ModuleLink = { url: string; }; -export type SelectModuleType = - | (IModule & { actionType: "edit" | "delete" | "create-issue" }) - | undefined; +export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined; diff --git a/web/types/pages.d.ts b/web/types/pages.d.ts index 6be966f7c..f7850d11d 100644 --- a/web/types/pages.d.ts +++ b/web/types/pages.d.ts @@ -14,7 +14,6 @@ export interface IPage { is_favorite: boolean; label_details: IIssueLabels[]; labels: string[]; - labels_list: string[]; name: string; owned_by: string; project: string; From 8072bbb5591863cb7a66c93f0376c4ae0cc9c4ad Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:26:10 +0530 Subject: [PATCH 11/86] fix: Debounce title and Editor initialization (#2530) * fixed debounce logic and extracted the same * fixed editor mounting with custom hook * removed console logs and improved structure * fixed comment editor behavior on Shift-Enter * fixed editor initialization behaviour for new peek view * fixed button type to avoid reload while editing comments * fixed initialization of content in peek overview * improved naming variables in updated title debounce logic * added react-hook-form support to the issue detail in peek view with save states * delete image plugin's ts support improved --- .../editor/core/src/ui/hooks/useEditor.tsx | 71 +++++++++------- .../src/ui/hooks/useInitializedContent.tsx | 19 +++++ .../core/src/ui/hooks/useReadOnlyEditor.tsx | 28 +++++-- .../core/src/ui/plugins/delete-image.tsx | 2 +- .../ui/extensions/custom-list-extension.tsx | 9 -- .../src/ui/extensions/enter-key-extension.tsx | 37 ++++---- .../src/ui/extensions/index.tsx | 2 - web/components/issues/comment/add-comment.tsx | 31 +------ .../issues/comment/comment-card.tsx | 3 +- web/components/issues/description-form.tsx | 20 +++-- .../issue-peek-overview/issue-detail.tsx | 84 +++++++++++++++---- 11 files changed, 194 insertions(+), 112 deletions(-) create mode 100644 packages/editor/core/src/ui/hooks/useInitializedContent.tsx delete mode 100644 packages/editor/lite-text-editor/src/ui/extensions/custom-list-extension.tsx diff --git a/packages/editor/core/src/ui/hooks/useEditor.tsx b/packages/editor/core/src/ui/hooks/useEditor.tsx index 837700915..f58c7964b 100644 --- a/packages/editor/core/src/ui/hooks/useEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useEditor.tsx @@ -1,18 +1,23 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; -import { useImperativeHandle, useRef, MutableRefObject } from "react"; -import { useDebouncedCallback } from "use-debounce"; -import { DeleteImage } from '../../types/delete-image'; +import { + useImperativeHandle, + useRef, + MutableRefObject, + useEffect, +} from "react"; +import { DeleteImage } from "../../types/delete-image"; import { CoreEditorProps } from "../props"; import { CoreEditorExtensions } from "../extensions"; -import { EditorProps } from '@tiptap/pm/view'; +import { EditorProps } from "@tiptap/pm/view"; import { getTrimmedHTML } from "../../lib/utils"; import { UploadImage } from "../../types/upload-image"; - -const DEBOUNCE_DELAY = 1500; +import { useInitializedContent } from "./useInitializedContent"; interface CustomEditorProps { uploadFile: UploadImage; - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void; setShouldShowAlert?: (showAlert: boolean) => void; value: string; deleteFile: DeleteImage; @@ -23,25 +28,37 @@ interface CustomEditorProps { forwardedRef?: any; } -export const useEditor = ({ uploadFile, deleteFile, editorProps = {}, value, extensions = [], onChange, setIsSubmitting, debouncedUpdatesEnabled, forwardedRef, setShouldShowAlert, }: CustomEditorProps) => { - const editor = useCustomEditor({ - editorProps: { - ...CoreEditorProps(uploadFile, setIsSubmitting), - ...editorProps, - }, - extensions: [...CoreEditorExtensions(deleteFile), ...extensions], - content: (typeof value === "string" && value.trim() !== "") ? value : "

", - onUpdate: async ({ editor }) => { - // for instant feedback loop - setIsSubmitting?.("submitting"); - setShouldShowAlert?.(true); - if (debouncedUpdatesEnabled) { - debouncedUpdates({ onChange: onChange, editor }); - } else { +export const useEditor = ({ + uploadFile, + deleteFile, + editorProps = {}, + value, + extensions = [], + onChange, + setIsSubmitting, + forwardedRef, + setShouldShowAlert, +}: CustomEditorProps) => { + const editor = useCustomEditor( + { + editorProps: { + ...CoreEditorProps(uploadFile, setIsSubmitting), + ...editorProps, + }, + extensions: [...CoreEditorExtensions(deleteFile), ...extensions], + content: + typeof value === "string" && value.trim() !== "" ? value : "

", + onUpdate: async ({ editor }) => { + // for instant feedback loop + setIsSubmitting?.("submitting"); + setShouldShowAlert?.(true); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); - } + }, }, - }); + [], + ); + + useInitializedContent(editor, value); const editorRef: MutableRefObject = useRef(null); editorRef.current = editor; @@ -55,12 +72,6 @@ export const useEditor = ({ uploadFile, deleteFile, editorProps = {}, value, ext }, })); - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { - if (onChange) { - onChange(editor.getJSON(), getTrimmedHTML(editor.getHTML())); - } - }, DEBOUNCE_DELAY); - if (!editor) { return null; } diff --git a/packages/editor/core/src/ui/hooks/useInitializedContent.tsx b/packages/editor/core/src/ui/hooks/useInitializedContent.tsx new file mode 100644 index 000000000..8e2ce1717 --- /dev/null +++ b/packages/editor/core/src/ui/hooks/useInitializedContent.tsx @@ -0,0 +1,19 @@ +import { Editor } from "@tiptap/react"; +import { useEffect, useRef } from "react"; + +export const useInitializedContent = (editor: Editor | null, value: string) => { + const hasInitializedContent = useRef(false); + + useEffect(() => { + if (editor) { + const cleanedValue = + typeof value === "string" && value.trim() !== "" ? value : "

"; + if (cleanedValue !== "

" && !hasInitializedContent.current) { + editor.commands.setContent(cleanedValue); + hasInitializedContent.current = true; + } else if (cleanedValue === "

" && hasInitializedContent.current) { + hasInitializedContent.current = false; + } + } + }, [value, editor]); +}; diff --git a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx index 3e32c5044..522cd94b8 100644 --- a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx @@ -1,8 +1,13 @@ import { useEditor as useCustomEditor, Editor } from "@tiptap/react"; -import { useImperativeHandle, useRef, MutableRefObject } from "react"; +import { + useImperativeHandle, + useRef, + MutableRefObject, + useEffect, +} from "react"; import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions"; import { CoreReadOnlyEditorProps } from "../../ui/read-only/props"; -import { EditorProps } from '@tiptap/pm/view'; +import { EditorProps } from "@tiptap/pm/view"; interface CustomReadOnlyEditorProps { value: string; @@ -11,10 +16,16 @@ interface CustomReadOnlyEditorProps { editorProps?: EditorProps; } -export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editorProps = {} }: CustomReadOnlyEditorProps) => { +export const useReadOnlyEditor = ({ + value, + forwardedRef, + extensions = [], + editorProps = {}, +}: CustomReadOnlyEditorProps) => { const editor = useCustomEditor({ editable: false, - content: (typeof value === "string" && value.trim() !== "") ? value : "

", + content: + typeof value === "string" && value.trim() !== "" ? value : "

", editorProps: { ...CoreReadOnlyEditorProps, ...editorProps, @@ -22,6 +33,14 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor extensions: [...CoreReadOnlyEditorExtensions, ...extensions], }); + const hasIntiliazedContent = useRef(false); + useEffect(() => { + if (editor && !value && !hasIntiliazedContent.current) { + editor.commands.setContent(value); + hasIntiliazedContent.current = true; + } + }, [value]); + const editorRef: MutableRefObject = useRef(null); editorRef.current = editor; @@ -34,7 +53,6 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor }, })); - if (!editor) { return null; } diff --git a/packages/editor/core/src/ui/plugins/delete-image.tsx b/packages/editor/core/src/ui/plugins/delete-image.tsx index ba21d686d..56284472b 100644 --- a/packages/editor/core/src/ui/plugins/delete-image.tsx +++ b/packages/editor/core/src/ui/plugins/delete-image.tsx @@ -16,7 +16,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => new Plugin({ key: deleteKey, appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { - const newImageSources = new Set(); + const newImageSources = new Set(); newState.doc.descendants((node) => { if (node.type.name === IMAGE_NODE_TYPE) { newImageSources.add(node.attrs.src); diff --git a/packages/editor/lite-text-editor/src/ui/extensions/custom-list-extension.tsx b/packages/editor/lite-text-editor/src/ui/extensions/custom-list-extension.tsx deleted file mode 100644 index f0bc70cff..000000000 --- a/packages/editor/lite-text-editor/src/ui/extensions/custom-list-extension.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ListItem from '@tiptap/extension-list-item' - -export const CustomListItem = ListItem.extend({ - addKeyboardShortcuts() { - return { - 'Shift-Enter': () => this.editor.chain().focus().splitListItem('listItem').run(), - } - }, -}) diff --git a/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx b/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx index 04c4a1fbe..129efa4ee 100644 --- a/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx +++ b/packages/editor/lite-text-editor/src/ui/extensions/enter-key-extension.tsx @@ -1,16 +1,25 @@ -import { Extension } from '@tiptap/core'; +import { Extension } from "@tiptap/core"; -export const EnterKeyExtension = (onEnterKeyPress?: () => void) => Extension.create({ - name: 'enterKey', +export const EnterKeyExtension = (onEnterKeyPress?: () => void) => + Extension.create({ + name: "enterKey", - addKeyboardShortcuts() { - return { - 'Enter': () => { - if (onEnterKeyPress) { - onEnterKeyPress(); - } - return true; - }, - } - }, -}); + addKeyboardShortcuts() { + return { + Enter: () => { + if (onEnterKeyPress) { + onEnterKeyPress(); + } + return true; + }, + "Shift-Enter": ({ editor }) => + editor.commands.first(({ commands }) => [ + () => commands.newlineInCode(), + () => commands.splitListItem("listItem"), + () => commands.createParagraphNear(), + () => commands.liftEmptyBlock(), + () => commands.splitBlock(), + ]), + }; + }, + }); diff --git a/packages/editor/lite-text-editor/src/ui/extensions/index.tsx b/packages/editor/lite-text-editor/src/ui/extensions/index.tsx index ccd04a395..358f15294 100644 --- a/packages/editor/lite-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/extensions/index.tsx @@ -1,7 +1,5 @@ -import { CustomListItem } from "./custom-list-extension"; import { EnterKeyExtension } from "./enter-key-extension"; export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [ - CustomListItem, EnterKeyExtension(onEnterKeyPress), ]; diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index f836efb01..7baca7d61 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -7,7 +7,7 @@ import { FileService } from "services/file.service"; // components import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; // ui -import { Button, Tooltip } from "@plane/ui"; +import { Button } from "@plane/ui"; import { Globe2, Lock } from "lucide-react"; // types @@ -72,35 +72,6 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc
- {showAccessSpecifier && ( -
- ( -
- {commentAccess.map((access) => ( - - - - ))} -
- )} - /> -
- )} = ({