diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index 104a3dd06..63abf3a03 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -40,6 +40,7 @@ class CycleSerializer(BaseSerializer): started_estimates = serializers.IntegerField(read_only=True) workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") project_detail = ProjectLiteSerializer(read_only=True, source="project") + status = serializers.CharField(read_only=True) def validate(self, data): if ( diff --git a/apiserver/plane/app/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py index 4a1cda779..2c2f26e4e 100644 --- a/apiserver/plane/app/serializers/estimate.py +++ b/apiserver/plane/app/serializers/estimate.py @@ -4,6 +4,7 @@ from .base import BaseSerializer from plane.db.models import Estimate, EstimatePoint from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer +from rest_framework import serializers class EstimateSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") @@ -19,6 +20,15 @@ class EstimateSerializer(BaseSerializer): class EstimatePointSerializer(BaseSerializer): + + def validate(self, data): + if not data: + raise serializers.ValidationError("Estimate points are required") + value = data.get("value") + if value and len(value) > 20: + raise serializers.ValidationError("Value can't be more than 20 characters") + return data + class Meta: model = EstimatePoint fields = "__all__" diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index d2f82d75b..02f259de3 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -11,6 +11,10 @@ from django.db.models import ( Count, Prefetch, Sum, + Case, + When, + Value, + CharField ) from django.core import serializers from django.utils import timezone @@ -157,6 +161,28 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ), ) ) + .annotate( + status=Case( + When( + Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), + then=Value("CURRENT") + ), + When( + start_date__gt=timezone.now(), + then=Value("UPCOMING") + ), + When( + end_date__lt=timezone.now(), + then=Value("COMPLETED") + ), + When( + Q(start_date__isnull=True) & Q(end_date__isnull=True), + then=Value("DRAFT") + ), + default=Value("DRAFT"), + output_field=CharField(), + ) + ) .prefetch_related( Prefetch( "issue_cycle__issue__assignees", @@ -177,7 +203,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): queryset = self.get_queryset() cycle_view = request.GET.get("cycle_view", "all") - queryset = queryset.order_by("-is_favorite","-created_at") + queryset = queryset.order_by("-is_favorite", "-created_at") # Current Cycle if cycle_view == "current": @@ -575,7 +601,9 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ) ) - issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data + issues = IssueStateSerializer( + issues, many=True, fields=fields if fields else None + ).data issue_dict = {str(issue["id"]): issue for issue in issues} return Response(issue_dict, status=status.HTTP_200_OK) @@ -805,4 +833,4 @@ class TransferCycleIssueEndpoint(BaseAPIView): updated_cycles, ["cycle_id"], batch_size=100 ) - return Response({"message": "Success"}, status=status.HTTP_200_OK) \ No newline at end of file + return Response({"message": "Success"}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/estimate.py b/apiserver/plane/app/views/estimate.py index ec9393f5b..8f14b230b 100644 --- a/apiserver/plane/app/views/estimate.py +++ b/apiserver/plane/app/views/estimate.py @@ -53,11 +53,11 @@ class BulkEstimatePointEndpoint(BaseViewSet): ) estimate_points = request.data.get("estimate_points", []) - - if not len(estimate_points) or len(estimate_points) > 8: + + serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True) + if not serializer.is_valid(): return Response( - {"error": "Estimate points are required"}, - status=status.HTTP_400_BAD_REQUEST, + serializer.errors, status=status.HTTP_400_BAD_REQUEST ) estimate_serializer = EstimateSerializer(data=request.data.get("estimate")) diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index ac560643a..4ecb71127 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -50,7 +50,8 @@ class GlobalSearchEndpoint(BaseAPIView): q = Q() for field in fields: if field == "sequence_id": - sequences = re.findall(r"\d+\.\d+|\d+", query) + # Match whole integers only (exclude decimal numbers) + sequences = re.findall(r"\b\d+\b", query) for sequence_id in sequences: q |= Q(**{"sequence_id": sequence_id}) else: diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 3b2b40223..5d4c0650c 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -112,8 +112,8 @@ def track_parent( epoch, ): if current_instance.get("parent") != requested_data.get("parent"): - old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() - new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() + old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() if current_instance.get("parent") is not None else None + new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None issue_activities.append( IssueActivity( diff --git a/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py b/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py new file mode 100644 index 000000000..19267dfc2 --- /dev/null +++ b/apiserver/plane/db/migrations/0051_cycle_external_id_cycle_external_source_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.7 on 2023-12-29 10:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0050_user_use_case_alter_workspace_organization_size'), + ] + + operations = [ + migrations.AddField( + model_name='cycle', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='cycle', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='inboxissue', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='inboxissue', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issuecomment', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issuecomment', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='label', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='label', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='module', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='module', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='state', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='state', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index 56301e3d3..e5e2c355b 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -18,6 +18,8 @@ class Cycle(ProjectBaseModel): ) view_props = models.JSONField(default=dict) sort_order = models.FloatField(default=65535) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) class Meta: verbose_name = "Cycle" diff --git a/apiserver/plane/db/models/inbox.py b/apiserver/plane/db/models/inbox.py index 497a20f00..6ad88e681 100644 --- a/apiserver/plane/db/models/inbox.py +++ b/apiserver/plane/db/models/inbox.py @@ -39,6 +39,8 @@ class InboxIssue(ProjectBaseModel): "db.Issue", related_name="inbox_duplicate", on_delete=models.SET_NULL, null=True ) source = models.TextField(blank=True, null=True) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) class Meta: verbose_name = "InboxIssue" diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 9b293a75d..54acd5c5d 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -102,6 +102,8 @@ class Issue(ProjectBaseModel): completed_at = models.DateTimeField(null=True) archived_at = models.DateField(null=True) is_draft = models.BooleanField(default=False) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) objects = models.Manager() issue_objects = IssueManager() @@ -366,6 +368,8 @@ class IssueComment(ProjectBaseModel): default="INTERNAL", max_length=100, ) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) def save(self, *args, **kwargs): self.comment_stripped = ( @@ -416,6 +420,8 @@ class Label(ProjectBaseModel): description = models.TextField(blank=True) color = models.CharField(max_length=255, blank=True) sort_order = models.FloatField(default=65535) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) class Meta: unique_together = ["name", "project"] diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index ae540cc6c..e485eea62 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -41,6 +41,8 @@ class Module(ProjectBaseModel): ) view_props = models.JSONField(default=dict) sort_order = models.FloatField(default=65535) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) class Meta: unique_together = ["name", "project"] diff --git a/apiserver/plane/db/models/state.py b/apiserver/plane/db/models/state.py index 2fa1ebe38..3370f239d 100644 --- a/apiserver/plane/db/models/state.py +++ b/apiserver/plane/db/models/state.py @@ -24,6 +24,8 @@ class State(ProjectBaseModel): max_length=20, ) default = models.BooleanField(default=False) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) def __str__(self): """Return name of the state""" diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index 353bb8fce..f0a3e3d90 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -18,7 +18,8 @@ type Props = { export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { const { user } = useUser(); - const { asPath: currentPath } = useRouter(); + const { query } = useRouter(); + const { next_path } = query; return ( @@ -37,7 +38,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { {user ? (

You have signed in as {user.email}.
- + Sign in {" "} with different account that has access to this page. @@ -45,7 +46,7 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { ) : (

You need to{" "} - + Sign in {" "} with an account that has access to this page. diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 37f8d2626..1ac34cf73 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -11,7 +11,6 @@ import { CopyPlus, Calendar, Link2Icon, - RocketIcon, Users2Icon, ArchiveIcon, PaperclipIcon, @@ -48,8 +47,8 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => { rel={activity.issue === null ? "" : "noopener noreferrer"} className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" > - {activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"} - + {activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}{" "} + {activity.issue_detail?.name} ); @@ -163,7 +162,6 @@ const activityDetails: { className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" > attachment - {showIssue && ( <> @@ -239,7 +237,6 @@ const activityDetails: { className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline" > {activity.new_value} - ); @@ -254,7 +251,6 @@ const activityDetails: { className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline" > {activity.new_value} - ); @@ -269,7 +265,6 @@ const activityDetails: { className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline" > {activity.old_value} - ); @@ -398,7 +393,6 @@ const activityDetails: { className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" > link - {showIssue && ( <> @@ -420,7 +414,6 @@ const activityDetails: { className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" > link - {showIssue && ( <> @@ -442,7 +435,6 @@ const activityDetails: { className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" > link - {showIssue && ( <> @@ -469,7 +461,6 @@ const activityDetails: { className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline" > {activity.new_value} - ); @@ -484,7 +475,6 @@ const activityDetails: { className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline" > {activity.new_value} - ); @@ -499,7 +489,6 @@ const activityDetails: { className="inline-flex items-center gap-1 truncate font-medium text-custom-text-100 hover:underline" > {activity.old_value} - ); diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index ea982099f..47beaa262 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -28,7 +28,7 @@ import { ViewIssueLabel } from "components/issues"; // icons import { AlarmClock, AlertTriangle, ArrowRight, CalendarDays, Star, Target } from "lucide-react"; // helpers -import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; +import { renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types import { ICycle } from "types"; @@ -137,7 +137,7 @@ export const ActiveCycleDetails: React.FC = observer((props cancelled: cycle.cancelled_issues, }; - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const cycleStatus = cycle.status.toLocaleLowerCase(); const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index f020b0998..d43d56872 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -10,15 +10,10 @@ import { Avatar, AvatarGroup, CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } // icons import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers -import { - getDateRangeStatus, - findHowManyDaysLeft, - renderShortDate, - renderShortMonthDate, -} from "helpers/date-time.helper"; +import { findHowManyDaysLeft, renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // types -import { ICycle } from "types"; +import { ICycle, TCycleGroups } from "types"; // store import { useMobxStore } from "lib/mobx/store-provider"; // constants @@ -45,7 +40,7 @@ export const CyclesBoardCard: FC = (props) => { const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); // computed - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const cycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/cycles-list-item.tsx index 86b3bffa9..9ea26ab39 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/cycles-list-item.tsx @@ -13,15 +13,10 @@ import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarG // icons import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; // helpers -import { - getDateRangeStatus, - findHowManyDaysLeft, - renderShortDate, - renderShortMonthDate, -} from "helpers/date-time.helper"; +import { findHowManyDaysLeft, renderShortDate, renderShortMonthDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // types -import { ICycle } from "types"; +import { ICycle, TCycleGroups } from "types"; // constants import { CYCLE_STATUS } from "constants/cycle"; import { EUserWorkspaceRoles } from "constants/workspace"; @@ -50,7 +45,7 @@ export const CyclesListItem: FC = (props) => { const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); // computed - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const cycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycle.end_date ?? ""); const startDate = new Date(cycle.start_date ?? ""); diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 03614592c..76a4d9235 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; // ui import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers -import { getDateRangeStatus, renderShortDate } from "helpers/date-time.helper"; +import { renderShortDate } from "helpers/date-time.helper"; // types import { ICycle } from "types"; @@ -11,8 +11,7 @@ export const CycleGanttBlock = ({ data }: { data: ICycle }) => { const router = useRouter(); const { workspaceSlug } = router.query; - const cycleStatus = getDateRangeStatus(data?.start_date, data?.end_date); - + const cycleStatus = data.status.toLocaleLowerCase(); return (

{ cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "", + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "", }} onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)} > @@ -52,7 +51,7 @@ export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => { const router = useRouter(); const { workspaceSlug } = router.query; - const cycleStatus = getDateRangeStatus(data?.start_date, data?.end_date); + const cycleStatus = data.status.toLocaleLowerCase(); return (
{ cycleStatus === "current" ? "#09a953" : cycleStatus === "upcoming" - ? "#f7ae59" - : cycleStatus === "completed" - ? "#3f76ff" - : cycleStatus === "draft" - ? "rgb(var(--color-text-200))" - : "" + ? "#f7ae59" + : cycleStatus === "completed" + ? "#3f76ff" + : cycleStatus === "draft" + ? "rgb(var(--color-text-200))" + : "" }`} />
{data?.name}
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 1fd1cd05c..18c233d6c 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -17,12 +17,20 @@ import { CycleDeleteModal } from "components/cycles/delete-modal"; import { CustomRangeDatePicker } from "components/ui"; import { Avatar, CustomMenu, Loader, LayersIcon } from "@plane/ui"; // icons -import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, MoveRight } from "lucide-react"; +import { + ChevronDown, + LinkIcon, + Trash2, + UserCircle2, + AlertCircle, + ChevronRight, + CalendarCheck2, + CalendarClock, +} from "lucide-react"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; import { findHowManyDaysLeft, - getDateRangeStatus, isDateGreaterThanToday, renderDateFormat, renderShortDate, @@ -266,10 +274,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); - const cycleStatus = - cycleDetails?.start_date && cycleDetails?.end_date - ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) - : "draft"; + const cycleStatus = cycleDetails.status.toLocaleLowerCase(); const isCompleted = cycleStatus === "completed"; const isStartValid = new Date(`${cycleDetails?.start_date}`) <= new Date(); @@ -357,8 +362,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
-

{cycleDetails.name}

+
{currentCycle && ( = observer((props) => { : `${currentCycle.label}`} )} -
- +
+

{cycleDetails.name}

+
+ + {cycleDetails.description && ( + + {cycleDetails.description} + + )} + +
+
+
+ + Start Date +
+
+ - {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} + + {areYearsEqual + ? renderShortDate(startDate, "No date selected") + : renderShortMonthDate(startDate, "No date selected")} + = observer((props) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - + { @@ -410,16 +438,32 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { - - +
+
+ +
+
+ + Target Date +
+
+ <> - {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + {areYearsEqual + ? renderShortDate(endDate, "No date selected") + : renderShortMonthDate(endDate, "No date selected")} + = observer((props) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - + { @@ -451,15 +495,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => {
-
- {cycleDetails.description && ( - - {cycleDetails.description} - - )} - -
diff --git a/web/components/cycles/transfer-issues-modal.tsx b/web/components/cycles/transfer-issues-modal.tsx index 55555e221..dd462e360 100644 --- a/web/components/cycles/transfer-issues-modal.tsx +++ b/web/components/cycles/transfer-issues-modal.tsx @@ -15,8 +15,6 @@ import { AlertCircle, Search, X } from "lucide-react"; import { INCOMPLETE_CYCLES_LIST } from "constants/fetch-keys"; // types import { ICycle } from "types"; -//helper -import { getDateRangeStatus } from "helpers/date-time.helper"; type Props = { isOpen: boolean; @@ -138,7 +136,7 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl
{option?.name} - {getDateRangeStatus(option?.start_date, option?.end_date)} + {option.status}
diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index 6f0f98e86..b24172688 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -129,6 +129,22 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { return; } + if ( + formData.value1.length > 20 || + formData.value2.length > 20 || + formData.value3.length > 20 || + formData.value4.length > 20 || + formData.value5.length > 20 || + formData.value6.length > 20 + ) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Estimate point cannot have more than 20 characters.", + }); + return; + } + if ( checkDuplicates([ formData.value1, @@ -269,6 +285,12 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { ( = observer((props) => { ? "Updating Estimate..." : "Update Estimate" : isSubmitting - ? "Creating Estimate..." - : "Create Estimate"} + ? "Creating Estimate..." + : "Create Estimate"}
diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index ef4c2b3f5..0b27bdd81 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -95,7 +95,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { ) } - label={`Workspace ${activeLayout === "spreadsheet" ? "Issues" : "Views"}`} + label={`All ${activeLayout === "spreadsheet" ? "Issues" : "Views"}`} />
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 6d7369bb9..31317366c 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -1,29 +1,32 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; - // types import { IIssueFilterOptions } from "types"; import { EFilterType } from "store/issues/types"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; export const ProjectAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string; }; - + // mobx stores const { projectLabel: { projectLabels }, projectState: projectStateStore, projectMember: { projectMembers }, projectIssuesFilter: { issueFilters, updateFilters }, + user: { currentProjectRole }, } = useMobxStore(); - + // derived values + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const userFilters = issueFilters?.filters; // filters whose value not null or empty array @@ -73,8 +76,9 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { members={projectMembers?.map((m) => m.member)} states={projectStateStore.states?.[projectId?.toString() ?? ""]} /> - - + {isEditingAllowed && ( + + )}
); }); diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index 9590c9068..5be5a12c5 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -166,7 +166,7 @@ export const KanBanProperties: React.FC = observer((props) => {/* sub-issues */} {displayProperties && displayProperties?.sub_issue_count && !!issue?.sub_issues_count && ( -
+
{issue.sub_issues_count}
@@ -176,7 +176,7 @@ export const KanBanProperties: React.FC = observer((props) => {/* attachments */} {displayProperties && displayProperties?.attachment_count && !!issue?.attachment_count && ( -
+
{issue.attachment_count}
@@ -186,7 +186,7 @@ export const KanBanProperties: React.FC = observer((props) => {/* link */} {displayProperties && displayProperties?.link && !!issue?.link_count && ( -
+
{issue.link_count}
diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index eeff3b273..07129910f 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -137,7 +137,7 @@ export const ListProperties: FC = observer((props) => { {/* sub-issues */} {displayProperties && displayProperties?.sub_issue_count && !!issue?.sub_issues_count && ( -
+
{issue.sub_issues_count}
@@ -147,7 +147,7 @@ export const ListProperties: FC = observer((props) => { {/* attachments */} {displayProperties && displayProperties?.attachment_count && !!issue?.attachment_count && ( -
+
{issue.attachment_count}
@@ -157,7 +157,7 @@ export const ListProperties: FC = observer((props) => { {/* link */} {displayProperties && displayProperties?.link && !!issue?.link_count && ( -
+
{issue.link_count}
diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx index cfe3481e3..d0bb29711 100644 --- a/web/components/issues/issue-layouts/properties/date.tsx +++ b/web/components/issues/issue-layouts/properties/date.tsx @@ -61,6 +61,7 @@ export const IssuePropertyDate: React.FC = observer((props) ref={dropdownBtn} className="border-none outline-none" onClick={(e) => e.stopPropagation()} + disabled={disabled} > = observer((props)
diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index 282268d7b..d0045c3d4 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -128,7 +128,11 @@ export const IssuePropertyLabels: React.FC = observer((pro ))} ) : ( -
+
= observer((pro ) : (
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 07631a7de..133cce1f9 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 @@ -23,7 +23,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) => const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { - copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", 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 5faace440..af018a652 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 @@ -27,7 +27,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { - copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 7bd35e321..0ad1f610b 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 @@ -27,7 +27,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { - copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 0f4fe28ce..12438b2a3 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 @@ -37,7 +37,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => const { setToastAlert } = useToast(); const handleCopyIssueLink = () => { - copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => + copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => setToastAlert({ type: "success", title: "Link copied", diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index f967956f0..f77dfbed4 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -17,8 +17,6 @@ import { import { TransferIssues, TransferIssuesModal } from "components/cycles"; // ui import { Spinner } from "@plane/ui"; -// helpers -import { getDateRangeStatus } from "helpers/date-time.helper"; export const CycleLayoutRoot: React.FC = observer(() => { const [transferIssuesModal, setTransferIssuesModal] = useState(false); @@ -50,10 +48,7 @@ export const CycleLayoutRoot: React.FC = observer(() => { const activeLayout = issueFilters?.displayFilters?.layout; const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined; - const cycleStatus = - cycleDetails?.start_date && cycleDetails?.end_date - ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) - : "draft"; + const cycleStatus = cycleDetails?.status.toLocaleLowerCase() ?? "draft"; return ( <> diff --git a/web/components/issues/peek-overview/activity/card.tsx b/web/components/issues/peek-overview/activity/card.tsx index b8acb63d9..86d1a138c 100644 --- a/web/components/issues/peek-overview/activity/card.tsx +++ b/web/components/issues/peek-overview/activity/card.tsx @@ -88,7 +88,7 @@ export const IssueActivityCard: FC = (props) => {
-
+
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( Plane ) : activityItem.actor_detail.is_bot ? ( @@ -101,8 +101,8 @@ export const IssueActivityCard: FC = (props) => { : activityItem.actor_detail.display_name} - )}{" "} - {message}{" "} + )} + {message} = observer((props) => { return ( + + {issueDetails?.parent && ( + )} - {issueDetails?.parent && } - +
); }; diff --git a/web/components/issues/view-select/due-date.tsx b/web/components/issues/view-select/due-date.tsx index d7e00148e..5b2bfb0ec 100644 --- a/web/components/issues/view-select/due-date.tsx +++ b/web/components/issues/view-select/due-date.tsx @@ -61,9 +61,9 @@ export const ViewDueDateSelect: React.FC = ({ className={`bg-transparent ${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`} customInput={
{issue.target_date ? ( <> diff --git a/web/components/issues/view-select/start-date.tsx b/web/components/issues/view-select/start-date.tsx index c1408f015..8cfac4a64 100644 --- a/web/components/issues/view-select/start-date.tsx +++ b/web/components/issues/view-select/start-date.tsx @@ -49,9 +49,9 @@ export const ViewStartDateSelect: React.FC = ({ handleOnOpen={handleOnOpen} customInput={
{issue?.start_date ? ( <> diff --git a/web/components/modules/select/lead.tsx b/web/components/modules/select/lead.tsx index d84d67237..8f376618c 100644 --- a/web/components/modules/select/lead.tsx +++ b/web/components/modules/select/lead.tsx @@ -7,7 +7,7 @@ import { ProjectMemberService } from "services/project"; import { Avatar, CustomSearchSelect } from "@plane/ui"; // icons import { Combobox } from "@headlessui/react"; -import { UserCircle } from "lucide-react"; +import { UserCircle, UserCircle2 } from "lucide-react"; // fetch-keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; @@ -65,9 +65,10 @@ export const ModuleLeadSelect: React.FC = ({ value, onChange }) => { value="" className="flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 text-custom-text-200" > - - No Lead - +
+ + No lead +
} onChange={onChange} diff --git a/web/components/modules/sidebar-select/select-lead.tsx b/web/components/modules/sidebar-select/select-lead.tsx index f675925c4..91a70504f 100644 --- a/web/components/modules/sidebar-select/select-lead.tsx +++ b/web/components/modules/sidebar-select/select-lead.tsx @@ -31,6 +31,17 @@ export const SidebarLeadSelect: FC = (props) => { : null ); + const noLeadOption = { + value: "", + query: "No lead", + content: ( +
+ + No lead +
+ ), + }; + const options = members?.map((member) => ({ value: member.member.id, query: member.member.display_name, @@ -42,6 +53,8 @@ export const SidebarLeadSelect: FC = (props) => { ), })); + const leadOption = (options || []).concat(noLeadOption); + const selectedOption = members?.find((m) => m.member.id === value)?.member; return ( @@ -69,7 +82,7 @@ export const SidebarLeadSelect: FC = (props) => {
) } - options={options} + options={leadOption} maxHeight="md" onChange={onChange} /> diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index 3c9552465..3fe7bf57e 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -15,7 +15,17 @@ import ProgressChart from "components/core/sidebar/progress-chart"; import { CustomRangeDatePicker } from "components/ui"; import { CustomMenu, Loader, LayersIcon, CustomSelect, ModuleStatusIcon } from "@plane/ui"; // icon -import { AlertCircle, ChevronDown, ChevronRight, Info, LinkIcon, MoveRight, Plus, Trash2 } from "lucide-react"; +import { + AlertCircle, + CalendarCheck2, + CalendarClock, + ChevronDown, + ChevronRight, + Info, + LinkIcon, + Plus, + Trash2, +} from "lucide-react"; // helpers import { isDateGreaterThanToday, @@ -227,7 +237,13 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { else newValues.push(value); } - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, moduleId); + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EFilterType.FILTERS, + { [key]: newValues }, + moduleId + ); }, [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] ); @@ -328,8 +344,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
-

{moduleDetails.name}

-
+
= observer((props) => { )} /> +
+

{moduleDetails.name}

+
-
- + {moduleDetails.description && ( + + {moduleDetails.description} + + )} + +
+
+
+ + + Start Date +
+
+ - {areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} + + {areYearsEqual + ? renderShortDate(startDate, "No date selected") + : renderShortMonthDate(startDate, "No date selected")} + = observer((props) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - + { @@ -402,16 +441,32 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { - - +
+
+ +
+
+ + Target Date +
+
+ <> - {areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")} + + {areYearsEqual + ? renderShortDate(endDate, "No date selected") + : renderShortMonthDate(endDate, "No date selected")} + = observer((props) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - + { @@ -442,15 +497,6 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
-
- - {moduleDetails.description && ( - - {moduleDetails.description} - - )} - -
{ href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline" + className="inline-flex items-center gap-1 font-medium text-custom-text-200 hover:underline" > - Issue - + Issue. )} diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 4ed1bf2c4..56cc6130d 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -253,32 +253,31 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { )} {project.archive_in > 0 && ( - router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`)} - > -
- - Archived Issues -
+ + +
+ + Archived Issues +
+
)} - router.push(`/${workspaceSlug}/projects/${project?.id}/draft-issues`)} - > -
- - Draft Issues -
+ + +
+ + Draft Issues +
+
- router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)} - > -
- - Settings -
+ + +
+ + Settings +
+
- {/* leave project */} {isViewerOrGuest && ( diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 6c9054ac0..9a71409d3 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -138,9 +138,9 @@ export const ProjectSidebarList: FC = observer(() => { > Favorites {open ? ( - + ) : ( - + )} {isAuthorizedUser && ( @@ -215,9 +215,9 @@ export const ProjectSidebarList: FC = observer(() => { > Projects {open ? ( - + ) : ( - + )} {isAuthorizedUser && ( diff --git a/web/components/workspace/issues-stats.tsx b/web/components/workspace/issues-stats.tsx index 1fa5cf2df..b8cc8432f 100644 --- a/web/components/workspace/issues-stats.tsx +++ b/web/components/workspace/issues-stats.tsx @@ -7,6 +7,7 @@ import { Info } from "lucide-react"; // types import { IUserWorkspaceDashboard } from "types"; import { useRouter } from "next/router"; +import Link from "next/link"; type Props = { data: IUserWorkspaceDashboard | undefined; @@ -19,61 +20,40 @@ export const IssuesStats: React.FC = ({ data }) => {
-
-

Issues assigned to you

-
- {data ? ( -
router.push(`/${workspaceSlug}/workspace-views/assigned`)} - > - {data.assigned_issues_count} -
- ) : ( - - - - )} -
-
-
-

Pending issues

-
- {data ? ( - data.pending_issues_count - ) : ( - - - - )} -
-
+ +
+

Issues assigned to you

+
+
{data?.assigned_issues_count}
+
+
+ + +
+

Pending issues

+
{data?.pending_issues_count}
+
+
-
-

Completed issues

-
- {data ? ( - data.completed_issues_count - ) : ( - - - - )} -
-
-
-

Issues due by this week

-
- {data ? ( - data.issues_due_week_count - ) : ( - - - - )} -
-
+ +
+

Completed issues

+
{data?.completed_issues_count}
+
+ + +
+

Issues due by this week

+
{data?.issues_due_week_count}
+
+
diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index b869d4d63..bd650c888 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -28,7 +28,8 @@ type Props = { display_name: string; role: TUserWorkspaceRole; status: boolean; - member: boolean; + is_member: boolean; + responded_at: string | null; accountCreated: boolean; }; }; @@ -102,9 +103,8 @@ export const WorkspaceMembersListItem: FC = observer((props) => { }; const handleRemove = async () => { - if (member.member) { + if (member.is_member) { const memberId = member.memberId; - if (memberId === currentUser?.id) await handleLeaveWorkspace(); else await handleRemoveMember(); } else await handleRemoveInvitation(); @@ -154,7 +154,7 @@ export const WorkspaceMembersListItem: FC = observer((props) => { )}
- {member.member ? ( + {member.is_member ? ( {member.first_name} {member.last_name} @@ -175,16 +175,21 @@ export const WorkspaceMembersListItem: FC = observer((props) => {
- {!member?.status && ( + {!member?.status && !member.responded_at && (

Pending

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

Account not created

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

Rejected

+
+ )} diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 8d1cfa27d..cc3c878e4 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -16,7 +16,7 @@ import { Avatar, Loader } from "@plane/ui"; import { IWorkspace } from "types"; // Static Data -const userLinks = (workspaceSlug: string, userId: string) => [ +const WORKSPACE_DROPDOWN_ITEMS = (workspaceSlug: string, userId: string) => [ { name: "Workspace Settings", href: `/${workspaceSlug}/settings`, @@ -155,8 +155,8 @@ export const WorkspaceSidebarDropdown = observer(() => { workspaces.map((workspace: IWorkspace) => ( {() => ( - + )} )) @@ -198,17 +198,19 @@ export const WorkspaceSidebarDropdown = observer(() => {

No workspace found!

)}
- { - setTrackElement("APP_SIEDEBAR_WORKSPACE_DROPDOWN"); - router.push("/create-workspace"); - }} - className="flex w-full items-center gap-2 px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" - > - - Create Workspace + + {() => ( + { + setTrackElement("APP_SIEDEBAR_WORKSPACE_DROPDOWN"); + }} + > + + Create Workspace + + )}
@@ -222,18 +224,20 @@ export const WorkspaceSidebarDropdown = observer(() => { )}
- {userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( - { - router.push(link.href); - }} - className="flex w-full cursor-pointer items-center justify-start rounded px-2 py-1 text-sm text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" - > - {link.name} - - ))} + {WORKSPACE_DROPDOWN_ITEMS(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map( + (link, index) => ( + + {() => ( + + {link.name} + + )} + + ) + )}
{ } }; -export const getDateRangeStatus = (startDate: string | null | undefined, endDate: string | null | undefined) => { - if (!startDate || !endDate) return "draft"; - - const now = new Date(); - const start = new Date(startDate); - const end = new Date(endDate); - - if (start <= now && end >= now) return "current"; - else if (start > now) return "upcoming"; - else return "completed"; -}; - export const renderShortDateWithYearFormat = (date: string | Date, placeholder?: string) => { if (!date || date === "") return null; diff --git a/web/hooks/use-sign-in-redirection.ts b/web/hooks/use-sign-in-redirection.ts index 1863e510e..25d4e8bbd 100644 --- a/web/hooks/use-sign-in-redirection.ts +++ b/web/hooks/use-sign-in-redirection.ts @@ -17,36 +17,54 @@ const useSignInRedirection = (): UseSignInRedirectionProps => { const [error, setError] = useState(null); // router const router = useRouter(); - const { next_url } = router.query; + const { next_path } = router.query; // mobx store const { user: { fetchCurrentUser, fetchCurrentUserSettings }, } = useMobxStore(); + const isValidURL = (url: string): boolean => { + const disallowedSchemes = /^(https?|ftp):\/\//i; + return !disallowedSchemes.test(url); + }; + + console.log("next_path", next_path); + const handleSignInRedirection = useCallback( async (user: IUser) => { - // if the user is not onboarded, redirect them to the onboarding page - if (!user.is_onboarded) { - router.push("/onboarding"); - return; - } - // if next_url is provided, redirect the user to that url - if (next_url) { - router.push(next_url.toString()); - return; - } + try { + // if the user is not onboarded, redirect them to the onboarding page + if (!user.is_onboarded) { + router.push("/onboarding"); + return; + } + // if next_path is provided, redirect the user to that url + if (next_path) { + if (isValidURL(next_path.toString())) { + router.push(next_path.toString()); + return; + } else { + router.push("/"); + return; + } + } - // if the user is onboarded, fetch their last workspace details - await fetchCurrentUserSettings() - .then((userSettings: IUserSettings) => { - const workspaceSlug = - userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug; - if (workspaceSlug) router.push(`/${workspaceSlug}`); - else router.push("/profile"); - }) - .catch((err) => setError(err)); + // Fetch the current user settings + const userSettings: IUserSettings = await fetchCurrentUserSettings(); + + // Extract workspace details + const workspaceSlug = + userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug; + + // Redirect based on workspace details or to profile if not available + if (workspaceSlug) router.push(`/${workspaceSlug}`); + else router.push("/profile"); + } catch (error) { + console.error("Error in handleSignInRedirection:", error); + setError(error); + } }, - [fetchCurrentUserSettings, router, next_url] + [fetchCurrentUserSettings, router, next_path] ); const updateUserInfo = useCallback(async () => { diff --git a/web/hooks/use-user-auth.tsx b/web/hooks/use-user-auth.tsx index 882c0b713..8290a4545 100644 --- a/web/hooks/use-user-auth.tsx +++ b/web/hooks/use-user-auth.tsx @@ -12,7 +12,7 @@ const workspaceService = new WorkspaceService(); const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "admin") => { const router = useRouter(); - const { next_url } = router.query; + const { next_path } = router.query; const [isRouteAccess, setIsRouteAccess] = useState(true); const { @@ -29,6 +29,11 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm shouldRetryOnError: false, }); + const isValidURL = (url: string): boolean => { + const disallowedSchemes = /^(https?|ftp):\/\//i; + return !disallowedSchemes.test(url); + }; + useEffect(() => { const handleWorkSpaceRedirection = async () => { workspaceService.userWorkspaces().then(async (userWorkspaces) => { @@ -84,8 +89,15 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm if (!isLoading) { setIsRouteAccess(() => true); if (user) { - if (next_url) router.push(next_url.toString()); - else handleUserRouteAuthentication(); + if (next_path) { + if (isValidURL(next_path.toString())) { + router.push(next_path.toString()); + return; + } else { + router.push("/"); + return; + } + } else handleUserRouteAuthentication(); } else { if (routeAuth === "sign-in") { setIsRouteAccess(() => false); @@ -97,7 +109,7 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm } } } - }, [user, isLoading, routeAuth, router, next_url]); + }, [user, isLoading, routeAuth, router, next_path]); return { isLoading: isRouteAccess, diff --git a/web/hooks/use-user.tsx b/web/hooks/use-user.tsx index 97518fed6..2203faaa2 100644 --- a/web/hooks/use-user.tsx +++ b/web/hooks/use-user.tsx @@ -31,7 +31,7 @@ export default function useUser({ redirectTo = "", redirectIfFound = false, opti ) { router.push(redirectTo); return; - // const nextLocation = router.asPath.split("?next=")[1]; + // const nextLocation = router.asPath.split("?next_path=")[1]; // if (nextLocation) { // router.push(nextLocation as string); // return; diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index 50b17fdb7..ccc30a382 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -56,7 +56,7 @@ export const UserAuthWrapper: FC = observer((props) => { if (currentUserError) { const redirectTo = router.asPath; - router.push(`/?next=${redirectTo}`); + router.push(`/?next_path=${redirectTo}`); return null; } diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index f6df0c4fc..e695b1f4c 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -11,8 +11,6 @@ import { Controller, useForm } from "react-hook-form"; import { useMobxStore } from "lib/mobx/store-provider"; // services import { WorkspaceService } from "services/workspace.service"; -// hooks -import useUserAuth from "hooks/use-user-auth"; // layouts import DefaultLayout from "layouts/default-layout"; import { UserAuthWrapper } from "layouts/auth-layout"; @@ -45,8 +43,6 @@ const OnboardingPage: NextPageWithLayout = observer(() => { const { setTheme } = useTheme(); - const {} = useUserAuth("onboarding"); - const { control, setValue } = useForm<{ full_name: string }>({ defaultValues: { full_name: "", @@ -158,8 +154,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : currentUser?.email + ? value + : currentUser?.email } src={currentUser?.avatar} size={35} @@ -174,8 +170,8 @@ const OnboardingPage: NextPageWithLayout = observer(() => { {currentUser?.first_name ? `${currentUser?.first_name} ${currentUser?.last_name ?? ""}` : value.length > 0 - ? value - : null} + ? value + : null}

)} diff --git a/web/pages/workspace-invitations/index.tsx b/web/pages/workspace-invitations/index.tsx index 32cb61432..d374549aa 100644 --- a/web/pages/workspace-invitations/index.tsx +++ b/web/pages/workspace-invitations/index.tsx @@ -52,6 +52,19 @@ const WorkspaceInvitationPage: NextPageWithLayout = () => { .catch((err) => console.error(err)); }; + const handleReject = () => { + if (!invitationDetail) return; + workspaceService + .joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, { + accepted: false, + email: invitationDetail.email, + }) + .then(() => { + router.push("/"); + }) + .catch((err) => console.error(err)); + }; + return (
{invitationDetail ? ( @@ -77,13 +90,7 @@ const WorkspaceInvitationPage: NextPageWithLayout = () => { description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account." > - { - router.push("/"); - }} - /> + )} diff --git a/web/store/cycle/cycles.store.ts b/web/store/cycle/cycles.store.ts index 96122ec14..b6602172d 100644 --- a/web/store/cycle/cycles.store.ts +++ b/web/store/cycle/cycles.store.ts @@ -7,7 +7,6 @@ import { RootStore } from "../root"; import { ProjectService } from "services/project"; import { IssueService } from "services/issue"; import { CycleService } from "services/cycle.service"; -import { getDateRangeStatus } from "helpers/date-time.helper"; export interface ICycleStore { loader: boolean; @@ -318,7 +317,7 @@ export class CycleStore implements ICycleStore { }; addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycle: ICycle) => { - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const cycleStatus = cycle.status; const statusCyclesList = this.cycles[projectId]?.[cycleStatus] ?? []; const allCyclesList = this.projectCycles ?? []; @@ -379,7 +378,7 @@ export class CycleStore implements ICycleStore { }; removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycle: ICycle) => { - const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date); + const cycleStatus = cycle.status; const statusCyclesList = this.cycles[projectId]?.[cycleStatus] ?? []; const allCyclesList = this.projectCycles ?? []; diff --git a/web/store/workspace/workspace-member.store.ts b/web/store/workspace/workspace-member.store.ts index e699cb467..dc6183245 100644 --- a/web/store/workspace/workspace-member.store.ts +++ b/web/store/workspace/workspace-member.store.ts @@ -114,8 +114,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { display_name: item.email, role: item.role, status: item.accepted, - member: false, - accountCreated: item.accepted, + is_member: false, + responded_at: item.responded_at, })) || []), ...(this.workspaceMembers?.map((item) => ({ id: item.id, @@ -127,8 +127,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { display_name: item.member?.display_name, role: item.role, status: true, - member: true, - accountCreated: true, + is_member: true, + responded_at: "accepted", })) || []), ]; } diff --git a/web/types/cycles.d.ts b/web/types/cycles.d.ts index c3c5248aa..4f243deeb 100644 --- a/web/types/cycles.d.ts +++ b/web/types/cycles.d.ts @@ -2,6 +2,8 @@ import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; +export type TCycleGroups = "current" | "upcoming" | "completed" | "draft"; + export type TCycleLayout = "list" | "board" | "gantt"; export interface ICycle { @@ -24,6 +26,7 @@ export interface ICycle { owned_by: IUser; project: string; project_detail: IProjectLite; + status: TCycleGroups; sort_order: number; start_date: string | null; started_issues: number;