diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 7a9cfb1b5..6f66c373e 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -262,7 +262,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): return Response( { "error": "Cycle with the same external id and external source already exists", - "cycle": str(cycle.id), + "id": str(cycle.id), }, status=status.HTTP_409_CONFLICT, ) @@ -325,7 +325,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView): return Response( { "error": "Cycle with the same external id and external source already exists", - "cycle_id": str(cycle.id), + "id": str(cycle.id), }, status=status.HTTP_409_CONFLICT, ) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 530eef5bf..a759b15f6 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -239,7 +239,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): return Response( { "error": "Issue with the same external id and external source already exists", - "issue_id": str(issue.id), + "id": str(issue.id), }, status=status.HTTP_409_CONFLICT, ) @@ -286,14 +286,16 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): and Issue.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get("external_source", issue.external_source), + external_source=request.data.get( + "external_source", issue.external_source + ), external_id=request.data.get("external_id"), ).exists() ): return Response( { "error": "Issue with the same external id and external source already exists", - "issue_id": str(issue.id), + "id": str(issue.id), }, status=status.HTTP_409_CONFLICT, ) @@ -362,6 +364,30 @@ class LabelAPIEndpoint(BaseAPIView): try: serializer = LabelSerializer(data=request.data) if serializer.is_valid(): + if ( + request.data.get("external_id") + and request.data.get("external_source") + and Label.objects.filter( + project_id=project_id, + workspace__slug=slug, + external_source=request.data.get("external_source"), + external_id=request.data.get("external_id"), + ).exists() + ): + label = Label.objects.filter( + workspace__slug=slug, + project_id=project_id, + external_id=request.data.get("external_id"), + external_source=request.data.get("external_source"), + ).first() + return Response( + { + "error": "Label with the same external id and external source already exists", + "id": str(label.id), + }, + status=status.HTTP_409_CONFLICT, + ) + serializer.save(project_id=project_id) return Response( serializer.data, status=status.HTTP_201_CREATED @@ -370,11 +396,17 @@ class LabelAPIEndpoint(BaseAPIView): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) except IntegrityError: + label = Label.objects.filter( + workspace__slug=slug, + project_id=project_id, + name=request.data.get("name"), + ).first() return Response( { - "error": "Label with the same name already exists in the project" + "error": "Label with the same name already exists in the project", + "id": str(label.id), }, - status=status.HTTP_400_BAD_REQUEST, + status=status.HTTP_409_CONFLICT, ) def get(self, request, slug, project_id, pk=None): @@ -401,6 +433,25 @@ class LabelAPIEndpoint(BaseAPIView): label = self.get_queryset().get(pk=pk) serializer = LabelSerializer(label, data=request.data, partial=True) if serializer.is_valid(): + if ( + str(request.data.get("external_id")) + and (label.external_id != str(request.data.get("external_id"))) + and Issue.objects.filter( + project_id=project_id, + workspace__slug=slug, + external_source=request.data.get( + "external_source", label.external_source + ), + external_id=request.data.get("external_id"), + ).exists() + ): + return Response( + { + "error": "Label with the same external id and external source already exists", + "id": str(label.id), + }, + status=status.HTTP_409_CONFLICT, + ) serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index e2d59e126..d509a53c7 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -151,7 +151,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): return Response( { "error": "Module with the same external id and external source already exists", - "module_id": str(module.id), + "id": str(module.id), }, status=status.HTTP_409_CONFLICT, ) @@ -185,7 +185,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): return Response( { "error": "Module with the same external id and external source already exists", - "module_id": str(module.id), + "id": str(module.id), }, status=status.HTTP_409_CONFLICT, ) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 88fb083f0..0a262a071 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -57,7 +57,7 @@ class StateAPIEndpoint(BaseAPIView): return Response( { "error": "State with the same external id and external source already exists", - "state_id": str(state.id), + "id": str(state.id), }, status=status.HTTP_409_CONFLICT, ) @@ -128,7 +128,7 @@ class StateAPIEndpoint(BaseAPIView): return Response( { "error": "State with the same external id and external source already exists", - "state_id": str(state.id), + "id": str(state.id), }, status=status.HTTP_409_CONFLICT, ) diff --git a/apiserver/plane/app/views/dashboard.py b/apiserver/plane/app/views/dashboard.py index 47fae2c9c..1366a2886 100644 --- a/apiserver/plane/app/views/dashboard.py +++ b/apiserver/plane/app/views/dashboard.py @@ -145,6 +145,23 @@ def dashboard_assigned_issues(self, request, slug): ) ).order_by("priority_order") + if issue_type == "pending": + pending_issues_count = assigned_issues.filter( + state__group__in=["backlog", "started", "unstarted"] + ).count() + pending_issues = assigned_issues.filter( + state__group__in=["backlog", "started", "unstarted"] + )[:5] + return Response( + { + "issues": IssueSerializer( + pending_issues, many=True, expand=self.expand + ).data, + "count": pending_issues_count, + }, + status=status.HTTP_200_OK, + ) + if issue_type == "completed": completed_issues_count = assigned_issues.filter( state__group__in=["completed"] @@ -257,6 +274,23 @@ def dashboard_created_issues(self, request, slug): ) ).order_by("priority_order") + if issue_type == "pending": + pending_issues_count = created_issues.filter( + state__group__in=["backlog", "started", "unstarted"] + ).count() + pending_issues = created_issues.filter( + state__group__in=["backlog", "started", "unstarted"] + )[:5] + return Response( + { + "issues": IssueSerializer( + pending_issues, many=True, expand=self.expand + ).data, + "count": pending_issues_count, + }, + status=status.HTTP_200_OK, + ) + if issue_type == "completed": completed_issues_count = created_issues.filter( state__group__in=["completed"] diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index b9f6bd411..b86ab5e78 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -353,13 +353,18 @@ def track_assignees( issue_activities, epoch, ): - requested_assignees = set( - [str(asg) for asg in requested_data.get("assignee_ids", [])] + requested_assignees = ( + set([str(asg) for asg in requested_data.get("assignee_ids", [])]) + if requested_data is not None + else set() ) - current_assignees = set( - [str(asg) for asg in current_instance.get("assignee_ids", [])] + current_assignees = ( + set([str(asg) for asg in current_instance.get("assignee_ids", [])]) + if current_instance is not None + else set() ) + added_assignees = requested_assignees - current_assignees dropped_assginees = current_assignees - requested_assignees @@ -547,6 +552,20 @@ def create_issue_activity( epoch=epoch, ) ) + requested_data = ( + json.loads(requested_data) if requested_data is not None else None + ) + if requested_data.get("assignee_ids") is not None: + track_assignees( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, + ) def update_issue_activity( diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 6f8a82e56..f254a3cb7 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -172,4 +172,9 @@ def create_user_notification(sender, instance, created, **kwargs): from plane.db.models import UserNotificationPreference UserNotificationPreference.objects.create( user=instance, + property_change=False, + state_change=False, + comment=False, + mention=False, + issue_completed=False, ) diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 3f306c559..4e505cff9 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -49,7 +49,7 @@ function buildLocalImage() { cd $PLANE_TEMP_CODE_DIR if [ "$BRANCH" == "master" ]; then - APP_RELEASE=latest + export APP_RELEASE=latest fi docker compose -f build.yml build --no-cache >&2 @@ -205,6 +205,11 @@ else PULL_POLICY=never fi +if [ "$BRANCH" == "master" ]; +then + export APP_RELEASE=latest +fi + # REMOVE SPECIAL CHARACTERS FROM BRANCH NAME if [ "$BRANCH" != "master" ]; then diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index 31751c0d0..7cfa6aa85 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -13,9 +13,10 @@ export type TWidgetKeys = | "recent_projects" | "recent_collaborators"; -export type TIssuesListTypes = "upcoming" | "overdue" | "completed"; +export type TIssuesListTypes = "pending" | "upcoming" | "overdue" | "completed"; export type TDurationFilterOptions = + | "none" | "today" | "this_week" | "this_month" diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index c37cdf4b9..8fd021403 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -125,7 +125,7 @@ export const SidebarProgressStats: React.FC = ({ - + {distribution?.assignees.length > 0 ? ( distribution.assignees.map((assignee, index) => { if (assignee.assignee_id) diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index d4a27afc1..e2a54c05f 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -17,7 +17,7 @@ import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper" // types import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; // constants -import { ISSUES_TABS_LIST } from "constants/dashboard"; +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; const WIDGET_KEY = "assigned_issues"; @@ -30,6 +30,8 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedTab = widgetDetails?.widget_filters.tab ?? "pending"; + const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none"; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -41,68 +43,79 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { filters, }); + const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - issue_type: filters.tab ?? widgetDetails.widget_filters.tab ?? "upcoming", - target_date: getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "this_week"), + issue_type: filters.tab ?? selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), expand: "issue_relation", }).finally(() => setFetching(false)); }; useEffect(() => { - const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "this_week"); + const filterDates = getCustomDates(selectedDurationFilter); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - issue_type: widgetDetails?.widget_filters.tab ?? "upcoming", - target_date: filterDates, + issue_type: selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), expand: "issue_relation", }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const filterParams = getRedirectionFilters(widgetDetails?.widget_filters.tab ?? "upcoming"); + const filterParams = getRedirectionFilters(selectedTab); + const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; if (!widgetDetails || !widgetStats) return ; return (
-
-
- - Assigned to you - -

- Filtered by{" "} - Due date -

-
+
+ + Assigned to you + - handleUpdateFilters({ - target_date: val, - }) - } + value={selectedDurationFilter} + onChange={(val) => { + if (val === selectedDurationFilter) return; + + // switch to pending tab if target date is changed to none + if (val === "none" && selectedTab !== "completed") { + handleUpdateFilters({ target_date: val, tab: "pending" }); + return; + } + // switch to upcoming tab if target date is changed to other than none + if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") { + handleUpdateFilters({ + target_date: val, + tab: "upcoming", + }); + return; + } + + handleUpdateFilters({ target_date: val }); + }} />
t.key === widgetDetails.widget_filters.tab ?? "upcoming")} + selectedIndex={tabsList.findIndex((tab) => tab.key === selectedTab)} onChange={(i) => { - const selectedTab = ISSUES_TABS_LIST[i]; - handleUpdateFilters({ tab: selectedTab.key ?? "upcoming" }); + const selectedTab = tabsList[i]; + handleUpdateFilters({ tab: selectedTab?.key ?? "pending" }); }} className="h-full flex flex-col" >
- +
- {ISSUES_TABS_LIST.map((tab) => ( + {tabsList.map((tab) => ( = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedTab = widgetDetails?.widget_filters.tab ?? "pending"; + const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none"; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -41,64 +43,76 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { filters, }); + const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - issue_type: filters.tab ?? widgetDetails.widget_filters.tab ?? "upcoming", - target_date: getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "this_week"), + issue_type: filters.tab ?? selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), }).finally(() => setFetching(false)); }; useEffect(() => { + const filterDates = getCustomDates(selectedDurationFilter); + fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - issue_type: widgetDetails?.widget_filters.tab ?? "upcoming", - target_date: getCustomDates(widgetDetails?.widget_filters.target_date ?? "this_week"), + issue_type: selectedTab, + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const filterParams = getRedirectionFilters(widgetDetails?.widget_filters.tab ?? "upcoming"); + const filterParams = getRedirectionFilters(selectedTab); + const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; if (!widgetDetails || !widgetStats) return ; return (
-
-
- - Created by you - -

- Filtered by{" "} - Due date -

-
+
+ + Created by you + - handleUpdateFilters({ - target_date: val, - }) - } + value={selectedDurationFilter} + onChange={(val) => { + if (val === selectedDurationFilter) return; + + // switch to pending tab if target date is changed to none + if (val === "none" && selectedTab !== "completed") { + handleUpdateFilters({ target_date: val, tab: "pending" }); + return; + } + // switch to upcoming tab if target date is changed to other than none + if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") { + handleUpdateFilters({ + target_date: val, + tab: "upcoming", + }); + return; + } + + handleUpdateFilters({ target_date: val }); + }} />
t.key === widgetDetails.widget_filters.tab ?? "upcoming")} + selectedIndex={tabsList.findIndex((tab) => tab.key === selectedTab)} onChange={(i) => { - const selectedTab = ISSUES_TABS_LIST[i]; - handleUpdateFilters({ tab: selectedTab.key ?? "upcoming" }); + const selectedTab = tabsList[i]; + handleUpdateFilters({ tab: selectedTab.key ?? "pending" }); }} className="h-full flex flex-col" >
- +
- {ISSUES_TABS_LIST.map((tab) => ( + {tabsList.map((tab) => ( = (props) => { const filterParams = getRedirectionFilters(tab); const ISSUE_LIST_ITEM: { - [key in string]: { + [key: string]: { [key in TIssuesListTypes]: React.FC; }; } = { assigned: { + pending: AssignedUpcomingIssueListItem, upcoming: AssignedUpcomingIssueListItem, overdue: AssignedOverdueIssueListItem, completed: AssignedCompletedIssueListItem, }, created: { + pending: CreatedUpcomingIssueListItem, upcoming: CreatedUpcomingIssueListItem, overdue: CreatedOverdueIssueListItem, completed: CreatedCompletedIssueListItem, @@ -61,12 +63,7 @@ export const WidgetIssuesList: React.FC = (props) => { <>
{isLoading ? ( - - - - - - + <> ) : issues.length > 0 ? ( <>
@@ -81,7 +78,7 @@ export const WidgetIssuesList: React.FC = (props) => { {totalIssues} - {tab === "upcoming" &&
Due date
} + {["upcoming", "pending"].includes(tab) &&
Due date
} {tab === "overdue" &&
Due by
} {type === "assigned" && tab !== "completed" &&
Blocked by
} {type === "created" &&
Assigned to
} diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index 6ef6ec0ee..9ce00a03c 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -1,26 +1,63 @@ +import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // helpers import { cn } from "helpers/common.helper"; +// types +import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; // constants -import { ISSUES_TABS_LIST } from "constants/dashboard"; +import { FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "constants/dashboard"; -export const TabsList = () => ( - - {ISSUES_TABS_LIST.map((tab) => ( - - cn("font-semibold text-xs rounded py-1.5 focus:outline-none", { - "bg-custom-background-100 text-custom-text-300 shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selected, - "text-custom-text-400": !selected, - }) - } - > - {tab.label} - - ))} - -); +type Props = { + durationFilter: TDurationFilterOptions; + selectedTab: TIssuesListTypes; +}; + +export const TabsList: React.FC = observer((props) => { + const { durationFilter, selectedTab } = props; + + const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; + const selectedTabIndex = tabsList.findIndex((tab) => tab.key === (selectedTab ?? "pending")); + + return ( + +
+ {tabsList.map((tab) => ( + + {tab.label} + + ))} + + ); +}); diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index 45b71466d..97884bccc 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -84,16 +84,18 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => filters, }); + const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none"); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - target_date: getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "this_week"), + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), }); }; useEffect(() => { + const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none"); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - target_date: getCustomDates(widgetDetails?.widget_filters.target_date ?? "this_week"), + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -129,21 +131,15 @@ export const IssuesByPriorityWidget: React.FC = observer((props) => return (
-
-
- - Assigned by priority - -

- Filtered by{" "} - Due date -

-
+
+ + Assigned by priority + handleUpdateFilters({ target_date: val, diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index bd4171cfa..2f7f6ffae 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -43,17 +43,19 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) filters, }); + const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none"); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - target_date: getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "this_week"), + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), }); }; // fetch widget stats useEffect(() => { + const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none"); fetchWidgetStats(workspaceSlug, dashboardId, { widget_key: WIDGET_KEY, - target_date: getCustomDates(widgetDetails?.widget_filters.target_date ?? "this_week"), + ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -72,14 +74,14 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) startedCount > 0 ? "started" : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; + ? "unstarted" + : backlogCount > 0 + ? "backlog" + : completedCount > 0 + ? "completed" + : canceledCount > 0 + ? "cancelled" + : null; setActiveStateGroup(stateGroup); setDefaultStateGroup(stateGroup); @@ -128,21 +130,15 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) return (
-
-
- - Assigned by state - -

- Filtered by{" "} - Due date -

-
+
+ + Assigned by state + handleUpdateFilters({ target_date: val, diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 1dba6f780..3cd9ca16a 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -105,6 +105,7 @@ export const DateDropdown: React.FC = (props) => { tabIndex={tabIndex} className={cn("h-full", className)} onKeyDown={handleKeyDown} + disabled={disabled} >
diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 1652aa89b..c48f23068 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -31,11 +31,21 @@ type Props = { viewId?: string ) => Promise; viewId?: string; + readOnly?: boolean; }; export const CalendarChart: React.FC = observer((props) => { - const { issuesFilterStore, issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } = - props; + const { + issuesFilterStore, + issues, + groupedIssueIds, + layout, + showWeekends, + quickActions, + quickAddCallback, + viewId, + readOnly = false, + } = props; // store hooks const { issues: { viewFlags }, @@ -80,6 +90,7 @@ export const CalendarChart: React.FC = observer((props) => { quickActions={quickActions} quickAddCallback={quickAddCallback} viewId={viewId} + readOnly={readOnly} /> ))}
@@ -95,6 +106,7 @@ export const CalendarChart: React.FC = observer((props) => { quickActions={quickActions} quickAddCallback={quickAddCallback} viewId={viewId} + readOnly={readOnly} /> )}
diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 5b4885bf3..f98dba93a 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -28,6 +28,7 @@ type Props = { viewId?: string ) => Promise; viewId?: string; + readOnly?: boolean; }; export const CalendarDayTile: React.FC = observer((props) => { @@ -41,6 +42,7 @@ export const CalendarDayTile: React.FC = observer((props) => { disableIssueCreation, quickAddCallback, viewId, + readOnly = false, } = props; const [showAllIssues, setShowAllIssues] = useState(false); const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; @@ -73,7 +75,7 @@ export const CalendarDayTile: React.FC = observer((props) => { {/* content */}
- + {(provided, snapshot) => (
= observer((props) => { issueIdList={issueIdList} quickActions={quickActions} showAllIssues={showAllIssues} + isDragDisabled={readOnly} /> - {enableQuickIssueCreate && !disableIssueCreation && ( + {enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
React.ReactNode; showAllIssues?: boolean; + isDragDisabled?: boolean; }; export const CalendarIssueBlocks: React.FC = observer((props) => { - const { issues, issueIdList, quickActions, showAllIssues = false } = props; + const { issues, issueIdList, quickActions, showAllIssues = false, isDragDisabled = false } = props; // hooks const { router: { workspaceSlug, projectId }, @@ -65,7 +66,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { getProjectStates(issue?.project_id)?.find((state) => state?.id == issue?.state_id)?.color || ""; return ( - + {(provided, snapshot) => (
{ const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { currentProjectCompletedCycleIds } = useCycle(); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -38,6 +39,9 @@ export const CycleCalendarLayout: React.FC = observer(() => { if (!cycleId) return null; + const isCompletedCycle = + cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + return ( { QuickActions={CycleIssueQuickActions} issueActions={issueActions} viewId={cycleId.toString()} + isCompletedCycle={isCompletedCycle} /> ); }); diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index c34aaef97..5a640a566 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -26,6 +26,7 @@ type Props = { viewId?: string ) => Promise; viewId?: string; + readOnly?: boolean; }; export const CalendarWeekDays: React.FC = observer((props) => { @@ -39,6 +40,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { disableIssueCreation, quickAddCallback, viewId, + readOnly = false, } = props; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; @@ -67,6 +69,7 @@ export const CalendarWeekDays: React.FC = observer((props) => { disableIssueCreation={disableIssueCreation} quickAddCallback={quickAddCallback} viewId={viewId} + readOnly={readOnly} /> ); })} diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 7ed3202aa..64b132267 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -46,6 +46,7 @@ export interface IBaseKanBanLayout { storeType?: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; + isCompletedCycle?: boolean; } type KanbanDragState = { @@ -65,6 +66,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas storeType, addIssuesToView, canEditPropertiesBasedOnProject, + isCompletedCycle = false, } = props; // router const router = useRouter(); @@ -183,6 +185,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas handleRemoveFromView={ issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined } + readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -282,7 +285,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas showEmptyGroup={userDisplayFilters?.show_empty_groups || true} quickAddCallback={issues?.quickAddIssue} viewId={viewId} - disableIssueCreation={!enableIssueCreation || !isEditingAllowed} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} canEditProperties={canEditProperties} storeType={storeType} addIssuesToView={addIssuesToView} diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 0903355ce..2b311f6eb 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues } from "hooks/store"; +import { useCycle, useIssues } from "hooks/store"; // ui import { CycleIssueQuickActions } from "components/issues"; // types @@ -20,6 +20,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { // store const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { currentProjectCompletedCycleIds } = useCycle(); const issueActions = useMemo( () => ({ @@ -42,6 +43,11 @@ export const CycleKanBanLayout: React.FC = observer(() => { [issues, workspaceSlug, cycleId] ); + const isCompletedCycle = + cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + + const canEditIssueProperties = () => !isCompletedCycle; + return ( { if (!workspaceSlug || !projectId || !cycleId) throw new Error(); return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); }} + canEditPropertiesBasedOnProject={canEditIssueProperties} + isCompletedCycle={isCompletedCycle} /> ); }); diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 10f3582f1..8f661a9e6 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -51,6 +51,7 @@ interface IBaseListRoot { storeType: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; + isCompletedCycle?: boolean; } export const BaseListRoot = observer((props: IBaseListRoot) => { @@ -63,6 +64,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { storeType, addIssuesToView, canEditPropertiesBasedOnProject, + isCompletedCycle = false, } = props; // mobx store const { @@ -112,6 +114,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { handleRemoveFromView={ issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined } + readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -136,6 +139,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { disableIssueCreation={!enableIssueCreation || !isEditingAllowed} storeType={storeType} addIssuesToView={addIssuesToView} + isCompletedCycle={isCompletedCycle} />
diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 95e31b758..dd6c8da22 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -37,6 +37,7 @@ export interface IGroupByList { storeType: TCreateModalStoreTypes; addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; + isCompletedCycle?: boolean; } const GroupByList: React.FC = (props) => { @@ -55,6 +56,7 @@ const GroupByList: React.FC = (props) => { disableIssueCreation, storeType, addIssuesToView, + isCompletedCycle = false, } = props; // store hooks const member = useMember(); @@ -115,7 +117,7 @@ const GroupByList: React.FC = (props) => { title={_list.name || ""} count={is_list ? issueIds?.length || 0 : issueIds?.[_list.id]?.length || 0} issuePayload={_list.payload} - disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} + disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle} storeType={storeType} addIssuesToView={addIssuesToView} /> @@ -132,7 +134,7 @@ const GroupByList: React.FC = (props) => { /> )} - {enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && ( + {enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && (
Promise; + isCompletedCycle?: boolean; } export const List: React.FC = (props) => { @@ -186,6 +189,7 @@ export const List: React.FC = (props) => { disableIssueCreation, storeType, addIssuesToView, + isCompletedCycle = false, } = props; return ( @@ -205,6 +209,7 @@ export const List: React.FC = (props) => { disableIssueCreation={disableIssueCreation} storeType={storeType} addIssuesToView={addIssuesToView} + isCompletedCycle={isCompletedCycle} />
); diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index 9e3bb8701..1838316cb 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -5,4 +5,5 @@ export interface IQuickActionProps { handleRemoveFromView?: () => Promise; customActionButton?: React.ReactElement; portalElement?: HTMLDivElement | null; + readOnly?: boolean; } diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 89da8dd54..e30c207b6 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useIssues } from "hooks/store"; +import { useCycle, useIssues } from "hooks/store"; // components import { CycleIssueQuickActions } from "components/issues"; // types @@ -19,6 +19,7 @@ export const CycleListLayout: React.FC = observer(() => { const { workspaceSlug, projectId, cycleId } = router.query; // store const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { currentProjectCompletedCycleIds } = useCycle(); const issueActions = useMemo( () => ({ @@ -40,6 +41,10 @@ export const CycleListLayout: React.FC = observer(() => { }), [issues, workspaceSlug, cycleId] ); + const isCompletedCycle = + cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + + const canEditIssueProperties = () => !isCompletedCycle; return ( { if (!workspaceSlug || !projectId || !cycleId) throw new Error(); return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); }} + canEditPropertiesBasedOnProject={canEditIssueProperties} + isCompletedCycle={isCompletedCycle} /> ); }); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 764f5605a..f97f1ea86 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -16,7 +16,7 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EIssuesStoreType } from "constants/issue"; export const AllIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props; + const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -82,40 +82,44 @@ export const AllIssueQuickActions: React.FC = (props) => { Copy link
- { - setTrackElement("Global issues"); + {!readOnly && ( + <> + { + setTrackElement("Global issues"); setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit issue +
+
+ { + setTrackElement("Global issues"); setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - setTrackElement("Global issues"); - setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - setTrackElement("Global issues"); + }} + > +
+ + Make a copy +
+
+ { + setTrackElement("Global issues"); setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
+ }} + > +
+ + Delete issue +
+
+ + )} ); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 2926b6433..f96270176 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -4,18 +4,18 @@ import { CustomMenu } from "@plane/ui"; import { Link, Trash2 } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; -import { useEventTracker, useIssues } from "hooks/store"; +import { useEventTracker, useIssues ,useUser} from "hooks/store"; // components import { DeleteArchivedIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IQuickActionProps } from "../list/list-view-types"; -// constants +import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, customActionButton, portalElement } = props; + const { issue, handleDelete, customActionButton, portalElement, readOnly = false } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -23,6 +23,13 @@ export const ArchivedIssueQuickActions: React.FC = (props) => const [deleteIssueModal, setDeleteIssueModal] = useState(false); // toast alert const { setToastAlert } = useToast(); + + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; // store hooks const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); @@ -64,17 +71,19 @@ export const ArchivedIssueQuickActions: React.FC = (props) => Copy link
- { - setTrackElement(activeLayout); + {isEditingAllowed && !readOnly && ( + { + setTrackElement(activeLayout); setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
+ }} + > +
+ + Delete issue +
+
+ )} ); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 6e6c2e518..8c3beb3d2 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -4,7 +4,7 @@ import { CustomMenu } from "@plane/ui"; import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; -import { useEventTracker, useIssues } from "hooks/store"; +import { useEventTracker, useIssues,useUser } from "hooks/store"; // components import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -14,9 +14,18 @@ import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; export const CycleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props; + const { + issue, + handleDelete, + handleUpdate, + handleRemoveFromView, + customActionButton, + portalElement, + readOnly = false, + } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -30,6 +39,13 @@ export const CycleIssueQuickActions: React.FC = (props) => { // toast alert const { setToastAlert } = useToast(); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const handleCopyIssueLink = () => { @@ -85,53 +101,57 @@ export const CycleIssueQuickActions: React.FC = (props) => { Copy link
- { - setIssueToEdit({ - ...issue, - cycle: cycleId?.toString() ?? null, - }); - setTrackElement(activeLayout); + {isEditingAllowed && !readOnly && ( + <> + { + setIssueToEdit({ + ...issue, + cycle: cycleId?.toString() ?? null, + }); + setTrackElement(activeLayout); setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - handleRemoveFromView && handleRemoveFromView(); - }} - > -
- - Remove from cycle -
-
- { - setTrackElement(activeLayout); + }} + > +
+ + Edit issue +
+
+ { + handleRemoveFromView && handleRemoveFromView(); + }} + > +
+ + Remove from cycle +
+
+ { + setTrackElement(activeLayout); setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - setTrackElement(activeLayout); + }} + > +
+ + Make a copy +
+
+ { + setTrackElement(activeLayout); setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
+ }} + > +
+ + Delete issue +
+
+ + )} ); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index ed52c9198..a3ed73ec0 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -4,7 +4,7 @@ import { CustomMenu } from "@plane/ui"; import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; -import { useIssues, useEventTracker } from "hooks/store"; +import { useIssues, useEventTracker ,useUser } from "hooks/store"; // components import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -14,9 +14,18 @@ import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; export const ModuleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props; + const { + issue, + handleDelete, + handleUpdate, + handleRemoveFromView, + customActionButton, + portalElement, + readOnly = false, + } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -30,6 +39,13 @@ export const ModuleIssueQuickActions: React.FC = (props) => { // toast alert const { setToastAlert } = useToast(); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const handleCopyIssueLink = () => { @@ -85,52 +101,56 @@ export const ModuleIssueQuickActions: React.FC = (props) => { Copy link
- { - setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null }); - setTrackElement(activeLayout); + {isEditingAllowed && !readOnly && ( + <> + { + setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null }); + setTrackElement(activeLayout); setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - handleRemoveFromView && handleRemoveFromView(); - }} - > -
- - Remove from module -
-
- { - setTrackElement(activeLayout); + }} + > +
+ + Edit issue +
+
+ { + handleRemoveFromView && handleRemoveFromView(); + }} + > +
+ + Remove from module +
+
+ { + setTrackElement(activeLayout); setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - e.preventDefault(); - e.stopPropagation(); - setTrackElement(activeLayout); + }} + > +
+ + Make a copy +
+
+ { + e.preventDefault(); + e.stopPropagation(); + setTrackElement(activeLayout); setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
+ }} + > +
+ + Delete issue +
+
+ + )} ); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index e28dd9a42..13c7ac02a 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -17,7 +17,7 @@ import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; export const ProjectIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props; + const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -91,7 +91,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => Copy link
- {isEditingAllowed && ( + {isEditingAllowed && !readOnly && ( <> { diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 54df6ca24..e4efc5137 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -28,10 +28,19 @@ interface IBaseSpreadsheetRoot { [EIssueActions.REMOVE]?: (issue: TIssue) => void; }; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; + isCompletedCycle?: boolean; } export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { - const { issueFiltersStore, issueStore, viewId, QuickActions, issueActions, canEditPropertiesBasedOnProject } = props; + const { + issueFiltersStore, + issueStore, + viewId, + QuickActions, + issueActions, + canEditPropertiesBasedOnProject, + isCompletedCycle = false, + } = props; // router const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; @@ -95,6 +104,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined } portalElement={portalElement} + readOnly={!isEditingAllowed || isCompletedCycle} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -113,7 +123,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { quickAddCallback={issueStore.quickAddIssue} viewId={viewId} enableQuickCreateIssue={enableQuickAdd} - disableIssueCreation={!enableIssueCreation || !isEditingAllowed} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} /> ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 40b933557..7f92bd74c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store -import { useIssues } from "hooks/store"; +import { useCycle, useIssues } from "hooks/store"; // components import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; import { EIssueActions } from "../../types"; @@ -15,6 +15,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); + const { currentProjectCompletedCycleIds } = useCycle(); const issueActions = useMemo( () => ({ @@ -35,6 +36,11 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { [issues, workspaceSlug, cycleId] ); + const isCompletedCycle = + cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; + + const canEditIssueProperties = () => !isCompletedCycle; + return ( { viewId={cycleId} issueActions={issueActions} QuickActions={CycleIssueQuickActions} + canEditPropertiesBasedOnProject={canEditIssueProperties} + isCompletedCycle={isCompletedCycle} /> ); }); diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 661d31ef5..be0792caa 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -70,7 +70,7 @@ export const ModuleForm: React.FC = ({ const startDate = watch("start_date"); const targetDate = watch("target_date"); - const minDate = startDate ? new Date(startDate) : new Date(); + const minDate = startDate ? new Date(startDate) : null; minDate?.setDate(minDate.getDate()); const maxDate = targetDate ? new Date(targetDate) : null; @@ -159,7 +159,6 @@ export const ModuleForm: React.FC = ({ onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)} buttonVariant="border-with-text" placeholder="Start date" - minDate={new Date()} maxDate={maxDate ?? undefined} tabIndex={3} /> diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index c72478005..947885f9a 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -168,18 +168,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { if (!watch("target_date") || watch("target_date") === "") endDateButtonRef.current?.click(); if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { - if (!isDateGreaterThanToday(`${watch("target_date")}`)) { - setToastAlert({ - type: "error", - title: "Error!", - message: "Unable to create module in past date. Please enter a valid date.", - }); - reset({ - ...moduleDetails, - }); - return; - } - submitChanges({ start_date: renderFormattedPayloadDate(`${watch("start_date")}`), target_date: renderFormattedPayloadDate(`${watch("target_date")}`), @@ -198,18 +186,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { if (!watch("start_date") || watch("start_date") === "") endDateButtonRef.current?.click(); if (watch("start_date") && watch("target_date") && watch("start_date") !== "" && watch("start_date") !== "") { - if (!isDateGreaterThanToday(`${watch("target_date")}`)) { - setToastAlert({ - type: "error", - title: "Error!", - message: "Unable to create module in past date. Please enter a valid date.", - }); - reset({ - ...moduleDetails, - }); - return; - } - submitChanges({ start_date: renderFormattedPayloadDate(`${watch("start_date")}`), target_date: renderFormattedPayloadDate(`${watch("target_date")}`), diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index dc9c4de61..93756d22a 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -62,16 +62,18 @@ export const WorkspaceDashboardView = observer(() => { {homeDashboardId && joinedProjectIds ? ( <> {joinedProjectIds.length > 0 ? ( -
+ <> - {currentUser && } - {currentUser && !currentUser.is_tour_completed && ( -
- -
- )} - -
+
+ {currentUser && } + {currentUser && !currentUser.is_tour_completed && ( +
+ +
+ )} + +
+ ) : ( { let firstDay, lastDay; switch (duration) { + case "none": + return ""; case "today": firstDay = renderFormattedPayloadDate(today); lastDay = renderFormattedPayloadDate(today); @@ -32,7 +34,9 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => { const today = renderFormattedPayloadDate(new Date()); const filterParams = - type === "upcoming" + type === "pending" + ? "?state_group=backlog,unstarted,started" + : type === "upcoming" ? `?target_date=${today};after` : type === "overdue" ? `?target_date=${today};before`