diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 48288f77e..b4a3ef31e 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -88,6 +88,25 @@ export const EmailCodeForm = ({ onSuccess }: any) => { setErrorResendingCode(false); }, [emailOld]); + useEffect(() => { + const submitForm = (e: KeyboardEvent) => { + if (!codeSent && e.key === "Enter") { + e.preventDefault(); + handleSubmit(onSubmit)().then(() => { + setResendCodeTimer(30); + }); + } + }; + + if (!codeSent) { + window.addEventListener("keydown", submitForm); + } + + return () => { + window.removeEventListener("keydown", submitForm); + }; + }, [handleSubmit, codeSent]); + return ( <>
diff --git a/apps/app/components/analytics/custom-analytics/sidebar.tsx b/apps/app/components/analytics/custom-analytics/sidebar.tsx index e9a9d5d7f..7910c2cc8 100644 --- a/apps/app/components/analytics/custom-analytics/sidebar.tsx +++ b/apps/app/components/analytics/custom-analytics/sidebar.tsx @@ -7,6 +7,7 @@ import analyticsService from "services/analytics.service"; import projectService from "services/project.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +import trackEventServices from "services/track-event.service"; // hooks import useProjects from "hooks/use-projects"; import useToast from "hooks/use-toast"; @@ -23,7 +24,13 @@ import { ContrastIcon, LayerDiagonalIcon } from "components/icons"; // helpers import { renderShortDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IProject } from "types"; +import { + IAnalyticsParams, + IAnalyticsResponse, + IExportAnalyticsFormData, + IProject, + IWorkspace, +} from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; // constants @@ -82,6 +89,59 @@ export const AnalyticsSidebar: React.FC = ({ : null ); + const trackExportAnalytics = () => { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + params: { + x_axis: params.x_axis, + y_axis: params.y_axis, + group: params.segment, + project: params.project, + }, + }; + + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + + trackEventServices.trackAnalyticsEvent( + eventPayload, + cycleId + ? "CYCLE_ANALYTICS_EXPORT" + : moduleId + ? "MODULE_ANALYTICS_EXPORT" + : projectId + ? "PROJECT_ANALYTICS_EXPORT" + : "WORKSPACE_ANALYTICS_EXPORT" + ); + }; + const exportAnalytics = () => { if (!workspaceSlug) return; @@ -95,13 +155,15 @@ export const AnalyticsSidebar: React.FC = ({ analyticsService .exportAnalytics(workspaceSlug.toString(), data) - .then((res) => + .then((res) => { setToastAlert({ type: "success", title: "Success!", message: res.message, - }) - ) + }); + + trackExportAnalytics(); + }) .catch(() => setToastAlert({ type: "error", diff --git a/apps/app/components/analytics/project-modal.tsx b/apps/app/components/analytics/project-modal.tsx index c2072642c..728ca4ed5 100644 --- a/apps/app/components/analytics/project-modal.tsx +++ b/apps/app/components/analytics/project-modal.tsx @@ -13,6 +13,7 @@ import analyticsService from "services/analytics.service"; import projectService from "services/project.service"; import cyclesService from "services/cycles.service"; import modulesService from "services/modules.service"; +import trackEventServices from "services/track-event.service"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; // icons @@ -22,7 +23,7 @@ import { XMarkIcon, } from "@heroicons/react/24/outline"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams, IWorkspace } from "types"; // fetch-keys import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys"; @@ -95,6 +96,50 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { : null ); + const trackAnalyticsEvent = (tab: string) => { + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + }; + + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + + const eventType = + tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS"; + + trackEventServices.trackAnalyticsEvent( + eventPayload, + cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}` + ); + }; + const handleClose = () => { onClose(); }; @@ -146,6 +191,7 @@ export const AnalyticsProjectModal: React.FC = ({ isOpen, onClose }) => { selected ? "bg-brand-surface-2" : "" }` } + onClick={() => trackAnalyticsEvent(tab)} > {tab} diff --git a/apps/app/components/cycles/active-cycle-details.tsx b/apps/app/components/cycles/active-cycle-details.tsx index 65c2e8b1b..7e8c8f7f7 100644 --- a/apps/app/components/cycles/active-cycle-details.tsx +++ b/apps/app/components/cycles/active-cycle-details.tsx @@ -40,20 +40,14 @@ import { } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, - IIssue, -} from "types"; +import { ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, CYCLE_ISSUES, + CYCLE_LIST, } from "constants/fetch-keys"; type TSingleStatProps = { @@ -111,51 +105,18 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + mutate( + CYCLE_CURRENT_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -180,51 +141,18 @@ export const ActiveCycleDetails: React.FC = ({ cycle, isComple const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + mutate( + CYCLE_CURRENT_LIST(projectId as string), + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/completed-cycles.tsx b/apps/app/components/cycles/completed-cycles.tsx index 36ef691f5..f561f32d4 100644 --- a/apps/app/components/cycles/completed-cycles.tsx +++ b/apps/app/components/cycles/completed-cycles.tsx @@ -38,7 +38,10 @@ export const CompletedCycles: React.FC = ({ const { data: completedCycles } = useSWR( workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "completed", + }) : null ); @@ -64,7 +67,7 @@ export const CompletedCycles: React.FC = ({ data={selectedCycleForDelete} /> {completedCycles ? ( - completedCycles.completed_cycles.length > 0 ? ( + completedCycles.length > 0 ? (
= ({
{cycleView === "list" && (
- {completedCycles.completed_cycles.map((cycle) => ( + {completedCycles.map((cycle) => (
= ({ )} {cycleView === "board" && (
- {completedCycles.completed_cycles.map((cycle) => ( + {completedCycles.map((cycle) => ( = ({ cycles }) => {
-
- {data?.name} -
+ +
+ {data?.name} +
+
); diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index aedd82ba3..0f1c917ce 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -18,26 +18,23 @@ import { EmptyState, Loader } from "components/ui"; import { ChartBarIcon, ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import emptyCycle from "public/empty-state/empty-cycle.svg"; // types -import { - SelectCycleType, - ICycle, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, -} from "types"; +import { SelectCycleType, ICycle } from "types"; type Props = { setSelectedCycle: React.Dispatch>; setCreateUpdateCycleModal: React.Dispatch>; cyclesCompleteList: ICycle[] | undefined; - currentAndUpcomingCycles: CurrentAndUpcomingCyclesResponse | undefined; - draftCycles: DraftCyclesResponse | undefined; + currentCycle: ICycle[] | undefined; + upcomingCycles: ICycle[] | undefined; + draftCycles: ICycle[] | undefined; }; export const CyclesView: React.FC = ({ setSelectedCycle, setCreateUpdateCycleModal, cyclesCompleteList, - currentAndUpcomingCycles, + currentCycle, + upcomingCycles, draftCycles, }) => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); @@ -182,8 +179,8 @@ export const CyclesView: React.FC = ({ {cyclesView !== "gantt_chart" && ( - {currentAndUpcomingCycles?.current_cycle?.[0] ? ( - + {currentCycle?.[0] ? ( + ) : ( = ({ {cyclesView === "list" && ( = ({ )} {cyclesView === "board" && ( )} {cyclesView === "gantt_chart" && ( - + )} @@ -226,7 +223,7 @@ export const CyclesView: React.FC = ({ {cyclesView === "list" && ( = ({ )} {cyclesView === "board" && ( >; @@ -28,10 +23,10 @@ type TConfirmCycleDeletionProps = { // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; import { getDateRangeStatus } from "helpers/date-time.helper"; @@ -60,63 +55,28 @@ export const DeleteCycleModal: React.FC = ({ await cycleService .deleteCycle(workspaceSlug as string, data.project, data.id) .then(() => { - switch (getDateRangeStatus(data.start_date, data.end_date)) { - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => { - if (!prevData) return; + const cycleType = getDateRangeStatus(data.start_date, data.end_date); + const fetchKey = + cycleType === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleType === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleType === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); - return { - completed_cycles: prevData.completed_cycles?.filter( - (cycle) => cycle.id !== data?.id - ), - }; - }, - false - ); - break; - case "current": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => { - if (!prevData) return; - return { - current_cycle: prevData.current_cycle?.filter((c) => c.id !== data?.id), - upcoming_cycle: prevData.upcoming_cycle, - }; - }, - false - ); - break; - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => { - if (!prevData) return; + mutate( + fetchKey, + (prevData) => { + if (!prevData) return; + + return prevData.filter((cycle) => cycle.id !== data?.id); + }, + false + ); - return { - current_cycle: prevData.current_cycle, - upcoming_cycle: prevData.upcoming_cycle?.filter((c) => c.id !== data?.id), - }; - }, - false - ); - break; - default: - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => { - if (!prevData) return; - return { - draft_cycles: prevData.draft_cycles?.filter((cycle) => cycle.id !== data?.id), - }; - }, - false - ); - } mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => { if (!prevData) return; return prevData.filter((cycle: any) => cycle.id !== data?.id); diff --git a/apps/app/components/cycles/gantt-chart.tsx b/apps/app/components/cycles/gantt-chart.tsx index 44abc392b..42ecad448 100644 --- a/apps/app/components/cycles/gantt-chart.tsx +++ b/apps/app/components/cycles/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view"; @@ -38,9 +40,23 @@ export const CycleIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const CycleIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index b3baacd65..8a0c6b8ad 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -19,10 +19,11 @@ import type { ICycle } from "types"; // fetch keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, CYCLE_INCOMPLETE_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; type CycleModalProps = { @@ -50,16 +51,16 @@ export const CreateUpdateCycleModal: React.FC = ({ mutate(CYCLE_COMPLETE_LIST(projectId as string)); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_CURRENT_LIST(projectId as string)); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_UPCOMING_LIST(projectId as string)); break; default: mutate(CYCLE_DRAFT_LIST(projectId as string)); } mutate(CYCLE_INCOMPLETE_LIST(projectId as string)); - mutate(CYCLE_DETAILS(projectId as string)); + mutate(CYCLE_LIST(projectId as string)); handleClose(); setToastAlert({ @@ -86,15 +87,15 @@ export const CreateUpdateCycleModal: React.FC = ({ mutate(CYCLE_COMPLETE_LIST(projectId as string)); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_CURRENT_LIST(projectId as string)); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_UPCOMING_LIST(projectId as string)); break; default: mutate(CYCLE_DRAFT_LIST(projectId as string)); } - mutate(CYCLE_DETAILS(projectId as string)); + mutate(CYCLE_LIST(projectId as string)); if ( getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date) @@ -104,10 +105,10 @@ export const CreateUpdateCycleModal: React.FC = ({ mutate(CYCLE_COMPLETE_LIST(projectId as string)); break; case "current": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_CURRENT_LIST(projectId as string)); break; case "upcoming": - mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string)); + mutate(CYCLE_UPCOMING_LIST(projectId as string)); break; default: mutate(CYCLE_DRAFT_LIST(projectId as string)); diff --git a/apps/app/components/cycles/select.tsx b/apps/app/components/cycles/select.tsx index 971c60ddd..854749ec5 100644 --- a/apps/app/components/cycles/select.tsx +++ b/apps/app/components/cycles/select.tsx @@ -38,7 +38,10 @@ export const CycleSelect: React.FC = ({ const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLE_LIST(projectId) : null, workspaceSlug && projectId - ? () => cycleServices.getCycles(workspaceSlug as string, projectId) + ? () => + cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "all", + }) : null ); diff --git a/apps/app/components/cycles/single-cycle-card.tsx b/apps/app/components/cycles/single-cycle-card.tsx index a5f6e8110..b6349b2ad 100644 --- a/apps/app/components/cycles/single-cycle-card.tsx +++ b/apps/app/components/cycles/single-cycle-card.tsx @@ -41,18 +41,14 @@ import { } from "helpers/date-time.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, -} from "types"; +import { ICycle } from "types"; // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; type TSingleStatProps = { @@ -108,51 +104,27 @@ export const SingleCycleCard: React.FC = ({ const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -177,51 +149,27 @@ export const SingleCycleCard: React.FC = ({ const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/single-cycle-list.tsx b/apps/app/components/cycles/single-cycle-list.tsx index a8976292e..aa2bf0975 100644 --- a/apps/app/components/cycles/single-cycle-list.tsx +++ b/apps/app/components/cycles/single-cycle-list.tsx @@ -32,18 +32,14 @@ import { } from "helpers/date-time.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // types -import { - CompletedCyclesResponse, - CurrentAndUpcomingCyclesResponse, - DraftCyclesResponse, - ICycle, -} from "types"; +import { ICycle } from "types"; // fetch-keys import { CYCLE_COMPLETE_LIST, - CYCLE_CURRENT_AND_UPCOMING_LIST, - CYCLE_DETAILS, + CYCLE_CURRENT_LIST, CYCLE_DRAFT_LIST, + CYCLE_LIST, + CYCLE_UPCOMING_LIST, } from "constants/fetch-keys"; import { type } from "os"; @@ -142,51 +138,27 @@ export const SingleCycleList: React.FC = ({ const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? true : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? true : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, @@ -211,51 +183,27 @@ export const SingleCycleList: React.FC = ({ const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !cycle) return; - switch (cycleStatus) { - case "current": - case "upcoming": - mutate( - CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string), - (prevData) => ({ - current_cycle: (prevData?.current_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "completed": - mutate( - CYCLE_COMPLETE_LIST(projectId as string), - (prevData) => ({ - completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - case "draft": - mutate( - CYCLE_DRAFT_LIST(projectId as string), - (prevData) => ({ - draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({ - ...c, - is_favorite: c.id === cycle.id ? false : c.is_favorite, - })), - }), - false - ); - break; - } + const fetchKey = + cycleStatus === "current" + ? CYCLE_CURRENT_LIST(projectId as string) + : cycleStatus === "upcoming" + ? CYCLE_UPCOMING_LIST(projectId as string) + : cycleStatus === "completed" + ? CYCLE_COMPLETE_LIST(projectId as string) + : CYCLE_DRAFT_LIST(projectId as string); + + mutate( + fetchKey, + (prevData) => + (prevData ?? []).map((c) => ({ + ...c, + is_favorite: c.id === cycle.id ? false : c.is_favorite, + })), + false + ); + mutate( - CYCLE_DETAILS(projectId as string), + CYCLE_LIST(projectId as string), (prevData: any) => (prevData ?? []).map((c: any) => ({ ...c, diff --git a/apps/app/components/cycles/transfer-issues-modal.tsx b/apps/app/components/cycles/transfer-issues-modal.tsx index c857e154e..8d44c14d0 100644 --- a/apps/app/components/cycles/transfer-issues-modal.tsx +++ b/apps/app/components/cycles/transfer-issues-modal.tsx @@ -59,7 +59,10 @@ export const TransferIssuesModal: React.FC = ({ isOpen, handleClose }) => const { data: incompleteCycles } = useSWR( workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "incomplete", + }) : null ); diff --git a/apps/app/components/gantt-chart/blocks/index.tsx b/apps/app/components/gantt-chart/blocks/index.tsx index f2d44b294..d5eadf2a0 100644 --- a/apps/app/components/gantt-chart/blocks/index.tsx +++ b/apps/app/components/gantt-chart/blocks/index.tsx @@ -49,7 +49,10 @@ export const GanttChartBlocks: FC<{ width: `${block?.position?.width}px`, }} > - {blockRender({ ...block?.data })} + {blockRender({ + ...block?.data, + infoToggle: block?.infoToggle ? true : false, + })}
diff --git a/apps/app/components/issues/gantt-chart.tsx b/apps/app/components/issues/gantt-chart.tsx index 1330c2438..571583707 100644 --- a/apps/app/components/issues/gantt-chart.tsx +++ b/apps/app/components/issues/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartIssues from "hooks/gantt-chart/issue-view"; @@ -37,9 +39,23 @@ export const IssueGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -51,17 +67,25 @@ export const IssueGanttChartView: FC = ({}) => { start_date: data?.start_date, target_date: data?.target_date, }; - - console.log("payload", payload); }; const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index 3396dde27..d9e7772a3 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -35,7 +35,10 @@ export const SidebarCycleSelect: React.FC = ({ const { data: incompleteCycles } = useSWR( workspaceSlug && projectId ? CYCLE_INCOMPLETE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cyclesService.getIncompleteCycles(workspaceSlug as string, projectId as string) + ? () => + cyclesService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "incomplete", + }) : null ); diff --git a/apps/app/components/modules/gantt-chart.tsx b/apps/app/components/modules/gantt-chart.tsx index fa92964c1..e24e1dd9a 100644 --- a/apps/app/components/modules/gantt-chart.tsx +++ b/apps/app/components/modules/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; @@ -38,9 +40,23 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/modules-list-gantt-chart.tsx index cb1c7bc07..0f109ba6a 100644 --- a/apps/app/components/modules/modules-list-gantt-chart.tsx +++ b/apps/app/components/modules/modules-list-gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // types import { IModule } from "types"; // constants @@ -38,9 +40,11 @@ export const ModulesListGanttChartView: FC = ({ modules }) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data.status)?.color }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
); diff --git a/apps/app/components/pages/pages-list/all-pages-list.tsx b/apps/app/components/pages/pages-list/all-pages-list.tsx index ec364eba6..8bae35530 100644 --- a/apps/app/components/pages/pages-list/all-pages-list.tsx +++ b/apps/app/components/pages/pages-list/all-pages-list.tsx @@ -18,7 +18,10 @@ export const AllPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? ALL_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getAllPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "all", + }) : null ); diff --git a/apps/app/components/pages/pages-list/favorite-pages-list.tsx b/apps/app/components/pages/pages-list/favorite-pages-list.tsx index bf0ab8133..b846fbb7e 100644 --- a/apps/app/components/pages/pages-list/favorite-pages-list.tsx +++ b/apps/app/components/pages/pages-list/favorite-pages-list.tsx @@ -18,7 +18,10 @@ export const FavoritePagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? FAVORITE_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getFavoritePages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "favorite", + }) : null ); diff --git a/apps/app/components/pages/pages-list/my-pages-list.tsx b/apps/app/components/pages/pages-list/my-pages-list.tsx index 41704e52e..832d2a350 100644 --- a/apps/app/components/pages/pages-list/my-pages-list.tsx +++ b/apps/app/components/pages/pages-list/my-pages-list.tsx @@ -18,7 +18,10 @@ export const MyPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? MY_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getMyPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "created_by_me", + }) : null ); diff --git a/apps/app/components/pages/pages-list/other-pages-list.tsx b/apps/app/components/pages/pages-list/other-pages-list.tsx index 7cf21a3e2..b8ab1bdee 100644 --- a/apps/app/components/pages/pages-list/other-pages-list.tsx +++ b/apps/app/components/pages/pages-list/other-pages-list.tsx @@ -18,7 +18,10 @@ export const OtherPagesList: React.FC = ({ viewType }) => { const { data: pages } = useSWR( workspaceSlug && projectId ? OTHER_PAGES_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => pagesService.getOtherPages(workspaceSlug as string, projectId as string) + ? () => + pagesService.getPagesWithParams(workspaceSlug as string, projectId as string, { + page_view: "created_by_other", + }) : null ); diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index e18e7480e..cfc8cb99c 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -162,14 +162,22 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member input width="w-full" > - {uninvitedPeople?.map((person) => ( - - {person.member.email} - - ))} + {uninvitedPeople && uninvitedPeople.length > 0 ? ( + <> + {uninvitedPeople?.map((person) => ( + + {person.member.email} + + ))} + + ) : ( +
+ Invite members to workspace before adding them to a project. +
+ )} )} /> diff --git a/apps/app/components/views/gantt-chart.tsx b/apps/app/components/views/gantt-chart.tsx index dc81f70fa..a445331b2 100644 --- a/apps/app/components/views/gantt-chart.tsx +++ b/apps/app/components/views/gantt-chart.tsx @@ -4,6 +4,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; // components import { GanttChartRoot } from "components/gantt-chart"; +// ui +import { Tooltip } from "components/ui"; // hooks import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view"; @@ -38,9 +40,23 @@ export const ViewIssuesGanttChartView: FC = ({}) => { className="flex-shrink-0 w-[4px] h-full" style={{ backgroundColor: data?.state_detail?.color || "#858e96" }} /> -
- {data?.name} -
+ +
+ {data?.name} +
+
+ {data.infoToggle && ( + +
+ + info + +
+
+ )} ); @@ -59,10 +75,20 @@ export const ViewIssuesGanttChartView: FC = ({}) => { const blockFormat = (blocks: any) => blocks && blocks.length > 0 ? blocks.map((_block: any) => { - if (_block?.start_date && _block.target_date) console.log("_block", _block); + let startDate = new Date(_block.created_at); + let targetDate = new Date(_block.updated_at); + let infoToggle = true; + + if (_block?.start_date && _block.target_date) { + startDate = _block?.start_date; + targetDate = _block.target_date; + infoToggle = false; + } + return { - start_date: new Date(_block.created_at), - target_date: new Date(_block.updated_at), + start_date: new Date(startDate), + target_date: new Date(targetDate), + infoToggle: infoToggle, data: _block, }; }) diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 044dad38b..5b286bcc1 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -83,8 +83,11 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; -export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => - `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId.toUpperCase()}`; + +export const CYCLE_CURRENT_LIST = (projectId: string) => + `CYCLE_CURRENT_LIST${projectId.toUpperCase()}`; +export const CYCLE_UPCOMING_LIST = (projectId: string) => + `CYCLE_UPCOMING_LIST${projectId.toUpperCase()}`; export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId.toUpperCase()}`; export const CYCLE_COMPLETE_LIST = (projectId: string) => diff --git a/apps/app/helpers/array.helper.ts b/apps/app/helpers/array.helper.ts index 3cd326be7..2432f88ad 100644 --- a/apps/app/helpers/array.helper.ts +++ b/apps/app/helpers/array.helper.ts @@ -8,10 +8,12 @@ export const groupBy = (array: any[], key: string) => { }; export const orderArrayBy = ( - array: any[], + orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending" ) => { + const array = [...orgArray]; + if (!array || !Array.isArray(array) || array.length === 0) return []; if (key[0] === "-") { diff --git a/apps/app/hooks/use-my-issues-filter.tsx b/apps/app/hooks/use-my-issues-filter.tsx index bd97427f5..8c5d3eaa5 100644 --- a/apps/app/hooks/use-my-issues-filter.tsx +++ b/apps/app/hooks/use-my-issues-filter.tsx @@ -1,26 +1,18 @@ -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import useSWR from "swr"; +import { useState, useEffect, useCallback } from "react"; +import useSWR, { mutate } from "swr"; // services -import stateService from "services/state.service"; -import userService from "services/user.service"; +import workspaceService from "services/workspace.service"; // hooks import useUser from "hooks/use-user"; -// helpers -import { groupBy } from "helpers/array.helper"; -import { getStatesList } from "helpers/state.helper"; // types -import { Properties, NestedKeyOf, IIssue } from "types"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; -// constants -import { PRIORITIES } from "constants/project"; +import { IWorkspaceMember, Properties } from "types"; +import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys"; const initialValues: Properties = { assignee: true, due_date: false, key: true, - labels: true, + labels: false, priority: false, state: true, sub_issue_count: false, @@ -29,99 +21,80 @@ const initialValues: Properties = { estimate: false, }; -// TODO: Refactor this logic -const useMyIssuesProperties = (issues?: IIssue[]) => { +const useMyIssuesProperties = (workspaceSlug?: string) => { const [properties, setProperties] = useState(initialValues); - const [groupByProperty, setGroupByProperty] = useState | null>(null); - - // FIXME: where this hook is used we may not have project id in the url - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; const { user } = useUser(); - const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - const states = getStatesList(stateGroups ?? {}); - - useEffect(() => { - if (!user) return; - setProperties({ ...initialValues, ...user.my_issues_prop?.properties }); - setGroupByProperty(user.my_issues_prop?.groupBy ?? null); - }, [user]); - - const groupedByIssues: { - [key: string]: IIssue[]; - } = { - ...(groupByProperty === "state_detail.name" - ? Object.fromEntries( - states - ?.sort((a, b) => a.sequence - b.sequence) - ?.map((state) => [ - state.name, - issues?.filter((issue) => issue.state === state.name) ?? [], - ]) ?? [] - ) - : groupByProperty === "priority" - ? Object.fromEntries( - PRIORITIES.map((priority) => [ - priority, - issues?.filter((issue) => issue.priority === priority) ?? [], - ]) - ) - : {}), - ...groupBy(issues ?? [], groupByProperty ?? ""), - }; - - const setMyIssueProperty = (key: keyof Properties) => { - if (!user) return; - userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } }); - setProperties((prevData) => ({ - ...prevData, - [key]: !prevData[key], - })); - localStorage.setItem( - "my_issues_prop", - JSON.stringify({ - properties: { - ...properties, - [key]: !properties[key], - }, - groupBy: groupByProperty, - }) - ); - }; - - const setMyIssueGroupByProperty = (groupByProperty: NestedKeyOf | null) => { - if (!user) return; - userService.updateUser({ my_issues_prop: { properties, groupBy: groupByProperty } }); - setGroupByProperty(groupByProperty); - localStorage.setItem( - "my_issues_prop", - JSON.stringify({ properties, groupBy: groupByProperty }) - ); - }; - - useEffect(() => { - const viewProps = localStorage.getItem("my_issues_prop"); - if (viewProps) { - const { properties, groupBy } = JSON.parse(viewProps); - setProperties(properties); - setGroupByProperty(groupBy); + const { data: myWorkspace } = useSWR( + workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug as string) : null, + workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug as string) : null, + { + shouldRetryOnError: false, } - }, []); + ); - return { - filteredIssues: groupedByIssues, - groupByProperty, - properties, - setMyIssueProperty, - setMyIssueGroupByProperty, - } as const; + useEffect(() => { + if (!myWorkspace || !workspaceSlug || !user) return; + + setProperties({ ...initialValues, ...myWorkspace.view_props }); + + if (!myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + }, [myWorkspace, workspaceSlug, user]); + + const updateIssueProperties = useCallback( + (key: keyof Properties) => { + if (!workspaceSlug || !user) return; + + setProperties((prev) => ({ ...prev, [key]: !prev[key] })); + + if (myWorkspace) { + mutate( + WORKSPACE_MEMBERS_ME(workspaceSlug.toString()), + (prevData) => { + if (!prevData) return; + return { + ...prevData, + view_props: { ...prevData?.view_props, [key]: !prevData.view_props?.[key] }, + }; + }, + false + ); + if (myWorkspace.view_props) { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { + ...myWorkspace.view_props, + [key]: !myWorkspace.view_props[key], + }, + }); + } else { + workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: { ...initialValues }, + }); + } + } + }, + [workspaceSlug, myWorkspace, user] + ); + + const newProperties: Properties = { + assignee: properties.assignee, + due_date: properties.due_date, + key: properties.key, + labels: properties.labels, + priority: properties.priority, + state: properties.state, + sub_issue_count: properties.sub_issue_count, + attachment_count: properties.attachment_count, + link: properties.link, + estimate: properties.estimate, + }; + + return [newProperties, updateIssueProperties] as const; }; export default useMyIssuesProperties; diff --git a/apps/app/hooks/use-projects.tsx b/apps/app/hooks/use-projects.tsx index 247fc0db6..a8a2d455b 100644 --- a/apps/app/hooks/use-projects.tsx +++ b/apps/app/hooks/use-projects.tsx @@ -19,9 +19,9 @@ const useProjects = () => { workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null ); - const recentProjects = projects + const recentProjects = [...(projects ?? [])] ?.sort((a, b) => Date.parse(`${a.updated_at}`) - Date.parse(`${b.updated_at}`)) - .filter((_item, index) => index < 3); + ?.slice(0, 3); return { projects: orderArrayBy(projects ?? [], "is_favorite", "descending") || [], diff --git a/apps/app/pages/[workspaceSlug]/analytics.tsx b/apps/app/pages/[workspaceSlug]/analytics.tsx index b32399448..ce161fbc4 100644 --- a/apps/app/pages/[workspaceSlug]/analytics.tsx +++ b/apps/app/pages/[workspaceSlug]/analytics.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from "react"; +import React, { Fragment, useEffect } from "react"; import { useRouter } from "next/router"; @@ -10,6 +10,7 @@ import { useForm } from "react-hook-form"; import { Tab } from "@headlessui/react"; // services import analyticsService from "services/analytics.service"; +import trackEventServices from "services/track-event.service"; // layouts import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; // components @@ -48,6 +49,28 @@ const Analytics = () => { workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null ); + const trackAnalyticsEvent = (tab: string) => { + const eventPayload = { + workspaceSlug: workspaceSlug?.toString(), + }; + + const eventType = + tab === "Scope and Demand" + ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + : "WORKSPACE_CUSTOM_ANALYTICS"; + + trackEventServices.trackAnalyticsEvent(eventPayload, eventType); + }; + + useEffect(() => { + if (!workspaceSlug) return; + + trackEventServices.trackAnalyticsEvent( + { workspaceSlug: workspaceSlug?.toString() }, + "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + ); + }, [workspaceSlug]); + return ( { selected ? "bg-brand-surface-2" : "" }` } + onClick={() => trackAnalyticsEvent(tab)} > {tab} diff --git a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx index d742a7aa5..c66fcee55 100644 --- a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx +++ b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx @@ -14,7 +14,7 @@ import useIssues from "hooks/use-issues"; import { Spinner, EmptySpace, EmptySpaceItem, PrimaryButton } from "components/ui"; import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // hooks -import useIssuesProperties from "hooks/use-issue-properties"; +import useMyIssuesProperties from "hooks/use-my-issues-filter"; // types import { IIssue, Properties } from "types"; // components @@ -31,10 +31,7 @@ const MyIssuesPage: NextPage = () => { // fetching user issues const { myIssues } = useIssues(workspaceSlug as string); - const [properties, setProperties] = useIssuesProperties( - workspaceSlug ? (workspaceSlug as string) : undefined, - undefined - ); + const [properties, setProperties] = useMyIssuesProperties(workspaceSlug as string); return ( { const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleServices.getCycles(workspaceSlug as string, projectId as string) + ? () => + cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "all", + }) : null ); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 377d20fae..fd6df9832 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -21,10 +21,11 @@ import { SelectCycleType } from "types"; import type { NextPage } from "next"; // fetch-keys import { - CYCLE_CURRENT_AND_UPCOMING_LIST, CYCLE_DRAFT_LIST, PROJECT_DETAILS, - CYCLE_DETAILS, + CYCLE_UPCOMING_LIST, + CYCLE_CURRENT_LIST, + CYCLE_LIST, } from "constants/fetch-keys"; const ProjectCycles: NextPage = () => { @@ -44,21 +45,40 @@ const ProjectCycles: NextPage = () => { const { data: draftCycles } = useSWR( workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleService.getDraftCycles(workspaceSlug as string, projectId as string) + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "draft", + }) : null ); - const { data: currentAndUpcomingCycles } = useSWR( - workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null, + const { data: currentCycle } = useSWR( + workspaceSlug && projectId ? CYCLE_CURRENT_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string) + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "current", + }) + : null + ); + + const { data: upcomingCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_UPCOMING_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "upcoming", + }) : null ); const { data: cyclesCompleteList } = useSWR( - workspaceSlug && projectId ? CYCLE_DETAILS(projectId as string) : null, + workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null, workspaceSlug && projectId - ? () => cycleService.getCycles(workspaceSlug as string, projectId as string) + ? () => + cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, { + cycle_view: "all", + }) : null ); @@ -121,7 +141,8 @@ const ProjectCycles: NextPage = () => { setSelectedCycle={setSelectedCycle} setCreateUpdateCycleModal={setCreateUpdateCycleModal} cyclesCompleteList={cyclesCompleteList} - currentAndUpcomingCycles={currentAndUpcomingCycles} + currentCycle={currentCycle} + upcomingCycles={upcomingCycles} draftCycles={draftCycles} />
diff --git a/apps/app/pages/[workspaceSlug]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/settings/members.tsx index 4e6e4f579..fd80de11b 100644 --- a/apps/app/pages/[workspaceSlug]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/members.tsx @@ -66,6 +66,7 @@ const MembersSettings: NextPage = () => { role: item.role, status: true, member: true, + accountCreated: true, })) || []), ...(workspaceInvitations?.map((item) => ({ id: item.id, @@ -77,6 +78,7 @@ const MembersSettings: NextPage = () => { role: item.role, status: item.accepted, member: false, + accountCreated: item?.accepted ? false : true, })) || []), ]; @@ -200,6 +202,11 @@ const MembersSettings: NextPage = () => {

Pending

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

Account not created

+
+ )} { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getIncompleteCycles(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/incomplete-cycles/`) + async getCyclesWithParams( + workspaceSlug: string, + projectId: string, + queries: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, { + params: queries, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -159,40 +149,6 @@ class ProjectCycleServices extends APIService { }); } - async getCurrentAndUpcomingCycles( - workspaceSlug: string, - projectId: string - ): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/` - ) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getDraftCycles(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getCompletedCycles( - workspaceSlug: string, - projectId: string - ): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/` - ) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - async addCycleToFavorites( workspaceSlug: string, projectId: string, diff --git a/apps/app/services/pages.service.ts b/apps/app/services/pages.service.ts index 89fba2624..72776c29e 100644 --- a/apps/app/services/pages.service.ts +++ b/apps/app/services/pages.service.ts @@ -83,42 +83,26 @@ class PageServices extends APIService { }); } + async getPagesWithParams( + workspaceSlug: string, + projectId: string, + queries: any + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getRecentPages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/recent-pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getAllPages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getFavoritePages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/favorite-pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getMyPages(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/my-pages/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getOtherPages(workspaceSlug: string, projectId: string): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/created-by-other-pages/` - ) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, { + params: { + page_view: "recent", + }, + }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/app/services/track-event.service.ts b/apps/app/services/track-event.service.ts index 54de7119b..29ea0084a 100644 --- a/apps/app/services/track-event.service.ts +++ b/apps/app/services/track-event.service.ts @@ -84,6 +84,21 @@ type ImporterEventType = | "GITHUB_IMPORTER_DELETE" | "JIRA_IMPORTER_CREATE" | "JIRA_IMPORTER_DELETE"; + +type AnalyticsEventType = + | "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" + | "WORKSPACE_CUSTOM_ANALYTICS" + | "WORKSPACE_ANALYTICS_EXPORT" + | "PROJECT_SCOPE_AND_DEMAND_ANALYTICS" + | "PROJECT_CUSTOM_ANALYTICS" + | "PROJECT_ANALYTICS_EXPORT" + | "CYCLE_SCOPE_AND_DEMAND_ANALYTICS" + | "CYCLE_CUSTOM_ANALYTICS" + | "CYCLE_ANALYTICS_EXPORT" + | "MODULE_SCOPE_AND_DEMAND_ANALYTICS" + | "MODULE_CUSTOM_ANALYTICS" + | "MODULE_ANALYTICS_EXPORT"; + class TrackEventServices extends APIService { constructor() { super("/"); @@ -615,6 +630,19 @@ class TrackEventServices extends APIService { }, }); } + + async trackAnalyticsEvent(data: any, eventName: AnalyticsEventType): Promise { + const payload = { ...data }; + + return this.request({ + url: "/api/track-event", + method: "POST", + data: { + eventName, + extra: payload, + }, + }); + } } const trackEventServices = new TrackEventServices(); diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index 9fb7d61ce..004240e5d 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -142,6 +142,14 @@ class WorkspaceService extends APIService { }); } + async updateWorkspaceView(workspaceSlug: string, data: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/workspace-views/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async updateWorkspaceMember( workspaceSlug: string, memberId: string, diff --git a/apps/app/types/cycles.d.ts b/apps/app/types/cycles.d.ts index ee4b2cef6..a8da352ed 100644 --- a/apps/app/types/cycles.d.ts +++ b/apps/app/types/cycles.d.ts @@ -38,19 +38,6 @@ export interface ICycle { workspace_detail: IWorkspaceLite; } -export interface CurrentAndUpcomingCyclesResponse { - current_cycle: ICycle[]; - upcoming_cycle: ICycle[]; -} - -export interface DraftCyclesResponse { - draft_cycles: ICycle[]; -} - -export interface CompletedCyclesResponse { - completed_cycles: ICycle[]; -} - export interface CycleIssueResponse { id: string; issue_detail: IIssue; diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index fbb54d759..a88a0f15b 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -33,6 +33,19 @@ export interface IWorkspaceMemberInvitation { workspace: IWorkspace; } +export type Properties = { + assignee: boolean; + due_date: boolean; + labels: boolean; + key: boolean; + priority: boolean; + state: boolean; + sub_issue_count: boolean; + link: boolean; + attachment_count: boolean; + estimate: boolean; +}; + export interface IWorkspaceMember { readonly id: string; user: IUserLite; @@ -40,6 +53,7 @@ export interface IWorkspaceMember { member: IUserLite; role: 5 | 10 | 15 | 20; company_role: string | null; + view_props: Properties; created_at: Date; updated_at: Date; created_by: string;