From 7800f98ce9c0fe7fea97d4442af76cdce99b7d6e Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Mon, 4 Nov 2024 20:22:10 +0000 Subject: [PATCH 001/164] ready for oauth --- admin/ce/components/common/upgrade-button.tsx | 9 +- .../components/admin-sidebar/help-section.tsx | 11 -- .../provider/credentials/magic_code.py | 12 +- .../authentication/utils/redirection_path.py | 4 +- .../plane/authentication/views/app/magic.py | 139 ++++++++++++------ docker-compose-local.yml | 16 +- .../constants/project/settings/features.tsx | 4 +- .../workspace/sidebar/help-section.tsx | 109 +------------- web/core/constants/dashboard.ts | 50 +++---- 9 files changed, 138 insertions(+), 216 deletions(-) diff --git a/admin/ce/components/common/upgrade-button.tsx b/admin/ce/components/common/upgrade-button.tsx index aa3c95fdbed..cdbe32a5baf 100644 --- a/admin/ce/components/common/upgrade-button.tsx +++ b/admin/ce/components/common/upgrade-button.tsx @@ -9,8 +9,9 @@ import { getButtonStyling } from "@plane/ui"; import { cn } from "@/helpers/common.helper"; export const UpgradeButton: React.FC = () => ( - - Available on One - - + + // + // Available on One + // + // ); diff --git a/admin/core/components/admin-sidebar/help-section.tsx b/admin/core/components/admin-sidebar/help-section.tsx index abba68e3eae..dcb96d0a512 100644 --- a/admin/core/components/admin-sidebar/help-section.tsx +++ b/admin/core/components/admin-sidebar/help-section.tsx @@ -61,17 +61,6 @@ export const HelpSection: FC = observer(() => { {!isSidebarCollapsed && "Redirect to plane"} - - - - } - customButtonClassName={`relative grid place-items-center rounded-md p-1.5 outline-none ${isCollapsed ? "w-full" : ""}`} - menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)} - onMenuClose={() => setIsNeedHelpOpen(false)} - placement={isCollapsed ? "left-end" : "top-end"} - maxHeight="lg" - closeOnSelect - > - - - - Documentation - - - {config?.intercom_app_id && config?.is_intercom_enabled && ( - - - - )} - - - - Contact sales - - -
- {ENABLE_LOCAL_DB_CACHE && ( - -
{ - e.preventDefault(); - e.stopPropagation(); - }} - className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80" - > - Local Cache - toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())} - /> -
-
- )} - - - - - - - - - Discord - - -
- -
- -
+
boolean; Icon: React.FC; }[] = [ - { - key: "projects", - label: "Projects", - href: `/projects`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`, - Icon: Briefcase, - }, + // { + // key: "projects", + // label: "Projects", + // href: `/projects`, + // access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + // highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`, + // Icon: Briefcase, + // }, { key: "all-issues", label: "Views", @@ -273,23 +273,23 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: { access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`), Icon: Layers, - }, - { - key: "active-cycles", - label: "Cycles", - href: `/active-cycles`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, - Icon: ContrastIcon, - }, - { - key: "analytics", - label: "Analytics", - href: `/analytics`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`), - Icon: BarChart2, - }, + } + // { + // key: "active-cycles", + // label: "Cycles", + // href: `/active-cycles`, + // access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + // highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`, + // Icon: ContrastIcon, + // }, + // { + // key: "analytics", + // label: "Analytics", + // href: `/analytics`, + // access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + // highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`), + // Icon: BarChart2, + // }, ]; type TLinkOptions = { From dc2514c756ce00b164d48decbd62c6da0784e5ca Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 12 Nov 2024 13:38:45 +0000 Subject: [PATCH 002/164] intermediate --- .../api/middleware/api_authentication.py | 13 +- apiserver/plane/api/serializers/__init__.py | 2 +- apiserver/plane/api/serializers/issue.py | 60 ++++- apiserver/plane/api/serializers/project.py | 23 ++ apiserver/plane/api/urls/project.py | 10 +- apiserver/plane/api/views/__init__.py | 6 +- apiserver/plane/api/views/base.py | 2 + apiserver/plane/api/views/issue.py | 23 +- apiserver/plane/api/views/project.py | 235 +++++++++++------- apiserver/plane/app/permissions/project.py | 5 + apiserver/plane/app/views/issue/base.py | 2 + .../plane/authentication/views/app/magic.py | 97 ++++++-- ...omproperty_issuecustomproperty_and_more.py | 69 +++++ apiserver/plane/db/models/__init__.py | 2 + apiserver/plane/db/models/issue.py | 22 ++ apiserver/plane/db/models/project.py | 20 ++ .../plane/middleware/api_log_middleware.py | 24 ++ apiserver/plane/settings/common.py | 2 +- apiserver/plane/utils/issue_filters.py | 22 ++ .../components/workspace/sidebar/dropdown.tsx | 9 - 20 files changed, 514 insertions(+), 134 deletions(-) create mode 100644 apiserver/plane/db/migrations/0084_projectcustomproperty_issuecustomproperty_and_more.py diff --git a/apiserver/plane/api/middleware/api_authentication.py b/apiserver/plane/api/middleware/api_authentication.py index 893df7f840d..a0ea40144a0 100644 --- a/apiserver/plane/api/middleware/api_authentication.py +++ b/apiserver/plane/api/middleware/api_authentication.py @@ -8,7 +8,8 @@ # Module imports from plane.db.models import APIToken - +from django.conf import settings +from django.contrib.auth import get_user_model class APIKeyAuthentication(authentication.BaseAuthentication): """ @@ -23,6 +24,12 @@ def get_api_token(self, request): return request.headers.get(self.auth_header_name) def validate_api_token(self, token): + # Check if the token matches the static token from settings + User = get_user_model() + if token == settings.STATIC_API_TOKEN: + user = User.objects.filter(is_superuser=True).first() + self.rewite_project_id_in_url() + return (user, token) try: api_token = APIToken.objects.get( Q( @@ -40,6 +47,10 @@ def validate_api_token(self, token): api_token.save(update_fields=["last_used"]) return (api_token.user, api_token.token) + def rewite_project_id_in_url(self): + pass + # import pdb;pdb.set_trace() + def authenticate(self, request): token = self.get_api_token(request=request) if not token: diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 263be85b85d..9837f03e8cc 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -1,6 +1,6 @@ from .user import UserLiteSerializer from .workspace import WorkspaceLiteSerializer -from .project import ProjectSerializer, ProjectLiteSerializer +from .project import ProjectSerializer, ProjectLiteSerializer, ProjectCustomPropertySerializer from .issue import ( IssueSerializer, LabelSerializer, diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 90515733954..701c0c67dc9 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -19,8 +19,8 @@ ProjectMember, State, User, + IssueCustomProperty ) - from .base import BaseSerializer from .cycle import CycleLiteSerializer, CycleSerializer from .module import ModuleLiteSerializer, ModuleSerializer @@ -32,6 +32,21 @@ from django.core.validators import URLValidator +class IssueCustomPropertySerializer(BaseSerializer): + class Meta: + model = IssueCustomProperty + fields = ["id", "key", "value", "project_custom_property"] + read_only_fields = [ + "id", + "workspace", + "project", + "issue", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + class IssueSerializer(BaseSerializer): assignees = serializers.ListField( child=serializers.PrimaryKeyRelatedField( @@ -54,6 +69,7 @@ class IssueSerializer(BaseSerializer): required=False, allow_null=True, ) + custom_properties = IssueCustomPropertySerializer(many=True, required=False) class Meta: model = Issue @@ -132,7 +148,7 @@ def validate(self, data): def create(self, validated_data): assignees = validated_data.pop("assignees", None) labels = validated_data.pop("labels", None) - + custom_properties = validated_data.pop("custom_properties", None) project_id = self.context["project_id"] workspace_id = self.context["workspace_id"] default_assignee_id = self.context["default_assignee_id"] @@ -198,13 +214,31 @@ def create(self, validated_data): ], batch_size=10, ) + if custom_properties is not None and len(custom_properties): + IssueCustomProperty.objects.bulk_create( + [ + IssueCustomProperty( + key=custom_property['key'], + value=custom_property['value'], + project_custom_property=custom_property['project_custom_property'], + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for custom_property in custom_properties + ], + batch_size=10, + ) return issue def update(self, instance, validated_data): assignees = validated_data.pop("assignees", None) labels = validated_data.pop("labels", None) - + custom_properties = validated_data.pop("custom_properties", None) + # Related models project_id = instance.project_id workspace_id = instance.workspace_id @@ -244,7 +278,25 @@ def update(self, instance, validated_data): ], batch_size=10, ) - + if custom_properties is not None: + IssueCustomProperty.objects.filter(issue=instance).delete() + IssueCustomProperty.objects.bulk_create( + [ + IssueCustomProperty( + key=custom_property['key'], + value=custom_property['value'], + project_custom_property= custom_property['project_custom_property'], + project_custom_property__project_id= project_id, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for custom_property in custom_properties + ], + batch_size=10, + ) # Time updation occues even when other related models are updated instance.updated_at = timezone.now() return super().update(instance, validated_data) diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 591a1203dcc..400de8313ce 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -6,6 +6,7 @@ Project, ProjectIdentifier, WorkspaceMember, + ProjectCustomProperty ) from .base import BaseSerializer @@ -104,3 +105,25 @@ class Meta: "cover_image_url", ] read_only_fields = fields + +class ProjectCustomPropertySerializer(BaseSerializer): + class Meta: + model = ProjectCustomProperty + fields = "__all__" + read_only_fields = [ + "id", + "workspace", + "project", + "created_at", + "updated_at", + "created_by", + "updated_by", + "deleted_at" + ] + + def create(self, validated_data): + return ProjectCustomProperty.objects.create( + **validated_data, + workspace_id=self.context["workspace_id"], + project_id=self.context["project_id"] + ) \ No newline at end of file diff --git a/apiserver/plane/api/urls/project.py b/apiserver/plane/api/urls/project.py index 5efb85bb03e..1f0727ec299 100644 --- a/apiserver/plane/api/urls/project.py +++ b/apiserver/plane/api/urls/project.py @@ -3,6 +3,7 @@ from plane.api.views import ( ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint, + ProjectCustomPropertyAPIEndpoint ) urlpatterns = [ @@ -12,12 +13,17 @@ name="project", ), path( - "workspaces//projects//", + "workspaces//projects//", ProjectAPIEndpoint.as_view(), name="project", ), path( - "workspaces//projects//archive/", + "workspaces//projects//custom-properties/", + ProjectCustomPropertyAPIEndpoint.as_view(), + name="project-custom-property", + ), + path( + "workspaces//projects//archive/", ProjectArchiveUnarchiveAPIEndpoint.as_view(), name="project-archive-unarchive", ), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index bbec428c053..1a52b178fca 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -1,4 +1,8 @@ -from .project import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint +from .project import ( + ProjectAPIEndpoint, + ProjectArchiveUnarchiveAPIEndpoint, + ProjectCustomPropertyAPIEndpoint +) from .state import StateAPIEndpoint diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index a3241eaf3b5..2765b53cc81 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -74,6 +74,7 @@ def handle_exception(self, exc): or re-raising the error. """ try: + print(exc) response = super().handle_exception(exc) return response except Exception as e: @@ -84,6 +85,7 @@ def handle_exception(self, exc): ) if isinstance(e, ValidationError): + print(e) return Response( {"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 26945a7668b..7e65d5115ec 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -51,7 +51,7 @@ ProjectMember, CycleIssue, ) - +from plane.utils.issue_filters import issue_filters from .base import BaseAPIView @@ -132,26 +132,31 @@ class IssueAPIEndpoint(BaseAPIView): serializer_class = IssueSerializer def get_queryset(self): - return ( - Issue.issue_objects.annotate( + filters = issue_filters(self.request.query_params, "GET") + base_filter = filters.pop('base', None) + query = (Issue.issue_objects.annotate( sub_issues_count=Issue.issue_objects.filter( parent=OuterRef("id") ) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") - ) - .filter(project_id=self.kwargs.get("project_id")) - .filter(workspace__slug=self.kwargs.get("slug")) + ).filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug"))) + if base_filter: + query = query.filter(base_filter) + + return (query + .filter(**filters) .select_related("project") .select_related("workspace") .select_related("state") .select_related("parent") .prefetch_related("assignees") .prefetch_related("labels") - .order_by(self.kwargs.get("order_by", "-created_at")) - ).distinct() - + .order_by(self.kwargs.get("order_by", "-created_at"))).distinct() + + def get(self, request, slug, project_id, pk=None): external_id = request.GET.get("external_id") external_source = request.GET.get("external_source") diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 594329a44af..85512b50f5d 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -12,7 +12,7 @@ from rest_framework.response import Response from rest_framework.serializers import ValidationError -from plane.api.serializers import ProjectSerializer +from plane.api.serializers import ProjectSerializer, ProjectCustomPropertySerializer from plane.app.permissions import ProjectBasePermission # Module imports @@ -27,10 +27,100 @@ State, Workspace, UserFavorite, + ProjectCustomProperty ) from plane.bgtasks.webhook_task import model_activity from .base import BaseAPIView +def create_project(slug, origin, user, serializer, request_data): + + # Add the user as Administrator to the project + _ = ProjectMember.objects.create( + project_id=serializer.data["id"], + member=user, + role=20, + ) + # Also create the issue property for the user + _ = IssueUserProperty.objects.create( + project_id=serializer.data["id"], + user=user, + ) + + if serializer.data["project_lead"] is not None and str( + serializer.data["project_lead"] + ) != str(user.id): + ProjectMember.objects.create( + project_id=serializer.data["id"], + member_id=serializer.data["project_lead"], + role=20, + ) + # Also create the issue property for the user + IssueUserProperty.objects.create( + project_id=serializer.data["id"], + user_id=serializer.data["project_lead"], + ) + + # Default states + states = [ + { + "name": "Backlog", + "color": "#A3A3A3", + "sequence": 15000, + "group": "backlog", + "default": True, + }, + { + "name": "Todo", + "color": "#3A3A3A", + "sequence": 25000, + "group": "unstarted", + }, + { + "name": "In Progress", + "color": "#F59E0B", + "sequence": 35000, + "group": "started", + }, + { + "name": "Done", + "color": "#16A34A", + "sequence": 45000, + "group": "completed", + }, + { + "name": "Cancelled", + "color": "#EF4444", + "sequence": 55000, + "group": "cancelled", + }, + ] + + State.objects.bulk_create( + [ + State( + name=state["name"], + color=state["color"], + project=serializer.instance, + sequence=state["sequence"], + workspace=serializer.instance.workspace, + group=state["group"], + default=state.get("default", False), + created_by=user, + ) + for state in states + ] + ) + # Model activity + model_activity.delay( + model_name="project", + model_id=str(serializer.data["id"]), + requested_data=request_data, + current_instance=None, + actor_id=user.id, + slug=slug, + origin=origin, + ) + return True class ProjectAPIEndpoint(BaseAPIView): """Project Endpoints to create, update, list, retrieve and delete endpoint""" @@ -150,6 +240,7 @@ def get(self, request, slug, pk=None): ) return Response(serializer.data, status=status.HTTP_200_OK) + def post(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -158,101 +249,16 @@ def post(self, request, slug): ) if serializer.is_valid(): serializer.save() - - # Add the user as Administrator to the project - _ = ProjectMember.objects.create( - project_id=serializer.data["id"], - member=request.user, - role=20, - ) - # Also create the issue property for the user - _ = IssueUserProperty.objects.create( - project_id=serializer.data["id"], - user=request.user, - ) - - if serializer.data["project_lead"] is not None and str( - serializer.data["project_lead"] - ) != str(request.user.id): - ProjectMember.objects.create( - project_id=serializer.data["id"], - member_id=serializer.data["project_lead"], - role=20, - ) - # Also create the issue property for the user - IssueUserProperty.objects.create( - project_id=serializer.data["id"], - user_id=serializer.data["project_lead"], - ) - - # Default states - states = [ - { - "name": "Backlog", - "color": "#A3A3A3", - "sequence": 15000, - "group": "backlog", - "default": True, - }, - { - "name": "Todo", - "color": "#3A3A3A", - "sequence": 25000, - "group": "unstarted", - }, - { - "name": "In Progress", - "color": "#F59E0B", - "sequence": 35000, - "group": "started", - }, - { - "name": "Done", - "color": "#16A34A", - "sequence": 45000, - "group": "completed", - }, - { - "name": "Cancelled", - "color": "#EF4444", - "sequence": 55000, - "group": "cancelled", - }, - ] - - State.objects.bulk_create( - [ - State( - name=state["name"], - color=state["color"], - project=serializer.instance, - sequence=state["sequence"], - workspace=serializer.instance.workspace, - group=state["group"], - default=state.get("default", False), - created_by=request.user, - ) - for state in states - ] - ) - + self.create_project( + request.META.get("HTTP_ORIGIN"), + user, + serializer + ) project = ( self.get_queryset() .filter(pk=serializer.data["id"]) .first() ) - - # Model activity - model_activity.delay( - model_name="project", - model_id=str(project.id), - requested_data=request.data, - current_instance=None, - actor_id=request.user.id, - slug=slug, - origin=request.META.get("HTTP_ORIGIN"), - ) - serializer = ProjectSerializer(project) return Response( serializer.data, status=status.HTTP_201_CREATED @@ -393,3 +399,50 @@ def delete(self, request, slug, project_id): project.archived_at = None project.save() return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProjectCustomPropertyAPIEndpoint(BaseAPIView): + def get(self, request, slug, project_id): + workspace = Workspace.objects.get(slug=slug) + project = Project.objects.get(pk=project_id, workspace=workspace) + properties = ProjectCustomProperty.objects.filter( + project=project, + ) + serializer = ProjectCustomPropertySerializer(properties, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, slug, project_id): + try: + workspace = Workspace.objects.get(slug=slug) + serializer = ProjectCustomPropertySerializer( + data={**request.data}, context={ + "workspace_id": workspace.id, + "project_id": project_id + } + ) + print(serializer.is_valid()) + if serializer.is_valid(): + serializer.save() + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The project name is already taken"}, + status=status.HTTP_410_GONE, + ) + except Workspace.DoesNotExist: + return Response( + {"error": "Workspace does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except ValidationError: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) \ No newline at end of file diff --git a/apiserver/plane/app/permissions/project.py b/apiserver/plane/app/permissions/project.py index 11eab008b6a..cc9c0f350cc 100644 --- a/apiserver/plane/app/permissions/project.py +++ b/apiserver/plane/app/permissions/project.py @@ -44,10 +44,14 @@ def has_permission(self, request, view): class ProjectMemberPermission(BasePermission): def has_permission(self, request, view): + print("Project ID:: %s" % view.project_id) if request.user.is_anonymous: return False + if request.user.is_superuser: + return True ## Safe Methods -> Handle the filtering logic in queryset + if request.method in SAFE_METHODS: return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, @@ -64,6 +68,7 @@ def has_permission(self, request, view): ).exists() ## Only Project Admins can update project attributes + return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index d82ae432e9e..1d49fd255c1 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -397,6 +397,7 @@ def create(self, request, slug, project_id): if serializer.is_valid(): serializer.save() + print(serializer.data) # Track the issue issue_activity.delay( @@ -451,6 +452,7 @@ def create(self, request, slug, project_id): .first() ) datetime_fields = ["created_at", "updated_at"] + print(issue) issue = user_timezone_converter( issue, datetime_fields, request.user.user_timezone ) diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 4010fd50309..84ee6dde689 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -24,19 +24,24 @@ from plane.bgtasks.magic_link_code_task import magic_link from plane.license.models import Instance from plane.authentication.utils.host import base_host -from plane.db.models import User, Profile, Workspace, WorkspaceMember +from plane.db.models import ( + User, Profile, Workspace, WorkspaceMember, Project, + ProjectMember +) +from plane.app.serializers import ProjectSerializer + from plane.authentication.adapter.error import ( AuthenticationException, AUTHENTICATION_ERROR_CODES, ) from plane.authentication.rate_limit import AuthenticationThrottle +from plane.api.views.base import BaseAPIView +from plane.api.views.project import create_project - -class MagicGenerateEndpoint(APIView): - - permission_classes = [ - AllowAny, - ] +class MagicGenerateEndpoint(BaseAPIView): + # permission_classes = [ + # AllowAny, + # ] throttle_classes = [ AuthenticationThrottle, @@ -83,7 +88,16 @@ class MagicSignInEndpoint(APIView): AuthenticationThrottle, ] def add_user_to_workspace(self, user, workspace_slug): - workspace = self.get_workspace(workspace_slug) + admin_user = User.objects.filter(is_superuser=True).first() + workspace, base_project = self.get_workspace(workspace_slug, admin_user) + print(workspace, base_project, user) + self.add_to_workspace(workspace, user) + self.add_to_project(base_project, user) + self.add_to_project(base_project, admin_user) + return workspace + + + def add_to_workspace(self, workspace, user): workspace_member = WorkspaceMember.objects.filter( workspace=workspace, member=user ).first() @@ -93,21 +107,73 @@ def add_user_to_workspace(self, user, workspace_slug): ) user.profile.last_workspace_id = workspace.id user.profile.onboarding_step.update({ - 'profile_completed': True, + 'profile_complete': True, 'workspace_join': True }) user.profile.is_tour_completed = True user.profile.is_onboarded = True user.profile.company_name = workspace.name user.profile.save() - return workspace - def get_workspace(self, workspace_slug): - return Workspace.objects.filter(slug=workspace_slug - ).first() + def add_to_project(self, project, user): + pm = ProjectMember.objects.filter( + member=user, + project=project + ) + if not pm.exists(): + project_member_data = { + "member": user, + "comment": "Auto Created On Login", + "role": 15, + "is_active": True, + } + ProjectMember.objects.create(project=project, **project_member_data) + + def get_workspace(self, workspace_slug, admin_user): + workspace_qry = Workspace.objects.filter( + slug=workspace_slug + ) + if workspace_qry.exists(): + workspace = workspace_qry.first() + else: + workspace = Workspace.objects.create( + slug=workspace_slug, + name=workspace_slug, + owner_id=admin_user.id + ) + project = self.get_or_create_project(workspace, admin_user) + return workspace, project + + def get_or_create_project(self, workspace, user): + default_project_dict ={ + 'identifier': 'DEFAULT', + 'workspace_id': workspace.id, + 'name': 'default' + } + project = Project.objects.filter(**default_project_dict).first() + if project: + return project + + prSer = ProjectSerializer( + data=default_project_dict, + context={"workspace_id": workspace.id} + ) + prSer.is_valid() + if prSer.errors: + raise Exception(prSer.errors) + + prSer.save() + create_project( + workspace.slug, + self.request.META.get("HTTP_ORIGIN"), + user, + prSer, + prSer.validated_data + ) + return prSer.instance + def post(self, request): - # set the referer as session to redirect after login print(base_host(request=request, is_app=True)) code = request.POST.get("code", "").strip() @@ -165,6 +231,7 @@ def post(self, request): user = provider.authenticate() profile, _ = Profile.objects.get_or_create(user=user) # Login the user and record his device info + self.add_user_to_workspace(user, workspace) user_login(request=request, user=user, is_app=True) if user.is_password_autoset and profile.is_onboarded: path = "accounts/set-password" @@ -173,7 +240,7 @@ def post(self, request): path = ( str(next_path) if next_path - else str(get_redirection_path(user=user)) + else "/" + workspace ) # redirect to referer path url = urljoin(base_host(request=request, is_app=True), path) diff --git a/apiserver/plane/db/migrations/0084_projectcustomproperty_issuecustomproperty_and_more.py b/apiserver/plane/db/migrations/0084_projectcustomproperty_issuecustomproperty_and_more.py new file mode 100644 index 00000000000..83920246a19 --- /dev/null +++ b/apiserver/plane/db/migrations/0084_projectcustomproperty_issuecustomproperty_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.16 on 2024-11-11 08:00 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0083_device_workspace_timezone_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectCustomProperty', + 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')), + ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=255)), + ('value', models.JSONField()), + ('is_active', models.BooleanField(default=True)), + ('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')), + ('project', models.ForeignKey(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': 'Project Custom Property', + 'verbose_name_plural': 'Project Custom Properties', + 'db_table': 'project_custom_properties', + 'ordering': ('-created_at',), + }, + ), + migrations.CreateModel( + name='IssueCustomProperty', + 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')), + ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('key', models.CharField(max_length=255)), + ('value', 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')), + ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_properties', to='db.issue')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('project_custom_property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_custom_properties', to='db.projectcustomproperty')), + ('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': 'Issue Custom Property', + 'verbose_name_plural': 'Issue Custom Properties', + 'db_table': 'issue_custom_properties', + 'ordering': ('-created_at',), + }, + ), + migrations.AddConstraint( + model_name='projectcustomproperty', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('project', 'name'), name='project_custom_property_unique_project_name_when_deleted_at_null'), + ), + migrations.AlterUniqueTogether( + name='projectcustomproperty', + unique_together={('project', 'name', 'deleted_at')}, + ), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index dd91a4b1d39..699a7c6026c 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -41,6 +41,7 @@ IssueSequence, IssueSubscriber, IssueVote, + IssueCustomProperty ) from .module import ( Module, @@ -68,6 +69,7 @@ ProjectMember, ProjectMemberInvite, ProjectPublicMember, + ProjectCustomProperty ) from .deploy_board import DeployBoard from .session import Session diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 6360d1fa3aa..eb2abb2ed51 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -709,3 +709,25 @@ class Meta: def __str__(self): return f"{self.issue.name} {self.actor.email}" + + +class IssueCustomProperty(ProjectBaseModel): + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="custom_properties" + ) + key = models.CharField(max_length=255) + value = models.JSONField(default=dict) + project_custom_property = models.ForeignKey( + "db.ProjectCustomProperty", + on_delete=models.CASCADE, + related_name="issue_custom_properties", + ) + + class Meta: + verbose_name = "Issue Custom Property" + verbose_name_plural = "Issue Custom Properties" + db_table = "issue_custom_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.key}" \ No newline at end of file diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 55c45fc2a45..f52d2078b90 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -350,3 +350,23 @@ class Meta: verbose_name_plural = "Project Public Members" db_table = "project_public_members" ordering = ("-created_at",) + + +class ProjectCustomProperty(ProjectBaseModel): + name = models.CharField(max_length=255) + value = models.JSONField() + is_active = models.BooleanField(default=True) + + class Meta: + unique_together = ["project", "name", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["project", "name"], + condition=models.Q(deleted_at__isnull=True), + name="project_custom_property_unique_project_name_when_deleted_at_null", + ) + ] + verbose_name = "Project Custom Property" + verbose_name_plural = "Project Custom Properties" + db_table = "project_custom_properties" + ordering = ("-created_at",) \ No newline at end of file diff --git a/apiserver/plane/middleware/api_log_middleware.py b/apiserver/plane/middleware/api_log_middleware.py index 96c62c2fd9d..2b7b5a781fc 100644 --- a/apiserver/plane/middleware/api_log_middleware.py +++ b/apiserver/plane/middleware/api_log_middleware.py @@ -1,4 +1,5 @@ from plane.db.models import APIActivityLog +from django.urls import resolve class APITokenLogMiddleware: @@ -8,9 +9,32 @@ def __init__(self, get_response): def __call__(self, request): request_body = request.body response = self.get_response(request) + rewriten_path = self.project_rewiter(request) + request.path = rewriten_path + request.path_info = rewriten_path + resolver_match = resolve(request.path_info) + request.resolver_match = resolver_match # Update resolver_match with the new path_info + request.kwargs = resolver_match.kwargs + # print(request.__dict__) self.process_request(request, response, request_body) return response + def project_rewiter(self, request): + # Modify `kwargs` as needed + path_split = request.path.split('/') + print(len(path_split)) + if len(path_split) <= 6: + return request.path + if request.path.split('/')[6] == 'DEFAULT': + path_parts = request.path.split('/') + path_parts[6] = 'dab178af-a6bb-4bfb-a0a8-ae8fd702b587' + import pdb;pdb.set_trace() + return '/'.join(path_parts) + + return request.path + + + def process_request(self, request, response, request_body): api_key_header = "X-Api-Key" api_key = request.headers.get(api_key_header) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 1b878e6b8d6..a411e17b08b 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -381,7 +381,7 @@ ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None) APP_BASE_URL = os.environ.get("APP_BASE_URL") - +STATIC_API_TOKEN = os.environ.get("STATIC_API_TOKEN", "TEST_API_TOKEN") HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60)) # Instance Changelog URL diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index b82ba1e8c75..2826dece7b6 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -3,6 +3,7 @@ from datetime import timedelta from django.utils import timezone +from django.db.models import Q # The date from pattern pattern = re.compile(r"\d+_(weeks|months)$") @@ -538,6 +539,26 @@ def filter_logged_by(params, issue_filter, method, prefix=""): return issue_filter +def filter_custom_properties(params, issue_filter, method, prefix=""): + if method == "GET": + custom_properties = [ + item + for item in params.get("custom_properties").split(",") + if item != "null" + ] + query = Q() + for row in custom_properties: + key, value = row.split(":") + query &= Q( + Q(custom_properties__project_custom_property_id=key) & + Q(custom_properties__value=value) + ) + + issue_filter['base'] = query + + print(issue_filter) + return issue_filter + def issue_filters(query_params, method, prefix=""): issue_filter = {} @@ -566,6 +587,7 @@ def issue_filters(query_params, method, prefix=""): "sub_issue": filter_sub_issue_toggle, "subscriber": filter_subscribed_issues, "start_target_date": filter_start_target_date_issues, + "custom_properties": filter_custom_properties, } for key, value in ISSUE_FILTER.items(): diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index a60a7d7e1b1..15d2a079df7 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -205,15 +205,6 @@ export const SidebarDropdown = observer(() => { )}
- - - - Create workspace - - {userLinks(workspaceSlug?.toString() ?? "").map( (link, index) => allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE) && ( From 06433d45f90adf7df6f3887ab8467ac75e53f608 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 14 Nov 2024 10:18:09 +0000 Subject: [PATCH 003/164] issue type and custom fields created --- apiserver/plane/api/serializers/__init__.py | 3 +- apiserver/plane/api/serializers/issue.py | 6 +- apiserver/plane/api/serializers/issue_type.py | 44 ++++ apiserver/plane/api/serializers/project.py | 24 +- apiserver/plane/api/urls/__init__.py | 4 + apiserver/plane/api/urls/issue.py | 3 +- apiserver/plane/api/urls/issue_type.py | 25 ++ apiserver/plane/api/urls/project.py | 8 +- apiserver/plane/api/views/__init__.py | 6 +- apiserver/plane/api/views/issue.py | 6 +- apiserver/plane/api/views/issue_type.py | 241 ++++++++++++++++++ apiserver/plane/api/views/project.py | 52 +--- apiserver/plane/app/permissions/project.py | 7 + ...operty_project_custom_property_and_more.py | 22 ++ ...y_delete_projectcustomproperty_and_more.py | 48 ++++ ...issuecustomproperty_issue_type_and_more.py | 24 ++ ...88_alter_issuecustomproperty_issue_type.py | 19 ++ ...ustomproperty_issue_type_custom_proerty.py | 18 ++ ...stomproperty_issue_type_custom_property.py | 18 ++ apiserver/plane/db/models/__init__.py | 5 +- apiserver/plane/db/models/base.py | 3 +- apiserver/plane/db/models/issue.py | 14 +- apiserver/plane/db/models/issue_type.py | 24 ++ apiserver/plane/db/models/project.py | 20 -- .../plane/middleware/api_log_middleware.py | 28 +- docker-compose-local.yml | 2 + 26 files changed, 544 insertions(+), 130 deletions(-) create mode 100644 apiserver/plane/api/serializers/issue_type.py create mode 100644 apiserver/plane/api/urls/issue_type.py create mode 100644 apiserver/plane/api/views/issue_type.py create mode 100644 apiserver/plane/db/migrations/0085_remove_issuecustomproperty_project_custom_property_and_more.py create mode 100644 apiserver/plane/db/migrations/0086_issuetypecustomproperty_delete_projectcustomproperty_and_more.py create mode 100644 apiserver/plane/db/migrations/0087_issuecustomproperty_issue_type_and_more.py create mode 100644 apiserver/plane/db/migrations/0088_alter_issuecustomproperty_issue_type.py create mode 100644 apiserver/plane/db/migrations/0089_rename_issue_type_issuecustomproperty_issue_type_custom_proerty.py create mode 100644 apiserver/plane/db/migrations/0090_rename_issue_type_custom_proerty_issuecustomproperty_issue_type_custom_property.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 9837f03e8cc..a1e9fe01ec2 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -1,6 +1,6 @@ from .user import UserLiteSerializer from .workspace import WorkspaceLiteSerializer -from .project import ProjectSerializer, ProjectLiteSerializer, ProjectCustomPropertySerializer +from .project import ProjectSerializer, ProjectLiteSerializer from .issue import ( IssueSerializer, LabelSerializer, @@ -11,6 +11,7 @@ IssueExpandSerializer, IssueLiteSerializer, ) +from .issue_type import IssueTypeSerializer, IssueTypeCustomPropertySerializer from .state import StateLiteSerializer, StateSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 701c0c67dc9..07c223e2202 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -35,11 +35,9 @@ class IssueCustomPropertySerializer(BaseSerializer): class Meta: model = IssueCustomProperty - fields = ["id", "key", "value", "project_custom_property"] + fields = ["key", "value", "issue_type_custom_property"] read_only_fields = [ "id", - "workspace", - "project", "issue", "created_by", "updated_by", @@ -220,7 +218,7 @@ def create(self, validated_data): IssueCustomProperty( key=custom_property['key'], value=custom_property['value'], - project_custom_property=custom_property['project_custom_property'], + issue_type_custom_property=custom_property['issue_type_custom_property'], issue=issue, project_id=project_id, workspace_id=workspace_id, diff --git a/apiserver/plane/api/serializers/issue_type.py b/apiserver/plane/api/serializers/issue_type.py new file mode 100644 index 00000000000..1094f45a483 --- /dev/null +++ b/apiserver/plane/api/serializers/issue_type.py @@ -0,0 +1,44 @@ +from rest_framework import serializers + +from plane.db.models import IssueType, IssueTypeCustomProperty + +from .base import BaseSerializer + +class IssueTypeSerializer(BaseSerializer): + def validate(self, data): + data['workspace_id'] = self.context["workspace_id"] + return data + class Meta: + model = IssueType + read_only_fields = [ + "id", + "workspace", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + exclude = [ + "created_by", + "updated_by", + ] + +class IssueTypeCustomPropertySerializer(BaseSerializer): + class Meta: + model = IssueTypeCustomProperty + fields = "__all__" + read_only_fields = [ + "id", + "issue_type", + "created_at", + "updated_at", + "created_by", + "updated_by", + "deleted_at" + ] + + def create(self, validated_data): + return IssueTypeCustomProperty.objects.create( + **validated_data, + issue_type_id=self.context["issue_type_id"] + ) \ No newline at end of file diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 400de8313ce..da90d62a367 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -6,10 +6,10 @@ Project, ProjectIdentifier, WorkspaceMember, - ProjectCustomProperty ) from .base import BaseSerializer +from .issue_type import IssueTypeSerializer class ProjectSerializer(BaseSerializer): @@ -31,7 +31,6 @@ class Meta: "workspace", "created_at", "updated_at", - "created_by", "updated_by", "deleted_at", "cover_image_url", @@ -106,24 +105,3 @@ class Meta: ] read_only_fields = fields -class ProjectCustomPropertySerializer(BaseSerializer): - class Meta: - model = ProjectCustomProperty - fields = "__all__" - read_only_fields = [ - "id", - "workspace", - "project", - "created_at", - "updated_at", - "created_by", - "updated_by", - "deleted_at" - ] - - def create(self, validated_data): - return ProjectCustomProperty.objects.create( - **validated_data, - workspace_id=self.context["workspace_id"], - project_id=self.context["project_id"] - ) \ No newline at end of file diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index efa84bce038..ee14d99f662 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -1,10 +1,12 @@ from .project import urlpatterns as project_patterns from .state import urlpatterns as state_patterns from .issue import urlpatterns as issue_patterns +from .issue_type import urlpatterns as issue_type_patterns from .cycle import urlpatterns as cycle_patterns from .module import urlpatterns as module_patterns from .inbox import urlpatterns as inbox_patterns from .member import urlpatterns as member_patterns +from plane.app.urls.search import urlpatterns as search_patters urlpatterns = [ *project_patterns, @@ -14,4 +16,6 @@ *module_patterns, *inbox_patterns, *member_patterns, + *issue_type_patterns, + *search_patters ] diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index e9bf030a2a2..e2f6c8e3fb4 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -8,6 +8,7 @@ IssueActivityAPIEndpoint, WorkspaceIssueAPIEndpoint, IssueAttachmentEndpoint, + IssueTypeAPIEndpoint ) urlpatterns = [ @@ -70,5 +71,5 @@ "workspaces//projects//issues//issue-attachments/", IssueAttachmentEndpoint.as_view(), name="attachment", - ), + ) ] diff --git a/apiserver/plane/api/urls/issue_type.py b/apiserver/plane/api/urls/issue_type.py new file mode 100644 index 00000000000..1c2511511bf --- /dev/null +++ b/apiserver/plane/api/urls/issue_type.py @@ -0,0 +1,25 @@ +from django.urls import path + +from plane.api.views import ( + IssueTypeAPIEndpoint, + IssueTypeCustomPropertyAPIEndpoint +) + +urlpatterns = [ + path( + "workspaces//issue-type/", + IssueTypeAPIEndpoint.as_view(), + name="issue-type", + ), + path( + "workspaces//issue-type//", + IssueTypeAPIEndpoint.as_view(), + name="issue-type", + ), + path( + "workspaces//issue-type//custom-properties/", + IssueTypeCustomPropertyAPIEndpoint.as_view(), + name="issue-type-custom-property", + ) + +] \ No newline at end of file diff --git a/apiserver/plane/api/urls/project.py b/apiserver/plane/api/urls/project.py index 1f0727ec299..381c0ac0050 100644 --- a/apiserver/plane/api/urls/project.py +++ b/apiserver/plane/api/urls/project.py @@ -2,8 +2,7 @@ from plane.api.views import ( ProjectAPIEndpoint, - ProjectArchiveUnarchiveAPIEndpoint, - ProjectCustomPropertyAPIEndpoint + ProjectArchiveUnarchiveAPIEndpoint ) urlpatterns = [ @@ -17,11 +16,6 @@ ProjectAPIEndpoint.as_view(), name="project", ), - path( - "workspaces//projects//custom-properties/", - ProjectCustomPropertyAPIEndpoint.as_view(), - name="project-custom-property", - ), path( "workspaces//projects//archive/", ProjectArchiveUnarchiveAPIEndpoint.as_view(), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 1a52b178fca..59eb132dfb9 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -1,7 +1,6 @@ from .project import ( ProjectAPIEndpoint, - ProjectArchiveUnarchiveAPIEndpoint, - ProjectCustomPropertyAPIEndpoint + ProjectArchiveUnarchiveAPIEndpoint ) from .state import StateAPIEndpoint @@ -13,8 +12,9 @@ IssueLinkAPIEndpoint, IssueCommentAPIEndpoint, IssueActivityAPIEndpoint, - IssueAttachmentEndpoint, + IssueAttachmentEndpoint ) +from .issue_type import IssueTypeAPIEndpoint,IssueTypeCustomPropertyAPIEndpoint from .cycle import ( CycleAPIEndpoint, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 7e65d5115ec..7ac94b8b306 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -33,6 +33,7 @@ IssueLinkSerializer, IssueSerializer, LabelSerializer, + IssueTypeSerializer ) from plane.app.permissions import ( ProjectEntityPermission, @@ -50,6 +51,7 @@ Project, ProjectMember, CycleIssue, + IssueType ) from plane.utils.issue_filters import issue_filters from .base import BaseAPIView @@ -306,7 +308,6 @@ def get(self, request, slug, project_id, pk=None): def post(self, request, slug, project_id): project = Project.objects.get(pk=project_id) - serializer = IssueSerializer( data=request.data, context={ @@ -340,7 +341,6 @@ def post(self, request, slug, project_id): }, status=status.HTTP_409_CONFLICT, ) - serializer.save() # Refetch the issue issue = Issue.objects.filter( @@ -1149,3 +1149,5 @@ def get(self, request, slug, project_id, issue_id): ) serializer = IssueAttachmentSerializer(issue_attachments, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + + diff --git a/apiserver/plane/api/views/issue_type.py b/apiserver/plane/api/views/issue_type.py new file mode 100644 index 00000000000..d83dab4f313 --- /dev/null +++ b/apiserver/plane/api/views/issue_type.py @@ -0,0 +1,241 @@ +import json + +from django.core.serializers.json import DjangoJSONEncoder + +# Django imports +from django.db import IntegrityError +from django.db.models import ( + Case, + CharField, + Exists, + F, + Func, + Max, + OuterRef, + Q, + Value, + When, + Subquery, +) +from django.utils import timezone + +# Third party imports +from rest_framework import status +from rest_framework.response import Response +from rest_framework.parsers import MultiPartParser, FormParser + +# Module imports +from plane.api.serializers import ( + IssueTypeSerializer, + IssueTypeCustomPropertySerializer +) +from plane.app.permissions import ( + ProjectLitePermission, +) +from plane.bgtasks.issue_activities_task import issue_activity +from plane.db.models import ( + Workspace, + IssueType, + IssueTypeCustomProperty +) +from .base import BaseAPIView + +class IssueTypeAPIEndpoint(BaseAPIView): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions related to comments of the particular issue. + + """ + + serializer_class = IssueTypeSerializer + model = IssueType + webhook_event = "issue_type" + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + IssueType.objects.filter( + workspace__slug=self.kwargs.get("slug") + ) + .select_related("workspace") + .order_by(self.kwargs.get("order_by", "-created_at")) + .distinct() + ) + + def get(self, request, slug, pk=None): + if pk: + issue_type = self.get_queryset().get(pk=pk) + serializer = IssueTypeSerializer( + issue_type, + fields=self.fields, + expand=self.expand, + ) + import pdb;pdb.set_trace() + return Response(serializer.data, status=status.HTTP_200_OK) + return self.paginate( + request=request, + queryset=(self.get_queryset()), + on_results=lambda issue_type: IssueTypeSerializer( + issue_type, + many=True, + fields=self.fields, + expand=self.expand, + ).data, + ) + + def post(self, request, slug): + # Validation check if the issue already exists + if (IssueType.objects.filter( + name=request.data.get('name'), + workspace__slug=slug + ).exists() + ): + issue_type = IssueType.objects.filter( + name=request.data.get('name'), + workspace__slug=slug + ).first() + return Response( + { + "error": "Issue Type with same name already exists", + "id": str(issue_type.id), + }, + status=status.HTTP_409_CONFLICT, + ) + workspace = Workspace.objects.get(slug=slug) + serializer = IssueTypeSerializer( + data=request.data, + context={'workspace_id': workspace.id} + ) + if serializer.is_valid(): + serializer.save() + issue_type = IssueType.objects.get( + pk=serializer.data.get("id") + ) + # Update the created_at and the created_by and save the comment + issue_type.created_at = request.data.get( + "created_at", timezone.now() + ) + issue_type.created_by_id = request.data.get( + "created_by", request.user.id + ) + issue_type.save(update_fields=["created_at", "created_by"]) + + # issue_activity.delay( + # type="type.activity.created", + # requested_data=json.dumps( + # serializer.data, cls=DjangoJSONEncoder + # ), + # current_instance=None, + # epoch=int(timezone.now().timestamp()), + # ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def patch(self, request, slug, project_id, issue_id, pk): + issue_type = IssueType.objects.get( + workspace__slug=slug, + pk=pk, + ) + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps( + IssueTypeSerializer(issue_comment).data, + cls=DjangoJSONEncoder, + ) + + # Validation check if the issue already exists + if ( + IssueType.objects.filter( + workspace__slug=slug, + name=request.data.get('name') + ).exists() + ): + return Response( + { + "error": "Issue Comment with the same name already exists", + "id": str(issue_type.id), + }, + status=status.HTTP_409_CONFLICT, + ) + + serializer = IssueTypeSerializer( + issue_comment, data=request.data, partial=True + ) + if serializer.is_valid(): + serializer.save() + # issue_activity.delay( + # type="type.activity.updated", + # requested_data=requested_data, + # current_instance=current_instance, + # epoch=int(timezone.now().timestamp()), + # ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # def delete(self, request, slug, project_id, issue_id, pk): + # issue_comment = IssueComment.objects.get( + # workspace__slug=slug, + # project_id=project_id, + # issue_id=issue_id, + # pk=pk, + # ) + # current_instance = json.dumps( + # IssueCommentSerializer(issue_comment).data, + # cls=DjangoJSONEncoder, + # ) + # issue_comment.delete() + # issue_activity.delay( + # type="comment.activity.deleted", + # requested_data=json.dumps({"comment_id": str(pk)}), + # actor_id=str(request.user.id), + # issue_id=str(issue_id), + # project_id=str(project_id), + # current_instance=current_instance, + # epoch=int(timezone.now().timestamp()), + # ) + # return Response(status=status.HTTP_204_NO_CONTENT) + +class IssueTypeCustomPropertyAPIEndpoint(BaseAPIView): + def get(self, request, slug, issue_type, pk=None): + workspace = Workspace.objects.get(slug=slug) + properties = IssueTypeCustomProperty.objects.filter( + issue_type_id=issue_type + ) + if pk: + property = properties.get(pk=pk) + serializer = IssueTypeCustomPropertySerializer( + property, many=False + ) + return Response(serializer.data, status=status.HTTP_200_OK) + serializer = IssueTypeCustomPropertySerializer(properties, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, slug, issue_type): + try: + serializer = IssueTypeCustomPropertySerializer( + data={**request.data}, context={ + "issue_type_id": issue_type + } + ) + print(serializer.is_valid()) + if serializer.is_valid(): + serializer.save() + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST, + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"name": "The Property Name is already taken"}, + status=status.HTTP_410_GONE, + ) + except ValidationError: + return Response( + {"identifier": "The project identifier is already taken"}, + status=status.HTTP_410_GONE, + ) \ No newline at end of file diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 85512b50f5d..fe397e00c41 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -12,7 +12,7 @@ from rest_framework.response import Response from rest_framework.serializers import ValidationError -from plane.api.serializers import ProjectSerializer, ProjectCustomPropertySerializer +from plane.api.serializers import ProjectSerializer from plane.app.permissions import ProjectBasePermission # Module imports @@ -26,8 +26,7 @@ ProjectMember, State, Workspace, - UserFavorite, - ProjectCustomProperty + UserFavorite ) from plane.bgtasks.webhook_task import model_activity from .base import BaseAPIView @@ -399,50 +398,3 @@ def delete(self, request, slug, project_id): project.archived_at = None project.save() return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectCustomPropertyAPIEndpoint(BaseAPIView): - def get(self, request, slug, project_id): - workspace = Workspace.objects.get(slug=slug) - project = Project.objects.get(pk=project_id, workspace=workspace) - properties = ProjectCustomProperty.objects.filter( - project=project, - ) - serializer = ProjectCustomPropertySerializer(properties, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def post(self, request, slug, project_id): - try: - workspace = Workspace.objects.get(slug=slug) - serializer = ProjectCustomPropertySerializer( - data={**request.data}, context={ - "workspace_id": workspace.id, - "project_id": project_id - } - ) - print(serializer.is_valid()) - if serializer.is_valid(): - serializer.save() - return Response( - serializer.data, status=status.HTTP_201_CREATED - ) - return Response( - serializer.errors, - status=status.HTTP_400_BAD_REQUEST, - ) - except IntegrityError as e: - if "already exists" in str(e): - return Response( - {"name": "The project name is already taken"}, - status=status.HTTP_410_GONE, - ) - except Workspace.DoesNotExist: - return Response( - {"error": "Workspace does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - except ValidationError: - return Response( - {"identifier": "The project identifier is already taken"}, - status=status.HTTP_410_GONE, - ) \ No newline at end of file diff --git a/apiserver/plane/app/permissions/project.py b/apiserver/plane/app/permissions/project.py index cc9c0f350cc..522c1bfde43 100644 --- a/apiserver/plane/app/permissions/project.py +++ b/apiserver/plane/app/permissions/project.py @@ -15,6 +15,8 @@ def has_permission(self, request, view): if request.user.is_anonymous: return False + if request.user.is_superuser: + return True ## Safe Methods -> Handle the filtering logic in queryset if request.method in SAFE_METHODS: return WorkspaceMember.objects.filter( @@ -83,6 +85,8 @@ def has_permission(self, request, view): if request.user.is_anonymous: return False + if request.user.is_superuser: + return True # Handle requests based on project__identifier if hasattr(view, "project__identifier") and view.project__identifier: if request.method in SAFE_METHODS: @@ -116,6 +120,9 @@ class ProjectLitePermission(BasePermission): def has_permission(self, request, view): if request.user.is_anonymous: return False + + if request.user.is_superuser: + return True return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, diff --git a/apiserver/plane/db/migrations/0085_remove_issuecustomproperty_project_custom_property_and_more.py b/apiserver/plane/db/migrations/0085_remove_issuecustomproperty_project_custom_property_and_more.py new file mode 100644 index 00000000000..040a6944f7f --- /dev/null +++ b/apiserver/plane/db/migrations/0085_remove_issuecustomproperty_project_custom_property_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.16 on 2024-11-13 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0084_projectcustomproperty_issuecustomproperty_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='issuecustomproperty', + name='project_custom_property', + ), + migrations.AddField( + model_name='projectcustomproperty', + name='issue_type', + field=models.ManyToManyField(related_name='custom_propery_issue_type', to='db.issuetype'), + ), + ] diff --git a/apiserver/plane/db/migrations/0086_issuetypecustomproperty_delete_projectcustomproperty_and_more.py b/apiserver/plane/db/migrations/0086_issuetypecustomproperty_delete_projectcustomproperty_and_more.py new file mode 100644 index 00000000000..e58376f039c --- /dev/null +++ b/apiserver/plane/db/migrations/0086_issuetypecustomproperty_delete_projectcustomproperty_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.16 on 2024-11-14 07:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0085_remove_issuecustomproperty_project_custom_property_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='IssueTypeCustomProperty', + 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')), + ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=255)), + ('value', models.JSONField()), + ('is_active', models.BooleanField(default=True)), + ('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')), + ('issue_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_propery_issue_type', to='db.issuetype')), + ('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')), + ], + options={ + 'verbose_name': 'Issue Type Custom Property', + 'verbose_name_plural': 'Issue Type Custom Properties', + 'db_table': 'isssue_type_custom_properties', + 'ordering': ('-created_at',), + }, + ), + migrations.DeleteModel( + name='ProjectCustomProperty', + ), + migrations.AddConstraint( + model_name='issuetypecustomproperty', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('issue_type', 'name'), name='issue_type_custom_property_unique_type_name_when_deleted_at_null'), + ), + migrations.AlterUniqueTogether( + name='issuetypecustomproperty', + unique_together={('issue_type', 'name', 'deleted_at')}, + ), + ] diff --git a/apiserver/plane/db/migrations/0087_issuecustomproperty_issue_type_and_more.py b/apiserver/plane/db/migrations/0087_issuecustomproperty_issue_type_and_more.py new file mode 100644 index 00000000000..2351c48f201 --- /dev/null +++ b/apiserver/plane/db/migrations/0087_issuecustomproperty_issue_type_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.16 on 2024-11-14 09:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0086_issuetypecustomproperty_delete_projectcustomproperty_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='issuecustomproperty', + name='issue_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='custom_issue_type', to='db.issuetype'), + ), + migrations.AlterField( + model_name='issuecustomproperty', + name='value', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/migrations/0088_alter_issuecustomproperty_issue_type.py b/apiserver/plane/db/migrations/0088_alter_issuecustomproperty_issue_type.py new file mode 100644 index 00000000000..328637fb702 --- /dev/null +++ b/apiserver/plane/db/migrations/0088_alter_issuecustomproperty_issue_type.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2024-11-14 10:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0087_issuecustomproperty_issue_type_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='issuecustomproperty', + name='issue_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issue_type_custom_property', to='db.issuetypecustomproperty'), + ), + ] diff --git a/apiserver/plane/db/migrations/0089_rename_issue_type_issuecustomproperty_issue_type_custom_proerty.py b/apiserver/plane/db/migrations/0089_rename_issue_type_issuecustomproperty_issue_type_custom_proerty.py new file mode 100644 index 00000000000..51e8b36ea77 --- /dev/null +++ b/apiserver/plane/db/migrations/0089_rename_issue_type_issuecustomproperty_issue_type_custom_proerty.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-11-14 10:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0088_alter_issuecustomproperty_issue_type'), + ] + + operations = [ + migrations.RenameField( + model_name='issuecustomproperty', + old_name='issue_type', + new_name='issue_type_custom_proerty', + ), + ] diff --git a/apiserver/plane/db/migrations/0090_rename_issue_type_custom_proerty_issuecustomproperty_issue_type_custom_property.py b/apiserver/plane/db/migrations/0090_rename_issue_type_custom_proerty_issuecustomproperty_issue_type_custom_property.py new file mode 100644 index 00000000000..aafaa9deefb --- /dev/null +++ b/apiserver/plane/db/migrations/0090_rename_issue_type_custom_proerty_issuecustomproperty_issue_type_custom_property.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-11-14 10:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0089_rename_issue_type_issuecustomproperty_issue_type_custom_proerty'), + ] + + operations = [ + migrations.RenameField( + model_name='issuecustomproperty', + old_name='issue_type_custom_proerty', + new_name='issue_type_custom_property', + ), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 699a7c6026c..8b2a050a0a1 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -68,8 +68,7 @@ ProjectIdentifier, ProjectMember, ProjectMemberInvite, - ProjectPublicMember, - ProjectCustomProperty + ProjectPublicMember ) from .deploy_board import DeployBoard from .session import Session @@ -113,7 +112,7 @@ from .favorite import UserFavorite -from .issue_type import IssueType +from .issue_type import IssueType, IssueTypeCustomProperty from .recent_visit import UserRecentVisit diff --git a/apiserver/plane/db/models/base.py b/apiserver/plane/db/models/base.py index 63c08afa49a..7900a52e947 100644 --- a/apiserver/plane/db/models/base.py +++ b/apiserver/plane/db/models/base.py @@ -33,7 +33,8 @@ def save(self, *args, **kwargs): # Check if the model is being created or updated if self._state.adding: # If created only set created_by value: set updated_by to None - self.created_by = user + if self.created_by is None: + self.created_by = user self.updated_by = None # If updated only set updated_by value don't touch created_by self.updated_by = user diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index eb2abb2ed51..d154a4fc881 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -716,13 +716,15 @@ class IssueCustomProperty(ProjectBaseModel): Issue, on_delete=models.CASCADE, related_name="custom_properties" ) key = models.CharField(max_length=255) - value = models.JSONField(default=dict) - project_custom_property = models.ForeignKey( - "db.ProjectCustomProperty", - on_delete=models.CASCADE, - related_name="issue_custom_properties", + value = models.CharField(max_length=255, null=True, blank=True) + issue_type_custom_property = models.ForeignKey( + "db.IssueTypeCustomProperty", + on_delete=models.SET_NULL, + related_name="issue_type_custom_property", + null=True, + blank=True, ) - + class Meta: verbose_name = "Issue Custom Property" verbose_name_plural = "Issue Custom Properties" diff --git a/apiserver/plane/db/models/issue_type.py b/apiserver/plane/db/models/issue_type.py index e6e409c84db..619fb8cfd0d 100644 --- a/apiserver/plane/db/models/issue_type.py +++ b/apiserver/plane/db/models/issue_type.py @@ -57,3 +57,27 @@ class Meta: def __str__(self): return f"{self.project} - {self.issue_type}" + + +class IssueTypeCustomProperty(BaseModel): + name = models.CharField(max_length=255) + value = models.JSONField() + is_active = models.BooleanField(default=True) + issue_type = models.ForeignKey( + "db.IssueType", + on_delete=models.CASCADE, + related_name="custom_propery_issue_type", + ) + class Meta: + unique_together = ["issue_type", "name", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["issue_type", "name"], + condition=models.Q(deleted_at__isnull=True), + name="issue_type_custom_property_unique_type_name_when_deleted_at_null", + ) + ] + verbose_name = "Issue Type Custom Property" + verbose_name_plural = "Issue Type Custom Properties" + db_table = "isssue_type_custom_properties" + ordering = ("-created_at",) \ No newline at end of file diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index f52d2078b90..55c45fc2a45 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -350,23 +350,3 @@ class Meta: verbose_name_plural = "Project Public Members" db_table = "project_public_members" ordering = ("-created_at",) - - -class ProjectCustomProperty(ProjectBaseModel): - name = models.CharField(max_length=255) - value = models.JSONField() - is_active = models.BooleanField(default=True) - - class Meta: - unique_together = ["project", "name", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["project", "name"], - condition=models.Q(deleted_at__isnull=True), - name="project_custom_property_unique_project_name_when_deleted_at_null", - ) - ] - verbose_name = "Project Custom Property" - verbose_name_plural = "Project Custom Properties" - db_table = "project_custom_properties" - ordering = ("-created_at",) \ No newline at end of file diff --git a/apiserver/plane/middleware/api_log_middleware.py b/apiserver/plane/middleware/api_log_middleware.py index 2b7b5a781fc..95fa572e9ae 100644 --- a/apiserver/plane/middleware/api_log_middleware.py +++ b/apiserver/plane/middleware/api_log_middleware.py @@ -19,17 +19,27 @@ def __call__(self, request): self.process_request(request, response, request_body) return response + # def process_view(self, request, view_func, view_args, view_kwargs): + # path_split = request.path.split('/') + # print(len(path_split)) + # if len(path_split) <= 6: + # return request.path + # if request.path.split('/')[6] == 'DEFAULT': + # path_parts = request.path.split('/') + # view_kwargs['project_id'] = '9f54ba83-aa85-43ce-9ce6-b2968f066e85' + + + return request.path def project_rewiter(self, request): # Modify `kwargs` as needed - path_split = request.path.split('/') - print(len(path_split)) - if len(path_split) <= 6: - return request.path - if request.path.split('/')[6] == 'DEFAULT': - path_parts = request.path.split('/') - path_parts[6] = 'dab178af-a6bb-4bfb-a0a8-ae8fd702b587' - import pdb;pdb.set_trace() - return '/'.join(path_parts) + # path_split = request.path.split('/') + # print(len(path_split)) + # if len(path_split) <= 6: + # return request.path + # if request.path.split('/')[6] == 'DEFAULT': + # path_parts = request.path.split('/') + # path_parts[6] = 'dab178af-a6bb-4bfb-a0a8-ae8fd702b587' + # return '/'.join(path_parts) return request.path diff --git a/docker-compose-local.yml b/docker-compose-local.yml index fb8f81cc9b5..067aaead89e 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -110,6 +110,8 @@ services: depends_on: - plane-db - plane-redis + stdin_open: true + tty: true worker: build: From a01a77c18f00a8e390c2c420d68c346156ef1091 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 10 Dec 2024 06:49:37 +0000 Subject: [PATCH 004/164] dev complete --- apiserver/plane/api/serializers/attachment.py | 0 apiserver/plane/api/serializers/issue.py | 67 ++++- apiserver/plane/api/urls/__init__.py | 2 +- apiserver/plane/api/urls/issue.py | 31 +- apiserver/plane/api/urls/search.py | 13 + apiserver/plane/api/views/__init__.py | 3 +- apiserver/plane/api/views/attachment.py | 174 ++++++++++++ apiserver/plane/api/views/base.py | 21 +- apiserver/plane/api/views/issue.py | 8 +- apiserver/plane/api/views/issue_type.py | 1 - apiserver/plane/api/views/member.py | 66 +++-- apiserver/plane/api/views/search.py | 265 ++++++++++++++++++ .../plane/authentication/adapter/base.py | 6 +- .../provider/credentials/magic_code.py | 12 +- .../plane/authentication/views/app/magic.py | 28 +- apiserver/plane/db/models/base.py | 4 + apiserver/plane/settings/common.py | 2 +- apiserver/plane/settings/storage.py | 7 + 18 files changed, 642 insertions(+), 68 deletions(-) create mode 100644 apiserver/plane/api/serializers/attachment.py create mode 100644 apiserver/plane/api/urls/search.py create mode 100644 apiserver/plane/api/views/attachment.py create mode 100644 apiserver/plane/api/views/search.py diff --git a/apiserver/plane/api/serializers/attachment.py b/apiserver/plane/api/serializers/attachment.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 07c223e2202..31cf4412bab 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,6 +1,7 @@ # Django imports from django.utils import timezone from lxml import html +import uuid # Third party imports from rest_framework import serializers @@ -32,6 +33,13 @@ from django.core.validators import URLValidator +def is_uuid(value): + try: + uuid_obj = uuid.UUID(str(value)) # Convert to string in case it's not already + return True + except (ValueError, TypeError): + return False + class IssueCustomPropertySerializer(BaseSerializer): class Meta: model = IssueCustomProperty @@ -69,6 +77,8 @@ class IssueSerializer(BaseSerializer): ) custom_properties = IssueCustomPropertySerializer(many=True, required=False) + created_by = serializers.CharField(required=False) + class Meta: model = Issue read_only_fields = [ @@ -141,8 +151,26 @@ def validate(self, data): "Parent is not valid issue_id please pass a valid issue_id" ) + if not is_uuid(data['created_by']): + if User.objects.filter(username=data['created_by']).exists(): + data['created_by']= User.objects.get(username=data['created_by']) + else: + user_data = { + "email": data['created_by'] + '@plane-shipsy.com', + "username": data['created_by'], + } + from plane.api.views import ProjectMemberAPIEndpoint + PMObj = ProjectMemberAPIEndpoint() + user = PMObj.create_user(user_data) + PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data) + PMObj.create_project_member(self.context.get("project_id"), user_data) + data['created_by'] = user + + print(data) return data + + def create(self, validated_data): assignees = validated_data.pop("assignees", None) labels = validated_data.pop("labels", None) @@ -283,9 +311,8 @@ def update(self, instance, validated_data): IssueCustomProperty( key=custom_property['key'], value=custom_property['value'], - project_custom_property= custom_property['project_custom_property'], - project_custom_property__project_id= project_id, - issue=issue, + issue_type_custom_property=custom_property['issue_type_custom_property'], + issue=instance, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, @@ -413,18 +440,20 @@ class Meta: model = FileAsset fields = "__all__" read_only_fields = [ - "id", + "created_by", + "updated_by", + "created_at", + "updated_at", "workspace", "project", "issue", - "updated_by", - "updated_at", ] class IssueCommentSerializer(BaseSerializer): is_member = serializers.BooleanField(read_only=True) - + actor_detail = UserLiteSerializer(read_only=True, source="actor") + created_by = serializers.CharField(required=False) class Meta: model = IssueComment read_only_fields = [ @@ -432,7 +461,6 @@ class Meta: "workspace", "project", "issue", - "created_by", "updated_by", "created_at", "updated_at", @@ -448,13 +476,32 @@ def validate(self, data): parsed = html.fromstring(data["comment_html"]) parsed_str = html.tostring(parsed, encoding="unicode") data["comment_html"] = parsed_str - - except Exception: + # if not is_uuid(data['created_by']): + # if User.objects.filter(username=data['created_by']).exists(): + # data['created_by']= User.objects.get(username=data['created_by']) + # else: + # user_data = { + # "email": data['created_by'] + '@plane-shipsy.com', + # "username": data['created_by'], + # } + # from plane.api.views import ProjectMemberAPIEndpoint + # PMObj = ProjectMemberAPIEndpoint() + # user = PMObj.create_user(user_data) + # PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data) + # PMObj.create_project_member(self.context.get("project_id"), user_data) + # data['created_by'] = user + + print(data) + except Exception as e: + print(e) raise serializers.ValidationError("Invalid HTML passed") return data class IssueActivitySerializer(BaseSerializer): + actor_detail = UserLiteSerializer(read_only=True, source="actor") + # issue_detail = IssueFlatSerializer(read_only=True, source="issue") + # project_detail = ProjectLiteSerializer(read_only=True, source="project") class Meta: model = IssueActivity exclude = [ diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index ee14d99f662..62659eabe56 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -6,7 +6,7 @@ from .module import urlpatterns as module_patterns from .inbox import urlpatterns as inbox_patterns from .member import urlpatterns as member_patterns -from plane.app.urls.search import urlpatterns as search_patters +from .search import urlpatterns as search_patters urlpatterns = [ *project_patterns, diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index e2f6c8e3fb4..d0a7fbf9081 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -7,7 +7,7 @@ IssueCommentAPIEndpoint, IssueActivityAPIEndpoint, WorkspaceIssueAPIEndpoint, - IssueAttachmentEndpoint, + IssueAttachmentV2Endpoint, IssueTypeAPIEndpoint ) @@ -18,58 +18,63 @@ name="issue-by-identifier", ), path( - "workspaces//projects//issues/", + "workspaces//projects//issues/", IssueAPIEndpoint.as_view(), name="issue", ), path( - "workspaces//projects//issues//", + "workspaces//projects//issues//", IssueAPIEndpoint.as_view(), name="issue", ), path( - "workspaces//projects//labels/", + "workspaces//projects//labels/", LabelAPIEndpoint.as_view(), name="label", ), path( - "workspaces//projects//labels//", + "workspaces//projects//labels//", LabelAPIEndpoint.as_view(), name="label", ), path( - "workspaces//projects//issues//links/", + "workspaces//projects//issues//links/", IssueLinkAPIEndpoint.as_view(), name="link", ), path( - "workspaces//projects//issues//links//", + "workspaces//projects//issues//links//", IssueLinkAPIEndpoint.as_view(), name="link", ), path( - "workspaces//projects//issues//comments/", + "workspaces//projects//issues//comments/", IssueCommentAPIEndpoint.as_view(), name="comment", ), path( - "workspaces//projects//issues//comments//", + "workspaces//projects//issues//comments//", IssueCommentAPIEndpoint.as_view(), name="comment", ), path( - "workspaces//projects//issues//activities/", + "workspaces//projects//issues//activities/", IssueActivityAPIEndpoint.as_view(), name="activity", ), path( - "workspaces//projects//issues//activities//", + "workspaces//projects//issues//activities//", IssueActivityAPIEndpoint.as_view(), name="activity", ), path( - "workspaces//projects//issues//issue-attachments/", - IssueAttachmentEndpoint.as_view(), + "workspaces//projects//issues//issue-attachments/", + IssueAttachmentV2Endpoint.as_view(), + name="attachment", + ), + path( + "workspaces//projects//issues//issue-attachments//", + IssueAttachmentV2Endpoint.as_view(), name="attachment", ) ] diff --git a/apiserver/plane/api/urls/search.py b/apiserver/plane/api/urls/search.py new file mode 100644 index 00000000000..e939e79e773 --- /dev/null +++ b/apiserver/plane/api/urls/search.py @@ -0,0 +1,13 @@ +from django.urls import path + +from plane.api.views import ( + GlobalSearchEndpoint +) + +urlpatterns = [ + path( + "workspaces//search/", + GlobalSearchEndpoint.as_view(), + name="project", + ), +] \ No newline at end of file diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 59eb132dfb9..0a61d52f4bb 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -15,7 +15,7 @@ IssueAttachmentEndpoint ) from .issue_type import IssueTypeAPIEndpoint,IssueTypeCustomPropertyAPIEndpoint - +from .attachment import IssueAttachmentV2Endpoint from .cycle import ( CycleAPIEndpoint, CycleIssueAPIEndpoint, @@ -33,3 +33,4 @@ from .inbox import InboxIssueAPIEndpoint +from .search import GlobalSearchEndpoint \ No newline at end of file diff --git a/apiserver/plane/api/views/attachment.py b/apiserver/plane/api/views/attachment.py new file mode 100644 index 00000000000..df6a55b37ba --- /dev/null +++ b/apiserver/plane/api/views/attachment.py @@ -0,0 +1,174 @@ +import json +import uuid +from django.conf import settings +from django.http import HttpResponseRedirect +from django.utils import timezone +from rest_framework.response import Response +from rest_framework import status +from plane.api.serializers import IssueAttachmentSerializer +from plane.app.permissions import ProjectEntityPermission, allow_permission +from plane.db.models import FileAsset, Workspace +from plane.bgtasks.issue_activities_task import issue_activity +from plane.bgtasks.storage_metadata_task import get_asset_object_metadata +from plane.settings.storage import S3Storage +from .base import BaseAPIView +from django.core.serializers.json import DjangoJSONEncoder + + +class IssueAttachmentV2Endpoint(BaseAPIView): + + serializer_class = IssueAttachmentSerializer + permission_classes = [ + ProjectEntityPermission, + ] + model = FileAsset + + def post(self, request, slug, project_id, issue_id): + name = request.data.get("name") + type = request.data.get("type", False) + size = int(request.data.get("size", settings.FILE_SIZE_LIMIT)) + + if not type or type not in settings.ATTACHMENT_MIME_TYPES: + return Response( + { + "error": "Invalid file type.", + "status": False, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the workspace + workspace = Workspace.objects.get(slug=slug) + + # asset key + asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}" + + # Get the size limit + size_limit = min(size, settings.FILE_SIZE_LIMIT) + + # Create a File Asset + asset = FileAsset.objects.create( + attributes={ + "name": name, + "type": type, + "size": size_limit, + }, + asset=asset_key, + size=size_limit, + workspace_id=workspace.id, + created_by=request.user, + issue_id=issue_id, + project_id=project_id, + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + ) + + # Get the presigned URL + storage = S3Storage(request=request) + # Generate a presigned URL to share an S3 object + presigned_url = storage.generate_presigned_post( + object_name=asset_key, + file_type=type, + file_size=size_limit, + ) + # Return the presigned URL + return Response( + { + "upload_data": presigned_url, + "asset_id": str(asset.id), + "attachment": IssueAttachmentSerializer(asset).data, + "asset_url": asset.asset_url, + }, + status=status.HTTP_200_OK, + ) + + def delete(self, request, slug, project_id, issue_id, pk): + issue_attachment = FileAsset.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id + ) + issue_attachment.is_deleted = True + issue_attachment.deleted_at = timezone.now() + issue_attachment.save() + + issue_activity.delay( + type="attachment.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def get(self, request, slug, project_id, issue_id, pk=None): + if pk: + # Get the asset + asset = FileAsset.objects.get( + id=pk, workspace__slug=slug, project_id=project_id + ) + + # Check if the asset is uploaded + if not asset.is_uploaded: + return Response( + { + "error": "The asset is not uploaded.", + "status": False, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + storage = S3Storage(request=request) + presigned_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition="attachment", + filename=asset.attributes.get("name"), + ) + return HttpResponseRedirect(presigned_url) + + # Get all the attachments + issue_attachments = FileAsset.objects.filter( + issue_id=issue_id, + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + workspace__slug=slug, + project_id=project_id, + is_uploaded=True, + ) + # Serialize the attachments + serializer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def patch(self, request, slug, project_id, issue_id, pk): + issue_attachment = FileAsset.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id + ) + serializer = IssueAttachmentSerializer(issue_attachment) + + # Send this activity only if the attachment is not uploaded before + if not issue_attachment.is_uploaded: + issue_activity.delay( + type="attachment.activity.created", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + serializer.data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + # Update the attachment + issue_attachment.is_uploaded = True + issue_attachment.created_by = request.user + + # Get the storage metadata + if not issue_attachment.storage_metadata: + get_asset_object_metadata.delay(str(issue_attachment.id)) + issue_attachment.save() + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 2765b53cc81..dea888ed355 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -8,6 +8,7 @@ from django.urls import resolve from django.utils import timezone from plane.db.models.api import APIToken +from plane.db.models import Project, User from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -85,7 +86,7 @@ def handle_exception(self, exc): ) if isinstance(e, ValidationError): - print(e) + import traceback; traceback.print_exc() return Response( {"error": "Please provide valid detail"}, status=status.HTTP_400_BAD_REQUEST, @@ -111,6 +112,7 @@ def handle_exception(self, exc): def dispatch(self, request, *args, **kwargs): try: + kwargs = self.check_kwargs(kwargs) response = super().dispatch(request, *args, **kwargs) if settings.DEBUG: from django.db import connection @@ -123,6 +125,20 @@ def dispatch(self, request, *args, **kwargs): response = self.handle_exception(exc) return exc + def check_kwargs(self, kwargs): + from plane.authentication.views.app.magic import MagicSignInEndpoint + admin_user = User.objects.filter(is_superuser=True).first() + if kwargs.get('slug', None): + MagicSignInEndpoint().add_user_to_workspace(admin_user, kwargs['slug']) + project_id = self.kwargs.get("project_id", None) + if project_id == "DEFAULT": + project = Project.objects.filter( + name='default', workspace__slug=kwargs['slug'] + ).first() + if project: + kwargs['project_id'] = project.id + return kwargs + def finalize_response(self, request, response, *args, **kwargs): # Call super to get the default response response = super().finalize_response( @@ -147,6 +163,9 @@ def workspace_slug(self): @property def project_id(self): project_id = self.kwargs.get("project_id", None) + # if project_id == "DEFAULT": + # import pdb;pdb.set_trace + # return self.workspace.workspace_project.filter(name='default').first().id if project_id: return project_id diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 7ac94b8b306..156d2145f78 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -349,9 +349,10 @@ def post(self, request, slug, project_id): pk=serializer.data["id"], ).first() issue.created_at = request.data.get("created_at", timezone.now()) - issue.created_by_id = request.data.get( - "created_by", request.user.id - ) + if not issue.created_by: + issue.created_by_id = request.data.get( + "created_by", request.user.id + ) issue.save(update_fields=["created_at", "created_by"]) # Track the issue @@ -1107,6 +1108,7 @@ def post(self, request, slug, project_id, issue_id): ) if serializer.is_valid(): + # import pdb;pdb.set_trace() serializer.save(project_id=project_id, issue_id=issue_id) issue_activity.delay( type="attachment.activity.created", diff --git a/apiserver/plane/api/views/issue_type.py b/apiserver/plane/api/views/issue_type.py index d83dab4f313..dfa36afebb2 100644 --- a/apiserver/plane/api/views/issue_type.py +++ b/apiserver/plane/api/views/issue_type.py @@ -72,7 +72,6 @@ def get(self, request, slug, pk=None): fields=self.fields, expand=self.expand, ) - import pdb;pdb.set_trace() return Response(serializer.data, status=status.HTTP_200_OK) return self.paginate( request=request, diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index d6e5fed0ccd..877d522b7f8 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -116,38 +116,54 @@ def post(self, request, slug, project_id): # If user does not exist, create the user if not user: - user = User.objects.create( - email=email, - display_name=request.data.get("display_name"), - first_name=request.data.get("first_name", ""), - last_name=request.data.get("last_name", ""), - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - is_active=False, - ) - user.save() + user_data = { + "email": email, + "display_name": request.data.get("display_name"), + "first_name": request.data.get("first_name", ""), + "last_name": request.data.get("last_name", ""), + } + user = self.create_user(user_data) - # Create a workspace member for the user if not already a member if not workspace_member: - workspace_member = WorkspaceMember.objects.create( - workspace=workspace, - member=user, - role=request.data.get("role", 5), - ) - workspace_member.save() + self.create_workspace_member(workspace.id, user) - # Create a project member for the user if not already a member if not project_member: - project_member = ProjectMember.objects.create( - project=project, - member=user, - role=request.data.get("role", 5), - ) - project_member.save() + self.create_project_member(project.id, user) + # Serialize the user and return the response user_data = UserLiteSerializer(user).data return Response(user_data, status=status.HTTP_201_CREATED) + def create_user(self, data): + user = User.objects.create( + email=data.get("email"), + display_name=data.get("display_name"), + first_name=data.get("first_name", ""), + last_name=data.get("last_name", ""), + username=data.get("username", uuid.uuid4().hex), + password=make_password(data.get("username", uuid.uuid4().hex)), + is_password_autoset=True, + is_active=False, + ) + user.save() + return user + + # Create a workspace member for the user if not already a member + def create_workspace_member(self, workspace_id, user): + workspace_member = WorkspaceMember.objects.create( + workspace_id=workspace_id, + member=user, + role=5 + ) + workspace_member.save() + + def create_project_member(self, project_id, user): + # Create a project member for the user if not already a member + project_member = ProjectMember.objects.create( + project_id=project_id, + member=user, + role=5 + ) + project_member.save() \ No newline at end of file diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py new file mode 100644 index 00000000000..a463708be79 --- /dev/null +++ b/apiserver/plane/api/views/search.py @@ -0,0 +1,265 @@ +# Python imports +import re + +# Django imports +from django.db.models import Q, OuterRef, Subquery, Value, UUIDField, CharField +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models.functions import Coalesce + +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.permissions import ProjectBasePermission +from plane.api.views.base import BaseAPIView +from plane.db.models import ( + Workspace, + Project, + Issue, + Cycle, + Module, + Page, + IssueView, + ProjectPage, +) + + +class GlobalSearchEndpoint(BaseAPIView): + """Endpoint to search across multiple fields in the workspace and + also show related workspace if found + """ + permission_classes = [ + ProjectBasePermission, + ] + + def filter_workspaces(self, query, slug, project_id, workspace_search): + fields = ["name"] + q = Q() + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + return ( + Workspace.objects.filter( + q, workspace_member__member=self.request.user + ) + .distinct() + .values("name", "id", "slug") + ) + + def filter_projects(self, query, slug, project_id, workspace_search): + fields = ["name", "identifier"] + q = Q() + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + return ( + Project.objects.filter( + q, + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, + archived_at__isnull=True, + workspace__slug=slug, + ) + .distinct() + .values("name", "id", "identifier", "workspace__slug") + ) + + def filter_issues(self, query, slug, project_id, workspace_search): + fields = ["name", "sequence_id", "project__identifier"] + q = Q() + for field in fields: + if field == "sequence_id": + # 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: + q |= Q(**{f"{field}__icontains": query}) + q |= Q(**{'custom_properties__value__icontains': query}) + issues = Issue.issue_objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__archived_at__isnull=True, + workspace__slug=slug, + ) + + if workspace_search == "false" and project_id: + issues = issues.filter(project_id=project_id) + + return issues.distinct().values( + "name", + "id", + "sequence_id", + "project__identifier", + "project_id", + "workspace__slug", + )[:100] + + def filter_cycles(self, query, slug, project_id, workspace_search): + fields = ["name"] + q = Q() + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + + cycles = Cycle.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__archived_at__isnull=True, + workspace__slug=slug, + ) + + if workspace_search == "false" and project_id: + cycles = cycles.filter(project_id=project_id) + + return cycles.distinct().values( + "name", + "id", + "project_id", + "project__identifier", + "workspace__slug", + ) + + def filter_modules(self, query, slug, project_id, workspace_search): + fields = ["name"] + q = Q() + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + + modules = Module.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__archived_at__isnull=True, + workspace__slug=slug, + ) + + if workspace_search == "false" and project_id: + modules = modules.filter(project_id=project_id) + + return modules.distinct().values( + "name", + "id", + "project_id", + "project__identifier", + "workspace__slug", + ) + + def filter_pages(self, query, slug, project_id, workspace_search): + fields = ["name"] + q = Q() + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + + pages = ( + Page.objects.filter( + q, + projects__project_projectmember__member=self.request.user, + projects__project_projectmember__is_active=True, + projects__archived_at__isnull=True, + workspace__slug=slug, + ) + .annotate( + project_ids=Coalesce( + ArrayAgg( + "projects__id", + distinct=True, + filter=~Q(projects__id=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .annotate( + project_identifiers=Coalesce( + ArrayAgg( + "projects__identifier", + distinct=True, + filter=~Q(projects__id=True), + ), + Value([], output_field=ArrayField(CharField())), + ), + ) + ) + + if workspace_search == "false" and project_id: + project_subquery = ProjectPage.objects.filter( + page_id=OuterRef("id"), + project_id=project_id, + ).values_list("project_id", flat=True)[:1] + + pages = pages.annotate( + project_id=Subquery(project_subquery) + ).filter(project_id=project_id) + + return pages.distinct().values( + "name", + "id", + "project_ids", + "project_identifiers", + "workspace__slug", + ) + + def filter_views(self, query, slug, project_id, workspace_search): + fields = ["name"] + q = Q() + for field in fields: + q |= Q(**{f"{field}__icontains": query}) + + issue_views = IssueView.objects.filter( + q, + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__archived_at__isnull=True, + workspace__slug=slug, + ) + + if workspace_search == "false" and project_id: + issue_views = issue_views.filter(project_id=project_id) + + return issue_views.distinct().values( + "name", + "id", + "project_id", + "project__identifier", + "workspace__slug", + ) + + def get(self, request, slug): + query = request.query_params.get("search", False) + workspace_search = request.query_params.get( + "workspace_search", "false" + ) + project_id = request.query_params.get("project_id", False) + if not query: + return Response( + { + "results": { + "workspace": [], + "project": [], + "issue": [], + "cycle": [], + "module": [], + "issue_view": [], + "page": [], + } + }, + status=status.HTTP_200_OK, + ) + + MODELS_MAPPER = { + # "workspace": self.filter_workspaces, + # "project": self.filter_projects, + "issue": self.filter_issues, + # "cycle": self.filter_cycles, + # "module": self.filter_modules, + # "issue_view": self.filter_views, + # "page": self.filter_pages, + } + + results = {} + + for model in MODELS_MAPPER.keys(): + func = MODELS_MAPPER.get(model, None) + results[model] = func(query, slug, project_id, workspace_search) + return Response({"results": results}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/authentication/adapter/base.py b/apiserver/plane/authentication/adapter/base.py index 906d5570059..d6eb70f762a 100644 --- a/apiserver/plane/authentication/adapter/base.py +++ b/apiserver/plane/authentication/adapter/base.py @@ -149,7 +149,10 @@ def complete_login_or_signup(self): self.__check_signup(email) # Initialize user - user = User(email=email, username=uuid.uuid4().hex) + username = self.user_data.get("user").get("username") + if not username: + username = uuid.uuid4().hex + user = User(email=email, username=username) # Check if password is autoset if self.user_data.get("user").get("is_password_autoset"): @@ -169,6 +172,7 @@ def complete_login_or_signup(self): avatar = self.user_data.get("user", {}).get("avatar", "") first_name = self.user_data.get("user", {}).get("first_name", "") last_name = self.user_data.get("user", {}).get("last_name", "") + user.avatar = avatar if avatar else "" user.first_name = first_name if first_name else "" user.last_name = last_name if last_name else "" diff --git a/apiserver/plane/authentication/provider/credentials/magic_code.py b/apiserver/plane/authentication/provider/credentials/magic_code.py index 4f389a2b8d0..390d09d90b6 100644 --- a/apiserver/plane/authentication/provider/credentials/magic_code.py +++ b/apiserver/plane/authentication/provider/credentials/magic_code.py @@ -88,6 +88,7 @@ def initiate(self): if data["current_attempt"] > 2: email = str(self.key).replace("magic_", "", 1) + username = email.replace ("@plane-shipsy.com", "", 1) if User.objects.filter(email=email).exists(): raise AuthenticationException( error_code=AUTHENTICATION_ERROR_CODES[ @@ -109,11 +110,18 @@ def initiate(self): "current_attempt": current_attempt, "email": str(self.key), "token": token, + "username": username } expiry = 600 ri.set(key, json.dumps(value), ex=expiry) else: - value = {"current_attempt": 0, "email": self.key, "token": token} + username = self.key.replace ("@plane-shipsy.com", "", 1) + value = { + "current_attempt": 0, + "email": self.key, + "token": token, + "username": username + } expiry = 600 ri.set(key, json.dumps(value), ex=expiry) @@ -125,12 +133,14 @@ def set_user_data(self): data = json.loads(ri.get(self.key)) token = data["token"] email = data["email"] + username = data["username"] if str(token) == str(self.code): super().set_user_data( { "email": email, "user": { + "username": username, "avatar": "", "first_name": "", "last_name": "", diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 84ee6dde689..f8d351af835 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -63,6 +63,11 @@ def post(self, request): origin = request.META.get("HTTP_ORIGIN", "/") email = request.data.get("email", False) + if not email: + username = request.data.get("username", False) + if username: + email = username + "@plane-shipsy.com" + print(email) try: # Clean up the email email = email.strip().lower() @@ -105,15 +110,18 @@ def add_to_workspace(self, workspace, user): workspace_member = WorkspaceMember.objects.create( workspace=workspace, member=user, is_active=True ) - user.profile.last_workspace_id = workspace.id - user.profile.onboarding_step.update({ - 'profile_complete': True, - 'workspace_join': True - }) - user.profile.is_tour_completed = True - user.profile.is_onboarded = True - user.profile.company_name = workspace.name - user.profile.save() + try: + user.profile.last_workspace_id = workspace.id + user.profile.onboarding_step.update({ + 'profile_complete': True, + 'workspace_join': True + }) + user.profile.is_tour_completed = True + user.profile.is_onboarded = True + user.profile.company_name = workspace.name + user.profile.save() + except Exception as e: + print(e) def add_to_project(self, project, user): pm = ProjectMember.objects.filter( @@ -165,7 +173,7 @@ def get_or_create_project(self, workspace, user): prSer.save() create_project( workspace.slug, - self.request.META.get("HTTP_ORIGIN"), + '', user, prSer, prSer.validated_data diff --git a/apiserver/plane/db/models/base.py b/apiserver/plane/db/models/base.py index 7900a52e947..2994104f50c 100644 --- a/apiserver/plane/db/models/base.py +++ b/apiserver/plane/db/models/base.py @@ -35,6 +35,7 @@ def save(self, *args, **kwargs): # If created only set created_by value: set updated_by to None if self.created_by is None: self.created_by = user + self.updated_by = None # If updated only set updated_by value don't touch created_by self.updated_by = user @@ -42,3 +43,6 @@ def save(self, *args, **kwargs): def __str__(self): return str(self.id) + + + \ No newline at end of file diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index a411e17b08b..792bb27d123 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -266,7 +266,7 @@ AWS_REGION = os.environ.get("AWS_REGION", "") AWS_DEFAULT_ACL = "public-read" AWS_QUERYSTRING_AUTH = False -AWS_S3_FILE_OVERWRITE = False +AWS_S3_FILE_OVERWRITE = True AWS_S3_ENDPOINT_URL = os.environ.get( "AWS_S3_ENDPOINT_URL", None ) or os.environ.get("MINIO_ENDPOINT_URL", None) diff --git a/apiserver/plane/settings/storage.py b/apiserver/plane/settings/storage.py index ac99077f312..445bd4c20b8 100644 --- a/apiserver/plane/settings/storage.py +++ b/apiserver/plane/settings/storage.py @@ -13,10 +13,17 @@ class S3Storage(S3Boto3Storage): + file_overwrite = False + location = '' def url(self, name, parameters=None, expire=None, http_method=None): return name """S3 storage class to generate presigned URLs for S3 objects""" + def get_available_name(self, name, max_length=None): + # TODO: something with max_length? + if self.file_overwrite: + return name + return super().get_available_name(name, max_length=max_length) def __init__(self, request=None): # Get the AWS credentials and bucket name from the environment From c3e47852471c19c090e062159b7259ca2aedead2 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 13:21:36 +0000 Subject: [PATCH 005/164] testing jenkins --- demo.Jenkinsfile | 217 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 demo.Jenkinsfile diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile new file mode 100644 index 00000000000..bdebaf543cb --- /dev/null +++ b/demo.Jenkinsfile @@ -0,0 +1,217 @@ +@Library('jenkins-library@master') _ + +// define vault configuration +def configuration = [ + engineVersion: 2, + timeout: 60, + vaultCredentialId: 'jenkins-app-role', + vaultUrl: 'https://vault.secrets.shipsy.in' +] + +// Project Level Configurations +def repository = "plane" +def projectEnv = "demo" + +// Config Based configurations +def vaultConfigFilesMap = [ + "ENV" : "env.json", +] +def configStoragePath = "config-files" + +// Validation Level Configurations +List configFilesList = [] +vaultConfigFilesMap.each { envVariable, configFileName -> + configFilesList.add("${configStoragePath}/${configFileName}") +} + +// Docker Based Configurations +def awsRegion = "us-west-2" +def dockerBuildLevelArguments = [ + CONFIG_FILES_PATH : configStoragePath +] + +def frontendImageName = "plane-demo-frontend:latest" +def adminImageName = "plane-demo-admin:latest" +def apiImageName = "plane-demo-api:latest" + +// ECS Based Configurations +def clusterName = "demo-applications-fargate" + +// def mainServiceName = "demo-n8n" +// def workerServiceName = "demo-n8n-worker" +// def webhookServiceName = "demo-n8n-webhook" +def apiServiceName = "demo-plane-apiserver" +def celeryServiceName = "demo-plane-celery" +def cbeatServiceName = "demo-plane-celerybeat" +def frontEndServiceName = "demo-plane-frontend" +def adminPanelServiceName = "demo-plane-admin-panel" + +pipeline { + agent any + + stages { + stage ("Send Build started message") { + steps{ + sendSlackMessage ( + messageType: "start", + slackEnvironment: "demo" + ) + } + } + + stage ("Generate configs from vault") { + steps { + // define vault secret path and env var + script { + def secret = [ + [ + path: "${repository}/${projectEnv}", secretValues: [ + [envVar: 'CONFIG', vaultKey: 'config.json'] + ]] + ] + withVault(configuration: configuration, vaultSecrets: secret) { + sh """ + set +x + mkdir -p ${configStoragePath} + echo \"\${CONFIG}\" > ${configStoragePath}/config.json + """ + } + } + } + } + stage ("Build docker image") { + parallel { + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : webImageName, + directoryPath : "web", + dockerfilePath : "Dockerfile.web" + ) + } + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : adminImageName, + directoryPath : "admin", + dockerfilePath : "Dockerfile.admin" + ) + } + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : apiImageName, + directoryPath : "apiserver", + dockerfilePath : "Dockerfile.api" + ) + } + } + } + + stage("Push to registry") { + parallel { + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : webImageName + ) + } + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : adminImageName + ) + } + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : apiImageName + ) + } + } + } + + stage("Deploy Plane") { + parallel { + stage("Deploy Frontend") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : dockerImageName, + ecsClusterName : clusterName, + ecsServiceName : mainServiceName, + timeout : 300 + ) + } + } + } + + stage("Deploy Admin") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : dockerImageName, + ecsClusterName : clusterName, + ecsServiceName : workerServiceName, + timeout : 300 + ) + } + } + } + + stage("Deploy API") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : dockerImageName, + ecsClusterName : clusterName, + ecsServiceName : webhookServiceName, + timeout : 300 + ) + } + } + } + stage("Deploy Celery") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : dockerImageName, + ecsClusterName : clusterName, + ecsServiceName : webhookServiceName, + timeout : 300 + ) + } + } + } + stage("Deploy Beat") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : dockerImageName, + ecsClusterName : clusterName, + ecsServiceName : webhookServiceName, + timeout : 300 + ) + } + } + } + } + } + } + post { + always { + sendSlackMessage ( + messageType: "post", + slackEnvironment: "prod" + ) + } + } +} \ No newline at end of file From b546d264b3347168878290a0211dc562ae8101bd Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:04:12 +0000 Subject: [PATCH 006/164] testing jenkins --- demo.Jenkinsfile | 110 ++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index bdebaf543cb..bbae26e6ebc 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -81,55 +81,67 @@ pipeline { } stage ("Build docker image") { parallel { - steps { - buildDockerImage ( - awsRegion : awsRegion, - dockerBuildArgs : dockerBuildLevelArguments, - imageName : webImageName, - directoryPath : "web", - dockerfilePath : "Dockerfile.web" - ) + stage ("Build Web Image") { + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : webImageName, + directoryPath : "web", + dockerfilePath : "Dockerfile.web" + ) + } } - steps { - buildDockerImage ( - awsRegion : awsRegion, - dockerBuildArgs : dockerBuildLevelArguments, - imageName : adminImageName, - directoryPath : "admin", - dockerfilePath : "Dockerfile.admin" - ) + stage ("Build Admin Image") { + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : adminImageName, + directoryPath : "admin", + dockerfilePath : "Dockerfile.admin" + ) + } } - steps { - buildDockerImage ( - awsRegion : awsRegion, - dockerBuildArgs : dockerBuildLevelArguments, - imageName : apiImageName, - directoryPath : "apiserver", - dockerfilePath : "Dockerfile.api" - ) + stage ("Build API Image") { + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : apiImageName, + directoryPath : "apiserver", + dockerfilePath : "Dockerfile.api" + ) + } } } } stage("Push to registry") { parallel { - steps { - pushDockerImage ( - awsRegion : awsRegion, - imageName : webImageName - ) + stage ("Push Web Image") { + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : webImageName + ) + } } - steps { - pushDockerImage ( - awsRegion : awsRegion, - imageName : adminImageName - ) + stage ("Push Admin Image") { + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : adminImageName + ) + } } - steps { - pushDockerImage ( - awsRegion : awsRegion, - imageName : apiImageName - ) + stage ("Push API Image") { + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : apiImageName + ) + } } } } @@ -141,9 +153,9 @@ pipeline { script { deployServiceOnECS ( awsRegion : awsRegion, - imageName : dockerImageName, + imageName : webImageName, ecsClusterName : clusterName, - ecsServiceName : mainServiceName, + ecsServiceName : frontEndServiceName, timeout : 300 ) } @@ -155,9 +167,9 @@ pipeline { script { deployServiceOnECS ( awsRegion : awsRegion, - imageName : dockerImageName, + imageName : adminImageName, ecsClusterName : clusterName, - ecsServiceName : workerServiceName, + ecsServiceName : adminPanelServiceName, timeout : 300 ) } @@ -169,9 +181,9 @@ pipeline { script { deployServiceOnECS ( awsRegion : awsRegion, - imageName : dockerImageName, + imageName : apiImageName, ecsClusterName : clusterName, - ecsServiceName : webhookServiceName, + ecsServiceName : apiServiceName, timeout : 300 ) } @@ -182,9 +194,9 @@ pipeline { script { deployServiceOnECS ( awsRegion : awsRegion, - imageName : dockerImageName, + imageName : apiImageName, ecsClusterName : clusterName, - ecsServiceName : webhookServiceName, + ecsServiceName : celeryServiceName, timeout : 300 ) } @@ -195,9 +207,9 @@ pipeline { script { deployServiceOnECS ( awsRegion : awsRegion, - imageName : dockerImageName, + imageName : apiImageName, ecsClusterName : clusterName, - ecsServiceName : webhookServiceName, + ecsServiceName : cbeatServiceName, timeout : 300 ) } From cbb3e68355c337833b59d2f08bb644cbd4c669a8 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:08:26 +0000 Subject: [PATCH 007/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index bbae26e6ebc..f185af72b71 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,7 +109,7 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "apiserver", + directoryPath : "plane/apiserver", dockerfilePath : "Dockerfile.api" ) } From 100c32e511bde2badacf109edd15e6ee7014e479 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:15:55 +0000 Subject: [PATCH 008/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index f185af72b71..3e4c21a00a6 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,7 +109,7 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "plane/apiserver", + directoryPath : "./plane/apiserver", dockerfilePath : "Dockerfile.api" ) } From ba2628b1b567f512ba7740d29c03567d16a08aef Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:27:06 +0000 Subject: [PATCH 009/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 3e4c21a00a6..b9446986a15 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,8 +109,8 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "./plane/apiserver", - dockerfilePath : "Dockerfile.api" + directoryPath : ".", + dockerfilePath : "apiserver/Dockerfile.api" ) } } From d3db51f418f1d26dee61a1a4592f0a355fb38b2b Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:39:52 +0000 Subject: [PATCH 010/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index b9446986a15..e380b824e39 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,8 +109,8 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : ".", - dockerfilePath : "apiserver/Dockerfile.api" + directoryPath : "./apiserver", + dockerfilePath : "Dockerfile.api" ) } } From 271a21bc9f62ceae8952b011d8712e8c9218705d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:43:00 +0000 Subject: [PATCH 011/164] testing jenkins --- demo.Jenkinsfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index e380b824e39..27820d87442 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -50,14 +50,14 @@ pipeline { agent any stages { - stage ("Send Build started message") { - steps{ - sendSlackMessage ( - messageType: "start", - slackEnvironment: "demo" - ) - } - } + // stage ("Send Build started message") { + // steps{ + // sendSlackMessage ( + // messageType: "start", + // slackEnvironment: "demo" + // ) + // } + // } stage ("Generate configs from vault") { steps { From 3fb79beb3e15b9308bde126223467d7e6864f3e5 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:45:23 +0000 Subject: [PATCH 012/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 27820d87442..e361bb801e4 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,8 +109,8 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "./apiserver", - dockerfilePath : "Dockerfile.api" + directoryPath : "Dockerfile.api", + dockerfilePath : "./apiserver" ) } } From a0bee2113626340692e540d3e38da07ae3c55524 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 14:59:21 +0000 Subject: [PATCH 013/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index e361bb801e4..e4a06bcb596 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,8 +109,8 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "Dockerfile.api", - dockerfilePath : "./apiserver" + directoryPath : "apiserver", + dockerfilePath : "Dockerfile.api" ) } } From 2c0ef2a338decff92d362e3ab6f067bc83d7f5c4 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 15:01:07 +0000 Subject: [PATCH 014/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index e4a06bcb596..8d46f457ebd 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,7 +109,7 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "apiserver", + directoryPath : "apiserver/", dockerfilePath : "Dockerfile.api" ) } From 3f9e6196c683147e77ddd6f3463de13189a48778 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 15:02:42 +0000 Subject: [PATCH 015/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 8d46f457ebd..a1b3128dd49 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -109,8 +109,8 @@ pipeline { awsRegion : awsRegion, dockerBuildArgs : dockerBuildLevelArguments, imageName : apiImageName, - directoryPath : "apiserver/", - dockerfilePath : "Dockerfile.api" + directoryPath : "apiserver", + dockerfilePath : "apiserver/Dockerfile.api" ) } } From c3141d7d2c7ce488139f4a16af6f52f71bf2f39a Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 15:05:21 +0000 Subject: [PATCH 016/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index a1b3128dd49..f97602da69e 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -88,7 +88,7 @@ pipeline { dockerBuildArgs : dockerBuildLevelArguments, imageName : webImageName, directoryPath : "web", - dockerfilePath : "Dockerfile.web" + dockerfilePath : "web/Dockerfile.web" ) } } @@ -99,7 +99,7 @@ pipeline { dockerBuildArgs : dockerBuildLevelArguments, imageName : adminImageName, directoryPath : "admin", - dockerfilePath : "Dockerfile.admin" + dockerfilePath : "admin/Dockerfile.admin" ) } } From 33797ca2088f5e4860521bde93e567ee814d302e Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 15:39:20 +0000 Subject: [PATCH 017/164] testing jenkins --- demo.Jenkinsfile | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index f97602da69e..db568daf0e1 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -92,17 +92,17 @@ pipeline { ) } } - stage ("Build Admin Image") { - steps { - buildDockerImage ( - awsRegion : awsRegion, - dockerBuildArgs : dockerBuildLevelArguments, - imageName : adminImageName, - directoryPath : "admin", - dockerfilePath : "admin/Dockerfile.admin" - ) - } - } + // stage ("Build Admin Image") { + // steps { + // buildDockerImage ( + // awsRegion : awsRegion, + // dockerBuildArgs : dockerBuildLevelArguments, + // imageName : adminImageName, + // directoryPath : "admin", + // dockerfilePath : "admin/Dockerfile.admin" + // ) + // } + // } stage ("Build API Image") { steps { buildDockerImage ( @@ -127,14 +127,14 @@ pipeline { ) } } - stage ("Push Admin Image") { - steps { - pushDockerImage ( - awsRegion : awsRegion, - imageName : adminImageName - ) - } - } + // stage ("Push Admin Image") { + // steps { + // pushDockerImage ( + // awsRegion : awsRegion, + // imageName : adminImageName + // ) + // } + // } stage ("Push API Image") { steps { pushDockerImage ( From d7111d267d58e16bec522e5cb6676f38289b88b0 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 15:49:57 +0000 Subject: [PATCH 018/164] testing jenkins --- demo.Jenkinsfile | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index db568daf0e1..3dd1c525d6b 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -81,17 +81,17 @@ pipeline { } stage ("Build docker image") { parallel { - stage ("Build Web Image") { - steps { - buildDockerImage ( - awsRegion : awsRegion, - dockerBuildArgs : dockerBuildLevelArguments, - imageName : webImageName, - directoryPath : "web", - dockerfilePath : "web/Dockerfile.web" - ) - } - } + // stage ("Build Web Image") { + // steps { + // buildDockerImage ( + // awsRegion : awsRegion, + // dockerBuildArgs : dockerBuildLevelArguments, + // imageName : webImageName, + // directoryPath : "web", + // dockerfilePath : "web/Dockerfile.web" + // ) + // } + // } // stage ("Build Admin Image") { // steps { // buildDockerImage ( @@ -119,14 +119,14 @@ pipeline { stage("Push to registry") { parallel { - stage ("Push Web Image") { - steps { - pushDockerImage ( - awsRegion : awsRegion, - imageName : webImageName - ) - } - } + // stage ("Push Web Image") { + // steps { + // pushDockerImage ( + // awsRegion : awsRegion, + // imageName : webImageName + // ) + // } + // } // stage ("Push Admin Image") { // steps { // pushDockerImage ( From dd1a3cc10504687785f2bda6fe05e20cd453f794 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 15:56:56 +0000 Subject: [PATCH 019/164] testing jenkins --- demo.Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 3dd1c525d6b..ed3aa95375c 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -30,9 +30,9 @@ def dockerBuildLevelArguments = [ CONFIG_FILES_PATH : configStoragePath ] -def frontendImageName = "plane-demo-frontend:latest" -def adminImageName = "plane-demo-admin:latest" -def apiImageName = "plane-demo-api:latest" +def frontendImageName = "plane-frontend:latest" +def adminImageName = "plane-adminpanel:latest" +def apiImageName = "plane-apiserver:latest" // ECS Based Configurations def clusterName = "demo-applications-fargate" From 1bc77d5b2571e690cffad8c46e9444322be29f21 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:15:54 +0000 Subject: [PATCH 020/164] testing jenkins --- apiserver/Dockerfile.api | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 97a2b2d4105..d4a273f85dd 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -46,7 +46,8 @@ RUN mkdir -p /code/plane/logs RUN chmod +x ./bin/* RUN chmod -R 777 /code -# Expose container port and run entry point script EXPOSE 8000 +CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - +# Expose container port and run entry point script From b92248be68b87fb5ca30856b96ab76b3659059fc Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:30:01 +0000 Subject: [PATCH 021/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index ed3aa95375c..0ba00418269 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -66,14 +66,14 @@ pipeline { def secret = [ [ path: "${repository}/${projectEnv}", secretValues: [ - [envVar: 'CONFIG', vaultKey: 'config.json'] + [envVar: 'ENV', vaultKey: 'env.json'] ]] ] withVault(configuration: configuration, vaultSecrets: secret) { sh """ set +x mkdir -p ${configStoragePath} - echo \"\${CONFIG}\" > ${configStoragePath}/config.json + echo \"\${ENV}\" > ${configStoragePath}/env.json """ } } From 07c00f8166a9d208d00a228c5ca7b5daf8b647bb Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:37:30 +0000 Subject: [PATCH 022/164] testing jenkins --- demo.Jenkinsfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 0ba00418269..8ba824f6c54 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -63,12 +63,14 @@ pipeline { steps { // define vault secret path and env var script { + def secret = [ [ path: "${repository}/${projectEnv}", secretValues: [ [envVar: 'ENV', vaultKey: 'env.json'] ]] ] + sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { sh """ set +x From e61227bf085b08b86bd179ecd57ae3a7b61312ff Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:42:16 +0000 Subject: [PATCH 023/164] testing jenkins --- apiserver/Dockerfile.api | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index d4a273f85dd..b95ab5f0bd2 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -47,6 +47,13 @@ RUN chmod +x ./bin/* RUN chmod -R 777 /code EXPOSE 8000 + +ARG CONFIG_FILES_PATH + +COPY $CONFIG_FILES_PATH/config.json config/config.json + +ENV N8N_CONFIG_FILES=/home/node/config/config.json + CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From 99f593a314bf3328732f11a66de9702b1eafa552 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:45:37 +0000 Subject: [PATCH 024/164] testing jenkins --- apiserver/Dockerfile.api | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index b95ab5f0bd2..c2977c880e6 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -50,9 +50,9 @@ EXPOSE 8000 ARG CONFIG_FILES_PATH -COPY $CONFIG_FILES_PATH/config.json config/config.json +COPY $CONFIG_FILES_PATH/env.json config/env.json -ENV N8N_CONFIG_FILES=/home/node/config/config.json +ENV N8N_CONFIG_FILES=/home/node/config/env.json CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From d6804eba422bf81f4ee553676111bf05b247edf9 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:51:39 +0000 Subject: [PATCH 025/164] testing jenkins --- demo.Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 8ba824f6c54..11b32b57055 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -72,6 +72,7 @@ pipeline { ] sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { + sh "echo ${secret}" sh """ set +x mkdir -p ${configStoragePath} From 26e25ac69d4068f36541ac6ebcccf0adad0e51c5 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:54:01 +0000 Subject: [PATCH 026/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 11b32b57055..15d853d82d0 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -75,7 +75,7 @@ pipeline { sh "echo ${secret}" sh """ set +x - mkdir -p ${configStoragePath} + mkdir -p ${ENV} echo \"\${ENV}\" > ${configStoragePath}/env.json """ } From 74123dd284ffefba9d029b7b07b0d3b7f2faeef3 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 17:57:34 +0000 Subject: [PATCH 027/164] testing jenkins --- demo.Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 15d853d82d0..18f47b963db 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -72,11 +72,11 @@ pipeline { ] sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { - sh "echo ${secret}" + sh "echo ${CONFIG}" sh """ set +x - mkdir -p ${ENV} - echo \"\${ENV}\" > ${configStoragePath}/env.json + mkdir -p ${configStoragePath} + echo \"\${CONFIG}\" > ${configStoragePath}/env.json """ } } From 0fc8bb0a446494b6776164b8646e40193353111b Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:07:26 +0000 Subject: [PATCH 028/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 18f47b963db..36e99bb7331 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -67,7 +67,7 @@ pipeline { def secret = [ [ path: "${repository}/${projectEnv}", secretValues: [ - [envVar: 'ENV', vaultKey: 'env.json'] + [envVar: 'CONFIG', vaultKey: 'env.json'] ]] ] sh "echo ${secret}" From 093245b2424769403fa512afa6233f48a864d5e5 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:11:45 +0000 Subject: [PATCH 029/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 36e99bb7331..0b53561d93c 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -14,7 +14,7 @@ def projectEnv = "demo" // Config Based configurations def vaultConfigFilesMap = [ - "ENV" : "env.json", + "CONFIG" : "env.json", ] def configStoragePath = "config-files" From d4948e5c37306b31aff33612231c154138a444f3 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:12:50 +0000 Subject: [PATCH 030/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 0b53561d93c..419e386220a 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -72,7 +72,7 @@ pipeline { ] sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { - sh "echo ${CONFIG}" + // sh "echo ${CONFIG}" sh """ set +x mkdir -p ${configStoragePath} From a86cbc372498fe868d9cfac8f1f2cb9efb596403 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:24:09 +0000 Subject: [PATCH 031/164] testing jenkins --- demo.Jenkinsfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 419e386220a..c35a561989d 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -63,14 +63,13 @@ pipeline { steps { // define vault secret path and env var script { - def secret = [ [ path: "${repository}/${projectEnv}", secretValues: [ [envVar: 'CONFIG', vaultKey: 'env.json'] ]] ] - sh "echo ${secret}" + // sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { // sh "echo ${CONFIG}" sh """ From 55019e59b9b7a20591c94cb362c938c5e9001378 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:25:06 +0000 Subject: [PATCH 032/164] testing jenkins --- apiserver/Dockerfile.api | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index c2977c880e6..a87ba16444b 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -48,11 +48,11 @@ RUN chmod -R 777 /code EXPOSE 8000 -ARG CONFIG_FILES_PATH +# ARG CONFIG_FILES_PATH -COPY $CONFIG_FILES_PATH/env.json config/env.json +# COPY $CONFIG_FILES_PATH/env.json config/env.json -ENV N8N_CONFIG_FILES=/home/node/config/env.json +# ENV N8N_CONFIG_FILES=/home/node/config/env.json CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From 5fd3638988b305abb419d64a6e639bcccd890041 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:28:45 +0000 Subject: [PATCH 033/164] testing jenkins --- apiserver/Dockerfile.api | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index a87ba16444b..e24b31c2366 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -48,9 +48,9 @@ RUN chmod -R 777 /code EXPOSE 8000 -# ARG CONFIG_FILES_PATH +ARG CONFIG_FILES_PATH -# COPY $CONFIG_FILES_PATH/env.json config/env.json +COPY $CONFIG_FILES_PATH/env.json config/env.json # ENV N8N_CONFIG_FILES=/home/node/config/env.json From 771761155e4eb19684946b790dfcb492abeea91d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:33:09 +0000 Subject: [PATCH 034/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index e24b31c2366..09dcd597835 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -50,7 +50,7 @@ EXPOSE 8000 ARG CONFIG_FILES_PATH -COPY $CONFIG_FILES_PATH/env.json config/env.json +COPY $CONFIG_FILES_PATH/.env config/.env # ENV N8N_CONFIG_FILES=/home/node/config/env.json From f54c03cfe56a59097b66d31386c01e453c43f5e0 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:34:06 +0000 Subject: [PATCH 035/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index c35a561989d..ed4bfeb0212 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -75,7 +75,7 @@ pipeline { sh """ set +x mkdir -p ${configStoragePath} - echo \"\${CONFIG}\" > ${configStoragePath}/env.json + echo \"\${CONFIG}\" > ${configStoragePath}/.env """ } } From e99955fe02098236b54ea06c636cec3af0e96d51 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:35:49 +0000 Subject: [PATCH 036/164] testing jenkins --- demo.Jenkinsfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index ed4bfeb0212..a1fdab25025 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -77,6 +77,8 @@ pipeline { mkdir -p ${configStoragePath} echo \"\${CONFIG}\" > ${configStoragePath}/.env """ + sh "cat ${configStoragePath}/.env" + sh "ls -l config-files/.env" } } } From f25596fd87e099221fefd2c167aa03dbf4807382 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:37:03 +0000 Subject: [PATCH 037/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index a1fdab25025..d361b735045 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -71,7 +71,7 @@ pipeline { ] // sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { - // sh "echo ${CONFIG}" + sh "echo ${CONFIG}" sh """ set +x mkdir -p ${configStoragePath} From 5be10553ba3a3eaa7e60095f18a0d00bb5bbefcc Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:37:56 +0000 Subject: [PATCH 038/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index d361b735045..ab680401576 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -71,12 +71,12 @@ pipeline { ] // sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { - sh "echo ${CONFIG}" sh """ set +x mkdir -p ${configStoragePath} echo \"\${CONFIG}\" > ${configStoragePath}/.env """ + sh "echo ${CONFIG}" sh "cat ${configStoragePath}/.env" sh "ls -l config-files/.env" } From 17d2ee082a8a6dc7921afb55a75d81bbf68d4fa0 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:41:56 +0000 Subject: [PATCH 039/164] testing jenkins --- demo.Jenkinsfile | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index ab680401576..2e2fdb0862e 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -64,21 +64,26 @@ pipeline { // define vault secret path and env var script { def secret = [ - [ - path: "${repository}/${projectEnv}", secretValues: [ - [envVar: 'CONFIG', vaultKey: 'env.json'] - ]] + [ + path: "${repository}/${projectEnv}", + secretValues: [ + [envVar: 'CONFIG', vaultKey: 'env.json'] + ] + ] ] // sh "echo ${secret}" withVault(configuration: configuration, vaultSecrets: secret) { sh """ set +x mkdir -p ${configStoragePath} - echo \"\${CONFIG}\" > ${configStoragePath}/.env + echo "\${CONFIG}" > ${configStoragePath}/.env """ - sh "echo ${CONFIG}" + + // Debugging: Show the contents of the .env file (optional for development only) sh "cat ${configStoragePath}/.env" - sh "ls -l config-files/.env" + + // Verify the file has been created + sh "ls -l ${configStoragePath}/.env" } } } From 102dc72911ca60b9a651ce9dc08891f9c0904336 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:43:22 +0000 Subject: [PATCH 040/164] testing jenkins --- demo.Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 2e2fdb0862e..4884f48d1d2 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -75,6 +75,7 @@ pipeline { withVault(configuration: configuration, vaultSecrets: secret) { sh """ set +x + echo "Vault CONFIG: \${CONFIG}" # Debugging, remove in production mkdir -p ${configStoragePath} echo "\${CONFIG}" > ${configStoragePath}/.env """ From abcaa3f5e32de0efc7266ea2a98bd7bcd7e6e72b Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 18:44:52 +0000 Subject: [PATCH 041/164] testing jenkins --- demo.Jenkinsfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 4884f48d1d2..24ad80e445d 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -9,12 +9,12 @@ def configuration = [ ] // Project Level Configurations -def repository = "plane" +def repository = "n8n" def projectEnv = "demo" // Config Based configurations def vaultConfigFilesMap = [ - "CONFIG" : "env.json", + "CONFIG" : "config.json", ] def configStoragePath = "config-files" @@ -67,7 +67,7 @@ pipeline { [ path: "${repository}/${projectEnv}", secretValues: [ - [envVar: 'CONFIG', vaultKey: 'env.json'] + [envVar: 'CONFIG', vaultKey: 'config.json'] ] ] ] @@ -77,7 +77,7 @@ pipeline { set +x echo "Vault CONFIG: \${CONFIG}" # Debugging, remove in production mkdir -p ${configStoragePath} - echo "\${CONFIG}" > ${configStoragePath}/.env + echo "\${CONFIG}" > ${configStoragePath}/config.json """ // Debugging: Show the contents of the .env file (optional for development only) From 0d7efc85e721787bd0f7246f812b31412c8ff019 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:04:12 +0000 Subject: [PATCH 042/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 24ad80e445d..3e20e556133 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -9,7 +9,7 @@ def configuration = [ ] // Project Level Configurations -def repository = "n8n" +def repository = "plane" def projectEnv = "demo" // Config Based configurations From ab4a1e5cd1b23a61b8d32c071b96e2cbd8d54a57 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:21:19 +0000 Subject: [PATCH 043/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 3e20e556133..dccd1278e5f 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -81,10 +81,10 @@ pipeline { """ // Debugging: Show the contents of the .env file (optional for development only) - sh "cat ${configStoragePath}/.env" + sh "cat ${configStoragePath}/config.json" // Verify the file has been created - sh "ls -l ${configStoragePath}/.env" + sh "ls -l ${configStoragePath}/config.json" } } } From 8b6fac68a7031d6b0fec8acf16eb11a5075dc6a5 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:22:38 +0000 Subject: [PATCH 044/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 09dcd597835..d44541cadb5 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -50,7 +50,7 @@ EXPOSE 8000 ARG CONFIG_FILES_PATH -COPY $CONFIG_FILES_PATH/.env config/.env +COPY $CONFIG_FILES_PATH/config.json config/config.json # ENV N8N_CONFIG_FILES=/home/node/config/env.json From aa5ec50d695ffdccdaa73b31ee0fa7e4b0a5e44d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:47:48 +0000 Subject: [PATCH 045/164] testing jenkins --- apiserver/Dockerfile.api | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index d44541cadb5..1da178638b6 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -48,11 +48,13 @@ RUN chmod -R 777 /code EXPOSE 8000 -ARG CONFIG_FILES_PATH +# ENV N8N_CONFIG_FILES=/home/node/config/env.json -COPY $CONFIG_FILES_PATH/config.json config/config.json +# Declare a build argument +ARG ENV_FILE_PATH -# ENV N8N_CONFIG_FILES=/home/node/config/env.json +# Use the build argument +COPY ${ENV_FILE_PATH} /app/.env CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From 809a63b4e13df676c31062a913745b314e4f544b Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:50:56 +0000 Subject: [PATCH 046/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index dccd1278e5f..62f25ba7a98 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -27,7 +27,7 @@ vaultConfigFilesMap.each { envVariable, configFileName -> // Docker Based Configurations def awsRegion = "us-west-2" def dockerBuildLevelArguments = [ - CONFIG_FILES_PATH : configStoragePath + ENV_FILE_PATH: configStoragePath ] def frontendImageName = "plane-frontend:latest" From f4b5273a706ca41166359a7ff1b9a790fa7d23f4 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:52:19 +0000 Subject: [PATCH 047/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 62f25ba7a98..afe8c17a605 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -27,7 +27,7 @@ vaultConfigFilesMap.each { envVariable, configFileName -> // Docker Based Configurations def awsRegion = "us-west-2" def dockerBuildLevelArguments = [ - ENV_FILE_PATH: configStoragePath + ENV_FILE_PATH: "${configStoragePath}/.env" ] def frontendImageName = "plane-frontend:latest" @@ -77,7 +77,7 @@ pipeline { set +x echo "Vault CONFIG: \${CONFIG}" # Debugging, remove in production mkdir -p ${configStoragePath} - echo "\${CONFIG}" > ${configStoragePath}/config.json + echo "\${CONFIG}" > ${configStoragePath}/.env """ // Debugging: Show the contents of the .env file (optional for development only) From 4f272e91ea3574f0d6352cb8d196fdd9819068c0 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 19:56:03 +0000 Subject: [PATCH 048/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index afe8c17a605..e3eb2119ab4 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -81,10 +81,10 @@ pipeline { """ // Debugging: Show the contents of the .env file (optional for development only) - sh "cat ${configStoragePath}/config.json" + sh "cat ${configStoragePath}/.env" // Verify the file has been created - sh "ls -l ${configStoragePath}/config.json" + sh "ls -l ${configStoragePath}/.env" } } } From f6961ef4da130e7118aebe1357db60f2b3117614 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:00:48 +0000 Subject: [PATCH 049/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 1da178638b6..417ecd3be7d 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -54,7 +54,7 @@ EXPOSE 8000 ARG ENV_FILE_PATH # Use the build argument -COPY ${ENV_FILE_PATH} /app/.env +COPY ../${ENV_FILE_PATH} /app/.env CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From e8fde6e29a9f3a3452329aaa131a4ae4a617d11b Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:02:11 +0000 Subject: [PATCH 050/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- demo.Jenkinsfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 417ecd3be7d..1da178638b6 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -54,7 +54,7 @@ EXPOSE 8000 ARG ENV_FILE_PATH # Use the build argument -COPY ../${ENV_FILE_PATH} /app/.env +COPY ${ENV_FILE_PATH} /app/.env CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index e3eb2119ab4..4f06305bcb1 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -76,8 +76,8 @@ pipeline { sh """ set +x echo "Vault CONFIG: \${CONFIG}" # Debugging, remove in production - mkdir -p ${configStoragePath} - echo "\${CONFIG}" > ${configStoragePath}/.env + mkdir -p ../${configStoragePath} + echo "\${CONFIG}" > ../${configStoragePath}/.env """ // Debugging: Show the contents of the .env file (optional for development only) From ecd4c78db3e2c4fdd3d6406ab3f971305136c553 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:04:53 +0000 Subject: [PATCH 051/164] testing jenkins --- demo.Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 4f06305bcb1..055f42af80b 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -27,7 +27,7 @@ vaultConfigFilesMap.each { envVariable, configFileName -> // Docker Based Configurations def awsRegion = "us-west-2" def dockerBuildLevelArguments = [ - ENV_FILE_PATH: "${configStoragePath}/.env" + ENV_FILE_PATH: "../${configStoragePath}/.env" ] def frontendImageName = "plane-frontend:latest" @@ -81,10 +81,10 @@ pipeline { """ // Debugging: Show the contents of the .env file (optional for development only) - sh "cat ${configStoragePath}/.env" + sh "cat ../${configStoragePath}/.env" // Verify the file has been created - sh "ls -l ${configStoragePath}/.env" + sh "ls -l ../${configStoragePath}/.env" } } } From 71880d5f7618106a71c800b8bef368514fbb8195 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:06:12 +0000 Subject: [PATCH 052/164] testing jenkins --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 055f42af80b..d48322ac27d 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -27,7 +27,7 @@ vaultConfigFilesMap.each { envVariable, configFileName -> // Docker Based Configurations def awsRegion = "us-west-2" def dockerBuildLevelArguments = [ - ENV_FILE_PATH: "../${configStoragePath}/.env" + ENV_FILE_PATH: "${configStoragePath}/.env" ] def frontendImageName = "plane-frontend:latest" From 40a209be144f850ba9d51b685c34cf0abccd43e8 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:09:16 +0000 Subject: [PATCH 053/164] testing jenkins --- demo.Jenkinsfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index d48322ac27d..190adf7994a 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -76,15 +76,16 @@ pipeline { sh """ set +x echo "Vault CONFIG: \${CONFIG}" # Debugging, remove in production - mkdir -p ../${configStoragePath} - echo "\${CONFIG}" > ../${configStoragePath}/.env + pwd + mkdir -p ${configStoragePath} + echo "\${CONFIG}" > ${configStoragePath}/.env """ // Debugging: Show the contents of the .env file (optional for development only) - sh "cat ../${configStoragePath}/.env" + sh "cat ${configStoragePath}/.env" // Verify the file has been created - sh "ls -l ../${configStoragePath}/.env" + sh "ls -l ${configStoragePath}/.env" } } } From c4e76bc297c49744c48ec99ce7c9f2245528fd10 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:10:44 +0000 Subject: [PATCH 054/164] testing jenkins --- demo.Jenkinsfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 190adf7994a..ace7c7f997f 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -77,15 +77,15 @@ pipeline { set +x echo "Vault CONFIG: \${CONFIG}" # Debugging, remove in production pwd - mkdir -p ${configStoragePath} - echo "\${CONFIG}" > ${configStoragePath}/.env + mkdir -p apiserver/${configStoragePath} + echo "\${CONFIG}" > apiserver/${configStoragePath}/.env """ // Debugging: Show the contents of the .env file (optional for development only) - sh "cat ${configStoragePath}/.env" + sh "cat apiserver/${configStoragePath}/.env" // Verify the file has been created - sh "ls -l ${configStoragePath}/.env" + sh "ls -l apiserver/${configStoragePath}/.env" } } } From a180116660422e0aabd97aa4d6c17d65012d6d1e Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:13:34 +0000 Subject: [PATCH 055/164] testing jenkins --- apiserver/Dockerfile.api | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 1da178638b6..b52b2982a24 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -52,7 +52,8 @@ EXPOSE 8000 # Declare a build argument ARG ENV_FILE_PATH - +RUN echo "The value of ENV_FILE_PATH is: ${ENV_FILE_PATH}" +RUN cat ${ENV_FILE_PATH} # Use the build argument COPY ${ENV_FILE_PATH} /app/.env From 8e926b125781b5b70ca09e3978876ccccce406e7 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:14:47 +0000 Subject: [PATCH 056/164] testing jenkins --- apiserver/Dockerfile.api | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index b52b2982a24..1da178638b6 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -52,8 +52,7 @@ EXPOSE 8000 # Declare a build argument ARG ENV_FILE_PATH -RUN echo "The value of ENV_FILE_PATH is: ${ENV_FILE_PATH}" -RUN cat ${ENV_FILE_PATH} + # Use the build argument COPY ${ENV_FILE_PATH} /app/.env From e8c8eaf73e613bbabe2705712e3c293c588e8be9 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:21:47 +0000 Subject: [PATCH 057/164] testing jenkins --- apiserver/Dockerfile.api | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 1da178638b6..c0d9fdf7d04 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -55,7 +55,11 @@ ARG ENV_FILE_PATH # Use the build argument COPY ${ENV_FILE_PATH} /app/.env +RUN export $(grep -v '^#' /app/.env | xargs) && \ + echo "Environment variables set successfully" +# For demonstration: Print an environment variable +RUN echo "Database host: $DB_HOST" CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From b2e1a7203cf03e25aef36572c1ee9cd09bdff355 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:24:26 +0000 Subject: [PATCH 058/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index c0d9fdf7d04..5a5342be9d8 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -59,7 +59,7 @@ RUN export $(grep -v '^#' /app/.env | xargs) && \ echo "Environment variables set successfully" # For demonstration: Print an environment variable -RUN echo "Database host: $DB_HOST" +RUN echo "Database host: $POSTGRES_USER" CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From c9df8449d775f64c862f6c400d4be86bc2c41cf4 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:30:42 +0000 Subject: [PATCH 059/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 5a5342be9d8..5737455396d 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -56,7 +56,7 @@ ARG ENV_FILE_PATH # Use the build argument COPY ${ENV_FILE_PATH} /app/.env RUN export $(grep -v '^#' /app/.env | xargs) && \ - echo "Environment variables set successfully" + echo "Environment ${ENV_FILE_PATH} variables set successfully" # For demonstration: Print an environment variable RUN echo "Database host: $POSTGRES_USER" From 555ee5c4679f21c1badf2318e5f0c100984ba656 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:38:04 +0000 Subject: [PATCH 060/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 5737455396d..dd71674f235 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -55,7 +55,7 @@ ARG ENV_FILE_PATH # Use the build argument COPY ${ENV_FILE_PATH} /app/.env -RUN export $(grep -v '^#' /app/.env | xargs) && \ +RUN export $(cat /app/.env | xargs) && \ echo "Environment ${ENV_FILE_PATH} variables set successfully" # For demonstration: Print an environment variable From 19166f65742e9033fd26bd725a221857eaaa9b18 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:39:33 +0000 Subject: [PATCH 061/164] testing jenkins --- apiserver/Dockerfile.api | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index dd71674f235..fc2865c1b45 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -55,7 +55,8 @@ ARG ENV_FILE_PATH # Use the build argument COPY ${ENV_FILE_PATH} /app/.env -RUN export $(cat /app/.env | xargs) && \ +RUN cat /app/.env && \ + export $(cat /app/.env | xargs) && \ echo "Environment ${ENV_FILE_PATH} variables set successfully" # For demonstration: Print an environment variable From 574033c2b914aebcc7eb76b8e3f356bbdce9137d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:44:22 +0000 Subject: [PATCH 062/164] testing jenkins --- apiserver/Dockerfile.api | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index fc2865c1b45..f5a1b20c81e 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -55,12 +55,11 @@ ARG ENV_FILE_PATH # Use the build argument COPY ${ENV_FILE_PATH} /app/.env -RUN cat /app/.env && \ - export $(cat /app/.env | xargs) && \ - echo "Environment ${ENV_FILE_PATH} variables set successfully" +RUN cat /app/.env +RUN pip install python-dotenv # For demonstration: Print an environment variable -RUN echo "Database host: $POSTGRES_USER" +RUN python -c "from dotenv import load_dotenv; load_dotenv('/app/.env'); import os; print(f'Database host: {os.getenv('POSTGRES_USER')}')" CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From 0b836dd2c1eb891cf3e207d1cb4e40fd8018f007 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:47:45 +0000 Subject: [PATCH 063/164] testing jenkins --- apiserver/Dockerfile.api | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index f5a1b20c81e..131777a5514 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -57,9 +57,9 @@ ARG ENV_FILE_PATH COPY ${ENV_FILE_PATH} /app/.env RUN cat /app/.env -RUN pip install python-dotenv -# For demonstration: Print an environment variable -RUN python -c "from dotenv import load_dotenv; load_dotenv('/app/.env'); import os; print(f'Database host: {os.getenv('POSTGRES_USER')}')" +# Export the environment file path as an image environment variable +ENV ENV_FILE_PATH=${ENV_FILE_PATH} + CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - # Expose container port and run entry point script From 805710d9ebfa62c8d52c6a5d520edebb959b7fdb Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 20:59:17 +0000 Subject: [PATCH 064/164] testing jenkins --- apiserver/plane/settings/redis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/settings/redis.py b/apiserver/plane/settings/redis.py index 628a3d8e63b..a95a44e3134 100644 --- a/apiserver/plane/settings/redis.py +++ b/apiserver/plane/settings/redis.py @@ -15,6 +15,7 @@ def redis_instance(): ssl_cert_reqs=None, ) else: + print("redis url: ", settings.REDIS_URL) ri = redis.Redis.from_url(settings.REDIS_URL, db=0) return ri From a08074bb96c1030f30731349ed3ffff9a39d2bda Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:04:31 +0000 Subject: [PATCH 065/164] testing jenkins --- apiserver/plane/settings/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 792bb27d123..3d541d17d62 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -181,6 +181,8 @@ # Redis Config REDIS_URL = os.environ.get("REDIS_URL") +for key, value in os.environ.items(): + print(f"Env Variable {key}: {value}") REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL if REDIS_SSL: From caa93416c3a4090a94e6e7d90079772d59ea1400 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:12:44 +0000 Subject: [PATCH 066/164] testing jenkins --- apiserver/plane/settings/common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 3d541d17d62..e03adaccb1c 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -25,7 +25,10 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.instrumentation.django import DjangoInstrumentor +from dotenv import load_dotenv +# Load environment variables from .env file if it exists +load_dotenv(os.path.join(BASE_DIR, ".env")) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 240b47a2e2c59ede3cd32ae661a1f2a74cbf77bf Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:15:12 +0000 Subject: [PATCH 067/164] testing jenkins --- apiserver/Dockerfile.api | 3 +-- apiserver/requirements/base.txt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 131777a5514..ee482ba337c 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -54,8 +54,7 @@ EXPOSE 8000 ARG ENV_FILE_PATH # Use the build argument -COPY ${ENV_FILE_PATH} /app/.env -RUN cat /app/.env +COPY ${ENV_FILE_PATH} /code/.env # Export the environment file path as an image environment variable ENV ENV_FILE_PATH=${ENV_FILE_PATH} diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index fbe6680d43f..b3e3ba6c65e 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,7 +1,7 @@ # base requirements # django -Django==4.2.16 +Django==4.2.14 # rest framework djangorestframework==3.15.2 # postgres @@ -29,7 +29,7 @@ jsonmodels==2.7.0 # sentry sentry-sdk==2.8.0 # storage -django-storages==1.14.2 +django-storages==1.14.4 # user management django-crum==0.7.9 # web server From 3e669259e74b74730f091d061cc8573d3adb3f8d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:22:50 +0000 Subject: [PATCH 068/164] testing jenkins --- apiserver/requirements/base.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index b3e3ba6c65e..7b9d7ffe4fc 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -10,6 +10,7 @@ psycopg-binary==3.1.18 psycopg-c==3.1.18 dj-database-url==2.1.0 # redis +python-dotenv==1.0.0 redis==5.0.4 django-redis==5.4.0 # cors From c861bc925c3cfa5bfefc2b026dbfad2207c6fd45 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:28:29 +0000 Subject: [PATCH 069/164] testing jenkins --- apiserver/plane/settings/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index e03adaccb1c..95616605542 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -28,10 +28,10 @@ from dotenv import load_dotenv # Load environment variables from .env file if it exists -load_dotenv(os.path.join(BASE_DIR, ".env")) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +load_dotenv(os.path.join(BASE_DIR, ".env")) # Secret Key SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key()) From c0fb7b650cbb348f49eb1fae4073f8902b26a228 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:33:30 +0000 Subject: [PATCH 070/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index ee482ba337c..a7b394ddfae 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -54,7 +54,7 @@ EXPOSE 8000 ARG ENV_FILE_PATH # Use the build argument -COPY ${ENV_FILE_PATH} /code/.env +COPY ${ENV_FILE_PATH} /code/file.env # Export the environment file path as an image environment variable ENV ENV_FILE_PATH=${ENV_FILE_PATH} From 896418750d056945f9ed50792cda0a68a44a4b3d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:36:02 +0000 Subject: [PATCH 071/164] testing jenkins --- apiserver/plane/settings/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 95616605542..baa933396a5 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -31,7 +31,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -load_dotenv(os.path.join(BASE_DIR, ".env")) +load_dotenv(os.path.join(BASE_DIR, "file.env")) # Secret Key SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key()) From 50e257ebafc2f838252670137169e8a75cae9aac Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:38:48 +0000 Subject: [PATCH 072/164] testing jenkins --- apiserver/plane/settings/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index baa933396a5..11666ff40a1 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -31,6 +31,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +print(f"BASE_DIR: {BASE_DIR}") load_dotenv(os.path.join(BASE_DIR, "file.env")) # Secret Key SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key()) From 1747cef403dad397039d2712a1962437e452174c Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:40:47 +0000 Subject: [PATCH 073/164] testing jenkins --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index a7b394ddfae..bfbff5e4678 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -54,7 +54,7 @@ EXPOSE 8000 ARG ENV_FILE_PATH # Use the build argument -COPY ${ENV_FILE_PATH} /code/file.env +COPY ${ENV_FILE_PATH} /code/plane/file.env # Export the environment file path as an image environment variable ENV ENV_FILE_PATH=${ENV_FILE_PATH} From 150410debcc48cd3b02c1a8e70f9b5f430cf10f2 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 21:51:19 +0000 Subject: [PATCH 074/164] Testing Multiple Dev --- apiserver/Dockerfile.api | 12 +++++++++- demo.Jenkinsfile | 52 ++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index bfbff5e4678..333ca60a67d 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -59,7 +59,17 @@ COPY ${ENV_FILE_PATH} /code/plane/file.env # Export the environment file path as an image environment variable ENV ENV_FILE_PATH=${ENV_FILE_PATH} -CMD gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - +CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \ + gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -; \ + elif [ "${ENV_TYPE}" = "celery" ]; then \ + celery -A plane worker -l info; \ + elif [ "${ENV_TYPE}" = "celery-beat" ]; then \ + celery -A plane beat -l info; \ + else \ + echo "Unknown ENV_TYPE: ${ENV_TYPE}"; \ + exit 1; \ + fi + # Expose container port and run entry point script diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index ace7c7f997f..4ddd1713ba5 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -159,33 +159,33 @@ pipeline { stage("Deploy Plane") { parallel { - stage("Deploy Frontend") { - steps { - script { - deployServiceOnECS ( - awsRegion : awsRegion, - imageName : webImageName, - ecsClusterName : clusterName, - ecsServiceName : frontEndServiceName, - timeout : 300 - ) - } - } - } + // stage("Deploy Frontend") { + // steps { + // script { + // deployServiceOnECS ( + // awsRegion : awsRegion, + // imageName : webImageName, + // ecsClusterName : clusterName, + // ecsServiceName : frontEndServiceName, + // timeout : 300 + // ) + // } + // } + // } - stage("Deploy Admin") { - steps { - script { - deployServiceOnECS ( - awsRegion : awsRegion, - imageName : adminImageName, - ecsClusterName : clusterName, - ecsServiceName : adminPanelServiceName, - timeout : 300 - ) - } - } - } + // stage("Deploy Admin") { + // steps { + // script { + // deployServiceOnECS ( + // awsRegion : awsRegion, + // imageName : adminImageName, + // ecsClusterName : clusterName, + // ecsServiceName : adminPanelServiceName, + // timeout : 300 + // ) + // } + // } + // } stage("Deploy API") { steps { From 2ae579f018f4a3ef777515e7833e813d4291b295 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:02:32 +0000 Subject: [PATCH 075/164] Testing Multiple Dev --- demo.Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 4ddd1713ba5..ff3dc1c33ca 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -42,7 +42,7 @@ def clusterName = "demo-applications-fargate" // def webhookServiceName = "demo-n8n-webhook" def apiServiceName = "demo-plane-apiserver" def celeryServiceName = "demo-plane-celery" -def cbeatServiceName = "demo-plane-celerybeat" +def cbeatServiceName = "demo-plane-celery-beat" def frontEndServiceName = "demo-plane-frontend" def adminPanelServiceName = "demo-plane-admin-panel" From 3c34f5921af5a0532f2be7ea3a7c714efefa26d8 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:10:12 +0000 Subject: [PATCH 076/164] Testing Multiple Dev --- demo.Jenkinsfile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index ff3dc1c33ca..c9ea3474dd8 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -92,17 +92,17 @@ pipeline { } stage ("Build docker image") { parallel { - // stage ("Build Web Image") { - // steps { - // buildDockerImage ( - // awsRegion : awsRegion, - // dockerBuildArgs : dockerBuildLevelArguments, - // imageName : webImageName, - // directoryPath : "web", - // dockerfilePath : "web/Dockerfile.web" - // ) - // } - // } + stage ("Build Web Image") { + steps { + buildDockerImage ( + awsRegion : awsRegion, + dockerBuildArgs : dockerBuildLevelArguments, + imageName : webImageName, + directoryPath : "web", + dockerfilePath : "web/Dockerfile.web" + ) + } + } // stage ("Build Admin Image") { // steps { // buildDockerImage ( From 6fc92fda366c844ae3c7a1f0bdb4998689365056 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:15:10 +0000 Subject: [PATCH 077/164] Testing Multiple Dev --- demo.Jenkinsfile | 1 - 1 file changed, 1 deletion(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index c9ea3474dd8..6d0c490777e 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -96,7 +96,6 @@ pipeline { steps { buildDockerImage ( awsRegion : awsRegion, - dockerBuildArgs : dockerBuildLevelArguments, imageName : webImageName, directoryPath : "web", dockerfilePath : "web/Dockerfile.web" From ec07052e480f180f9f6015bd75aea394ca1013e4 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:16:52 +0000 Subject: [PATCH 078/164] Testing Multiple Dev --- demo.Jenkinsfile | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 6d0c490777e..a76e6f1dc2b 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -102,17 +102,16 @@ pipeline { ) } } - // stage ("Build Admin Image") { - // steps { - // buildDockerImage ( - // awsRegion : awsRegion, - // dockerBuildArgs : dockerBuildLevelArguments, - // imageName : adminImageName, - // directoryPath : "admin", - // dockerfilePath : "admin/Dockerfile.admin" - // ) - // } - // } + stage ("Build Admin Image") { + steps { + buildDockerImage ( + awsRegion : awsRegion, + imageName : adminImageName, + directoryPath : "admin", + dockerfilePath : "admin/Dockerfile.admin" + ) + } + } stage ("Build API Image") { steps { buildDockerImage ( From c2a36cd011201a2bea9247ba07af9da4d8f49276 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:23:24 +0000 Subject: [PATCH 079/164] testing jenkins --- demo.Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index a76e6f1dc2b..281de3164ac 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -97,7 +97,7 @@ pipeline { buildDockerImage ( awsRegion : awsRegion, imageName : webImageName, - directoryPath : "web", + directoryPath : ".", dockerfilePath : "web/Dockerfile.web" ) } @@ -107,7 +107,7 @@ pipeline { buildDockerImage ( awsRegion : awsRegion, imageName : adminImageName, - directoryPath : "admin", + directoryPath : ".", dockerfilePath : "admin/Dockerfile.admin" ) } From ac39e7e2bc0967d1782896e1e28a54a438ceab51 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:31:12 +0000 Subject: [PATCH 080/164] Testing Multiple Dev --- admin/Dockerfile.admin | 4 +- demo.Jenkinsfile | 88 +++++++++++++++++++++--------------------- web/Dockerfile.web | 5 ++- 3 files changed, 51 insertions(+), 46 deletions(-) diff --git a/admin/Dockerfile.admin b/admin/Dockerfile.admin index ad9469110e7..3d9897705e8 100644 --- a/admin/Dockerfile.admin +++ b/admin/Dockerfile.admin @@ -83,4 +83,6 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL ENV NEXT_TELEMETRY_DISABLED 1 ENV TURBO_TELEMETRY_DISABLED 1 -EXPOSE 3000 \ No newline at end of file +EXPOSE 3000 + +CMD node admin/server.js admin \ No newline at end of file diff --git a/demo.Jenkinsfile b/demo.Jenkinsfile index 281de3164ac..93f250ff14b 100644 --- a/demo.Jenkinsfile +++ b/demo.Jenkinsfile @@ -30,7 +30,7 @@ def dockerBuildLevelArguments = [ ENV_FILE_PATH: "${configStoragePath}/.env" ] -def frontendImageName = "plane-frontend:latest" +def webImageName = "plane-frontend:latest" def adminImageName = "plane-adminpanel:latest" def apiImageName = "plane-apiserver:latest" @@ -128,22 +128,22 @@ pipeline { stage("Push to registry") { parallel { - // stage ("Push Web Image") { - // steps { - // pushDockerImage ( - // awsRegion : awsRegion, - // imageName : webImageName - // ) - // } - // } - // stage ("Push Admin Image") { - // steps { - // pushDockerImage ( - // awsRegion : awsRegion, - // imageName : adminImageName - // ) - // } - // } + stage ("Push Web Image") { + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : webImageName + ) + } + } + stage ("Push Admin Image") { + steps { + pushDockerImage ( + awsRegion : awsRegion, + imageName : adminImageName + ) + } + } stage ("Push API Image") { steps { pushDockerImage ( @@ -157,33 +157,33 @@ pipeline { stage("Deploy Plane") { parallel { - // stage("Deploy Frontend") { - // steps { - // script { - // deployServiceOnECS ( - // awsRegion : awsRegion, - // imageName : webImageName, - // ecsClusterName : clusterName, - // ecsServiceName : frontEndServiceName, - // timeout : 300 - // ) - // } - // } - // } - - // stage("Deploy Admin") { - // steps { - // script { - // deployServiceOnECS ( - // awsRegion : awsRegion, - // imageName : adminImageName, - // ecsClusterName : clusterName, - // ecsServiceName : adminPanelServiceName, - // timeout : 300 - // ) - // } - // } - // } + stage("Deploy Frontend") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : webImageName, + ecsClusterName : clusterName, + ecsServiceName : frontEndServiceName, + timeout : 300 + ) + } + } + } + + stage("Deploy Admin") { + steps { + script { + deployServiceOnECS ( + awsRegion : awsRegion, + imageName : adminImageName, + ecsClusterName : clusterName, + ecsServiceName : adminPanelServiceName, + timeout : 300 + ) + } + } + } stage("Deploy API") { steps { diff --git a/web/Dockerfile.web b/web/Dockerfile.web index d7d924d7a47..2c09674faef 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -70,7 +70,7 @@ COPY --from=installer /app/web/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=installer /app/web/.next/standalone ./ +COPY --from=installer /app/web/.next/export ./ COPY --from=installer /app/web/.next ./web/.next COPY --from=installer /app/web/public ./web/public @@ -101,4 +101,7 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL ENV NEXT_TELEMETRY_DISABLED 1 ENV TURBO_TELEMETRY_DISABLED 1 + EXPOSE 3000 + +CMD node web/server.js web From 7c5f5eaca744767c29f3873d5bdf0cc6a2576722 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 22:47:06 +0000 Subject: [PATCH 081/164] Testing Multiple Dev --- web/Dockerfile.web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/Dockerfile.web b/web/Dockerfile.web index 2c09674faef..cc5464c7713 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -71,7 +71,7 @@ COPY --from=installer /app/web/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=installer /app/web/.next/export ./ -COPY --from=installer /app/web/.next ./web/.next +COPY --from=installer /app/web/.next ./.next COPY --from=installer /app/web/public ./web/public ARG NEXT_PUBLIC_API_BASE_URL="" From 9bec624d65aca536ad9883caa94a476bbbc96a54 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 23:17:12 +0000 Subject: [PATCH 082/164] Testing Multiple Dev --- web/Dockerfile.web | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/Dockerfile.web b/web/Dockerfile.web index cc5464c7713..45b31eda3a1 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -70,8 +70,8 @@ COPY --from=installer /app/web/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=installer /app/web/.next/export ./ -COPY --from=installer /app/web/.next ./.next +# COPY --from=installer /app/web/.next/export ./ +# COPY --from=installer /app/web/.next ./.next COPY --from=installer /app/web/public ./web/public ARG NEXT_PUBLIC_API_BASE_URL="" From 2c5c9b779a9c282865a22e9f8efa373f617c1af2 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 19 Dec 2024 23:52:26 +0000 Subject: [PATCH 083/164] Testing Multiple Dev --- web/Dockerfile.web | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/Dockerfile.web b/web/Dockerfile.web index 45b31eda3a1..8cbfd75a276 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -70,8 +70,8 @@ COPY --from=installer /app/web/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -# COPY --from=installer /app/web/.next/export ./ -# COPY --from=installer /app/web/.next ./.next +COPY --from=installer /app/web/.next/standalone ./ +COPY --from=installer /app/web/.next ./.next COPY --from=installer /app/web/public ./web/public ARG NEXT_PUBLIC_API_BASE_URL="" From 78227f698193f15e6725753153bf756c218db428 Mon Sep 17 00:00:00 2001 From: K Lakshmi Surya Teja Date: Fri, 27 Dec 2024 18:46:59 +0530 Subject: [PATCH 084/164] added custom properties in issue component --- .../issues/issue-detail/sidebar.tsx | 19 +++++++++++++++++-- .../store/issue/issue-details/issue.store.ts | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 102dc6e5a33..d71d9cc1a1f 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -2,7 +2,7 @@ import React from "react"; import { observer } from "mobx-react"; -import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, UserCircle2, Users } from "lucide-react"; +import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, UserCircle2, Users, Info } from "lucide-react"; // ui import { ContrastIcon, DiceIcon, DoubleCircleIcon } from "@plane/ui"; // components @@ -53,7 +53,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { // derived values const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); - + const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -299,6 +299,21 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { isDisabled={!isEditable} /> )} + {issue?.custom_properties?.length > 0 && ( +
+ )} + {issue?.custom_properties?.map((element) => ( +
+
+ + {element.key} +
+
+ {element.value} +
+
+ )) + }
diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts index db0ccc39af2..02c723b3f6e 100644 --- a/web/core/store/issue/issue-details/issue.store.ts +++ b/web/core/store/issue/issue-details/issue.store.ts @@ -181,6 +181,7 @@ export class IssueStore implements IIssueStore { updated_by: issue?.updated_by, is_draft: issue?.is_draft, is_subscribed: issue?.is_subscribed, + custom_properties: issue?.custom_properties }; this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]); From d1e4f3d3b8b7fd3b5201e9c85ce8fcb298c72a7c Mon Sep 17 00:00:00 2001 From: K Lakshmi Surya Teja Date: Mon, 30 Dec 2024 16:04:28 +0530 Subject: [PATCH 085/164] fixed array error --- web/core/components/issues/issue-detail/sidebar.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index d71d9cc1a1f..1c0765675a7 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -53,7 +53,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { // derived values const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); - + const customProperties = issue?.custom_properties || []; const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -299,17 +299,17 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { isDisabled={!isEditable} /> )} - {issue?.custom_properties?.length > 0 && ( + {customProperties?.length > 0 && (
)} - {issue?.custom_properties?.map((element) => ( + {Array.isArray(customProperties) && customProperties.map((element) => (
- {element.key} + {element?.key}
- {element.value} + {element?.value}
)) From e7aab55194135d016a3106d2e2775e73164c033d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 31 Dec 2024 11:30:33 +0000 Subject: [PATCH 086/164] migration file --- apiserver/Dockerfile.api | 4 ++++ apiserver/plane/app/serializers/issue.py | 16 +++++++++++++++- apiserver/plane/app/views/issue/base.py | 3 ++- apiserver/plane/utils/grouper.py | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 333ca60a67d..ee99c50b4aa 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -59,6 +59,10 @@ COPY ${ENV_FILE_PATH} /code/plane/file.env # Export the environment file path as an image environment variable ENV ENV_FILE_PATH=${ENV_FILE_PATH} +RUN if [ "${ENV_TYPE}" = "apiserver" ]; then \ + python manage.py migrate --noinput; \ + fi + CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \ gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -; \ elif [ "${ENV_TYPE}" = "celery" ]; then \ diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 2323c248a86..83f5aeea3cb 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -32,6 +32,7 @@ CommentReaction, IssueVote, IssueRelation, + IssueCustomProperty, State, ) @@ -663,6 +664,18 @@ class Meta: ] read_only_fields = fields +class IssueCustomPropertySerializer(BaseSerializer): + class Meta: + model = IssueCustomProperty + fields = ["key", "value", "issue_type_custom_property"] + read_only_fields = [ + "id", + "issue", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] class IssueSerializer(DynamicBaseSerializer): # ids @@ -681,7 +694,6 @@ class IssueSerializer(DynamicBaseSerializer): child=serializers.UUIDField(), required=False, ) - # Count items sub_issues_count = serializers.IntegerField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) @@ -733,11 +745,13 @@ class Meta: class IssueDetailSerializer(IssueSerializer): description_html = serializers.CharField() is_subscribed = serializers.BooleanField(read_only=True) + custom_properties = IssueCustomPropertySerializer(many=True, required=False) class Meta(IssueSerializer.Meta): fields = IssueSerializer.Meta.fields + [ "description_html", "is_subscribed", + "custom_properties" ] read_only_fields = fields diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 1d49fd255c1..0c9db7597bb 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -617,8 +617,9 @@ def retrieve(self, request, slug, project_id, pk=None): user_id=request.user.id, project_id=project_id, ) - + serializer = IssueDetailSerializer(issue, expand=self.expand) + print(serializer.data) return Response(serializer.data, status=status.HTTP_200_OK) @allow_permission( diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index 38ac74a1647..4cf97d56566 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -93,7 +93,7 @@ def issue_on_results(issues, group_by, sub_group_by): "link_count", "is_draft", "archived_at", - "state__group", + "state__group" ] if group_by in FIELD_MAPPER: From cd0382899721c1e0c874d866c9141cff994707a7 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 31 Dec 2024 11:51:42 +0000 Subject: [PATCH 087/164] migration file --- packages/types/src/issues/issue.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index ae4a98d63f0..024ab5f6070 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -58,6 +58,7 @@ export type TIssue = TBaseIssue & { issue_link?: TIssueLink[]; issue_relation?: IssueRelation[]; issue_related?: IssueRelation[]; + custom_properties?: Record; // tempId is used for optimistic updates. It is not a part of the API response. tempId?: string; // sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response. From fb53aafe1ca725fede507a9b9d600f5945810df5 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 24 Feb 2025 21:29:38 +0530 Subject: [PATCH 088/164] web dev complete --- web/Dockerfile.web | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/Dockerfile.web b/web/Dockerfile.web index 8cbfd75a276..eb67cbbc486 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -71,7 +71,7 @@ COPY --from=installer /app/web/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=installer /app/web/.next/standalone ./ -COPY --from=installer /app/web/.next ./.next +COPY --from=installer /app/web/.next ./web/.next COPY --from=installer /app/web/public ./web/public ARG NEXT_PUBLIC_API_BASE_URL="" From 7157b8512c18baf47b3743c26827d35651c12bd2 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 25 Feb 2025 08:38:16 +0000 Subject: [PATCH 089/164] dev changes --- aio/nginx.conf | 3 + apiserver/plane/api/serializers/issue.py | 69 ++-- apiserver/plane/api/serializers/webhook.py | 135 ++++++++ apiserver/plane/api/urls/__init__.py | 4 +- apiserver/plane/api/urls/issue.py | 1 - apiserver/plane/api/urls/state.py | 4 +- apiserver/plane/api/urls/webhook.py | 31 ++ apiserver/plane/api/views/__init__.py | 7 +- apiserver/plane/api/views/base.py | 3 +- apiserver/plane/api/views/issue.py | 7 +- apiserver/plane/api/views/member.py | 37 +- apiserver/plane/api/views/search.py | 11 +- apiserver/plane/api/views/state.py | 1 + apiserver/plane/api/views/webhook.py | 129 +++++++ apiserver/plane/app/serializers/issue.py | 6 + apiserver/plane/app/urls/issue.py | 6 + apiserver/plane/app/urls/views.py | 9 + apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/base.py | 3 +- apiserver/plane/app/views/issue/base.py | 130 ++++++- apiserver/plane/app/views/view/base.py | 54 ++- .../provider/credentials/magic_code.py | 14 +- .../plane/authentication/views/app/magic.py | 12 +- ...e_customer_code_issue_hub_code_and_more.py | 43 +++ apiserver/plane/db/models/issue.py | 6 + apiserver/plane/settings/common.py | 2 +- apiserver/plane/settings/storage.py | 5 +- apiserver/plane/utils/constants.py | 13 + apiserver/plane/utils/grouper.py | 8 +- apiserver/plane/utils/issue_filters.py | 68 +++- deploy/selfhost/install.sh | 2 +- docker-compose-local.yml | 2 + docker-compose.yml | 319 +++++++++--------- packages/types/src/view-props.d.ts | 3 + .../components/issues/filters/selection.tsx | 2 +- space/next.config.js | 2 +- web/app/[workspaceSlug]/(projects)/header.tsx | 21 -- web/app/workspace-invitations/page.tsx | 1 - .../components/issues/custom-properties.tsx | 35 ++ web/core/components/issues/index.ts | 1 + .../issues/issue-detail/sidebar.tsx | 34 +- .../applied-filters/additional-properties.tsx | 32 ++ .../filters/applied-filters/filters-list.tsx | 9 + .../filters/applied-filters/index.ts | 1 + .../header/filters/additional-properties.tsx | 80 +++++ .../header/filters/custom-properties.tsx | 129 +++++++ .../header/filters/filters-selection.tsx | 29 +- .../filters/header/filters/index.ts | 1 + .../issues/peek-overview/properties.tsx | 25 +- .../workspace/sidebar/help-section.tsx | 10 +- .../workspace/sidebar/projects-list-item.tsx | 30 -- web/core/constants/analytics.ts | 4 + web/core/constants/issue.ts | 70 ++++ web/core/services/workspace.service.ts | 21 ++ .../helpers/issue-filter-helper.store.ts | 13 + .../store/issue/issue-details/issue.store.ts | 8 +- web/next.config.js | 6 +- 57 files changed, 1380 insertions(+), 332 deletions(-) create mode 100644 apiserver/plane/api/serializers/webhook.py create mode 100644 apiserver/plane/api/urls/webhook.py create mode 100644 apiserver/plane/api/views/webhook.py create mode 100644 apiserver/plane/db/migrations/0091_issue_customer_code_issue_hub_code_and_more.py create mode 100644 web/core/components/issues/custom-properties.tsx create mode 100644 web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx create mode 100644 web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx create mode 100644 web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx diff --git a/aio/nginx.conf b/aio/nginx.conf index 78ae00d28ce..ac485460e8b 100644 --- a/aio/nginx.conf +++ b/aio/nginx.conf @@ -3,6 +3,9 @@ events { http { sendfile on; + chunked_transfer_encoding on; + client_body_buffer_size 10M; + client_max_body_size 50M; server { listen 80; diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 31cf4412bab..a865f303ba2 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -92,7 +92,7 @@ class Meta: "description", "description_stripped", ] - + def validate(self, data): if ( data.get("start_date", None) is not None @@ -150,21 +150,23 @@ def validate(self, data): raise serializers.ValidationError( "Parent is not valid issue_id please pass a valid issue_id" ) - - if not is_uuid(data['created_by']): - if User.objects.filter(username=data['created_by']).exists(): - data['created_by']= User.objects.get(username=data['created_by']) - else: - user_data = { + if self.instance is None: + if not is_uuid(data.get('created_by')): + if User.objects.filter(username=data['created_by']).exists(): + data['created_by'] = User.objects.get(username=data['created_by']) + else: + user_data = { "email": data['created_by'] + '@plane-shipsy.com', "username": data['created_by'], - } - from plane.api.views import ProjectMemberAPIEndpoint - PMObj = ProjectMemberAPIEndpoint() - user = PMObj.create_user(user_data) - PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data) - PMObj.create_project_member(self.context.get("project_id"), user_data) - data['created_by'] = user + "role": 5, + "display_name": data['created_by'] + } + from plane.api.views import ProjectMemberAPIEndpoint + PMObj = ProjectMemberAPIEndpoint() + user = PMObj.create_user(user_data) + PMObj.create_workspace_member(self.context.get("workspace_id"), user_data) + PMObj.create_project_member(self.context.get("project_id"), user_data) + data['created_by'] = user print(data) return data @@ -463,7 +465,7 @@ class Meta: "issue", "updated_by", "created_at", - "updated_at", + "updated_at" ] exclude = [ "comment_stripped", @@ -472,29 +474,32 @@ class Meta: def validate(self, data): try: - if data.get("comment_html", None) is not None: + if data.get("comment_html", None) is not None: + print("Am Here") + parsed = html.fromstring(data["comment_html"]) parsed_str = html.tostring(parsed, encoding="unicode") + print(html.tostring(parsed, encoding="unicode")) data["comment_html"] = parsed_str - # if not is_uuid(data['created_by']): - # if User.objects.filter(username=data['created_by']).exists(): - # data['created_by']= User.objects.get(username=data['created_by']) - # else: - # user_data = { - # "email": data['created_by'] + '@plane-shipsy.com', - # "username": data['created_by'], - # } - # from plane.api.views import ProjectMemberAPIEndpoint - # PMObj = ProjectMemberAPIEndpoint() - # user = PMObj.create_user(user_data) - # PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data) - # PMObj.create_project_member(self.context.get("project_id"), user_data) - # data['created_by'] = user + ## if not data.get('created_by', None): + ## if User.objects.filter(username=data['created_by']).exists(): + ## data['created_by'] = User.objects.get(username=data['created_by']) + ## else: +## user_data = { + ## "email": data['created_by'] + '@plane-shipsy.com', + ## "username": data['created_by'], + ## } + ## from plane.api.views import ProjectMemberAPIEndpoint + ## PMObj = ProjectMemberAPIEndpoint() + ## user = PMObj.create_user(user_data) + ## PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data) + ## PMObj.create_project_member(self.context.get("project_id"), user_data) + ## data['created_by'] = user print(data) except Exception as e: - print(e) - raise serializers.ValidationError("Invalid HTML passed") + print(e.__cause__) + raise serializers.ValidationError(e) return data diff --git a/apiserver/plane/api/serializers/webhook.py b/apiserver/plane/api/serializers/webhook.py new file mode 100644 index 00000000000..918d1705c1a --- /dev/null +++ b/apiserver/plane/api/serializers/webhook.py @@ -0,0 +1,135 @@ +# Python imports +import socket +import ipaddress +from urllib.parse import urlparse + +# Third party imports +from rest_framework import serializers + +# Module imports +from .base import DynamicBaseSerializer +from plane.db.models import Webhook, WebhookLog +from plane.db.models.webhook import validate_domain, validate_schema + + +class WebhookSerializer(DynamicBaseSerializer): + url = serializers.URLField(validators=[validate_schema, validate_domain]) + + def create(self, validated_data): + url = validated_data.get("url", None) + + # Extract the hostname from the URL + hostname = urlparse(url).hostname + if not hostname: + raise serializers.ValidationError( + {"url": "Invalid URL: No hostname found."} + ) + + # Resolve the hostname to IP addresses + try: + ip_addresses = socket.getaddrinfo(hostname, None) + except socket.gaierror: + raise serializers.ValidationError( + {"url": "Hostname could not be resolved."} + ) + + if not ip_addresses: + raise serializers.ValidationError( + {"url": "No IP addresses found for the hostname."} + ) + + for addr in ip_addresses: + ip = ipaddress.ip_address(addr[4][0]) + if ip.is_loopback: + raise serializers.ValidationError( + {"url": "URL resolves to a blocked IP address."} + ) + + # Additional validation for multiple request domains and their subdomains + request = self.context.get("request") + disallowed_domains = [ + "plane.so", + ] # Add your disallowed domains here + if request: + request_host = request.get_host().split(":")[ + 0 + ] # Remove port if present + disallowed_domains.append(request_host) + + # Check if hostname is a subdomain or exact match of any disallowed domain + if any( + hostname == domain or hostname.endswith("." + domain) + for domain in disallowed_domains + ): + raise serializers.ValidationError( + {"url": "URL domain or its subdomain is not allowed."} + ) + + return Webhook.objects.create(**validated_data) + + def update(self, instance, validated_data): + url = validated_data.get("url", None) + if url: + # Extract the hostname from the URL + hostname = urlparse(url).hostname + if not hostname: + raise serializers.ValidationError( + {"url": "Invalid URL: No hostname found."} + ) + + # Resolve the hostname to IP addresses + try: + ip_addresses = socket.getaddrinfo(hostname, None) + except socket.gaierror: + raise serializers.ValidationError( + {"url": "Hostname could not be resolved."} + ) + + if not ip_addresses: + raise serializers.ValidationError( + {"url": "No IP addresses found for the hostname."} + ) + + for addr in ip_addresses: + ip = ipaddress.ip_address(addr[4][0]) + if ip.is_loopback: + raise serializers.ValidationError( + {"url": "URL resolves to a blocked IP address."} + ) + + # Additional validation for multiple request domains and their subdomains + request = self.context.get("request") + disallowed_domains = [ + "plane.so", + ] # Add your disallowed domains here + if request: + request_host = request.get_host().split(":")[ + 0 + ] # Remove port if present + disallowed_domains.append(request_host) + + # Check if hostname is a subdomain or exact match of any disallowed domain + if any( + hostname == domain or hostname.endswith("." + domain) + for domain in disallowed_domains + ): + raise serializers.ValidationError( + {"url": "URL domain or its subdomain is not allowed."} + ) + + return super().update(instance, validated_data) + + class Meta: + model = Webhook + fields = "__all__" + read_only_fields = [ + "workspace", + "secret_key", + ] + + +class WebhookLogSerializer(DynamicBaseSerializer): + class Meta: + model = WebhookLog + fields = "__all__" + read_only_fields = ["workspace", "webhook"] diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index 62659eabe56..603ee239f2a 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -7,6 +7,7 @@ from .inbox import urlpatterns as inbox_patterns from .member import urlpatterns as member_patterns from .search import urlpatterns as search_patters +from .webhook import urlpatterns as webhook_patters urlpatterns = [ *project_patterns, @@ -17,5 +18,6 @@ *inbox_patterns, *member_patterns, *issue_type_patterns, - *search_patters + *search_patters, + *webhook_patters ] diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index d0a7fbf9081..afaec678f00 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -1,5 +1,4 @@ from django.urls import path - from plane.api.views import ( IssueAPIEndpoint, LabelAPIEndpoint, diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/api/urls/state.py index b03f386e648..e18380fbc78 100644 --- a/apiserver/plane/api/urls/state.py +++ b/apiserver/plane/api/urls/state.py @@ -4,12 +4,12 @@ urlpatterns = [ path( - "workspaces//projects//states/", + "workspaces//projects//states/", StateAPIEndpoint.as_view(), name="states", ), path( - "workspaces//projects//states//", + "workspaces//projects//states//", StateAPIEndpoint.as_view(), name="states", ), diff --git a/apiserver/plane/api/urls/webhook.py b/apiserver/plane/api/urls/webhook.py new file mode 100644 index 00000000000..74a8da75965 --- /dev/null +++ b/apiserver/plane/api/urls/webhook.py @@ -0,0 +1,31 @@ +from django.urls import path + +from plane.api.views import ( + WebhookEndpoint, + WebhookLogsEndpoint, + WebhookSecretRegenerateEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//webhooks/", + WebhookEndpoint.as_view(), + name="webhooks", + ), + path( + "workspaces//webhooks//", + WebhookEndpoint.as_view(), + name="webhooks", + ), + path( + "workspaces//webhooks//regenerate/", + WebhookSecretRegenerateEndpoint.as_view(), + name="webhooks", + ), + path( + "workspaces//webhook-logs//", + WebhookLogsEndpoint.as_view(), + name="webhooks", + ), +] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 0a61d52f4bb..d6b77e99ca2 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -33,4 +33,9 @@ from .inbox import InboxIssueAPIEndpoint -from .search import GlobalSearchEndpoint \ No newline at end of file +from .search import GlobalSearchEndpoint + +from .webhook import ( + WebhookEndpoint, WebhookLogsEndpoint, + WebhookSecretRegenerateEndpoint +) \ No newline at end of file diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index dea888ed355..6dbf4d52f13 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -116,7 +116,6 @@ def dispatch(self, request, *args, **kwargs): response = super().dispatch(request, *args, **kwargs) if settings.DEBUG: from django.db import connection - print( f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" ) @@ -133,7 +132,7 @@ def check_kwargs(self, kwargs): project_id = self.kwargs.get("project_id", None) if project_id == "DEFAULT": project = Project.objects.filter( - name='default', workspace__slug=kwargs['slug'] + name='TICKET', workspace__slug=kwargs['slug'] ).first() if project: kwargs['project_id'] = project.id diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 156d2145f78..227685acae9 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -118,7 +118,6 @@ def get( status=status.HTTP_200_OK, ) - class IssueAPIEndpoint(BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, @@ -148,6 +147,7 @@ def get_queryset(self): if base_filter: query = query.filter(base_filter) + print(query) return (query .filter(**filters) .select_related("project") @@ -158,7 +158,7 @@ def get_queryset(self): .prefetch_related("labels") .order_by(self.kwargs.get("order_by", "-created_at"))).distinct() - + def get(self, request, slug, project_id, pk=None): external_id = request.GET.get("external_id") external_source = request.GET.get("external_source") @@ -925,7 +925,9 @@ def post(self, request, slug, project_id, issue_id): status=status.HTTP_409_CONFLICT, ) + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): serializer.save( project_id=project_id, @@ -1108,7 +1110,6 @@ def post(self, request, slug, project_id, issue_id): ) if serializer.is_valid(): - # import pdb;pdb.set_trace() serializer.save(project_id=project_id, issue_id=issue_id) issue_activity.delay( type="attachment.activity.created", diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index 877d522b7f8..aee6cbc6af3 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -19,6 +19,7 @@ Project, WorkspaceMember, ProjectMember, + Profile ) from plane.app.permissions import ( @@ -63,17 +64,15 @@ def post(self, request, slug, project_id): # ------------------- Validation ------------------- if ( request.data.get("email") is None - or request.data.get("display_name") is None ): return Response( { - "error": "Expected email, display_name, workspace_slug, project_id, one or more of the fields are missing." + "error": "Expected email, workspace_slug, project_id, one or more of the fields are missing." }, status=status.HTTP_400_BAD_REQUEST, ) email = request.data.get("email") - try: validate_email(email) except ValidationError: @@ -92,7 +91,7 @@ def post(self, request, slug, project_id): ) # Check if user exists - user = User.objects.filter(email=email).first() + user = User.objects.filter(email=email.lower()).first() workspace_member = None project_member = None @@ -115,20 +114,32 @@ def post(self, request, slug, project_id): ) # If user does not exist, create the user - if not user: - user_data = { + user_data = { "email": email, "display_name": request.data.get("display_name"), "first_name": request.data.get("first_name", ""), "last_name": request.data.get("last_name", ""), + "role": request.data.get("role", 15), } + if not user: user = self.create_user(user_data) + profile, _ = Profile.objects.get_or_create(user=user) + profile.last_workspace_id = workspace.id + profile.onboarding_step.update({ + 'profile_complete': True, + 'workspace_join': True + }) + profile.is_tour_completed = True + profile.is_onboarded = True + profile.company_name = workspace.name + profile.save() if not workspace_member: - self.create_workspace_member(workspace.id, user) + self.create_workspace_member(workspace.id, user, role=user_data.get("role")) + if not project_member: - self.create_project_member(project.id, user) + self.create_project_member(project.id, user, role=user_data.get("role")) # Serialize the user and return the response @@ -144,26 +155,26 @@ def create_user(self, data): last_name=data.get("last_name", ""), username=data.get("username", uuid.uuid4().hex), password=make_password(data.get("username", uuid.uuid4().hex)), - is_password_autoset=True, + is_password_autoset=False, is_active=False, ) user.save() return user # Create a workspace member for the user if not already a member - def create_workspace_member(self, workspace_id, user): + def create_workspace_member(self, workspace_id, user, role=15): workspace_member = WorkspaceMember.objects.create( workspace_id=workspace_id, member=user, - role=5 + role=role ) workspace_member.save() - def create_project_member(self, project_id, user): + def create_project_member(self, project_id, user, role=15): # Create a project member for the user if not already a member project_member = ProjectMember.objects.create( project_id=project_id, member=user, - role=5 + role=role ) project_member.save() \ No newline at end of file diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index a463708be79..b514937f3e2 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -64,7 +64,7 @@ def filter_projects(self, query, slug, project_id, workspace_search): .values("name", "id", "identifier", "workspace__slug") ) - def filter_issues(self, query, slug, project_id, workspace_search): + def filter_issues(self, query, slug, project_id, workspace_search, created_by_username=None): fields = ["name", "sequence_id", "project__identifier"] q = Q() for field in fields: @@ -81,8 +81,10 @@ def filter_issues(self, query, slug, project_id, workspace_search): project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, project__archived_at__isnull=True, - workspace__slug=slug, + workspace__slug=slug ) + if created_by_username: + issues = issues.filter(created_by__username=created_by_username) if workspace_search == "false" and project_id: issues = issues.filter(project_id=project_id) @@ -93,7 +95,7 @@ def filter_issues(self, query, slug, project_id, workspace_search): "sequence_id", "project__identifier", "project_id", - "workspace__slug", + "workspace__slug" )[:100] def filter_cycles(self, query, slug, project_id, workspace_search): @@ -230,6 +232,7 @@ def get(self, request, slug): workspace_search = request.query_params.get( "workspace_search", "false" ) + created_by_username = request.query_params.get("created_by_username", False) project_id = request.query_params.get("project_id", False) if not query: return Response( @@ -261,5 +264,5 @@ def get(self, request, slug): for model in MODELS_MAPPER.keys(): func = MODELS_MAPPER.get(model, None) - results[model] = func(query, slug, project_id, workspace_search) + results[model] = func(query, slug, project_id, workspace_search, created_by_username=created_by_username) return Response({"results": results}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index dd239754cb8..c39e6f34ebb 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -21,6 +21,7 @@ class StateAPIEndpoint(BaseAPIView): ] def get_queryset(self): + print(self.kwargs.get("project_id")) return ( State.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) diff --git a/apiserver/plane/api/views/webhook.py b/apiserver/plane/api/views/webhook.py new file mode 100644 index 00000000000..0e281a03a33 --- /dev/null +++ b/apiserver/plane/api/views/webhook.py @@ -0,0 +1,129 @@ +# Django imports +from django.db import IntegrityError + +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.db.models import Webhook, WebhookLog, Workspace +from plane.db.models.webhook import generate_token +from .base import BaseAPIView +from plane.app.permissions import allow_permission, ROLE +from plane.app.serializers import WebhookSerializer, WebhookLogSerializer + + +class WebhookEndpoint(BaseAPIView): + + def post(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + try: + serializer = WebhookSerializer( + data=request.data, context={"request": request} + ) + if serializer.is_valid(): + serializer.save(workspace_id=workspace.id) + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"error": "URL already exists for the workspace"}, + status=status.HTTP_410_GONE, + ) + raise IntegrityError + + def get(self, request, slug, pk=None): + if pk is None: + webhooks = Webhook.objects.filter(workspace__slug=slug) + serializer = WebhookSerializer( + webhooks, + fields=( + "id", + "url", + "is_active", + "created_at", + "updated_at", + "project", + "issue", + "cycle", + "module", + "issue_comment", + ), + many=True, + ) + return Response(serializer.data, status=status.HTTP_200_OK) + else: + webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) + serializer = WebhookSerializer( + webhook, + fields=( + "id", + "url", + "is_active", + "created_at", + "updated_at", + "project", + "issue", + "cycle", + "module", + "issue_comment", + ), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + def patch(self, request, slug, pk): + webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) + serializer = WebhookSerializer( + webhook, + data=request.data, + context={request: request}, + partial=True, + fields=( + "id", + "url", + "is_active", + "created_at", + "updated_at", + "project", + "issue", + "cycle", + "module", + "issue_comment", + ), + ) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, pk): + webhook = Webhook.objects.get(pk=pk, workspace__slug=slug) + webhook.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class WebhookSecretRegenerateEndpoint(BaseAPIView): + + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") + def post(self, request, slug, pk): + webhook = Webhook.objects.get(workspace__slug=slug, pk=pk) + webhook.secret_key = generate_token() + webhook.save() + serializer = WebhookSerializer(webhook) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WebhookLogsEndpoint(BaseAPIView): + + @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") + def get(self, request, slug, webhook_id): + webhook_logs = WebhookLog.objects.filter( + workspace__slug=slug, webhook_id=webhook_id + ) + serializer = WebhookLogSerializer(webhook_logs, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 83f5aeea3cb..2bc4afea4ee 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -708,6 +708,12 @@ class Meta: "sort_order", "completed_at", "estimate_point", + "hub_code", + "customer_code", + "reference_number", + "trip_reference_number", + "vendor_code", + "worker_code", "priority", "start_date", "target_date", diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index e8ad4408dfb..47b34954f06 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -24,6 +24,7 @@ IssueDetailEndpoint, IssueAttachmentV2Endpoint, IssueBulkUpdateDateEndpoint, + SearchAPIEndpoint ) urlpatterns = [ @@ -320,4 +321,9 @@ IssueBulkUpdateDateEndpoint.as_view(), name="project-issue-dates", ), + path( + "workspaces//projects//search", + SearchAPIEndpoint.as_view(), + name="key-codes", + ), ] diff --git a/apiserver/plane/app/urls/views.py b/apiserver/plane/app/urls/views.py index a2f8e2ac88b..985da5d3dd0 100644 --- a/apiserver/plane/app/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -63,6 +63,15 @@ ), name="global-view-issues", ), + path( + "workspaces//custom-properties/", + WorkspaceViewIssuesViewSet.as_view( + { + "get": "listCustomProperty", + } + ), + name="global-view-issues", + ), path( "workspaces//projects//user-favorite-views/", IssueViewFavoriteViewSet.as_view( diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 1f94cb7ac9a..e6ca8f8634f 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -132,6 +132,7 @@ IssuePaginatedViewSet, IssueDetailEndpoint, IssueBulkUpdateDateEndpoint, + SearchAPIEndpoint ) from .issue.activity import ( diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index 45488b64e9a..10f5c205135 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -120,7 +120,8 @@ def dispatch(self, request, *args, **kwargs): if settings.DEBUG: from django.db import connection - + if len(connection.queries) > 20: + print(connection.queries) print( f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" ) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 0c9db7597bb..8fa4c8197d8 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -5,6 +5,7 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.core.serializers.json import DjangoJSONEncoder +from rest_framework.pagination import PageNumberPagination from django.db.models import ( Exists, F, @@ -34,6 +35,7 @@ IssueDetailSerializer, IssueUserPropertySerializer, IssueSerializer, + BaseSerializer, ) from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( @@ -46,6 +48,7 @@ Project, ProjectMember, CycleIssue, + IssueCustomProperty, ) from plane.utils.grouper import ( issue_group_values, @@ -54,6 +57,7 @@ ) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset +from plane.utils.constants import ALLOWED_CUSTOM_PROPERTY_WORKSPACE_MAP from plane.utils.paginator import ( GroupedOffsetPaginator, SubGroupedOffsetPaginator, @@ -64,6 +68,24 @@ from plane.utils.global_paginator import paginate from plane.bgtasks.webhook_task import model_activity +from plane.app.permissions import ( + ProjectEntityPermission, + ProjectLitePermission, + ProjectMemberPermission, +) + +class IssueCustomPropertySerializer(BaseSerializer): + class Meta: + model = IssueCustomProperty + fields = ["key", "value", "issue_type_custom_property"] + read_only_fields = [ + "id", + "issue", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] class IssueListEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @@ -123,6 +145,7 @@ def get(self, request, slug, project_id): order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = queryset.filter(**filters) + # Issue queryset issue_queryset, _ = order_issue_queryset( issue_queryset=issue_queryset, @@ -209,7 +232,8 @@ def get_serializer_class(self): "workspace__id", ] - def get_queryset(self): + def get_queryset(self, filters={}): + custom_properties = filters.get("custom_properties", {}) return ( Issue.issue_objects.filter( project_id=self.kwargs.get("project_id") @@ -247,6 +271,18 @@ def get_queryset(self): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .filter( + *[ + Q( + id__in=IssueCustomProperty.objects.filter( + issue_id=OuterRef("id"), + key=group_key, + value__in=values + ).values("issue_id") + ) + for group_key, values in custom_properties.items() + ] + ) ).distinct() @method_decorator(gzip_page) @@ -260,9 +296,11 @@ def list(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) filters = issue_filters(request.query_params, "GET") + filtersWithoutCustomProperties = filters.copy() + filtersWithoutCustomProperties.pop('custom_properties', None) order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = self.get_queryset().filter(**filters, **extra_filters) + issue_queryset = self.get_queryset(filters).filter(**filtersWithoutCustomProperties, **extra_filters) # Custom ordering for priority and state # Issue queryset @@ -428,6 +466,12 @@ def create(self, request, slug, project_id): "sort_order", "completed_at", "estimate_point", + "hub_code", + "customer_code", + "reference_number", + "trip_reference_number", + "vendor_code", + "worker_code", "priority", "start_date", "target_date", @@ -1078,6 +1122,38 @@ def get(self, request, slug, project_id): ).data, ) +# class HubViewSet(BaseAPIView): +# @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) +# def list(self, request, slug, project_id): +# hubs = Hub.objects.filter(workspace__slug=slug, project_id=project_id) + +# # Extract query parameters +# hub_code = request.GET.get("hub_code", "").strip() +# reference_number = request.GET.get("reference_number", "").strip() +# trip_reference_number = request.GET.get("trip_reference_number", "").strip() +# vendor_code = request.GET.get("vendor_code", "").strip() +# worker_code = request.GET.get("worker_code", "").strip() + +# # Apply filtering dynamically +# filter_params = {} +# if hub_code: +# filter_params["hub_code__icontains"] = hub_code +# if reference_number: +# filter_params["reference_number__icontains"] = reference_number +# if trip_reference_number: +# filter_params["trip_reference_number__icontains"] = trip_reference_number +# if vendor_code: +# filter_params["vendor_code__icontains"] = vendor_code +# if worker_code: +# filter_params["worker_code__icontains"] = worker_code + +# hubs = hubs.filter(**filter_params) + +# # Serialize the results +# hub_data = HubSerializer(hubs, many=True).data +# return Response(hub_data, status=status.HTTP_200_OK) + + class IssueBulkUpdateDateEndpoint(BaseAPIView): @@ -1170,3 +1246,53 @@ def post(self, request, slug, project_id): {"message": "Issues updated successfully"}, status=status.HTTP_200_OK, ) + +class SearchAPIEndpoint(BaseAPIView): + """ + API to fetch hub codes for a specific workspace and project. + """ + model = Issue + webhook_event = "issue" + permission_classes = [ProjectEntityPermission] + def get(self, request, slug, project_id): + """ + Fetch unique values for hub_code, worker_code, reference_number, trip_reference_number, + customer_code, or vendor_code based on the requested query parameters. + """ + allowed_fields = ["hub_code", "worker_code", "reference_number", "trip_reference_number", "customer_code", "vendor_code"] + + field = request.GET.get("field") # Get the single field value + + if not field: # Ensure that 'field' is provided in the request + return Response( + {"error": "Missing 'field' parameter. Allowed values: " + ", ".join(allowed_fields)}, + status=status.HTTP_400_BAD_REQUEST + ) + + if field not in allowed_fields: # Validate the field + return Response( + {"error": f"Invalid field. Allowed values: {', '.join(allowed_fields)}"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Fetch values dynamically based on the requested field + values = Issue.objects.filter( + workspace__slug=slug, project_id=project_id + ).values_list(field, flat=True) + + unique_values = list(set(filter(None, values))) # Remove duplicates and nulls + + paginator = PageNumberPagination() + paginator.page_size = int(request.GET.get("limit", 10)) # Default limit = 10 + paginated_values = paginator.paginate_queryset(unique_values, request) + + # Custom pagination response + response_data = { + "total_results": len(unique_values), + "page": paginator.page.number, + "limit": paginator.page.paginator.per_page, + "total_pages": paginator.page.paginator.num_pages, + "data": paginated_values + } + + return Response(response_data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index e999dc32926..f82199cae23 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -1,5 +1,6 @@ # Django imports from django.contrib.postgres.aggregates import ArrayAgg +from django.db.models import Count from django.contrib.postgres.fields import ArrayField from django.db.models import ( Exists, @@ -27,6 +28,7 @@ ) from plane.app.serializers import ( IssueViewSerializer, + BaseSerializer, ) from plane.db.models import ( Issue, @@ -38,6 +40,7 @@ ProjectMember, Project, CycleIssue, + IssueCustomProperty ) from plane.utils.grouper import ( issue_group_values, @@ -45,6 +48,7 @@ issue_queryset_grouper, ) from plane.utils.issue_filters import issue_filters +from plane.utils.constants import ALLOWED_CUSTOM_PROPERTY_WORKSPACE_MAP from plane.utils.order_queryset import order_issue_queryset from plane.utils.paginator import ( GroupedOffsetPaginator, @@ -56,6 +60,18 @@ UserFavorite, ) +class IssueCustomPropertySerializer(BaseSerializer): + class Meta: + model = IssueCustomProperty + fields = ["key", "value", "issue_type_custom_property"] + read_only_fields = [ + "id", + "issue", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] class WorkspaceViewViewSet(BaseViewSet): serializer_class = IssueViewSerializer @@ -190,7 +206,8 @@ def destroy(self, request, slug, pk): class WorkspaceViewIssuesViewSet(BaseViewSet): - def get_queryset(self): + def get_queryset(self, filters): + custom_properties = filters.get("custom_properties", {}) return ( Issue.issue_objects.annotate( sub_issues_count=Issue.issue_objects.filter( @@ -274,6 +291,18 @@ def get_queryset(self): Value([], output_field=ArrayField(UUIDField())), ), ) + .filter( + *[ + Q( + id__in=IssueCustomProperty.objects.filter( + issue_id=OuterRef("id"), + key=group_key, + value__in=values + ).values("issue_id") + ) + for group_key, values in custom_properties.items() + ] + ) ) @method_decorator(gzip_page) @@ -281,13 +310,32 @@ def get_queryset(self): allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE", ) + + def listCustomProperty(self, request, slug): + customPropertyAllowedKeys = ALLOWED_CUSTOM_PROPERTY_WORKSPACE_MAP.get(slug, []) + custom_properties = IssueCustomProperty.objects.filter(key__in=customPropertyAllowedKeys) + serializer = IssueCustomPropertySerializer(custom_properties, many=True) + + groupedCustomProperties = {} + for item in serializer.data: + key = item.get("key") + value = item.get("value") + if value is None or key not in customPropertyAllowedKeys: + continue + groupedCustomProperties.setdefault(key, set()).add(value) + + groupedUniqueCustomProperties = {key: list(values) for key, values in groupedCustomProperties.items()} + return Response(groupedUniqueCustomProperties, status=status.HTTP_200_OK) + def list(self, request, slug): filters = issue_filters(request.query_params, "GET") + filtersWithoutCustomProperties = filters.copy() + filtersWithoutCustomProperties.pop('custom_properties', None) order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = ( - self.get_queryset() - .filter(**filters) + self.get_queryset(filters) + .filter(**filtersWithoutCustomProperties) .annotate( cycle_id=Subquery( CycleIssue.objects.filter( diff --git a/apiserver/plane/authentication/provider/credentials/magic_code.py b/apiserver/plane/authentication/provider/credentials/magic_code.py index 390d09d90b6..a531ca57043 100644 --- a/apiserver/plane/authentication/provider/credentials/magic_code.py +++ b/apiserver/plane/authentication/provider/credentials/magic_code.py @@ -83,12 +83,10 @@ def initiate(self): # Check if the key already exists in python if ri.exists(key): data = json.loads(ri.get(key)) - current_attempt = data["current_attempt"] + 1 - + email = str(self.key).replace("magic_", "", 1) + username = email.replace("@plane-shipsy.com", "", 1) if data["current_attempt"] > 2: - email = str(self.key).replace("magic_", "", 1) - username = email.replace ("@plane-shipsy.com", "", 1) if User.objects.filter(email=email).exists(): raise AuthenticationException( error_code=AUTHENTICATION_ERROR_CODES[ @@ -112,10 +110,11 @@ def initiate(self): "token": token, "username": username } - expiry = 600 + expiry = 30 + print(key, value) ri.set(key, json.dumps(value), ex=expiry) else: - username = self.key.replace ("@plane-shipsy.com", "", 1) + username = self.key.replace("@plane-shipsy.com", "", 1) value = { "current_attempt": 0, "email": self.key, @@ -129,6 +128,9 @@ def initiate(self): def set_user_data(self): ri = redis_instance() + keys = ri.keys('*') + print("All keys in redis:", keys) + print("Key to be checked:", self.key) if ri.exists(self.key): data = json.loads(ri.get(self.key)) token = data["token"] diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index f8d351af835..8002e34acf1 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -85,7 +85,7 @@ def post(self, request): ) -class MagicSignInEndpoint(APIView): +class MagicSignInEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] @@ -95,7 +95,6 @@ class MagicSignInEndpoint(APIView): def add_user_to_workspace(self, user, workspace_slug): admin_user = User.objects.filter(is_superuser=True).first() workspace, base_project = self.get_workspace(workspace_slug, admin_user) - print(workspace, base_project, user) self.add_to_workspace(workspace, user) self.add_to_project(base_project, user) self.add_to_project(base_project, admin_user) @@ -108,7 +107,8 @@ def add_to_workspace(self, workspace, user): ).first() if not workspace_member: workspace_member = WorkspaceMember.objects.create( - workspace=workspace, member=user, is_active=True + workspace=workspace, member=user, is_active=True, + role=15 ) try: user.profile.last_workspace_id = workspace.id @@ -118,7 +118,9 @@ def add_to_workspace(self, workspace, user): }) user.profile.is_tour_completed = True user.profile.is_onboarded = True + user.is_password_autoset = True user.profile.company_name = workspace.name + user.save() user.profile.save() except Exception as e: print(e) @@ -154,9 +156,9 @@ def get_workspace(self, workspace_slug, admin_user): def get_or_create_project(self, workspace, user): default_project_dict ={ - 'identifier': 'DEFAULT', + 'identifier': 'TICKET', 'workspace_id': workspace.id, - 'name': 'default' + 'name': 'TICKET' } project = Project.objects.filter(**default_project_dict).first() if project: diff --git a/apiserver/plane/db/migrations/0091_issue_customer_code_issue_hub_code_and_more.py b/apiserver/plane/db/migrations/0091_issue_customer_code_issue_hub_code_and_more.py new file mode 100644 index 00000000000..d3d42273346 --- /dev/null +++ b/apiserver/plane/db/migrations/0091_issue_customer_code_issue_hub_code_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.14 on 2025-02-18 10:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0090_rename_issue_type_custom_proerty_issuecustomproperty_issue_type_custom_property'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='customer_code', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='hub_code', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='reference_number', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='trip_reference_number', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='vendor_code', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issue', + name='worker_code', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index d154a4fc881..2bb3e371001 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -133,6 +133,12 @@ class Issue(ProjectBaseModel): null=True, blank=True, ) + hub_code = models.CharField(max_length=255, blank=True, null=True) + vendor_code = models.CharField(max_length=255, blank=True, null=True) + customer_code = models.CharField(max_length=255, blank=True, null=True) + worker_code = models.CharField(max_length=255, blank=True, null=True) + reference_number = models.CharField(max_length=255, blank=True, null=True) + trip_reference_number = models.CharField(max_length=255, blank=True, null=True) name = models.CharField(max_length=255, verbose_name="Issue Name") description = models.JSONField(blank=True, default=dict) description_html = models.TextField(blank=True, default="

") diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 11666ff40a1..e0975d37f9c 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -378,7 +378,7 @@ # CSRF cookies CSRF_COOKIE_SECURE = secure_origins -CSRF_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = False CSRF_TRUSTED_ORIGINS = cors_allowed_origins CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure" diff --git a/apiserver/plane/settings/storage.py b/apiserver/plane/settings/storage.py index 445bd4c20b8..72057c79ac9 100644 --- a/apiserver/plane/settings/storage.py +++ b/apiserver/plane/settings/storage.py @@ -38,7 +38,6 @@ def __init__(self, request=None): self.aws_s3_endpoint_url = os.environ.get( "AWS_S3_ENDPOINT_URL" ) or os.environ.get("MINIO_ENDPOINT_URL") - if os.environ.get("USE_MINIO") == "1": # Create an S3 client for MinIO self.s3_client = boto3.client( @@ -47,7 +46,7 @@ def __init__(self, request=None): aws_secret_access_key=self.aws_secret_access_key, region_name=self.aws_region, endpoint_url=( - f"{request.scheme}://{request.get_host()}" + f"https://{request.get_host()}" if request else self.aws_s3_endpoint_url ), @@ -89,6 +88,7 @@ def generate_presigned_post( # Generate the presigned POST URL try: + # Generate a presigned URL for the S3 object response = self.s3_client.generate_presigned_post( Bucket=self.aws_storage_bucket_name, @@ -97,6 +97,7 @@ def generate_presigned_post( Conditions=conditions, ExpiresIn=expiration, ) + # Handle errors except ClientError as e: print(f"Error generating presigned POST URL: {e}") diff --git a/apiserver/plane/utils/constants.py b/apiserver/plane/utils/constants.py index a4d1e1ea662..36f411eaba7 100644 --- a/apiserver/plane/utils/constants.py +++ b/apiserver/plane/utils/constants.py @@ -33,3 +33,16 @@ "signup", "config", ] + +ALLOWED_CUSTOM_PROPERTY_WORKSPACE_MAP = { + "heineken": [ + "Hub Code", + "Customer Name", + "Vendor Code", + "Vendor Name", + "Customer Code", + "Consignment Number", + "Trip Number", + "Hub Name" + ] +} \ No newline at end of file diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index 4cf97d56566..f064036eb8e 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -93,7 +93,13 @@ def issue_on_results(issues, group_by, sub_group_by): "link_count", "is_draft", "archived_at", - "state__group" + "state__group", + "hub_code", + "vendor_code", + "customer_code", + "worker_code", + "reference_number", + "trip_reference_number" ] if group_by in FIELD_MAPPER: diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 2826dece7b6..d981c343745 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -268,7 +268,9 @@ def filter_created_by(params, issue_filter, method, prefix=""): ] if "None" in created_bys: issue_filter[f"{prefix}created_by__isnull"] = True + print(created_bys) created_bys = filter_valid_uuids(created_bys) + if len(created_bys) and "" not in created_bys: issue_filter[f"{prefix}created_by__in"] = created_bys else: @@ -281,6 +283,25 @@ def filter_created_by(params, issue_filter, method, prefix=""): return issue_filter +def filter_created_by_username(params, issue_filter, method, prefix=""): + if method == "GET": + created_bys = [ + item + for item in params.get("created_by_username").split(",") + if item != "null" + ] + if len(created_bys) and "" not in created_bys: + issue_filter[f"{prefix}created_by__username__in"] = created_bys + # else: + # if ( + # params.get("created_by", None) + # and len(params.get("created_by")) + # and params.get("created_by") != "null" + # ): + # issue_filter[f"{prefix}created_by__in"] = params.get("created_by") + return issue_filter + + def filter_name(params, issue_filter, method, prefix=""): if params.get("name", "") != "": issue_filter[f"{prefix}name__icontains"] = params.get("name") @@ -546,19 +567,47 @@ def filter_custom_properties(params, issue_filter, method, prefix=""): for item in params.get("custom_properties").split(",") if item != "null" ] - query = Q() - for row in custom_properties: - key, value = row.split(":") - query &= Q( - Q(custom_properties__project_custom_property_id=key) & - Q(custom_properties__value=value) - ) + + # query = Q() + # for row in custom_properties: + # key, value = row.split(":") + # query &= Q( + # Q(custom_properties__project_custom_property_id=key) & + # Q(custom_properties__value=value) + # ) - issue_filter['base'] = query + # issue_filter['base'] = query + groupedCustomProperties = {} + for row in custom_properties: + key, value = row.split(':') + if key not in groupedCustomProperties: + groupedCustomProperties[key] = [] + groupedCustomProperties[key].append(value) + + issue_filter['custom_properties'] = groupedCustomProperties + print(issue_filter) return issue_filter +def filter_character_fields(params, issue_filter, method, prefix=""): + character_fields = [ + "hub_code", + "worker_code", + "vendor_code", + "trip_reference_number", + "reference_number", + "customer_code" + ] + + for field in character_fields: + value = params.get(field, "").strip() # Remove spaces to avoid blank entries + if value: + issue_filter[f"{prefix}{field}__iexact"] = value # Case-insensitive filtering + + return issue_filter + + def issue_filters(query_params, method, prefix=""): issue_filter = {} @@ -572,6 +621,7 @@ def issue_filters(query_params, method, prefix=""): "assignees": filter_assignees, "mentions": filter_mentions, "created_by": filter_created_by, + "created_by_username": filter_created_by_username, "logged_by": filter_logged_by, "name": filter_name, "created_at": filter_created_at, @@ -594,4 +644,6 @@ def issue_filters(query_params, method, prefix=""): if key in query_params: func = value func(query_params, issue_filter, method, prefix) + + filter_character_fields(query_params, issue_filter, method, prefix) return issue_filter diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 08cd4d9169e..185a1a05600 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -182,7 +182,7 @@ function buildYourOwnImage(){ local PLANE_TEMP_CODE_DIR=~/tmp/plane rm -rf $PLANE_TEMP_CODE_DIR mkdir -p $PLANE_TEMP_CODE_DIR - REPO=https://github.com/makeplane/plane.git + REPO=https://github.com/shipsy/plane.git git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1 cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml" diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 067aaead89e..473fc8a4fc9 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -20,6 +20,8 @@ services: environment: MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} + ports: + - 9090:9090 plane-db: image: postgres:15.7-alpine diff --git a/docker-compose.yml b/docker-compose.yml index 861fa9dd89f..a15bec98aa1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,174 +7,177 @@ services: args: DOCKER_BUILDKIT: 1 restart: always - command: node web/server.js web - depends_on: - - api + volumes: + - ./web:/app/web + - ./output:/app/out + command: tail -f #node web/server.js web + # depends_on: + # - api - admin: - container_name: admin - build: - context: . - dockerfile: ./admin/Dockerfile.admin - args: - DOCKER_BUILDKIT: 1 - restart: always - command: node admin/server.js admin - depends_on: - - api - - web +# admin: +# container_name: admin +# build: +# context: . +# dockerfile: ./admin/Dockerfile.admin +# args: +# DOCKER_BUILDKIT: 1 +# restart: always +# command: node admin/server.js admin +# depends_on: +# - api +# - web - space: - container_name: space - build: - context: . - dockerfile: ./space/Dockerfile.space - args: - DOCKER_BUILDKIT: 1 - restart: always - command: node space/server.js space - depends_on: - - api - - web +# space: +# container_name: space +# build: +# context: . +# dockerfile: ./space/Dockerfile.space +# args: +# DOCKER_BUILDKIT: 1 +# restart: always +# command: node space/server.js space +# depends_on: +# - api +# - web - api: - container_name: api - build: - context: ./apiserver - dockerfile: Dockerfile.api - args: - DOCKER_BUILDKIT: 1 - restart: always - command: ./bin/docker-entrypoint-api.sh - env_file: - - ./apiserver/.env - depends_on: - - plane-db - - plane-redis +# api: +# container_name: api +# build: +# context: ./apiserver +# dockerfile: Dockerfile.api +# args: +# DOCKER_BUILDKIT: 1 +# restart: always +# command: ./bin/docker-entrypoint-api.sh +# env_file: +# - ./apiserver/.env +# depends_on: +# - plane-db +# - plane-redis - worker: - container_name: bgworker - build: - context: ./apiserver - dockerfile: Dockerfile.api - args: - DOCKER_BUILDKIT: 1 - restart: always - command: ./bin/docker-entrypoint-worker.sh - env_file: - - ./apiserver/.env - depends_on: - - api - - plane-db - - plane-redis +# worker: +# container_name: bgworker +# build: +# context: ./apiserver +# dockerfile: Dockerfile.api +# args: +# DOCKER_BUILDKIT: 1 +# restart: always +# command: ./bin/docker-entrypoint-worker.sh +# env_file: +# - ./apiserver/.env +# depends_on: +# - api +# - plane-db +# - plane-redis - beat-worker: - container_name: beatworker - build: - context: ./apiserver - dockerfile: Dockerfile.api - args: - DOCKER_BUILDKIT: 1 - restart: always - command: ./bin/docker-entrypoint-beat.sh - env_file: - - ./apiserver/.env - depends_on: - - api - - plane-db - - plane-redis +# beat-worker: +# container_name: beatworker +# build: +# context: ./apiserver +# dockerfile: Dockerfile.api +# args: +# DOCKER_BUILDKIT: 1 +# restart: always +# command: ./bin/docker-entrypoint-beat.sh +# env_file: +# - ./apiserver/.env +# depends_on: +# - api +# - plane-db +# - plane-redis - migrator: - container_name: plane-migrator - build: - context: ./apiserver - dockerfile: Dockerfile.api - args: - DOCKER_BUILDKIT: 1 - restart: no - command: ./bin/docker-entrypoint-migrator.sh - env_file: - - ./apiserver/.env - depends_on: - - plane-db - - plane-redis +# migrator: +# container_name: plane-migrator +# build: +# context: ./apiserver +# dockerfile: Dockerfile.api +# args: +# DOCKER_BUILDKIT: 1 +# restart: no +# command: ./bin/docker-entrypoint-migrator.sh +# env_file: +# - ./apiserver/.env +# depends_on: +# - plane-db +# - plane-redis - live: - container_name: plane-live - build: - context: . - dockerfile: ./live/Dockerfile.live - args: - DOCKER_BUILDKIT: 1 - restart: always - command: node live/dist/server.js +# live: +# container_name: plane-live +# build: +# context: . +# dockerfile: ./live/Dockerfile.live +# args: +# DOCKER_BUILDKIT: 1 +# restart: always +# command: node live/dist/server.js - plane-db: - container_name: plane-db - image: postgres:15.7-alpine - restart: always - command: postgres -c 'max_connections=1000' - volumes: - - pgdata:/var/lib/postgresql/data - env_file: - - .env - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - PGDATA: /var/lib/postgresql/data +# plane-db: +# container_name: plane-db +# image: postgres:15.7-alpine +# restart: always +# command: postgres -c 'max_connections=1000' +# volumes: +# - pgdata:/var/lib/postgresql/data +# env_file: +# - .env +# environment: +# POSTGRES_USER: ${POSTGRES_USER} +# POSTGRES_DB: ${POSTGRES_DB} +# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} +# PGDATA: /var/lib/postgresql/data - plane-redis: - container_name: plane-redis - image: valkey/valkey:7.2.5-alpine - restart: always - volumes: - - redisdata:/data +# plane-redis: +# container_name: plane-redis +# image: valkey/valkey:7.2.5-alpine +# restart: always +# volumes: +# - redisdata:/data - plane-mq: - container_name: plane-mq - image: rabbitmq:3.13.6-management-alpine - restart: always - env_file: - - .env - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST} - volumes: - - rabbitmq_data:/var/lib/rabbitmq +# plane-mq: +# container_name: plane-mq +# image: rabbitmq:3.13.6-management-alpine +# restart: always +# env_file: +# - .env +# environment: +# RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} +# RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} +# RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST} +# volumes: +# - rabbitmq_data:/var/lib/rabbitmq - plane-minio: - container_name: plane-minio - image: minio/minio - restart: always - command: server /export --console-address ":9090" - volumes: - - uploads:/export - environment: - MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} - MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} +# plane-minio: +# container_name: plane-minio +# image: minio/minio +# restart: always +# command: server /export --console-address ":9090" +# volumes: +# - uploads:/export +# environment: +# MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} +# MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} - # Comment this if you already have a reverse proxy running - proxy: - container_name: proxy - build: - context: ./nginx - dockerfile: Dockerfile - restart: always - ports: - - ${NGINX_PORT}:80 - environment: - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} - BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} - depends_on: - - web - - api - - space - - admin +# # Comment this if you already have a reverse proxy running +# proxy: +# container_name: proxy +# build: +# context: ./nginx +# dockerfile: Dockerfile +# restart: always +# ports: +# - ${NGINX_PORT}:80 +# environment: +# FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} +# BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} +# depends_on: +# - web +# - api +# - space +# - admin -volumes: - pgdata: - redisdata: - uploads: - rabbitmq_data: +# volumes: +# pgdata: +# redisdata: +# uploads: +# rabbitmq_data: diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 0c543241eef..9c79296da4f 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -77,6 +77,7 @@ export type TIssueParams = | "show_empty_groups" | "cursor" | "per_page" + | "hub_code" | "issue_type" | "layout" | "expand"; @@ -98,6 +99,8 @@ export interface IIssueFilterOptions { subscriber?: string[] | null; target_date?: string[] | null; issue_type?: string[] | null; + hub_code?: string[] | null; + custom_properties?: string[] | null; } export interface IIssueDisplayFilterOptions { diff --git a/space/core/components/issues/filters/selection.tsx b/space/core/components/issues/filters/selection.tsx index 2b5d54074c7..b8bd632feac 100644 --- a/space/core/components/issues/filters/selection.tsx +++ b/space/core/components/issues/filters/selection.tsx @@ -6,7 +6,7 @@ import { Search, X } from "lucide-react"; // types import { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; // components -import { FilterPriority, FilterState } from "."; +import { FilterPriority, FilterState, FilterCustomProperty } from "."; type Props = { filters: IIssueFilterOptions; diff --git a/space/next.config.js b/space/next.config.js index d18ce805f4d..de3afceb17c 100644 --- a/space/next.config.js +++ b/space/next.config.js @@ -13,7 +13,7 @@ const nextConfig = { return [ { source: "/", - headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }], // clickjacking protection + headers: [{ key: "X-Frame-Options", value: "ALLOWALL" }], // clickjacking protection }, ]; }, diff --git a/web/app/[workspaceSlug]/(projects)/header.tsx b/web/app/[workspaceSlug]/(projects)/header.tsx index a7a19afe6b0..6607f3a9383 100644 --- a/web/app/[workspaceSlug]/(projects)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/header.tsx @@ -33,27 +33,6 @@ export const WorkspaceDashboardHeader = () => { - - - captureEvent(GITHUB_REDIRECTED, { - element: "navbar", - }) - } - className="flex flex-shrink-0 items-center gap-1.5 rounded bg-custom-background-80 px-3 py-1.5" - href="https://github.com/makeplane/plane" - target="_blank" - rel="noopener noreferrer" - > - GitHub Logo - Star us on GitHub - - ); diff --git a/web/app/workspace-invitations/page.tsx b/web/app/workspace-invitations/page.tsx index a6829019892..3e0aafb7202 100644 --- a/web/app/workspace-invitations/page.tsx +++ b/web/app/workspace-invitations/page.tsx @@ -107,7 +107,6 @@ const WorkspaceInvitationPage = observer(() => { ) : ( )} - = ({ customProperties }) => { + if (!Array.isArray(customProperties) || customProperties.length === 0) { + return null; + } + + return ( +
+
+ {customProperties.map((element) => ( +
+
+ + {element.key} +
+
+ {element.value} +
+
+ ))} +
+ ); +}; diff --git a/web/core/components/issues/index.ts b/web/core/components/issues/index.ts index 483d53da918..42114945798 100644 --- a/web/core/components/issues/index.ts +++ b/web/core/components/issues/index.ts @@ -12,6 +12,7 @@ export * from "./issue-update-status"; export * from "./create-issue-toast-action-items"; export * from "./relations"; export * from "./issue-detail-widgets"; +export * from "./custom-properties"; // issue details export * from "./issue-detail"; diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 1c0765675a7..1caee007e7d 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -14,7 +14,7 @@ import { StateDropdown, } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; -import { IssueCycleSelect, IssueLabel, IssueModuleSelect, IssueParentSelect } from "@/components/issues"; +import { IssueCycleSelect, IssueLabel, IssueModuleSelect, IssueParentSelect, CustomProperties} from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; @@ -54,6 +54,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); const customProperties = issue?.custom_properties || []; + const fields = [ { key: "hub_code", label: "Hub Code" }, { key: "vendor_code", label: "Vendor Code" }, { key: "customer_code", label: "Customer Code" }, + { key: "worker_code", label: "Worker Code" }, { key: "reference_number", label: "Reference No" }, { key: "trip_reference_number", label: "Trip Ref No" }]; const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -283,6 +285,18 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { + {fields.map(({ key, label }) => + issue?.[key] ? ( +
+
+ + {label} +
+
{issue[key]}
+
+ ) : null + )} + = observer((props) => { isDisabled={!isEditable} /> )} - {customProperties?.length > 0 && ( -
- )} - {Array.isArray(customProperties) && customProperties.map((element) => ( -
-
- - {element?.key} -
-
- {element?.value} -
-
- )) - } + diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx new file mode 100644 index 00000000000..3e4139df851 --- /dev/null +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { observer } from "mobx-react"; + +// icons +import { X } from "lucide-react"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedAdditionalPropertiesFilters: React.FC = observer((props) => { + const { handleRemove, values } = props; + + return ( + <> + {values.map((element) => ( +
+ {element} + +
+ ))} + + ); +}); diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 268358efea2..64acb2ccac6 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -14,6 +14,7 @@ import { AppliedProjectFilters, AppliedStateFilters, AppliedStateGroupFilters, + AppliedAdditionalPropertiesFilters } from "@/components/issues"; // constants // helpers @@ -36,6 +37,7 @@ type Props = { const membersFilters = ["assignees", "mentions", "created_by", "subscriber"]; const dateFilters = ["start_date", "target_date"]; +const additionalFilters = ["hub_code", "customer_code", "reference_number", "trip_reference_number", "vendor_code", "worker_code"]; export const AppliedFiltersList: React.FC = observer((props) => { const { @@ -134,6 +136,13 @@ export const AppliedFiltersList: React.FC = observer((props) => { values={value} /> )} + {additionalFilters?.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} {isEditingAllowed && ( + )} + + ) : ( +

No Matches Found

+ )} + + )} + + ); +}); \ No newline at end of file diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx new file mode 100644 index 00000000000..772cc5cc136 --- /dev/null +++ b/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx @@ -0,0 +1,129 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +// import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { IState } from "@plane/types"; +import { WorkspaceService } from "@/services/workspace.service"; +import { API_BASE_URL } from "@/helpers/common.helper"; +// import { Loader, StateGroupIcon } from "@plane/ui"; +import { FilterHeader, FilterOption } from "@/components/issues"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; + states: IState[] | undefined; +}; + +export const FilterCustomProperty: React.FC = observer((props) => { + const { workspaceSlug } = useParams(); + const workspaceService = new WorkspaceService(API_BASE_URL); + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [mainPreviewEnabled, setMainPreviewEnabled] = useState(true); + const [groupPreviewEnabled, setGroupPreviewEnabled] = useState>({}); + const [renderMoreGroupItems, setRenderMoreGroupItems] = useState>({}); + const [groupedProperties, setGroupedProperties] = useState>({}); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const handleViewToggle = (groupKey) => { + setRenderMoreGroupItems((prev) => { + return { + ...prev, + [groupKey]: !prev[groupKey] + }; + }); + }; + + React.useEffect(() => { + const fetchCustomProperties = async (): Promise => { + try { + const data = await workspaceService.getIssuesCustomProperties(workspaceSlug); + setGroupedProperties(data); + Object?.keys(data)?.forEach((groupKey) => { + setGroupPreviewEnabled((prev) => ({ + ...prev, + [groupKey]: true + })); + }); + } catch (error) { + console.error("Error fetching custom properties:", error); + } + }; + + fetchCustomProperties(); + }, [workspaceSlug]); + + const filteredGroupOptions = useMemo(() => { + return Object.keys(groupedProperties).reduce((acc, groupKey) => { + const properties = groupedProperties[groupKey]; + + const filteredValues = properties + .filter((property) => property?.toLowerCase()?.includes(searchQuery.toLowerCase())) + .map((property) => property); + + if (filteredValues?.length > 0) { + acc[groupKey] = filteredValues; + } + + return acc; + }, {}); + }, [searchQuery, groupedProperties]); + + const toggleGroupPreview = (groupKey: string) => { + setGroupPreviewEnabled((prev) => ({ + ...prev, + [groupKey]: !prev[groupKey], + })); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={mainPreviewEnabled} + handleIsPreviewEnabled={() => setMainPreviewEnabled(!mainPreviewEnabled)} + /> + {mainPreviewEnabled && ( +
+ {Object.keys(filteredGroupOptions).map((groupKey) => { + const properties = filteredGroupOptions[groupKey]; + return ( +
+ toggleGroupPreview(groupKey)} + /> + {groupPreviewEnabled[groupKey] && ( +
+ {properties + .slice(0, renderMoreGroupItems[groupKey] ? properties.length : 5) + .map((property) => ( + handleUpdate(`${groupKey}:${property}`)} + title={property} + /> + ))} + {properties.length > 5 ? : null} +
+ )} +
+ ); + })} +
+ )} + + ); +}); diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index 0bf180aefa7..3c6c2d43663 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -16,16 +16,18 @@ import { FilterState, FilterStateGroup, FilterTargetDate, + FilterAdditionalProperties, FilterCycle, FilterModule, FilterIssueGrouping, } from "@/components/issues"; // constants -import { ILayoutDisplayFiltersOptions } from "@/constants/issue"; +import { ILayoutDisplayFiltersOptions, ISSUE_ADDITIONAL_PROPERTIES } from "@/constants/issue"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { FilterIssueTypes } from "@/plane-web/components/issues"; +import { FilterCustomProperty } from "./custom-properties"; type Props = { filters: IIssueFilterOptions; @@ -242,6 +244,31 @@ export const FilterSelection: React.FC = observer((props) => { /> )} + + {ISSUE_ADDITIONAL_PROPERTIES.map((prop) => ( +
+ handleFiltersUpdate(prop.key, val)} + searchQuery={filtersSearchQuery} + /> +
+ ))} + + {/* custom_properties */} + {isFilterEnabled("custom_properties") && ( +
+ handleFiltersUpdate("custom_properties", val)} + // handleUpdate={(val) => handleFiltersUpdate("custom_properties", null, val)} + searchQuery={filtersSearchQuery} + /> +
+ )} ); diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/index.ts b/web/core/components/issues/issue-layouts/filters/header/filters/index.ts index ab5756bf4d7..495020f9244 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/index.ts +++ b/web/core/components/issues/issue-layouts/filters/header/filters/index.ts @@ -11,3 +11,4 @@ export * from "./state"; export * from "./cycle"; export * from "./module"; export * from "./target-date"; +export * from "./additional-properties"; diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index fcc706cbb86..e995aa909cb 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2 } from "lucide-react"; +import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2, Info } from "lucide-react"; // hooks // ui icons import { DiceIcon, DoubleCircleIcon, ContrastIcon } from "@plane/ui"; @@ -21,6 +21,7 @@ import { IssueParentSelect, IssueLabel, TIssueOperations, + CustomProperties } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; @@ -29,7 +30,7 @@ import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store"; // plane web components import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values"; -import { IssueWorklogProperty } from "@/plane-web/components/issues"; +import { IssueWorklogProperty} from "@/plane-web/components/issues"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -53,9 +54,11 @@ export const PeekOverviewProperties: FC = observer((pro if (!issue) return <>; const createdByDetails = getUserDetails(issue?.created_by); const projectDetails = getProjectById(issue.project_id); + const customProperties = issue?.custom_properties || []; const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); - + const fields = [ { key: "hub_code", label: "Hub Code" }, { key: "vendor_code", label: "Vendor Code" }, { key: "customer_code", label: "Customer Code" }, + { key: "worker_code", label: "Worker Code" }, { key: "reference_number", label: "Reference No" }, { key: "trip_reference_number", label: "Trip Ref No" }]; const minDate = getDate(issue.start_date); minDate?.setDate(minDate.getDate()); @@ -283,6 +286,22 @@ export const PeekOverviewProperties: FC = observer((pro +
+ {fields.map(({ key, label }) => + issue?.[key] ? ( +
+
+ + {label} +
+
{issue[key]}
+
+ ) : null + )} +
+ + + = observer( >
- -
-
- {fields.map(({ key, label }) => - issue?.[key] ? ( + {ISSUE_ADDITIONAL_PROPERTIES.map((prop) => + issue[prop.key] ? (
- {label} + {prop.title}
-
{issue[key]}
+
{issue[prop.key]}
) : null )} From db341b28d106a23956c9ffb40f591553124e8dbc Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 25 Feb 2025 09:52:33 +0000 Subject: [PATCH 091/164] dev issues --- packages/types/src/issues/issue.d.ts | 6 ++++++ .../components/issues/peek-overview/properties.tsx | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index 024ab5f6070..108c1034830 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -59,6 +59,12 @@ export type TIssue = TBaseIssue & { issue_relation?: IssueRelation[]; issue_related?: IssueRelation[]; custom_properties?: Record; + hub_code?: string; + worker_code?: string; + customer_code?: string; + trip_reference_number?: string; + reference_number?: string; + vendor_code?: string; // tempId is used for optimistic updates. It is not a part of the API response. tempId?: string; // sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response. diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index e995aa909cb..cde6f597672 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -31,6 +31,7 @@ import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/ // plane web components import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values"; import { IssueWorklogProperty} from "@/plane-web/components/issues"; +import { ISSUE_ADDITIONAL_PROPERTIES } from "@/constants/issue"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -40,6 +41,7 @@ interface IPeekOverviewProperties { issueOperations: TIssueOperations; } + export const PeekOverviewProperties: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, disabled } = props; // store hooks @@ -287,14 +289,14 @@ export const PeekOverviewProperties: FC = observer((pro
- {fields.map(({ key, label }) => - issue?.[key] ? ( -
+ {ISSUE_ADDITIONAL_PROPERTIES.map((prop) => + issue[prop.key] ? ( +
- {label} + {prop?.title}
-
{issue[key]}
+
{issue[prop.key]}
) : null )} From 08a42fdb4c3ad1d9ccf18fe6d5be09d0f7ca5ca2 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 25 Feb 2025 11:36:51 +0000 Subject: [PATCH 092/164] dev issues --- web/core/components/issues/filters.tsx | 2 ++ web/core/constants/issue.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index 4b80ba12f16..ff496d0e7fc 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -119,6 +119,8 @@ const HeaderFilters = observer(({ currentProjectDetails, projectId, workspaceSlu states={projectStates} cycleViewDisabled={!currentProjectDetails?.cycle_view} moduleViewDisabled={!currentProjectDetails?.module_view} + projectId={projectId} + workspaceSlug={workspaceSlug} /> diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index d4ccadfd571..c8691b56bdc 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -554,7 +554,7 @@ export const groupReactionEmojis = (reactions: any) => { export const ISSUE_ADDITIONAL_PROPERTIES: { key: TIssueAdditionalProperties; - title: string; + title: any; }[] = [ { key: "hub_code", title: "Hub Code" }, { key: "customer_code", title: "Customer Code" }, From 5c50c21a6b0aaaa0b578a9a051a18ef1c48fd442 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 25 Feb 2025 12:05:47 +0000 Subject: [PATCH 093/164] dev issues --- packages/types/src/issues/issue.d.ts | 12 ++++++------ packages/types/src/view-props.d.ts | 5 +++++ web/core/components/issues/issue-detail/sidebar.tsx | 2 -- .../components/issues/peek-overview/properties.tsx | 2 -- web/core/constants/issue.ts | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index 108c1034830..7b1024e941b 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -59,12 +59,12 @@ export type TIssue = TBaseIssue & { issue_relation?: IssueRelation[]; issue_related?: IssueRelation[]; custom_properties?: Record; - hub_code?: string; - worker_code?: string; - customer_code?: string; - trip_reference_number?: string; - reference_number?: string; - vendor_code?: string; + hub_code?: string[] | null; + worker_code?: string[] | null; + customer_code?: string[] | null; + trip_reference_number?: string[] | null; + reference_number?: string[] | null; + vendor_code?: string[] | null; // tempId is used for optimistic updates. It is not a part of the API response. tempId?: string; // sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response. diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 9c79296da4f..fc0561ff565 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -100,6 +100,11 @@ export interface IIssueFilterOptions { target_date?: string[] | null; issue_type?: string[] | null; hub_code?: string[] | null; + customer_code?: string[] | null; + worker_code?: string[] | null; + trip_reference_number?: string[] | null; + reference_number?: string[] | null; + vendor_code?: string[] | null; custom_properties?: string[] | null; } diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index c5ce1632c08..67c90a9b106 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -55,8 +55,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); const customProperties = issue?.custom_properties || []; - const fields = [ { key: "hub_code", label: "Hub Code" }, { key: "vendor_code", label: "Vendor Code" }, { key: "customer_code", label: "Customer Code" }, - { key: "worker_code", label: "Worker Code" }, { key: "reference_number", label: "Reference No" }, { key: "trip_reference_number", label: "Trip Ref No" }]; const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index cde6f597672..1675ac3e410 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -59,8 +59,6 @@ export const PeekOverviewProperties: FC = observer((pro const customProperties = issue?.custom_properties || []; const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); - const fields = [ { key: "hub_code", label: "Hub Code" }, { key: "vendor_code", label: "Vendor Code" }, { key: "customer_code", label: "Customer Code" }, - { key: "worker_code", label: "Worker Code" }, { key: "reference_number", label: "Reference No" }, { key: "trip_reference_number", label: "Trip Ref No" }]; const minDate = getDate(issue.start_date); minDate?.setDate(minDate.getDate()); diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index c8691b56bdc..d4ccadfd571 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -554,7 +554,7 @@ export const groupReactionEmojis = (reactions: any) => { export const ISSUE_ADDITIONAL_PROPERTIES: { key: TIssueAdditionalProperties; - title: any; + title: string; }[] = [ { key: "hub_code", title: "Hub Code" }, { key: "customer_code", title: "Customer Code" }, From daa3d2e83238fe588b423a1211ce87d8efc0a0ca Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 25 Feb 2025 12:30:48 +0000 Subject: [PATCH 094/164] dev issues --- web/core/components/issues/filters.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index ff496d0e7fc..4b80ba12f16 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -119,8 +119,6 @@ const HeaderFilters = observer(({ currentProjectDetails, projectId, workspaceSlu states={projectStates} cycleViewDisabled={!currentProjectDetails?.cycle_view} moduleViewDisabled={!currentProjectDetails?.module_view} - projectId={projectId} - workspaceSlug={workspaceSlug} /> From 08bd96abe0521a49715f1995f8a0632701fc21bd Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Feb 2025 06:11:35 +0000 Subject: [PATCH 095/164] fixed --- .../issues/issue-detail/sidebar.tsx | 6 ++-- .../applied-filters/additional-properties.tsx | 1 + .../header/filters/additional-properties.tsx | 18 ++++++------ .../header/filters/custom-properties.tsx | 29 ++++++++++++++----- .../header/filters/filters-selection.tsx | 6 ++-- .../issues/peek-overview/properties.tsx | 4 +-- web/core/constants/analytics.ts | 6 +--- web/core/constants/issue.ts | 3 +- web/core/services/workspace.service.ts | 2 +- 9 files changed, 42 insertions(+), 33 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 67c90a9b106..b80ac7c44cc 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -284,9 +284,9 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
- {ISSUE_ADDITIONAL_PROPERTIES.map((prop) => + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => issue[prop.key] ? ( -
+
{prop.title} @@ -313,7 +313,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { /> )}
diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx index 3e4139df851..c90a3533b16 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/additional-properties.tsx @@ -8,6 +8,7 @@ import { X } from "lucide-react"; type Props = { handleRemove: (val: string) => void; values: string[]; + editable: boolean | undefined; }; export const AppliedAdditionalPropertiesFilters: React.FC = observer((props) => { diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx index 79a84d5479a..62d19bb871b 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx @@ -8,10 +8,10 @@ import { WorkspaceService } from "@/services/workspace.service"; import { API_BASE_URL } from "@/helpers/common.helper"; type Props = { - appliedFilters: Record; - handleUpdate: (field: string, val: string) => void; - additionalPropertyTitle: string; - additionalPropertyKey: string; + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + additionalPropertyTitle: string | undefined; + additionalPropertyKey: string | undefined; searchQuery: string; }; @@ -27,8 +27,8 @@ export const FilterAdditionalProperties: React.FC = observer((props) => { useEffect(() => { const fetchOptions = async () => { try { - const response = await workspaceService.getIssueAdditionalProperties(workspaceSlug, projectId, additionalPropertyKey); - const formattedOptions = response.data.map((item: string) => ({ key: item, value: item })) || []; + const response = await workspaceService.getIssueAdditionalProperties(workspaceSlug.toString(), projectId.toString(), props.additionalPropertyKey); + const formattedOptions = response.data.map((item: string) => ({ label: item, value: item })) || []; setOptions(formattedOptions); } catch (error) { console.error(`Error fetching ${additionalPropertyKey} options:`, error); @@ -39,7 +39,7 @@ export const FilterAdditionalProperties: React.FC = observer((props) => { }, [workspaceSlug, projectId, additionalPropertyKey]); const filteredOptions = searchQuery - ? options.filter(({ key }) => key.toLowerCase().includes(searchQuery.toLowerCase())) + ? options.filter((label: any) => label.toLowerCase().includes(searchQuery.toLowerCase())) : options; return ( @@ -53,12 +53,12 @@ export const FilterAdditionalProperties: React.FC = observer((props) => {
{filteredOptions.length > 0 ? ( <> - {filteredOptions.slice(0, visibleOptions).map(({ key, value }) => ( + {filteredOptions.slice(0, visibleOptions).map(({ label, value }) => ( handleUpdate(value)} - title={key} + title={label} /> ))} {filteredOptions.length > 5 && ( diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx index 772cc5cc136..e67eaaac70d 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx @@ -14,7 +14,6 @@ type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; searchQuery: string; - states: IState[] | undefined; }; export const FilterCustomProperty: React.FC = observer((props) => { @@ -29,7 +28,7 @@ export const FilterCustomProperty: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const handleViewToggle = (groupKey) => { + const handleViewToggle = (groupKey: string) => { setRenderMoreGroupItems((prev) => { return { ...prev, @@ -41,24 +40,38 @@ export const FilterCustomProperty: React.FC = observer((props) => { React.useEffect(() => { const fetchCustomProperties = async (): Promise => { try { - const data = await workspaceService.getIssuesCustomProperties(workspaceSlug); - setGroupedProperties(data); - Object?.keys(data)?.forEach((groupKey) => { + const data = await workspaceService.getIssuesCustomProperties(workspaceSlug.toString()); + + if (Array.isArray(data)) { + // Convert array to an object with group keys + const groupedData: Record = data.reduce((acc, item) => { + const key = item.group || "default"; // Adjust based on API response + acc[key] = acc[key] ? [...acc[key], item] : [item]; + return acc; + }, {} as Record); + + setGroupedProperties(groupedData); + } else { + setGroupedProperties(data); + } + + Object.keys(data).forEach((groupKey) => { setGroupPreviewEnabled((prev) => ({ ...prev, - [groupKey]: true + [groupKey]: true, })); }); } catch (error) { console.error("Error fetching custom properties:", error); } }; + fetchCustomProperties(); }, [workspaceSlug]); const filteredGroupOptions = useMemo(() => { - return Object.keys(groupedProperties).reduce((acc, groupKey) => { + return Object.keys(groupedProperties).reduce>((acc, groupKey) => { const properties = groupedProperties[groupKey]; const filteredValues = properties @@ -105,7 +118,7 @@ export const FilterCustomProperty: React.FC = observer((props) => { .map((property) => ( handleUpdate(`${groupKey}:${property}`)} title={property} /> diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index 3c6c2d43663..71ace44f61a 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -245,14 +245,14 @@ export const FilterSelection: React.FC = observer((props) => {
)} - {ISSUE_ADDITIONAL_PROPERTIES.map((prop) => ( + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => (
handleFiltersUpdate(prop.key, val)} + handleUpdate={(val) => handleFiltersUpdate(prop.key as keyof IIssueFilterOptions, val)} searchQuery={filtersSearchQuery} />
diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index 1675ac3e410..333222791f5 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -287,7 +287,7 @@ export const PeekOverviewProperties: FC = observer((pro
- {ISSUE_ADDITIONAL_PROPERTIES.map((prop) => + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => issue[prop.key] ? (
@@ -300,7 +300,7 @@ export const PeekOverviewProperties: FC = observer((pro )}
- + { }; export const ISSUE_ADDITIONAL_PROPERTIES: { - key: TIssueAdditionalProperties; + key: keyof TIssue; title: string; }[] = [ { key: "hub_code", title: "Hub Code" }, diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index 7c97d05b227..20a68c64abf 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -289,7 +289,7 @@ export class WorkspaceService extends APIService { }); } - async getIssueAdditionalProperties(workspaceSlug: string, projectId: string , field: any): Promise[]> { + async getIssueAdditionalProperties(workspaceSlug: string, projectId: string , field: any): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/search?field=${field}`, ) From 4ffe507e3d9b504ca34d75828a0293b94fb0fcb9 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Feb 2025 06:25:34 +0000 Subject: [PATCH 096/164] fix --- packages/types/src/issues/issue.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index 7b1024e941b..287a64eb1d9 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -69,6 +69,7 @@ export type TIssue = TBaseIssue & { tempId?: string; // sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response. sourceIssueId?: string; + [key: string]: any; }; export type TIssueMap = { From 2007e99df195ad1153c7bc64e4257ae4b0c69d24 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Feb 2025 07:39:34 +0000 Subject: [PATCH 097/164] build fix --- web/core/constants/issue.ts | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 55480d21683..257e08cb84d 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -9,6 +9,7 @@ import { TIssueOrderByOptions, TIssuePriorities, TIssueGroupingFilters, + TIssue } from "@plane/types"; export const DRAG_ALLOWED_GROUPS: TIssueGroupByOptions[] = [ @@ -244,12 +245,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "start_date", "target_date", "issue_type", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", ], display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, display_filters: { @@ -262,12 +257,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "labels", "assignees", "created_by", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", null, ], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], @@ -348,12 +337,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "project", "start_date", "target_date", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", ], display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, display_filters: { @@ -411,12 +394,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "start_date", "target_date", "issue_type", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", ], display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, display_filters: { @@ -442,12 +419,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "labels", "start_date", "issue_type", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", ], display_properties: ["key", "issue_type"], display_filters: { @@ -502,12 +473,6 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "start_date", "target_date", "issue_type", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", ], display_properties: ["key", "issue_type"], display_filters: { From 5ad130fa6983b6f76af9e8a107793648a7dbedfd Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Feb 2025 10:11:13 +0000 Subject: [PATCH 098/164] minor fixes --- .../components/issues/custom-properties.tsx | 2 -- .../issues/issue-detail/sidebar.tsx | 27 +++++++++-------- .../issues/peek-overview/properties.tsx | 30 +++++++++---------- web/core/constants/issue.ts | 2 +- web/core/hooks/use-group-dragndrop.ts | 4 +-- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index ebe4c1d71db..4c6d74717e3 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Info } from "lucide-react"; type CustomProperty = { key: string; @@ -22,7 +21,6 @@ export const CustomProperties: React.FC = ({ customProper {customProperties.map((element) => (
- {element.key}
diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index b80ac7c44cc..ad5b18bd839 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -283,18 +283,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { />
- - {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => - issue[prop.key] ? ( -
-
- - {prop.title} -
-
{issue[prop.key]}
-
- ) : null - )} = observer((props) => { isDisabled={!isEditable} /> )} + + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => + issue[prop.key] ? ( +
+
+ + {prop?.title} +
+
+ {issue[prop.key]} +
+
+ ) : null + )} + diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index 333222791f5..daf1929351a 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -286,22 +286,6 @@ export const PeekOverviewProperties: FC = observer((pro
-
- {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => - issue[prop.key] ? ( -
-
- - {prop?.title} -
-
{issue[prop.key]}
-
- ) : null - )} -
- - - = observer((pro isDisabled={disabled} /> )} + + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => + issue[prop.key] ? ( +
+
+ + {prop?.title} +
+
{issue[prop.key]}
+
+ ) : null + )} + + ); diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 257e08cb84d..00647d91d12 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -524,6 +524,6 @@ export const ISSUE_ADDITIONAL_PROPERTIES: { { key: "customer_code", title: "Customer Code" }, { key: "worker_code", title: "Worker Code" }, { key: "vendor_code", title: "Vendor Code" }, - { key: "trip_reference_number", title: "Trip Reference Number" }, + { key: "trip_reference_number", title: "Trip Ref Number" }, { key: "reference_number", title: "Reference Number" } ]; \ No newline at end of file diff --git a/web/core/hooks/use-group-dragndrop.ts b/web/core/hooks/use-group-dragndrop.ts index 08695d341d2..0a1c3ef697a 100644 --- a/web/core/hooks/use-group-dragndrop.ts +++ b/web/core/hooks/use-group-dragndrop.ts @@ -61,8 +61,8 @@ export const useGroupIssuesDragNDrop = ( const moduleKey = ISSUE_FILTER_DEFAULT_DATA["module"]; const cycleKey = ISSUE_FILTER_DEFAULT_DATA["cycle"]; - const isModuleChanged = Object.keys(data).includes(moduleKey); - const isCycleChanged = Object.keys(data).includes(cycleKey); + const isModuleChanged = Object.keys(data).includes(moduleKey.toString()); + const isCycleChanged = Object.keys(data).includes(cycleKey.toString()); if (isCycleChanged && workspaceSlug) { if (data[cycleKey]) { From bed3c3367e5417bf8101cd82f58a9179fb620227 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Feb 2025 10:26:57 +0000 Subject: [PATCH 099/164] added custom properties in filters --- web/core/constants/issue.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 00647d91d12..eddd6417ba0 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -368,7 +368,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "vendor_code", "trip_reference_number", "reference_number", - // "custom_properties" + "custom_properties" ], display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, display_filters: { @@ -449,6 +449,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { "vendor_code", "trip_reference_number", "reference_number", + "custom_properties" ], display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, display_filters: { From a0882d18cbe099f4075e8d46600c4a47d28b3dd7 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Feb 2025 10:35:48 +0000 Subject: [PATCH 100/164] added properties --- packages/types/src/view-props.d.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index fc0561ff565..2ae3ff29952 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -80,7 +80,14 @@ export type TIssueParams = | "hub_code" | "issue_type" | "layout" - | "expand"; + | "expand" + | "customer_code" + | "worker_code" + | "trip_reference_number" + | "reference_number" + | "vendor_code" + | "custom_properties" + | "hub_code"; export type TCalendarLayouts = "month" | "week"; From 7809f0ee425dfe0c6335918305758f041498decd Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 27 Feb 2025 11:03:55 +0000 Subject: [PATCH 101/164] celery and flower --- apiserver/Dockerfile.api | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index ee99c50b4aa..ef6e3266221 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -63,10 +63,11 @@ RUN if [ "${ENV_TYPE}" = "apiserver" ]; then \ python manage.py migrate --noinput; \ fi +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf \ CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \ gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -; \ elif [ "${ENV_TYPE}" = "celery" ]; then \ - celery -A plane worker -l info; \ + supervisord -c /etc/supervisor/conf.d/supervisord.conf \ elif [ "${ENV_TYPE}" = "celery-beat" ]; then \ celery -A plane beat -l info; \ else \ From 475fdcbcf5355931d5cc9c7264bc390645028f51 Mon Sep 17 00:00:00 2001 From: Naman-SIngla-Shipsy Date: Mon, 3 Mar 2025 03:09:03 +0000 Subject: [PATCH 102/164] Health check fix --- apiserver/Dockerfile.api | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index ee99c50b4aa..f703b2131f6 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -27,13 +27,13 @@ RUN apk add --no-cache --virtual .build-deps \ "postgresql-dev" \ "libc-dev" \ "linux-headers" \ - && \ - pip install -r requirements.txt --compile --no-cache-dir \ - && \ - apk del .build-deps + && pip install -r requirements.txt --compile --no-cache-dir \ + && apk del .build-deps +# Install Flower for Celery Monitoring +RUN pip install flower -# Add in Django deps and generate Django's static files +# Copy application files COPY manage.py manage.py COPY plane plane/ COPY templates templates/ @@ -46,9 +46,8 @@ RUN mkdir -p /code/plane/logs RUN chmod +x ./bin/* RUN chmod -R 777 /code -EXPOSE 8000 - -# ENV N8N_CONFIG_FILES=/home/node/config/env.json +# Expose ports for API and Flower UI +EXPOSE 8000 5555 # Declare a build argument ARG ENV_FILE_PATH @@ -62,10 +61,11 @@ ENV ENV_FILE_PATH=${ENV_FILE_PATH} RUN if [ "${ENV_TYPE}" = "apiserver" ]; then \ python manage.py migrate --noinput; \ fi - + CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \ gunicorn -w 2 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -; \ elif [ "${ENV_TYPE}" = "celery" ]; then \ + celery -A plane flower --port=5555 --address=0.0.0.0 --url_prefix=flower & \ celery -A plane worker -l info; \ elif [ "${ENV_TYPE}" = "celery-beat" ]; then \ celery -A plane beat -l info; \ @@ -73,7 +73,3 @@ CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \ echo "Unknown ENV_TYPE: ${ENV_TYPE}"; \ exit 1; \ fi - -# Expose container port and run entry point script - - From 217204ba8b9b7735d1be2fd08842b40347e62117 Mon Sep 17 00:00:00 2001 From: Naman-SIngla-Shipsy Date: Mon, 3 Mar 2025 05:28:44 +0000 Subject: [PATCH 103/164] env var fix --- apiserver/Dockerfile.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index f703b2131f6..5eaa699d01b 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -33,6 +33,8 @@ RUN apk add --no-cache --virtual .build-deps \ # Install Flower for Celery Monitoring RUN pip install flower +ENV FLOWER_UNAUTHENTICATED_API=true + # Copy application files COPY manage.py manage.py COPY plane plane/ From af3d837f0aa2de8ca4f87479d7e4d5f2695083c9 Mon Sep 17 00:00:00 2001 From: Naman-SIngla-Shipsy Date: Tue, 4 Mar 2025 03:51:43 +0000 Subject: [PATCH 104/164] dockerfile fix --- apiserver/Dockerfile.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/Dockerfile.api b/apiserver/Dockerfile.api index 5eaa699d01b..c55b670df5a 100644 --- a/apiserver/Dockerfile.api +++ b/apiserver/Dockerfile.api @@ -70,7 +70,7 @@ CMD if [ "${ENV_TYPE}" = "apiserver" ]; then \ celery -A plane flower --port=5555 --address=0.0.0.0 --url_prefix=flower & \ celery -A plane worker -l info; \ elif [ "${ENV_TYPE}" = "celery-beat" ]; then \ - celery -A plane beat -l info; \ + (celery -A plane flower --port=5555 --address=0.0.0.0 --url-prefix=flower &) && celery -A plane beat -l info; \ else \ echo "Unknown ENV_TYPE: ${ENV_TYPE}"; \ exit 1; \ From 316984e63187a8e0578ab29994f899a92eced53a Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 12 Mar 2025 10:27:42 +0000 Subject: [PATCH 105/164] changes till 12th march --- .../api/middleware/api_authentication.py | 13 ++- apiserver/plane/api/serializers/issue.py | 25 ++---- apiserver/plane/api/views/base.py | 38 ++++++++- apiserver/plane/api/views/issue.py | 33 +++++--- apiserver/plane/app/permissions/__init__.py | 1 + apiserver/plane/app/permissions/project.py | 36 +++++++++ apiserver/plane/app/urls/issue.py | 10 ++- apiserver/plane/app/views/__init__.py | 3 +- apiserver/plane/app/views/issue/base.py | 80 +++++++++---------- .../plane/bgtasks/issue_activities_task.py | 11 ++- apiserver/plane/bgtasks/webhook_task.py | 4 +- apiserver/plane/db/models/issue.py | 2 +- apiserver/plane/settings/common.py | 6 +- apiserver/supervisord.conf | 16 ++++ .../templates/emails/exports/analytics.html | 9 ++- web/app/provider.tsx | 23 +++++- web/next.config.js | 2 +- 17 files changed, 223 insertions(+), 89 deletions(-) create mode 100644 apiserver/supervisord.conf diff --git a/apiserver/plane/api/middleware/api_authentication.py b/apiserver/plane/api/middleware/api_authentication.py index a0ea40144a0..d7c74a8f85c 100644 --- a/apiserver/plane/api/middleware/api_authentication.py +++ b/apiserver/plane/api/middleware/api_authentication.py @@ -19,15 +19,19 @@ class APIKeyAuthentication(authentication.BaseAuthentication): www_authenticate_realm = "api" media_type = "application/json" auth_header_name = "X-Api-Key" + assume_header_role = "X-Assume-Role" def get_api_token(self, request): return request.headers.get(self.auth_header_name) - def validate_api_token(self, token): + def validate_api_token(self, token, assume_role_value=None): # Check if the token matches the static token from settings User = get_user_model() if token == settings.STATIC_API_TOKEN: - user = User.objects.filter(is_superuser=True).first() + if assume_role_value: + user = User.objects.filter(username=assume_role_value).first() + else: + user = User.objects.filter(is_superuser=True).first() self.rewite_project_id_in_url() return (user, token) try: @@ -57,5 +61,8 @@ def authenticate(self, request): return None # Validate the API token - user, token = self.validate_api_token(token) + assume_role_value = request.headers.get(self.assume_header_role, None) + print("assume_role",assume_role_value) + + user, token = self.validate_api_token(token, assume_role_value) return user, token diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index a865f303ba2..5aeaaf6208f 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -61,6 +61,12 @@ class IssueSerializer(BaseSerializer): write_only=True, required=False, ) + state_id = serializers.PrimaryKeyRelatedField( + source="state", + queryset=State.objects.all(), + required=False, + allow_null=True, + ) labels = serializers.ListField( child=serializers.PrimaryKeyRelatedField( @@ -164,10 +170,9 @@ def validate(self, data): from plane.api.views import ProjectMemberAPIEndpoint PMObj = ProjectMemberAPIEndpoint() user = PMObj.create_user(user_data) - PMObj.create_workspace_member(self.context.get("workspace_id"), user_data) - PMObj.create_project_member(self.context.get("project_id"), user_data) + PMObj.create_workspace_member(self.context.get("workspace_id"), user,5) + PMObj.create_project_member(self.context.get("project_id"), user,5) data['created_by'] = user - print(data) return data @@ -481,20 +486,6 @@ def validate(self, data): parsed_str = html.tostring(parsed, encoding="unicode") print(html.tostring(parsed, encoding="unicode")) data["comment_html"] = parsed_str - ## if not data.get('created_by', None): - ## if User.objects.filter(username=data['created_by']).exists(): - ## data['created_by'] = User.objects.get(username=data['created_by']) - ## else: -## user_data = { - ## "email": data['created_by'] + '@plane-shipsy.com', - ## "username": data['created_by'], - ## } - ## from plane.api.views import ProjectMemberAPIEndpoint - ## PMObj = ProjectMemberAPIEndpoint() - ## user = PMObj.create_user(user_data) - ## PMObj.create_workspace_member(self.context.get("workspace_id") ,user_data) - ## PMObj.create_project_member(self.context.get("project_id"), user_data) - ## data['created_by'] = user print(data) except Exception as e: diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 6dbf4d52f13..d45ee898300 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -113,6 +113,8 @@ def handle_exception(self, exc): def dispatch(self, request, *args, **kwargs): try: kwargs = self.check_kwargs(kwargs) + request.user = self.get_or_create_user_from_headers(request) + self.ensure_member_in_workspace(request.user, kwargs) response = super().dispatch(request, *args, **kwargs) if settings.DEBUG: from django.db import connection @@ -136,8 +138,42 @@ def check_kwargs(self, kwargs): ).first() if project: kwargs['project_id'] = project.id + + return kwargs - + + def get_or_create_user_from_headers(self, request): + """Extracts user info from headers and ensures they exist in the database.""" + from django.contrib.auth import get_user_model + User = get_user_model() + + assume_role = request.headers.get("X-Assume-Role") + + if assume_role: + user, created = User.objects.get_or_create( + username=assume_role, + defaults={"email": f"{assume_role}@example.com"} + ) + + if created: + print(f"New user created: {assume_role}") + + return user + + return request.user # Default user if no assume role is found + + + def ensure_member_in_workspace(self, user, kwargs): + """ Ensures the given user is a member of the workspace. """ + from plane.authentication.views.app.magic import MagicSignInEndpoint + + if not user or not user.is_authenticated: + return + + if kwargs.get('slug', None): + MagicSignInEndpoint().add_user_to_workspace(user, kwargs['slug']) + + def finalize_response(self, request, response, *args, **kwargs): # Call super to get the default response response = super().finalize_response( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 227685acae9..f5d3f49fd1b 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -39,6 +39,7 @@ ProjectEntityPermission, ProjectLitePermission, ProjectMemberPermission, + ProjectEntityGuestPermission ) from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( @@ -65,7 +66,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView): model = Issue webhook_event = "issue" - permission_classes = [ProjectEntityPermission] + permission_classes = [ProjectEntityGuestPermission] serializer_class = IssueSerializer @property @@ -489,6 +490,10 @@ def put(self, request, slug, project_id): ) def patch(self, request, slug, project_id, pk=None): + modified_data = request.data.copy() if hasattr(request.data, 'copy') else dict(request.data) + # If 'state' is present in the data, rename it to 'state_id' + if 'state' in modified_data: + modified_data['state_id'] = modified_data.pop('state') issue = Issue.objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) @@ -496,10 +501,10 @@ def patch(self, request, slug, project_id, pk=None): current_instance = json.dumps( IssueSerializer(issue).data, cls=DjangoJSONEncoder ) - requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + requested_data = json.dumps(modified_data, cls=DjangoJSONEncoder) serializer = IssueSerializer( issue, - data=request.data, + data=modified_data, context={ "project_id": project_id, "workspace_id": project.workspace_id, @@ -508,15 +513,15 @@ def patch(self, request, slug, project_id, pk=None): ) if serializer.is_valid(): if ( - request.data.get("external_id") - and (issue.external_id != str(request.data.get("external_id"))) + modified_data.get("external_id") + and (issue.external_id != str(modified_data.get("external_id"))) and Issue.objects.filter( project_id=project_id, workspace__slug=slug, - external_source=request.data.get( + external_source=modified_data.get( "external_source", issue.external_source ), - external_id=request.data.get("external_id"), + external_id=modified_data.get("external_id"), ).exists() ): return Response( @@ -526,7 +531,6 @@ def patch(self, request, slug, project_id, pk=None): }, status=status.HTTP_409_CONFLICT, ) - serializer.save() issue_activity.delay( type="issue.activity.updated", @@ -535,7 +539,7 @@ def patch(self, request, slug, project_id, pk=None): issue_id=str(pk), project_id=str(project_id), current_instance=current_instance, - epoch=int(timezone.now().timestamp()), + epoch=int(timezone.now().timestamp()) ) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -1058,13 +1062,16 @@ def get(self, request, slug, project_id, issue_id, pk=None): ).order_by(request.GET.get("order_by", "created_at")) if pk: - issue_activities = issue_activities.get(pk=pk) - serializer = IssueActivitySerializer(issue_activities) - return Response(serializer.data, status=status.HTTP_200_OK) + try: + issue_activity = issue_activities.get(pk=pk) + serializer = IssueActivitySerializer(issue_activity) + return Response(serializer.data, status=status.HTTP_200_OK) + except IssueActivity.DoesNotExist: + return Response({"error": "IssueActivity not found"}, status=status.HTTP_404_NOT_FOUND) return self.paginate( request=request, - queryset=(issue_activities), + queryset=issue_activities, on_results=lambda issue_activity: IssueActivitySerializer( issue_activity, many=True, diff --git a/apiserver/plane/app/permissions/__init__.py b/apiserver/plane/app/permissions/__init__.py index e453881441b..82fdf5f5744 100644 --- a/apiserver/plane/app/permissions/__init__.py +++ b/apiserver/plane/app/permissions/__init__.py @@ -11,5 +11,6 @@ ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission, + ProjectEntityGuestPermission ) from .base import allow_permission, ROLE \ No newline at end of file diff --git a/apiserver/plane/app/permissions/project.py b/apiserver/plane/app/permissions/project.py index 522c1bfde43..b462e6492e2 100644 --- a/apiserver/plane/app/permissions/project.py +++ b/apiserver/plane/app/permissions/project.py @@ -116,6 +116,42 @@ def has_permission(self, request, view): ).exists() +class ProjectEntityGuestPermission(BasePermission): + def has_permission(self, request, view): + if request.user.is_anonymous: + return False + + if request.user.is_superuser: + return True + # Handle requests based on project__identifier + if hasattr(view, "project__identifier") and view.project__identifier: + if request.method in SAFE_METHODS: + return ProjectMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + project__identifier=view.project__identifier, + is_active=True, + ).exists() + + ## Safe Methods -> Handle the filtering logic in queryset + if request.method in SAFE_METHODS: + return ProjectMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + project_id=view.project_id, + is_active=True, + ).exists() + + ## Only project members or admins can create and edit the project attributes + return ProjectMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + role__in=[Admin, Member, Guest], + project_id=view.project_id, + is_active=True, + ).exists() + + class ProjectLitePermission(BasePermission): def has_permission(self, request, view): if request.user.is_anonymous: diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 47b34954f06..4a23752ca85 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -24,7 +24,8 @@ IssueDetailEndpoint, IssueAttachmentV2Endpoint, IssueBulkUpdateDateEndpoint, - SearchAPIEndpoint + SearchAPIEndpoint, + SearchSingleValueAPI ) urlpatterns = [ @@ -321,9 +322,14 @@ IssueBulkUpdateDateEndpoint.as_view(), name="project-issue-dates", ), - path( + path( "workspaces//projects//search", SearchAPIEndpoint.as_view(), name="key-codes", ), + path( + "workspaces//projects//filed_search", + SearchSingleValueAPI.as_view(), + name="key-codes" + ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index e6ca8f8634f..30d5c1cb35c 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -132,7 +132,8 @@ IssuePaginatedViewSet, IssueDetailEndpoint, IssueBulkUpdateDateEndpoint, - SearchAPIEndpoint + SearchAPIEndpoint, + SearchSingleValueAPI ) from .issue.activity import ( diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 8fa4c8197d8..f846ed84204 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -1122,38 +1122,6 @@ def get(self, request, slug, project_id): ).data, ) -# class HubViewSet(BaseAPIView): -# @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) -# def list(self, request, slug, project_id): -# hubs = Hub.objects.filter(workspace__slug=slug, project_id=project_id) - -# # Extract query parameters -# hub_code = request.GET.get("hub_code", "").strip() -# reference_number = request.GET.get("reference_number", "").strip() -# trip_reference_number = request.GET.get("trip_reference_number", "").strip() -# vendor_code = request.GET.get("vendor_code", "").strip() -# worker_code = request.GET.get("worker_code", "").strip() - -# # Apply filtering dynamically -# filter_params = {} -# if hub_code: -# filter_params["hub_code__icontains"] = hub_code -# if reference_number: -# filter_params["reference_number__icontains"] = reference_number -# if trip_reference_number: -# filter_params["trip_reference_number__icontains"] = trip_reference_number -# if vendor_code: -# filter_params["vendor_code__icontains"] = vendor_code -# if worker_code: -# filter_params["worker_code__icontains"] = worker_code - -# hubs = hubs.filter(**filter_params) - -# # Serialize the results -# hub_data = HubSerializer(hubs, many=True).data -# return Response(hub_data, status=status.HTTP_200_OK) - - class IssueBulkUpdateDateEndpoint(BaseAPIView): @@ -1248,17 +1216,10 @@ def post(self, request, slug, project_id): ) class SearchAPIEndpoint(BaseAPIView): - """ - API to fetch hub codes for a specific workspace and project. - """ model = Issue webhook_event = "issue" - permission_classes = [ProjectEntityPermission] def get(self, request, slug, project_id): - """ - Fetch unique values for hub_code, worker_code, reference_number, trip_reference_number, - customer_code, or vendor_code based on the requested query parameters. - """ + allowed_fields = ["hub_code", "worker_code", "reference_number", "trip_reference_number", "customer_code", "vendor_code"] field = request.GET.get("field") # Get the single field value @@ -1283,7 +1244,7 @@ def get(self, request, slug, project_id): unique_values = list(set(filter(None, values))) # Remove duplicates and nulls paginator = PageNumberPagination() - paginator.page_size = int(request.GET.get("limit", 10)) # Default limit = 10 + paginator.page_size = int(request.GET.get("limit", 20)) # Default limit = 10 paginated_values = paginator.paginate_queryset(unique_values, request) # Custom pagination response @@ -1296,3 +1257,40 @@ def get(self, request, slug, project_id): } return Response(response_data, status=status.HTTP_200_OK) + +class SearchSingleValueAPI(BaseAPIView): + model = Issue + allowed_fields = ["hub_code", "trip_reference_number", "reference_number", "worker_code", "vendor_code","customer_code"] + + def get(self, request, slug, project_id): + # Extract query parameters (only one should be provided) + query_params = {field: request.GET.get(field) for field in self.allowed_fields if request.GET.get(field)} + + if len(query_params) != 1: + return Response( + {"error": f"Provide exactly one search parameter from: {', '.join(self.allowed_fields)}"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Extract the field and search value + search_field, search_value = next(iter(query_params.items())) + + # Query for exact matches first + exact_matches = Issue.objects.filter( + workspace__slug=slug, + project_id=project_id, + **{search_field: search_value} # Exact match + ).values_list(search_field, flat=True) + + # Query for partial matches using `icontains` + startwith_matches = Issue.objects.filter( + workspace__slug=slug, + project_id=project_id, + **{f"{search_field}__istartswith": search_value} # starts with match + ).exclude(**{search_field: search_value}) # Exclude exact match + startwith_matches = startwith_matches.values_list(search_field, flat=True) + + # Combine results: exact matches first, then partial matches + unique_values = list(set(exact_matches)) + list(set(startwith_matches)) + + return Response({"data": unique_values}, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index 371204aa8a6..c4a3ce05033 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -367,14 +367,16 @@ def track_assignees( actor_id, issue_activities, epoch, + verb="updated" ): + requested_assignees = ( - set([str(asg) for asg in requested_data.get("assignee_ids", [])]) + set([str(asg) for asg in requested_data.get("assignee_ids", requested_data.get("assignees", []))]) if requested_data is not None else set() ) current_assignees = ( - set([str(asg) for asg in current_instance.get("assignee_ids", [])]) + set([str(asg) for asg in current_instance.get("assignee_ids", current_instance.get("assignees", []))]) if current_instance is not None else set() ) @@ -389,7 +391,7 @@ def track_assignees( IssueActivity( issue_id=issue_id, actor_id=actor_id, - verb="updated", + verb=verb, old_value="", new_value=assignee.display_name, field="assignees", @@ -597,7 +599,7 @@ def create_issue_activity( requested_data = ( json.loads(requested_data) if requested_data is not None else None ) - if requested_data.get("assignee_ids") is not None: + if requested_data.get("assignee_ids") is not None or requested_data.get("assignees")is not None: track_assignees( requested_data, current_instance, @@ -607,6 +609,7 @@ def create_issue_activity( actor_id, issue_activities, epoch, + "created" ) diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 7614c4b2f57..ee35054b120 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -402,8 +402,10 @@ def webhook_activity( if event == "issue_comment": webhooks = webhooks.filter(issue_comment=True) - + for webhook in webhooks: + event_data = get_model_data(event=event, event_id=event_id) + actor_data = get_model_data(event="user", event_id=actor_id) webhook_send_task.delay( webhook=webhook.id, slug=slug, diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 2bb3e371001..4cfd89115d0 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -45,7 +45,7 @@ def get_default_filters(): "labels": None, "start_date": None, "target_date": None, - "subscriber": None, + "subscriber": None } diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index e0975d37f9c..05774297339 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -153,7 +153,7 @@ CORS_ALLOW_ALL_ORIGINS = True secure_origins = False -CORS_ALLOW_HEADERS = [*default_headers, "X-API-Key"] +CORS_ALLOW_HEADERS = [*default_headers, "X-API-Key", "X-Assume-Role"] # Application Settings WSGI_APPLICATION = "plane.wsgi.application" @@ -362,7 +362,9 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) # Cookie Settings +# SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = secure_origins +# SESSION_COOKIE_SAMESITE = "None" SESSION_COOKIE_HTTPONLY = True SESSION_ENGINE = "plane.db.models.session" SESSION_COOKIE_AGE = os.environ.get("SESSION_COOKIE_AGE", 604800) @@ -377,7 +379,9 @@ ADMIN_SESSION_COOKIE_AGE = os.environ.get("ADMIN_SESSION_COOKIE_AGE", 3600) # CSRF cookies +# CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = secure_origins +# CSRF_COOKIE_SAMESITE = "None" CSRF_COOKIE_HTTPONLY = False CSRF_TRUSTED_ORIGINS = cors_allowed_origins CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) diff --git a/apiserver/supervisord.conf b/apiserver/supervisord.conf new file mode 100644 index 00000000000..d4f4eebe3cb --- /dev/null +++ b/apiserver/supervisord.conf @@ -0,0 +1,16 @@ +[supervisord] +nodaemon=true + +[program:celery] +command=celery -A plane worker -l info +autostart=true +autorestart=true +stderr_logfile=/var/log/celery.err.log +stdout_logfile=/var/log/celery.out.log + +[program:flower] +command=celery -A plane flower --port=5555 +autostart=true +autorestart=true +stderr_logfile=/var/log/flower.err.log +stdout_logfile=/var/log/flower.out.log \ No newline at end of file diff --git a/apiserver/templates/emails/exports/analytics.html b/apiserver/templates/emails/exports/analytics.html index d2caa9d7a41..c21d67f8e9e 100644 --- a/apiserver/templates/emails/exports/analytics.html +++ b/apiserver/templates/emails/exports/analytics.html @@ -1,2 +1,7 @@ - - Hey there,
Your requested data export from Plane Analytics is now ready. The information has been compiled into a CSV format for your convenience.
Please find the attachment and download the CSV file. This file can easily be imported into any spreadsheet program for further analysis.
If you require any assistance or have any questions, please do not hesitate to contact us.
Thank you \ No newline at end of file + + Hey there,
Your requested data export from Plane Analytics is now ready. The information has been compiled +into a CSV format for your convenience.
Please find the attachment and download the CSV file. This file can easily +be imported into any spreadsheet program for further analysis.
If you require any assistance or have any +questions, please do not hesitate to contact us.
Thank you + + \ No newline at end of file diff --git a/web/app/provider.tsx b/web/app/provider.tsx index dba975a6373..680cb7023b7 100644 --- a/web/app/provider.tsx +++ b/web/app/provider.tsx @@ -1,9 +1,10 @@ "use client"; -import { FC, ReactNode } from "react"; +import { FC, ReactNode, useEffect } from "react"; import dynamic from "next/dynamic"; import { useTheme, ThemeProvider } from "next-themes"; import { SWRConfig } from "swr"; +import { usePathname, useRouter } from "next/navigation"; // ui import { Toast } from "@plane/ui"; // constants @@ -35,6 +36,26 @@ const ToastWithTheme = () => { export const AppProvider: FC = (props) => { const { children } = props; // themes + + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + // const onIncomingMessage = (event) => { + // console.log('onIncomingMessage', event); + // if (event.data.type === 'ROUTE_CHANGE') { + // console.log(location.pathname, event.data.path); + // router.replace(event.data.path); + // } + // }; + console.log(pathname, 'pathname'); + window.parent.postMessage({ type: "ROUTE_CHANGE", path: pathname }, "*"); + // window.addEventListener('message', onIncomingMessage); + // return () => { + // window.removeEventListener("message", onIncomingMessage); + // }; + }, [pathname]); + return ( <> diff --git a/web/next.config.js b/web/next.config.js index 848f1366bdc..f36c86494ca 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -18,7 +18,7 @@ const nextConfig = { { key: "X-Frame-Options", value: "ALLOWALL" }, { key: "Content-Security-Policy", - value: "frame-ancestors 'self' https://*.shipsy.io http://*.localhost:3001 http://5.223.42.156:3000/", // Replace with the domains allowed to embed your app + value: "frame-ancestors 'self' https://*.shipsy.io http://*.localhost:3001 http://*.localhost:3002 http://5.223.42.156:3000/", // Replace with the domains allowed to embed your app }, // { // key: "Referrer-Policy", From 442897682cd4ae1e6a83fb84e09e7522feafb18d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 13 Mar 2025 09:34:16 +0000 Subject: [PATCH 106/164] commented un used variables and imports --- web/app/[workspaceSlug]/(projects)/header.tsx | 16 ++++++++-------- web/app/provider.tsx | 5 ++--- web/app/workspace-invitations/page.tsx | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/header.tsx b/web/app/[workspaceSlug]/(projects)/header.tsx index 6607f3a9383..167d3b6f8d0 100644 --- a/web/app/[workspaceSlug]/(projects)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/header.tsx @@ -1,24 +1,24 @@ "use client"; -import Image from "next/image"; -import { useTheme } from "next-themes"; +// import Image from "next/image"; +// import { useTheme } from "next-themes"; import { Home } from "lucide-react"; // images -import githubBlackImage from "/public/logos/github-black.png"; -import githubWhiteImage from "/public/logos/github-white.png"; +// import githubBlackImage from "/public/logos/github-black.png"; +// import githubWhiteImage from "/public/logos/github-white.png"; // ui import { Breadcrumbs, Header } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; // constants -import { GITHUB_REDIRECTED } from "@/constants/event-tracker"; +// import { GITHUB_REDIRECTED } from "@/constants/event-tracker"; // hooks -import { useEventTracker } from "@/hooks/store"; +// import { useEventTracker } from "@/hooks/store"; export const WorkspaceDashboardHeader = () => { // hooks - const { captureEvent } = useEventTracker(); - const { resolvedTheme } = useTheme(); + // const { captureEvent } = useEventTracker(); + // const { resolvedTheme } = useTheme(); return ( <> diff --git a/web/app/provider.tsx b/web/app/provider.tsx index 680cb7023b7..951ff131174 100644 --- a/web/app/provider.tsx +++ b/web/app/provider.tsx @@ -2,10 +2,9 @@ import { FC, ReactNode, useEffect } from "react"; import dynamic from "next/dynamic"; +import { usePathname } from "next/navigation";//ui import { useTheme, ThemeProvider } from "next-themes"; import { SWRConfig } from "swr"; -import { usePathname, useRouter } from "next/navigation"; -// ui import { Toast } from "@plane/ui"; // constants import { SWR_CONFIG } from "@/constants/swr-config"; @@ -37,7 +36,7 @@ export const AppProvider: FC = (props) => { const { children } = props; // themes - const router = useRouter(); + // const router = useRouter(); const pathname = usePathname(); useEffect(() => { diff --git a/web/app/workspace-invitations/page.tsx b/web/app/workspace-invitations/page.tsx index 3e0aafb7202..a715a2f469f 100644 --- a/web/app/workspace-invitations/page.tsx +++ b/web/app/workspace-invitations/page.tsx @@ -4,7 +4,7 @@ import React from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; import useSWR from "swr"; -import { Boxes, Check, Share2, Star, User2, X } from "lucide-react"; +import { Boxes, Check, Share2, User2, X } from "lucide-react"; // components import { LogoSpinner } from "@/components/common"; import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space"; From 51c2e58cd186036e18ed8490292328467bfd541e Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 13 Mar 2025 11:01:05 +0000 Subject: [PATCH 107/164] un commented --- web/app/[workspaceSlug]/(projects)/header.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/header.tsx b/web/app/[workspaceSlug]/(projects)/header.tsx index 167d3b6f8d0..0d6a3e451db 100644 --- a/web/app/[workspaceSlug]/(projects)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/header.tsx @@ -1,7 +1,7 @@ "use client"; // import Image from "next/image"; -// import { useTheme } from "next-themes"; +import { useTheme } from "next-themes"; import { Home } from "lucide-react"; // images // import githubBlackImage from "/public/logos/github-black.png"; @@ -13,12 +13,12 @@ import { BreadcrumbLink } from "@/components/common"; // constants // import { GITHUB_REDIRECTED } from "@/constants/event-tracker"; // hooks -// import { useEventTracker } from "@/hooks/store"; +import { useEventTracker } from "@/hooks/store"; export const WorkspaceDashboardHeader = () => { // hooks - // const { captureEvent } = useEventTracker(); - // const { resolvedTheme } = useTheme(); + const { captureEvent } = useEventTracker(); + const { resolvedTheme } = useTheme(); return ( <> From cd2a8e00f649ebf28ba851ce9e9769f455c6468e Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Mon, 17 Mar 2025 04:29:40 +0000 Subject: [PATCH 108/164] Allowing the project member permission for guests users --- apiserver/plane/app/permissions/project.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/app/permissions/project.py b/apiserver/plane/app/permissions/project.py index b462e6492e2..8856a7b534c 100644 --- a/apiserver/plane/app/permissions/project.py +++ b/apiserver/plane/app/permissions/project.py @@ -30,7 +30,7 @@ def has_permission(self, request, view): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[Admin, Member, Guest], is_active=True, ).exists() @@ -65,7 +65,7 @@ def has_permission(self, request, view): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[Admin, Member, Guest], is_active=True, ).exists() @@ -74,7 +74,7 @@ def has_permission(self, request, view): return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[Admin, Member, Guest], project_id=view.project_id, is_active=True, ).exists() @@ -110,7 +110,7 @@ def has_permission(self, request, view): return ProjectMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[Admin, Member], + role__in=[Admin, Member, Guest], project_id=view.project_id, is_active=True, ).exists() From 1fb0c8e71e0b4d78c99cd1f893466e34fcf2d295 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Mon, 17 Mar 2025 09:10:55 +0000 Subject: [PATCH 109/164] Fix for the user's second login issue --- .../plane/authentication/provider/credentials/magic_code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/authentication/provider/credentials/magic_code.py b/apiserver/plane/authentication/provider/credentials/magic_code.py index a531ca57043..a62650edaa9 100644 --- a/apiserver/plane/authentication/provider/credentials/magic_code.py +++ b/apiserver/plane/authentication/provider/credentials/magic_code.py @@ -147,7 +147,7 @@ def set_user_data(self): "first_name": "", "last_name": "", "provider_id": "", - "is_password_autoset": True, + "is_password_autoset": False, }, } ) From 61a9692f26e0d297457453075ce2c44055a1da9d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 18 Mar 2025 09:38:42 +0000 Subject: [PATCH 110/164] fixed filter search issue --- .../filters/header/filters/additional-properties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx index 62d19bb871b..4921eddb09b 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/additional-properties.tsx @@ -39,7 +39,7 @@ export const FilterAdditionalProperties: React.FC = observer((props) => { }, [workspaceSlug, projectId, additionalPropertyKey]); const filteredOptions = searchQuery - ? options.filter((label: any) => label.toLowerCase().includes(searchQuery.toLowerCase())) + ? options.filter((option: any) => option?.label.toLowerCase().includes(searchQuery.toLowerCase())) : options; return ( From a8528e2a4fe57de3fde7a71b04662b5955a8f9d6 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Tue, 18 Mar 2025 19:15:15 +0530 Subject: [PATCH 111/164] Get issueTypeCustomProperties and make these fields editable on UI --- .../components/issues/custom-properties.tsx | 91 +++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 4c6d74717e3..4916a0c2ed8 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -1,6 +1,7 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import axios from "axios"; -type CustomProperty = { +export type CustomProperty = { key: string; value: string; issue_type_custom_property: string; @@ -8,23 +9,101 @@ type CustomProperty = { type CustomPropertiesProps = { customProperties?: CustomProperty[]; + issue_type_id: string; + workspaceSlug: string; + updateCustomProperties: (updatedProperties: CustomProperty[]) => void; }; -export const CustomProperties: React.FC = ({ customProperties }) => { - if (!Array.isArray(customProperties) || customProperties.length === 0) { +export const CustomProperties: React.FC = ({ customProperties, issue_type_id, workspaceSlug, updateCustomProperties }) => { + const [issueTypeCustomProperties, setissueTypeCustomProperties] = useState([]); + const [error, setError] = useState(null); + const [editableError, setEditableError] = useState(null); + useEffect(() => { + const getIssueTypeCustomProperties = async () => { + try { + const response = await axios.get(`/api/workspaces/${workspaceSlug}/issue-type/${issue_type_id}/custom-properties/`); + setissueTypeCustomProperties(response.data); + console.log("getIssueTypeCustomProperties response is", response); + } catch (error) { + console.error("Error fetching custom properties:", error); + setError("Failed to load custom properties."); + } + }; + + getIssueTypeCustomProperties(); + }, [workspaceSlug, issue_type_id]); + + const mergedCustomProperties = issueTypeCustomProperties.map((customProp) => { + // Find the corresponding property in customProperties + const customProperty = customProperties?.find( + (prop) => prop.key === customProp.name + ); + + return { + key: customProp.name, + value: customProperty ? customProperty.value : "", // Use existing value or set empty if not found + issue_type_custom_property: customProp.issue_type, + }; + }); + console.log("CustomProperties is", customProperties); + console.log("mergedCustomProperties is", mergedCustomProperties); + + if (error) { + return
{error}
; + } + + if (!Array.isArray(mergedCustomProperties) || mergedCustomProperties.length === 0) { return null; } + // Inline editable component for each property + const EditableProperty: React.FC<{ property: CustomProperty }> = ({ property }) => { + const [value, setValue] = useState(property.value); + + const handleBlur = async () => { + try { + if (value !== property.value) { + console.log(`Updating property: ${property.key}, new value: ${value}`); + // Log the change + const updatedCustomProperties = mergedCustomProperties.map((prop) => + prop.key === property.key ? { ...prop, value } : prop + ); + updateCustomProperties(updatedCustomProperties); + } + } catch (error) { + // Handle errors related to updating + console.error("Error updating custom property:", error); + setEditableError("Failed to update custom property."); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + console.log(`Editing property: ${property.key}, current value: ${e.target.value}`); + }; + + return ( + + ); + }; + return (

- {customProperties.map((element) => ( + {editableError &&
{editableError}
} + {mergedCustomProperties.map((element) => (
{element.key}
- {element.value} +
))} From fb850e1c65519456ee2a13bbc66d969fc0a1264c Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Tue, 18 Mar 2025 19:15:33 +0530 Subject: [PATCH 112/164] Update custom properties on being edited --- .../issues/issue-detail/sidebar.tsx | 22 +++++++++++++++++-- .../issues/peek-overview/properties.tsx | 22 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index ad5b18bd839..07437f25480 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -27,6 +27,7 @@ import { IssueWorklogProperty } from "@/plane-web/components/issues"; // components import type { TIssueOperations } from "./root"; import { ISSUE_ADDITIONAL_PROPERTIES } from "@/constants/issue"; +import { CustomProperty } from "../custom-properties"; type Props = { workspaceSlug: string; @@ -55,12 +56,26 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); const customProperties = issue?.custom_properties || []; + const issue_type_id = issue?.issue_type_id || "defaultIssueTypeId"; + console.log("issue_type_id in sidebar.tsx is", issue_type_id); const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); const maxDate = issue.target_date ? getDate(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); + const handleCustomPropertiesUpdate = async (updatedProperties: CustomProperty[]) => { + try { + console.log("Updating custom properties", updatedProperties); + await issueOperations.update(workspaceSlug, projectId, issueId, { + custom_properties: updatedProperties, + }); + console.log("Custom properties updated successfully"); + } catch (error) { + console.error("Error updating custom properties:", error); + } + }; + return ( <>
@@ -315,8 +330,11 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { ) : null )} -
diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index daf1929351a..c23d22e3f95 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -32,6 +32,7 @@ import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/ import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values"; import { IssueWorklogProperty} from "@/plane-web/components/issues"; import { ISSUE_ADDITIONAL_PROPERTIES } from "@/constants/issue"; +import { CustomProperty } from "../custom-properties"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -57,6 +58,8 @@ export const PeekOverviewProperties: FC = observer((pro const createdByDetails = getUserDetails(issue?.created_by); const projectDetails = getProjectById(issue.project_id); const customProperties = issue?.custom_properties || []; + const issue_type_id = issue?.issue_type_id || "defaultIssueTypeId"; + console.log("issue_type_id in properties.tsx is", issue_type_id); const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); const minDate = getDate(issue.start_date); @@ -65,6 +68,18 @@ export const PeekOverviewProperties: FC = observer((pro const maxDate = getDate(issue.target_date); maxDate?.setDate(maxDate.getDate()); + const handleCustomPropertiesUpdate = async (updatedProperties: CustomProperty[]) => { + try { + console.log("Updating custom properties", updatedProperties); + await issueOperations.update(workspaceSlug, projectId, issueId, { + custom_properties: updatedProperties, + }); + console.log("Custom properties updated successfully"); + } catch (error) { + console.error("Error updating custom properties:", error); + } + }; + return (
Properties
@@ -315,7 +330,12 @@ export const PeekOverviewProperties: FC = observer((pro ) : null )} - +
); From 78ec6d8b0603e84b49a63b3a7da2302eba60efb2 Mon Sep 17 00:00:00 2001 From: sumarpreet-kaur-shipsy Date: Wed, 19 Mar 2025 09:13:12 +0530 Subject: [PATCH 113/164] Add is required in custom properties --- apiserver/plane/db/models/issue_type.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/db/models/issue_type.py b/apiserver/plane/db/models/issue_type.py index 619fb8cfd0d..67d5f9dc1dc 100644 --- a/apiserver/plane/db/models/issue_type.py +++ b/apiserver/plane/db/models/issue_type.py @@ -63,6 +63,7 @@ class IssueTypeCustomProperty(BaseModel): name = models.CharField(max_length=255) value = models.JSONField() is_active = models.BooleanField(default=True) + is_required = models.BooleanField(default=False) issue_type = models.ForeignKey( "db.IssueType", on_delete=models.CASCADE, From be35966e02e531ee5681957110def1359af7cc6c Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Wed, 19 Mar 2025 10:33:57 +0530 Subject: [PATCH 114/164] Prevent leaving required custom properties blank --- .../components/issues/custom-properties.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 4916a0c2ed8..66c6c0d48f1 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -4,6 +4,7 @@ import axios from "axios"; export type CustomProperty = { key: string; value: string; + is_required: boolean; issue_type_custom_property: string; }; @@ -43,6 +44,7 @@ export const CustomProperties: React.FC = ({ customProper key: customProp.name, value: customProperty ? customProperty.value : "", // Use existing value or set empty if not found issue_type_custom_property: customProp.issue_type, + is_required: customProp.is_required, }; }); console.log("CustomProperties is", customProperties); @@ -83,13 +85,20 @@ export const CustomProperties: React.FC = ({ customProper }; return ( - +
+ + + {error &&
{error}
} +
); }; From 9bcc3e9b52dc01116ea1e1dae0822da88dd2b673 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Wed, 19 Mar 2025 10:39:48 +0530 Subject: [PATCH 115/164] Hardcoding issue_type_id for now, revert back later --- web/core/components/issues/issue-detail/sidebar.tsx | 2 +- web/core/components/issues/peek-overview/properties.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 07437f25480..2d1313f4cf3 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -56,7 +56,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); const customProperties = issue?.custom_properties || []; - const issue_type_id = issue?.issue_type_id || "defaultIssueTypeId"; + const issue_type_id = issue?.issue_type_id || "f9f10db2-ea2d-4b43-839c-5fe0c974b0f4"; console.log("issue_type_id in sidebar.tsx is", issue_type_id); const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index c23d22e3f95..237e99809ce 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -58,7 +58,7 @@ export const PeekOverviewProperties: FC = observer((pro const createdByDetails = getUserDetails(issue?.created_by); const projectDetails = getProjectById(issue.project_id); const customProperties = issue?.custom_properties || []; - const issue_type_id = issue?.issue_type_id || "defaultIssueTypeId"; + const issue_type_id = issue?.issue_type_id || "f9f10db2-ea2d-4b43-839c-5fe0c974b0f4"; console.log("issue_type_id in properties.tsx is", issue_type_id); const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); From 6ebc6013394995f84a70099938b27db83ce6e0ac Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Wed, 19 Mar 2025 12:31:17 +0530 Subject: [PATCH 116/164] Fetch issueTypeCustomProperties --- web/core/components/issues/custom-properties.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 66c6c0d48f1..bf1b1d80d13 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -22,7 +22,14 @@ export const CustomProperties: React.FC = ({ customProper useEffect(() => { const getIssueTypeCustomProperties = async () => { try { - const response = await axios.get(`/api/workspaces/${workspaceSlug}/issue-type/${issue_type_id}/custom-properties/`); + const response = await axios.get( + `/api/v1/workspaces/${workspaceSlug}/issue-type/${issue_type_id}/custom-properties/`, + { + headers: { + 'x-api-key': 'TEST_API_TOKEN', + } + } + ); setissueTypeCustomProperties(response.data); console.log("getIssueTypeCustomProperties response is", response); } catch (error) { From 6de5554d6a7b335f5391b10b146186e2c1cd5651 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Wed, 19 Mar 2025 12:48:54 +0530 Subject: [PATCH 117/164] Append issue-type-custom-property id while creating merged properties --- web/core/components/issues/custom-properties.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index bf1b1d80d13..7aa53d052bf 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -4,8 +4,8 @@ import axios from "axios"; export type CustomProperty = { key: string; value: string; - is_required: boolean; issue_type_custom_property: string; + is_required: boolean; }; type CustomPropertiesProps = { @@ -50,7 +50,7 @@ export const CustomProperties: React.FC = ({ customProper return { key: customProp.name, value: customProperty ? customProperty.value : "", // Use existing value or set empty if not found - issue_type_custom_property: customProp.issue_type, + issue_type_custom_property: customProp.id, is_required: customProp.is_required, }; }); From 565385f70d55355afdc254e9caa5f46b9093bc61 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Wed, 19 Mar 2025 16:41:09 +0530 Subject: [PATCH 118/164] Added ApiView for updating custom property values in issue_custom_properties --- apiserver/plane/api/views/issue.py | 65 +++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index f5d3f49fd1b..2447569c46c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -19,6 +19,7 @@ Subquery, ) from django.utils import timezone +from django.shortcuts import get_object_or_404 # Third party imports from rest_framework import status @@ -33,7 +34,8 @@ IssueLinkSerializer, IssueSerializer, LabelSerializer, - IssueTypeSerializer + IssueTypeSerializer, + IssueCustomPropertySerializer ) from plane.app.permissions import ( ProjectEntityPermission, @@ -52,7 +54,8 @@ Project, ProjectMember, CycleIssue, - IssueType + IssueType, + IssueCustomProperty ) from plane.utils.issue_filters import issue_filters from .base import BaseAPIView @@ -1160,4 +1163,62 @@ def get(self, request, slug, project_id, issue_id): serializer = IssueAttachmentSerializer(issue_attachments, many=True) return Response(serializer.data, status=status.HTTP_200_OK) +class IssueCustomPropertyUpdateAPIView(BaseAPIView): + """ + This view handles the update of custom property values for issues. + """ + + model = IssueCustomProperty + serializer_class = IssueCustomPropertySerializer + + def patch(self, request, slug, issue_id, pk): + """ + Partially update a custom property value for a specific issue. + """ + # Fetch the custom property using primary key (pk) and issue_id + custom_property = get_object_or_404(IssueCustomProperty, pk=pk, issue_id=issue_id) + + # Ensure 'value' is in the request data + new_value = request.data.get('value') + if not new_value: + return Response( + {"error": "The value field is required."}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Update the value field with the new value + custom_property.value = new_value + custom_property.save() + + # Serialize the updated custom property + serializer = self.serializer_class(custom_property) + + # Track the update activity (similar to other views) + # requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + # current_instance = json.dumps(serializer.data, cls=DjangoJSONEncoder) + # issue_activity.delay( + # type="custom_property.activity.updated", + # requested_data=requested_data, + # actor_id=str(request.user.id), + # issue_id=str(issue_id), + # project_id=str(self.kwargs.get("project_id")), + # current_instance=current_instance, + # epoch=int(timezone.now().timestamp()), + # ) + + # Return the updated data as the response + return Response(serializer.data, status=status.HTTP_200_OK) + + def get(self, request, slug, issue_id, pk=None): + """ + Retrieve the custom property details for a given issue. + """ + if pk: + custom_property = get_object_or_404(IssueCustomProperty, pk=pk, issue_id=issue_id) + serializer = self.serializer_class(custom_property) + return Response(serializer.data, status=status.HTTP_200_OK) + # If no primary key is given, return a list of all custom properties for the issue + custom_properties = IssueCustomProperty.objects.filter(issue_id=issue_id) + serializer = self.serializer_class(custom_properties, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file From a9a66c9dbcc2af4371f21dc2216c845159bb4769 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Wed, 19 Mar 2025 16:41:47 +0530 Subject: [PATCH 119/164] Added api paths for update issueCustomProperties --- apiserver/plane/api/urls/issue.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index afaec678f00..87d705f57c6 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -7,7 +7,8 @@ IssueActivityAPIEndpoint, WorkspaceIssueAPIEndpoint, IssueAttachmentV2Endpoint, - IssueTypeAPIEndpoint + IssueTypeAPIEndpoint, + IssueCustomPropertyUpdateAPIView ) urlpatterns = [ @@ -75,5 +76,15 @@ "workspaces//projects//issues//issue-attachments//", IssueAttachmentV2Endpoint.as_view(), name="attachment", - ) + ), + path( + 'workspaces//issues//custom-properties//', + IssueCustomPropertyUpdateAPIView.as_view(), + name="update-issue-custom-property", + ), + path( + 'workspaces//issues//custom-properties/', + IssueCustomPropertyUpdateAPIView.as_view(), + name="create-issue-custom-property", + ), ] From 621121f8c403a8d1703f573fe0d5fda4db4beac2 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 19 Mar 2025 16:50:52 +0000 Subject: [PATCH 120/164] fixed profile creation failure --- apiserver/plane/authentication/views/app/magic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 8002e34acf1..4181b96a522 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -218,6 +218,7 @@ def post(self, request): callback=post_user_auth_workflow, ) user = provider.authenticate() + profile, _ = Profile.objects.get_or_create(user=user) # Login the user and record his device info user_login(request=request, user=user, is_app=True) self.add_user_to_workspace(user, workspace) From a7f417756274f138cec3ab0e1daf7d0ffeb6c45d Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:29:07 +0530 Subject: [PATCH 121/164] Add issueCustomProperty Serializer --- apiserver/plane/api/serializers/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index a1e9fe01ec2..7f24f63d8de 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -10,6 +10,7 @@ IssueActivitySerializer, IssueExpandSerializer, IssueLiteSerializer, + IssueCustomPropertySerializer ) from .issue_type import IssueTypeSerializer, IssueTypeCustomPropertySerializer from .state import StateLiteSerializer, StateSerializer From 4474daa91a13e4ca96151de49b95b0dd67fe836b Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:29:29 +0530 Subject: [PATCH 122/164] Get issueCustomProperty id also --- apiserver/plane/api/serializers/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 5aeaaf6208f..72c712e40b9 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -43,7 +43,7 @@ def is_uuid(value): class IssueCustomPropertySerializer(BaseSerializer): class Meta: model = IssueCustomProperty - fields = ["key", "value", "issue_type_custom_property"] + fields = ["key", "value", "issue_type_custom_property", "id"] read_only_fields = [ "id", "issue", From 4febf28ca13e5713b14a0f0219d02f1b79d0972d Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:29:59 +0530 Subject: [PATCH 123/164] Add issueCustomPropertyView --- apiserver/plane/api/views/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index d6b77e99ca2..829ae4c6d3d 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,7 +12,8 @@ IssueLinkAPIEndpoint, IssueCommentAPIEndpoint, IssueActivityAPIEndpoint, - IssueAttachmentEndpoint + IssueAttachmentEndpoint, + IssueCustomPropertyUpdateAPIView ) from .issue_type import IssueTypeAPIEndpoint,IssueTypeCustomPropertyAPIEndpoint from .attachment import IssueAttachmentV2Endpoint From 068bafe67cf7fb6f2387f947a86bbff579231978 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:30:15 +0530 Subject: [PATCH 124/164] Add issueCustomPropertyView --- apiserver/plane/api/views/issue.py | 90 ++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 2447569c46c..20732e53255 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -55,7 +55,8 @@ ProjectMember, CycleIssue, IssueType, - IssueCustomProperty + IssueCustomProperty, + IssueTypeCustomProperty ) from plane.utils.issue_filters import issue_filters from .base import BaseAPIView @@ -1180,11 +1181,8 @@ def patch(self, request, slug, issue_id, pk): # Ensure 'value' is in the request data new_value = request.data.get('value') - if not new_value: - return Response( - {"error": "The value field is required."}, - status=status.HTTP_400_BAD_REQUEST - ) + if new_value is None: + new_value = "" # Update the value field with the new value custom_property.value = new_value @@ -1210,15 +1208,75 @@ def patch(self, request, slug, issue_id, pk): return Response(serializer.data, status=status.HTTP_200_OK) def get(self, request, slug, issue_id, pk=None): - """ - Retrieve the custom property details for a given issue. - """ - if pk: - custom_property = get_object_or_404(IssueCustomProperty, pk=pk, issue_id=issue_id) - serializer = self.serializer_class(custom_property) + """ + Retrieve the custom property details for a given issue. + """ + if pk: + custom_property = get_object_or_404(IssueCustomProperty, pk=pk, issue_id=issue_id) + serializer = self.serializer_class(custom_property) + return Response(serializer.data, status=status.HTTP_200_OK) + + # If no primary key is given, return a list of all custom properties for the issue + custom_properties = IssueCustomProperty.objects.filter(issue_id=issue_id) + serializer = self.serializer_class(custom_properties, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - # If no primary key is given, return a list of all custom properties for the issue - custom_properties = IssueCustomProperty.objects.filter(issue_id=issue_id) - serializer = self.serializer_class(custom_properties, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file + def post(self, request, slug, issue_id): + """ + Create a new custom property for a specific issue. + """ + # Ensure 'key', 'value', and 'issue_type_custom_property' are in the request data + key = request.data.get('key') + value = request.data.get('value') + issue_type_custom_property_id = request.data.get('issue_type_custom_property') + + print(f"Received key: {key}, value: {value}, issue_type_custom_property: {issue_type_custom_property_id}") # Debugging line + + if not key or not value or not issue_type_custom_property_id: + return Response( + {"error": "'key', 'value', and 'issue_type_custom_property' fields are required."}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Get the IssueTypeCustomProperty instance based on the provided ID + try: + print(f"Attempting to retrieve IssueTypeCustomProperty with ID: {issue_type_custom_property_id}") # Debugging line + issue_type_custom_property = IssueTypeCustomProperty.objects.get(id=issue_type_custom_property_id) + except IssueTypeCustomProperty.DoesNotExist: + return Response( + {"error": f"The provided issue_type_custom_property (ID: {issue_type_custom_property_id}) does not exist."}, + status=status.HTTP_404_NOT_FOUND + ) + + # Log the retrieved issue_type_custom_property object (optional) + print(f"Found IssueTypeCustomProperty: {issue_type_custom_property}") # Debugging line + + # Get the Issue instance based on the provided issue_id + try: + issue = Issue.objects.get(id=issue_id) + except Issue.DoesNotExist: + return Response( + {"error": "The provided issue does not exist."}, + status=status.HTTP_404_NOT_FOUND + ) + + if not issue.project: + return Response( + {"error": "The issue must be associated with a project."}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Create the new custom property for the issue + custom_property = IssueCustomProperty.objects.create( + issue=issue, # the related issue + key=key, + value=value, + issue_type_custom_property=issue_type_custom_property, # The type of the custom property + project=issue.project, # Ensure project is set via Issue + ) + + # Serialize the created custom property + serializer = self.serializer_class(custom_property) + + # Return the newly created custom property as a response + return Response(serializer.data, status=status.HTTP_201_CREATED) \ No newline at end of file From a3f810946167bcdfa8a681326c61fea0a86f66fc Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:30:44 +0530 Subject: [PATCH 125/164] Get issueCustomProperty id also --- apiserver/plane/app/serializers/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 2bc4afea4ee..d4c087802db 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -667,7 +667,7 @@ class Meta: class IssueCustomPropertySerializer(BaseSerializer): class Meta: model = IssueCustomProperty - fields = ["key", "value", "issue_type_custom_property"] + fields = ["key", "value", "issue_type_custom_property", "id"] read_only_fields = [ "id", "issue", From 67976a875277dd8f3eda24ce61f84aed33ef5f80 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:31:11 +0530 Subject: [PATCH 126/164] Make custom-properties editable, is_required validation, update custom properties --- .../components/issues/custom-properties.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 7aa53d052bf..3a29d3de362 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -52,6 +52,7 @@ export const CustomProperties: React.FC = ({ customProper value: customProperty ? customProperty.value : "", // Use existing value or set empty if not found issue_type_custom_property: customProp.id, is_required: customProp.is_required, + id: customProperty ? customProperty.id : "", }; }); console.log("CustomProperties is", customProperties); @@ -66,18 +67,23 @@ export const CustomProperties: React.FC = ({ customProper } // Inline editable component for each property - const EditableProperty: React.FC<{ property: CustomProperty }> = ({ property }) => { + const EditableProperty: React.FC<{ property: CustomProperty }> = React.memo(({ property }) => { const [value, setValue] = useState(property.value); - + console.log("Component re-rendering due to state change"); const handleBlur = async () => { try { + console.log("property.is_required is", property.is_required); + if (property.is_required && value.trim() === "") { + setEditableError("This field is required and cannot be left empty or consist of spaces."); + return; // Stop further execution if the field is empty or consists of spaces + } if (value !== property.value) { console.log(`Updating property: ${property.key}, new value: ${value}`); // Log the change - const updatedCustomProperties = mergedCustomProperties.map((prop) => - prop.key === property.key ? { ...prop, value } : prop - ); - updateCustomProperties(updatedCustomProperties); + const updatedProperty = { ...property, value }; + console.log("Property sending to update is", [updatedProperty]); + // Call updateCustomProperties with only the updated property + updateCustomProperties([updatedProperty]); } } catch (error) { // Handle errors related to updating @@ -93,10 +99,10 @@ export const CustomProperties: React.FC = ({ customProper return (
- */} = ({ customProper /> {error &&
{error}
}
- ); - }; + ) + }); return (
@@ -116,7 +122,10 @@ export const CustomProperties: React.FC = ({ customProper {mergedCustomProperties.map((element) => (
- {element.key} + + {element.is_required && * } + {element.key} +
From 813b8de53c9926fbed3a3cafb69ed35560314e0f Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 14:31:22 +0530 Subject: [PATCH 127/164] Editable Custom Properties --- .../issues/issue-detail/sidebar.tsx | 47 ++++++++++++++-- .../issues/peek-overview/properties.tsx | 53 +++++++++++++++++-- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 2d1313f4cf3..862c57719df 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -3,6 +3,7 @@ import React from "react"; import { observer } from "mobx-react"; import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, UserCircle2, Users, Info } from "lucide-react"; +import axios from "axios"; // ui import { ContrastIcon, DiceIcon, DoubleCircleIcon } from "@plane/ui"; // components @@ -56,7 +57,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); const customProperties = issue?.custom_properties || []; - const issue_type_id = issue?.issue_type_id || "f9f10db2-ea2d-4b43-839c-5fe0c974b0f4"; + const issue_type_id = issue?.issue_type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; console.log("issue_type_id in sidebar.tsx is", issue_type_id); const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -67,10 +68,50 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const handleCustomPropertiesUpdate = async (updatedProperties: CustomProperty[]) => { try { console.log("Updating custom properties", updatedProperties); - await issueOperations.update(workspaceSlug, projectId, issueId, { - custom_properties: updatedProperties, + + // Create the necessary data for the API call + const updateRequests = updatedProperties.map((property) => { + const customPropertyId = property?.id || ""; + // const issueIdHardCode = "0d41ef0f-4267-46b6-9ce7-ec207431a1d7"; + const apiUrl = `/api/v1/workspaces/${workspaceSlug}/issues/${issueId}/custom-properties/`; + + // If an ID exists, we PATCH the existing property + if (customPropertyId) { + return axios.patch( + `${apiUrl}${customPropertyId}/`, + { value: property.value }, + { + headers: { + 'x-api-key': 'TEST_API_TOKEN', + 'Content-Type': 'application/json' + } + } + ); + } else { + // If no ID exists, we POST to create a new custom property + return axios.post( + apiUrl, + { + key: property.key, + value: property.value, + issue_type_custom_property: property.issue_type_custom_property, // Include this if needed + }, + { + headers: { + 'x-api-key': 'TEST_API_TOKEN', + 'Content-Type': 'application/json' + } + } + ); + } }); + + // Wait for all requests to complete + await Promise.all(updateRequests); + console.log("Custom properties updated successfully"); + // Optionally, you can refresh the custom properties after the update + // You can either trigger a re-fetch from the server or update the local state } catch (error) { console.error("Error updating custom properties:", error); } diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index 237e99809ce..df18f8de967 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -3,6 +3,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2, Info } from "lucide-react"; +import axios from "axios"; // hooks // ui icons import { DiceIcon, DoubleCircleIcon, ContrastIcon } from "@plane/ui"; @@ -58,7 +59,7 @@ export const PeekOverviewProperties: FC = observer((pro const createdByDetails = getUserDetails(issue?.created_by); const projectDetails = getProjectById(issue.project_id); const customProperties = issue?.custom_properties || []; - const issue_type_id = issue?.issue_type_id || "f9f10db2-ea2d-4b43-839c-5fe0c974b0f4"; + const issue_type_id = issue?.issue_type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; console.log("issue_type_id in properties.tsx is", issue_type_id); const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); @@ -71,15 +72,59 @@ export const PeekOverviewProperties: FC = observer((pro const handleCustomPropertiesUpdate = async (updatedProperties: CustomProperty[]) => { try { console.log("Updating custom properties", updatedProperties); - await issueOperations.update(workspaceSlug, projectId, issueId, { - custom_properties: updatedProperties, + + // Create the necessary data for the API call + const updateRequests = updatedProperties.map((property) => { + const customPropertyId = property?.id || ""; + console.log("customPropertyId is", customPropertyId); + const apiUrl = `/api/v1/workspaces/${workspaceSlug}/issues/${issueId}/custom-properties/`; + + // If an ID exists, we PATCH the existing property + if (customPropertyId) { + console.log("patch"); + console.log("property in patch is", property); + return axios.patch( + `${apiUrl}${customPropertyId}/`, + { value: property.value }, + { + headers: { + 'x-api-key': 'TEST_API_TOKEN', + 'Content-Type': 'application/json' + } + } + ); + } else { + console.log("post"); + console.log("property in post is", property); + // If no ID exists, we POST to create a new custom property + return axios.post( + apiUrl, + { + key: property.key, + value: property.value, + issue_type_custom_property: property.issue_type_custom_property, // Include this if needed + }, + { + headers: { + 'x-api-key': 'TEST_API_TOKEN', + 'Content-Type': 'application/json' + } + } + ); + } }); + + // Wait for all requests to complete + await Promise.all(updateRequests); + console.log("Custom properties updated successfully"); + // Optionally, you can refresh the custom properties after the update + // You can either trigger a re-fetch from the server or update the local state } catch (error) { console.error("Error updating custom properties:", error); } }; - + return (
Properties
From f266562fb7e5f04a821a58b80ce4c0f31007861f Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 20 Mar 2025 09:58:08 +0000 Subject: [PATCH 128/164] Fix for the redirecting to set password page issue --- .../plane/authentication/views/app/magic.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 8002e34acf1..4fc49c7aa93 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -118,7 +118,7 @@ def add_to_workspace(self, workspace, user): }) user.profile.is_tour_completed = True user.profile.is_onboarded = True - user.is_password_autoset = True + user.is_password_autoset = False user.profile.company_name = workspace.name user.save() user.profile.save() @@ -243,15 +243,12 @@ def post(self, request): # Login the user and record his device info self.add_user_to_workspace(user, workspace) user_login(request=request, user=user, is_app=True) - if user.is_password_autoset and profile.is_onboarded: - path = "accounts/set-password" - else: - # Get the redirection path - path = ( - str(next_path) - if next_path - else "/" + workspace - ) + # Get the redirection path + path = ( + str(next_path) + if next_path + else "/" + workspace + ) # redirect to referer path url = urljoin(base_host(request=request, is_app=True), path) if app_url: From 3baa08cd02342d349499975974bbf3cf995318e9 Mon Sep 17 00:00:00 2001 From: naman-agrawal-shipsy Date: Thu, 20 Mar 2025 16:49:53 +0530 Subject: [PATCH 129/164] display-property-std -- code --- apiserver/plane/db/models/issue.py | 6 ++ .../spreadsheet/columns/index.ts | 1 + .../columns/standard-property-column.tsx | 19 ++++++ .../spreadsheet/issue-column.tsx | 1 + web/core/constants/issue.ts | 11 ++++ web/core/constants/spreadsheet.ts | 62 +++++++++++++++++++ web/helpers/issue.helper.ts | 6 ++ 7 files changed, 106 insertions(+) create mode 100644 web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 6360d1fa3aa..d454fbef999 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -76,6 +76,12 @@ def get_default_display_properties(): "state": True, "sub_issue_count": True, "updated_on": True, + "hub_code": True, + "customer_code": True, + "worker_code": True, + "vendor_code": True, + "trip_reference_number": True, + "reference_number": True, } diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts b/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts index 3439d398b41..eec02b8ca18 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts @@ -12,3 +12,4 @@ export * from "./sub-issue-column"; export * from "./updated-on-column"; export * from "./module-column"; export * from "./cycle-column"; +export * from "./standard-property-column"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx new file mode 100644 index 00000000000..6977a6b914c --- /dev/null +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { observer } from "mobx-react"; +// types +import { TIssue } from "@plane/types"; +import { Row } from "@plane/ui"; + +type Props = { + issue: TIssue; +}; + +export const SpreadsheetStandardPropertyColumn: React.FC = observer((props) => { + const { issue, property } = props; + + return ( + + {issue?.[property]} + + ); +}); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx index dcd8773a0f1..9b8fb27e3db 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -43,6 +43,7 @@ export const IssueColumn = observer((props: Props) => { > , updates: any) => updateIssue && updateIssue(issue.project_id, issue.id, data).then(() => { diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 8629c05a253..97fae866e69 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -109,6 +109,15 @@ export const ISSUE_FILTER_OPTIONS: { // { key: "draft", title: "Draft Issues" }, ]; +const whileListedCustomProperties = [ + { key: "hub_code", title: "Hub Code" }, + { key: "customer_code", title: "Customer Code" }, + { key: "worker_code", title: "Worker Code" }, + { key: "vendor_code", title: "Vendor Code" }, + { key: "trip_reference_number", title: "Trip Reference Number" }, + { key: "reference_number", title: "Reference Number" }, +]; + export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [ "assignee", "start_date", @@ -126,6 +135,7 @@ export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = "modules", "cycle", "issue_type", + ...whileListedCustomProperties.map(prop => prop.key), ]; export const ISSUE_DISPLAY_PROPERTIES: { @@ -146,6 +156,7 @@ export const ISSUE_DISPLAY_PROPERTIES: { { key: "estimate", title: "Estimate" }, { key: "modules", title: "Modules" }, { key: "cycle", title: "Cycle" }, + ...whileListedCustomProperties, ]; export const ISSUE_EXTRA_OPTIONS: { diff --git a/web/core/constants/spreadsheet.ts b/web/core/constants/spreadsheet.ts index e4e8d8ee57b..b78a3bc1cea 100644 --- a/web/core/constants/spreadsheet.ts +++ b/web/core/constants/spreadsheet.ts @@ -11,6 +11,7 @@ import { CalendarCheck2, CalendarClock, Users, + Tags, } from "lucide-react"; // types import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types"; @@ -32,6 +33,7 @@ import { SpreadsheetStateColumn, SpreadsheetSubIssueColumn, SpreadsheetUpdatedOnColumn, + SpreadsheetStandardPropertyColumn, } from "@/components/issues/issue-layouts/spreadsheet"; export const SPREADSHEET_PROPERTY_DETAILS: { @@ -176,6 +178,60 @@ export const SPREADSHEET_PROPERTY_DETAILS: { icon: LayersIcon, Column: SpreadsheetSubIssueColumn, }, + hub_code: { + title: "Hub Code", + ascendingOrderKey: "hub_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-hub_code", + descendingOrderTitle: "Z", + icon: DoubleCircleIcon, + Column: SpreadsheetStandardPropertyColumn, + }, + customer_code: { + title: "Customer Code", + ascendingOrderKey: "customer_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-customer_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + worker_code: { + title: "Worker Code", + ascendingOrderKey: "worker_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-worker_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + vendor_code: { + title: "Vendor Code", + ascendingOrderKey: "vendor_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-vendor_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + trip_reference_number: { + title: "Trip Reference Number", + ascendingOrderKey: "trip_reference_number", + ascendingOrderTitle: "A", + descendingOrderKey: "-trip_reference_number", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + reference_number: { + title: "Reference Number", + ascendingOrderKey: "reference_number", + ascendingOrderTitle: "A", + descendingOrderKey: "-reference_number", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, }; export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [ @@ -193,6 +249,12 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [ "link", "attachment_count", "sub_issue_count", + "hub_code", + "customer_code", + "worker_code", + "vendor_code", + "trip_reference_number", + "reference_number", ]; export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues"; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index d1fc970185e..ca9d6afd681 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -307,4 +307,10 @@ export const getComputedDisplayProperties = ( modules: displayProperties?.modules ?? true, cycle: displayProperties?.cycle ?? true, issue_type: displayProperties?.issue_type ?? true, + hub_code: displayProperties?.hub_code ?? true, + customer_code: displayProperties?.customer_code ?? true, + worker_code: displayProperties?.worker_code ?? true, + vendor_code: displayProperties?.vendor_code ?? true, + trip_reference_number: displayProperties?.trip_reference_number ?? true, + reference_number: displayProperties?.reference_number ?? true, }); From 9bf0eef3c289d075875fe939b0a6576c1f5fc90b Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 17:51:40 +0530 Subject: [PATCH 130/164] Added task for creating custom property update activity --- .../plane/bgtasks/issue_activities_task.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index c4a3ce05033..e5220a8ac99 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -1610,6 +1610,34 @@ def create_inbox_activity( ) +# Track custom property +def track_custom_property( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + if current_instance.get("value") != requested_data.get("value"): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="updated", + old_value=current_instance.get("value"), + new_value=requested_data.get("value"), + field="custom_property", + project_id=project_id, + workspace_id=workspace_id, + comment="updated the custom property to", + epoch=epoch, + ) + ) + + # Receive message from room group @shared_task def issue_activity( @@ -1672,6 +1700,7 @@ def issue_activity( "issue_draft.activity.updated": update_draft_issue_activity, "issue_draft.activity.deleted": delete_draft_issue_activity, "inbox.activity.created": create_inbox_activity, + "custom_property.activity.updated": track_custom_property, } func = ACTIVITY_MAPPER.get(type) From 2b951d73dcd2e0e438ec6246ac6dc7b7f2afed9e Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 17:52:11 +0530 Subject: [PATCH 131/164] Create activity for custom property update --- apiserver/plane/api/views/issue.py | 66 ++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 20732e53255..7ef604b5427 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -56,7 +56,8 @@ CycleIssue, IssueType, IssueCustomProperty, - IssueTypeCustomProperty + IssueTypeCustomProperty, + Workspace ) from plane.utils.issue_filters import issue_filters from .base import BaseAPIView @@ -1184,25 +1185,56 @@ def patch(self, request, slug, issue_id, pk): if new_value is None: new_value = "" - # Update the value field with the new value - custom_property.value = new_value - custom_property.save() - # Serialize the updated custom property serializer = self.serializer_class(custom_property) + # Track the update activity (uncommented) + requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + # Log the requested data to verify what was received in the request + print(f"Requested data: {requested_data}") + # Serialize the current instance (updated custom property) and log it + current_instance = json.dumps(serializer.data, cls=DjangoJSONEncoder) + print(f"Current instance (updated custom property): {current_instance}") + # Log the user ID to verify the actor + actor_id = str(request.user.id) if request.user else "Unknown" + print(f"Actor ID (user making the update): {actor_id}") + # Log the issue ID to verify the issue being updated + issue_id_str = str(issue_id) if issue_id else "Unknown issue ID" + print(f"Issue ID being updated: {issue_id_str}") + # Log the project ID being passed to verify its correctness + print(f"self.kwargs are {self.kwargs}") + slug = slug=self.kwargs.get("slug") + print(f"slug is {slug}") + workspace = Workspace.objects.get(slug=slug) + print(f"Workspace is {workspace}") + try: + project = Project.objects.get(workspace=workspace) + project_id_str = project.id + print(f"Project ID: {project_id_str}") + except Project.DoesNotExist: + return Response({"error": "Project not found."}, status=status.HTTP_404_NOT_FOUND) + # project_id_str = str(self.kwargs.get("project_id")) if self.kwargs.get("project_id") else "Unknown project ID" + print(f"Project ID: {project_id_str}") + # Log the timestamp to verify the epoch + epoch_timestamp = int(timezone.now().timestamp()) + print(f"Epoch timestamp: {epoch_timestamp}") + # Trigger the issue activity background task + print("Triggering issue activity tracking...") + issue_activity.delay( + type="custom_property.activity.updated", + requested_data=requested_data, + actor_id=actor_id, + issue_id=issue_id_str, + project_id=project_id_str, + current_instance=current_instance, + epoch=epoch_timestamp, + ) + + # Log completion of the background task trigger + print("Issue activity background task triggered successfully.") - # Track the update activity (similar to other views) - # requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) - # current_instance = json.dumps(serializer.data, cls=DjangoJSONEncoder) - # issue_activity.delay( - # type="custom_property.activity.updated", - # requested_data=requested_data, - # actor_id=str(request.user.id), - # issue_id=str(issue_id), - # project_id=str(self.kwargs.get("project_id")), - # current_instance=current_instance, - # epoch=int(timezone.now().timestamp()), - # ) + # Update the value field with the new value + custom_property.value = new_value + custom_property.save() # Return the updated data as the response return Response(serializer.data, status=status.HTTP_200_OK) From 6035905b1e4990441a02f0d1d81d3fb8358d88be Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 20 Mar 2025 17:52:27 +0530 Subject: [PATCH 132/164] Make custom proerties editable --- .../components/issues/custom-properties.tsx | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 3a29d3de362..66b9e01ea9d 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -31,9 +31,7 @@ export const CustomProperties: React.FC = ({ customProper } ); setissueTypeCustomProperties(response.data); - console.log("getIssueTypeCustomProperties response is", response); } catch (error) { - console.error("Error fetching custom properties:", error); setError("Failed to load custom properties."); } }; @@ -42,21 +40,18 @@ export const CustomProperties: React.FC = ({ customProper }, [workspaceSlug, issue_type_id]); const mergedCustomProperties = issueTypeCustomProperties.map((customProp) => { - // Find the corresponding property in customProperties const customProperty = customProperties?.find( (prop) => prop.key === customProp.name ); return { key: customProp.name, - value: customProperty ? customProperty.value : "", // Use existing value or set empty if not found + value: customProperty ? customProperty.value : "", issue_type_custom_property: customProp.id, is_required: customProp.is_required, id: customProperty ? customProperty.id : "", }; }); - console.log("CustomProperties is", customProperties); - console.log("mergedCustomProperties is", mergedCustomProperties); if (error) { return
{error}
; @@ -66,43 +61,29 @@ export const CustomProperties: React.FC = ({ customProper return null; } - // Inline editable component for each property const EditableProperty: React.FC<{ property: CustomProperty }> = React.memo(({ property }) => { const [value, setValue] = useState(property.value); - console.log("Component re-rendering due to state change"); const handleBlur = async () => { try { - console.log("property.is_required is", property.is_required); if (property.is_required && value.trim() === "") { setEditableError("This field is required and cannot be left empty or consist of spaces."); - return; // Stop further execution if the field is empty or consists of spaces + return; } if (value !== property.value) { - console.log(`Updating property: ${property.key}, new value: ${value}`); - // Log the change const updatedProperty = { ...property, value }; - console.log("Property sending to update is", [updatedProperty]); - // Call updateCustomProperties with only the updated property updateCustomProperties([updatedProperty]); } } catch (error) { - // Handle errors related to updating - console.error("Error updating custom property:", error); setEditableError("Failed to update custom property."); } }; const handleChange = (e: React.ChangeEvent) => { setValue(e.target.value); - console.log(`Editing property: ${property.key}, current value: ${e.target.value}`); }; return (
- {/* */} Date: Thu, 20 Mar 2025 17:52:55 +0530 Subject: [PATCH 133/164] Update custom properties when edited --- .../issues/issue-detail/sidebar.tsx | 16 +------------- .../issues/peek-overview/properties.tsx | 21 +------------------ 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 862c57719df..d38f3807215 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -67,15 +67,9 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const handleCustomPropertiesUpdate = async (updatedProperties: CustomProperty[]) => { try { - console.log("Updating custom properties", updatedProperties); - - // Create the necessary data for the API call const updateRequests = updatedProperties.map((property) => { const customPropertyId = property?.id || ""; - // const issueIdHardCode = "0d41ef0f-4267-46b6-9ce7-ec207431a1d7"; const apiUrl = `/api/v1/workspaces/${workspaceSlug}/issues/${issueId}/custom-properties/`; - - // If an ID exists, we PATCH the existing property if (customPropertyId) { return axios.patch( `${apiUrl}${customPropertyId}/`, @@ -88,13 +82,12 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { } ); } else { - // If no ID exists, we POST to create a new custom property return axios.post( apiUrl, { key: property.key, value: property.value, - issue_type_custom_property: property.issue_type_custom_property, // Include this if needed + issue_type_custom_property: property.issue_type_custom_property, }, { headers: { @@ -105,15 +98,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { ); } }); - - // Wait for all requests to complete await Promise.all(updateRequests); - - console.log("Custom properties updated successfully"); - // Optionally, you can refresh the custom properties after the update - // You can either trigger a re-fetch from the server or update the local state } catch (error) { - console.error("Error updating custom properties:", error); } }; diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index df18f8de967..3ed23ab027c 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -60,7 +60,6 @@ export const PeekOverviewProperties: FC = observer((pro const projectDetails = getProjectById(issue.project_id); const customProperties = issue?.custom_properties || []; const issue_type_id = issue?.issue_type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; - console.log("issue_type_id in properties.tsx is", issue_type_id); const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); const minDate = getDate(issue.start_date); @@ -71,18 +70,10 @@ export const PeekOverviewProperties: FC = observer((pro const handleCustomPropertiesUpdate = async (updatedProperties: CustomProperty[]) => { try { - console.log("Updating custom properties", updatedProperties); - - // Create the necessary data for the API call const updateRequests = updatedProperties.map((property) => { const customPropertyId = property?.id || ""; - console.log("customPropertyId is", customPropertyId); const apiUrl = `/api/v1/workspaces/${workspaceSlug}/issues/${issueId}/custom-properties/`; - - // If an ID exists, we PATCH the existing property if (customPropertyId) { - console.log("patch"); - console.log("property in patch is", property); return axios.patch( `${apiUrl}${customPropertyId}/`, { value: property.value }, @@ -94,15 +85,12 @@ export const PeekOverviewProperties: FC = observer((pro } ); } else { - console.log("post"); - console.log("property in post is", property); - // If no ID exists, we POST to create a new custom property return axios.post( apiUrl, { key: property.key, value: property.value, - issue_type_custom_property: property.issue_type_custom_property, // Include this if needed + issue_type_custom_property: property.issue_type_custom_property, }, { headers: { @@ -113,15 +101,8 @@ export const PeekOverviewProperties: FC = observer((pro ); } }); - - // Wait for all requests to complete await Promise.all(updateRequests); - - console.log("Custom properties updated successfully"); - // Optionally, you can refresh the custom properties after the update - // You can either trigger a re-fetch from the server or update the local state } catch (error) { - console.error("Error updating custom properties:", error); } }; From cf27a0f1403028f5f061a8b82c54877d1eae6fc9 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 20 Mar 2025 12:23:17 +0000 Subject: [PATCH 134/164] style --- .../spreadsheet/columns/standard-property-column.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx index 6977a6b914c..138c7916172 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx @@ -12,7 +12,7 @@ export const SpreadsheetStandardPropertyColumn: React.FC = observer((prop const { issue, property } = props; return ( - + {issue?.[property]} ); From caf9cf8e6ad0ac93532c11f07da7238985855065 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 20 Mar 2025 09:58:08 +0000 Subject: [PATCH 135/164] Fix for the redirecting to set password page issue --- .../plane/authentication/views/app/magic.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py index 4181b96a522..99c686eb0c7 100644 --- a/apiserver/plane/authentication/views/app/magic.py +++ b/apiserver/plane/authentication/views/app/magic.py @@ -118,7 +118,7 @@ def add_to_workspace(self, workspace, user): }) user.profile.is_tour_completed = True user.profile.is_onboarded = True - user.is_password_autoset = True + user.is_password_autoset = False user.profile.company_name = workspace.name user.save() user.profile.save() @@ -244,15 +244,12 @@ def post(self, request): # Login the user and record his device info self.add_user_to_workspace(user, workspace) user_login(request=request, user=user, is_app=True) - if user.is_password_autoset and profile.is_onboarded: - path = "accounts/set-password" - else: - # Get the redirection path - path = ( - str(next_path) - if next_path - else "/" + workspace - ) + # Get the redirection path + path = ( + str(next_path) + if next_path + else "/" + workspace + ) # redirect to referer path url = urljoin(base_host(request=request, is_app=True), path) if app_url: From b82176c61c4cc86131c88a2db801fa4b7bad1a48 Mon Sep 17 00:00:00 2001 From: naman-agrawal-shipsy Date: Thu, 20 Mar 2025 18:20:36 +0530 Subject: [PATCH 136/164] changes in custom prop column component --- .../spreadsheet/columns/standard-property-column.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx index 6977a6b914c..ae3ee6e4231 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx @@ -2,7 +2,7 @@ import React from "react"; import { observer } from "mobx-react"; // types import { TIssue } from "@plane/types"; -import { Row } from "@plane/ui"; +import { Row, Tooltip } from "@plane/ui"; type Props = { issue: TIssue; @@ -12,8 +12,10 @@ export const SpreadsheetStandardPropertyColumn: React.FC = observer((prop const { issue, property } = props; return ( - - {issue?.[property]} - + + + {issue?.[property]} + + ); }); From e4aa3d901e1fdd17b092cf60576d6b7604eb0692 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 20 Mar 2025 22:13:25 +0000 Subject: [PATCH 137/164] Create and Update Custom Properties Activity --- .../plane/bgtasks/issue_activities_task.py | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index e5220a8ac99..6a7cd5dfc2e 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -1611,7 +1611,7 @@ def create_inbox_activity( # Track custom property -def track_custom_property( +def update_custom_property_activity( requested_data, current_instance, issue_id, @@ -1621,6 +1621,11 @@ def track_custom_property( issue_activities, epoch, ): + if isinstance(current_instance, str): + current_instance = json.loads(current_instance) + if isinstance(requested_data, str): + requested_data = json.loads(requested_data) + custom_property_key = current_instance.get("key", "unknown_key") if current_instance.get("value") != requested_data.get("value"): issue_activities.append( IssueActivity( @@ -1629,15 +1634,47 @@ def track_custom_property( verb="updated", old_value=current_instance.get("value"), new_value=requested_data.get("value"), - field="custom_property", + field=custom_property_key, project_id=project_id, workspace_id=workspace_id, - comment="updated the custom property to", + comment=f"updated the custom property ({custom_property_key}) to", epoch=epoch, ) ) +# Track custom property addition +def create_custom_property_activity( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + if isinstance(current_instance, str): + current_instance = json.loads(current_instance) + if isinstance(requested_data, str): + requested_data = json.loads(requested_data) + custom_property_key = requested_data.get("key", "unknown_key") + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="created", + old_value="", + new_value=requested_data.get("value"), + field=custom_property_key, + project_id=project_id, + workspace_id=workspace_id, + comment=f"added a custom property ({custom_property_key})", + epoch=epoch, + ) + ) + + # Receive message from room group @shared_task def issue_activity( @@ -1700,7 +1737,8 @@ def issue_activity( "issue_draft.activity.updated": update_draft_issue_activity, "issue_draft.activity.deleted": delete_draft_issue_activity, "inbox.activity.created": create_inbox_activity, - "custom_property.activity.updated": track_custom_property, + "custom_property.activity.updated": update_custom_property_activity, + "custom_property.activity.created": create_custom_property_activity, } func = ACTIVITY_MAPPER.get(type) From ceda621fd05e1ec824886b5d10d63946aab477d8 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Thu, 20 Mar 2025 22:13:47 +0000 Subject: [PATCH 138/164] Track activity for update and create custom property --- apiserver/plane/api/views/issue.py | 75 ++++++++---------------------- 1 file changed, 19 insertions(+), 56 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 7ef604b5427..133f252a460 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1177,49 +1177,24 @@ def patch(self, request, slug, issue_id, pk): """ Partially update a custom property value for a specific issue. """ - # Fetch the custom property using primary key (pk) and issue_id custom_property = get_object_or_404(IssueCustomProperty, pk=pk, issue_id=issue_id) - - # Ensure 'value' is in the request data new_value = request.data.get('value') if new_value is None: new_value = "" - - # Serialize the updated custom property serializer = self.serializer_class(custom_property) - # Track the update activity (uncommented) requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) - # Log the requested data to verify what was received in the request - print(f"Requested data: {requested_data}") - # Serialize the current instance (updated custom property) and log it current_instance = json.dumps(serializer.data, cls=DjangoJSONEncoder) - print(f"Current instance (updated custom property): {current_instance}") - # Log the user ID to verify the actor actor_id = str(request.user.id) if request.user else "Unknown" - print(f"Actor ID (user making the update): {actor_id}") - # Log the issue ID to verify the issue being updated issue_id_str = str(issue_id) if issue_id else "Unknown issue ID" - print(f"Issue ID being updated: {issue_id_str}") - # Log the project ID being passed to verify its correctness - print(f"self.kwargs are {self.kwargs}") slug = slug=self.kwargs.get("slug") - print(f"slug is {slug}") workspace = Workspace.objects.get(slug=slug) - print(f"Workspace is {workspace}") try: project = Project.objects.get(workspace=workspace) project_id_str = project.id - print(f"Project ID: {project_id_str}") except Project.DoesNotExist: return Response({"error": "Project not found."}, status=status.HTTP_404_NOT_FOUND) - # project_id_str = str(self.kwargs.get("project_id")) if self.kwargs.get("project_id") else "Unknown project ID" - print(f"Project ID: {project_id_str}") - # Log the timestamp to verify the epoch epoch_timestamp = int(timezone.now().timestamp()) - print(f"Epoch timestamp: {epoch_timestamp}") - # Trigger the issue activity background task - print("Triggering issue activity tracking...") - issue_activity.delay( + issue_activity( type="custom_property.activity.updated", requested_data=requested_data, actor_id=actor_id, @@ -1228,15 +1203,8 @@ def patch(self, request, slug, issue_id, pk): current_instance=current_instance, epoch=epoch_timestamp, ) - - # Log completion of the background task trigger - print("Issue activity background task triggered successfully.") - - # Update the value field with the new value custom_property.value = new_value custom_property.save() - - # Return the updated data as the response return Response(serializer.data, status=status.HTTP_200_OK) def get(self, request, slug, issue_id, pk=None): @@ -1248,7 +1216,6 @@ def get(self, request, slug, issue_id, pk=None): serializer = self.serializer_class(custom_property) return Response(serializer.data, status=status.HTTP_200_OK) - # If no primary key is given, return a list of all custom properties for the issue custom_properties = IssueCustomProperty.objects.filter(issue_id=issue_id) serializer = self.serializer_class(custom_properties, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -1257,33 +1224,21 @@ def post(self, request, slug, issue_id): """ Create a new custom property for a specific issue. """ - # Ensure 'key', 'value', and 'issue_type_custom_property' are in the request data key = request.data.get('key') value = request.data.get('value') issue_type_custom_property_id = request.data.get('issue_type_custom_property') - - print(f"Received key: {key}, value: {value}, issue_type_custom_property: {issue_type_custom_property_id}") # Debugging line - if not key or not value or not issue_type_custom_property_id: return Response( {"error": "'key', 'value', and 'issue_type_custom_property' fields are required."}, status=status.HTTP_400_BAD_REQUEST ) - - # Get the IssueTypeCustomProperty instance based on the provided ID try: - print(f"Attempting to retrieve IssueTypeCustomProperty with ID: {issue_type_custom_property_id}") # Debugging line issue_type_custom_property = IssueTypeCustomProperty.objects.get(id=issue_type_custom_property_id) except IssueTypeCustomProperty.DoesNotExist: return Response( {"error": f"The provided issue_type_custom_property (ID: {issue_type_custom_property_id}) does not exist."}, status=status.HTTP_404_NOT_FOUND ) - - # Log the retrieved issue_type_custom_property object (optional) - print(f"Found IssueTypeCustomProperty: {issue_type_custom_property}") # Debugging line - - # Get the Issue instance based on the provided issue_id try: issue = Issue.objects.get(id=issue_id) except Issue.DoesNotExist: @@ -1291,24 +1246,32 @@ def post(self, request, slug, issue_id): {"error": "The provided issue does not exist."}, status=status.HTTP_404_NOT_FOUND ) - if not issue.project: return Response( {"error": "The issue must be associated with a project."}, status=status.HTTP_400_BAD_REQUEST ) - - # Create the new custom property for the issue custom_property = IssueCustomProperty.objects.create( - issue=issue, # the related issue + issue=issue, key=key, value=value, - issue_type_custom_property=issue_type_custom_property, # The type of the custom property - project=issue.project, # Ensure project is set via Issue + issue_type_custom_property=issue_type_custom_property, + project=issue.project, ) - - # Serialize the created custom property serializer = self.serializer_class(custom_property) - - # Return the newly created custom property as a response + requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) + current_instance = json.dumps(serializer.data, cls=DjangoJSONEncoder) + actor_id = str(request.user.id) if request.user else "Unknown" + issue_id_str = str(issue_id) if issue_id else "Unknown issue ID" + project_id_str = str(issue.project.id) + epoch_timestamp = int(timezone.now().timestamp()) + issue_activity( + type="custom_property.activity.created", + requested_data=requested_data, + actor_id=actor_id, + issue_id=issue_id_str, + project_id=project_id_str, + current_instance=current_instance, + epoch=epoch_timestamp, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) \ No newline at end of file From 06a3cc4d7dfd7ad142e5305f233cfa97d33d47f1 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Fri, 21 Mar 2025 04:51:39 +0000 Subject: [PATCH 139/164] Get issue_type_id from IssueDetail and Editable Custom Properties --- web/core/components/issues/peek-overview/properties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index 3ed23ab027c..e057c594f8f 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -59,7 +59,7 @@ export const PeekOverviewProperties: FC = observer((pro const createdByDetails = getUserDetails(issue?.created_by); const projectDetails = getProjectById(issue.project_id); const customProperties = issue?.custom_properties || []; - const issue_type_id = issue?.issue_type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; + const issue_type_id = issue?.type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); const minDate = getDate(issue.start_date); From 71a21cdc7853be4a6619bdf3fa97036404a54705 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Fri, 21 Mar 2025 05:13:26 +0000 Subject: [PATCH 140/164] Get issue_type_id from IssueDetails and EditableCustomProperties --- web/core/components/issues/issue-detail/sidebar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index d38f3807215..9b68b6ed3dc 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -57,8 +57,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); const customProperties = issue?.custom_properties || []; - const issue_type_id = issue?.issue_type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; - console.log("issue_type_id in sidebar.tsx is", issue_type_id); + const issue_type_id = issue?.type_id || "afd30f86-5ae5-428c-aa3a-e633ea973740"; const minDate = issue.start_date ? getDate(issue.start_date) : null; minDate?.setDate(minDate.getDate()); From 964d66b86166ad95084a0508dea401264b27dd42 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Fri, 21 Mar 2025 05:49:40 +0000 Subject: [PATCH 141/164] Get issue_type_id in IssueSerializer also --- apiserver/plane/app/serializers/issue.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index d4c087802db..9a4343ff74c 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -733,6 +733,7 @@ class Meta: "link_count", "is_draft", "archived_at", + "type_id", ] read_only_fields = fields From aab6dcf2dff0e11f063fe09e220c42cc47ff64d8 Mon Sep 17 00:00:00 2001 From: vignesh-kandula-shipsy Date: Fri, 21 Mar 2025 12:39:50 +0530 Subject: [PATCH 142/164] Disable the dropdown of workspaces --- web/core/components/workspace/sidebar/dropdown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index 15d2a079df7..1310618f191 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -105,6 +105,7 @@ export const SidebarDropdown = observer(() => { {({ open }) => ( <> Date: Fri, 21 Mar 2025 12:54:41 +0530 Subject: [PATCH 143/164] hub_code icon change --- web/core/constants/spreadsheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/constants/spreadsheet.ts b/web/core/constants/spreadsheet.ts index b78a3bc1cea..76dba528bb5 100644 --- a/web/core/constants/spreadsheet.ts +++ b/web/core/constants/spreadsheet.ts @@ -184,7 +184,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-hub_code", descendingOrderTitle: "Z", - icon: DoubleCircleIcon, + icon: Tags, Column: SpreadsheetStandardPropertyColumn, }, customer_code: { From 2b96095dfdc1b556575fa9d4af80344341c6b811 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Fri, 21 Mar 2025 07:36:22 +0000 Subject: [PATCH 144/164] hide tooltip when no content --- .../spreadsheet/columns/standard-property-column.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx index ae3ee6e4231..4974f6c38e2 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx @@ -12,7 +12,7 @@ export const SpreadsheetStandardPropertyColumn: React.FC = observer((prop const { issue, property } = props; return ( - + {issue?.[property]} From bb685174bd7ef761abbd4b13980e33044669973d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Fri, 21 Mar 2025 07:39:19 +0000 Subject: [PATCH 145/164] small-fix --- web/core/constants/spreadsheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/constants/spreadsheet.ts b/web/core/constants/spreadsheet.ts index 76dba528bb5..14e68aba4be 100644 --- a/web/core/constants/spreadsheet.ts +++ b/web/core/constants/spreadsheet.ts @@ -215,7 +215,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { Column: SpreadsheetStandardPropertyColumn, }, trip_reference_number: { - title: "Trip Reference Number", + title: "Trip Ref Number", ascendingOrderKey: "trip_reference_number", ascendingOrderTitle: "A", descendingOrderKey: "-trip_reference_number", From 8c7bbe1c96c643cba4f9583f465046877be6ff45 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Fri, 21 Mar 2025 16:21:40 +0530 Subject: [PATCH 146/164] Create celery job for custom property update activity tracking --- apiserver/plane/api/views/issue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 133f252a460..8e17558074c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1194,7 +1194,7 @@ def patch(self, request, slug, issue_id, pk): except Project.DoesNotExist: return Response({"error": "Project not found."}, status=status.HTTP_404_NOT_FOUND) epoch_timestamp = int(timezone.now().timestamp()) - issue_activity( + issue_activity.delay( type="custom_property.activity.updated", requested_data=requested_data, actor_id=actor_id, @@ -1265,7 +1265,7 @@ def post(self, request, slug, issue_id): issue_id_str = str(issue_id) if issue_id else "Unknown issue ID" project_id_str = str(issue.project.id) epoch_timestamp = int(timezone.now().timestamp()) - issue_activity( + issue_activity.delay( type="custom_property.activity.created", requested_data=requested_data, actor_id=actor_id, From b41137f0808ef154014a5b10df8be36f98734ef9 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Fri, 21 Mar 2025 18:42:08 +0530 Subject: [PATCH 147/164] UI Improvements in Edit Custom Properties --- .../components/issues/custom-properties.tsx | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 66b9e01ea9d..3ed5d44e27b 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -1,4 +1,6 @@ import React, { useState, useEffect } from "react"; +import { Pencil } from "lucide-react"; +import { Tooltip } from "@plane/ui"; import axios from "axios"; export type CustomProperty = { @@ -19,6 +21,7 @@ export const CustomProperties: React.FC = ({ customProper const [issueTypeCustomProperties, setissueTypeCustomProperties] = useState([]); const [error, setError] = useState(null); const [editableError, setEditableError] = useState(null); + const [hoveredPropertyKey, setHoveredPropertyKey] = useState(null); useEffect(() => { const getIssueTypeCustomProperties = async () => { try { @@ -91,28 +94,46 @@ export const CustomProperties: React.FC = ({ customProper onBlur={handleBlur} className="text-sm border rounded px-1 py-0.5" /> - {error &&
{error}
} + {editableError &&
{editableError}
}
) }); - return ( -
-
- {editableError &&
{editableError}
} - {mergedCustomProperties.map((element) => ( -
-
- - {element.is_required && * } - {element.key} - + return ( +
+
+ {editableError &&
{editableError}
} + {mergedCustomProperties.map((element) => ( +
setHoveredPropertyKey(element.key)} + onMouseLeave={() => setHoveredPropertyKey(null)} + > +
+ + {element.is_required && * } + {element.value ? ( + {element.value} + ) : ( + Add {element.key} + )} + +
+
+ {hoveredPropertyKey === element.key && ( + +
+ +
+
+ )} + {hoveredPropertyKey === element.key && ( + + )} +
-
- -
-
- ))} -
- ); -}; + ))} +
+ ); + }; From c1356529d889c008071e4cf0e6c1714aa9aa6c3b Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Fri, 21 Mar 2025 14:12:34 +0000 Subject: [PATCH 148/164] UI Improvement for Editable Custom Properties --- .../components/issues/custom-properties.tsx | 187 +++++++++++------- 1 file changed, 117 insertions(+), 70 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 3ed5d44e27b..747c7bced40 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; import { Pencil } from "lucide-react"; -import { Tooltip } from "@plane/ui"; import axios from "axios"; export type CustomProperty = { @@ -11,17 +10,24 @@ export type CustomProperty = { }; type CustomPropertiesProps = { - customProperties?: CustomProperty[]; + customProperties?: CustomProperty[]; issue_type_id: string; workspaceSlug: string; updateCustomProperties: (updatedProperties: CustomProperty[]) => void; }; -export const CustomProperties: React.FC = ({ customProperties, issue_type_id, workspaceSlug, updateCustomProperties }) => { +export const CustomProperties: React.FC = ({ + customProperties, + issue_type_id, + workspaceSlug, + updateCustomProperties, +}) => { const [issueTypeCustomProperties, setissueTypeCustomProperties] = useState([]); const [error, setError] = useState(null); const [editableError, setEditableError] = useState(null); - const [hoveredPropertyKey, setHoveredPropertyKey] = useState(null); + const [editingPropertyKey, setEditingPropertyKey] = useState(null); + const [localCustomProperties, setLocalCustomProperties] = useState([]); + useEffect(() => { const getIssueTypeCustomProperties = async () => { try { @@ -42,8 +48,14 @@ export const CustomProperties: React.FC = ({ customProper getIssueTypeCustomProperties(); }, [workspaceSlug, issue_type_id]); + useEffect(() => { + if (customProperties) { + setLocalCustomProperties(customProperties); + } + }, [customProperties]); + const mergedCustomProperties = issueTypeCustomProperties.map((customProp) => { - const customProperty = customProperties?.find( + const customProperty = localCustomProperties?.find( (prop) => prop.key === customProp.name ); @@ -61,79 +73,114 @@ export const CustomProperties: React.FC = ({ customProper } if (!Array.isArray(mergedCustomProperties) || mergedCustomProperties.length === 0) { - return null; + return null; } - const EditableProperty: React.FC<{ property: CustomProperty }> = React.memo(({ property }) => { - const [value, setValue] = useState(property.value); - const handleBlur = async () => { - try { - if (property.is_required && value.trim() === "") { - setEditableError("This field is required and cannot be left empty or consist of spaces."); - return; - } - if (value !== property.value) { - const updatedProperty = { ...property, value }; - updateCustomProperties([updatedProperty]); - } - } catch (error) { - setEditableError("Failed to update custom property."); + const handlePropertyUpdate = async (updatedProperty: CustomProperty) => { + try { + setLocalCustomProperties(prev => { + const updatedProperties = [...(prev || [])]; + const existingIndex = updatedProperties.findIndex(p => p.key === updatedProperty.key); + + if (existingIndex >= 0) { + updatedProperties[existingIndex] = {...updatedProperties[existingIndex], ...updatedProperty}; + } else { + updatedProperties.push(updatedProperty); } - }; - - const handleChange = (e: React.ChangeEvent) => { - setValue(e.target.value); - }; - - return ( -
- - {editableError &&
{editableError}
} -
- ) - }); + + return updatedProperties; + }); + + await updateCustomProperties([updatedProperty]); + + setEditableError(null); + } catch (error) { + setEditableError("Failed to update custom property."); + } + }; + + const EditableProperty: React.FC<{ property: CustomProperty }> = React.memo(({ property }) => { + const [value, setValue] = useState(property.value); + + const handleBlur = async () => { + if (property.is_required && value.trim() === "") { + setEditableError("This field is required and cannot be left empty or consist of spaces."); + return; + } + + if (value !== property.value) { + const updatedProperty = { ...property, value }; + await handlePropertyUpdate(updatedProperty); + } + + setEditingPropertyKey(null); + }; + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + // Clear error when user starts typing + if (editableError && e.target.value.trim() !== "") { + setEditableError(null); + } + }; return ( -
-
- {editableError &&
{editableError}
} - {mergedCustomProperties.map((element) => ( -
setHoveredPropertyKey(element.key)} - onMouseLeave={() => setHoveredPropertyKey(null)} - > -
- - {element.is_required && * } + + ); + }); + + return ( +
+
+ {editableError && ( +
{editableError}
+ )} + + {mergedCustomProperties.map((element) => ( +
+
+ + {element.key} + {element.is_required && *} + +
+ +
+ {editingPropertyKey === element.key ? ( +
+ +
+ ) : ( +
-
- {hoveredPropertyKey === element.key && ( - -
- -
-
- )} - {hoveredPropertyKey === element.key && ( - - )} -
+ + + + + + )}
- ))} -
- ); - }; +
+ ))} +
+ ); +}; \ No newline at end of file From 5c7061a3c1cd4395141cba91fd5d2b2df7091f7e Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Sat, 22 Mar 2025 11:42:26 +0000 Subject: [PATCH 149/164] Changes to support multipart data in nginx config files --- nginx/nginx.conf.dev | 3 +++ nginx/nginx.conf.template | 3 +++ 2 files changed, 6 insertions(+) diff --git a/nginx/nginx.conf.dev b/nginx/nginx.conf.dev index 7b94982104e..8a5a8e62027 100644 --- a/nginx/nginx.conf.dev +++ b/nginx/nginx.conf.dev @@ -3,6 +3,9 @@ events { http { sendfile on; + chunked_transfer_encoding on; + client_body_buffer_size 10M; + client_max_body_size 50M; server { listen 80; diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 819c00f21d2..7f2c0784560 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -3,6 +3,9 @@ events { http { sendfile on; + chunked_transfer_encoding on; + client_body_buffer_size 10M; + client_max_body_size 50M; server { listen 80; From 1c00fe09906b56e3c1afece14d4885f1f26333b4 Mon Sep 17 00:00:00 2001 From: vignesh-kandula-shipsy Date: Mon, 24 Mar 2025 11:44:31 +0530 Subject: [PATCH 150/164] Filter the issues with usernames from headers( X-Assume-Role) --- apiserver/plane/api/views/search.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index b514937f3e2..49d5c1428a4 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -84,7 +84,13 @@ def filter_issues(self, query, slug, project_id, workspace_search, created_by_us workspace__slug=slug ) if created_by_username: - issues = issues.filter(created_by__username=created_by_username) + created_by_usernames = [ + username + for username in created_by_username.split(",") + if username.strip() and username != "null" + ] + if created_by_usernames: + issues = issues.filter(created_by__username__in=created_by_usernames) if workspace_search == "false" and project_id: issues = issues.filter(project_id=project_id) @@ -232,7 +238,7 @@ def get(self, request, slug): workspace_search = request.query_params.get( "workspace_search", "false" ) - created_by_username = request.query_params.get("created_by_username", False) + created_by_username = request.headers.get("X-Assume-Role", False) project_id = request.query_params.get("project_id", False) if not query: return Response( From b67a44e11b2dedc06d5dcfc86d39a6d69f7f1e90 Mon Sep 17 00:00:00 2001 From: vignesh-kandula-shipsy Date: Tue, 25 Mar 2025 12:53:45 +0530 Subject: [PATCH 151/164] Fix for filteting the issues by type_id --- apiserver/plane/utils/issue_filters.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index d981c343745..60b6be9e189 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -607,6 +607,13 @@ def filter_character_fields(params, issue_filter, method, prefix=""): return issue_filter +def filter_issue_type_id(params, issue_filter, method, prefix=""): + """ + Filter issues by type_id + """ + type_id = params.get(f"{prefix}type_id", None) + if type_id is not None: + issue_filter[f"{prefix}type_id"] = type_id def issue_filters(query_params, method, prefix=""): issue_filter = {} @@ -630,6 +637,7 @@ def issue_filters(query_params, method, prefix=""): "target_date": filter_target_date, "completed_at": filter_completed_at, "type": filter_issue_state_type, + "type_id": filter_issue_type_id, "project": filter_project, "cycle": filter_cycle, "module": filter_module, From dbb28f0a495c9baa28a7bf4f1c4fb8d08c77e43d Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 25 Mar 2025 07:51:07 +0000 Subject: [PATCH 152/164] added postmessage multiple user fix --- web/app/provider.tsx | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/web/app/provider.tsx b/web/app/provider.tsx index 951ff131174..4f4dc0d24f5 100644 --- a/web/app/provider.tsx +++ b/web/app/provider.tsx @@ -5,7 +5,7 @@ import dynamic from "next/dynamic"; import { usePathname } from "next/navigation";//ui import { useTheme, ThemeProvider } from "next-themes"; import { SWRConfig } from "swr"; -import { Toast } from "@plane/ui"; +import { Toast, setToast, TOAST_TYPE } from "@plane/ui"; // constants import { SWR_CONFIG } from "@/constants/swr-config"; //helpers @@ -18,6 +18,7 @@ import "@/lib/polyfills"; import { StoreProvider } from "@/lib/store-context"; // wrappers import { InstanceWrapper } from "@/lib/wrappers"; +import { useUser } from "@/hooks/store"; // dynamic imports const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false }); const PostHogProvider = dynamic(() => import("@/lib/posthog-provider"), { ssr: false }); @@ -36,25 +37,38 @@ export const AppProvider: FC = (props) => { const { children } = props; // themes - // const router = useRouter(); const pathname = usePathname(); + const { signOut } = useUser(); + + const handleSignOut = async () => { + await signOut().catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Failed to sign out. Please try again.", + }) + ); + }; useEffect(() => { - // const onIncomingMessage = (event) => { - // console.log('onIncomingMessage', event); - // if (event.data.type === 'ROUTE_CHANGE') { - // console.log(location.pathname, event.data.path); - // router.replace(event.data.path); - // } - // }; console.log(pathname, 'pathname'); window.parent.postMessage({ type: "ROUTE_CHANGE", path: pathname }, "*"); - // window.addEventListener('message', onIncomingMessage); - // return () => { - // window.removeEventListener("message", onIncomingMessage); - // }; }, [pathname]); + useEffect(() => { + const onIncomingMessage = async (event) => { + console.log('onIncomingMessage', event); + if (event.data.type === "PLANE_SIGN_OUT") { + await handleSignOut(); + window.parent.postMessage({ type: "PLANE_SIGN_OUT_SUCCESS", path: pathname }, "*"); + } + }; + window.addEventListener('message', onIncomingMessage); + return () => { + window.removeEventListener("message", onIncomingMessage); + }; + }, []); + return ( <> From 1a6b2ab639cafde80f4edcf14094d2bf9634db7e Mon Sep 17 00:00:00 2001 From: naman-agrawal-shipsy Date: Tue, 25 Mar 2025 15:48:57 +0530 Subject: [PATCH 153/164] added try catch --- web/app/provider.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/app/provider.tsx b/web/app/provider.tsx index 4f4dc0d24f5..368e4c9b5d0 100644 --- a/web/app/provider.tsx +++ b/web/app/provider.tsx @@ -59,8 +59,10 @@ export const AppProvider: FC = (props) => { const onIncomingMessage = async (event) => { console.log('onIncomingMessage', event); if (event.data.type === "PLANE_SIGN_OUT") { - await handleSignOut(); - window.parent.postMessage({ type: "PLANE_SIGN_OUT_SUCCESS", path: pathname }, "*"); + try { + await handleSignOut(); + window.parent.postMessage({ type: "PLANE_SIGN_OUT_SUCCESS", path: pathname }, "*"); + } catch (err) {} } }; window.addEventListener('message', onIncomingMessage); From 884ab3d36c7d91998c548e0c445088b473f1cf8f Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Wed, 26 Mar 2025 13:04:40 +0000 Subject: [PATCH 154/164] Added four new fileds to issue model and added filteration --- apiserver/plane/app/serializers/issue.py | 4 ++++ apiserver/plane/app/views/issue/base.py | 4 ++++ apiserver/plane/db/models/issue.py | 4 ++++ apiserver/plane/utils/grouper.py | 6 +++++- apiserver/plane/utils/issue_filters.py | 6 +++++- 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 9a4343ff74c..10c8c7513c3 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -714,6 +714,10 @@ class Meta: "trip_reference_number", "vendor_code", "worker_code", + "hub_name", + "customer_name", + "vendor_name", + "worker_name", "priority", "start_date", "target_date", diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index f846ed84204..2d95e321e6a 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -472,6 +472,10 @@ def create(self, request, slug, project_id): "trip_reference_number", "vendor_code", "worker_code", + "hub_name", + "customer_name", + "vendor_name", + "worker_name", "priority", "start_date", "target_date", diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 8f485cfa7cf..22ac7cf6add 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -145,6 +145,10 @@ class Issue(ProjectBaseModel): worker_code = models.CharField(max_length=255, blank=True, null=True) reference_number = models.CharField(max_length=255, blank=True, null=True) trip_reference_number = models.CharField(max_length=255, blank=True, null=True) + hub_name = models.CharField(max_length=255, blank=True, null=True) + customer_name = models.CharField(max_length=255, blank=True, null=True) + vendor_name = models.CharField(max_length=255, blank=True, null=True) + worker_name = models.CharField(max_length=255, blank=True, null=True) name = models.CharField(max_length=255, verbose_name="Issue Name") description = models.JSONField(blank=True, default=dict) description_html = models.TextField(blank=True, default="

") diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index f064036eb8e..db744500624 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -99,7 +99,11 @@ def issue_on_results(issues, group_by, sub_group_by): "customer_code", "worker_code", "reference_number", - "trip_reference_number" + "trip_reference_number", + "hub_name", + "customer_name", + "vendor_name", + "worker_name" ] if group_by in FIELD_MAPPER: diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 60b6be9e189..1f37e8d1575 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -597,7 +597,11 @@ def filter_character_fields(params, issue_filter, method, prefix=""): "vendor_code", "trip_reference_number", "reference_number", - "customer_code" + "customer_code", + "hub_name", + "customer_name", + "vendor_name", + "worker_name" ] for field in character_fields: From f15b733f92ff35c9285a67564292b550855d7bed Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 27 Mar 2025 08:03:17 +0530 Subject: [PATCH 155/164] Fixed custom-properties alignment --- .../components/issues/custom-properties.tsx | 76 ++++++++++--------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 747c7bced40..31ee8845488 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -14,6 +14,7 @@ type CustomPropertiesProps = { issue_type_id: string; workspaceSlug: string; updateCustomProperties: (updatedProperties: CustomProperty[]) => void; + layout?: "quarter" | "two-fifths"; // Accept layout prop }; export const CustomProperties: React.FC = ({ @@ -21,6 +22,7 @@ export const CustomProperties: React.FC = ({ issue_type_id, workspaceSlug, updateCustomProperties, + layout = "quarter", // Default to "quarter" if no layout prop is passed }) => { const [issueTypeCustomProperties, setissueTypeCustomProperties] = useState([]); const [error, setError] = useState(null); @@ -137,6 +139,10 @@ export const CustomProperties: React.FC = ({ ); }); + // Determine the layout widths based on the layout prop + const labelWidth = layout === "two-fifths" ? "w-2/5" : "w-1/4"; + const valueWidth = layout === "two-fifths" ? "w-3/5" : "w-3/4"; + return (

@@ -144,43 +150,43 @@ export const CustomProperties: React.FC = ({
{editableError}
)} - {mergedCustomProperties.map((element) => ( -
-
- - {element.key} - {element.is_required && *} - -
- -
- {editingPropertyKey === element.key ? ( -
+
+ {mergedCustomProperties.map((element) => ( +
+ {/* Label area */} +
+ + {element.key} + {element.is_required && *} + +
+ {/* Value area */} +
+ {editingPropertyKey === element.key ? ( -
- ) : ( - - )} + ) : ( + + )} +
-
- ))} + ))} +
); }; \ No newline at end of file From 3f5355cb2043a5aac4e004634287f37696539c9f Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 27 Mar 2025 08:03:36 +0530 Subject: [PATCH 156/164] Custom-property widgth as props --- web/core/components/issues/issue-detail/sidebar.tsx | 1 + web/core/components/issues/peek-overview/properties.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 9b68b6ed3dc..e3a0ae77ed6 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -361,6 +361,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { issue_type_id={issue_type_id} workspaceSlug={workspaceSlug} updateCustomProperties={handleCustomPropertiesUpdate} + layout="two-fifths" />
diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index e057c594f8f..b938f268784 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -361,6 +361,7 @@ export const PeekOverviewProperties: FC = observer((pro issue_type_id={issue_type_id} workspaceSlug={workspaceSlug} updateCustomProperties={handleCustomPropertiesUpdate} + layout="quarter" />
From 0438fdd7b2a9dddeffc3bc53446d59361dbcba26 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 27 Mar 2025 08:47:34 +0530 Subject: [PATCH 157/164] Fixed error message positioning --- .../components/issues/custom-properties.tsx | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/web/core/components/issues/custom-properties.tsx b/web/core/components/issues/custom-properties.tsx index 31ee8845488..3be049ab0ba 100644 --- a/web/core/components/issues/custom-properties.tsx +++ b/web/core/components/issues/custom-properties.tsx @@ -14,7 +14,7 @@ type CustomPropertiesProps = { issue_type_id: string; workspaceSlug: string; updateCustomProperties: (updatedProperties: CustomProperty[]) => void; - layout?: "quarter" | "two-fifths"; // Accept layout prop + layout?: "quarter" | "two-fifths"; }; export const CustomProperties: React.FC = ({ @@ -22,11 +22,10 @@ export const CustomProperties: React.FC = ({ issue_type_id, workspaceSlug, updateCustomProperties, - layout = "quarter", // Default to "quarter" if no layout prop is passed + layout = "quarter", }) => { const [issueTypeCustomProperties, setissueTypeCustomProperties] = useState([]); const [error, setError] = useState(null); - const [editableError, setEditableError] = useState(null); const [editingPropertyKey, setEditingPropertyKey] = useState(null); const [localCustomProperties, setLocalCustomProperties] = useState([]); @@ -94,73 +93,75 @@ export const CustomProperties: React.FC = ({ }); await updateCustomProperties([updatedProperty]); - - setEditableError(null); } catch (error) { - setEditableError("Failed to update custom property."); + console.error("Failed to update custom property:", error); } }; const EditableProperty: React.FC<{ property: CustomProperty }> = React.memo(({ property }) => { const [value, setValue] = useState(property.value); + const [localError, setLocalError] = useState(null); const handleBlur = async () => { if (property.is_required && value.trim() === "") { - setEditableError("This field is required and cannot be left empty or consist of spaces."); + setLocalError("This field is required and cannot be left empty or consist of spaces."); + setValue(property.value); return; } - + if (value !== property.value) { - const updatedProperty = { ...property, value }; - await handlePropertyUpdate(updatedProperty); + try { + const updatedProperty = { ...property, value }; + await handlePropertyUpdate(updatedProperty); + setLocalError(null); + } catch (error) { + setLocalError("Failed to update custom property."); + } } - + setEditingPropertyKey(null); + setLocalError(null); }; const handleChange = (e: React.ChangeEvent) => { setValue(e.target.value); - // Clear error when user starts typing - if (editableError && e.target.value.trim() !== "") { - setEditableError(null); - } + setLocalError(null); }; return ( - +
+ + {localError && ( +
{localError}
+ )} +
); }); - // Determine the layout widths based on the layout prop const labelWidth = layout === "two-fifths" ? "w-2/5" : "w-1/4"; const valueWidth = layout === "two-fifths" ? "w-3/5" : "w-3/4"; return (

- {editableError && ( -
{editableError}
- )}
{mergedCustomProperties.map((element) => (
- {/* Label area */}
{element.key} {element.is_required && *}
- {/* Value area */}
{editingPropertyKey === element.key ? ( @@ -171,10 +172,9 @@ export const CustomProperties: React.FC = ({ onClick={() => setEditingPropertyKey(element.key)} > {element.value ? ( - - {element.value} + + {element.value} + ) : ( Add {element.key} )} From d8df23f58d8c7ad73ba93ede3a94f90f7b715f03 Mon Sep 17 00:00:00 2001 From: vignesh-kandula-shipsy Date: Thu, 27 Mar 2025 12:58:14 +0530 Subject: [PATCH 158/164] added new fileds to display properties --- apiserver/plane/db/models/issue.py | 4 +++ web/core/constants/issue.ts | 5 ++++ web/core/constants/spreadsheet.ts | 40 ++++++++++++++++++++++++++++++ web/helpers/issue.helper.ts | 4 +++ 4 files changed, 53 insertions(+) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 22ac7cf6add..a27072bcc02 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -82,6 +82,10 @@ def get_default_display_properties(): "vendor_code": True, "trip_reference_number": True, "reference_number": True, + "hub_name":True, + "customer_name":True, + "vendor_name":True, + "worker_name":True, } diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 8fa2abb662b..b7fb385245b 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -117,6 +117,11 @@ const whileListedCustomProperties = [ { key: "vendor_code", title: "Vendor Code" }, { key: "trip_reference_number", title: "Trip Reference Number" }, { key: "reference_number", title: "Reference Number" }, + { key: "hub_name", title: "Hub Name" }, + { key: "customer_name", title: "Customer Name" }, + { key: "vendor_name", title: "Vendor Name" }, + { key: "worker_name", title: "Worker Name" }, + ]; export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [ diff --git a/web/core/constants/spreadsheet.ts b/web/core/constants/spreadsheet.ts index 14e68aba4be..ec07a0f9dff 100644 --- a/web/core/constants/spreadsheet.ts +++ b/web/core/constants/spreadsheet.ts @@ -232,6 +232,42 @@ export const SPREADSHEET_PROPERTY_DETAILS: { icon: Tags, Column: SpreadsheetStandardPropertyColumn, }, + hub_name: { + title: "Hub Name", + ascendingOrderKey: "hub_name", + ascendingOrderTitle: "A", + descendingOrderKey: "-hub_name", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + customer_name: { + title: "Customer Name", + ascendingOrderKey: "customer_name", + ascendingOrderTitle: "A", + descendingOrderKey: "-customer_name", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + vendor_name: { + title: "Vendor Name", + ascendingOrderKey: "vendor_name", + ascendingOrderTitle: "A", + descendingOrderKey: "-vendor_name", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + worker_name: { + title: "Worker Name", + ascendingOrderKey: "worker_name", + ascendingOrderTitle: "A", + descendingOrderKey: "-worker_name", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, }; export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [ @@ -255,6 +291,10 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [ "vendor_code", "trip_reference_number", "reference_number", + "hub_name", + "customer_name", + "vendor_name", + "worker_name", ]; export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues"; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index ca9d6afd681..15536454bc5 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -313,4 +313,8 @@ export const getComputedDisplayProperties = ( vendor_code: displayProperties?.vendor_code ?? true, trip_reference_number: displayProperties?.trip_reference_number ?? true, reference_number: displayProperties?.reference_number ?? true, + hub_name: displayProperties?.hub_name ?? true, + customer_name: displayProperties?.customer_name ?? true, + vendor_name: displayProperties?.vendor_name ?? true, + worker_name: displayProperties?.worker_name ?? true, }); From c60c1fe2f8b8fb3628a0b49578fd1a4826d9851a Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 27 Mar 2025 15:11:50 +0530 Subject: [PATCH 159/164] Activity for updating/creating custom properties values --- .../plane/bgtasks/issue_activities_task.py | 4 +- .../activity/actions/custom-properties.tsx | 47 +++++++++++++++++++ .../issue-activity/activity/actions/index.ts | 1 + .../issue-activity/activity/activity-list.tsx | 8 ++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index 6a7cd5dfc2e..f374fd14487 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -1634,7 +1634,7 @@ def update_custom_property_activity( verb="updated", old_value=current_instance.get("value"), new_value=requested_data.get("value"), - field=custom_property_key, + field=f"Custom Property {custom_property_key}", project_id=project_id, workspace_id=workspace_id, comment=f"updated the custom property ({custom_property_key}) to", @@ -1666,7 +1666,7 @@ def create_custom_property_activity( verb="created", old_value="", new_value=requested_data.get("value"), - field=custom_property_key, + field=f"Custom Property {custom_property_key}", project_id=project_id, workspace_id=workspace_id, comment=f"added a custom property ({custom_property_key})", diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx new file mode 100644 index 00000000000..b2340f28fec --- /dev/null +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +// hooks +import { useIssueDetail } from "@/hooks/store"; +// components +import { IssueActivityBlockComponent, IssueLink } from "./"; +// icons +import { DoubleCircleIcon } from "@plane/ui"; + +type TIssueCustomPropertyActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined }; + +export const IssueCustomPropertyActivity: FC = observer((props) => { + const { activityId, showIssue = true, ends } = props; + // hooks + const { + activity: { getActivityById }, + } = useIssueDetail(); + + const activity = getActivityById(activityId); + + if (!activity) return <>; + + const isNewProperty = !activity.old_value; + const customPropertyKey = activity?.field.replace("Custom Property ", ""); + const newValue = activity.new_value; + const oldValue = activity.old_value; + + return ( + } + activityId={activityId} + ends={ends} + > + <> + {isNewProperty ? ( + // When a custom property is added + <>Added a Custom Property {customPropertyKey} with value {newValue}. + ) : ( + // When a custom property value is updated + <>Updated the value of {customPropertyKey} from {oldValue} to {newValue}. + )} + + + ); +}); diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/index.ts b/web/core/components/issues/issue-detail/issue-activity/activity/actions/index.ts index ea69163085a..f76b1c66549 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/index.ts +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/index.ts @@ -17,6 +17,7 @@ export * from "./attachment"; export * from "./archived-at"; export * from "./inbox"; export * from "./label-activity-chip"; +export * from "./custom-properties"; // helpers export * from "./helpers/activity-block"; diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx index 18cd3481fe9..2a6342b0515 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx @@ -24,6 +24,7 @@ import { IssueAttachmentActivity, IssueArchivedAtActivity, IssueInboxActivity, + IssueCustomPropertyActivity, } from "./actions"; type TIssueActivityItem = { @@ -42,6 +43,11 @@ export const IssueActivityItem: FC = observer((props) => { const componentDefaultProps = { activityId, ends }; const activityField = getActivityById(activityId)?.field; + + if (activityField && activityField.startsWith("Custom Property")) { + return ; + } + switch (activityField) { case null: // default issue creation return ; @@ -81,6 +87,8 @@ export const IssueActivityItem: FC = observer((props) => { return ; case "type": return ; + // case activityField && activityField.startsWith("Custom Property"): + // return ; default: return <>; } From 28a57d4cda753451c088d5a13a4a88a7b8fd095f Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 27 Mar 2025 17:22:12 +0530 Subject: [PATCH 160/164] Activity block component for update in custom properties values --- .../issue-activity/activity/actions/custom-properties.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx index b2340f28fec..96c8e352e8e 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/custom-properties.tsx @@ -35,10 +35,8 @@ export const IssueCustomPropertyActivity: FC = obs > <> {isNewProperty ? ( - // When a custom property is added <>Added a Custom Property {customPropertyKey} with value {newValue}. ) : ( - // When a custom property value is updated <>Updated the value of {customPropertyKey} from {oldValue} to {newValue}. )} From 21e349917b445c22cd82dee8d46df644f5b131cf Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Thu, 27 Mar 2025 17:22:29 +0530 Subject: [PATCH 161/164] Case for rendering custom property value update activities --- .../issue-detail/issue-activity/activity/activity-list.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx index 2a6342b0515..6fc4e1e27c4 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx @@ -87,8 +87,6 @@ export const IssueActivityItem: FC = observer((props) => { return ; case "type": return ; - // case activityField && activityField.startsWith("Custom Property"): - // return ; default: return <>; } From 3cf7a5d12f882004105b98e4bec425d6920a8d9d Mon Sep 17 00:00:00 2001 From: vignesh-kandula-shipsy Date: Thu, 27 Mar 2025 18:22:00 +0530 Subject: [PATCH 162/164] Sequence chnage --- apiserver/plane/app/serializers/issue.py | 10 +-- apiserver/plane/db/models/issue.py | 12 ++-- apiserver/plane/utils/grouper.py | 12 ++-- apiserver/plane/utils/issue_filters.py | 14 ++--- web/core/constants/issue.ts | 9 ++- web/core/constants/spreadsheet.ts | 80 ++++++++++++------------ web/helpers/issue.helper.ts | 8 +-- 7 files changed, 72 insertions(+), 73 deletions(-) diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 10c8c7513c3..e8435c45303 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -708,15 +708,15 @@ class Meta: "sort_order", "completed_at", "estimate_point", - "hub_code", - "customer_code", - "reference_number", "trip_reference_number", - "vendor_code", - "worker_code", + "reference_number", + "hub_code", "hub_name", + "customer_code", "customer_name", "vendor_name", + "vendor_code", + "worker_code", "worker_name", "priority", "start_date", diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index a27072bcc02..a7f84542971 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -76,15 +76,15 @@ def get_default_display_properties(): "state": True, "sub_issue_count": True, "updated_on": True, - "hub_code": True, - "customer_code": True, - "worker_code": True, - "vendor_code": True, "trip_reference_number": True, "reference_number": True, - "hub_name":True, - "customer_name":True, + "hub_code": True, + "hub_name": True, + "customer_code": True, + "customer_name": True, "vendor_name":True, + "vendor_code":True, + "worker_code":True, "worker_name":True, } diff --git a/apiserver/plane/utils/grouper.py b/apiserver/plane/utils/grouper.py index db744500624..945747f7237 100644 --- a/apiserver/plane/utils/grouper.py +++ b/apiserver/plane/utils/grouper.py @@ -94,16 +94,16 @@ def issue_on_results(issues, group_by, sub_group_by): "is_draft", "archived_at", "state__group", - "hub_code", - "vendor_code", - "customer_code", - "worker_code", - "reference_number", "trip_reference_number", + "reference_number", + "hub_code", "hub_name", + "customer_code", "customer_name", "vendor_name", - "worker_name" + "vendor_code", + "worker_code", + "worker_name", ] if group_by in FIELD_MAPPER: diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 1f37e8d1575..e8b94c48103 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -592,16 +592,16 @@ def filter_custom_properties(params, issue_filter, method, prefix=""): def filter_character_fields(params, issue_filter, method, prefix=""): character_fields = [ - "hub_code", - "worker_code", - "vendor_code", - "trip_reference_number", - "reference_number", - "customer_code", + "trip_reference_number", + "reference_number", + "hub_code", "hub_name", + "customer_code", "customer_name", "vendor_name", - "worker_name" + "vendor_code", + "worker_code", + "worker_name", ] for field in character_fields: diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index b7fb385245b..e0c8eb46f12 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -111,17 +111,16 @@ export const ISSUE_FILTER_OPTIONS: { ]; const whileListedCustomProperties = [ - { key: "hub_code", title: "Hub Code" }, - { key: "customer_code", title: "Customer Code" }, - { key: "worker_code", title: "Worker Code" }, - { key: "vendor_code", title: "Vendor Code" }, { key: "trip_reference_number", title: "Trip Reference Number" }, { key: "reference_number", title: "Reference Number" }, + { key: "hub_code", title: "Hub Code" }, { key: "hub_name", title: "Hub Name" }, + { key: "customer_code", title: "Customer Code" }, { key: "customer_name", title: "Customer Name" }, { key: "vendor_name", title: "Vendor Name" }, + { key: "vendor_code", title: "Vendor Code" }, + { key: "worker_code", title: "Worker Code" }, { key: "worker_name", title: "Worker Name" }, - ]; export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [ diff --git a/web/core/constants/spreadsheet.ts b/web/core/constants/spreadsheet.ts index ec07a0f9dff..4e4661b9465 100644 --- a/web/core/constants/spreadsheet.ts +++ b/web/core/constants/spreadsheet.ts @@ -178,42 +178,6 @@ export const SPREADSHEET_PROPERTY_DETAILS: { icon: LayersIcon, Column: SpreadsheetSubIssueColumn, }, - hub_code: { - title: "Hub Code", - ascendingOrderKey: "hub_code", - ascendingOrderTitle: "A", - descendingOrderKey: "-hub_code", - descendingOrderTitle: "Z", - icon: Tags, - Column: SpreadsheetStandardPropertyColumn, - }, - customer_code: { - title: "Customer Code", - ascendingOrderKey: "customer_code", - ascendingOrderTitle: "A", - descendingOrderKey: "-customer_code", - descendingOrderTitle: "Z", - icon: Tags, - Column: SpreadsheetStandardPropertyColumn, - }, - worker_code: { - title: "Worker Code", - ascendingOrderKey: "worker_code", - ascendingOrderTitle: "A", - descendingOrderKey: "-worker_code", - descendingOrderTitle: "Z", - icon: Tags, - Column: SpreadsheetStandardPropertyColumn, - }, - vendor_code: { - title: "Vendor Code", - ascendingOrderKey: "vendor_code", - ascendingOrderTitle: "A", - descendingOrderKey: "-vendor_code", - descendingOrderTitle: "Z", - icon: Tags, - Column: SpreadsheetStandardPropertyColumn, - }, trip_reference_number: { title: "Trip Ref Number", ascendingOrderKey: "trip_reference_number", @@ -232,6 +196,15 @@ export const SPREADSHEET_PROPERTY_DETAILS: { icon: Tags, Column: SpreadsheetStandardPropertyColumn, }, + hub_code: { + title: "Hub Code", + ascendingOrderKey: "hub_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-hub_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, hub_name: { title: "Hub Name", ascendingOrderKey: "hub_name", @@ -241,6 +214,15 @@ export const SPREADSHEET_PROPERTY_DETAILS: { icon: Tags, Column: SpreadsheetStandardPropertyColumn, }, + customer_code: { + title: "Customer Code", + ascendingOrderKey: "customer_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-customer_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, customer_name: { title: "Customer Name", ascendingOrderKey: "customer_name", @@ -259,6 +241,24 @@ export const SPREADSHEET_PROPERTY_DETAILS: { icon: Tags, Column: SpreadsheetStandardPropertyColumn, }, + vendor_code: { + title: "Vendor Code", + ascendingOrderKey: "vendor_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-vendor_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, + worker_code: { + title: "Worker Code", + ascendingOrderKey: "worker_code", + ascendingOrderTitle: "A", + descendingOrderKey: "-worker_code", + descendingOrderTitle: "Z", + icon: Tags, + Column: SpreadsheetStandardPropertyColumn, + }, worker_name: { title: "Worker Name", ascendingOrderKey: "worker_name", @@ -285,15 +285,15 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [ "link", "attachment_count", "sub_issue_count", - "hub_code", - "customer_code", - "worker_code", - "vendor_code", "trip_reference_number", "reference_number", + "hub_code", "hub_name", + "customer_code", "customer_name", "vendor_name", + "vendor_code", + "worker_code", "worker_name", ]; diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 15536454bc5..5eee1804304 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -307,14 +307,14 @@ export const getComputedDisplayProperties = ( modules: displayProperties?.modules ?? true, cycle: displayProperties?.cycle ?? true, issue_type: displayProperties?.issue_type ?? true, - hub_code: displayProperties?.hub_code ?? true, - customer_code: displayProperties?.customer_code ?? true, - worker_code: displayProperties?.worker_code ?? true, - vendor_code: displayProperties?.vendor_code ?? true, trip_reference_number: displayProperties?.trip_reference_number ?? true, reference_number: displayProperties?.reference_number ?? true, + hub_code: displayProperties?.hub_code ?? true, hub_name: displayProperties?.hub_name ?? true, + customer_code: displayProperties?.customer_code ?? true, customer_name: displayProperties?.customer_name ?? true, vendor_name: displayProperties?.vendor_name ?? true, + vendor_code: displayProperties?.vendor_code ?? true, + worker_code: displayProperties?.worker_code ?? true, worker_name: displayProperties?.worker_name ?? true, }); From 9b520fdf8d78e3080d362fc5c3263c8b11b697d0 Mon Sep 17 00:00:00 2001 From: rahulsahay-shipsy Date: Fri, 28 Mar 2025 09:21:06 +0530 Subject: [PATCH 163/164] Added 4 additional properties (names) in fixed issue properties --- web/core/constants/issue.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index b7fb385245b..8992d03ae98 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -537,10 +537,14 @@ export const ISSUE_ADDITIONAL_PROPERTIES: { key: keyof TIssue; title: string; }[] = [ + { key: "trip_reference_number", title: "Trip Ref Number" }, + { key: "reference_number", title: "Reference Number" }, { key: "hub_code", title: "Hub Code" }, + { key: "hub_name", title: "Hub Name" }, { key: "customer_code", title: "Customer Code" }, - { key: "worker_code", title: "Worker Code" }, + { key: "customer_name", title: "Customer Name" }, { key: "vendor_code", title: "Vendor Code" }, - { key: "trip_reference_number", title: "Trip Ref Number" }, - { key: "reference_number", title: "Reference Number" } + { key: "vendor_name", title: "Vendor Name" }, + { key: "worker_code", title: "Worker Code" }, + { key: "worker_name", title: "Worker Name" } ]; \ No newline at end of file From 40a01e90b36c2330e505220f17da17f0388a7624 Mon Sep 17 00:00:00 2001 From: abu-siddique-shipsy Date: Tue, 1 Apr 2025 11:21:09 +0000 Subject: [PATCH 164/164] added property type prop --- .../spreadsheet/columns/standard-property-column.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx index 4974f6c38e2..23959e4701a 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx @@ -6,6 +6,7 @@ import { Row, Tooltip } from "@plane/ui"; type Props = { issue: TIssue; + property: string; }; export const SpreadsheetStandardPropertyColumn: React.FC = observer((props) => {