diff --git a/apiserver/plane/app/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py index d28f38c75..e73b5ceef 100644 --- a/apiserver/plane/app/serializers/estimate.py +++ b/apiserver/plane/app/serializers/estimate.py @@ -11,10 +11,6 @@ from rest_framework import serializers class EstimateSerializer(BaseSerializer): - workspace_detail = WorkspaceLiteSerializer( - read_only=True, source="workspace" - ) - project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = Estimate @@ -48,10 +44,6 @@ class EstimatePointSerializer(BaseSerializer): class EstimateReadSerializer(BaseSerializer): points = EstimatePointSerializer(read_only=True, many=True) - workspace_detail = WorkspaceLiteSerializer( - read_only=True, source="workspace" - ) - project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = Estimate diff --git a/apiserver/plane/app/urls/estimate.py b/apiserver/plane/app/urls/estimate.py index d8571ff0c..7db94aa46 100644 --- a/apiserver/plane/app/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -4,6 +4,7 @@ from django.urls import path from plane.app.views import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + EstimatePointEndpoint, ) @@ -34,4 +35,23 @@ urlpatterns = [ ), name="bulk-create-estimate-points", ), + path( + "workspaces//projects//estimates//estimate-points/", + EstimatePointEndpoint.as_view( + { + "post": "create", + } + ), + name="estimate-points", + ), + path( + "workspaces//projects//estimates//estimate-points//", + EstimatePointEndpoint.as_view( + { + "patch": "partial_update", + "delete": "destroy", + } + ), + name="estimate-points", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 592d897e0..8da0268b9 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -190,6 +190,7 @@ from .external.base import ( from .estimate.base import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, + EstimatePointEndpoint, ) from .inbox.base import InboxViewSet, InboxIssueViewSet diff --git a/apiserver/plane/app/views/analytic/base.py b/apiserver/plane/app/views/analytic/base.py index 256d3cae5..3d27641e3 100644 --- a/apiserver/plane/app/views/analytic/base.py +++ b/apiserver/plane/app/views/analytic/base.py @@ -33,7 +33,7 @@ class AnalyticsEndpoint(BaseAPIView): "state__group", "labels__id", "assignees__id", - "estimate_point", + "estimate_point__value", "issue_cycle__cycle_id", "issue_module__module_id", "priority", @@ -381,9 +381,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView): ) open_estimate_sum = open_issues_queryset.aggregate( - sum=Sum("estimate_point") + sum=Sum("point") )["sum"] - total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[ + total_estimate_sum = base_issues.aggregate(sum=Sum("point"))[ "sum" ] diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index 7ac3035a9..2bd9e3dfe 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -1,3 +1,6 @@ +import random +import string + # Third party imports from rest_framework.response import Response from rest_framework import status @@ -5,7 +8,7 @@ from rest_framework import status # Module imports from ..base import BaseViewSet, BaseAPIView from plane.app.permissions import ProjectEntityPermission -from plane.db.models import Project, Estimate, EstimatePoint +from plane.db.models import Project, Estimate, EstimatePoint, Issue from plane.app.serializers import ( EstimateSerializer, EstimatePointSerializer, @@ -13,6 +16,12 @@ from plane.app.serializers import ( ) from plane.utils.cache import invalidate_cache + +def generate_random_name(length=10): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for i in range(length)) + + class ProjectEstimatePointEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, @@ -49,13 +58,17 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer = EstimateReadSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def create(self, request, slug, project_id): - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) + estimate = request.data.get('estimate') + estimate_name = estimate.get("name", generate_random_name()) + estimate_type = estimate.get("type", 'categories') + last_used = estimate.get("last_used", False) + estimate = Estimate.objects.create( + name=estimate_name, project_id=project_id, last_used=last_used, type=estimate_type + ) estimate_points = request.data.get("estimate_points", []) @@ -67,14 +80,6 @@ class BulkEstimatePointEndpoint(BaseViewSet): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - estimate_serializer = EstimateSerializer( - data=request.data.get("estimate") - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - estimate = estimate_serializer.save(project_id=project_id) estimate_points = EstimatePoint.objects.bulk_create( [ EstimatePoint( @@ -93,17 +98,8 @@ class BulkEstimatePointEndpoint(BaseViewSet): ignore_conflicts=True, ) - estimate_point_serializer = EstimatePointSerializer( - estimate_points, many=True - ) - - return Response( - { - "estimate": estimate_serializer.data, - "estimate_points": estimate_point_serializer.data, - }, - status=status.HTTP_200_OK, - ) + serializer = EstimateReadSerializer(estimate) + return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( @@ -115,13 +111,10 @@ class BulkEstimatePointEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def partial_update(self, request, slug, project_id, estimate_id): - if not request.data.get("estimate", False): - return Response( - {"error": "Estimate is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) if not len(request.data.get("estimate_points", [])): return Response( @@ -131,15 +124,10 @@ class BulkEstimatePointEndpoint(BaseViewSet): estimate = Estimate.objects.get(pk=estimate_id) - estimate_serializer = EstimateSerializer( - estimate, data=request.data.get("estimate"), partial=True - ) - if not estimate_serializer.is_valid(): - return Response( - estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - - estimate = estimate_serializer.save() + if request.data.get("estimate"): + estimate.name = request.data.get("estimate").get("name", estimate.name) + estimate.type = request.data.get("estimate").get("type", estimate.type) + estimate.save() estimate_points_data = request.data.get("estimate_points", []) @@ -165,29 +153,113 @@ class BulkEstimatePointEndpoint(BaseViewSet): estimate_point.value = estimate_point_data[0].get( "value", estimate_point.value ) + estimate_point.key = estimate_point_data[0].get( + "key", estimate_point.key + ) updated_estimate_points.append(estimate_point) EstimatePoint.objects.bulk_update( updated_estimate_points, - ["value"], + ["key", "value"], batch_size=10, ) - estimate_point_serializer = EstimatePointSerializer( - estimate_points, many=True - ) + estimate_serializer = EstimateReadSerializer(estimate) return Response( - { - "estimate": estimate_serializer.data, - "estimate_points": estimate_point_serializer.data, - }, + estimate_serializer.data, status=status.HTTP_200_OK, ) - @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False) + @invalidate_cache( + path="/api/workspaces/:slug/estimates/", url_params=True, user=False + ) def destroy(self, request, slug, project_id, estimate_id): estimate = Estimate.objects.get( pk=estimate_id, workspace__slug=slug, project_id=project_id ) estimate.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class EstimatePointEndpoint(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + + def create(self, request, slug, project_id, estimate_id): + # TODO: add a key validation if the same key already exists + if not request.data.get("key") or not request.data.get("value"): + return Response( + {"error": "Key and value are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + key = request.data.get("key", 0) + value = request.data.get("value", "") + estimate_point = EstimatePoint.objects.create( + estimate_id=estimate_id, + project_id=project_id, + key=key, + value=value, + ) + serializer = EstimatePointSerializer(estimate_point).data + return Response(serializer, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, estimate_id, estimate_point_id): + # TODO: add a key validation if the same key already exists + estimate_point = EstimatePoint.objects.get( + pk=estimate_point_id, + estimate_id=estimate_id, + project_id=project_id, + workspace__slug=slug, + ) + serializer = EstimatePointSerializer( + estimate_point, data=request.data, partial=True + ) + if not serializer.is_valid(): + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy( + self, request, slug, project_id, estimate_id, estimate_point_id + ): + new_estimate_id = request.GET.get("new_estimate_id", None) + estimate_points = EstimatePoint.objects.filter( + estimate_id=estimate_id, + project_id=project_id, + workspace__slug=slug, + ) + # update all the issues with the new estimate + if new_estimate_id: + _ = Issue.objects.filter( + project_id=project_id, + workspace__slug=slug, + estimate_id=estimate_point_id, + ).update(estimate_id=new_estimate_id) + + # delete the estimate point + old_estimate_point = EstimatePoint.objects.filter( + pk=estimate_point_id + ).first() + + # rearrange the estimate points + updated_estimate_points = [] + for estimate_point in estimate_points: + if estimate_point.key > old_estimate_point.key: + estimate_point.key -= 1 + updated_estimate_points.append(estimate_point) + + EstimatePoint.objects.bulk_update( + updated_estimate_points, + ["key"], + batch_size=10, + ) + + old_estimate_point.delete() + + return Response( + EstimatePointSerializer(updated_estimate_points, many=True).data, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/db/migrations/0067_issue_estimate.py b/apiserver/plane/db/migrations/0067_issue_estimate.py new file mode 100644 index 000000000..b341f9864 --- /dev/null +++ b/apiserver/plane/db/migrations/0067_issue_estimate.py @@ -0,0 +1,260 @@ +# # Generated by Django 4.2.7 on 2024-05-24 09:47 +# Python imports +import uuid +from uuid import uuid4 +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models +import plane.db.models.deploy_board + + +def issue_estimate_point(apps, schema_editor): + Issue = apps.get_model("db", "Issue") + Project = apps.get_model("db", "Project") + EstimatePoint = apps.get_model("db", "EstimatePoint") + IssueActivity = apps.get_model("db", "IssueActivity") + updated_estimate_point = [] + updated_issue_activity = [] + + # loop through all the projects + for project in Project.objects.filter(estimate__isnull=False): + estimate_points = EstimatePoint.objects.filter( + estimate=project.estimate, project=project + ) + + for issue_activity in IssueActivity.objects.filter( + field="estimate_point", project=project + ): + if issue_activity.new_value: + new_identifier = estimate_points.filter( + key=issue_activity.new_value + ).first().id + issue_activity.new_identifier = new_identifier + new_value = estimate_points.filter( + key=issue_activity.new_value + ).first().value + issue_activity.new_value = new_value + + if issue_activity.old_value: + old_identifier = estimate_points.filter( + key=issue_activity.old_value + ).first().id + issue_activity.old_identifier = old_identifier + old_value = estimate_points.filter( + key=issue_activity.old_value + ).first().value + issue_activity.old_value = old_value + updated_issue_activity.append(issue_activity) + + for issue in Issue.objects.filter( + point__isnull=False, project=project + ): + # get the estimate id for the corresponding estimate point in the issue + estimate = estimate_points.filter(key=issue.point).first() + issue.estimate_point = estimate + updated_estimate_point.append(issue) + + Issue.objects.bulk_update( + updated_estimate_point, ["estimate_point"], batch_size=1000 + ) + IssueActivity.objects.bulk_update( + updated_issue_activity, + ["new_value", "old_value", "new_identifier", "old_identifier"], + batch_size=1000, + ) + + +def last_used_estimate(apps, schema_editor): + Project = apps.get_model("db", "Project") + Estimate = apps.get_model("db", "Estimate") + + # Get all estimate ids used in projects + estimate_ids = Project.objects.filter(estimate__isnull=False).values_list( + "estimate", flat=True + ) + + # Update all matching estimates + Estimate.objects.filter(id__in=estimate_ids).update(last_used=True) + + +def populate_deploy_board(apps, schema_editor): + DeployBoard = apps.get_model("db", "DeployBoard") + ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard") + + DeployBoard.objects.bulk_create( + [ + DeployBoard( + entity_identifier=deploy_board.project_id, + project_id=deploy_board.project_id, + entity_name="project", + anchor=uuid4().hex, + is_comments_enabled=deploy_board.comments, + is_reactions_enabled=deploy_board.reactions, + inbox=deploy_board.inbox, + is_votes_enabled=deploy_board.votes, + view_props=deploy_board.views, + workspace_id=deploy_board.workspace_id, + created_at=deploy_board.created_at, + updated_at=deploy_board.updated_at, + created_by_id=deploy_board.created_by_id, + updated_by_id=deploy_board.updated_by_id, + ) + for deploy_board in ProjectDeployBoard.objects.all() + ], + batch_size=100, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0066_account_id_token_cycle_logo_props_module_logo_props"), + ] + + operations = [ + migrations.CreateModel( + name="DeployBoard", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("entity_identifier", models.UUIDField(null=True)), + ( + "entity_name", + models.CharField( + choices=[ + ("project", "Project"), + ("issue", "Issue"), + ("module", "Module"), + ("cycle", "Task"), + ("page", "Page"), + ("view", "View"), + ], + max_length=30, + ), + ), + ( + "anchor", + models.CharField( + db_index=True, + default=plane.db.models.deploy_board.get_anchor, + max_length=255, + unique=True, + ), + ), + ("is_comments_enabled", models.BooleanField(default=False)), + ("is_reactions_enabled", models.BooleanField(default=False)), + ("is_votes_enabled", models.BooleanField(default=False)), + ("view_props", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "inbox", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="board_inbox", + to="db.inbox", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Deploy Board", + "verbose_name_plural": "Deploy Boards", + "db_table": "deploy_boards", + "ordering": ("-created_at",), + "unique_together": {("entity_name", "entity_identifier")}, + }, + ), + migrations.AddField( + model_name="estimate", + name="last_used", + field=models.BooleanField(default=False), + ), + # Rename the existing field + migrations.RenameField( + model_name="issue", + old_name="estimate_point", + new_name="point", + ), + # Add a new field with the original name as a foreign key + migrations.AddField( + model_name="issue", + name="estimate_point", + field=models.ForeignKey( + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_estimates", + to="db.EstimatePoint", + blank=True, + null=True, + ), + ), + migrations.AlterField( + model_name="estimate", + name="type", + field=models.CharField(default="categories", max_length=255), + ), + migrations.AlterField( + model_name="estimatepoint", + name="value", + field=models.CharField(max_length=255), + ), + migrations.RunPython(issue_estimate_point), + migrations.RunPython(last_used_estimate), + migrations.RunPython(populate_deploy_board), + ] diff --git a/packages/types/src/analytics.d.ts b/packages/types/src/analytics.d.ts index 35da4b723..2fb7ad51a 100644 --- a/packages/types/src/analytics.d.ts +++ b/packages/types/src/analytics.d.ts @@ -54,7 +54,7 @@ export type TXAxisValues = | "state__group" | "labels__id" | "assignees__id" - | "estimate_point" + | "estimate_point__value" | "issue_cycle__cycle_id" | "issue_module__module_id" | "priority" diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index a4d098506..cc8575374 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -24,3 +24,16 @@ export enum EIssueCommentAccessSpecifier { EXTERNAL = "EXTERNAL", INTERNAL = "INTERNAL", } + +// estimates +export enum EEstimateSystem { + POINTS = "points", + CATEGORIES = "categories", + TIME = "time", +} + +export enum EEstimateUpdateStages { + CREATE = "create", + EDIT = "edit", + SWITCH = "switch", +} diff --git a/packages/types/src/estimate.d.ts b/packages/types/src/estimate.d.ts index 96b584ce1..9bad7e260 100644 --- a/packages/types/src/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -1,40 +1,77 @@ -export interface IEstimate { - created_at: Date; - created_by: string; - description: string; - id: string; - name: string; - project: string; - project_detail: IProject; - updated_at: Date; - updated_by: string; - points: IEstimatePoint[]; - workspace: string; - workspace_detail: IWorkspace; -} +import { EEstimateSystem, EEstimateUpdateStages } from "./enums"; export interface IEstimatePoint { - created_at: string; - created_by: string; - description: string; - estimate: string; - id: string; - key: number; - project: string; - updated_at: string; - updated_by: string; - value: string; - workspace: string; + id: string | undefined; + key: number | undefined; + value: string | undefined; + description: string | undefined; + workspace: string | undefined; + project: string | undefined; + estimate: string | undefined; + created_at: Date | undefined; + updated_at: Date | undefined; + created_by: string | undefined; + updated_by: string | undefined; +} + +export type TEstimateSystemKeys = + | EEstimateSystem.POINTS + | EEstimateSystem.CATEGORIES + | EEstimateSystem.TIME; + +export interface IEstimate { + id: string | undefined; + name: string | undefined; + description: string | undefined; + type: TEstimateSystemKeys | undefined; // categories, points, time + points: IEstimatePoint[] | undefined; + workspace: string | undefined; + project: string | undefined; + last_used: boolean | undefined; + created_at: Date | undefined; + updated_at: Date | undefined; + created_by: string | undefined; + updated_by: string | undefined; } export interface IEstimateFormData { - estimate: { - name: string; - description: string; + estimate?: { + name?: string; + type?: string; + last_used?: boolean; }; estimate_points: { - id?: string; + id?: string | undefined; key: number; value: string; }[]; } + +export type TEstimatePointsObject = { + id?: string | undefined; + key: number; + value: string; +}; + +export type TTemplateValues = { + title: string; + values: TEstimatePointsObject[]; + hide?: boolean; +}; + +export type TEstimateSystem = { + name: string; + templates: Record; + is_available: boolean; + is_ee: boolean; +}; + +export type TEstimateSystems = { + [K in TEstimateSystemKeys]: TEstimateSystem; +}; + +// update estimates +export type TEstimateUpdateStageKeys = + | EEstimateUpdateStages.CREATE + | EEstimateUpdateStages.EDIT + | EEstimateUpdateStages.SWITCH; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index b8dd2d3c1..94277e8b6 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -15,7 +15,6 @@ export * from "./importer"; export * from "./inbox"; export * from "./analytics"; export * from "./api_token"; -export * from "./app"; export * from "./auth"; export * from "./calendar"; export * from "./instance"; diff --git a/packages/ui/package.json b/packages/ui/package.json index 84a655bf7..ed0932339 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -24,7 +24,7 @@ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@blueprintjs/core": "^4.16.3", "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^2.0.3", "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", "emoji-picker-react": "^4.5.16", @@ -33,7 +33,7 @@ "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", - "sonner": "^1.4.2", + "sonner": "^1.4.41", "tailwind-merge": "^2.0.0" }, "devDependencies": { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 23fc7ed62..ec2db4ef1 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -13,5 +13,6 @@ export * from "./loader"; export * from "./control-link"; export * from "./toast"; export * from "./drag-handle"; +export * from "./typography"; export * from "./drop-indicator"; export * from "./sortable"; diff --git a/packages/ui/src/sortable/sortable.stories.tsx b/packages/ui/src/sortable/sortable.stories.tsx index 6d40ddc2e..2d469b767 100644 --- a/packages/ui/src/sortable/sortable.stories.tsx +++ b/packages/ui/src/sortable/sortable.stories.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; import React from "react"; -import { Draggable } from "./draggable"; import { Sortable } from "./sortable"; const meta: Meta = { @@ -13,7 +12,7 @@ type Story = StoryObj; const data = [ { id: "1", name: "John Doe" }, - { id: "2", name: "Jane Doe 2" }, + { id: "2", name: "Satish" }, { id: "3", name: "Alice" }, { id: "4", name: "Bob" }, { id: "5", name: "Charlie" }, diff --git a/packages/ui/src/sortable/sortable.tsx b/packages/ui/src/sortable/sortable.tsx index 9d79a8f59..b495d535e 100644 --- a/packages/ui/src/sortable/sortable.tsx +++ b/packages/ui/src/sortable/sortable.tsx @@ -8,7 +8,7 @@ type Props = { onChange: (data: T[]) => void; keyExtractor: (item: T, index: number) => string; containerClassName?: string; - id: string; + id?: string; }; const moveItem = ( @@ -17,7 +17,7 @@ const moveItem = ( destination: T & Record, keyExtractor: (item: T, index: number) => string ) => { - const sourceIndex = data.indexOf(source); + const sourceIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(source, 0)); if (sourceIndex === -1) return data; const destinationIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(destination, 0)); diff --git a/web/components/core/modals/gpt-assistant-popover.tsx b/web/components/core/modals/gpt-assistant-popover.tsx index 099b90254..0c9c30b31 100644 --- a/web/components/core/modals/gpt-assistant-popover.tsx +++ b/web/components/core/modals/gpt-assistant-popover.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, Fragment } from "react"; +import React, { useEffect, useState, useRef, Fragment, Ref } from "react"; import { Placement } from "@popperjs/core"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // services @@ -196,7 +196,7 @@ export const GptAssistantPopover: React.FC = (props) => { } style={styles.popper} {...attributes.popper} > diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx index ce646f893..99e8106de 100644 --- a/web/components/dropdowns/estimate.tsx +++ b/web/components/dropdowns/estimate.tsx @@ -1,5 +1,4 @@ import { Fragment, ReactNode, useRef, useState } from "react"; -import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; import { usePopper } from "react-popper"; import { Check, ChevronDown, Search, Triangle } from "lucide-react"; @@ -7,7 +6,12 @@ import { Combobox } from "@headlessui/react"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppRouter, useEstimate } from "@/hooks/store"; +import { + useAppRouter, + useEstimate, + useProjectEstimates, + // useEstimate +} from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; // components import { DropdownButton } from "./buttons"; @@ -19,10 +23,10 @@ type Props = TDropdownProps & { button?: ReactNode; dropdownArrow?: boolean; dropdownArrowClassName?: string; - onChange: (val: string | null) => void; + onChange: (val: string | undefined) => void; onClose?: () => void; projectId: string; - value: string | null; + value: string | undefined; }; type DropdownOptions = @@ -76,19 +80,29 @@ export const EstimateDropdown: React.FC = observer((props) => { }); // store hooks const { workspaceSlug } = useAppRouter(); - const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate(); - const activeEstimate = getProjectActiveEstimateDetails(projectId); - const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({ - value: point.id, - query: `${point?.value}`, - content: ( -
- - {point.value} -
- ), - })); + const { currentActiveEstimateId, getProjectEstimates } = useProjectEstimates(); + const { estimatePointIds, estimatePointById } = useEstimate( + currentActiveEstimateId ? currentActiveEstimateId : undefined + ); + + const options: DropdownOptions = (estimatePointIds ?? []) + ?.map((estimatePoint) => { + const currentEstimatePoint = estimatePointById(estimatePoint); + if (currentEstimatePoint) + return { + value: currentEstimatePoint.id, + query: `${currentEstimatePoint?.value}`, + content: ( +
+ + {currentEstimatePoint.value} +
+ ), + }; + else undefined; + }) + .filter((estimatePointDropdownOption) => estimatePointDropdownOption != undefined) as DropdownOptions; options?.unshift({ value: null, query: "No estimate", @@ -103,10 +117,10 @@ export const EstimateDropdown: React.FC = observer((props) => { const filteredOptions = query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null; + const selectedEstimate = value && estimatePointById ? estimatePointById(value) : undefined; const onOpen = async () => { - if (!activeEstimate && workspaceSlug) await fetchProjectEstimates(workspaceSlug, projectId); + if (!currentActiveEstimateId && workspaceSlug) await getProjectEstimates(workspaceSlug, projectId); }; const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({ @@ -120,7 +134,7 @@ export const EstimateDropdown: React.FC = observer((props) => { setQuery, }); - const dropdownOnChange = (val: string | null) => { + const dropdownOnChange = (val: string | undefined) => { onChange(val); handleClose(); }; @@ -164,13 +178,13 @@ export const EstimateDropdown: React.FC = observer((props) => { className={buttonClassName} isActive={isOpen} tooltipHeading="Estimate" - tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder} + tooltipContent={selectedEstimate ? selectedEstimate?.value : placeholder} showTooltip={showTooltip} variant={buttonVariant} > {!hideIcon && } {(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( - {selectedEstimate !== null ? selectedEstimate : placeholder} + {selectedEstimate ? selectedEstimate?.value : placeholder} )} {dropdownArrow && (