diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 0b5c612d39..34bce8a0a9 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1668,15 +1668,9 @@ class IssueDraftViewSet(BaseViewSet): def get_queryset(self): return ( - Issue.objects.annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") + Issue.objects.filter( + project_id=self.kwargs.get("project_id") ) - .filter(project_id=self.kwargs.get("project_id")) .filter(workspace__slug=self.kwargs.get("slug")) .filter(is_draft=True) .select_related("workspace", "project", "state", "parent") @@ -1710,7 +1704,7 @@ class IssueDraftViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - ) + ).distinct() @method_decorator(gzip_page) def list(self, request, slug, project_id): @@ -1832,7 +1826,10 @@ class IssueDraftViewSet(BaseViewSet): notification=True, origin=request.META.get("HTTP_ORIGIN"), ) - return Response(serializer.data, status=status.HTTP_201_CREATED) + issue = ( + self.get_queryset().filter(pk=serializer.data["id"]).first() + ) + return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def partial_update(self, request, slug, project_id, pk): @@ -1868,10 +1865,13 @@ class IssueDraftViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True + issue = self.get_queryset().filter(pk=pk).first() + return Response( + IssueSerializer( + issue, fields=self.fields, expand=self.expand + ).data, + status=status.HTTP_200_OK, ) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get( diff --git a/apiserver/plane/db/migrations/0059_auto_20240208_0957.py b/apiserver/plane/db/migrations/0059_auto_20240208_0957.py new file mode 100644 index 0000000000..c4c43fa4bf --- /dev/null +++ b/apiserver/plane/db/migrations/0059_auto_20240208_0957.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.7 on 2024-02-08 09:57 + +from django.db import migrations + + +def widgets_filter_change(apps, schema_editor): + Widget = apps.get_model("db", "Widget") + widgets_to_update = [] + + # Define the filter dictionaries for each widget key + filters_mapping = { + "assigned_issues": {"duration": "none", "tab": "pending"}, + "created_issues": {"duration": "none", "tab": "pending"}, + "issues_by_state_groups": {"duration": "none"}, + "issues_by_priority": {"duration": "none"}, + } + + # Iterate over widgets and update filters if applicable + for widget in Widget.objects.all(): + if widget.key in filters_mapping: + widget.filters = filters_mapping[widget.key] + widgets_to_update.append(widget) + + # Bulk update the widgets + Widget.objects.bulk_update(widgets_to_update, ["filters"], batch_size=10) + +class Migration(migrations.Migration): + dependencies = [ + ('db', '0058_alter_moduleissue_issue_and_more'), + ] + operations = [ + migrations.RunPython(widgets_filter_change) + ] diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html index a7990562d4..3c561f37ac 100644 --- a/apiserver/templates/emails/notifications/issue-updates.html +++ b/apiserver/templates/emails/notifications/issue-updates.html @@ -66,7 +66,7 @@ style="margin-left: 30px; margin-bottom: 20px; margin-top: 20px" > - + {% if update.changes.state.old_value.0 == 'Backlog' or update.changes.state.old_value.0 == 'In Progress' or update.changes.state.old_value.0 == 'Done' or update.changes.state.old_value.0 == 'Cancelled' %} + + {% endif %}

- + {% if update.changes.state.new_value|last == 'Backlog' or update.changes.state.new_value|last == 'In Progress' or update.changes.state.new_value|last == 'Done' or update.changes.state.new_value|last == 'Cancelled' %} + + {% endif %}

(

{React.Children.map(children, (child, index) => ( -
+
{child} {index !== React.Children.count(children) - 1 && (
diff --git a/web/components/gantt-chart/sidebar/module-sidebar.tsx b/web/components/gantt-chart/sidebar/module-sidebar.tsx index 30f146dc55..8f8788787c 100644 --- a/web/components/gantt-chart/sidebar/module-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/module-sidebar.tsx @@ -93,7 +93,7 @@ export const ModuleGanttSidebar: React.FC = (props) => { <> {blocks ? ( blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? ""); + const duration = findTotalDaysInRange(block.start_date, block.target_date); return ( = (props) => {
-
- {duration} day{duration > 1 ? "s" : ""} -
+ {duration !== undefined && ( +
+ {duration} day{duration > 1 ? "s" : ""} +
+ )}
diff --git a/web/components/gantt-chart/sidebar/project-view-sidebar.tsx b/web/components/gantt-chart/sidebar/project-view-sidebar.tsx index da7382859f..6e31215c1c 100644 --- a/web/components/gantt-chart/sidebar/project-view-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/project-view-sidebar.tsx @@ -94,7 +94,7 @@ export const ProjectViewGanttSidebar: React.FC = (props) => { <> {blocks ? ( blocks.map((block, index) => { - const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? ""); + const duration = findTotalDaysInRange(block.start_date, block.target_date); return ( = (props) => {
-
- {duration} day{duration > 1 ? "s" : ""} -
+ {duration !== undefined && ( +
+ {duration} day{duration > 1 ? "s" : ""} +
+ )} diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index bca39a0bd6..12de8e127f 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -119,10 +119,7 @@ export const IssueGanttSidebar: React.FC = (props) => { // hide the block if it doesn't have start and target dates and showAllBlocks is false if (!showAllBlocks && !isBlockVisibleOnSidebar) return; - const duration = - !block.start_date || !block.target_date - ? null - : findTotalDaysInRange(block.start_date, block.target_date); + const duration = findTotalDaysInRange(block.start_date, block.target_date); return ( = (props) => {
-
- {duration && ( + {duration !== undefined && ( +
{duration} day{duration > 1 ? "s" : ""} - )} -
+
+ )} diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 0fe6a74c5b..139ec02579 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -103,7 +103,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => { } /> + } /> } /> diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index c819b25c34..81e2d2d76b 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -200,8 +200,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => { handleDisplayPropertiesUpdate={handleDisplayProperties} /> - - {currentProjectDetails?.inbox_view && inboxDetails && ( + + {currentProjectDetails?.inbox_view && inboxDetails && ( - )} - - + )} {canUserCreateIssue && ( <> + - -); + ) +}); + + diff --git a/web/components/headers/workspace-dashboard.tsx b/web/components/headers/workspace-dashboard.tsx index d074132e26..d8306ab40e 100644 --- a/web/components/headers/workspace-dashboard.tsx +++ b/web/components/headers/workspace-dashboard.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { LayoutGrid, Zap } from "lucide-react"; import Image from "next/image"; import { useTheme } from "next-themes"; @@ -6,18 +5,16 @@ import { useTheme } from "next-themes"; import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; // components -import { BreadcrumbLink, ProductUpdatesModal } from "components/common"; +import { BreadcrumbLink } from "components/common"; import { Breadcrumbs } from "@plane/ui"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; export const WorkspaceDashboardHeader = () => { - const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false); // hooks const { resolvedTheme } = useTheme(); return ( <> -
diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 3cf88cb7c4..3904049e9c 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -1,6 +1,5 @@ export * from "./attachment"; export * from "./issue-modal"; -export * from "./view-select"; export * from "./delete-issue-modal"; export * from "./description-form"; export * from "./issue-layouts"; diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 99d774bd57..203ac4938b 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -66,16 +66,22 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop
- handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - > + {issue?.is_draft ? ( {issue.name} - + ) : ( + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > + + {issue.name} + + + )} = observer((props) => { return ( <> - {isDraftIssue ? ( - setIsOpen(false)} - prePopulateData={issuePayload} - fieldsToShow={["all"]} - /> - ) : ( - setIsOpen(false)} - data={issuePayload} - storeType={storeType} - /> - )} + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + isDraft={isDraftIssue} + /> + {renderExistingIssueModal && ( = observer((props: IssueBlock
)} - handleIssuePeekOverview(issue)} - className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" - > + {issue?.is_draft ? ( {issue.name} - + ) : ( + handleIssuePeekOverview(issue)} + className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" + > + + {issue.name} + + + )}
{!issue?.tempId ? ( diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 49c9f7e406..90270e1a15 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -109,21 +109,13 @@ export const HeaderGroupByCard = observer(
))} - {isDraftIssue ? ( - setIsOpen(false)} - prePopulateData={issuePayload} - fieldsToShow={["all"]} - /> - ) : ( - setIsOpen(false)} - data={issuePayload} - storeType={storeType} - /> - )} + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + isDraft={isDraftIssue} + /> {renderExistingIssueModal && ( = (props) => }; delete duplicateIssuePayload.id; + const isDraftIssue = router?.asPath?.includes("draft-issues") || false; + return ( <> = (props) => handleClose={() => setDeleteIssueModal(false)} onSubmit={handleDelete} /> + { @@ -73,7 +76,9 @@ export const ProjectIssueQuickActions: React.FC = (props) => if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data }); }} storeType={EIssuesStoreType.PROJECT} + isDraft={isDraftIssue} /> + void; onSubmit: (formData: Partial) => Promise; projectId: string; + isDraft: boolean; } const issueDraftService = new IssueDraftService(); @@ -35,6 +36,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { projectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange, + isDraft, } = props; // states const [issueDiscardModal, setIssueDiscardModal] = useState(false); @@ -107,6 +109,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { onClose={handleClose} onSubmit={onSubmit} projectId={projectId} + isDraft={isDraft} /> ); diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 31cb9dd669..544ebeb15d 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState, useRef, useEffect } from "react"; +import React, { FC, useState, useRef, useEffect, Fragment } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; @@ -55,8 +55,9 @@ export interface IssueFormProps { onCreateMoreToggleChange: (value: boolean) => void; onChange?: (formData: Partial | null) => void; onClose: () => void; - onSubmit: (values: Partial) => Promise; + onSubmit: (values: Partial, is_draft_issue?: boolean) => Promise; projectId: string; + isDraft: boolean; } // services @@ -72,6 +73,7 @@ export const IssueFormRoot: FC = observer((props) => { projectId: defaultProjectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange, + isDraft, } = props; // states const [labelModal, setLabelModal] = useState(false); @@ -137,8 +139,8 @@ export const IssueFormRoot: FC = observer((props) => { const issueName = watch("name"); - const handleFormSubmit = async (formData: Partial) => { - await onSubmit(formData); + const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { + await onSubmit(formData, is_draft_issue); setGptAssistantModal(false); @@ -248,7 +250,7 @@ export const IssueFormRoot: FC = observer((props) => { }} /> )} -
+ handleFormSubmit(data))}>
{/* Don't show project selection if editing an issue */} @@ -670,7 +672,34 @@ export const IssueFormRoot: FC = observer((props) => { - + ) : ( + + )} + + )} + +
diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 3b5b35ceac..02a0873148 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -20,10 +20,19 @@ export interface IssuesModalProps { onSubmit?: (res: TIssue) => Promise; withDraftIssueWrapper?: boolean; storeType?: TCreateModalStoreTypes; + isDraft?: boolean; } export const CreateUpdateIssueModal: React.FC = observer((props) => { - const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true, storeType = EIssuesStoreType.PROJECT } = props; + const { + data, + isOpen, + onClose, + onSubmit, + withDraftIssueWrapper = true, + storeType = EIssuesStoreType.PROJECT, + isDraft = false, + } = props; // states const [changesMade, setChangesMade] = useState | null>(null); const [createMore, setCreateMore] = useState(false); @@ -42,6 +51,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); + const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT); // store mapping based on current store const issueStores = { [EIssuesStoreType.PROJECT]: { @@ -122,11 +132,16 @@ export const CreateUpdateIssueModal: React.FC = observer((prop onClose(); }; - const handleCreateIssue = async (payload: Partial): Promise => { + const handleCreateIssue = async ( + payload: Partial, + is_draft_issue: boolean = false + ): Promise => { if (!workspaceSlug || !payload.project_id) return; try { - const response = await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); + const response = is_draft_issue + ? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload) + : await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); if (!response) throw new Error(); currentIssueStore.fetchIssues(workspaceSlug, payload.project_id, "mutation", viewId); @@ -213,7 +228,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop } }; - const handleFormSubmit = async (formData: Partial) => { + const handleFormSubmit = async (formData: Partial, is_draft_issue: boolean = false) => { if (!workspaceSlug || !formData.project_id || !storeType) return; const payload: Partial = { @@ -222,7 +237,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop }; let response: TIssue | undefined = undefined; - if (!data?.id) response = await handleCreateIssue(payload); + if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue); else response = await handleUpdateIssue(payload); if (response != undefined && onSubmit) await onSubmit(response); @@ -274,6 +289,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop projectId={activeProjectId} isCreateMoreToggleEnabled={createMore} onCreateMoreToggleChange={handleCreateMoreToggleChange} + isDraft={isDraft} /> ) : ( = observer((prop onCreateMoreToggleChange={handleCreateMoreToggleChange} onSubmit={handleFormSubmit} projectId={activeProjectId} + isDraft={isDraft} /> )} diff --git a/web/components/issues/view-select/due-date.tsx b/web/components/issues/view-select/due-date.tsx deleted file mode 100644 index d61e7586ac..0000000000 --- a/web/components/issues/view-select/due-date.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// ui -import { CustomDatePicker } from "components/ui"; -import { Tooltip } from "@plane/ui"; -import { CalendarCheck } from "lucide-react"; -// helpers -import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; -// types -import { TIssue } from "@plane/types"; - -type Props = { - issue: TIssue; - onChange: (date: string | null) => void; - handleOnOpen?: () => void; - handleOnClose?: () => void; - tooltipPosition?: "top" | "bottom"; - className?: string; - noBorder?: boolean; - disabled: boolean; -}; - -export const ViewDueDateSelect: React.FC = ({ - issue, - onChange, - handleOnOpen, - handleOnClose, - tooltipPosition = "top", - className = "", - noBorder = false, - disabled, -}) => { - const minDate = issue.start_date ? new Date(issue.start_date) : null; - minDate?.setDate(minDate.getDate()); - - return ( - -
- - {issue.target_date ? ( - <> - - {renderFormattedDate(issue.target_date) ?? "_ _"} - - ) : ( - <> - - Due Date - - )} -
- } - minDate={minDate ?? undefined} - noBorder={noBorder} - handleOnOpen={handleOnOpen} - handleOnClose={handleOnClose} - disabled={disabled} - /> -
- - ); -}; diff --git a/web/components/issues/view-select/estimate.tsx b/web/components/issues/view-select/estimate.tsx deleted file mode 100644 index 1739f3aaad..0000000000 --- a/web/components/issues/view-select/estimate.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react-lite"; -import { Triangle } from "lucide-react"; -import sortBy from "lodash/sortBy"; -// store hooks -import { useEstimate } from "hooks/store"; -// ui -import { CustomSelect, Tooltip } from "@plane/ui"; -// types -import { TIssue } from "@plane/types"; - -type Props = { - issue: TIssue; - onChange: (data: number) => void; - tooltipPosition?: "top" | "bottom"; - customButton?: boolean; - disabled: boolean; -}; - -export const ViewEstimateSelect: React.FC = observer((props) => { - const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props; - const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate(); - - const estimateValue = getEstimatePointValue(issue.estimate_point, issue.project_id); - - const estimateLabels = ( - -
- - {estimateValue ?? "None"} -
-
- ); - - if (!areEstimatesEnabledForCurrentProject) return null; - - return ( - - - <> - - - - None - - - {sortBy(activeEstimateDetails?.points, "key")?.map((estimate) => ( - - <> - - {estimate.value} - - - ))} - - ); -}); diff --git a/web/components/issues/view-select/index.ts b/web/components/issues/view-select/index.ts deleted file mode 100644 index 8eb88cb0de..0000000000 --- a/web/components/issues/view-select/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./due-date"; -export * from "./estimate"; -export * from "./start-date"; diff --git a/web/components/issues/view-select/start-date.tsx b/web/components/issues/view-select/start-date.tsx deleted file mode 100644 index 039bc0cb5e..0000000000 --- a/web/components/issues/view-select/start-date.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// ui -import { CustomDatePicker } from "components/ui"; -import { Tooltip } from "@plane/ui"; -import { CalendarClock } from "lucide-react"; -// helpers -import { renderFormattedDate } from "helpers/date-time.helper"; -// types -import { TIssue } from "@plane/types"; - -type Props = { - issue: TIssue; - onChange: (date: string | null) => void; - handleOnOpen?: () => void; - handleOnClose?: () => void; - tooltipPosition?: "top" | "bottom"; - className?: string; - noBorder?: boolean; - disabled: boolean; -}; - -export const ViewStartDateSelect: React.FC = ({ - issue, - onChange, - handleOnOpen, - handleOnClose, - tooltipPosition = "top", - className = "", - noBorder = false, - disabled, -}) => { - const maxDate = issue.target_date ? new Date(issue.target_date) : null; - maxDate?.setDate(maxDate.getDate()); - - return ( - -
- - {issue?.start_date ? ( - <> - - {renderFormattedDate(issue?.start_date ?? "_ _")} - - ) : ( - <> - - Start Date - - )} -
- } - handleOnClose={handleOnClose} - disabled={disabled} - /> -
- - ); -}; diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index d1cbd0dfab..53948f71db 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -13,37 +13,32 @@ export const ModulesListGanttChartView: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store - const { projectModuleIds, moduleMap } = useModule(); const { currentProjectDetails } = useProject(); + const { projectModuleIds, moduleMap, updateModuleDetails } = useModule(); - const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => { - if (!workspaceSlug) return; - // FIXME - //updateModuleGanttStructure(workspaceSlug.toString(), module.project, module, payload); + const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => { + if (!workspaceSlug || !module) return; + + const payload: any = { ...data }; + if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder; + + await updateModuleDetails(workspaceSlug.toString(), module.project, module.id, payload); }; const blockFormat = (blocks: string[]) => - blocks && blocks.length > 0 - ? blocks - .filter((blockId) => { - const block = moduleMap[blockId]; - return block.start_date && block.target_date && new Date(block.start_date) <= new Date(block.target_date); - }) - .map((blockId) => { - const block = moduleMap[blockId]; - return { - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.target_date ?? ""), - }; - }) - : []; + blocks?.map((blockId) => { + const block = moduleMap[blockId]; + return { + data: block, + id: block.id, + sort_order: block.sort_order, + start_date: block.start_date ? new Date(block.start_date) : null, + target_date: block.target_date ? new Date(block.target_date) : null, + }; + }); const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15; - const modules = projectModuleIds; return (
{ enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} enableReorder={isAllowed} + showAllBlocks />
); diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index b08c443d2c..e9ed56a8de 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -77,7 +77,7 @@ export const ModuleMobileHeader = () => { ); return ( - <> +
setAnalyticsModal(false)} @@ -157,6 +157,6 @@ export const ModuleMobileHeader = () => { Analytics
- +
); }; diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 58b79ac3eb..7a372c5d8d 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -208,9 +208,6 @@ export const NotificationCard: React.FC = (props) => { void }) => { - e.stopPropagation(); - }} customButton={
diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 44dfe57d18..4361b7a9d2 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -22,16 +22,15 @@ export const ProfileNavbar: React.FC = (props) => { const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; return ( -
+
{tabsList.map((tab) => ( {tab.label} diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 3ce7747c9b..b356b5adb3 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -4,7 +4,7 @@ import useSWR from "swr"; import { Disclosure, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; +import { useApplication, useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; // components @@ -18,6 +18,8 @@ import { renderFormattedDate } from "helpers/date-time.helper"; import { renderEmoji } from "helpers/emoji.helper"; // fetch-keys import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { useEffect, useRef } from "react"; // services const userService = new UserService(); @@ -28,6 +30,8 @@ export const ProfileSidebar = observer(() => { const { workspaceSlug, userId } = router.query; // store hooks const { currentUser } = useUser(); + const { theme: themStore } = useApplication(); + const ref = useRef(null); const { data: userProjectsData } = useSWR( workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null, @@ -36,6 +40,14 @@ export const ProfileSidebar = observer(() => { : null ); + useOutsideClickDetector(ref, () => { + if (themStore.profileSidebarCollapsed === false) { + if (window.innerWidth < 768) { + themStore.toggleProfileSidebar(); + } + } + }); + const userDetails = [ { label: "Joined on", @@ -47,8 +59,26 @@ export const ProfileSidebar = observer(() => { }, ]; + useEffect(() => { + const handleToggleProfileSidebar = () => { + if (window && window.innerWidth < 768) { + themStore.toggleProfileSidebar(true); + } + if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) { + themStore.toggleProfileSidebar(false); + } + }; + + window.addEventListener("resize", handleToggleProfileSidebar); + handleToggleProfileSidebar(); + return () => window.removeEventListener("resize", handleToggleProfileSidebar); + }, [themStore]); + return ( -
+
{userProjectsData ? ( <>
@@ -132,13 +162,12 @@ export const ProfileSidebar = observer(() => { {project.assigned_issues > 0 && (
{completedIssuePercentage}%
diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index 8003f15e32..90319a90b9 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -4,6 +4,10 @@ import { renderFormattedPayloadDate } from "./date-time.helper"; // types import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; +/** + * @description returns date range based on the duration filter + * @param duration + */ export const getCustomDates = (duration: TDurationFilterOptions): string => { const today = new Date(); let firstDay, lastDay; @@ -30,6 +34,10 @@ export const getCustomDates = (duration: TDurationFilterOptions): string => { } }; +/** + * @description returns redirection filters for the issues list + * @param type + */ export const getRedirectionFilters = (type: TIssuesListTypes): string => { const today = renderFormattedPayloadDate(new Date()); @@ -44,3 +52,20 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { return filterParams; }; + +/** + * @description returns the tab key based on the duration filter + * @param duration + * @param tab + */ +export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListTypes | undefined): TIssuesListTypes => { + if (!tab) return "completed"; + + if (tab === "completed") return tab; + + if (duration === "none") return "pending"; + else { + if (["upcoming", "overdue"].includes(tab)) return tab; + else return "upcoming"; + } +}; diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index bc5daa2a35..b629e60ec3 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -87,11 +87,11 @@ export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | * @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8 */ export const findTotalDaysInRange = ( - startDate: Date | string, - endDate: Date | string, + startDate: Date | string | undefined | null, + endDate: Date | string | undefined | null, inclusive: boolean = true -): number => { - if (!startDate || !endDate) return 0; +): number | undefined => { + if (!startDate || !endDate) return undefined; // Parse the dates to check if they are valid const parsedStartDate = new Date(startDate); const parsedEndDate = new Date(endDate); @@ -110,8 +110,11 @@ export const findTotalDaysInRange = ( * @param {boolean} inclusive (optional) // default true * @example findHowManyDaysLeft("2024-01-01") // 3 */ -export const findHowManyDaysLeft = (date: string | Date, inclusive: boolean = true): number => { - if (!date) return 0; +export const findHowManyDaysLeft = ( + date: Date | string | undefined | null, + inclusive: boolean = true +): number | undefined => { + if (!date) return undefined; // Pass the date to findTotalDaysInRange function to find the total number of days in range from today return findTotalDaysInRange(new Date(), date, inclusive); }; diff --git a/web/hooks/use-reload-confirmation.tsx b/web/hooks/use-reload-confirmation.tsx index cdaff73652..8343ea78df 100644 --- a/web/hooks/use-reload-confirmation.tsx +++ b/web/hooks/use-reload-confirmation.tsx @@ -1,26 +1,41 @@ import { useCallback, useEffect, useState } from "react"; +import { useRouter } from "next/router"; -const useReloadConfirmations = (message?: string) => { +//TODO: remove temp flag isActive later and use showAlert as the source of truth +const useReloadConfirmations = (isActive = true) => { const [showAlert, setShowAlert] = useState(false); + const router = useRouter(); const handleBeforeUnload = useCallback( (event: BeforeUnloadEvent) => { + if (!isActive || !showAlert) return; event.preventDefault(); event.returnValue = ""; - return message ?? "Are you sure you want to leave?"; }, - [message] + [isActive, showAlert] + ); + + const handleRouteChangeStart = useCallback( + (url: string) => { + if (!isActive || !showAlert) return; + const leave = confirm("Are you sure you want to leave? Changes you made may not be saved."); + if (!leave) { + router.events.emit("routeChangeError"); + throw `Route change to "${url}" was aborted (this error can be safely ignored).`; + } + }, + [isActive, showAlert, router.events] ); useEffect(() => { - if (!showAlert) { - window.removeEventListener("beforeunload", handleBeforeUnload); - return; - } - window.addEventListener("beforeunload", handleBeforeUnload); - return () => window.removeEventListener("beforeunload", handleBeforeUnload); - }, [handleBeforeUnload, showAlert]); + router.events.on("routeChangeStart", handleRouteChangeStart); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + router.events.off("routeChangeStart", handleRouteChangeStart); + }; + }, [handleBeforeUnload, handleRouteChangeStart, router.events]); return { setShowAlert }; }; diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index 9d17350a98..b25935f4e2 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -2,6 +2,11 @@ import { FC, ReactNode } from "react"; // layout import { ProfileSettingsLayout } from "layouts/settings-layout"; import { ProfilePreferenceSettingsSidebar } from "./sidebar"; +import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; +import { CustomMenu } from "@plane/ui"; +import { ChevronDown } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; interface IProfilePreferenceSettingsLayout { children: ReactNode; @@ -10,9 +15,57 @@ interface IProfilePreferenceSettingsLayout { export const ProfilePreferenceSettingsLayout: FC = (props) => { const { children, header } = props; + const router = useRouter(); + + const showMenuItem = () => { + const item = router.asPath.split('/'); + let splittedItem = item[item.length - 1]; + splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase()); + console.log(splittedItem); + return splittedItem; + } + + const profilePreferenceLinks: Array<{ + label: string; + href: string; + }> = [ + { + label: "Theme", + href: `/profile/preferences/theme`, + }, + { + label: "Email", + href: `/profile/preferences/email`, + }, + ]; return ( - + + + + {showMenuItem()} + +
+ } + customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm" + > + <> + {profilePreferenceLinks.map((link) => ( + + {link.label} + + ))} + +
+ }>
diff --git a/web/layouts/settings-layout/profile/preferences/sidebar.tsx b/web/layouts/settings-layout/profile/preferences/sidebar.tsx index d1eec12331..7f43f3cad1 100644 --- a/web/layouts/settings-layout/profile/preferences/sidebar.tsx +++ b/web/layouts/settings-layout/profile/preferences/sidebar.tsx @@ -9,28 +9,27 @@ export const ProfilePreferenceSettingsSidebar = () => { label: string; href: string; }> = [ - { - label: "Theme", - href: `/profile/preferences/theme`, - }, - { - label: "Email", - href: `/profile/preferences/email`, - }, - ]; + { + label: "Theme", + href: `/profile/preferences/theme`, + }, + { + label: "Email", + href: `/profile/preferences/email`, + }, + ]; return ( -
+
Preference
{profilePreferenceLinks.map((link) => (
{link.label}
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 0a97b33648..4b8a1b8543 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { mutate } from "swr"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -12,6 +12,7 @@ import useToast from "hooks/use-toast"; import { Tooltip } from "@plane/ui"; // constants import { PROFILE_ACTION_LINKS } from "constants/profile"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; const WORKSPACE_ACTION_LINKS = [ { @@ -52,6 +53,35 @@ export const ProfileLayoutSidebar = observer(() => { currentUserSettings?.workspace?.fallback_workspace_slug || ""; + const ref = useRef(null); + + useOutsideClickDetector(ref, () => { + if (sidebarCollapsed === false) { + if (window.innerWidth < 768) { + toggleSidebar(); + } + } + }); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth <= 768) { + toggleSidebar(true); + } + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [toggleSidebar]); + + const handleItemClick = () => { + if (window.innerWidth < 768) { + toggleSidebar(); + } + }; + const handleSignOut = async () => { setIsSigningOut(true); @@ -73,16 +103,18 @@ export const ProfileLayoutSidebar = observer(() => { return (
-
+
@@ -101,14 +133,13 @@ export const ProfileLayoutSidebar = observer(() => { if (link.key === "change-password" && currentUser?.is_password_autoset) return null; return ( - +
{} {!sidebarCollapsed && link.label} @@ -129,19 +160,17 @@ export const ProfileLayoutSidebar = observer(() => { {workspace?.logo && workspace.logo !== "" ? ( { )}
{WORKSPACE_ACTION_LINKS.map((link) => ( - +
{} {!sidebarCollapsed && link.label} @@ -180,9 +208,8 @@ export const ProfileLayoutSidebar = observer(() => {